[feat] solution: 일정을 담을 자리 — 업장·기간당 한 행

This commit is contained in:
김성경 2026-09-11 11:09:56 +09:00
parent a61d9a278c
commit 18e86a0d12
3 changed files with 87 additions and 0 deletions

View File

@ -223,6 +223,26 @@ CREATE TABLE IF NOT EXISTS public.place_faqs (
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
-- LLM 이 만든 여행 일정. 기간당 한 행, body 에 코스 5개가 통째로 든다.
-- ★ 키는 (place_id, duration) 이다 — 업소 이름이 프롬프트에 들어가므로 옆집이 나눠 쓸 수 없다.
-- ★ body 는 렌더러 계약(shared/lib/section-data.ts 의 ItineraryItem[]) 그대로다.
CREATE TABLE IF NOT EXISTS public.place_itineraries (
place_itinerary_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
place_id uuid NOT NULL,
duration VARCHAR(20) NOT NULL, -- '1박 2일' · '2박 3일'
body JSONB NOT NULL, -- ItineraryItem[]
generated_by SMALLINT NOT NULL, -- SourceType: 4=llm
model VARCHAR(100) NULL, -- 'perplexity:sonar'
generated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS ix_place_itineraries_place ON public.place_itineraries (place_id);
CREATE UNIQUE INDEX IF NOT EXISTS uq_place_itineraries
ON public.place_itineraries (place_id, duration)
WHERE deleted = FALSE;
-- ============================================================
-- 지역 콘텐츠 — 접두어가 없는 유일한 표. **그게 공유물이라는 표시다.**
-- ★ 키는 place_id 가 아니다. 축제·관광지·맛집의 유일성은 출처가 준 external_id 이고,

View File

@ -0,0 +1,29 @@
-- 0010 · LLM 여행 일정 저장 표
--
-- 일정은 지금까지 저장하지 않고 빌드마다 즉석 계산했다(services/itinerary.py).
-- LLM 생성으로 바꾸면서 호출이 건당 20~50초·유료가 되어 매 빌드 재생성이 불가능하다 —
-- 업장·기간당 한 번 만들어 두고 그대로 쓴다.
-- ★ area_contents 에 넣지 않는다: 그 표의 유일성 근거는 지역·출처 기준이고 이 값은 업장에 붙는다.
--
-- ★ 2026-09-11 기준 dev DB 에는 아직 적용하지 않았다. 그 DB 의 schema_migrations 에는
-- 다른 브랜치의 0009_faq_catalog_columns 가 기록돼 있어, migrate.py 가 이 브랜치의
-- 0009_align_with_init_sql 까지 함께 적용하려 든다 — 번호 정리가 먼저다.
CREATE TABLE IF NOT EXISTS public.place_itineraries (
place_itinerary_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
place_id uuid NOT NULL,
duration VARCHAR(20) NOT NULL,
body JSONB NOT NULL,
generated_by SMALLINT NOT NULL,
model VARCHAR(100) NULL,
generated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS ix_place_itineraries_place ON public.place_itineraries (place_id);
CREATE UNIQUE INDEX IF NOT EXISTS uq_place_itineraries
ON public.place_itineraries (place_id, duration)
WHERE deleted = FALSE;

View File

@ -255,6 +255,44 @@ class place_faqs(MainTableMixin, MAIN_BASE):
sort_order = Column(Integer, nullable=False, server_default=text("0"), default=0)
class place_itineraries(MainTableMixin, MAIN_BASE):
"""LLM 이 만든 여행 일정. **기간당 한 행**이고 `body` 에 코스 5개가 통째로 든다.
area_contents 아닌가
표의 유일성 근거는 지역·출처 기준이다((source, external_id) ·
(region_code, kind) · (region_code, content_type)). 값은 **업장 하나에 붙는다**
업소 이름이 프롬프트에 들어가고, 같은 지역 옆집이 나눠 없다.
넷째 근거를 표에 더하면 0004·0007 에서 겪은 "제약이 겹쳐 조용히 틀리는" 사고를
다시 만든다(area_contents.__table_args__ 주석).
body 렌더러 계약 그대로다(`shared/lib/section-data.ts` ItineraryItem[]).
읽는 쪽이 모양을 다시 바꾸지 않아야 사장님이 손으로 붙여넣은 것과 갈리지 않는다
지역 이야기가 body 봉투째 담는 것과 같은 이유다.
코스마다 행으로 쪼개지 않는다. 다시 생성할 행을 덮어쓰면 되고,
쪼개면 "5개를 받았는데 3개만 갱신된" 상태가 생긴다.
"""
__tablename__ = "place_itineraries"
__table_args__ = (
Index(
"uq_place_itineraries",
"place_id", "duration",
unique=True,
postgresql_where=text("deleted = false"),
),
)
place_itinerary_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
place_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# '1박 2일' · '2박 3일'. 화면 탭이 되는 값이라 표기를 바꾸지 않는다(prompts/itinerary.DURATIONS).
duration = Column(String(20), nullable=False)
body = Column(JSONB, nullable=False) # ItineraryItem[]
generated_by = Column(SmallInteger, nullable=False) # SourceType — LLM
model = Column(String(100), nullable=True) # 'perplexity:sonar'
generated_at = Column(DateTime(timezone=True), nullable=False, server_default=_utc_now_sql())
# ============================================================
# local : 지역 정보(행정구역 코드 단위 캐시) / 가는 길 / 주변
# ============================================================