o2o-site-AEO/solution/backend/services/prompts/itinerary.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

107 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""여행 일정 프롬프트 — "1박 2일·2박 3일을 각각 5개 컨셉 × 2개씩, 총 10개로"."""
# 화면 탭이 되는 값이다(`ItineraryItem.duration`).
DURATIONS: tuple[str, str] = ("1박 2일", "2박 3일")
MAX_TOKENS = 16000
SYSTEM_PROMPT = (
"너는 지역 여행 코스 플래너다. 검색으로 확인한 실제 장소만 쓰고, "
"확인하지 못한 값은 필드를 통째로 뺀다. JSON 하나만 출력한다."
)
_CONCEPTS = """1. 역사·근대건축 — 박물관·옛 건물·유적 위주
2. 자연 풍경 — 바다·산·호수·공원 중 그 지역에 실제로 있는 것 위주
3. 미식 — 시장·맛집·지역 음식 위주
4. 아이와 함께 — 체험·동물·놀이·넓은 공원 위주
5. 야외활동 — 걷기·자전거·물놀이·전망처럼 몸으로 즐기는 것 위주"""
# 하루하루의 시작·종료 시각.
DAY_SCHEDULE: dict[str, tuple[dict[str, object], ...]] = {
"1박 2일": (
{"label": "첫째 날", "start": "15:00", "end": "19:00", "returns": True},
{"label": "둘째 날", "start": "09:00", "end": "14:00", "returns": False},
),
"2박 3일": (
{"label": "첫째 날", "start": "15:00", "end": "19:00", "returns": True},
{"label": "둘째 날", "start": "09:00", "end": "19:00", "returns": True},
{"label": "셋째 날", "start": "09:00", "end": "14:00", "returns": False},
),
}
def _schedule_text(duration: str) -> str:
"""DAY_SCHEDULE 을 프롬프트에 박을 문장으로."""
lines = []
for slot in DAY_SCHEDULE[duration]:
ending = "업소 복귀" if slot["returns"] else "종료(체크아웃, 복귀 없음)"
lines.append(f"· {slot['label']}: {slot['start']} 시작 ~ {slot['end']} {ending}")
return "\n".join(lines)
# verified 를 요구하지 않는다.
_SCHEMA = """{ "kind":"itinerary", "version":1, "title":"추천 일정", "items":[
{ "name":"코스 이름(컨셉이 드러나게)",
"duration":"%(duration)s",
"audience":"누구에게 맞는 일정인가",
"why":"왜 이 일정인가 (두 문장 이내)",
"days":[
{ "label":"첫째 날", "startTime":"15:00", "stops":[
{ "name":"장소", "minutes":90, "moveMinutes":12,
"note":"한 줄 설명", "searchQuery":"지도 검색어",
"latitude":35.9908197, "longitude":126.7121231 } ] } ],
"source":{ "name":"출처 이름", "url":"https://..." } } ] }"""
_TASK = """[업소] %(place)s
[지역] %(region)s
[해야 할 일]
[업소]에 숙박하는 손님을 위한 %(duration)s 여행 일정을 **아래 5개 컨셉마다 2개씩, 총 10개** 만든다.
[컨셉]
%(concepts)s
· 같은 컨셉 안의 두 코스도 서로 다른 일정이어야 한다 — 정거장이 겹치는 것은 괜찮지만
(규칙 7 참고), 같은 장소를 그대로 두 번 우려내지 않는다. 컨셉에 맞는 장소가 둘째 코스를
못 채울 만큼 모자라면 그 컨셉은 1개만 낸다 — 억지로 채우지 않는다.
· 모든 일정의 duration 은 "%(duration)s" 이다.
· 하루하루의 시작·종료 시각은 고정이다. days 는 이 순서·시각대로 나눈다.
%(schedule)s
· 마지막 정거장은 그 날 종료 시각 전에 끝나야 한다(머무는 시간까지 포함해서) — 업소로
돌아오거나 체크아웃하러 이동하는 시간으로 30분 정도는 남겨 둔다.
· 정거장은 하루에 3~5곳. 여섯 곳부터는 아무도 그대로 못 돈다.
· minutes 는 거기서 머무는 시간, moveMinutes 는 앞 칸에서 오는 데 걸리는 시간이다.
· 첫 정거장의 moveMinutes 는 **업소에서 나서는 시간**이다.
· [업소] 자신은 정거장에 넣지 않는다 — 일정은 업소에서 출발하는 것이다.
[스키마]
%(schema)s
[규칙]
1. JSON 하나만 출력한다. 인사말·설명·코드펜스를 붙이지 않는다.
2. 확인되지 않은 값은 필드를 통째로 뺀다. 빈 문자열로 채우거나 지어내지 않는다.
3. 검색으로 실제 존재가 확인된 장소만 쓴다. 폐업·휴업한 곳은 넣지 않는다.
4. 순위를 매기지 않는다. rank 같은 칸은 없다.
5. 링크를 만들지 않는다. searchQuery 에 지도 검색어만 적는다.
6. 모든 정거장에 latitude·longitude 를 적는다.
7. ★ 코스 둘이 **같은 정거장 집합**이면 안 된다. 순서만 바꿔 놓은 것은 같은 코스다.
각 코스에는 다른 코스들에 없는 정거장이 **최소 두 곳** 들어가야 한다.
컨셉에 맞는 장소가 모자라면 그 코스의 정거장을 줄인다 — 다른 코스의 장소를 빌려오지 않는다.
8. ★ 하루 종료 시각을 넘기는 정거장은 아예 적지 않는다 — 넘긴 값은 서버가 뒤에서부터
잘라내므로, 넘길 걸 알면서 적을 이유가 없다.
9. source.url 은 실제로 열리는 공식·기관·언론 페이지여야 한다."""
def build_prompt(place_name: str, region_label: str, duration: str) -> str:
"""업소 하나 × 기간 하나의 프롬프트."""
if duration not in DURATIONS:
raise ValueError(f"모르는 기간: {duration}")
ctx = {"place": place_name, "region": region_label, "duration": duration}
return _TASK % {
**ctx,
"concepts": _CONCEPTS,
"schema": _SCHEMA % ctx,
"schedule": _schedule_text(duration),
}