# `/s/stay` 시연본의 콘텐츠를 만드는 프롬프트 시연본에 들어간 것(히어로 문구 100개 · 여행 일정 21개 · 가요 다방 25곡 · 인물 열전 57명 · 오늘의 엽서)을 **어떤 프롬프트로 뽑는지** 정리한 문서다. 지금 시연본은 이 프롬프트로 뽑은 뒤 사람이 검수해 스크립트에 박아 둔 상태다. --- ## 0. 이 프롬프트들이 어디로 가야 하나 **단일 출처는 `solution/shared/src/lib/section-prompts.ts` 다.** 사장님이 [콘텐츠] 탭에서 복사해 가는 프롬프트와, 서버가 지역 단위로 돌리는 생성 잡 (`services/story_service.py`)이 **같은 문장**을 써야 한다. 두 벌로 두면 "빌더에서 뽑은 것과 서버가 채운 것의 모양이 다르다"가 조용히 생긴다. ``` solution/shared/src/lib/section-prompts.ts ← 여기만 고친다 │ npm run export:prompts ▼ solution/backend/services/prompts/section_prompts.json ← 산출물, 커밋됨. 손으로 고치지 않는다 ``` 아래 프롬프트 중 **2·3 은 새로 만들 것**이고(지금 계약에 없다), **4·5·6 은 기존 것을 고칠 것**이다. 옮길 때 `maxItems` 와 프롬프트 안의 숫자를 **같이** 바꾼다 — 어긋나면 받아 놓고 잘라 버린다. | 프롬프트 | 지금 상태 | 옮길 자리 | |---|---|---| | 2. 히어로 캐치프레이즈 | **없음** — 새로 만든다 | `SECTION_PROMPTS.catchphrase` (신규) | | 3. 여행 일정 · 장소 대장 | 있으나 규칙이 부족 | `canvas/dataSpec.ts` `itinerary` 교체 | | 4. 가요 다방 | 있음(8곡) | `SECTION_PROMPTS.songs` 개선 | | 5. 인물 열전 | 있음(10명) | `SECTION_PROMPTS.people` 개선 | | 6. 오늘의 엽서 | 있음(12개) | `SECTION_PROMPTS.postcard` 개선 | --- ## 1. 공통 규칙 (모든 프롬프트 뒤에 붙는다) ``` [공통 규칙] 1. JSON 하나만 출력한다. 인사말·설명·코드펜스를 붙이지 않는다. 2. 확인되지 않은 값은 필드를 통째로 뺀다. 빈 문자열로 채우거나 "미상" 이라고 쓰지 않는다. 3. source.url 은 실제로 열리는 공식·기관·언론 페이지여야 한다. 검색 결과 주소는 쓰지 않는다. 4. 근거가 확실하면 verified 를 "확인", 애매하면 "확인필요" 로 적는다. 5. 가사·시·소설의 원문을 한 줄도 옮기지 않는다. 제목과 배경만 쓴다. 6. 설명 문장은 항목당 두 문장을 넘기지 않는다. 7. 이미지 주소는 만들지 않는다. 필요하면 imageQuery 에 검색어만 적는다. ``` ★ 2번과 3번이 이 레포의 절대규칙을 프롬프트로 옮긴 것이다. 모델이 이걸 어기면 검증기가 뒤에서 걸러야 하는데, 걸러진 항목은 결국 화면에서 빈자리가 된다. --- ## 2. 히어로 캐치프레이즈 (신규) 상호 아래에서 **7초마다 바뀌는 한 줄**이다. 100개를 미리 쌓아 두고, 계절·월·날씨에 맞는 것을 섞어 순환 노출한다. ### 프롬프트 ``` 너는 숙소의 카피라이터다. 아래 조건에 맞는 JSON 하나만 출력한다. [업소] {상호} ({업종}) [지역] {지명} [업소가 가진 사실] {fact 목록을 한 줄씩. 예: 1920년대 적산가옥 · 독채 2동 · 최대 4인 · 마당과 정원 · 창고형 카페 공간 · 침구 매일 세탁 · 히로쓰 가옥 옆 · 초원사진관 도보 3분} [해야 할 일] [업소] 의 히어로 문구를 100개 만든다. 상호 바로 아래에 한 줄로 뜨고 7초마다 바뀐다. 네 종류로 나눠 만든다. · 일반 40개 — 언제 나와도 되는 문구 · 계절 24개 — 봄·여름·가을·겨울 각 6개 (계절은 3개월 단위: 3~5 봄 / 6~8 여름 / 9~11 가을 / 12~2 겨울) · 월별 24개 — 1월부터 12월까지 각 2개 · 날씨 12개 — 맑음 3 · 구름많음 2 · 흐림 2 · 비 3 · 눈 2 [스키마] { "kind":"catchphrase", "version":1, "items":[ { "text":"한 줄", "kind":"general|season|month|weather", "season":"봄|여름|가을|겨울", // kind 가 season 일 때만 "month":9, // kind 가 month 일 때만 (1~12) "weather":"맑음|구름많음|흐림|비|눈" } // kind 가 weather 일 때만 ] } [이 아이템만의 규칙] · 한 줄은 25자 안쪽이다. 두 문장으로 쓰지 않는다. · **[업소가 가진 사실] 안에서만 쓴다.** 거기 없는 시설·거리·연도를 지어내지 않는다. · 느낌표와 이모지를 쓰지 않는다. "최고" "완벽" "힐링" 같은 광고 단어를 쓰지 않는다. · 상호를 문구에 넣지 않는다 — 바로 위에 이미 크게 떠 있다. · 같은 사실을 두 번 쓰지 않는다. 100개가 다 다른 것을 말해야 한다. · 계절 문구는 그 계절에만 맞는 말이어야 한다. "좋습니다" 처럼 아무 때나 되는 말은 일반으로 보낸다. · 날씨 문구는 그 날씨일 때 손님이 **무엇을 할 수 있는지**를 말한다. 날씨 묘사만 하지 않는다. · 월별 문구는 그 달의 날씨·행사·빛이 근거여야 한다. 숫자만 바꾼 문구를 12번 쓰지 않는다. · source 와 verified 는 없다. 사실이 아니라 문구이고, 근거는 [업소가 가진 사실] 이다. ``` ### 잘 나온 예 (시연본에서) | 종류 | 문구 | 근거가 된 사실 | |---|---|---| | 일반 | 담 너머는 히로쓰 가옥입니다 | 위치 | | 일반 | 침구는 매일 새것처럼 나갑니다 | 매일 세탁·살균 | | 일반 | 네 사람까지 묶는 독채입니다 | 최대 4인 | | 계절(가을) | 기와 위로 가을 볕이 마릅니다 | 기와 지붕 | | 월(9월) | 가을이 담을 넘어오는 구월입니다 | 담·계절 | | 날씨(비) | 카페 공간에서 빗소리를 들으실 수 있습니다 | 창고형 카페 | ### 받은 뒤 검사할 것 - [ ] 100개인가. 종류별 개수가 40/24/24/12 인가 - [ ] 25자를 넘는 것이 있는가 - [ ] [업소가 가진 사실] 에 없는 말이 있는가 ← **이게 제일 많이 틀린다** - [ ] 계절 문구가 그 계절에만 맞는가 - [ ] 같은 말이 두 번 나오는가 --- ## 3. 여행 일정 (신규 — 두 단계로 나눈다) ★ **한 프롬프트로 일정을 통째로 받지 않는다.** 시연본 첫 판이 그랬고, 나온 것이 "둘째 날 해산물축제 → 근대건축관" 으로 끝나는 하루였다. 모델은 **시각 계산을 못 한다** — 이동시간을 지어내고, 점심을 09:40 에 넣고, 21시를 넘겨 화면에서 잘려 나가는 칸을 만든다. 그래서 **3-1 로 장소 대장만 받고, 3-2 는 코드가 계산한다.** ### 3-1. 장소 대장 (LLM 이 만든다) ``` 너는 지역 여행 코디네이터다. 아래 조건에 맞는 JSON 하나만 출력한다. [업소] {상호} ({업종}) [지역] {지명} [좌표] {위도},{경도} [해야 할 일] [업소] 에서 출발해 당일로 다녀올 수 있는 장소를 30곳까지 찾는다. 관광지·박물관·골목·바다·절 같은 볼거리 20곳과, 끼니를 해결할 곳 10곳을 섞는다. [업소] 에서 30km 안쪽만 고른다. [스키마] { "kind":"places", "version":1, "items":[ { "name":"장소 이름", "searchQuery":"지도에서 검색할 말", "category":"볼거리|식당|카페", "stayMinutes":{"min":20,"base":30,"max":45}, "todo":"거기서 무엇을 하는지 한 문장", "indoor":true, "needsCar":false, "bestTime":"오전|오후|저녁|밤|상관없음", "verified":"확인|확인필요", "source":{"name":"출처명","url":"https://..."} } ] } [이 아이템만의 규칙] · **todo 는 장소 설명이 아니라 할 일이다.** ✗ "1899년에 지어진 근대 건축물입니다" ✓ "옛 조선은행 금고와 은행 창구를 봅니다. 2층 전시까지 보면 45분입니다." 손님이 거기서 무엇을 하는지 모르면 그 줄은 쓸모가 없다. · stayMinutes 는 **실제로 걸리는 시간**이다. min 은 빨리 보고 나오는 사람, max 는 천천히 보는 사람. 박물관 60~120분, 골목 15~40분, 식당 40~70분이 기준이다. 무엇이든 90분으로 적지 않는다. · 식당은 **무엇을 먹는지**까지 적는다. "맛집입니다" 로는 무엇을 시킬지 모른다. · indoor 는 비 오는 날 갈 수 있는 곳만 true 다. 처마 밑·실내 전시가 여기 들어간다. · needsCar 는 [업소] 에서 걸어서 못 가는 곳(1.5km 초과)이면 true 다. · 좌표는 적지 않는다. 코드가 카카오 로컬에서 받는다 — 지어낸 좌표는 지도에 엉뚱한 핀을 찍는다. · 영업시간·휴무일이 확인되면 source 에 그 페이지를 단다. ``` ### 3-2. 일정 조립 (코드가 한다 — LLM 아님) `solution/site/scripts/mockup/build_itinerary.py` 가 하는 일이고, 제품에서는 `solution/backend/services/itinerary.py` 가 같은 규칙을 가져야 한다. **LLM 에게 맡기지 않는 것** | | 왜 | |---|---| | 이동시간 | 좌표에서 계산한다. 도보 4km/h, 1.5km 초과는 차 25km/h + 주차 5분 | | 도착·출발 시각 | 시작 시각부터 누적 | | 끼니 시각 맞추기 | 창을 벗어나면 앞 칸 체류를 범위 안에서 늘이고 줄인다 | | 하루 구성 | 아래 뼈대 | **하루 뼈대** — `@` 는 숙소 ``` 첫날 체크인@ · 오후 · 오후 · 저녁 · 밤산책 · 복귀@ 가운데날 아침@ · 오전 · 점심 · 오후 · 오후 · 쉼@ · 저녁 · 복귀@ 쉬는날 숙소에서 쉬는 날@ · 점심 · 쉼@ · 저녁 · 복귀@ 마지막날 아침(퇴실)@ · 오전 · 점심 · 오후 · 마무리@ ``` **지켜야 하는 규칙 14종** (어기면 그 일정을 만들지 않는다) 1. 하루의 첫 칸·마지막 칸은 숙소 — 출발지와 복귀지 2. **같은 자리가 연달아 서지 않는다** (숙소 칸 사이에는 반드시 바깥 정거장이 있다) 3. 가운데 숙소 칸은 '쉼'·'쉬는 날' 뿐 4. 이동시간 = 좌표 계산값 5. 입실 15:00+ · 점심 11:30~13:30 · 늦은 점심 13:00~15:00 · 저녁 17:30~20:00 · 밤산책 18:30~20:00 6. 퇴실 11:00 전 7. 21:00 초과 금지 — **넘는 칸은 렌더러가 조용히 버린다** 8. 끼니 자리에는 먹는 곳만 9. 같은 날 같은 식당 두 번 금지 10. 한 테마 안에서 같은 곳 반복 금지 11. "비가 와도 되는" 테마는 `indoor: true` 만 12. "걸어서만/차 없이" 테마에 `needsCar: true` 금지 13. 1박 2일 = 2일, 2박 3일 = 3일 14. 아이 동반 테마에 술집 금지 **테마 이름·대상·이유는 LLM 이 만들어도 된다** (아래 3-3). ### 3-3. 테마 짓기 (LLM) ``` [해야 할 일] [장소 대장] 을 재료로 1박 2일 10개, 2박 3일 10개의 테마를 만든다. 테마는 **손님의 성격**으로 가른다 — 차가 있는지, 걷기를 좋아하는지, 비가 오는지, 아이가 있는지, 사진을 찍는지, 쉬러 오는지. [스키마] { "kind":"themes", "version":1, "items":[ { "name":"테마 이름", "duration":"1박 2일|2박 3일", "audience":"누구에게", "why":"왜 이 순서인지 한두 문장", "days":[ { "kind":"first|mid|mid_rest|mid_island|last", "places":["장소명","장소명"] } ] } ] } [이 아이템만의 규칙] · places 는 [장소 대장] 에 있는 이름만 쓴다. 없는 곳을 적으면 그 테마는 버려진다. · 뼈대의 자리 수와 places 개수가 같아야 한다. · 끼니 자리에는 category 가 식당·카페인 곳만 넣는다. · **한 테마 안에서 같은 곳을 두 번 넣지 않는다.** · 테마 이름에 "최고" "완벽" 을 쓰지 않는다. 무엇을 하는 일정인지가 이름이다. · 20개 중 하나는 **숙소에서 거의 나가지 않는 날**을 포함한다. ``` --- ## 4. 가요 다방 (기존 개선) 지금 계약은 8곡이다. **50곡으로 올린다** — 다만 한 번에 못 받으므로 나눠 부른다. ``` [해야 할 일] [지역]을 노래한 대중가요를 찾아 아래 JSON 으로 정리한다. 이번에는 {N}번째부터 {N+14}번째까지, 15곡을 낸다. 이미 받은 곡은 아래에 있다 — 같은 곡을 다시 내지 않는다. [이미 받은 곡] {곡명 목록} 지명·항구·강·다리가 제목이나 배경에 나오는 곡을 고른다. 1960~80년대 곡을 먼저 찾고, 다 떨어지면 지자체가 만든 곡·지역 가수의 곡으로 넓힌다. [스키마] { "kind":"songs", "version":1, "title":"가요 다방", "items":[ { "title":"곡명", "artist":"가수", "lyricist":"작사", "composer":"작곡", "year":1966, "label":"음반사", "labelColor":"#d4551f", "story":"곡의 배경 (두 문장 이내, 가사 없이)", "verified":"확인|확인필요", "source":{"name":"출처명","url":"https://..."} } ] } [이 아이템만의 규칙] · lyrics 필드는 스키마에 없다. 어떤 이유로도 만들지 마라. 가사 한 소절도 안 된다. · **곡이 실재하는지 출처로 확인한다.** 지역 노래 목록·언론 기사·지자체 음반 안내가 근거다. 검색으로 못 찾으면 그 곡을 내지 않는다 — 15곡을 억지로 채우지 않는다. · labelColor 는 레코드 라벨 색이다. 곡의 분위기에 맞춰 진한 색 하나를 hex 로 고른다. · 작사·작곡·발표연도를 모르면 그 필드를 뺀다. "미상" 이라고 쓰지 않는다. · 유튜브 영상 ID 를 적지 않는다 — 코드가 검색 주소를 만든다. 틀린 영상이 붙는 것보다 낫다. · 숙소가 직접 만든 곡은 여기 넣지 않는다. 이 섹션은 **도시의 노래**를 모으는 자리다. ``` ★ 시연본은 25곡에서 멈췄다. 출처에서 확인되는 군산 곡이 거기까지였다. **모자라면 모자란 채로 내는 것이 맞다** — 지어낸 곡은 아는 사람이 바로 알아본다. --- ## 5. 인물 열전 (기존 개선) 지금 계약은 10명이다. **50명 이상으로 올린다.** ``` [해야 할 일] [지역] 출신이거나 [지역]과 깊이 얽힌 인물을 50명까지 찾는다. 문학·음악·미술·연기·체육·정치·학문·기업을 고루 섞는다. 한 분야가 절반을 넘지 않게 한다. 생존 인물은 공개된 활동 사실만 쓴다. [스키마] { "kind":"people", "version":1, "title":"인물 열전", "items":[ { "name":"이름", "aka":"호·예명", "years":"1902–1950", "role":"소설가", "oneLine":"한 문장 소개", "imageQuery":"사진 검색어", "verified":"확인|확인필요", "source":{"name":"출처명","url":"https://..."} } ] } [이 아이템만의 규칙] · **source.url 은 그 사람의 문서**여야 한다(위키백과 문서, 기관 소개 페이지). "OO시 출신 인물 목록" 같은 목록 페이지는 근거가 아니다. · oneLine 은 **그 사람이 무엇을 한 사람인지**다. "군산 출신입니다" 는 이름 아래 이미 있다. · "~ 출신으로 알려진" 처럼 근거가 전언뿐이면 verified 를 "확인필요" 로 한다. · 생존 인물의 가족·거주지·건강·재산 같은 사생활은 쓰지 않는다. · 사진 URL 을 넣지 않는다. imageQuery 만 넣는다 — 초상권과 저작권은 사람이 확인한다. · years 를 모르면 뺀다. ``` ★ **사진은 프롬프트로 받지 않는다.** 시연본은 위키백과 API 로 문서 사진을 받고, **라이선스가 확인된 것만**(CC BY / CC BY-SA / KOGL / 공용) 썼다 — 57명 중 17명. CC BY-SA 는 저작자 표시가 라이선스 조건이라 화면에 그대로 찍는다. --- ## 6. 오늘의 엽서 (기존 개선 — 링크가 붙게) ``` [해야 할 일] [지역]에 대해 손님이 자기 SNS 에 그대로 붙여 쓸 만한 한 문장을 12개 쓴다. 사실 하나가 반드시 들어가되, 설명하지 말고 툭 던지는 문장으로 쓴다. [스키마] { "kind":"postcard", "version":1, "title":"오늘의 엽서", "items":[ { "line":"한 문장", "hashtags":["#태그"], "place":"그 문장이 가리키는 장소 이름", "searchQuery":"지도에서 검색할 말", "postmark":"소인에 찍을 짧은 지명", "verified":"확인|확인필요", "source":{"name":"출처명","url":"https://..."} } ] } [이 아이템만의 규칙] · 한 문장은 40자 안쪽이다. 두 문장으로 쓰지 않는다. · 느낌표와 이모지를 쓰지 않는다. 광고 문구처럼 들리면 실패다. · 해시태그는 3개까지. 지역명 하나는 반드시 넣는다. · **place 와 searchQuery 를 반드시 채운다.** 화면이 그 값으로 지도 링크를 건다. postmark 는 도장 문구라 지명이 아닐 수 있다 — 그걸로 지도를 열면 엉뚱한 데가 나온다. ``` ★ 시연본에서 실제로 그랬다. 소인 글자(`군산 內港`)로 링크를 만들었더니 `군산 군산` 같은 검색어가 나왔다. `place` 를 따로 받는 이유다. --- ## 7. 프롬프트를 고칠 때 지킬 것 1. **`maxItems` 와 프롬프트 안의 숫자를 같이 바꾼다.** 어긋나면 받아 놓고 잘라 버린다. 2. **개수를 억지로 채우게 하지 않는다.** "확실한 것이 12개면 12개만 낸다" 를 규칙에 넣는다. 채우라고 하면 채운다 — 지어내서. 3. **계산은 시키지 않는다.** 시각·거리·요금은 코드가 한다. 모델은 자신 있게 틀린 숫자를 준다. 4. **"설명" 이 아니라 "할 일" 을 시킨다.** 손님이 그 자리에서 무엇을 하는지가 콘텐츠다. 5. **출처의 종류를 지정한다.** "출처를 달아라" 로는 목록 페이지가 온다. 6. 고친 뒤 **`npm run export:prompts`** 를 돌린다. 안 돌리면 서버는 옛 프롬프트로 돈다.