템플릿 정보가 빌더·렌더러·백엔드에 따로 적혀 어긋나 있었다 — 백엔드 기본값이 없는 id 를 가리켰고, 음식점 강조색 오타, 섹션 간격이 빌더와 서버에서 달랐다. - shared/src/data/templates.json: 템플릿 4개(simple·magazine·retro·paper)와 업종별 허용·기본 - shared/lib/catalog.ts · backend/common/template_catalog.py: 같은 JSON 을 읽는다, 모르는 id 는 에러 - 저장·미리보기·발행에서 모르는 id 를 거절한다(site_service · site.py 422 · build_service 실패) - site: 레이아웃 등록표(basic·paper) + LayoutProvider, Shell→Frame, HomePage→SectionList - 연결 안 된 레이아웃 5개, 배치 고르기(variant), 서체 선택, 빌더 canvas/DevShowcase 삭제 - 빌더: 템플릿을 바꾸면 이전 템플릿이 켠 섹션을 끄고 안내 문구를 띄운다 - postgres-init/migrations/0023: stay-retro → retro, 병원 허용 밖은 NULL(운영 미적용) - Dockerfile·worker: 백엔드 이미지에 shared/src/data 복사 - docs: TEMPLATES.md 신설(세 폴더 역할·템플릿 추가·렌더링 순서), DATA_MODEL·ARCHITECTURE 등 갱신 shared·site·frontend tsc 통과, site vitest 통과, 백엔드 DB 없는 테스트 41 passed(DB 테스트 미실행) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
127 lines
6.0 KiB
Python
127 lines
6.0 KiB
Python
"""내 사이트 목록 — 로그인한 사장님이 자기 사이트 전부를 보는 화면의 뒷단.
|
|
|
|
이 경로가 절대 하면 안 되는 것:
|
|
- 사이트가 아직 없는 사업장을 빼는 것 — 위저드를 걸어오다 만 가게가 목록에서 사라지면
|
|
사장님은 그걸 다시 찾을 길이 없다(에디터 주소를 아무도 기억하지 않는다).
|
|
- 사장님 스코프를 놓치는 것 — 남의 가게가 내 목록에 섞이면 그건 목록이 아니라 사고다.
|
|
- 단건(GET /v1/place/{id}/site)과 다른 재빌드 판정을 내는 것 — 목록과 에디터가 서로 다른
|
|
답을 하면 사장님은 어느 쪽을 믿을지 알 수 없다.
|
|
"""
|
|
import uuid
|
|
|
|
from sqlalchemy import text
|
|
|
|
from common.enums import ErrorType, SiteStatus
|
|
|
|
|
|
async def _place(client, headers, name):
|
|
r = await client.post("/v1/place", headers=headers, json={"name": name, "category": 1})
|
|
return r.json()["place"]["place_id"]
|
|
|
|
|
|
async def _list(client, headers, **params):
|
|
return (await client.get("/v1/site/list", headers=headers, params=params)).json()
|
|
|
|
|
|
async def test_place_without_site_is_still_listed(auth_headers, client):
|
|
"""검증: 사이트 행이 없는 사업장(위저드만 걸어온 것)도 목록에 나온다.
|
|
기대결과: 줄은 있고 site_id 는 없다 — 화면이 '만드는 중'으로 그릴 근거다."""
|
|
h = await auth_headers("my1")
|
|
await _place(client, h, "아직펜션")
|
|
|
|
body = await _list(client, h)
|
|
assert body["result"]["code"] == ErrorType.SUCCESS.value
|
|
assert body["total"] == 1
|
|
row = body["sites"][0]
|
|
assert row["name"] == "아직펜션"
|
|
assert row.get("site_id") is None
|
|
assert row.get("status") is None
|
|
|
|
|
|
async def test_site_row_is_joined_into_the_line(auth_headers, client):
|
|
"""검증: 사업장과 사이트가 한 줄로 합쳐져 온다(줄마다 사이트를 다시 묻지 않는다).
|
|
기대결과: 템플릿·주소가 목록에 그대로 보인다."""
|
|
h = await auth_headers("my2")
|
|
pid = await _place(client, h, "합쳐진펜션")
|
|
await client.post(f"/v1/place/{pid}/site/template", headers=h, json={"template_id": "paper"})
|
|
await client.post(f"/v1/place/{pid}/site/slug", headers=h, json={"slug": "joined-stay"})
|
|
|
|
row = (await _list(client, h))["sites"][0]
|
|
assert row["site_id"]
|
|
assert row["template_id"] == "paper"
|
|
assert row["domain"] == "joined-stay"
|
|
assert row["status"] == SiteStatus.DRAFT.value
|
|
|
|
|
|
async def test_row_carries_what_the_card_draws(auth_headers, client, db_engine):
|
|
"""검증: 목록 줄이 카드가 그릴 값을 다 들고 온다 — 주소·생성일·썸네일.
|
|
|
|
★ 왜 이걸 본다: 같은 상호로 만든 사업장이 여러 줄일 때(실측: 한 계정에 '버터브루' 4줄)
|
|
이름과 상태 배지만으로는 어느 게 어느 건지 가릴 수 없다. 가르는 값은 주소와 시각이고,
|
|
**한 번이라도 발행한 줄은 그림**이다."""
|
|
h = await auth_headers("my2b")
|
|
pid = await _place(client, h, "카드펜션")
|
|
async with db_engine.begin() as conn:
|
|
await conn.execute(
|
|
text("UPDATE places SET road_address = :addr WHERE place_id = :pid"),
|
|
{"addr": "강원특별자치도 양양군 현북면 하조대3길 12", "pid": uuid.UUID(pid)},
|
|
)
|
|
await conn.execute(
|
|
text("INSERT INTO sites (site_id, place_id, domain, status, thumbnail_url) "
|
|
"VALUES (:s, :p, :d, :st, :t)"),
|
|
{"s": uuid.uuid4(), "p": uuid.UUID(pid), "d": "card-stay",
|
|
"st": SiteStatus.PUBLISHED.value, "t": "https://w4ai.o2o.kr/thumbs/card-stay.jpg?v=2"},
|
|
)
|
|
|
|
row = (await _list(client, h))["sites"][0]
|
|
assert row["road_address"] == "강원특별자치도 양양군 현북면 하조대3길 12"
|
|
assert row["created_at"]
|
|
assert row["thumbnail_url"] == "https://w4ai.o2o.kr/thumbs/card-stay.jpg?v=2"
|
|
|
|
|
|
async def test_row_without_a_site_has_no_thumbnail(auth_headers, client):
|
|
"""검증: 아직 발행 안 한 줄은 그림이 없다(키 자체가 없다).
|
|
기대결과: 화면이 '그림 없음' 자리를 그릴 근거가 된다 — 빈 문자열로 오면 깨진 이미지가 뜬다."""
|
|
h = await auth_headers("my2c")
|
|
await _place(client, h, "그림없는펜션")
|
|
|
|
row = (await _list(client, h))["sites"][0]
|
|
assert "thumbnail_url" not in row
|
|
|
|
|
|
async def test_other_owners_sites_are_not_listed(auth_headers, client):
|
|
"""검증: 사장님 스코프. 남의 사업장은 보이지 않는다.
|
|
기대결과: 각자 자기 것만 1건."""
|
|
mine = await auth_headers("my3")
|
|
theirs = await auth_headers("my3b")
|
|
await _place(client, mine, "내펜션")
|
|
await _place(client, theirs, "남의펜션")
|
|
|
|
assert [r["name"] for r in (await _list(client, mine))["sites"]] == ["내펜션"]
|
|
assert [r["name"] for r in (await _list(client, theirs))["sites"]] == ["남의펜션"]
|
|
|
|
|
|
async def test_needs_rebuild_matches_the_single_site_answer(auth_headers, client, db_engine):
|
|
"""검증: 재빌드 판정이 단건 조회와 같은 답을 낸다.
|
|
기대결과: 노출값이 바뀐 사업장은 목록에서도 needs_rebuild=true."""
|
|
h = await auth_headers("my4")
|
|
pid = await _place(client, h, "고친펜션")
|
|
# 템플릿 저장이 사이트 행을 만든다. 그 뒤 노출값이 바뀐 것으로 표시한다.
|
|
await client.post(f"/v1/place/{pid}/site/template", headers=h, json={"template_id": "paper"})
|
|
async with db_engine.begin() as conn:
|
|
await conn.execute(
|
|
text("UPDATE places SET content_updated_at = now() WHERE place_id = :pid"),
|
|
{"pid": uuid.UUID(pid)},
|
|
)
|
|
|
|
single = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
|
|
row = (await _list(client, h))["sites"][0]
|
|
assert row["needs_rebuild"] is True
|
|
assert row["needs_rebuild"] == single["needs_rebuild"]
|
|
|
|
|
|
async def test_list_requires_login(client):
|
|
"""검증: 내 것을 보는 화면이므로 토큰 없이는 열리지 않는다.
|
|
기대결과: 401."""
|
|
assert (await client.get("/v1/site/list")).status_code == 401
|