시안과 나란히 놓으니 세 가지가 달랐다(2026-09-10 실측).
① 엽서 네 장이 **같은 사진**이었다 ② 그 네 장이 전부 같은 박물관의 관람안내(주소·휴관일·
운영시간)였다 ③ 인물 열전은 사진이 한 장도 없었다.
★ 인물 사진 — 위키미디어 공식 API (services/external/wikimedia.py)
공공데이터는 관광지 사진을 주지 사람 얼굴을 주지 않는다. 인물 사진이 공개돼 있으면서
**재게시 권리를 기계가 읽을 수 있게** 알려주는 곳은 사실상 위키백과·위키공용뿐이다.
- 크롤링이 아니라 MediaWiki API 다. 페이지를 긁어 파싱하지 않는다
- 권리 판정을 여기서 끝낸다: 위키에는 자유 저작물만 있지 않다 — 인물에는 특히 '공정 이용'
(비자유) 파일이 섞이고, 그걸 발행본에 실으면 상업적 이용이라 바로 침해다.
PD·CC0·CC BY·CC BY-SA 만 통과시키고, NC·ND·fair use·판정 불가는 버린다
- 통과한 사진에는 **출처 표시가 따라붙는다**(CC BY 계열의 조건). imageCredit 이 그 값이고
화면에 찍는다 — 표시하지 않을 거면 애초에 쓰지 않는다
- 실측: 10명 중 2명(전봉준·신석정류)만 자유 저작물이 있다. 나머지는 없는 것이 정답이고
그 자리는 렌더러가 이니셜로 세운다. 비슷한 이름의 다른 사람 사진을 붙이는 게 더 나쁘다
★ 사진이 본체인 종류는 사진 없는 항목을 버린다 (_IMAGE_REQUIRED_KINDS = postcard)
엽서는 앞면 사진이 본체다. 없으면 뒷면만 남아 빈 카드로 보인다. 연표는 다르다 —
활자만으로도 레일에 서므로 버리면 오히려 구멍이 난다. 실측: 12건 중 6건이 빠졌다
★ 같은 사진을 두 번 쓰지 않는다
네 항목이 같은 시설을 말하면 검색이 같은 사진을 네 번 준다. 두 번째부터는 없는 것으로 친다
★ 프롬프트(shared 한 벌, export 포함)
- postcard: "같은 대상을 두 번 쓰지 않는다" · "운영시간·휴관일·주소·요금은 엽서에 적지
않는다 — 그건 이용 정보지 엽서 문장이 아니다" · place 가 사진을 찾는 열쇠임을 명시
- shared/section-data: PeopleItem·ChronicleItem·PostcardItem 에 imageCredit 추가
- site: SourceLine 이 사진 출처를 함께 찍는다(글 출처가 없어도 사진 출처만으로 한 줄 선다).
엽서는 사진 바로 아래에 따로 찍는다 — 앞면이 사진이라 거기 붙는 게 맞다
실측(전북 군산시) 발행본: 인물 2장 · 엽서 6장(전부 고유) · 연표 3장(전부 고유),
사진 11장 모두 출처 표시. site vitest 51 passed · story·snapshot 23 passed.
지역 이야기(연표·엽서)가 활자만으로 서 있었다. 계약(`shared/section-data.ts`)과 렌더러는
`imageUrl` 을 이미 받는데 **아무도 채우지 않았다** — 생성 프롬프트가 사진을 묻지 않고,
붙이는 코드도 없었다.
★ 이미 DB 에 있는 사진을 가져다 쓰지 않는다. 그건 "이 항목의 사진" 이 아니라 "마침 우리가
갖고 있던 사진" 이고, 엉뚱한 장소가 그 해의 사진으로 붙는다. **이야기를 만드는 그 순간
그 대상의 이름으로** 찾아온 것만 쓴다.
★ 사진을 모델에게 묻지 않는다. 모델이 주는 이미지 주소는 대개 존재하지 않거나 남의
저작물이다. 공공데이터(TourAPI)가 그 대상의 사진으로 준 것만 쓴다.
- external/tour_api.find_image: 이름으로 사진 한 장. 권리 판정은 이 파일의 기존 규칙 그대로
공공누리 **Type1·Type3 만**(발행본은 상업적 이용이라 Type2·Type4 는 못 싣고, 유형을
모르면 버린다). searchKeyword2 는 전국에서 이름만 맞으면 주므로 **주소에 지역 토막이
없는 결과는 버린다** — '군산항' 을 찾다 다른 지역 동명 시설이 붙으면 그 사진은 이 이야기와
아무 관계가 없다. 실패하면 None 이다(사진 한 장 때문에 생성을 실패시키지 않는다)
- story_service._attach_images: 연표(place→title) · 엽서(place→postmark)만 찾는다.
**인물은 넣지 않았다** — 공공데이터는 관광지 사진을 주지 사람 얼굴을 주지 않는다.
열 명을 찔러야 0건이고, 그 자리는 렌더러가 이니셜로 세우도록 이미 설계돼 있다
(PeopleItem.imageUrl 주석). 가요·퀴즈도 시안에 사진이 없다. 종류당 6건 상한
- shared/section-prompts(chronicle): "찾아갈 수 있는 자리가 있으면 **반드시** 적는다" 를
더했다. 기존 문구가 "없으면 뺀다" 뿐이라 모델이 행정 개편류에 place 를 통째로 비웠고
(실측: 12건 전부 비었다), 그러면 붙일 사진을 찾을 키워드가 없다.
프롬프트는 한 벌이라 `npm run export:prompts` 로 백엔드 JSON 도 함께 갱신했다
실측(전북 군산시): 연표 3장 · 엽서 6장이 발행본에 실렸다(공공누리 Type1/Type3).
site tsc·eslint 통과 · story·tour_api 테스트 59 passed.
- 지역 이야기(가요·인물·연표·엽서·퀴즈) 생성 경로: story_service · grounding/story ·
section_prompts. 지금까지 만들 자리가 없어 시안에만 손으로 넣은 3만 자였다
- 발행본 섹션: ItinerarySection · Carousel 레일 자동재생(use-rail-autoplay) ·
Festival · LocalGuide · Weather · Gallery · Header/Footer
- 목업 payload 를 payloads-mockup/ 으로 분리 — 발행 대상과 섞이지 않게
- DB 새 구조 후속: site_payload · local_content_crud 조인 정리 · 테스트
- 마이그레이션 주석 축약: 9개 파일 합계 주석 비율 48% → 25%.
실측과 밟은 함정만 남기고 논증은 커밋 메시지로 옮겼다
검증: site·frontend 빌드 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
가요·인물·연표·엽서·퀴즈는 생성기가 없어 **사람이 손으로 넣지 않으면 영영 빈칸**이었다.
`/s/stay` 시안이 다섯을 다 갖고 있는 건 그때 손으로 채웠기 때문이고, 새 업장은 옛 항구
템플릿을 골라도 그 자리가 비었다. 실측(2026-09-09, 전북 군산시): 생성 54건 · 62초 · 버린 항목 0.
**생성**
- Perplexity 종류당 1회, 지역당 1세트. 순차로 돈다 — 동시에 다섯을 띄웠더니 둘이 HTTP 429 였다
(같은 키라 한 지역이 자기를 막는다). 순차도 건당 9~15초다. 타임아웃 240s — 가요 다방이
기본 90s 를 넘겼다(후보를 넓게 훑는 프롬프트다).
- 출처 없는 항목은 버린다. 항목 자신의 출처가 없어 검색 출처로 때운 것은 모델이 "확인" 이라
우겨도 "확인필요" 로 내린다. 항목 **모양은 검사하지 않는다** — shared 계약을 파이썬에
한 벌 더 적으면 필드가 는 날 서버가 조용히 떨어뜨린다.
- 프롬프트는 한 벌이다(`shared/section-prompts.ts`). 사장님이 [콘텐츠] 탭에서 복사해 가던
그 문장을 서버도 그대로 쓴다. `npm run export:prompts` 가 백엔드용 JSON 으로 뽑는다(커밋).
- 트리거는 수집 완료 직후다. 전에는 에디터 캔버스가 주변정보를 처음 부를 때 시작해서
사장님이 처음 보는 화면이 **늘 절반만 그려진 상태**였다.
**자리 가르기**
area_* = 공용. 지역 단위, 여러 사이트가 나눠 쓴다 → 렌더러 모양 그대로.
site_sections = 개인화 싸그리. 사이트마다 달라지는 것 전부(거리·숨김·순서·편집).
- `area_contents.body` 가 TourAPI 원문 이름이라 빌드마다 렌더러 이름으로 바꿔 실었다 —
같은 변환을 발행할 때마다 다시 하는 셈이었다. 수집 시점에 바꿔 넣는다.
- 거리·숨김은 사이트마다 다르니 `site_sections('local').data.places` 맵으로. **맵이지
배열이 아니다** — 화면에 순서대로 서는 항목이 아니라 ref → 값 조회표다. 정렬 기준은
읽는 쪽이 갖는다.
- ★ 유일 인덱스 함정 둘. `uq_local_contents_single` 이 kind 를 안 봐서 이야기 다섯 중
**첫 종류만 저장되고 잡은 "성공" 으로 끝났고**, backfill 때는 인덱스를 먼저 떼지 않으면
UPDATE 가 통째로 막힌다(`(gunsan, festival) already exists`). 둘 다 조용히 틀리는 종류다.
- 검수 게이트는 두지 않는다(사장님이 에디터에서 뺀다). 근거는 DECISIONS.md 6절.
검증: 지역 이야기 단위 테스트 12건 통과 · 군산 실행 후 payload.local.story 에
songs 8 · people 10 · chronicle 12 · postcard 12 · quiz 12.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>