Merge branch 'feature/stay-mockup-postcard-reading'
146
solution/site/scripts/mockup/AUTOPLAY.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# 자동재생 — 안 된다. 왜 안 되는지와, 그래서 뭘 했는지
|
||||||
|
|
||||||
|
`/s/stay` 배경음악을 **새로고침 직후 바로** 나게 하려고 시도한 것 전부와 실측값이다.
|
||||||
|
같은 얘기가 다시 나오면 이 파일부터 본다.
|
||||||
|
|
||||||
|
측정: 2026-09-14 · 킹서버 `https://web4ai.o2osolution.ai/s/stay` · Chrome(정식 채널)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 결론
|
||||||
|
|
||||||
|
**브라우저에서 첫 방문에 소리가 저절로 나게 하는 방법은 없다.**
|
||||||
|
사람 손 입력(`isTrusted: true`) 없이는 크롬이 `play()` 를 `NotAllowedError` 로 거절한다.
|
||||||
|
자바스크립트로 그 검사를 통과시키는 길은 없고, 그건 우회 대상이 아니라
|
||||||
|
**그 우회를 막으려고 만들어진 검사**다.
|
||||||
|
|
||||||
|
지금 동작: **손님이 화면에 처음 닿으면(클릭·탭·키) 켜진다.** 그 이상은 어떤 사이트도 못 한다.
|
||||||
|
|
||||||
|
### 해 본 것과 결과
|
||||||
|
|
||||||
|
| 시도 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| 로드 즉시 `audio.play()` | `NotAllowedError` |
|
||||||
|
| 무음(`muted=true`)으로 시작 → 나중에 unmute | `NotAllowedError` (무음도 거절된다) |
|
||||||
|
| `<audio muted playsinline>` 태그로 두고 `play()` | `NotAllowedError` |
|
||||||
|
| 가짜 이벤트 강제 발생 — `dispatchEvent` 로 `pointerdown`·`mousedown`·`pointerup`·`mouseup`·`click`·`keydown` 6종 | `navigator.userActivation` **false 그대로**, 거절 |
|
||||||
|
| 재생 단추를 코드로 누르기 — `btn.click()`, 0.7초 간격 3회 | `play=0` |
|
||||||
|
| `--app=URL` 앱 모드로 띄우기 | `play=0` |
|
||||||
|
| **Web Audio API** — `new AudioContext()` | `state: "suspended"` |
|
||||||
|
| `ctx.resume()` | **약속이 영원히 안 돌아온다** (제스처를 기다리며 매달린다) |
|
||||||
|
| mp3 를 `decodeAudioData` 로 풀어 `BufferSource` 재생 | `currentTime` **0.000 → 0.000**, 안 돈다 |
|
||||||
|
| **`speechSynthesis.speak()`** | `error: not-allowed` |
|
||||||
|
| 마우스만 움직임 / 스크롤만 | `play=0` (`act=false`) |
|
||||||
|
| **진짜 클릭·탭·키 입력 한 번** | **켜진다** (`act=true`, `play=1`) |
|
||||||
|
|
||||||
|
`dispatchEvent` 와 `el.click()` 이 만든 이벤트는 `isTrusted: false` 로 찍혀 나간다.
|
||||||
|
크롬의 사용자 활성화 카운터(`navigator.userActivation`)는 `isTrusted: true` 인 입력만 세고,
|
||||||
|
그게 올라야 소리가 풀린다.
|
||||||
|
|
||||||
|
브라우저에서 **소리를 낼 수 있는 API 는 셋뿐이고 셋 다 같은 게이트 뒤에 있다** —
|
||||||
|
HTMLMediaElement(`<audio>`) · Web Audio(`AudioContext`) · speechSynthesis. 우회로가 없는 이유다.
|
||||||
|
|
||||||
|
**활성화를 준다**: `mousedown` · `pointerdown` · `pointerup` · `touchend` · `keydown`(Esc 제외) · `click`
|
||||||
|
**안 준다**: `mousemove` · `pointermove` · `touchmove` · `wheel` · `scroll`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 안 한 것과 그 이유
|
||||||
|
|
||||||
|
크롬 문서(developer.chrome.com/blog/autoplay)에 예외가 넷 있지만 **전부 손님 쪽 설정이라
|
||||||
|
제품에서는 쓸 수 없다.** 한 번 만들었다가 되돌렸다.
|
||||||
|
|
||||||
|
| 예외 | 왜 안 쓰나 |
|
||||||
|
|---|---|
|
||||||
|
| PWA 설치본 | **펜션 사이트 보러 온 손님이 앱을 설치할 리 없다.** 매니페스트까지 붙였다가 걷어냈다 |
|
||||||
|
| MEI (같은 출처에서 7초 이상 여러 번) | 손님이 쌓는 것이라 첫 방문에는 없다 |
|
||||||
|
| `--autoplay-policy=no-user-gesture-required` | 브라우저를 그 플래그로 띄워야 한다 |
|
||||||
|
| 기업정책 `AutoplayAllowlist` | 기계 관리자가 넣는 것이다 |
|
||||||
|
|
||||||
|
★ **강제 클릭 코드도 뺐다.** 넣어서 배포까지 해 봤지만 기본 크롬에서 `play=0` 이고,
|
||||||
|
동작하지 않는 코드가 남으면 다음 사람이 "이미 처리돼 있네" 로 읽는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지금 코드가 하는 일 (`inject.js` `startPlayer`)
|
||||||
|
|
||||||
|
1. 로드 즉시 `play()` 를 건다. (거절되지만, 자동재생이 허락된 환경이면 여기서 켜진다)
|
||||||
|
2. 거절되면 **첫 사람 손길**을 기다린다. 창 전체에 걸고, 플레이어 자기 단추만 빼고
|
||||||
|
어디를 눌러도 걸린다.
|
||||||
|
- STRONG(활성화를 주는 입력): 스로틀 없이 매번 시도
|
||||||
|
- WEAK(안 주는 입력): 1초에 한 번만 — 초당 수십 번 오는 mousemove 로 단추가 깜빡이는 것을 막는다
|
||||||
|
3. 탭이 다시 앞으로 나올 때(`visibilitychange`) 한 번 더 건다.
|
||||||
|
|
||||||
|
### 여기서 밟았던 함정 둘
|
||||||
|
|
||||||
|
- **가짜 pointerdown 이 대기를 태웠다.** `tameRails()` 가 레일 자동 넘김을 세우려고 레일마다
|
||||||
|
`pointerdown` 을 쏘는데(정상 동작), 그게 첫 손길 대기를 먼저 먹었다. 그 뒤로는 손님이
|
||||||
|
아무 데나 눌러도 안 켜졌다. → `onFirstTouch` 가 `event.isTrusted` 를 먼저 본다.
|
||||||
|
- **스로틀이 클릭을 걸렀다.** 입력을 안 가리고 1초 스로틀을 걸었더니 클릭 직전의 mousemove 가
|
||||||
|
그 1초를 먹어 **정작 활성화를 주는 mousedown 이 걸러졌다**(실측: `act=true` 인데 `play=0`).
|
||||||
|
→ STRONG/WEAK 로 갈라 STRONG 에는 스로틀을 안 건다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## "트리플픽은 되던데?" — 실측 결과
|
||||||
|
|
||||||
|
**트리플픽도 안 된다. 똑같이 거절당한다.**
|
||||||
|
|
||||||
|
`https://triplepick.o2o.kr/match/MLB_BAL_TOR_20260914?song=BAL` (공유 링크 = 자동 로드 경로)에
|
||||||
|
깨끗한 크롬으로 들어가 `HTMLMediaElement.prototype.play` 를 가로채 찍은 값이다.
|
||||||
|
|
||||||
|
```
|
||||||
|
[T] play() 거절 NotAllowedError .kr/api/songs/audio/815/0?v=89a0fcd6
|
||||||
|
```
|
||||||
|
|
||||||
|
코드도 우리와 같은 한 줄이다 — `usePlayer.tsx:133` `if (shouldPlay) audio.play().catch(() => {})`.
|
||||||
|
게스처 감지 코드는 0줄이고, 평소 재생 경로는 `MatchupHUD.tsx:55` 의 **「♪ 응원가 듣기」 버튼**이다.
|
||||||
|
`playlist.ts:19` 의 `PATH_PLAYLISTS` 는 비어 있어서 경로 진입 자동 로드는 실을 곡도 없다.
|
||||||
|
|
||||||
|
### 그럼 왜 내 브라우저에서는 나나 — MEI
|
||||||
|
|
||||||
|
크롬은 **같은 출처에서 소리를 여러 번 들은 사람에게만** 그 출처의 자동재생을 풀어 준다
|
||||||
|
(Media Engagement Index). 조건: 7초 초과 재생 · 음소거 아님 · 탭 활성 · (영상이면) 200x140px 초과.
|
||||||
|
|
||||||
|
트리플픽을 자주 쓰면서 응원가를 여러 번 들었으면 그 브라우저에는 이미 허락이 쌓여 있다.
|
||||||
|
**그 브라우저에서만** 자동재생된다. 손님의 새 브라우저에서는 안 된다.
|
||||||
|
|
||||||
|
확인: `chrome://media-engagement/` 에서 출처별 점수와 `Has high score` 를 본다.
|
||||||
|
|
||||||
|
★ **같은 일이 이 사이트에도 일어난다.** 목업을 자주 열고 음악을 들으면 그 브라우저는
|
||||||
|
`web4ai.o2osolution.ai` 에 허락을 쌓고, 그 뒤로는 새로고침해도 바로 난다.
|
||||||
|
**대표님 노트북에서 시연하면 그 상태가 된다** — 다만 손님 브라우저는 아니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 재보고 싶을 때 — 측정 주의
|
||||||
|
|
||||||
|
★ **Playwright 로 잴 때 `page.evaluate` 를 쓰면 안 된다.** CDP 가 `userGesture: true` 로 넣어서
|
||||||
|
**상태를 읽으려고 부른 그 호출이 브라우저를 "조작됨" 으로 만든다.** 그 뒤 측정은 전부 거짓
|
||||||
|
양성이다 — 실제로 이것 때문에 "마우스만 움직여도 켜진다" 는 틀린 결론을 한 번 냈다.
|
||||||
|
|
||||||
|
상태는 `addInitScript` 로 심고 `console` 로만 받는다.
|
||||||
|
|
||||||
|
```js
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
setInterval(() => {
|
||||||
|
const el = document.getElementById('w4d-play');
|
||||||
|
console.log('[C] play=' + (el ? el.dataset.playing : '-')
|
||||||
|
+ ' act=' + navigator.userActivation.isActive);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
page.on('console', (m) => { /* 여기서만 읽는다 */ });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지금 무엇을 보고 있는지 가리는 법
|
||||||
|
|
||||||
|
`/s/` 는 `max-age=300, must-revalidate` 다. "고쳤는데 안 된다" 의 상당수가 **캐시된 옛 파일**이었다.
|
||||||
|
콘솔 첫 줄에 빌드 시각이 찍힌다.
|
||||||
|
|
||||||
|
```
|
||||||
|
[w4d] 주입분 로드 · 빌드 2026-09-14T07:12:00+00:00
|
||||||
|
```
|
||||||
|
|
||||||
|
이 시각이 방금 구운 것과 다르면 옛 파일을 보고 있는 것이다. ⌘⇧R.
|
||||||
@ -59,7 +59,7 @@
|
|||||||
| 파일 | 옮길 자리 |
|
| 파일 | 옮길 자리 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 이 문서 **1.4 프롬프트 전문** | `shared/src/lib/section-prompts.ts` |
|
| 이 문서 **1.4 프롬프트 전문** | `shared/src/lib/section-prompts.ts` |
|
||||||
| `build_itinerary.py` (규칙 16종) | `backend/services/itinerary.py` |
|
| `build_itinerary.py` (규칙 17종) | `backend/services/itinerary.py` |
|
||||||
| `build_story.py` (위키 사진 수집) | `backend/services/external/wikimedia.py` |
|
| `build_story.py` (위키 사진 수집) | `backend/services/external/wikimedia.py` |
|
||||||
| `inject.js` (렌더러에 **없는** 기능 3개) | `site/src/sections/` |
|
| `inject.js` (렌더러에 **없는** 기능 3개) | `site/src/sections/` |
|
||||||
|
|
||||||
@ -81,25 +81,26 @@
|
|||||||
| `photos` | 사진 갤러리 | `media[]` | 사장님이 올림 | |
|
| `photos` | 사진 갤러리 | `media[]` | 사장님이 올림 | |
|
||||||
| `map` | 오시는 길 | `place` + `routes[]` | 수집 | |
|
| `map` | 오시는 길 | `place` + `routes[]` | 수집 | |
|
||||||
| `festival` | 계절별 축제 | `local.festivals` | TourAPI 수집 | |
|
| `festival` | 계절별 축제 | `local.festivals` | TourAPI 수집 | |
|
||||||
| `local` | 지역 정보 | `local.attractions/restaurants` | TourAPI 수집 | |
|
| `local` | 지역 정보 | `local.attractions/restaurants` | TourAPI 수집 (시연본 맛집은 `restaurants.json` 31곳) | |
|
||||||
| `itinerary` | **추천 일정** | `data(kind=itinerary)` | `services/itinerary.py` + 빌더 프롬프트 `dataSpec.ts:556` | **T2** |
|
| `itinerary` | **추천 일정** | `data(kind=itinerary)` | `services/itinerary.py` + 빌더 프롬프트 `dataSpec.ts:556` | **T2** |
|
||||||
| `story` | 군산 이야기 | 아래 6탭 껍데기 | — | |
|
| `story` | 군산 이야기 | 아래 6탭 껍데기 | — | |
|
||||||
| `songs` | 가요 다방 | `data(kind=songs)` | LLM `section-prompts.ts:67` | **T3 T5** |
|
| `songs` | 가요 다방 | `data(kind=songs)` | LLM `section-prompts.ts:67` | **T3 T5** |
|
||||||
| `daily` | 오늘의 한 장 | `data(kind=daily)` | LLM `:91` | 섹션 자리가 없다 |
|
| `daily` | 오늘의 한 장 | `data(kind=daily)` | LLM `:91` | 시연본은 노래·문학 39장(`build_daily.py`) |
|
||||||
| `people` | 인물 열전 | `data(kind=people)` | LLM `:113` | **T3 T5** |
|
| `people` | 인물 열전 | `data(kind=people)` | LLM `:113` | **T3 T5** |
|
||||||
| `chronicle` | 시간의 골목 | `data(kind=chronicle)` | LLM `:135` | |
|
| `chronicle` | 시간의 골목 | `data(kind=chronicle)` | LLM `:135` | |
|
||||||
| `postcard` | 오늘의 엽서 | `data(kind=postcard)` | LLM `:160` | |
|
| `postcard` | 오늘의 엽서 | `data(kind=postcard)` | LLM `:160` | 시연본은 탭을 비우고 독립 "엽서 쓰기" 로 대체(**2.5 항목 9**) |
|
||||||
| `quiz` | 뒤집어 보는 질문 | `data(kind=quiz)` | LLM `:188` | |
|
| `quiz` | 뒤집어 보는 질문 | `data(kind=quiz)` | LLM `:188` | |
|
||||||
| `faq` | 자주 묻는 질문 | `faqs[]` | LLM + 사장님 | |
|
| `faq` | 자주 묻는 질문 | `faqs[]` | LLM + 사장님 | |
|
||||||
| `rules` | 이용 규정 | `facts[]` | 사장님이 적음 | |
|
| `rules` | 이용 규정 | `facts[]` | 사장님이 적음 | |
|
||||||
| `weather` | 날씨 | `local.weather` | open-meteo | |
|
| `weather` | 날씨 | `local.weather` | open-meteo | |
|
||||||
| `planner` | 계절별 추천 하루 | `data(kind=planner)` | **없음** | (시연본은 꺼 둠) |
|
| `planner` | 계절별 추천 하루 | `data(kind=planner)` | **없음** | (시연본은 꺼 둠) |
|
||||||
|
|
||||||
섹션이 아닌 것 하나가 더 있다 — 헤더 음악 플레이어(**T4**).
|
섹션이 아닌 것이 셋 더 있다 — 헤더 음악 플레이어(**T4**) · **군산 읽기**(**T7**, '군산 이야기'
|
||||||
|
탭 묶음의 여섯 번째 탭) · **엽서 쓰기**(독립 섹션, `festival` 바로 앞).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1.3 할 일 여섯
|
## 1.3 할 일 여덟
|
||||||
|
|
||||||
### T1 — 히어로 캐치프레이즈
|
### T1 — 히어로 캐치프레이즈
|
||||||
|
|
||||||
@ -118,7 +119,10 @@
|
|||||||
weather?: '맑음'|'구름많음'|'흐림'|'비'|'눈';
|
weather?: '맑음'|'구름많음'|'흐림'|'비'|'눈';
|
||||||
}[] };
|
}[] };
|
||||||
```
|
```
|
||||||
2. 히어로가 **지금 조건에 맞는 것만 후보**로 두고 돌린다.
|
2. **대표 문구(`tagline`)는 고정하고, 순환은 그 아래 줄에서 돈다** (2026-09-11 사장님 지시).
|
||||||
|
대표 문구 자리를 갈아 끼우면 숙소의 한 줄이 첫 화면에서 사라진다. 아래 줄은 대표 문구보다
|
||||||
|
한 단 작고 옅게, 높이는 가장 긴 문구에 맞춰 잡는다(히어로가 아래 정렬이라 줄 수가 바뀌면 튄다).
|
||||||
|
히어로가 **지금 조건에 맞는 것만 후보**로 두고 돌린다.
|
||||||
순환 순서는 `일반 → 계절 → 일반 → 월 → 일반 → 날씨` 한 바퀴를 만들고 그 바퀴를 돈다.
|
순환 순서는 `일반 → 계절 → 일반 → 월 → 일반 → 날씨` 한 바퀴를 만들고 그 바퀴를 돈다.
|
||||||
일반은 자루에서 뽑아 쓰고 비면 다시 채운다(같은 문장이 연달아 안 나오게).
|
일반은 자루에서 뽑아 쓰고 비면 다시 채운다(같은 문장이 연달아 안 나오게).
|
||||||
계절은 3개월 단위(3~5 봄 · 6~8 여름 · 9~11 가을 · 12~2 겨울).
|
계절은 3개월 단위(3~5 봄 · 6~8 여름 · 9~11 가을 · 12~2 겨울).
|
||||||
@ -161,7 +165,7 @@
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **① 장소 대장** | LLM | 장소 30곳 × `{stayMinutes:{min,base,max}, todo, indoor, needsCar, bestTime, searchQuery}` | **1.4 ②** |
|
| **① 장소 대장** | LLM | 장소 30곳 × `{stayMinutes:{min,base,max}, todo, indoor, needsCar, bestTime, searchQuery}` | **1.4 ②** |
|
||||||
| **② 좌표** | 코드 | 카카오 로컬로 `searchQuery` → 위경도. **LLM 이 적은 좌표는 쓰지 않는다** | — |
|
| **② 좌표** | 코드 | 카카오 로컬로 `searchQuery` → 위경도. **LLM 이 적은 좌표는 쓰지 않는다** | — |
|
||||||
| **③ 조립** | **코드** | 이동시간·도착시각·끼니 창 맞추기·규칙 16종 감사 | 없음 (`build_itinerary.py`) |
|
| **③ 조립** | **코드** | 이동시간·도착시각·끼니 창 맞추기·규칙 17종 감사 | 없음 (`build_itinerary.py`) |
|
||||||
| **④ 테마** | LLM | 테마 20개 × `{name, duration, audience, why, days[{kind, places[]}]}` | **1.4 ③** |
|
| **④ 테마** | LLM | 테마 20개 × `{name, duration, audience, why, days[{kind, places[]}]}` | **1.4 ③** |
|
||||||
|
|
||||||
**코드가 하는 일(③)에서 반드시 옮겨야 하는 것** — 근거는 `build_itinerary.py` 줄번호
|
**코드가 하는 일(③)에서 반드시 옮겨야 하는 것** — 근거는 `build_itinerary.py` 줄번호
|
||||||
@ -174,7 +178,7 @@
|
|||||||
| 창 맞추기 | 어긋나면 **앞 칸 체류를 범위(min~max) 안에서** 늘이고 줄인다. 이동시간을 고치지 않는다. 그래도 안 되면 시작 시각을 10분씩 옮기고, 그래도 안 되면 **그 조합을 만들지 않는다** | `plan_day()` |
|
| 창 맞추기 | 어긋나면 **앞 칸 체류를 범위(min~max) 안에서** 늘이고 줄인다. 이동시간을 고치지 않는다. 그래도 안 되면 시작 시각을 10분씩 옮기고, 그래도 안 되면 **그 조합을 만들지 않는다** | `plan_day()` |
|
||||||
| 테마별 완급 | 시작 시각·체류 배율이 테마마다 다르다(쉬러 오면 1.25배, 사진 찍으면 0.8배) → 끝나는 시각이 13:35~21:00 로 퍼진다 | `:470` |
|
| 테마별 완급 | 시작 시각·체류 배율이 테마마다 다르다(쉬러 오면 1.25배, 사진 찍으면 0.8배) → 끝나는 시각이 13:35~21:00 로 퍼진다 | `:470` |
|
||||||
| 끼니 간격 | 앞 끼니가 끝나고 **3시간**이 지나야 다음 끼니 창이 열린다(`MEAL_GAP`). 창만 지키면 통과하던 자리다 | `_window_of()` |
|
| 끼니 간격 | 앞 끼니가 끝나고 **3시간**이 지나야 다음 끼니 창이 열린다(`MEAL_GAP`). 창만 지키면 통과하던 자리다 | `_window_of()` |
|
||||||
| **감사 16종** | 하나라도 어기면 **payload 를 쓰지 않고 멈춘다** | `:526` · 목록은 **2.4** |
|
| **감사 17종** | 하나라도 어기면 **payload 를 쓰지 않고 멈춘다** | `:575` · 목록은 **2.4** |
|
||||||
|
|
||||||
★ **감사는 생성기 안에 둔다.** 밖에 두면 뼈대를 고칠 때마다 "이번에 안 본 규칙"이 생긴다 —
|
★ **감사는 생성기 안에 둔다.** 밖에 두면 뼈대를 고칠 때마다 "이번에 안 본 규칙"이 생긴다 —
|
||||||
실제로 그래서 33건이 통과했다(**3.2**).
|
실제로 그래서 33건이 통과했다(**3.2**).
|
||||||
@ -182,12 +186,18 @@
|
|||||||
**하루 뼈대** — `@` 는 숙소
|
**하루 뼈대** — `@` 는 숙소
|
||||||
|
|
||||||
```
|
```
|
||||||
첫날 체크인@ · 오후 · 오후 · 저녁 · 밤산책 · 복귀@
|
첫날 체크인@ · 오후 · 오후 · 저녁 · 밤산책 · 복귀@
|
||||||
가운데날 아침@ · 오전 · 점심 · 오후 · 오후 · 쉼@ · 저녁 · 복귀@
|
가운데날 아침@ · 오전 · 점심 · 오후 · 오후 · 쉼@ · 저녁 · 복귀@
|
||||||
쉬는날 숙소에서 쉬는 날@ · 점심 · 쉼@ · 저녁 · 복귀@
|
쉬는날 숙소에서 쉬는 날@ · 점심 · 쉼@ · 저녁 · 복귀@
|
||||||
마지막날 아침(퇴실)@ · 오전 · 점심 · 오후 · 마무리@
|
마지막날(도보) 아침(퇴실)@ · 오전 · 점심 · 오후 · 마무리@ ← 짐을 맡기고 저녁에 찾으러 온다
|
||||||
|
마지막날(차) 아침(퇴실·차)@ · 오전 · 점심 · 오후 ← 짐을 싣고 나섰다, 복귀 칸이 없다
|
||||||
```
|
```
|
||||||
|
|
||||||
|
★ **떠나는 날은 둘로 갈린다** (2026-09-14 대표 의견). 차로 온 손님에게 "짐을 맡기고 저녁에
|
||||||
|
찾으러 오세요" 는 **한 번 더 들르라는 말**이다 — 퇴실할 때 실으면 끝날 일이다. 그래서 차 테마의
|
||||||
|
마지막 날은 마지막 정거장에서 끝난다. 짐 보관 동선은 걸어 다니는 손님 것이고 그 테마만 남는다.
|
||||||
|
뼈대를 두 벌로 적지 않고 **복귀 칸만 떼어** 만든다(`build_itinerary.py` `CAR_KINDS`).
|
||||||
|
|
||||||
**완료 기준** 새 업장에서 테마 20개가 나오고 `audit()` 위반 0건.
|
**완료 기준** 새 업장에서 테마 20개가 나오고 `audit()` 위반 0건.
|
||||||
하루가 숙소로 시작해 숙소로 끝나고, 끼니가 제 시각에 있고, 21시를 안 넘는다.
|
하루가 숙소로 시작해 숙소로 끝나고, 끼니가 제 시각에 있고, 21시를 안 넘는다.
|
||||||
|
|
||||||
@ -220,9 +230,18 @@
|
|||||||
|
|
||||||
**할 일** `MediaItem` 에 `kind:'audio'`(+`title` `artist` `durationSec`)를 넣고 발행 때 같이 올린다.
|
**할 일** `MediaItem` 에 `kind:'audio'`(+`title` `artist` `durationSec`)를 넣고 발행 때 같이 올린다.
|
||||||
헤더에 붙는 작은 플레이어를 만든다 — 아이콘 · 현재 곡명 · 재생/멈춤 · 목록.
|
헤더에 붙는 작은 플레이어를 만든다 — 아이콘 · 현재 곡명 · 재생/멈춤 · 목록.
|
||||||
**자동재생은 넣지 않는다**(2026-09-11 지시). 순서는 **랜덤 시작 후 순차**.
|
순서는 **랜덤 시작 후 순차**.
|
||||||
목업 구현은 `inject.js` 의 `startPlayer()`, 모양은 `inject.css` 의 `#w4d-mini` · `#w4d-panel`.
|
목업 구현은 `inject.js` 의 `startPlayer()`, 모양은 `inject.css` 의 `#w4d-mini` · `#w4d-panel`.
|
||||||
|
|
||||||
|
★ **자동재생한다**(2026-09-14 대표 지시 — 2026-09-11 의 "자동 빼" 를 뒤집은 것이다).
|
||||||
|
다만 **브라우저가 막는다.** 크롬·사파리는 손님이 페이지를 한 번도 만지지 않았으면 소리 나는
|
||||||
|
재생을 거절한다(정책이고, 거절된 약속으로 온다). 실측 2026-09-14, 크롬: 불러온 직후 재생 0,
|
||||||
|
세로 스크롤 한 번에 0:02 부터 재생. 그래서 구현이 **두 단계**다 —
|
||||||
|
① 불러오자마자 걸어 본다 ② 막히면 손님이 **처음 만지는 순간**(스크롤·클릭·키·터치) 켠다.
|
||||||
|
→ 첫 손길 리스너는 `#w4d-mini`·`#w4d-panel` 안에서 난 이벤트를 **흘려보내야 한다.**
|
||||||
|
예전 판은 window 캡처라 재생 단추의 클릭을 먼저 먹었고, 단추 핸들러가 다시 꺼서
|
||||||
|
두 번 눌러야 켜졌다(2026-09-11). 검사는 `audit-all.mjs` 의 `자동재생`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### T5 — 상한(`maxItems`)
|
### T5 — 상한(`maxItems`)
|
||||||
@ -254,6 +273,68 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### T7 — 도시를 펼쳐 보이는 자리
|
||||||
|
|
||||||
|
**증상** 도시 이야기를 담는 자리가 전부 한 번에 하나만 보인다. 하루에 한 장만 보여주는
|
||||||
|
일력이나, 넉 장짜리 엽서 레일 정도로는 군산이라는 도시를 소개하기엔 부족하다.
|
||||||
|
|
||||||
|
**할 일** 문학·역사·섬과 바다·장소·음식처럼 갈래로 나눈 도시 이야기를, 한 화면에서
|
||||||
|
옆으로 넘겨 가며 볼 수 있는 자리를 만든다.
|
||||||
|
|
||||||
|
1. 이 콘텐츠는 **사장님마다 새로 만드는 게 아니다.** 군산이라는 지역이 공유하는 고정
|
||||||
|
자료다(계절별 축제·주변 명소와 같은 성격) — 사이트를 만들 때마다 AI 가 새로 글을
|
||||||
|
지어내는 게 아니라, 군산에 있는 모든 사이트가 같은 이야기 목록을 나눠 쓴다.
|
||||||
|
2. 항목마다 출처 링크를 단다. 정확한 문서 주소를 일일이 확인하기 어려우니, 제목으로
|
||||||
|
검색되는 링크(예: 네이버 검색)를 걸어 둔다 — 없는 주소를 지어내 걸지 않는다.
|
||||||
|
3. 옆으로 넘겨 보는 방식은 **이 사이트가 이미 쓰고 있는 것**을 그대로 쓴다(사진 갤러리·
|
||||||
|
추천 일정에 쓰는 그 슬라이드). 새로 스크롤 상자를 만들지 않는다. 한 슬라이드에는
|
||||||
|
이야기 하나만 크게 보여준다 — 여러 개를 한 슬라이드에 욱여넣지 않는다.
|
||||||
|
4. 한 번 볼 때 전체 이야기를 다 보여주지 않는다. 그중 몇 개만 무작위로 뽑아 보여주고,
|
||||||
|
다시 오거나 새로고침하면 다른 조합이 뜬다. 화면 문구에 "전체 몇 개"라고 숫자를
|
||||||
|
박지 않는다 — 실제로 보이는 개수와 어긋나면 안 되기 때문이다.
|
||||||
|
5. 화면 안에서는 '군산 이야기'(가요 다방·인물 열전 등이 모여 있는 탭 묶음)의 탭
|
||||||
|
하나로 들어간다 — 독립된 섹션으로 따로 빼지 않는다. 다른 탭들처럼 탭 안에도
|
||||||
|
제목을 그대로 보여준다.
|
||||||
|
6. 이미 다른 자리에 나온 이야기는 다시 넣지 않는다 — 인물·가요·축제·연표·명소·맛집이
|
||||||
|
각자 자리를 갖고 있다. 같은 이야기를 두 번 보여주면 페이지만 길어진다.
|
||||||
|
|
||||||
|
**완료 기준** 갈래를 가리지 않고 훑어볼 수 있고, 이야기마다 출처가 붙어 있고, 넘기는
|
||||||
|
방식이 이 사이트에서 이미 쓰는 방식과 같다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### T8 — 엽서 쓰기
|
||||||
|
|
||||||
|
**증상** '오늘의 엽서'는 미리 써 둔 문구를 보여주기만 했다. 손님이 직접 한마디를 써서
|
||||||
|
자기만의 엽서를 만들고, 그걸 저장하거나 공유할 수 있게 바꾼다.
|
||||||
|
|
||||||
|
**이건 AI 가 글을 짓는 기능이 아니다.** 새 데이터 형식이나 문구를 설계할 필요 없이,
|
||||||
|
이미 만들어 확인까지 끝낸 화면 기능 하나를 그대로 옮겨 붙이면 되는 일이다.
|
||||||
|
|
||||||
|
1. 숙소 사진(가진 사진 전체를 보여주고 손님이 그중 하나를 고르게 한다) + 손님이
|
||||||
|
직접 쓴 한마디 + 우표·소인 모양을 합쳐서 한 장의 카드 이미지로 만든다.
|
||||||
|
2. 문구는 최대 네 줄까지만 쓸 수 있게 막는다. 줄이 넘어가면 자동으로 다음 줄로
|
||||||
|
넘어가는데, 띄어쓰기 없이 길게 이어 쓴 글자도 카드 밖으로 삐져나오지 않고
|
||||||
|
글자 단위로 잘 접혀야 한다.
|
||||||
|
3. 완성한 카드는 사진으로 저장하거나 공유할 수 있다. 공유할 때는 카드 이미지와
|
||||||
|
함께, 지금 보고 있는 이 사이트 페이지로 가는 링크를 같이 보낸다. 손님이 쓴
|
||||||
|
문구는 이미 카드 그림 안에 박혀 있으니, 공유 문구 자리에 그 글을 따로 한 번 더
|
||||||
|
적지 않는다 — 같은 말이 두 번 보이면 안 된다. 서버에 새로 올리는 기능을 만들
|
||||||
|
필요 없이 휴대폰·브라우저가 원래 갖고 있는 공유 기능을 그대로 쓰고, 그래서 이
|
||||||
|
기능은 서버를 한 번도 부르지 않는다.
|
||||||
|
4. 공유 버튼이나 안내 문구에 특정 메신저 이름을 못 박지 않는다 — 기기에 따라
|
||||||
|
실제로 뜨는 공유 대상이 다르다(휴대폰은 설치된 메신저가 뜨지만, 컴퓨터 특히
|
||||||
|
맥은 카카오톡이 그 목록에 안 뜬다). 버튼 문구는 손끝으로 조작하는 기기에서만
|
||||||
|
"카카오톡 등으로 공유"라고 쓰고, 그 외에는 그냥 "공유하기"라고 쓴다.
|
||||||
|
5. 상호명을 코드 안에 고정으로 박아 넣지 않는다 — 이 기능은 다른 숙소나 카페에서도
|
||||||
|
그대로 가져다 써야 한다.
|
||||||
|
6. 화면 위치는 "계절별 축제" 섹션 바로 앞에 독립된 섹션으로 둔다.
|
||||||
|
|
||||||
|
**완료 기준** 손님이 사진 고르고 글 쓰고 저장·공유하는 것까지 한 화면에서 끝난다.
|
||||||
|
새 서버 기능이나 AI 프롬프트가 필요 없다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1.4 프롬프트 전문
|
## 1.4 프롬프트 전문
|
||||||
|
|
||||||
**프롬프트는 지금 두 파일에 나뉘어 있다. 이게 문제다.**
|
**프롬프트는 지금 두 파일에 나뉘어 있다. 이게 문제다.**
|
||||||
@ -307,7 +388,7 @@ backend/services/prompts/section_prompts.json ← 산출물. 손으로 고치
|
|||||||
창고형 카페 공간 · 침구 매일 세탁 · 히로쓰 가옥 옆 · 초원사진관 도보 3분}
|
창고형 카페 공간 · 침구 매일 세탁 · 히로쓰 가옥 옆 · 초원사진관 도보 3분}
|
||||||
|
|
||||||
[해야 할 일]
|
[해야 할 일]
|
||||||
[업소] 의 히어로 문구를 100개 만든다. 상호 바로 아래에 한 줄로 뜨고 7초마다 바뀐다.
|
[업소] 의 히어로 문구를 100개 만든다. 상호와 대표 문구 아래에 한 줄로 뜨고 7초마다 바뀐다.
|
||||||
네 종류로 나눠 만든다.
|
네 종류로 나눠 만든다.
|
||||||
|
|
||||||
· 일반 40개 — 언제 나와도 되는 문구
|
· 일반 40개 — 언제 나와도 되는 문구
|
||||||
@ -552,9 +633,11 @@ backend/services/prompts/section_prompts.json ← 산출물. 손으로 고치
|
|||||||
stay-payload.json 원본 payload (건드리지 않는다 · 되돌릴 때 기준)
|
stay-payload.json 원본 payload (건드리지 않는다 · 되돌릴 때 기준)
|
||||||
orig/index.html 원본 HTML (건드리지 않는다)
|
orig/index.html 원본 HTML (건드리지 않는다)
|
||||||
│
|
│
|
||||||
├─ build_itinerary.py 여행 일정을 계산해 넣는다 (+ 규칙 16종 감사)
|
├─ build_itinerary.py 여행 일정을 계산해 넣는다 (+ 규칙 17종 감사)
|
||||||
├─ build_story.py 가요 다방 · 인물 열전을 넣는다
|
├─ build_story.py 가요 다방 · 인물 열전을 넣는다
|
||||||
└─ patch_stay.py 캐치프레이즈 · 객실 사진 · 자작곡 · 주입 CSS/JS · 자산 경로 교정
|
├─ build_reading.py 군산 읽기(카로셀 34꼭지, 화면엔 매번 5~6개 무작위)를 넣는다
|
||||||
|
├─ build_daily.py 오늘의 한 장(일력 39장)을 넣는다 — 가요·인물·문학을 읽어 쓰므로 맨 뒤
|
||||||
|
└─ patch_stay.py 캐치프레이즈 · 주변 맛집 · 객실 사진 · 자작곡 · 주입 CSS/JS · 자산 경로 교정
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
build/index.html ← 배포하는 파일 (git 이그노어)
|
build/index.html ← 배포하는 파일 (git 이그노어)
|
||||||
@ -599,19 +682,27 @@ node audit-all.mjs
|
|||||||
### 내용을 바꾼 뒤 (전체 굽기)
|
### 내용을 바꾼 뒤 (전체 굽기)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 build_itinerary.py && python3 build_story.py && python3 patch_stay.py
|
python3 build_itinerary.py && python3 build_story.py && python3 build_reading.py \
|
||||||
|
&& python3 build_daily.py && python3 patch_stay.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**순서를 지킨다.** `build_itinerary.py` 가 `stay-payload.json`(원본)을 읽어
|
**순서를 지킨다.** `build_itinerary.py` 가 `stay-payload.json`(원본)을 읽어
|
||||||
`stay-payload-new.json` 을 만들고, 뒤 둘이 그 파일을 이어서 고친다.
|
`stay-payload-new.json` 을 만들고, 뒤 셋이 그 파일을 이어서 고친다.
|
||||||
순서를 바꾸면 앞 단계 결과가 지워진다.
|
순서를 바꾸면 앞 단계 결과가 지워진다.
|
||||||
|
`build_daily.py` 는 가요 다방·인물 열전·군산 읽기를 **읽어서** 일력을 만든다 — 맨 뒤여야 한다.
|
||||||
|
손으로 적는 자리가 한 곳이어야 해서다: 문학 8꼭지의 문장은 `build_reading.py` 에만 있다.
|
||||||
|
|
||||||
|
주변 맛집을 다시 받을 때만 `solution/backend` 에서 `.venv/bin/python ../site/scripts/mockup/fetch_restaurants.py` 를 먼저 돌린다(TourAPI 키가 필요하다).
|
||||||
|
굽기는 그 결과 파일만 읽는다 — 굽기가 키에 매이면 다른 기계에서 못 굽는다.
|
||||||
|
그다음 `python3 fill_restaurant_photos.py` 로 사진 없는 집의 사진을 받는다. ⚠ 이 11장은 블로그·네이버 플레이스·관광 사이트에 올라온 것이라 **권리가 게시자에게 있다** — 시연 뒤 내리거나 사장님 사진으로 바꾼다. 제품으로 옮기지 않는다.
|
||||||
|
|
||||||
로컬 반영 · 검사:
|
로컬 반영 · 검사:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker cp build/index.html o2o-web4ai-solution-prerender:/app/solution/site/out/s/stay/index.html
|
docker cp build/index.html o2o-web4ai-solution-prerender:/app/solution/site/out/s/stay/index.html
|
||||||
node audit-all.mjs # 34개 항목
|
node audit-all.mjs # 47개 항목
|
||||||
node rails-test.mjs # 카로셀 13개
|
node rails-test.mjs # 카로셀 13개 (모바일은 +3)
|
||||||
|
python3 audit_schedule.py # 일정 전수 검수표(SCHEDULE.md)
|
||||||
```
|
```
|
||||||
|
|
||||||
`/s/` 는 **5분 캐시**(`max-age=300`)다 — 확인할 때 ⌘⇧R.
|
`/s/` 는 **5분 캐시**(`max-age=300`)다 — 확인할 때 ⌘⇧R.
|
||||||
@ -645,14 +736,32 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
|
|
||||||
| 자리 | 줄 | 무엇 |
|
| 자리 | 줄 | 무엇 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GENERAL` | `:20` | 일반 40개 |
|
| `GENERAL` | `:29` | 일반 40개 |
|
||||||
| `SEASON` | `:62` | 계절별 24개 — 봄/여름/가을/겨울 각 6 |
|
| `SEASON` | `:71` | 계절별 24개 — 봄/여름/가을/겨울 각 6 |
|
||||||
| `MONTH` | `:72` | 월별 24개 — 1~12월 각 2 |
|
| `MONTH` | `:81` | 월별 24개 — 1~12월 각 2 |
|
||||||
| `WEATHER` | `:87` | 날씨별 12개 — 맑음/구름많음/흐림/비/눈 |
|
| `WEATHER` | `:96` | 날씨별 12개 — 맑음/구름많음/흐림/비/눈 |
|
||||||
|
|
||||||
문장만 더하거나 지우면 된다. 개수 제한은 없다.
|
문장만 더하거나 지우면 된다. 개수 제한은 없다.
|
||||||
순환 순서·계절 구분·날씨 연동은 **1.3 T1** 에 적어 둔 그대로다(`inject.js`).
|
순환 순서·계절 구분·날씨 연동은 **1.3 T1** 에 적어 둔 그대로다(`inject.js`).
|
||||||
|
|
||||||
|
### 오늘의 날씨 문구 50개 → `patch_stay.py`
|
||||||
|
|
||||||
|
| 자리 | 줄 | 무엇 |
|
||||||
|
|---|---|---|
|
||||||
|
| `SKY_NOTES` | `:114` | 하늘 25개 — 맑음/구름많음/그외/비/눈 각 5. **집 안**에서 뭘 할지 |
|
||||||
|
| `TEMP_NOTES` | `:152` | 기온대 25개 — 혹서/더움/선선/쌀쌀/추움 각 5. **밖에서** 어디를 갈지 |
|
||||||
|
|
||||||
|
케이스마다 여러 벌이고 20초마다 자루에서 다음 것이 나온다(`inject.js` ⑥).
|
||||||
|
굽는 판에는 케이스마다 **첫 문장**이 박히고, 나머지는 `local.weather.noteSets` ·
|
||||||
|
`tempNoteSets` 로 실려 주입분만 읽는다 — 렌더러 계약에 없는 칸이라 그대로 지나간다.
|
||||||
|
|
||||||
|
- 개수 제한은 없다. 다만 **문장이 겹치면 빌드가 멈춘다** — 주입분이 DOM 에서 바꿀 줄을
|
||||||
|
문장으로 찾기 때문에, 같은 문장이 둘이면 어느 줄인지 못 가른다.
|
||||||
|
- 이름을 대는 곳은 이 사이트에 **이미 있는 자리**만 쓴다(주변 안내 12곳 · 맛집 31곳).
|
||||||
|
화면에 없는 곳을 날씨 칸에서만 권하면 손님이 그걸 찾을 데가 없다.
|
||||||
|
- 기온대 경계(30·25·20·10)는 렌더러가 정한다(`site/src/lib/derive.ts weatherBand`).
|
||||||
|
화면의 배지는 렌더러가 붙이고 문장만 주입분이 갈아 끼우므로, 경계를 바꾸면 둘이 어긋난다.
|
||||||
|
|
||||||
### 여행 일정 → `build_itinerary.py`
|
### 여행 일정 → `build_itinerary.py`
|
||||||
|
|
||||||
**사람은 "어느 자리에 어느 장소" 만 고르고, 시각은 스크립트가 계산한다.**
|
**사람은 "어느 자리에 어느 장소" 만 고르고, 시각은 스크립트가 계산한다.**
|
||||||
@ -660,17 +769,55 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
| 자리 | 줄 | 무엇 |
|
| 자리 | 줄 | 무엇 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PLACES` | `:40` | **장소 대장** — 좌표 · 지도 검색어 · 체류시간(min/base/max) · **거기서 무엇을 하는지** |
|
| `PLACES` | `:40` | **장소 대장** — 좌표 · 지도 검색어 · 체류시간(min/base/max) · **거기서 무엇을 하는지** |
|
||||||
| `STAY_ACTION` | `:108` | 머뭄 칸의 행위 — 체크인 · 아침 · 아침(퇴실) · 쉼 · 쉬는 날 · 머뭄으로 · 마무리 |
|
| `STAY_ACTION` | `:108` | 머뭄 칸의 행위 — 체크인 · 아침 · 아침(퇴실) · **아침(퇴실·차)** · 쉼 · 쉬는 날 · 머뭄으로 · 마무리 |
|
||||||
| `WINDOW` | `:156` | 시간대가 정해진 자리 |
|
| `WINDOW` | `:162` | 시간대가 정해진 자리 |
|
||||||
| `SKELETON` | `:182` | 하루 뼈대. `@` 가 붙은 것은 머뭄 |
|
| `SKELETON` | `:194` | 하루 뼈대. `@` 가 붙은 것은 머뭄. **떠나는 날은 차·도보 두 벌**(`CAR_KINDS` 가 복귀 칸을 떼어 만든다) |
|
||||||
| `THEMES` | `:368` | 테마 21개 |
|
| `THEMES` | `:414` | 테마 21개. 마지막 날 뼈대가 `last_car` 계열이면 **차로 떠나는 테마**다(지금 14개 — 1박 2일 7 · 2박 3일 7) |
|
||||||
| `PACING` | `:470` | 테마별 시작 시각과 체류 배율 |
|
| `PACING` | `:516` | 테마별 시작 시각과 체류 배율 |
|
||||||
|
|
||||||
**새 장소** → `PLACES` 에 한 줄. 좌표는 카카오 로컬에서 받은 실제 값이어야 한다(지어내지 않는다).
|
**새 장소** → `PLACES` 에 한 줄. 좌표는 카카오 로컬에서 받은 실제 값이어야 한다(지어내지 않는다).
|
||||||
그다음 `THEMES` 의 원하는 자리에 이름을 적는다.
|
그다음 `THEMES` 의 원하는 자리에 이름을 적는다.
|
||||||
**새 테마** → `THEMES` 에 한 덩이, `PACING` 에 한 줄. 뼈대의 `@` 없는 자리 수와 장소 개수가
|
**새 테마** → `THEMES` 에 한 덩이, `PACING` 에 한 줄. 뼈대의 `@` 없는 자리 수와 장소 개수가
|
||||||
같아야 한다 — 틀리면 빌드가 멈추고 알려 준다.
|
같아야 한다 — 틀리면 빌드가 멈추고 알려 준다.
|
||||||
|
|
||||||
|
### 군산 읽기 → `build_reading.py`
|
||||||
|
|
||||||
|
| 자리 | 무엇 |
|
||||||
|
|---|---|
|
||||||
|
| `GROUPS` | 갈래와 그 아래 한 줄. 화면에 이 순서로 선다 |
|
||||||
|
| `ITEMS` | 꼭지 34개 — (갈래, 제목, 글, 연도). **문학 문장의 단일 출처**다 |
|
||||||
|
| `naver(title)` | 항목마다 다는 출처 — 네이버 검색 링크 하나로 통일(아래 참고) |
|
||||||
|
|
||||||
|
- **이미 다른 섹션에 선 것은 넣지 않는다.** 인물 57 · 가요 25 · 축제 10 · 연표 7 · 명소 12 ·
|
||||||
|
맛집 31 이 각자 자리를 갖는다. 받은 엑셀 52주제에서 그걸 빼면 34꼭지가 남는다.
|
||||||
|
- 연도는 **확인된 것만** 적는다(연표에서 이미 확인한 값, 엑셀이 명시한 1944 등).
|
||||||
|
- 글은 **3~5문장**이다(2026-09-14 대표: "2줄있는애들 최소 3-5줄 이였으면 좋겠오") —
|
||||||
|
두 문장짜리 21꼭지를 네 문장으로 늘렸다. 새로 보탠 문장은 없는 사실을 지어낸 게 아니라
|
||||||
|
널리 알려진 배경 설명이다.
|
||||||
|
- **`verified`("확인"/"확인필요") 필드는 뺐다.** 대신 `source` 를 **네이버 검색 링크**로
|
||||||
|
통일했다(`naver(title)` — `https://search.naver.com/search.naver?query=...`). 기관·위키
|
||||||
|
링크를 항목마다 골라 달던 방식(`CHAE`·`STAMP`·`TOUR`·`WIKI`·`KCI`·`GUIDE` 상수)은 없앴다 —
|
||||||
|
개별 문서 URL 은 실제로 확인 안 하고 적으면 없는 문서로 이어질 위험이 있어서다(레포 절대
|
||||||
|
규칙: 없는 사실은 안 짓는다). 검색 링크는 제목을 그대로 넘기니 항상 관련 결과로 뜬다.
|
||||||
|
- 모양은 `inject.css` `.w4d-read-*` — 이 사이트의 실제 카로셀(embla)이다. 신문 조판(제호·면·단)
|
||||||
|
은 한 번 시도했다가 되돌렸다 — 경위는 **2.5 항목 3**.
|
||||||
|
- 화면은 **전체 중 5~6개만 매번 무작위로** 보여준다(갈래 안 가림) — payload 에는 34개가
|
||||||
|
전부 있고, `subtitle` 에도 전체 개수를 안 박는다(**2.5 항목 3**).
|
||||||
|
|
||||||
|
### 오늘의 한 장(일력) → `build_daily.py`
|
||||||
|
|
||||||
|
| 자리 | 무엇 |
|
||||||
|
|---|---|
|
||||||
|
| `POETS` | 시인 6명. 이름만 적으면 인물 열전에서 한 줄과 위키 출처를 가져온다 |
|
||||||
|
| (자동) | 가요 25장은 가요 다방을, 문학 8장은 군산 읽기를 **그대로 읽는다.** 목록을 두 벌로 두지 않는다 |
|
||||||
|
|
||||||
|
- 이 자리는 **도시를 소개하는 자리가 아니다** — 세 장만 화면에 선다. 소개는 '군산 읽기' 가 한다.
|
||||||
|
- 날짜는 **고르게 편다**(39장 → 9일 간격). 곡이 나온 날·작가의 생일에 맞추고 싶지만
|
||||||
|
그 날짜를 출처로 확인한 것이 없다 — 지어낸 날짜는 해마다 틀린다.
|
||||||
|
- 오늘 자리가 비면 렌더러가 **다음 장**을 편다(`DailySection.tsx`). 365장을 채우려면
|
||||||
|
같은 글을 여러 날짜에 복사해야 하는데, 인용되려고 만든 사이트에서 할 일이 아니다.
|
||||||
|
- 받은 자료(`gunsan_365_story_db.xlsx`)를 그대로 싣지 않는 이유는 `build_daily.py` 머리말에 있다.
|
||||||
|
|
||||||
### 가요 다방 · 인물 열전 → `build_story.py`
|
### 가요 다방 · 인물 열전 → `build_story.py`
|
||||||
|
|
||||||
| 자리 | 줄 | 무엇 |
|
| 자리 | 줄 | 무엇 |
|
||||||
@ -685,13 +832,13 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
- ★ 이름만으로 커먼즈를 검색하지 않는다(**1.3 T3**).
|
- ★ 이름만으로 커먼즈를 검색하지 않는다(**1.3 T3**).
|
||||||
- 숙소가 만든 곡은 이 섹션에 넣지 않는다. 도시의 노래를 모으는 자리다.
|
- 숙소가 만든 곡은 이 섹션에 넣지 않는다. 도시의 노래를 모으는 자리다.
|
||||||
|
|
||||||
### 객실 사진 → `patch_stay.py` `ROOM_PHOTOS`(`:142`) · `rooms.json`
|
### 객실 사진 → `patch_stay.py` `ROOM_PHOTOS`(`:267`) · `rooms.json`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.4 규칙 16종 — 어기면 빌드가 멈춘다
|
## 2.4 규칙 17종 — 어기면 빌드가 멈춘다
|
||||||
|
|
||||||
`build_itinerary.py` 의 `audit()`(`:526`)이 매 실행 검사한다.
|
`build_itinerary.py` 의 `audit()` 가 매 실행 검사한다.
|
||||||
하나라도 어기면 **payload 파일을 쓰지 않고** 위반 목록을 찍고 멈춘다.
|
하나라도 어기면 **payload 파일을 쓰지 않고** 위반 목록을 찍고 멈춘다.
|
||||||
|
|
||||||
| | 규칙 | 이 규칙이 없어서 났던 일 |
|
| | 규칙 | 이 규칙이 없어서 났던 일 |
|
||||||
@ -712,24 +859,91 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
| 14 | 아이 동반 테마에 술집 금지 | |
|
| 14 | 아이 동반 테마에 술집 금지 | |
|
||||||
| **15** | **끼니 사이 3시간 이상** | 늦은 점심 15:47 종료 → 저녁 17:30. 창은 둘 다 합법인데 배가 안 꺼진다 |
|
| **15** | **끼니 사이 3시간 이상** | 늦은 점심 15:47 종료 → 저녁 17:30. 창은 둘 다 합법인데 배가 안 꺼진다 |
|
||||||
| **16** | **볼거리는 편도 이동보다 오래 머문다**(끼니 제외) | 사장님 지적 — 밥집은 목적지가 아니라 돌아오는 길에 들르는 자리라 뺀다 |
|
| **16** | **볼거리는 편도 이동보다 오래 머문다**(끼니 제외) | 사장님 지적 — 밥집은 목적지가 아니라 돌아오는 길에 들르는 자리라 뺀다 |
|
||||||
|
| **17** | **떠나는 날의 끝맺음이 테마와 맞는다** — 차 테마는 복귀 칸이 없고, 도보 테마는 짐을 찾는 `마무리` 칸으로 끝난다 | 2026-09-14. 한쪽만 고치면 차인데 숙소로 되돌아오거나(한 번 더 들르기) 맡긴 짐을 못 찾고 끝난다 — 둘 다 화면만 봐서는 멀쩡하다 |
|
||||||
|
|
||||||
★ **왜 빌드 안인가** — 전에는 이 검사를 매번 따로 쓴 일회용 스크립트로 돌렸다.
|
★ **왜 빌드 안인가** — 전에는 이 검사를 매번 따로 쓴 일회용 스크립트로 돌렸다.
|
||||||
그래서 뼈대를 고칠 때마다 "이번에 안 본 규칙"이 생겼고 사장님이 화면에서 먼저 찾으셨다.
|
그래서 뼈대를 고칠 때마다 "이번에 안 본 규칙"이 생겼고 사장님이 화면에서 먼저 찾으셨다.
|
||||||
**검사가 빌드의 일부가 아니면 검사한 적 없는 것과 같다.**
|
**검사가 빌드의 일부가 아니면 검사한 적 없는 것과 같다.**
|
||||||
|
|
||||||
`audit-all.mjs` 는 그다음 단계다 — 구워진 HTML 을 **실제 브라우저로 열어** 34개 항목을 센다.
|
`audit-all.mjs` 는 그다음 단계다 — 구워진 HTML 을 **실제 브라우저로 열어** 47개 항목을 센다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.5 주입분(`inject.js` · `inject.css`) 다섯
|
## 2.5 주입분(`inject.js` · `inject.css`) 아홉
|
||||||
|
|
||||||
렌더러에 없어서 이 목업에만 얹은 것이다. 제품으로 옮길 자리는 1부에 적었다.
|
렌더러에 없어서 이 목업에만 얹은 것이다. 제품으로 옮길 자리는 1부에 적었다.
|
||||||
|
|
||||||
1. **캐치프레이즈 순환** — React 가 그린 `<p>` 를 찾아 단어 단위로 갈아 끼운다 → T1
|
1. **캐치프레이즈 순환** — 대표 문구 `<p>` 를 찾아 **그 아래에 줄(`#w4d-sub`)을 붙이고** 단어 단위로 갈아 끼운다. 대표 문구는 안 건드린다 → T1
|
||||||
2. **헤더 미니 플레이어** — 카세트 아이콘 + 재생/멈춤 + 목록. 자작곡 5곡 → T4
|
2. **헤더 미니 플레이어** — 카세트 아이콘 + 재생/멈춤 + 목록. 자작곡 5곡.
|
||||||
3. **링크 걸기** — 일정 정거장 187개 · 엽서 4개를 네이버 지도로
|
**자동재생**은 걸어 보고, 브라우저가 막으면 첫 손길에 켠다(**1.3 T4**) → T4
|
||||||
4. **사진 저작자 표시** — 이 목업이 물고 있는 번들이 옛 판이라 `imageCredit` 을 안 그린다 → T3
|
3. **군산 읽기** — 렌더러에 없는 섹션. '군산 이야기' 탭 묶음의 **여섯 번째 탭**으로 들어가
|
||||||
5. **카로셀 제어** — 아래
|
34꼭지 중 5~6개를 매번 무작위로 카로셀(embla, 한 슬라이드에 한 꼭지) 로 보여준다.
|
||||||
|
`startReading()` 이 story 탭 패널들의 마지막(postcard) 뒤에 자기 패널을 꽂고,
|
||||||
|
자기 탭 버튼은 진짜 탭들의 `[role="tablist"]` 끝에 붙인다 → **T7**
|
||||||
|
- 처음엔 "신문 조판"(제호·면·단)으로 시작했다가, 3~4개씩 갈래별로 뽑아 한 카로셀에
|
||||||
|
잇는 중간 단계를 거쳐, 지금은 갈래를 안 가리고 **전체에서 5~6개**로 정착했다
|
||||||
|
(2026-09-14 하루 안에서: "신문처럼" → "포스트 하나씩만…나랑 장난하자는거?" →
|
||||||
|
"문학면? 말이 안 되는거 같네" → "34개중 15~19개인데 왜 34개라고 써놔?" →
|
||||||
|
"그냥 5-6개 정도만…랜덤으로").
|
||||||
|
- ★ **탭 전환 버그** (2026-09-14 대표 제보: "시간의 골목 누르면 사라지기만 함,
|
||||||
|
탭활성화도 그렇고"). 진짜 탭 다섯 개의 `hidden`·`aria-selected`·style 은 전부
|
||||||
|
React 가 `active` state 로 매번 다시 계산해 주는 값인데, 예전 코드가 내 탭으로
|
||||||
|
넘어갈 때 그 값들을 **직접 덮어썼다.** 그 뒤 손님이 (내 탭에 있다가) **직전까지
|
||||||
|
활성이던 그 탭을 다시 누르면** `setActive(sameIndex)` 는 React 입장에서 상태가
|
||||||
|
안 바뀐 것이라 **리렌더를 생략한다**(bail-out) — 내가 덮은 값이 영영 안 돌아왔다.
|
||||||
|
→ **React 가 매 렌더 계산해 주는 속성은 아예 안 건드린다.** 대신 내가 전적으로
|
||||||
|
소유한 `style.display`(JSX 에 없는 속성이라 React 가 절대 안 건드린다)로만 가리고,
|
||||||
|
되돌아올 때는 그 오버라이드만 지운다 — `hidden` 은 그동안 한 번도 틀린 적이 없으니
|
||||||
|
지우자마자 바로 맞다. 방금 누른 진짜 탭의 스타일도 직접 켠다(React 리렌더가
|
||||||
|
생략돼도 항상 맞게).
|
||||||
|
4. **링크 걸기** — 일정 정거장 187개 · 엽서 4개를 네이버 지도로
|
||||||
|
5. **사진 저작자 표시** — 이 목업이 물고 있는 번들이 옛 판이라 `imageCredit` 을 안 그린다 → T3
|
||||||
|
6. **카로셀 제어** — 아래
|
||||||
|
7. **날씨 카드 문구 순환** (`startWeatherNote`) — 하늘 5 · 기온대 5, 케이스마다 다섯 벌을
|
||||||
|
자루에서 뽑아 20초마다 갈아 끼운다. 렌더러는 케이스마다 한 칸씩만 읽는다.
|
||||||
|
하늘이 다섯인 것도 여기서만 가능하다 — 카드는 넷으로만 가르므로 구름많음(1~3)과
|
||||||
|
그 외(안개·소나기·뇌우…)가 `notes.흐림` **한 칸**을 같이 본다 → **2.3**
|
||||||
|
8. **가요 다방 판꽂이** (`inject.css` + `inject.js` `startSongsRail()`) — 렌더러는 판을
|
||||||
|
`flex-wrap` 으로 편다. 4곡일 때 만든 모양이라 25곡이면 세 줄, 50곡이면 여섯 줄짜리
|
||||||
|
음반 진열대가 된다. 한 줄 가로 스크롤로 눕힌다(넘치면 옆으로 민다). **곡명은 항상
|
||||||
|
보인다** — 렌더러가 원래 판 아래에 그리는 그 글자를 그대로 두고 줄 폭만 맞춘다.
|
||||||
|
★ 한 번 이 자리를 hover 로만 곡명이 뜨게 고친 적이 있다(2026-09-14, 판을 겹쳐 꽂고
|
||||||
|
손을 얹거나 고른 판에만 이름표를 띄우는 식). 데스크톱 벽은 없앴지만 모바일엔 hover 가
|
||||||
|
없어 **곡명이 아예 안 보이는 화면**이 됐다 — 데스크톱만 보고 고친 값이었다(같은 날 대표
|
||||||
|
지적으로 되돌림).
|
||||||
|
★ **화살표 + 실제 카로셀**(같은 날: "옆으로 갈수있는 버튼도 주고 slider.js 적용해줘").
|
||||||
|
렌더러가 이 판 목록을 `Carousel.tsx` 로 안 감싸서(그냥 `flex flex-wrap` 상자다)
|
||||||
|
"군산 읽기" 처럼 기존 `.slider-viewport` 를 마크업에서 못 가져다 쓴다 — 그래서
|
||||||
|
`startSongsRail()` 이 그 상자(`:has()` 로 잡는 판 목록 div) 하나를 통째로 새 상자로
|
||||||
|
감싸 `.slider-viewport` 를 붙이고, 그 위에 embla-carousel 코어(`vendor/embla-carousel.umd.js`,
|
||||||
|
군산 읽기와 같은 라이브러리)를 얹는다. **판 버튼 25개를 옮기지 않고 그 부모 하나만
|
||||||
|
옮긴다** — React 는 그 노드를 그대로 갖고 있으니(`setPlaying` 이 속성만 고친다) 감싼
|
||||||
|
뒤에도 클릭이 그대로 먹는다.
|
||||||
|
★ **마크업을 고치지 않는다**(React 가 되돌린다) — `:has()` 로 판이 든 상자만 집는다.
|
||||||
|
→ 렌더러도 같은 문제다(**3.4**)
|
||||||
|
9. **엽서 쓰기** (`postcardMaker()`, `#w4d-pm-section`) — "오늘의 엽서" 탭을 비우고
|
||||||
|
(`patch_stay.py` 가 `postcard` 섹션 `data` 를 빈 `items:[]` 로 덮는다), 그 자리 대신
|
||||||
|
**손님이 직접 문구를 써서 자기 것으로 저장·공유하는 카드**를 `festival` 섹션 바로 앞에
|
||||||
|
독립 섹션으로 얹는다(2026-09-14: "오늘의 엽서 칸을 빼고 이 엽서 포맷으로…이건 축제소개
|
||||||
|
전에 섹션으로 빼줘"). 캔버스(1080×1080)에 숙소 사진(`P.media` 전체, 카로셀로 고른다) +
|
||||||
|
문구 + 우표·소인을 그려 PNG 로 만들고, `navigator.share({files:[...]})` (Web Share API)
|
||||||
|
로 공유하거나 다운로드한다 — 새 익명 업로드 백엔드를 만들지 않으려고 파일 공유 쪽을 골랐다
|
||||||
|
(`solution/backend` 미디어 라우터는 전부 로그인 필요, `IsValidAccessToken`).
|
||||||
|
- 공유 버튼 문구는 기기별로 다르다 — `matchMedia('(pointer: coarse)')` 로 터치 기기에서만
|
||||||
|
"카카오톡 등으로 공유", 나머지(주로 macOS 데스크톱)는 "공유하기". macOS 는 카카오톡이
|
||||||
|
시스템 공유 확장을 안 만들어 시트에 메시지·메모 정도만 뜨는 게 OS 사양이라 그렇다.
|
||||||
|
- 상호명(placeName)·소인 텍스트·플레이스홀더 전부 하드코딩 안 한다 — 다른 숙소·카페도
|
||||||
|
이 스크립트를 그대로 쓸 수 있어야 해서다("스테이머뭄뿐만 아니라 다른 숙박업소나 카페도
|
||||||
|
써야 하는데"). 소인은 `stampLines(name, maxWidth)` 가 콤마·공백 경계로, 안 되면
|
||||||
|
글자 단위로 접는다.
|
||||||
|
- ★ **줄바꿈이 폭을 안 넘던 버그** (2026-09-14 대표 제보 — 스크린샷: 공백 없이 이어 친
|
||||||
|
글자열이 우표 칸까지 넘쳐 나감). `wrapLines()` 가 **공백 단위로만** 접어서, 단어 하나가
|
||||||
|
이미 칸보다 넓으면 아예 안 잘랐다. → `stampLines()` 와 같은 글자 단위 폴백을 추가했다.
|
||||||
|
- ★ **4줄 제한** (2026-09-14 대표: "4줄이상 못쓰게 막으라고"). 글자 수로는 몇 줄이 될지
|
||||||
|
못 잰다(줄바꿈은 폭·서체에 달렸다) — `textarea` 의 `input` 이벤트마다 `wrapLines()` 로
|
||||||
|
**실제로 접히는 줄 수**를 재서, 4줄을 넘으면 방금 입력을 되돌린다(엔터를 여러 번 눌러도,
|
||||||
|
자음만 이어 쳐도 똑같이 막힌다). `draw()` 쪽에도 4줄로 자르고 말줄임표(…)를 붙이는
|
||||||
|
방어선을 하나 더 둔다 — 붙여넣기처럼 입력 쪽 검사를 안 거치는 경로까지 대비한다.
|
||||||
|
|
||||||
### 카로셀 제어 (`tameRails`)
|
### 카로셀 제어 (`tameRails`)
|
||||||
|
|
||||||
@ -765,7 +979,7 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
★ **React 가 다시 그리면 사라진다.** 그래서
|
★ **React 가 다시 그리면 사라진다.** 그래서
|
||||||
- 플레이어 목록·패널은 `#root` **밖**(body 직속)에 둔다
|
- 플레이어 목록·패널은 `#root` **밖**(body 직속)에 둔다
|
||||||
- 헤더에 꽂는 단추는 `MutationObserver` 로 감시해 다시 꽂는다
|
- 헤더에 꽂는 단추는 `MutationObserver` 로 감시해 다시 꽂는다
|
||||||
- 캐치프레이즈는 노드를 들고 있지 않고 **매 순환마다 다시 찾는다**
|
- 캐치프레이즈 줄은 노드를 들고 있지 않고 **매 순환마다 붙어 있는지 보고, 없으면 대표 문구를 다시 찾아 새로 붙인다**
|
||||||
|
|
||||||
★ **`window.load` 를 기다리지 않는다.** 경로 지도 때문에 OSM 타일 `<img>` 가 140장이라
|
★ **`window.load` 를 기다리지 않는다.** 경로 지도 때문에 OSM 타일 `<img>` 가 140장이라
|
||||||
`load` 는 그게 다 끝나야 나온다 — 타일이 느리면 영원히 안 뜬다.
|
`load` 는 그게 다 끝나야 나온다 — 타일이 느리면 영원히 안 뜬다.
|
||||||
@ -787,6 +1001,15 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
바꾸는 것이라, SSR 마크업과 `llms.txt` 는 구운 날 그대로다. 시연에는 문제없지만
|
바꾸는 것이라, SSR 마크업과 `llms.txt` 는 구운 날 그대로다. 시연에는 문제없지만
|
||||||
검색·AI 에 인용되는 값은 아니다.
|
검색·AI 에 인용되는 값은 아니다.
|
||||||
- **`og:image`** 도 마찬가지다. 화면은 새 사진, 카톡 공유 썸네일만 옛것이다.
|
- **`og:image`** 도 마찬가지다. 화면은 새 사진, 카톡 공유 썸네일만 옛것이다.
|
||||||
|
- **React 가 `hidden`·`aria-selected`·style 로 계산해 주는 속성은 주입분이 직접 안 건드린다.**
|
||||||
|
건드리면 그 값과 React 내부 상태(`useState`)가 어긋나고, 손님이 **같은 값으로 되돌리는
|
||||||
|
클릭**(예: 방금까지 활성이던 탭을 다시 누름)을 하면 `setState` 가 같은 값이라 **리렌더를
|
||||||
|
생략**해 버려(bail-out) 주입분이 덮은 값이 영영 안 돌아온다(2026-09-14, 군산 읽기 탭
|
||||||
|
버그 — **2.5 항목 3**). React 가 그 요소에 아예 안 쓰는 속성(`style.display` 등)으로만
|
||||||
|
가리고, 걷을 때는 그 오버라이드만 지운다.
|
||||||
|
- **캔버스 줄바꿈은 공백 기준만으로는 모자란다.** 단어 하나가 이미 칸보다 넓으면
|
||||||
|
(예: 공백 없이 이어 친 자·모음) 안 잘리고 그대로 넘친다 — 글자 단위 폴백을 같이 둬야
|
||||||
|
한다(`wrapLines()`·`stampLines()`, **2.5 항목 9**).
|
||||||
|
|
||||||
---
|
---
|
||||||
---
|
---
|
||||||
@ -799,7 +1022,7 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
|
|
||||||
1. **실행하지 않은 것을 됐다고 쓰지 않는다.** 코드에서 빠진 것을 보는 건 확인이 아니다.
|
1. **실행하지 않은 것을 됐다고 쓰지 않는다.** 코드에서 빠진 것을 보는 건 확인이 아니다.
|
||||||
화면을 열어 세거나 명령을 돌려서 **숫자·출력**을 얻은 뒤에 말한다.
|
화면을 열어 세거나 명령을 돌려서 **숫자·출력**을 얻은 뒤에 말한다.
|
||||||
2. **"했다" 옆에 무엇으로 확인했는지 같이 쓴다.** `audit-all.mjs 34/34` · `md5 일치` ·
|
2. **"했다" 옆에 무엇으로 확인했는지 같이 쓴다.** `audit-all.mjs 38/38` · `md5 일치` ·
|
||||||
`20초 동안 0px` 처럼. 근거를 못 쓰면 아직 안 끝난 것이다.
|
`20초 동안 0px` 처럼. 근거를 못 쓰면 아직 안 끝난 것이다.
|
||||||
3. **절차를 안내할 때는 그 절차를 먼저 밟아 본다.**
|
3. **절차를 안내할 때는 그 절차를 먼저 밟아 본다.**
|
||||||
4. **일부만 했으면 일부만 했다고 쓴다.** 못 한 것을 빼고 요약하지 않는다.
|
4. **일부만 했으면 일부만 했다고 쓴다.** 못 한 것을 빼고 요약하지 않는다.
|
||||||
@ -861,15 +1084,19 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `README.md` | **이 문서.** 지침은 전부 여기 |
|
| `README.md` | **이 문서.** 지침은 전부 여기 |
|
||||||
| `TEXT.md` | 화면에 나가는 텍스트 전문 (생성물 — `python3 dump_text.py`) |
|
| `TEXT.md` | 화면에 나가는 텍스트 전문 (생성물 — `python3 dump_text.py`) |
|
||||||
| `build_itinerary.py` | 여행 일정 계산 + 규칙 16종 감사 |
|
| `build_itinerary.py` | 여행 일정 계산 + 규칙 17종 감사 |
|
||||||
| `build_story.py` | 가요 다방 · 인물 열전 + 위키 사진 |
|
| `build_story.py` | 가요 다방 · 인물 열전 + 위키 사진 |
|
||||||
| `patch_stay.py` | 캐치프레이즈 · 객실 · 자작곡 · HTML 조립 · 자산 경로 교정 |
|
| `build_reading.py` | 군산 읽기 — 카로셀 34꼭지(문학·섬과 바다·역사·장소·음식), 화면엔 매번 5~6개 무작위 |
|
||||||
|
| `build_daily.py` | 오늘의 한 장 — 노래·문학 일력 39장 |
|
||||||
|
| `patch_stay.py` | 캐치프레이즈 · 주변 맛집 · 객실 · 자작곡 · HTML 조립 · 자산 경로 교정 |
|
||||||
|
| `fetch_restaurants.py` | 주변 맛집을 TourAPI 2km 로 다시 받아 `restaurants.json` · 사진은 `img/mirror/` (굽기와 따로, 한 번만) |
|
||||||
|
| `fill_restaurant_photos.py` | TourAPI 에 사진이 없는 맛집 11곳 사진을 받아 1600px 로 줄여 둔다 — ⚠ **공공누리 아님, 시연본 전용** |
|
||||||
| `dump_text.py` | `TEXT.md` 생성 |
|
| `dump_text.py` | `TEXT.md` 생성 |
|
||||||
| `inject.css` · `inject.js` | 화면에 덧대는 다섯 가지 |
|
| `inject.css` · `inject.js` | 화면에 덧대는 다섯 가지 |
|
||||||
| `audit-all.mjs` | 브라우저로 34개 항목 전수 점검 |
|
| `audit-all.mjs` | 브라우저로 47개 항목 전수 점검 |
|
||||||
| `rails-test.mjs` | 카로셀 13개 × 데스크톱·모바일 |
|
| `rails-test.mjs` | 카로셀 13개 × 데스크톱·모바일. 모바일에는 이름 없는 가로 상자 3개가 더 잡힌다 — 그중 하나가 가요 다방 판꽂이다(자동으로 안 넘어가므로 전 칸 0 이 맞다) |
|
||||||
| `kingcheck.mjs` | 킹서버 반영분 확인 |
|
| `kingcheck.mjs` | 킹서버 반영분 확인 |
|
||||||
| `audit_schedule.py` | 일정 전수 검수표 생성 → `SCHEDULE.md` (규칙이 안 보는 논리 12가지) |
|
| `audit_schedule.py` | 일정 전수 검수표 생성 → `SCHEDULE.md` (규칙이 안 보는 논리 13가지) |
|
||||||
| `SCHEDULE.md` | 그 결과물 (생성물) |
|
| `SCHEDULE.md` | 그 결과물 (생성물) |
|
||||||
|
|
||||||
### 원본 · 원자료 (건드리지 않는다)
|
### 원본 · 원자료 (건드리지 않는다)
|
||||||
@ -878,6 +1105,8 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `stay-payload.json` · `orig/index.html` | **원본.** 되돌릴 때 기준 |
|
| `stay-payload.json` · `orig/index.html` | **원본.** 되돌릴 때 기준 |
|
||||||
| `rooms.json` | 야놀자 등록본에서 뽑은 객실 사진 목록 (A동 12 · B동 10) |
|
| `rooms.json` | 야놀자 등록본에서 뽑은 객실 사진 목록 (A동 12 · B동 10) |
|
||||||
|
| `restaurants.json` | 주변 맛집 31곳 — 원본 8 + TourAPI 2km 23 (사진 20) |
|
||||||
|
| `restaurant-photos.json` | 사진 없던 맛집 11곳의 사진 출처 주소·게시물 제목. 굽기가 사진 없는 집에만 덧댄다 |
|
||||||
| `people-photos.json` · `people-images.json` · `people-images-meta.json` | 위키 문서 사진 + 라이선스 |
|
| `people-photos.json` · `people-images.json` · `people-images-meta.json` | 위키 문서 사진 + 라이선스 |
|
||||||
| `people-raw.json` · `people-rows.json` | 인물 수집 원자료 |
|
| `people-raw.json` · `people-rows.json` | 인물 수집 원자료 |
|
||||||
| `coords.json` | 카카오 로컬에서 받은 좌표 |
|
| `coords.json` | 카카오 로컬에서 받은 좌표 |
|
||||||
@ -890,7 +1119,7 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
|||||||
| `img/people/` | 17장 | 위키 사진. 저작자 표시가 라이선스 조건이라 같이 관리한다 |
|
| `img/people/` | 17장 | 위키 사진. 저작자 표시가 라이선스 조건이라 같이 관리한다 |
|
||||||
| `img/mirror/` | 61장 | 히어로·소개·객실. 없으면 **다른 기계에서 흰 화면** |
|
| `img/mirror/` | 61장 | 히어로·소개·객실. 없으면 **다른 기계에서 흰 화면** |
|
||||||
| `audio/` | 5곡 | 사장님 자작곡 |
|
| `audio/` | 5곡 | 사장님 자작곡 |
|
||||||
| `vendor/` | 2개 | 번들 css/js. 해시가 달라도 뜨게 하려고 |
|
| `vendor/` | 3개 | 번들 css/js(해시가 달라도 뜨게 하려고) + `embla-carousel.umd.js`(군산 읽기·가요 다방 카로셀이 쓰는 코어, `node_modules/embla-carousel` 그대로 복사) |
|
||||||
|
|
||||||
합쳐서 약 77M 이다. 전부 **한 번 넣고 안 바뀌는 파일**이라 git 이 싫어하는 경우가 아니다.
|
합쳐서 약 77M 이다. 전부 **한 번 넣고 안 바뀌는 파일**이라 git 이 싫어하는 경우가 아니다.
|
||||||
자산은 커밋을 따로 떼어 뒀다 — 히스토리에서 덩치를 걷어낼 일이 생기면 그 커밋만 건드린다.
|
자산은 커밋을 따로 떼어 뒀다 — 히스토리에서 덩치를 걷어낼 일이 생기면 그 커밋만 건드린다.
|
||||||
@ -935,4 +1164,7 @@ ssh King_admin "rm -rf ~/data2/o2o-site-AEO/tmp-stay-deploy"
|
|||||||
| 인물 사진 18/57 | 위키 문서에 사진이 있는 사람이 18명뿐이다. 동명이인 함정 때문에 이름 검색으로 늘리지 않는다 |
|
| 인물 사진 18/57 | 위키 문서에 사진이 있는 사람이 18명뿐이다. 동명이인 함정 때문에 이름 검색으로 늘리지 않는다 |
|
||||||
| 렌더러의 카로셀 자동 넘김 | 운영 사이트에 그대로 있다. 고치려면 `use-rail-autoplay.ts` 를 고치고 **전체 재굽기 + `republish_all.py`** |
|
| 렌더러의 카로셀 자동 넘김 | 운영 사이트에 그대로 있다. 고치려면 `use-rail-autoplay.ts` 를 고치고 **전체 재굽기 + `republish_all.py`** |
|
||||||
| ~~일정 정거장의 체류 대비 이동~~ | **해결(2026-09-11)** — 규칙 15·16 을 감사에 넣었다. 전수 검수 21/21 (`SCHEDULE.md`) |
|
| ~~일정 정거장의 체류 대비 이동~~ | **해결(2026-09-11)** — 규칙 15·16 을 감사에 넣었다. 전수 검수 21/21 (`SCHEDULE.md`) |
|
||||||
|
| 렌더러의 가요 다방 판 배치 | 운영 사이트도 `flex-wrap` 이라 곡이 늘면 진열대가 된다(`SongsSection.tsx`). 목업은 CSS 로 덮었다(**2.5** 7). 제품에 옮기려면 **전체 재굽기 + `republish_all.py`** |
|
||||||
|
| 날씨 문구를 만드는 경로 | 제품에는 없다 — `site_payload.py` `_weather()` 는 기온·하늘·관측시각만 싣는다. `notes`·`tempNotes` 는 이 목업에서 손으로 적은 것이고, 사이트마다 자동으로 나오려면 생성 잡이 하나 필요하다 |
|
||||||
|
| 오늘의 한 장 39장 (365일 중 39일) | 확인되는 소재가 거기까지다. 받은 `gunsan_365_story_db.xlsx` 는 365행이지만 실제 주제가 52개(한 주제를 운영 슬롯 7가지로 복제)이고 231행이 `검증필요`, 시·수필은 빈 슬롯이다. 더 채우려면 **군산을 소재로 한 시·수필을 출처와 함께 찾아야** 한다 |
|
||||||
| 일정을 이미지/링크로 공유 | 이미지 저장은 가능(카드를 PNG 로). 링크는 지금 섹션까지만 간다 — 탭·코스가 URL 에 안 남는다 |
|
| 일정을 이미지/링크로 공유 | 이미지 저장은 가능(카드를 PNG 로). 링크는 지금 섹션까지만 간다 — 탭·코스가 URL 에 안 남는다 |
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
# `/s/stay` 여행 일정 전수 검수표
|
# `/s/stay` 여행 일정 전수 검수표
|
||||||
|
|
||||||
생성 `python3 audit_schedule.py` · 테마 21개 · 53일 · 304정거장
|
생성 `python3 audit_schedule.py` · 테마 21개 · 53일 · 290정거장
|
||||||
|
|
||||||
표는 **구워진 payload 를 렌더러와 같은 방식으로 계산한 값**이다(화면에 뜨는 것과 같다).
|
표는 **구워진 payload 를 렌더러와 같은 방식으로 계산한 값**이다(화면에 뜨는 것과 같다).
|
||||||
규칙 14종(만들 때 거는 것)은 `build_itinerary.py` `audit()` 가 이미 통과시켰고,
|
규칙 17종(만들 때 거는 것)은 `build_itinerary.py` `audit()` 가 이미 통과시켰고,
|
||||||
이 표는 그 규칙이 **안 보는 논리**를 따로 센다.
|
이 표는 그 규칙이 **안 보는 논리**를 따로 센다.
|
||||||
|
|
||||||
## 결과 — 21/21 검수완료
|
## 결과 — 21/21 검수완료
|
||||||
|
|
||||||
**전부 통과.** 12개 항목 위반 0건.
|
**전부 통과.** 13개 항목 위반 0건.
|
||||||
|
|
||||||
| 코드 | 검수 항목 |
|
| 코드 | 검수 항목 |
|
||||||
|---|---|
|
|---|---|
|
||||||
@ -20,10 +20,11 @@
|
|||||||
| L5 | 바깥 정거장 체류 15분 이상 |
|
| L5 | 바깥 정거장 체류 15분 이상 |
|
||||||
| L6 | 하루가 14시간을 넘지 않는다 |
|
| L6 | 하루가 14시간을 넘지 않는다 |
|
||||||
| L7 | 입실 15:00 이후 · 퇴실 11:00 전 |
|
| L7 | 입실 15:00 이후 · 퇴실 11:00 전 |
|
||||||
| L8 | 하루의 첫 칸·마지막 칸이 숙소 |
|
| L8 | 하루의 첫 칸·마지막 칸이 숙소 (**떠나는 날 차 테마는 복귀 없이 끝난다**) |
|
||||||
| L9 | 같은 자리가 연달아 서지 않는다 |
|
| L9 | 같은 자리가 연달아 서지 않는다 |
|
||||||
| L10 | 한 테마 안에서 같은 곳을 두 번 가지 않는다 |
|
| L10 | 한 테마 안에서 같은 곳을 두 번 가지 않는다 |
|
||||||
| L11 | 걷는 테마에 편도 25분 넘는 이동이 없다 |
|
| L11 | 걷는 테마에 편도 25분 넘는 이동이 없다 |
|
||||||
|
| L12 | 떠나는 날의 끝맺음이 퇴실 문구와 같다 — 짐을 차에 실었으면 안 돌아오고, 맡겼으면 찾으러 온다 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -44,14 +45,13 @@
|
|||||||
| 5 | 18:45–19:25 | 밤 산책 | 군산 내항 | 5분 | 40분 |
|
| 5 | 18:45–19:25 | 밤 산책 | 군산 내항 | 5분 | 40분 |
|
||||||
| 6 | 19:41–20:11 | 머뭄으로 | 스테이 머뭄 | 16분 | 30분 |
|
| 6 | 19:41–20:11 | 머뭄으로 | 스테이 머뭄 | 16분 | 30분 |
|
||||||
|
|
||||||
**둘째 날** 08:20–16:36 (총 8시간 16분 · 이동 176분 · 정거장 4)
|
**둘째 날** 08:20–15:49 (총 7시간 29분 · 이동 169분 · 정거장 3)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| 1 | 08:20–09:35 | 아침 | 스테이 머뭄 | –분 | 75분 |
|
| 1 | 08:20–09:35 | 아침 | 스테이 머뭄 | –분 | 75분 |
|
||||||
| 2 | 10:59–13:29 | 오전 | 고군산군도 선유도 | 84분 | 150분 |
|
| 2 | 10:59–13:29 | 오전 | 고군산군도 선유도 | 84분 | 150분 |
|
||||||
| 3 | 14:54–15:49 | 늦은 점심 | 국제반점 | 85분 | 55분 |
|
| 3 | 14:54–15:49 | 늦은 점심 | 국제반점 | 85분 | 55분 |
|
||||||
| 4 | 15:56–16:36 | 마무리 | 스테이 머뭄 | 7분 | 40분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -101,7 +101,7 @@
|
|||||||
| 5 | 19:33–20:12 | 밤 산책 | 군산 내항 | 5분 | 39분 |
|
| 5 | 19:33–20:12 | 밤 산책 | 군산 내항 | 5분 | 39분 |
|
||||||
| 6 | 20:28–20:57 | 머뭄으로 | 스테이 머뭄 | 16분 | 29분 |
|
| 6 | 20:28–20:57 | 머뭄으로 | 스테이 머뭄 | 16분 | 29분 |
|
||||||
|
|
||||||
**둘째 날** 09:40–15:36 (총 5시간 56분 · 이동 52분 · 정거장 5)
|
**둘째 날** 09:40–14:31 (총 4시간 51분 · 이동 33분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -109,7 +109,6 @@
|
|||||||
| 2 | 11:10–12:02 | 오전 | 군산근대건축관 | 10분 | 52분 |
|
| 2 | 11:10–12:02 | 오전 | 군산근대건축관 | 10분 | 52분 |
|
||||||
| 3 | 12:10–13:07 | 점심 | 일흥옥 | 8분 | 57분 |
|
| 3 | 12:10–13:07 | 점심 | 일흥옥 | 8분 | 57분 |
|
||||||
| 4 | 13:22–14:31 | 오후 | 째보선창 근처 카페 | 15분 | 69분 |
|
| 4 | 13:22–14:31 | 오후 | 째보선창 근처 카페 | 15분 | 69분 |
|
||||||
| 5 | 14:50–15:36 | 마무리 | 스테이 머뭄 | 19분 | 46분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -130,7 +129,7 @@
|
|||||||
| 5 | 18:52–19:30 | 밤 산책 | 군산 내항 | 11분 | 38분 |
|
| 5 | 18:52–19:30 | 밤 산책 | 군산 내항 | 11분 | 38분 |
|
||||||
| 6 | 19:46–20:14 | 머뭄으로 | 스테이 머뭄 | 16분 | 28분 |
|
| 6 | 19:46–20:14 | 머뭄으로 | 스테이 머뭄 | 16분 | 28분 |
|
||||||
|
|
||||||
**둘째 날** 08:50–13:35 (총 4시간 45분 · 이동 37분 · 정거장 5)
|
**둘째 날** 08:50–12:49 (총 3시간 59분 · 이동 29분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -138,7 +137,6 @@
|
|||||||
| 2 | 10:12–11:22 | 오전 | 군산근대미술관 | 9분 | 70분 |
|
| 2 | 10:12–11:22 | 오전 | 군산근대미술관 | 9분 | 70분 |
|
||||||
| 3 | 11:30–12:18 | 점심 | 일흥옥 | 8분 | 48분 |
|
| 3 | 11:30–12:18 | 점심 | 일흥옥 | 8분 | 48분 |
|
||||||
| 4 | 12:30–12:49 | 오후 | 해망굴 | 12분 | 19분 |
|
| 4 | 12:30–12:49 | 오후 | 해망굴 | 12분 | 19분 |
|
||||||
| 5 | 12:57–13:35 | 마무리 | 스테이 머뭄 | 8분 | 38분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -188,7 +186,7 @@
|
|||||||
| 5 | 18:42–19:20 | 밤 산책 | 군산 내항 | 9분 | 38분 |
|
| 5 | 18:42–19:20 | 밤 산책 | 군산 내항 | 9분 | 38분 |
|
||||||
| 6 | 19:36–20:04 | 머뭄으로 | 스테이 머뭄 | 16분 | 28분 |
|
| 6 | 19:36–20:04 | 머뭄으로 | 스테이 머뭄 | 16분 | 28분 |
|
||||||
|
|
||||||
**둘째 날** 08:30–14:06 (총 5시간 36분 · 이동 36분 · 정거장 5)
|
**둘째 날** 08:30–13:21 (총 4시간 51분 · 이동 29분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -196,7 +194,6 @@
|
|||||||
| 2 | 09:59–11:19 | 오전 | 경암동 철길마을 | 12분 | 80분 |
|
| 2 | 09:59–11:19 | 오전 | 경암동 철길마을 | 12분 | 80분 |
|
||||||
| 3 | 11:30–12:18 | 점심 | 일흥옥 | 11분 | 48분 |
|
| 3 | 11:30–12:18 | 점심 | 일흥옥 | 11분 | 48분 |
|
||||||
| 4 | 12:24–13:21 | 오후 | 군산 시간여행마을 | 6분 | 57분 |
|
| 4 | 12:24–13:21 | 오후 | 군산 시간여행마을 | 6분 | 57분 |
|
||||||
| 5 | 13:28–14:06 | 마무리 | 스테이 머뭄 | 7분 | 38분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -246,7 +243,7 @@
|
|||||||
| 5 | 18:46–19:30 | 밤 산책 | 군산 내항 | 11분 | 44분 |
|
| 5 | 18:46–19:30 | 밤 산책 | 군산 내항 | 11분 | 44분 |
|
||||||
| 6 | 19:46–20:19 | 머뭄으로 | 스테이 머뭄 | 16분 | 33분 |
|
| 6 | 19:46–20:19 | 머뭄으로 | 스테이 머뭄 | 16분 | 33분 |
|
||||||
|
|
||||||
**둘째 날** 09:00–14:40 (총 5시간 40분 · 이동 60분 · 정거장 5)
|
**둘째 날** 09:00–13:43 (총 4시간 43분 · 이동 47분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -254,7 +251,6 @@
|
|||||||
| 2 | 10:40–11:46 | 오전 | 채만식문학관 | 18분 | 66분 |
|
| 2 | 10:40–11:46 | 오전 | 채만식문학관 | 18분 | 66분 |
|
||||||
| 3 | 12:03–12:58 | 점심 | 일흥옥 | 17분 | 55분 |
|
| 3 | 12:03–12:58 | 점심 | 일흥옥 | 17분 | 55분 |
|
||||||
| 4 | 13:10–13:43 | 오후 | 구 군산세관 본관 | 12분 | 33분 |
|
| 4 | 13:10–13:43 | 오후 | 구 군산세관 본관 | 12분 | 33분 |
|
||||||
| 5 | 13:56–14:40 | 마무리 | 스테이 머뭄 | 13분 | 44분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -275,7 +271,7 @@
|
|||||||
| 5 | 18:30–19:04 | 밤 산책 | 군산 내항 | 13분 | 34분 |
|
| 5 | 18:30–19:04 | 밤 산책 | 군산 내항 | 13분 | 34분 |
|
||||||
| 6 | 19:20–19:46 | 머뭄으로 | 스테이 머뭄 | 16분 | 26분 |
|
| 6 | 19:20–19:46 | 머뭄으로 | 스테이 머뭄 | 16분 | 26분 |
|
||||||
|
|
||||||
**둘째 날** 10:10–14:10 (총 4시간 0분 · 이동 42분 · 정거장 5)
|
**둘째 날** 10:10–13:27 (총 3시간 17분 · 이동 33분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -283,7 +279,6 @@
|
|||||||
| 2 | 11:12–11:54 | 오전 | 경암동 철길마을 | 12분 | 42분 |
|
| 2 | 11:12–11:54 | 오전 | 경암동 철길마을 | 12분 | 42분 |
|
||||||
| 3 | 12:05–12:35 | 점심 | 이성당 | 11분 | 30분 |
|
| 3 | 12:05–12:35 | 점심 | 이성당 | 11분 | 30분 |
|
||||||
| 4 | 12:45–13:27 | 오후 | 월명호수 | 10분 | 42분 |
|
| 4 | 12:45–13:27 | 오후 | 월명호수 | 10분 | 42분 |
|
||||||
| 5 | 13:36–14:10 | 마무리 | 스테이 머뭄 | 9분 | 34분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -301,7 +296,7 @@
|
|||||||
| 2 | 19:05–20:20 | 저녁 | 명월갈비 | 5분 | 75분 |
|
| 2 | 19:05–20:20 | 저녁 | 명월갈비 | 5분 | 75분 |
|
||||||
| 3 | 20:25–20:55 | 머뭄으로 | 스테이 머뭄 | 5분 | 30분 |
|
| 3 | 20:25–20:55 | 머뭄으로 | 스테이 머뭄 | 5분 | 30분 |
|
||||||
|
|
||||||
**둘째 날** 08:40–14:39 (총 5시간 59분 · 이동 39분 · 정거장 6)
|
**둘째 날** 08:40–13:51 (총 5시간 11분 · 이동 31분 · 정거장 5)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -310,7 +305,6 @@
|
|||||||
| 3 | 11:42–12:32 | 점심 | 한일옥 | 6분 | 50분 |
|
| 3 | 11:42–12:32 | 점심 | 한일옥 | 6분 | 50분 |
|
||||||
| 4 | 12:36–13:21 | 오후 | 군산근대건축관 | 4분 | 45분 |
|
| 4 | 12:36–13:21 | 오후 | 군산근대건축관 | 4분 | 45분 |
|
||||||
| 5 | 13:31–13:51 | 오후 | 해망굴 | 10분 | 20분 |
|
| 5 | 13:31–13:51 | 오후 | 해망굴 | 10분 | 20분 |
|
||||||
| 6 | 13:59–14:39 | 마무리 | 스테이 머뭄 | 8분 | 40분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -382,7 +376,7 @@
|
|||||||
| 6 | 18:47–20:02 | 저녁 | 명월갈비 | 5분 | 75분 |
|
| 6 | 18:47–20:02 | 저녁 | 명월갈비 | 5분 | 75분 |
|
||||||
| 7 | 20:07–20:37 | 머뭄으로 | 스테이 머뭄 | 5분 | 30분 |
|
| 7 | 20:07–20:37 | 머뭄으로 | 스테이 머뭄 | 5분 | 30분 |
|
||||||
|
|
||||||
**셋째 날** 09:20–13:46 (총 4시간 26분 · 이동 19분 · 정거장 5)
|
**셋째 날** 09:20–12:59 (총 3시간 39분 · 이동 12분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -390,7 +384,6 @@
|
|||||||
| 2 | 10:41–11:26 | 오전 | 초원사진관 | 4분 | 45분 |
|
| 2 | 10:41–11:26 | 오전 | 초원사진관 | 4분 | 45분 |
|
||||||
| 3 | 11:30–12:20 | 점심 | 일흥옥 | 4분 | 50분 |
|
| 3 | 11:30–12:20 | 점심 | 일흥옥 | 4분 | 50분 |
|
||||||
| 4 | 12:24–12:59 | 오후 | 이성당 | 4분 | 35분 |
|
| 4 | 12:24–12:59 | 오후 | 이성당 | 4분 | 35분 |
|
||||||
| 5 | 13:06–13:46 | 마무리 | 스테이 머뭄 | 7분 | 40분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -466,7 +459,7 @@
|
|||||||
| 7 | 17:30–18:50 | 저녁 | 빈해원 | 11분 | 80분 |
|
| 7 | 17:30–18:50 | 저녁 | 빈해원 | 11분 | 80분 |
|
||||||
| 8 | 19:01–19:35 | 머뭄으로 | 스테이 머뭄 | 11분 | 34분 |
|
| 8 | 19:01–19:35 | 머뭄으로 | 스테이 머뭄 | 11분 | 34분 |
|
||||||
|
|
||||||
**셋째 날** 08:40–15:18 (총 6시간 38분 · 이동 32분 · 정거장 5)
|
**셋째 날** 08:40–14:21 (총 5시간 41분 · 이동 21분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -474,7 +467,6 @@
|
|||||||
| 2 | 10:30–11:20 | 오전 | 동국사 | 6분 | 50분 |
|
| 2 | 10:30–11:20 | 오전 | 동국사 | 6분 | 50분 |
|
||||||
| 3 | 11:30–12:33 | 점심 | 국제반점 | 10분 | 63분 |
|
| 3 | 11:30–12:33 | 점심 | 국제반점 | 10분 | 63분 |
|
||||||
| 4 | 12:38–14:21 | 오후 | 군산근대역사박물관 | 5분 | 103분 |
|
| 4 | 12:38–14:21 | 오후 | 군산근대역사박물관 | 5분 | 103분 |
|
||||||
| 5 | 14:32–15:18 | 마무리 | 스테이 머뭄 | 11분 | 46분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -505,7 +497,7 @@
|
|||||||
| 4 | 17:30–18:25 | 저녁 | 일흥옥 | 5분 | 55분 |
|
| 4 | 17:30–18:25 | 저녁 | 일흥옥 | 5분 | 55분 |
|
||||||
| 5 | 18:30–19:03 | 머뭄으로 | 스테이 머뭄 | 5분 | 33분 |
|
| 5 | 18:30–19:03 | 머뭄으로 | 스테이 머뭄 | 5분 | 33분 |
|
||||||
|
|
||||||
**셋째 날** 09:00–14:34 (총 5시간 34분 · 이동 27분 · 정거장 5)
|
**셋째 날** 09:00–13:33 (총 4시간 33분 · 이동 18분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -513,7 +505,6 @@
|
|||||||
| 2 | 10:44–11:30 | 오전 | 동국사 | 6분 | 46분 |
|
| 2 | 10:44–11:30 | 오전 | 동국사 | 6분 | 46분 |
|
||||||
| 3 | 11:33–12:19 | 점심 | 물밀소 | 3분 | 46분 |
|
| 3 | 11:33–12:19 | 점심 | 물밀소 | 3분 | 46분 |
|
||||||
| 4 | 12:28–13:33 | 오후 | 월명호수 | 9분 | 65분 |
|
| 4 | 12:28–13:33 | 오후 | 월명호수 | 9분 | 65분 |
|
||||||
| 5 | 13:42–14:34 | 마무리 | 스테이 머뭄 | 9분 | 52분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -547,7 +538,7 @@
|
|||||||
| 7 | 17:30–18:15 | 저녁 | 국제반점 | 7분 | 45분 |
|
| 7 | 17:30–18:15 | 저녁 | 국제반점 | 7분 | 45분 |
|
||||||
| 8 | 18:22–18:46 | 머뭄으로 | 스테이 머뭄 | 7분 | 24분 |
|
| 8 | 18:22–18:46 | 머뭄으로 | 스테이 머뭄 | 7분 | 24분 |
|
||||||
|
|
||||||
**셋째 날** 08:50–13:35 (총 4시간 45분 · 이동 29분 · 정거장 5)
|
**셋째 날** 08:50–12:54 (총 4시간 4분 · 이동 20분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -555,7 +546,6 @@
|
|||||||
| 2 | 10:46–11:26 | 오전 | 우체통거리 | 8분 | 40분 |
|
| 2 | 10:46–11:26 | 오전 | 우체통거리 | 8분 | 40분 |
|
||||||
| 3 | 11:30–12:10 | 점심 | 일흥옥 | 4분 | 40분 |
|
| 3 | 11:30–12:10 | 점심 | 일흥옥 | 4분 | 40분 |
|
||||||
| 4 | 12:18–12:54 | 오후 | 군산근대미술관 | 8분 | 36분 |
|
| 4 | 12:18–12:54 | 오후 | 군산근대미술관 | 8분 | 36분 |
|
||||||
| 5 | 13:03–13:35 | 마무리 | 스테이 머뭄 | 9분 | 32분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -589,7 +579,7 @@
|
|||||||
| 7 | 17:37–19:07 | 저녁 | 명월갈비 | 5분 | 90분 |
|
| 7 | 17:37–19:07 | 저녁 | 명월갈비 | 5분 | 90분 |
|
||||||
| 8 | 19:12–19:48 | 머뭄으로 | 스테이 머뭄 | 5분 | 36분 |
|
| 8 | 19:12–19:48 | 머뭄으로 | 스테이 머뭄 | 5분 | 36분 |
|
||||||
|
|
||||||
**셋째 날** 09:40–15:27 (총 5시간 47분 · 이동 45분 · 정거장 5)
|
**셋째 날** 09:40–14:32 (총 4시간 52분 · 이동 38분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -597,7 +587,6 @@
|
|||||||
| 2 | 11:18–12:30 | 오전 | 채만식문학관 | 18분 | 72분 |
|
| 2 | 11:18–12:30 | 오전 | 채만식문학관 | 18분 | 72분 |
|
||||||
| 3 | 12:47–13:47 | 점심 | 한일옥 | 17분 | 60분 |
|
| 3 | 12:47–13:47 | 점심 | 한일옥 | 17분 | 60분 |
|
||||||
| 4 | 13:50–14:32 | 오후 | 이성당 | 3분 | 42분 |
|
| 4 | 13:50–14:32 | 오후 | 이성당 | 3분 | 42분 |
|
||||||
| 5 | 14:39–15:27 | 마무리 | 스테이 머뭄 | 7분 | 48분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -671,7 +660,7 @@
|
|||||||
| 6 | 18:37–19:48 | 저녁 | 명월갈비 | 5분 | 71분 |
|
| 6 | 18:37–19:48 | 저녁 | 명월갈비 | 5분 | 71분 |
|
||||||
| 7 | 19:53–20:21 | 머뭄으로 | 스테이 머뭄 | 5분 | 28분 |
|
| 7 | 19:53–20:21 | 머뭄으로 | 스테이 머뭄 | 5분 | 28분 |
|
||||||
|
|
||||||
**셋째 날** 09:40–14:20 (총 4시간 40분 · 이동 42분 · 정거장 5)
|
**셋째 날** 09:40–13:30 (총 3시간 50분 · 이동 30분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -679,7 +668,6 @@
|
|||||||
| 2 | 11:01–11:44 | 오전 | 군산근대건축관 | 10분 | 43분 |
|
| 2 | 11:01–11:44 | 오전 | 군산근대건축관 | 10분 | 43분 |
|
||||||
| 3 | 11:53–12:31 | 점심 | 만남스넥 | 9분 | 38분 |
|
| 3 | 11:53–12:31 | 점심 | 만남스넥 | 9분 | 38분 |
|
||||||
| 4 | 12:42–13:30 | 오후 | 경암동 철길마을 | 11분 | 48분 |
|
| 4 | 12:42–13:30 | 오후 | 경암동 철길마을 | 11분 | 48분 |
|
||||||
| 5 | 13:42–14:20 | 마무리 | 스테이 머뭄 | 12분 | 38분 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -752,7 +740,7 @@
|
|||||||
| 7 | 17:30–18:38 | 저녁 | 명월갈비 | 5분 | 68분 |
|
| 7 | 17:30–18:38 | 저녁 | 명월갈비 | 5분 | 68분 |
|
||||||
| 8 | 18:43–19:10 | 머뭄으로 | 스테이 머뭄 | 5분 | 27분 |
|
| 8 | 18:43–19:10 | 머뭄으로 | 스테이 머뭄 | 5분 | 27분 |
|
||||||
|
|
||||||
**셋째 날** 09:30–13:48 (총 4시간 18분 · 이동 42분 · 정거장 5)
|
**셋째 날** 09:30–13:03 (총 3시간 33분 · 이동 33분 · 정거장 4)
|
||||||
|
|
||||||
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
| | 시각 | 자리 | 장소 | 이동 | 체류 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@ -760,5 +748,4 @@
|
|||||||
| 2 | 10:51–11:18 | 오전 | 구 군산세관 본관 | 13분 | 27분 |
|
| 2 | 10:51–11:18 | 오전 | 구 군산세관 본관 | 13분 | 27분 |
|
||||||
| 3 | 11:30–12:15 | 점심 | 일흥옥 | 12분 | 45분 |
|
| 3 | 11:30–12:15 | 점심 | 일흥옥 | 12분 | 45분 |
|
||||||
| 4 | 12:23–13:03 | 오후 | 군산근대미술관 | 8분 | 40분 |
|
| 4 | 12:23–13:03 | 오후 | 군산근대미술관 | 8분 | 40분 |
|
||||||
| 5 | 13:12–13:48 | 마무리 | 스테이 머뭄 | 9분 | 36분 |
|
|
||||||
|
|
||||||
|
|||||||
@ -218,7 +218,7 @@
|
|||||||
| 56 | | 전미라 | 1978– | 테니스 선수 | 테니스 선수로 뛰었고 지금은 해설위원입니다. |
|
| 56 | | 전미라 | 1978– | 테니스 선수 | 테니스 선수로 뛰었고 지금은 해설위원입니다. |
|
||||||
| 57 | | 한왕용 | 1966– | 산악인 | 히말라야 8000m 14좌를 세계 11번째로 완등했습니다. |
|
| 57 | | 한왕용 | 1966– | 산악인 | 히말라야 8000m 14좌를 세계 11번째로 완등했습니다. |
|
||||||
|
|
||||||
## 4. 일정 — 정거장에서 할 일 36줄 — `build_itinerary.py` `PLACES`·`STAY_ACTION`
|
## 4. 일정 — 정거장에서 할 일 37줄 — `build_itinerary.py` `PLACES`·`STAY_ACTION`
|
||||||
|
|
||||||
| 장소 | 무엇을 하는지 | 머무는 시간 |
|
| 장소 | 무엇을 하는지 | 머무는 시간 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@ -240,10 +240,11 @@
|
|||||||
| 물밀소 | 저온숙성 빵을 골라 나옵니다. 마당에서 먹을 것으로 두어 개 더 삽니다. | 40분 |
|
| 물밀소 | 저온숙성 빵을 골라 나옵니다. 마당에서 먹을 것으로 두어 개 더 삽니다. | 40분 |
|
||||||
| 빈해원 | 2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다. | 70분 |
|
| 빈해원 | 2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다. | 70분 |
|
||||||
| 스테이 머뭄 | 돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다. | 30분 |
|
| 스테이 머뭄 | 돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다. | 30분 |
|
||||||
| 스테이 머뭄 | 맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다. | 40분 |
|
| 스테이 머뭄 | 맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다. | 42분 |
|
||||||
| 스테이 머뭄 | 부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다. | 60분 |
|
| 스테이 머뭄 | 부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다. | 60분 |
|
||||||
| 스테이 머뭄 | 숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다. | 173분 |
|
| 스테이 머뭄 | 숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다. | 173분 |
|
||||||
| 스테이 머뭄 | 아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요. | 75분 |
|
| 스테이 머뭄 | 아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요. | 79분 |
|
||||||
|
| 스테이 머뭄 | 아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 차에 싣고 그대로 나섭니다. | 75분 |
|
||||||
| 스테이 머뭄 | 오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다. | 223분 |
|
| 스테이 머뭄 | 오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다. | 223분 |
|
||||||
| 스테이 머뭄 | 짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다. | 45분 |
|
| 스테이 머뭄 | 짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다. | 45분 |
|
||||||
| 우체통거리 | 주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다. | 26분 |
|
| 우체통거리 | 주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다. | 26분 |
|
||||||
@ -285,17 +286,200 @@
|
|||||||
| 소설 『탁류』를 따라 걷는 2박 3일 | 2박 3일 | 채만식을 읽어 본 손님 | 소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다. |
|
| 소설 『탁류』를 따라 걷는 2박 3일 | 2박 3일 | 채만식을 읽어 본 손님 | 소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다. |
|
||||||
| 철길마을과 항구를 같이 보는 2박 3일 | 2박 3일 | 사진을 찍는 손님 | 기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다. |
|
| 철길마을과 항구를 같이 보는 2박 3일 | 2박 3일 | 사진을 찍는 손님 | 기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다. |
|
||||||
|
|
||||||
## 6. 오늘의 엽서 4장 — 원본 payload (아직 다시 안 뽑음)
|
## 6. 군산 읽기 31가지 — `build_reading.py` `ITEMS`
|
||||||
|
|
||||||
|
한 화면에 다 펴 두는 카드. 일력(7번)과 달리 손님이 훑어 내려가며 읽는 자리다.
|
||||||
|
|
||||||
|
### 문학 8가지 — 이 도시를 쓴 사람과 그 자리
|
||||||
|
|
||||||
|
| 제목 | 글 | 연도 | 확인 | 출처 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 채만식 『탁류』 | 일제강점기 군산을 무대로 한 장편소설입니다. 금강 하구의 흙탕물처럼 시대에 휩쓸리는 사람들을 그렸습니다. | 1937 | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 백릉 채만식 | 1902년 옥구(지금의 군산) 임피에서 태어난 소설가입니다. 풍자로 시대를 그렸고 1950년에 세상을 떠났습니다. | 1902 | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 채만식문학관 | 금강 하구 옆에 선 문학관입니다. 작가의 생애와 『탁류』의 판본을 한자리에서 봅니다. | | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 『탁류』라는 제목 | 금강 하류의 흙탕물을 가리키는 말입니다. 그 물살에 휩쓸려 가는 사람들을 함께 가리킵니다. | | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 채만식의 풍자 | 웃기면서 찌르는 문장이 이 작가의 방식입니다. 개항장 군산에서 본 것이 그 바탕에 있습니다. | | 확인필요 | 군산시 채만식문학관 |
|
||||||
|
| 탁류길 | 소설의 배경을 따라 걷는 군산 스탬프투어 코스입니다. 원도심 구간이라 머뭄에서 걸어 닿습니다. | | 확인 | 군산 스탬프투어 |
|
||||||
|
| 조정래 『아리랑』과 아리랑길 | 군산과 김제 들녘이 무대인 대하소설입니다. 스탬프투어에 같은 이름의 코스가 있습니다. | | 확인필요 | 군산 스탬프투어 |
|
||||||
|
| 군산이 낳은 시인들 | 고은 · 이병훈 · 심호택 · 문효치 · 이연주 · 주하림. 아래 '인물 열전' 에서 한 사람씩 봅니다. | | 확인 | 한국어 위키백과 |
|
||||||
|
|
||||||
|
### 섬과 바다 6가지 — 다리를 건너면 나오는 것
|
||||||
|
|
||||||
|
| 제목 | 글 | 연도 | 확인 | 출처 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 고군산군도 | 예순 남짓한 섬이 모인 군도입니다. 새만금 방조제와 연결 다리로 이어져 차로 들어갑니다. | | 확인 | 군산시 문화관광 |
|
||||||
|
| 선유도 | 고군산군도의 대표 섬입니다. 망주봉 아래 해변을 걷고 스카이라인을 탑니다. 머뭄에서 차로 한 시간 남짓입니다. | | 확인 | 군산시 문화관광 |
|
||||||
|
| 장자도 | 선유도와 다리로 이어진 작은 섬입니다. 포구와 바다길을 봅니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 무녀도 | 고군산군도의 섬 하나입니다. 이름의 유래가 전해집니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 신시도 | 새만금과 고군산군도를 잇는 자리의 섬입니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 비응항 | 새만금 방조제가 시작하는 항구입니다. 서해 관광의 관문으로 씁니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
|
||||||
|
### 역사 5가지 — 연표에 안 들어간 이야기
|
||||||
|
|
||||||
|
| 제목 | 글 | 연도 | 확인 | 출처 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 미곡 수탈과 군산항 | 일제강점기 호남평야의 쌀이 이 항구로 모여 일본으로 나갔습니다. 부잔교와 창고가 그 통로였습니다. | | 확인필요 | KCI 학술자료 |
|
||||||
|
| 째보선창 | 내항 옆 선창입니다. 『탁류』의 인물들이 오가던 자리이고, 지금은 배 대신 낡은 창고가 남았습니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 해망굴 | 시내와 항구를 잇는 굴입니다. 걸어서 통과해 반대편 항구 쪽으로 나옵니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 경암동 철길 | 1944년에 놓인 철길입니다. 선로가 집 앞 골목을 그대로 지나갑니다. | 1944 | 확인 | 군산시 문화관광 |
|
||||||
|
| 군산항 | 만남과 이별, 물류와 근대사가 한자리에 겹치는 항구입니다. 도시의 성격이 여기서 정해졌습니다. | | 확인 | 군산시 문화관광 |
|
||||||
|
|
||||||
|
### 장소 9가지 — 걸어서 또는 차로 닿는 자리
|
||||||
|
|
||||||
|
| 제목 | 글 | 연도 | 확인 | 출처 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 군산근대역사박물관 | 항구도시의 근대사를 3층부터 내려오며 봅니다. 도시를 걷기 전에 여기서 한 번 잡고 나섭니다. | | 확인 | 군산시 문화관광 |
|
||||||
|
| 군산 내항과 부잔교 | 조수에 따라 오르내리는 뜬다리입니다. 다리 위를 끝까지 걸어 나갔다 돌아옵니다. | 1926 | 확인 | 군산시 문화관광 |
|
||||||
|
| 동국사 | 일본식 사찰 건축이 남은 절입니다. 대웅전과 마당을 둘러봅니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 월명공원 | 시내와 바다를 함께 내려다보는 공원입니다. 계절마다 오르는 길이 다릅니다. | | 확인 | 군산시 문화관광 |
|
||||||
|
| 은파호수공원 | 호수를 두르는 산책길입니다. 저녁에 한 바퀴 돌기 좋습니다. | | 확인 | 군산시 문화관광 |
|
||||||
|
| 금강하굿둑 | 금강과 서해가 만나는 자리입니다. 강과 바다의 경계를 사람 손으로 그은 곳입니다. | 1990 | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 말랭이마을 | 언덕과 골목이 남은 마을입니다. 도시재생으로 되살린 자리입니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 장미갤러리 | 근대역사문화거리의 전시 공간입니다. 옛 창고 건물을 고쳐 씁니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 진포해양테마공원 | 진포대첩의 기억을 항구 옆에 펼쳐 둔 공원입니다. 실제 군함과 항공기를 봅니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
|
||||||
|
### 음식과 생활 3가지 — 이 도시가 먹는 것
|
||||||
|
|
||||||
|
| 제목 | 글 | 연도 | 확인 | 출처 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 군산 짬뽕 | 항구도시의 중국집 문화가 남았습니다. 빈해원과 국제반점이 그 자리입니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 박대와 군산 밥상 | 서해에서 나는 박대를 말려 구워 냅니다. 백반집 상에 자주 오릅니다. | | 확인필요 | 군산시 문화관광 |
|
||||||
|
| 이성당 | 오래된 빵집입니다. 단팥빵과 야채빵이 나오는 시간에 맞추면 줄이 짧습니다. | | 확인 | 대한민국 구석구석 |
|
||||||
|
|
||||||
|
## 7. 오늘의 한 장 39장 — `build_daily.py`
|
||||||
|
|
||||||
|
날마다 한 장씩 넘어가는 일력. 가요는 가요 다방을 그대로 읽고, 시·소설은 손으로 적는다.
|
||||||
|
|
||||||
|
| 날짜 | 분류 | 제목 | 글 | 확인 | 출처 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 01-01 | 문학 | 채만식 『탁류』 | 일제강점기 군산을 무대로 한 장편소설입니다. 금강 하구의 흙탕물처럼 시대에 휩쓸리는 사람들을 그렸습니다. | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 01-10 | 대중가요 | 「잘 있거라 군산항」 · 안다성 | 1963년 곡. 작사 손석우 · 작곡 손석우. 월명산 고갯길로 시작하는 이별가. 군산을 노래한 곡 가운데 가장 널리 알려졌습니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 01-20 | 문학 | 백릉 채만식 | 1902년 옥구(지금의 군산) 임피에서 태어난 소설가입니다. 풍자로 시대를 그렸고 1950년에 세상을 떠났습니다. | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 01-29 | 대중가요 | 「헤어진 군산항」 · 박제연 | 1965년 곡. 작사 송운선 · 작곡 송운선. 비 내리는 밤 부두에서 헤어지는 노래입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 02-07 | 대중가요 | 「님 떠난 군산항」 · 이미자 | 1966년 곡. 작사 조형식 · 작곡 백영호. 떠나는 배를 보내는 항구의 정서를 담았습니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 02-17 | 문학 | 채만식문학관 | 금강 하구 옆에 선 문학관입니다. 작가의 생애와 『탁류』의 판본을 한자리에서 봅니다. | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 02-26 | 대중가요 | 「금강 나그네」 · 이미자 | 1966년 곡. 작사 이병훈 · 작곡 박춘석. 금강 하구와 군산항 부두를 배경으로 한 곡입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 03-08 | 대중가요 | 「잘 있거라 군산항구」 · 최정자 | 1967년 곡. 작사 최치수 · 작곡 유금춘. 떠나는 사람의 말로 부르는 이별가입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 03-17 | 문학 | 『탁류』라는 제목 | 금강 하류의 흙탕물을 가리키는 말입니다. 그 물살에 휩쓸려 가는 사람들을 함께 가리킵니다. | 확인 | 군산시 채만식문학관 |
|
||||||
|
| 03-26 | 대중가요 | 「군산 마도로스」 · 이상열 | 1967년 곡. 항구를 드나들던 뱃사람의 노래입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 04-05 | 대중가요 | 「군산 부르스」 · 유주용 | 1969년 곡. 작사 반야월 · 작곡 강도미. 1960년대 말 군산 거리의 분위기를 담은 곡입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 04-14 | 문학 | 채만식의 풍자 | 웃기면서 찌르는 문장이 이 작가의 방식입니다. 개항장 군산에서 본 것이 그 바탕에 있습니다. | 확인필요 | 군산시 채만식문학관 |
|
||||||
|
| 04-23 | 대중가요 | 「이별의 군산항 부두」 · 최정자 | 1969년 곡. 작사 최치수 · 작곡 유금춘. 부두에서의 이별을 다시 한 번 부른 곡입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 05-03 | 문학 | 탁류길 | 소설의 배경을 따라 걷는 군산 스탬프투어 코스입니다. 원도심 구간이라 머뭄에서 걸어 닿습니다. | 확인 | 군산 스탬프투어 |
|
||||||
|
| 05-12 | 대중가요 | 「군산항 부르스」 · 최하우 | 1984년 곡. 작사 최하우 · 작곡 김성근. 1980년대에 나온 군산항 노래입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 05-21 | 대중가요 | 「나의 사랑 군산이여」 · 오단비 | 1989년 곡. 작사 이수 · 작곡 정종택. 군산을 향한 애정을 정면으로 부르는 곡입니다. | 확인 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 05-31 | 문학 | 조정래 『아리랑』과 아리랑길 | 군산과 김제 들녘이 무대인 대하소설입니다. 스탬프투어에 같은 이름의 코스가 있습니다. | 확인필요 | 군산 스탬프투어 |
|
||||||
|
| 06-09 | 대중가요 | 「군산항 엘레지」 · 오단비 | 작사 이수 · 작곡 정종택. 항구 도시의 비가입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 06-18 | 대중가요 | 「선유도 아가씨」 · 오단비 | 작사 이수 · 작곡 서승일. 고군산군도 선유도를 배경으로 한 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 06-28 | 문학 | 군산이 낳은 시인들 | 고은 · 이병훈 · 심호택 · 문효치 · 이연주 · 주하림. 아래 '인물 열전' 에서 한 사람씩 봅니다. | 확인 | 한국어 위키백과 |
|
||||||
|
| 07-07 | 대중가요 | 「군산찬가」 · 오단비 | 작사 이수 · 작곡 서승일. 은파와 새만금이 가사에 나오는 군산 찬가입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 07-17 | 대중가요 | 「오성산」 · 오단비 | 작사 이수 · 작곡 서승일. 금강 하구를 내려다보는 오성산을 노래했습니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 07-26 | 시 | 시인 고은 | 1933– · 군산에서 태어나 60년 넘게 시를 써 온 시인입니다. | 확인 | 한국어 위키백과 |
|
||||||
|
| 08-04 | 대중가요 | 「함께하는 군산」 · 오은주 | 작사 김동찬 · 작곡 김동찬. 군산시가 제작한 음반의 첫 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 08-14 | 대중가요 | 「새만금 연가」 · 채미영 | 작사 김동찬 · 작곡 김동찬 · 김수환. 새만금을 소재로 한 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 08-23 | 시 | 시인 이병훈 | 1925–2009 · 옥구(지금의 군산) 출신으로 1950년대에 등단한 현대 시인입니다. | 확인 | 한국어 위키백과 |
|
||||||
|
| 09-01 | 대중가요 | 「군산시민의 노래」 · 오단비 | 작사 군산문화원 · 작곡 군산예총. 금만경 들녘과 서해 바다로 시작하는 시민의 노래입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 09-11 | 시 | 시인 심호택 | 1947–2010 · 민중의 삶을 시로 옮긴 시인입니다. | 확인 | 한국어 위키백과 |
|
||||||
|
| 09-20 | 대중가요 | 「군산 아줌마」 · 박솔미 | 작사 송두호 · 작곡 송두호. 선창가 거리와 새만금이 함께 나오는 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 09-29 | 대중가요 | 「내 고향 군산항」 · 정애란 | 작사 정애란 · 작곡 정애란. 고향으로서의 군산항을 부른 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 10-09 | 시 | 시인 문효치 | 1943– · 군산에서 태어나 1966년 신춘문예로 등단했습니다. | 확인 | 한국어 위키백과 |
|
||||||
|
| 10-18 | 대중가요 | 「군산 가는 길」 · 황상준 | 군산으로 향하는 길을 제목으로 삼은 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 10-27 | 대중가요 | 「군산 추억」 · 임인건 | 재즈 피아니스트 임인건이 군산을 소재로 만든 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 11-06 | 시 | 시인 이연주 | 1953–1992 · 짧게 활동하고 떠난 시인입니다. | 확인 | 한국어 위키백과 |
|
||||||
|
| 11-15 | 대중가요 | 「군산에서」 · 임인건 | 같은 연주자가 군산에서 받은 인상을 담았습니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 11-25 | 대중가요 | 「헤어진 군산항」 · 김창환 | 작사 송운선 · 작곡 송운선. 군산시 제작 음반에 다시 실린 판입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 12-04 | 시 | 시인 주하림 | 군산에서 태어나 2009년 창비신인시인상으로 등단했습니다. | 확인 | 한국어 위키백과 |
|
||||||
|
| 12-13 | 대중가요 | 「금강 아가씨」 · 이미자 | 금강을 배경으로 한 이미자의 또 다른 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
| 12-23 | 대중가요 | 「금강의 비가」 · 전승애 | 금강을 제목에 올린 곡입니다. | 확인필요 | 음악의 오솔길 — 군산 관련 노래 |
|
||||||
|
|
||||||
|
## 8. 오늘의 엽서 4장 — 원본 payload (아직 다시 안 뽑음)
|
||||||
|
|
||||||
- 부잔교 위에서는 물이 어디까지 올라왔었는지가 다리에 새겨져 있다. `#군산 #내항 #부잔교`
|
- 부잔교 위에서는 물이 어디까지 올라왔었는지가 다리에 새겨져 있다. `#군산 #내항 #부잔교`
|
||||||
- 짬뽕 한 그릇이 항구도시의 이력서인 줄은 몰랐다. `#군산짬뽕 #항구도시`
|
- 짬뽕 한 그릇이 항구도시의 이력서인 줄은 몰랐다. `#군산짬뽕 #항구도시`
|
||||||
- 단팥빵을 사러 줄을 서는 일이 여행이 되는 도시. `#이성당 #오래된가게`
|
- 단팥빵을 사러 줄을 서는 일이 여행이 되는 도시. `#이성당 #오래된가게`
|
||||||
- 섬과 섬 사이를 다리로 건너면, 바다가 길이 된다. `#고군산군도 #선유도`
|
- 섬과 섬 사이를 다리로 건너면, 바다가 길이 된다. `#고군산군도 #선유도`
|
||||||
|
|
||||||
## 7. 머뭄이 만든 노래 5곡 — `patch_stay.py` `OWN_SONGS`
|
## 9. 머뭄이 만든 노래 5곡 — `patch_stay.py` `OWN_SONGS`
|
||||||
|
|
||||||
- 머뭄의 아침 — `/s/stay/audio/01-mumum-morning.mp3`
|
- 머뭄의 아침 — `/s/stay/audio/01-mumum-morning.mp3`
|
||||||
- CHECK IN · 머뭄 — `/s/stay/audio/02-check-in.mp3`
|
- CHECK IN · 머뭄 — `/s/stay/audio/02-check-in.mp3`
|
||||||
- Stay in 머뭄 — `/s/stay/audio/03-stay-in.mp3`
|
- Stay in 머뭄 — `/s/stay/audio/03-stay-in.mp3`
|
||||||
- 오늘은 안 갈란다 — `/s/stay/audio/04-not-going.mp3`
|
- 오늘은 안 갈란다 — `/s/stay/audio/04-not-going.mp3`
|
||||||
- 군산에 두고 온 님 — `/s/stay/audio/05-gunsan-nim.mp3`
|
- 군산에 두고 온 님 — `/s/stay/audio/05-gunsan-nim.mp3`
|
||||||
|
|
||||||
|
## 10. 오늘의 날씨 문구 50개 — `patch_stay.py` `SKY_NOTES`·`TEMP_NOTES`
|
||||||
|
|
||||||
|
하늘 줄은 **집 안**에서 뭘 할지, 기온 줄은 **밖에서** 어디를 갈지다. 두 줄이 나란히 선다.
|
||||||
|
### 하늘 25개 (`SKY_NOTES`)
|
||||||
|
|
||||||
|
**맑음**
|
||||||
|
- 마당 평상에 앉으면 새소리만 들립니다. 정원 쪽으로 그늘이 길게 눕는 시간입니다.
|
||||||
|
- 기와가 마르는 냄새가 나는 날입니다. 아침에 널어 두신 것은 오후면 다 마릅니다.
|
||||||
|
- 볕이 좋아 대문을 열어 둡니다. 마당 사진은 해가 기울기 시작할 때가 가장 곱습니다.
|
||||||
|
- 창을 다 열어도 좋은 날입니다. A동 장지문으로 드는 빛이 오전에 가장 깊습니다.
|
||||||
|
- 해가 길게 남는 날입니다. 짐을 풀고 골목부터 한 바퀴 돌고 오셔도 늦지 않습니다.
|
||||||
|
|
||||||
|
**구름많음**
|
||||||
|
- 빛이 부드러운 날입니다. 골목 사진이 제일 잘 나오는 빛입니다.
|
||||||
|
- 그늘이 옅어 마당이 눈부시지 않습니다. 평상에 오래 앉아 계시기 좋습니다.
|
||||||
|
- 해가 들었다 숨었다 합니다. 창고형 CAFÉ 공간과 마당을 오가며 쉬시면 됩니다.
|
||||||
|
- 구름이 얇은 날입니다. 우체통거리까지 걷기에 덥지도 춥지도 않습니다.
|
||||||
|
- 빛이 고른 날입니다. 초원사진관까지 걸어서 삼 분, 사진은 오늘 같은 하늘이 낫습니다.
|
||||||
|
|
||||||
|
**그외**
|
||||||
|
- 해가 숨은 날입니다. 기와와 담이 한 색으로 가라앉고, 마당 평상에 앉아도 눈이 부시지 않습니다.
|
||||||
|
- 하늘이 자주 바뀌는 날입니다. 나가실 때 우산을 챙기시는 편이 낫습니다.
|
||||||
|
- 안개가 낮게 깔리면 골목 끝이 흐려집니다. 서두르지 마시고 늦게 나가셔도 됩니다.
|
||||||
|
- 소나기가 지나갈 수 있는 하늘입니다. 창고형 CAFÉ 공간에서 한 차례 보내고 나가셔도 됩니다.
|
||||||
|
- 바람이 먼저 바뀌는 날입니다. 마당에 널어 두신 것은 들여놓으시는 편이 좋습니다.
|
||||||
|
|
||||||
|
**비**
|
||||||
|
- 백 년 된 기와를 타고 내리는 소리가 다릅니다. 창고형 CAFÉ 공간에서 빗소리를 들으실 수 있습니다.
|
||||||
|
- 비 오는 골목에는 사람이 없습니다. 히로쓰 가옥 담길은 오늘 같은 날이 가장 조용합니다.
|
||||||
|
- 마당 디딤돌이 미끄럽습니다. 밤에 나가실 때는 정원 석등 쪽으로 도세요.
|
||||||
|
- 실내로 도는 편이 낫습니다. 근대건축관과 군산근대미술관이 걸어서 십 분 안에 나란히 있습니다.
|
||||||
|
- 우산은 대문 안쪽에 두시면 됩니다. 젖은 것은 테라스 쪽이 잘 마릅니다.
|
||||||
|
|
||||||
|
**눈**
|
||||||
|
- 기와에 눈이 앉으면 골목이 통째로 조용해집니다. 정원 석등만 켜 두겠습니다.
|
||||||
|
- 눈 밟는 소리가 담 안에서 크게 들립니다. 마당은 아침에 한 번 쓸어 두겠습니다.
|
||||||
|
- 눈 오는 날의 원도심은 사람이 적습니다. 초원사진관 골목이 오늘 가장 한산합니다.
|
||||||
|
- 디딤돌에 눈이 얼 수 있습니다. 밤에 드나드실 때는 조심해서 딛으세요.
|
||||||
|
- 창을 닫아 두시는 편이 낫습니다. 창고형 CAFÉ 공간이 이런 날 가장 아늑합니다.
|
||||||
|
|
||||||
|
### 기온대 25개 (`TEMP_NOTES`)
|
||||||
|
|
||||||
|
**혹서**
|
||||||
|
- 낮에는 군산근대미술관·근대건축관처럼 실내로 도는 편이 낫습니다. 월명호수 한 바퀴는 해가 넘어간 뒤가 시원합니다.
|
||||||
|
- 한낮 골목에는 그늘이 없습니다. 초원사진관·영화의 거리는 이른 아침이나 해 질 무렵에 도세요.
|
||||||
|
- 옛 군산세관과 근대건축관은 걸어서 이어집니다. 실내를 징검다리 삼아 도시면 덜 지칩니다.
|
||||||
|
- 경암동 철길마을은 그늘이 적습니다. 오늘은 저녁에 가시는 편이 낫습니다.
|
||||||
|
- 물은 나가시기 전에 챙기세요. 이성당까지는 걸어서 오백 미터, 해가 기운 뒤가 낫습니다.
|
||||||
|
|
||||||
|
**더움**
|
||||||
|
- 선유도까지 다녀오기 좋은 기온입니다. 골목은 오후 늦게 도세요 — 경암동 철길마을은 해 질 때가 사람이 적습니다.
|
||||||
|
- 낮에는 월명호수 그늘길이 시원합니다. 물가를 따라 걷다 돌아오시면 됩니다.
|
||||||
|
- 우체통거리와 영화의 거리는 그늘이 이어집니다. 걸어서 도시기에 무리가 없습니다.
|
||||||
|
- 해가 길어 저녁이 넉넉합니다. 내항 쪽으로 나가 노을을 보고 오셔도 됩니다.
|
||||||
|
- 가장 더운 시간만 실내가 낫습니다. 군산근대미술관에서 한 차례 쉬어 가세요.
|
||||||
|
|
||||||
|
**선선**
|
||||||
|
- 걷기에 가장 좋은 날입니다. 히로쓰 가옥에서 초원사진관·영화의 거리까지 걸어서 그대로 이어집니다.
|
||||||
|
- 원도심은 오늘 하루면 다 걸립니다. 군산 시간여행마을까지 천천히 다녀오셔도 됩니다.
|
||||||
|
- 차를 두고 걷는 편이 낫습니다. 우체통거리에서 옛 군산세관까지 골목이 계속 이어집니다.
|
||||||
|
- 해가 부담스럽지 않습니다. 경암동 철길마을까지 다녀오기에 알맞은 기온입니다.
|
||||||
|
- 낮에는 월명호수를 한 바퀴 돌고, 밤에는 마당에 앉아 계셔도 좋습니다.
|
||||||
|
|
||||||
|
**쌀쌀**
|
||||||
|
- 월명호수를 한 바퀴 돌고 이성당이나 국제반점에서 따뜻한 것을 드시면 알맞습니다.
|
||||||
|
- 겉옷을 하나 더 챙기세요. 해가 지면 항구 쪽 바람이 먼저 찹니다.
|
||||||
|
- 실내와 골목을 섞어 도는 편이 낫습니다. 근대건축관에서 몸을 녹이고 다시 나오시면 됩니다.
|
||||||
|
- 따뜻한 국물이 도는 날입니다. 복성루·빈해원처럼 가까운 집부터 보세요.
|
||||||
|
- 오후 볕이 짧습니다. 초원사진관·영화의 거리를 먼저 돌고 저녁에 들어오세요.
|
||||||
|
|
||||||
|
**추움**
|
||||||
|
- 밖은 바람이 매섭습니다. 근대건축관·근대미술관과 시간여행마을 실내를 묶어 도는 편이 낫습니다.
|
||||||
|
- 항구 바람이 매운 날입니다. 골목은 짧게 돌고 실내를 길게 잡으세요.
|
||||||
|
- 장갑과 목도리를 챙기세요. 옛 군산세관 앞은 바람이 그대로 지나갑니다.
|
||||||
|
- 따뜻한 것을 먼저 드시는 편이 낫습니다. 이성당·국제반점은 걸어서 오백 미터입니다.
|
||||||
|
- 일찍 들어오셔도 됩니다. 벽난로가 있는 거실이 이런 날 가장 좋습니다.
|
||||||
|
|||||||
@ -25,12 +25,48 @@ const d = await page.evaluate(() => {
|
|||||||
catchKinds: ['general', 'season', 'month', 'weather'].map((k) => (P.narrative.catchphrases?.items || []).filter((x) => x.kind === k).length),
|
catchKinds: ['general', 'season', 'month', 'weather'].map((k) => (P.narrative.catchphrases?.items || []).filter((x) => x.kind === k).length),
|
||||||
themes: itin.length,
|
themes: itin.length,
|
||||||
firstIsStay: days.every((day) => day.stops[0].name.endsWith('스테이 머뭄')),
|
firstIsStay: days.every((day) => day.stops[0].name.endsWith('스테이 머뭄')),
|
||||||
lastIsStay: days.every((day) => day.stops[day.stops.length - 1].name.endsWith('스테이 머뭄')),
|
// 떠나는 날은 갈린다 — 차로 온 손님은 짐을 싣고 나서므로 복귀 칸이 없다(2026-09-14 대표 의견).
|
||||||
|
lastIsStay: itin.every((i) => i.days.slice(0, -1)
|
||||||
|
.every((day) => day.stops[day.stops.length - 1].name.endsWith('스테이 머뭄'))),
|
||||||
|
depart: (() => {
|
||||||
|
const tail = (i) => i.days[i.days.length - 1];
|
||||||
|
const car = itin.filter((i) => !tail(i).stops[tail(i).stops.length - 1].name.endsWith('스테이 머뭄'));
|
||||||
|
const walk = itin.filter((i) => !car.includes(i));
|
||||||
|
return {
|
||||||
|
car: car.length, walk: walk.length,
|
||||||
|
car1: car.filter((i) => i.duration === '1박 2일').length,
|
||||||
|
car2: car.filter((i) => i.duration === '2박 3일').length,
|
||||||
|
// 퇴실 칸의 문구가 동선과 같은 말을 하는가 — 차인데 "맡겨 두고" 면 손님이 한 번 더 들른다
|
||||||
|
carNote: car.every((i) => (tail(i).stops[0].note || '').includes('차에 싣고')),
|
||||||
|
walkNote: walk.every((i) => (tail(i).stops[0].note || '').includes('맡겨 두고')),
|
||||||
|
walkEnd: walk.every((i) => tail(i).stops[tail(i).stops.length - 1].name.startsWith('마무리')),
|
||||||
|
};
|
||||||
|
})(),
|
||||||
consecutive: days.filter((day) => day.stops.some((s, i) => i && s.name.split(' · ').pop() === day.stops[i - 1].name.split(' · ').pop())).length,
|
consecutive: days.filter((day) => day.stops.some((s, i) => i && s.name.split(' · ').pop() === day.stops[i - 1].name.split(' · ').pop())).length,
|
||||||
hasNote: stops.every((s) => s.name.endsWith('스테이 머뭄') || s.note),
|
hasNote: stops.every((s) => s.name.endsWith('스테이 머뭄') || s.note),
|
||||||
stopImages: stops.filter((s) => s.imageUrl).length,
|
stopImages: stops.filter((s) => s.imageUrl).length,
|
||||||
restDays: days.filter((day) => day.stops.some((s) => s.name.startsWith('머뭄에서 쉬는 날'))).length,
|
restDays: days.filter((day) => day.stops.some((s) => s.name.startsWith('머뭄에서 쉬는 날'))).length,
|
||||||
endTimes: (() => { const t = days.map((day) => { let c = +day.startTime.slice(0, 2) * 60 + +day.startTime.slice(3); day.stops.forEach((s) => { c += s.moveMinutes + s.minutes; }); return c; }); return [Math.min(...t), Math.max(...t)]; })(),
|
endTimes: (() => { const t = days.map((day) => { let c = +day.startTime.slice(0, 2) * 60 + +day.startTime.slice(3); day.stops.forEach((s) => { c += s.moveMinutes + s.minutes; }); return c; }); return [Math.min(...t), Math.max(...t)]; })(),
|
||||||
|
daily: sec('daily').items.length,
|
||||||
|
reading: (() => { const e = sec('reading'); return {
|
||||||
|
items: e.items.length,
|
||||||
|
groups: (e.groups || []).length,
|
||||||
|
sourced: e.items.filter((x) => x.source && x.source.url).length,
|
||||||
|
}; })(),
|
||||||
|
domReadCards: document.querySelectorAll('#w4d-reading .w4d-read-card').length,
|
||||||
|
domReadGroups: document.querySelectorAll('#w4d-reading .w4d-read-group').length,
|
||||||
|
domReadSrc: document.querySelectorAll('#w4d-reading .w4d-read-src a').length,
|
||||||
|
// 레일이 아니어야 한다 — 가로로 밀어야 보이면 '펼쳐 둔다' 가 아니다.
|
||||||
|
// 신문 조판이라 단(column)으로 흐른다(2026-09-14 대표: "신문처럼").
|
||||||
|
readCols: (() => { const el = document.querySelector('#w4d-reading .w4d-read-grid');
|
||||||
|
if (!el) return null;
|
||||||
|
return {cols: getComputedStyle(el).columnCount, overflow: el.scrollWidth - el.clientWidth}; })(),
|
||||||
|
readPaper: !!document.querySelector('#w4d-reading .w4d-read-head h2')
|
||||||
|
&& !!document.querySelector('#w4d-reading .w4d-read-dateline'),
|
||||||
|
dailyCats: [...new Set(sec('daily').items.map((x) => x.category))].sort(),
|
||||||
|
dailySourced: sec('daily').items.filter((x) => x.source && x.source.url).length,
|
||||||
|
domDailyPages: document.querySelectorAll('#daily article').length,
|
||||||
|
storyTabs: [...document.querySelectorAll('#story [role="tab"]')].map((t) => t.textContent.trim()),
|
||||||
songs: sec('songs').items.length,
|
songs: sec('songs').items.length,
|
||||||
ownInSongs: sec('songs').items.filter((s) => s.own).length,
|
ownInSongs: sec('songs').items.filter((s) => s.own).length,
|
||||||
songLinks: sec('songs').items.filter((s) => s.listenUrl).length,
|
songLinks: sec('songs').items.filter((s) => s.listenUrl).length,
|
||||||
@ -55,19 +91,57 @@ const d = await page.evaluate(() => {
|
|||||||
roomA: P.units[0].mediaIds.length, roomB: P.units[1].mediaIds.length,
|
roomA: P.units[0].mediaIds.length, roomB: P.units[1].mediaIds.length,
|
||||||
roomMirror: P.units.flatMap((u) => u.mediaIds).length,
|
roomMirror: P.units.flatMap((u) => u.mediaIds).length,
|
||||||
tagline: document.querySelector('#root h1')?.nextElementSibling?.textContent.trim(),
|
tagline: document.querySelector('#root h1')?.nextElementSibling?.textContent.trim(),
|
||||||
|
sub: document.getElementById('w4d-sub')?.textContent.trim(),
|
||||||
|
subIsCatch: (() => { const t = document.getElementById('w4d-sub')?.textContent.trim(); return !!t && (P.narrative.catchphrases?.items || []).some((x) => x.text.replace(/\s+/g, ' ').trim() === t); })(),
|
||||||
|
restaurants: P.local.restaurants.length,
|
||||||
|
// 걸어서 10분(800m) 넘는 맛집 — 없으면 '10분 이상' 탭에서 맛집 칸이 빈다(2026-09-11 사장님 지적)
|
||||||
|
restPhotos: P.local.restaurants.filter((r) => r.imageUrl).length,
|
||||||
|
restFar: P.local.restaurants.filter((r) => { const m = /^([\d.]+)\s*(km|m)$/i.exec(r.distanceText || ''); return m && Number(m[1]) * (m[2].toLowerCase() === 'km' ? 1000 : 1) > 800; }).length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ok('캐치프레이즈 100개', d.catch === 100, `${d.catch}개 · 일반${d.catchKinds[0]}/계절${d.catchKinds[1]}/월${d.catchKinds[2]}/날씨${d.catchKinds[3]}`);
|
ok('캐치프레이즈 100개', d.catch === 100, `${d.catch}개 · 일반${d.catchKinds[0]}/계절${d.catchKinds[1]}/월${d.catchKinds[2]}/날씨${d.catchKinds[3]}`);
|
||||||
ok('캐치프레이즈 순환', d.tagline && d.tagline !== '히로쓰 가옥 담 너머, 백 년 된 집 한 채', `현재 "${d.tagline}"`);
|
// 대표 문구는 고정, 순환은 그 아래 줄 (2026-09-11 사장님 지시)
|
||||||
|
ok('대표 문구 고정', d.tagline === '히로쓰 가옥 담 너머, 백 년 된 집 한 채', `"${d.tagline}"`);
|
||||||
|
ok('아래 줄에 캐치프레이즈', d.subIsCatch, `"${d.sub}"`);
|
||||||
|
const sub2 = await page.waitForTimeout(7500).then(() => page.evaluate(() => document.getElementById('w4d-sub')?.textContent.trim()));
|
||||||
|
ok('아래 줄 순환', !!sub2 && sub2 !== d.sub, `→ "${sub2}"`);
|
||||||
|
/* 자동재생 (2026-09-14 대표 지시). 브라우저는 조작 없는 소리 재생을 막는다 — 정책이라 못 넘는다.
|
||||||
|
그래서 **첫 손길에 켜지는가**를 센다. 막지 않는 브라우저면 그 전에 이미 켜져 있다. */
|
||||||
|
const autoAtLoad = await page.evaluate(() => document.getElementById('w4d-play')?.dataset.playing);
|
||||||
|
await page.mouse.wheel(0, 300);
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
const autoAfter = await page.evaluate(() => ({
|
||||||
|
btn: document.getElementById('w4d-play')?.dataset.playing,
|
||||||
|
time: document.querySelector('.w4d-item[data-playing="1"] .w4d-time')?.textContent,
|
||||||
|
}));
|
||||||
|
ok('자동재생', autoAfter.btn === '1',
|
||||||
|
`불러온 직후 ${autoAtLoad === '1' ? '바로 재생' : '브라우저가 막음'} → 첫 손길에 ${autoAfter.time || '안 켜짐'}`);
|
||||||
|
ok('주변 맛집', d.restaurants >= 30 && d.restFar > 0, `${d.restaurants}곳 · 걸어서 10분 넘는 곳 ${d.restFar}`);
|
||||||
|
ok('맛집 사진', d.restPhotos === d.restaurants, `${d.restPhotos}/${d.restaurants}`);
|
||||||
ok('일정 테마', d.themes === 21, `${d.themes}개`);
|
ok('일정 테마', d.themes === 21, `${d.themes}개`);
|
||||||
ok('1번 = 스테이 머뭄', d.firstIsStay, d.firstIsStay ? '전 일자' : '어긋남');
|
ok('1번 = 스테이 머뭄', d.firstIsStay, d.firstIsStay ? '전 일자' : '어긋남');
|
||||||
ok('마지막 = 스테이 머뭄', d.lastIsStay, d.lastIsStay ? '전 일자' : '어긋남');
|
ok('마지막 = 머뭄 (떠나는 날 빼고)', d.lastIsStay, d.lastIsStay ? '전 일자' : '어긋남');
|
||||||
|
ok('떠나는 날 복귀 없음 = 차 테마', d.depart.car === 14 && d.depart.car1 === 7 && d.depart.car2 === 7,
|
||||||
|
`차 ${d.depart.car}(1박${d.depart.car1}·2박${d.depart.car2}) · 도보 ${d.depart.walk}`);
|
||||||
|
ok('떠나는 날 퇴실 문구', d.depart.carNote && d.depart.walkNote && d.depart.walkEnd,
|
||||||
|
d.depart.carNote && d.depart.walkNote && d.depart.walkEnd ? '차=싣고 · 도보=맡기고 찾으러' : '어긋남');
|
||||||
ok('같은 자리 연속 없음', d.consecutive === 0, `${d.consecutive}건`);
|
ok('같은 자리 연속 없음', d.consecutive === 0, `${d.consecutive}건`);
|
||||||
ok('정거장마다 할 일 문구', d.hasNote, d.hasNote ? '전 정거장' : '빠짐');
|
ok('정거장마다 할 일 문구', d.hasNote, d.hasNote ? '전 정거장' : '빠짐');
|
||||||
ok('일정 사진 없음', d.stopImages === 0 && d.domItinImg === 0, `payload ${d.stopImages} · 화면 ${d.domItinImg}`);
|
ok('일정 사진 없음', d.stopImages === 0 && d.domItinImg === 0, `payload ${d.stopImages} · 화면 ${d.domItinImg}`);
|
||||||
ok('숙소에서 쉬는 날', d.restDays >= 3, `${d.restDays}일`);
|
ok('숙소에서 쉬는 날', d.restDays >= 3, `${d.restDays}일`);
|
||||||
ok('종료 시각 분산', d.endTimes[1] - d.endTimes[0] > 240, `${(d.endTimes[0] / 60 | 0)}:${String(d.endTimes[0] % 60).padStart(2, '0')} ~ ${(d.endTimes[1] / 60 | 0)}:${String(d.endTimes[1] % 60).padStart(2, '0')}`);
|
ok('종료 시각 분산', d.endTimes[1] - d.endTimes[0] > 240, `${(d.endTimes[0] / 60 | 0)}:${String(d.endTimes[0] % 60).padStart(2, '0')} ~ ${(d.endTimes[1] / 60 | 0)}:${String(d.endTimes[1] % 60).padStart(2, '0')}`);
|
||||||
|
ok('군산 읽기 — 한 화면에 다 편다',
|
||||||
|
d.reading.items >= 30 && d.domReadCards === d.reading.items
|
||||||
|
&& d.readCols && d.readCols.overflow <= 2 && Number(d.readCols.cols) >= 2,
|
||||||
|
`payload ${d.reading.items} · 화면 ${d.domReadCards}꼭지 · ${d.domReadGroups}면 · ${d.readCols?.cols}단 · 가로넘침 ${d.readCols?.overflow}px`);
|
||||||
|
ok('군산 읽기 신문 조판', d.readPaper, d.readPaper ? '제호 · 발행일 줄 있음' : '없음');
|
||||||
|
ok('군산 읽기 출처', d.reading.sourced === d.reading.items && d.domReadSrc === d.reading.items,
|
||||||
|
`payload ${d.reading.sourced}/${d.reading.items} · 화면 ${d.domReadSrc}`);
|
||||||
|
ok('오늘의 한 장', d.daily >= 39 && d.domDailyPages === d.daily,
|
||||||
|
`payload ${d.daily}장 · 화면 ${d.domDailyPages}장 · ${d.dailyCats.join('/')}`);
|
||||||
|
ok('오늘의 한 장 출처', d.dailySourced === d.daily, `${d.dailySourced}/${d.daily}`);
|
||||||
|
ok('이야기 탭에 일력', d.storyTabs.includes('오늘의 한 장'), d.storyTabs.join(','));
|
||||||
ok('가요 다방 곡', d.songs >= 25, `${d.songs}곡`);
|
ok('가요 다방 곡', d.songs >= 25, `${d.songs}곡`);
|
||||||
ok('가요 다방에 자작곡 없음', d.ownInSongs === 0, `${d.ownInSongs}곡`);
|
ok('가요 다방에 자작곡 없음', d.ownInSongs === 0, `${d.ownInSongs}곡`);
|
||||||
ok('노래마다 링크', d.songLinks === d.songs, `${d.songLinks}/${d.songs}`);
|
ok('노래마다 링크', d.songLinks === d.songs, `${d.songLinks}/${d.songs}`);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""구워진 payload 의 일정 21개를 표로 뽑고, 규칙 14종이 **안 보는** 논리까지 검수한다.
|
"""구워진 payload 의 일정 21개를 표로 뽑고, 규칙 17종이 **안 보는** 논리까지 검수한다.
|
||||||
|
|
||||||
★ 왜 따로 있나 — build_itinerary.py 의 audit() 는 "만들 때" 지키는 규칙이다.
|
★ 왜 따로 있나 — build_itinerary.py 의 audit() 는 "만들 때" 지키는 규칙이다.
|
||||||
이 스크립트는 **만들어진 결과**를 사람이 읽는 표로 펴고, 비율·간격처럼
|
이 스크립트는 **만들어진 결과**를 사람이 읽는 표로 펴고, 비율·간격처럼
|
||||||
@ -95,10 +95,19 @@ def check(item):
|
|||||||
if is_last and rows[0]["place"] == STAY and rows[0]["to"] > 11 * 60:
|
if is_last and rows[0]["place"] == STAY and rows[0]["to"] > 11 * 60:
|
||||||
bad.append(("L7", f"{label}: 퇴실이 {hm(rows[0]['to'])}"))
|
bad.append(("L7", f"{label}: 퇴실이 {hm(rows[0]['to'])}"))
|
||||||
# L8 시작·끝이 숙소인가
|
# L8 시작·끝이 숙소인가
|
||||||
|
# ★ 떠나는 날은 갈린다 (2026-09-14 대표 의견). 차로 온 손님은 퇴실할 때 짐을 싣고
|
||||||
|
# 나서므로 복귀 칸이 없다 — 그 하루는 마지막 정거장에서 끝나는 것이 맞다.
|
||||||
|
# 판별은 **퇴실 칸의 문구**로 한다(payload 에 남는 값이라 표와 화면이 같은 것을 본다).
|
||||||
|
car_out = is_last and "차에 싣고" in (day["stops"][0].get("note") or "")
|
||||||
if rows[0]["place"] != STAY:
|
if rows[0]["place"] != STAY:
|
||||||
bad.append(("L8", f"{label}: 첫 칸이 {rows[0]['place']}"))
|
bad.append(("L8", f"{label}: 첫 칸이 {rows[0]['place']}"))
|
||||||
if rows[-1]["place"] != STAY:
|
if rows[-1]["place"] != STAY and not car_out:
|
||||||
bad.append(("L8", f"{label}: 마지막 칸이 {rows[-1]['place']}"))
|
bad.append(("L8", f"{label}: 마지막 칸이 {rows[-1]['place']}"))
|
||||||
|
# L12 떠나는 날의 끝맺음이 퇴실 문구와 같은 말을 하는가
|
||||||
|
if car_out and rows[-1]["place"] == STAY:
|
||||||
|
bad.append(("L12", f"{label}: 짐을 차에 싣고 나섰는데 숙소로 돌아온다"))
|
||||||
|
if is_last and not car_out and rows[-1]["slot"] != "마무리":
|
||||||
|
bad.append(("L12", f"{label}: 짐을 맡겨 두고 나섰는데 찾으러 돌아오지 않는다"))
|
||||||
# L9 같은 자리 연속
|
# L9 같은 자리 연속
|
||||||
for i in range(1, len(rows)):
|
for i in range(1, len(rows)):
|
||||||
if rows[i]["place"] == rows[i - 1]["place"]:
|
if rows[i]["place"] == rows[i - 1]["place"]:
|
||||||
@ -124,7 +133,7 @@ lines = ["# `/s/stay` 여행 일정 전수 검수표", "",
|
|||||||
f"{sum(len(i['days']) for i in items)}일 · "
|
f"{sum(len(i['days']) for i in items)}일 · "
|
||||||
f"{sum(len(d['stops']) for i in items for d in i['days'])}정거장", "",
|
f"{sum(len(d['stops']) for i in items for d in i['days'])}정거장", "",
|
||||||
"표는 **구워진 payload 를 렌더러와 같은 방식으로 계산한 값**이다(화면에 뜨는 것과 같다).",
|
"표는 **구워진 payload 를 렌더러와 같은 방식으로 계산한 값**이다(화면에 뜨는 것과 같다).",
|
||||||
"규칙 14종(만들 때 거는 것)은 `build_itinerary.py` `audit()` 가 이미 통과시켰고,",
|
"규칙 17종(만들 때 거는 것)은 `build_itinerary.py` `audit()` 가 이미 통과시켰고,",
|
||||||
"이 표는 그 규칙이 **안 보는 논리**를 따로 센다.", "",
|
"이 표는 그 규칙이 **안 보는 논리**를 따로 센다.", "",
|
||||||
"| 코드 | 검수 항목 |", "|---|---|",
|
"| 코드 | 검수 항목 |", "|---|---|",
|
||||||
"| L0 | 21시를 넘겨 화면에서 버려지는 칸이 없다 |",
|
"| L0 | 21시를 넘겨 화면에서 버려지는 칸이 없다 |",
|
||||||
@ -135,10 +144,11 @@ lines = ["# `/s/stay` 여행 일정 전수 검수표", "",
|
|||||||
"| L5 | 바깥 정거장 체류 15분 이상 |",
|
"| L5 | 바깥 정거장 체류 15분 이상 |",
|
||||||
"| L6 | 하루가 14시간을 넘지 않는다 |",
|
"| L6 | 하루가 14시간을 넘지 않는다 |",
|
||||||
"| L7 | 입실 15:00 이후 · 퇴실 11:00 전 |",
|
"| L7 | 입실 15:00 이후 · 퇴실 11:00 전 |",
|
||||||
"| L8 | 하루의 첫 칸·마지막 칸이 숙소 |",
|
"| L8 | 하루의 첫 칸·마지막 칸이 숙소 (**떠나는 날 차 테마는 복귀 없이 끝난다**) |",
|
||||||
"| L9 | 같은 자리가 연달아 서지 않는다 |",
|
"| L9 | 같은 자리가 연달아 서지 않는다 |",
|
||||||
"| L10 | 한 테마 안에서 같은 곳을 두 번 가지 않는다 |",
|
"| L10 | 한 테마 안에서 같은 곳을 두 번 가지 않는다 |",
|
||||||
"| L11 | 걷는 테마에 편도 25분 넘는 이동이 없다 |", ""]
|
"| L11 | 걷는 테마에 편도 25분 넘는 이동이 없다 |",
|
||||||
|
"| L12 | 떠나는 날의 끝맺음이 퇴실 문구와 같다 — 짐을 차에 실었으면 안 돌아오고, 맡겼으면 찾으러 온다 |", ""]
|
||||||
|
|
||||||
fails = []
|
fails = []
|
||||||
for item in items:
|
for item in items:
|
||||||
@ -169,7 +179,7 @@ if fails:
|
|||||||
head += ["| 테마 | 걸린 항목 |", "|---|---|"]
|
head += ["| 테마 | 걸린 항목 |", "|---|---|"]
|
||||||
head += [f"| {n} | {', '.join(sorted({c for c, _ in b}))} |" for n, b in fails]
|
head += [f"| {n} | {', '.join(sorted({c for c, _ in b}))} |" for n, b in fails]
|
||||||
else:
|
else:
|
||||||
head += ["**전부 통과.** 12개 항목 위반 0건."]
|
head += ["**전부 통과.** 13개 항목 위반 0건."]
|
||||||
head.append("")
|
head.append("")
|
||||||
lines = lines[:8] + head + lines[8:]
|
lines = lines[:8] + head + lines[8:]
|
||||||
|
|
||||||
|
|||||||
152
solution/site/scripts/mockup/build_daily.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
"""오늘의 한 장 — 군산의 노래와 문학을 하루에 한 장씩 넘기는 일력.
|
||||||
|
|
||||||
|
★ 왜 이 자리인가 (2026-09-14 대표 의견: "엽서 형식보다 대중가요·시·소설이 더 풍부했으면")
|
||||||
|
렌더러에는 `daily`(오늘의 한 장) 탭이 이미 있는데 **payload 에 그 섹션 자체가 없었다** —
|
||||||
|
자리는 있고 값이 없어 탭이 서지 않던 자리다(README 1.2). 엽서를 갈아 끼우는 대신
|
||||||
|
빈 자리를 채운다. 엽서는 그대로 둔다.
|
||||||
|
|
||||||
|
★ **이 자리는 도시를 소개하는 자리가 아니다** (같은 날 대표 의견: "오늘의 한 장으로 군산을
|
||||||
|
소개하기는 무리다 — 하루에 하나씩밖에 안 알려 주니까"). 일력은 오늘·어제·내일 세 장만
|
||||||
|
화면에 선다. 군산을 펼쳐 보이는 자리는 `build_reading.py`(군산 읽기)가 맡는다.
|
||||||
|
여기는 **날마다 바뀌는 한 장**이고, 그래서 곡·시인·문학을 한 장씩 돌린다.
|
||||||
|
|
||||||
|
★ 받은 자료(`gunsan_365_story_db.xlsx`)를 그대로 싣지 않는다.
|
||||||
|
365행이지만 **실제 주제는 52개**고, 한 주제를 '작품 배경 / 역사 맥락 / 계절 연결 /
|
||||||
|
SNS 한 문장 / 교육용 질문 …' 일곱 가지 운영 슬롯으로 복제해 둔 시드다. 그대로 실으면
|
||||||
|
같은 문장이 일주일에 일곱 번 돈다. 게다가 231행이 `검증필요`, 14행이 `후보`이고
|
||||||
|
시·수필 카테고리는 전부 빈 슬롯이다("…를 찾아 소개하기 위한 **운영용 슬롯**이다") —
|
||||||
|
손님 화면에 나갈 문장이 아니라 우리끼리 쓰는 메모다.
|
||||||
|
→ **소재 목록으로만 쓴다.** 화면에 나가는 문장은 여기서 다시 쓰고, 출처는 그 주제를
|
||||||
|
실제로 설명하는 기관 페이지를 단다.
|
||||||
|
|
||||||
|
★ 365장을 억지로 채우지 않는다. 확인되는 소재가 39개면 39장이다 —
|
||||||
|
렌더러는 오늘 자리가 비면 **다음 장**을 편다(`DailySection.tsx`). 채우려면 같은 글을
|
||||||
|
여러 날짜에 복사해야 하는데, 그건 인용되려고 만든 사이트에서 할 일이 아니다.
|
||||||
|
|
||||||
|
입력: stay-payload-new.json (build_story.py · build_reading.py 까지 끝난 것 —
|
||||||
|
가요 25곡 · 인물 57명 · 문학 8가지를 **전부 거기서 읽는다.** 여기서 손으로 적는 것은 없다)
|
||||||
|
출력: 같은 파일의 theme.sections 에 `daily` 섹션을 넣는다.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SP = Path(__file__).parent
|
||||||
|
SRC = SP / "stay-payload-new.json"
|
||||||
|
|
||||||
|
payload = json.loads(SRC.read_text(encoding="utf-8"))
|
||||||
|
SECTIONS = payload["theme"]["sections"]
|
||||||
|
SECT = {s["id"]: s for s in SECTIONS}
|
||||||
|
|
||||||
|
# ── 문학은 '군산 읽기' 표에서 가져온다 ─────────────────────────────────────
|
||||||
|
# ★ 한 벌만 둔다 (2026-09-14). 문학 카드를 여기에도 적어 두면 한쪽만 고치는 날이 온다 —
|
||||||
|
# 이 레포에서 프롬프트가 두 곳으로 갈려 서버가 세 종류를 못 만들던 것과 같은 종류의 사고다.
|
||||||
|
# 손으로 적는 자리는 `build_reading.py` `ITEMS` 하나다.
|
||||||
|
LIT_GROUP = "문학"
|
||||||
|
|
||||||
|
# ── 시 — 군산이 낳은 시인 ────────────────────────────────────────────────────
|
||||||
|
# ★ 시 작품을 싣지 않는다. 확인되는 것은 '이 도시에서 난 시인' 까지다 —
|
||||||
|
# 받은 자료의 시 카테고리도 전부 빈 슬롯이었고(위 머리말), 군산을 노래한 시를
|
||||||
|
# 출처와 함께 확인한 것이 아직 없다. 없는 것을 지어내느니 시인을 세운다.
|
||||||
|
POETS = ["고은", "이병훈", "심호택", "문효치", "이연주", "주하림"]
|
||||||
|
|
||||||
|
|
||||||
|
def _items(section_id: str) -> list[dict]:
|
||||||
|
section = SECT.get(section_id)
|
||||||
|
if not section or not section.get("data"):
|
||||||
|
raise SystemExit(f"{section_id} 섹션이 비었다 — build_story.py · build_reading.py 를 먼저 돌린다")
|
||||||
|
return json.loads(section["data"])["items"]
|
||||||
|
|
||||||
|
|
||||||
|
# ── 가요 25곡 → 25장 ─────────────────────────────────────────────────────────
|
||||||
|
# 가요 다방(`songs`)에 이미 선 곡을 그대로 읽는다. 목록을 두 벌로 두면 한쪽만 고치게 된다.
|
||||||
|
song_pages = []
|
||||||
|
for song in _items("songs"):
|
||||||
|
facts = []
|
||||||
|
if song.get("year"):
|
||||||
|
facts.append(f"{song['year']}년 곡")
|
||||||
|
credit = " · ".join(
|
||||||
|
part for part in (
|
||||||
|
f"작사 {song['lyricist']}" if song.get("lyricist") else "",
|
||||||
|
f"작곡 {song['composer']}" if song.get("composer") else "",
|
||||||
|
) if part)
|
||||||
|
if credit:
|
||||||
|
facts.append(credit)
|
||||||
|
body = ". ".join(facts)
|
||||||
|
body = f"{body}. {song['story']}" if body else song["story"]
|
||||||
|
song_pages.append({"category": "대중가요",
|
||||||
|
"title": f"「{song['title']}」 · {song['artist']}",
|
||||||
|
"body": body,
|
||||||
|
"verified": song.get("verified", "확인필요"),
|
||||||
|
"source": song["source"]})
|
||||||
|
|
||||||
|
# ── 시인 6명 → 6장 ───────────────────────────────────────────────────────────
|
||||||
|
people = {p["name"]: p for p in _items("people")}
|
||||||
|
poet_pages = []
|
||||||
|
for name in POETS:
|
||||||
|
person = people.get(name)
|
||||||
|
if not person:
|
||||||
|
raise SystemExit(f"인물 열전에 {name} 이 없다 — build_story.py 의 PEOPLE 을 본다")
|
||||||
|
years = f"{person['years']} · " if person.get("years") else ""
|
||||||
|
poet_pages.append({"category": "시", "title": f"시인 {name}",
|
||||||
|
"body": f"{years}{person['oneLine']}",
|
||||||
|
"verified": person.get("verified", "확인필요"),
|
||||||
|
"source": person["source"]})
|
||||||
|
|
||||||
|
lit_pages = [{"category": LIT_GROUP, "title": x["title"], "body": x["body"],
|
||||||
|
# 2026-09-14 부로 build_reading.py 의 ITEMS 에서 verified 필드를 뺐다
|
||||||
|
# (대표: "확인필요 없애고 링크는 네이버 링크로") — 여기 기본값은 그 결정을 따른다.
|
||||||
|
"verified": x.get("verified", "확인"), "source": x["source"]}
|
||||||
|
for x in _items("reading") if x["group"] == LIT_GROUP]
|
||||||
|
|
||||||
|
# ── 섞고, 날짜를 준다 ────────────────────────────────────────────────────────
|
||||||
|
# ★ 같은 분류를 연달아 세우지 않는다 — 일력은 어제·오늘·내일 세 장이 한 화면에 걸린다
|
||||||
|
# (`DailySection.tsx`). 가요만 석 장 나란히 서면 일력이 아니라 음반 목록으로 보인다.
|
||||||
|
others = lit_pages + poet_pages
|
||||||
|
pages, si, oi = [], 0, 0
|
||||||
|
total = len(song_pages) + len(others)
|
||||||
|
for step in range(total):
|
||||||
|
take_other = oi < len(others) and (si >= len(song_pages)
|
||||||
|
or (step + 1) * len(others) / total > oi)
|
||||||
|
if take_other:
|
||||||
|
pages.append(others[oi])
|
||||||
|
oi += 1
|
||||||
|
else:
|
||||||
|
pages.append(song_pages[si])
|
||||||
|
si += 1
|
||||||
|
|
||||||
|
# ★ 날짜는 **고르게 편다**. 곡이 나온 날·작가의 생일에 맞추고 싶지만 그 날짜를 출처로
|
||||||
|
# 확인한 것이 없다 — 지어낸 날짜를 일력에 박으면 틀린 줄 모르고 해마다 틀린다.
|
||||||
|
for index, page in enumerate(pages):
|
||||||
|
day = round(index * 365 / len(pages)) + 1
|
||||||
|
month, rest = 1, day
|
||||||
|
for length in (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31):
|
||||||
|
if rest <= length:
|
||||||
|
break
|
||||||
|
rest -= length
|
||||||
|
month += 1
|
||||||
|
page["monthDay"] = f"{month:02d}-{rest:02d}"
|
||||||
|
|
||||||
|
env = {"kind": "daily", "version": 1, "title": "오늘의 한 장",
|
||||||
|
"subtitle": f"군산의 노래와 문학 {len(pages)}장 — 날마다 한 장씩 넘어갑니다",
|
||||||
|
"items": [{"monthDay": p["monthDay"], "category": p["category"], "title": p["title"],
|
||||||
|
"body": p["body"], "verified": p["verified"], "source": p["source"]}
|
||||||
|
for p in pages]}
|
||||||
|
|
||||||
|
# 없으면 만든다 — 원본 payload 에는 `daily` 섹션 자체가 없다(그래서 탭이 안 섰다).
|
||||||
|
if "daily" not in SECT:
|
||||||
|
# 형제(가요 다방) 옆에 둔다. 탭 순서는 렌더러가 정하므로 배열 순서는 보기 좋으라고 맞추는 것이다.
|
||||||
|
SECTIONS.insert(next(i for i, s in enumerate(SECTIONS) if s["id"] == "songs") + 1,
|
||||||
|
{"id": "daily", "name": "오늘의 한 장", "enabled": False, "locked": False})
|
||||||
|
SECT = {s["id"]: s for s in SECTIONS}
|
||||||
|
SECT["daily"]["data"] = json.dumps(env, ensure_ascii=False)
|
||||||
|
|
||||||
|
SRC.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
for page in pages:
|
||||||
|
counts[page["category"]] = counts.get(page["category"], 0) + 1
|
||||||
|
unverified = sum(1 for p in pages if p["verified"] != "확인")
|
||||||
|
print(f"오늘의 한 장 {len(pages)}장 — "
|
||||||
|
+ " · ".join(f"{k} {v}" for k, v in counts.items())
|
||||||
|
+ f" (확인필요 {unverified}장)")
|
||||||
|
print(f" 날짜 {pages[0]['monthDay']} … {pages[-1]['monthDay']}")
|
||||||
@ -122,8 +122,14 @@ STAY_ACTION = {
|
|||||||
# 일정도 있어"). 아침(머뭄)과 체크아웃(머뭄)을 따로 세웠더니 **같은 자리에 두 칸이 연달아**
|
# 일정도 있어"). 아침(머뭄)과 체크아웃(머뭄)을 따로 세웠더니 **같은 자리에 두 칸이 연달아**
|
||||||
# 섰다 — 손님은 그 사이에 아무 데도 가지 않는다. 한 칸이 맞다.
|
# 섰다 — 손님은 그 사이에 아무 데도 가지 않는다. 한 칸이 맞다.
|
||||||
# 마지막 날 아침은 체류 범위가 다르다 — 짐을 싸고 퇴실까지 한 칸에서 끝낸다.
|
# 마지막 날 아침은 체류 범위가 다르다 — 짐을 싸고 퇴실까지 한 칸에서 끝낸다.
|
||||||
|
# ★ 퇴실 뒤가 **차로 온 손님과 걸어 온 손님이 갈리는 자리**다 (2026-09-14 대표 의견).
|
||||||
|
# 차로 온 손님은 짐을 싣고 그대로 나선다 — 짐을 맡겨 두고 저녁에 숙소로 되돌아오는 동선은
|
||||||
|
# 차가 있는 사람에게는 한 번 더 들르라는 말이 된다. 그래서 그 하루는 **마지막 정거장에서 끝난다**.
|
||||||
|
# 짐 보관은 걸어 다니는 손님에게 필요한 것이고, 그 테마만 저녁에 찾으러 돌아온다.
|
||||||
"아침(퇴실)": ((45, 75, 115),
|
"아침(퇴실)": ((45, 75, 115),
|
||||||
"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요."),
|
"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요."),
|
||||||
|
"아침(퇴실·차)": ((45, 75, 115),
|
||||||
|
"아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 차에 싣고 그대로 나섭니다."),
|
||||||
"머뭄에서 쉬는 날": ((120, 210, 360),
|
"머뭄에서 쉬는 날": ((120, 210, 360),
|
||||||
"오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다."),
|
"오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다."),
|
||||||
# ★ 10분짜리 칸을 시간표에 세우지 않는다 (2026-09-10 사장님: "10분 정도 왜 잡아 놓는 거야?").
|
# ★ 10분짜리 칸을 시간표에 세우지 않는다 (2026-09-10 사장님: "10분 정도 왜 잡아 놓는 거야?").
|
||||||
@ -203,6 +209,17 @@ SKELETON = {
|
|||||||
}
|
}
|
||||||
# 이 뼈대의 첫 칸(아침 · 퇴실)은 **11시 전에 끝나야** 한다 — payload 의 check_out_time.
|
# 이 뼈대의 첫 칸(아침 · 퇴실)은 **11시 전에 끝나야** 한다 — payload 의 check_out_time.
|
||||||
CHECKOUT_KINDS = {"last", "last_plus", "last_island"}
|
CHECKOUT_KINDS = {"last", "last_plus", "last_island"}
|
||||||
|
# ★ 차로 온 손님의 마지막 날은 **복귀 칸이 없다** (2026-09-14 대표 의견).
|
||||||
|
# 퇴실할 때 짐을 차에 싣고 나서므로 숙소로 되돌아올 일이 없다 — 마지막 정거장이 하루의 끝이다.
|
||||||
|
# 뼈대를 따로 적지 않고 **복귀 칸만 떼어** 만든다. 그래야 도보 뼈대를 고칠 때 차 뼈대가 같이 따라온다
|
||||||
|
# (두 벌로 두면 한쪽만 고치고 넘어가는 자리가 생긴다 — 이 파일이 실제로 그래서 틀린 적이 있다).
|
||||||
|
CAR_KINDS = set()
|
||||||
|
for _base in ("last", "last_plus", "last_island"):
|
||||||
|
_car = _base + "_car"
|
||||||
|
assert SKELETON[_base][-1] == "마무리@"
|
||||||
|
SKELETON[_car] = SKELETON[_base][:-1]
|
||||||
|
CHECKOUT_KINDS.add(_car)
|
||||||
|
CAR_KINDS.add(_car)
|
||||||
# 하루의 시작 = **머뭄에서 아침을 여는 시각**이다(첫날만 입실 시각).
|
# 하루의 시작 = **머뭄에서 아침을 여는 시각**이다(첫날만 입실 시각).
|
||||||
START = {"first": "15:00", "first_late": "19:00", "mid": "08:30", "mid_island": "07:40",
|
START = {"first": "15:00", "first_late": "19:00", "mid": "08:30", "mid_island": "07:40",
|
||||||
"mid_late": "09:40", "mid_rest": "10:00", "last": "08:10", "last_plus": "07:50",
|
"mid_late": "09:40", "mid_rest": "10:00", "last": "08:10", "last_plus": "07:50",
|
||||||
@ -211,6 +228,10 @@ START = {"first": "15:00", "first_late": "19:00", "mid": "08:30", "mid_island":
|
|||||||
FLOOR = {"first": "15:00", "first_late": "18:00", "mid": "07:20", "mid_island": "07:00",
|
FLOOR = {"first": "15:00", "first_late": "18:00", "mid": "07:20", "mid_island": "07:00",
|
||||||
"mid_late": "09:00", "mid_rest": "09:30", "last": "07:20", "last_plus": "07:20",
|
"mid_late": "09:00", "mid_rest": "09:30", "last": "07:20", "last_plus": "07:20",
|
||||||
"last_island": "07:20"}
|
"last_island": "07:20"}
|
||||||
|
for _car in CAR_KINDS:
|
||||||
|
START[_car] = START[_car[:-4]]
|
||||||
|
FLOOR[_car] = FLOOR[_car[:-4]]
|
||||||
|
|
||||||
DAY_LABEL = ["첫째 날", "둘째 날", "셋째 날"]
|
DAY_LABEL = ["첫째 날", "둘째 날", "셋째 날"]
|
||||||
DURATION = {2: "1박 2일", 3: "2박 3일"}
|
DURATION = {2: "1박 2일", 3: "2박 3일"}
|
||||||
|
|
||||||
@ -221,9 +242,9 @@ def _minutes(text: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _action(slot: str, kind: str) -> tuple:
|
def _action(slot: str, kind: str) -> tuple:
|
||||||
"""머뭄 칸의 (체류범위, 한 줄). 마지막 날 아침만 퇴실을 겸한다."""
|
"""머뭄 칸의 (체류범위, 한 줄). 마지막 날 아침만 퇴실을 겸하고, 차 테마는 문구가 다르다."""
|
||||||
if slot == "아침" and kind in CHECKOUT_KINDS:
|
if slot == "아침" and kind in CHECKOUT_KINDS:
|
||||||
return STAY_ACTION["아침(퇴실)"]
|
return STAY_ACTION["아침(퇴실·차)" if kind in CAR_KINDS else "아침(퇴실)"]
|
||||||
return STAY_ACTION[slot]
|
return STAY_ACTION[slot]
|
||||||
|
|
||||||
|
|
||||||
@ -394,7 +415,7 @@ THEMES = [
|
|||||||
("섬까지 다녀오는 1박 2일", "1박 2일", "차를 가져온 손님",
|
("섬까지 다녀오는 1박 2일", "1박 2일", "차를 가져온 손님",
|
||||||
"첫날은 걸어서 도심, 이튿날은 다리를 건너 섬입니다. 두 날의 성격이 아주 다릅니다.",
|
"첫날은 걸어서 도심, 이튿날은 다리를 건너 섬입니다. 두 날의 성격이 아주 다릅니다.",
|
||||||
[("first", ["히로쓰 가옥", "초원사진관", "빈해원", "군산 내항"]),
|
[("first", ["히로쓰 가옥", "초원사진관", "빈해원", "군산 내항"]),
|
||||||
("last_island", ["고군산군도 선유도", "국제반점"])]),
|
("last_island_car", ["고군산군도 선유도", "국제반점"])]),
|
||||||
("걸어서만 도는 1박 2일", "1박 2일", "차 없이 오는 손님",
|
("걸어서만 도는 1박 2일", "1박 2일", "차 없이 오는 손님",
|
||||||
"모든 정거장이 머뭄에서 걸어 닿습니다. 이틀 동안 차를 한 번도 타지 않습니다.",
|
"모든 정거장이 머뭄에서 걸어 닿습니다. 이틀 동안 차를 한 번도 타지 않습니다.",
|
||||||
[("first", ["히로쓰 가옥", "초원사진관", "일흥옥", "군산 내항"]),
|
[("first", ["히로쓰 가옥", "초원사진관", "일흥옥", "군산 내항"]),
|
||||||
@ -402,11 +423,11 @@ THEMES = [
|
|||||||
("비가 와도 되는 1박 2일", "1박 2일", "비 예보를 보고 오는 손님",
|
("비가 와도 되는 1박 2일", "1박 2일", "비 예보를 보고 오는 손님",
|
||||||
"실내가 이어집니다. 비가 그치기를 기다리지 않아도 하루가 찹니다.",
|
"실내가 이어집니다. 비가 그치기를 기다리지 않아도 하루가 찹니다.",
|
||||||
[("first", ["군산근대역사박물관", "군산근대미술관", "빈해원", "군산 내항"]),
|
[("first", ["군산근대역사박물관", "군산근대미술관", "빈해원", "군산 내항"]),
|
||||||
("last", ["군산근대건축관", "일흥옥", "째보선창 근처 카페"])]),
|
("last_car", ["군산근대건축관", "일흥옥", "째보선창 근처 카페"])]),
|
||||||
("근대건축만 보는 1박 2일", "1박 2일", "건물을 보러 오는 손님",
|
("근대건축만 보는 1박 2일", "1박 2일", "건물을 보러 오는 손님",
|
||||||
"1900년대 건물만 골라 돕니다. 머뭄도 그 시기에 지은 집입니다.",
|
"1900년대 건물만 골라 돕니다. 머뭄도 그 시기에 지은 집입니다.",
|
||||||
[("first", ["구 군산세관 본관", "군산근대건축관", "명월갈비", "군산 내항"]),
|
[("first", ["구 군산세관 본관", "군산근대건축관", "명월갈비", "군산 내항"]),
|
||||||
("last", ["군산근대미술관", "일흥옥", "해망굴"])]),
|
("last_car", ["군산근대미술관", "일흥옥", "해망굴"])]),
|
||||||
("이성당으로 시작하는 1박 2일", "1박 2일", "빵을 좋아하는 손님",
|
("이성당으로 시작하는 1박 2일", "1박 2일", "빵을 좋아하는 손님",
|
||||||
"이튿날 아침을 빵으로 엽니다. 문 여는 시간에 맞춰 걸어갑니다.",
|
"이튿날 아침을 빵으로 엽니다. 문 여는 시간에 맞춰 걸어갑니다.",
|
||||||
[("first", ["초원사진관", "우체통거리", "도란", "군산 내항"]),
|
[("first", ["초원사진관", "우체통거리", "도란", "군산 내항"]),
|
||||||
@ -414,7 +435,7 @@ THEMES = [
|
|||||||
("철길마을까지 가는 1박 2일", "1박 2일", "사진을 찍는 손님",
|
("철길마을까지 가는 1박 2일", "1박 2일", "사진을 찍는 손님",
|
||||||
"빛이 좋은 자리만 이어 붙였습니다. 철길마을은 오전 빛이 낫습니다.",
|
"빛이 좋은 자리만 이어 붙였습니다. 철길마을은 오전 빛이 낫습니다.",
|
||||||
[("first", ["초원사진관", "동국사", "국제반점", "군산 내항"]),
|
[("first", ["초원사진관", "동국사", "국제반점", "군산 내항"]),
|
||||||
("last", ["경암동 철길마을", "일흥옥", "군산 시간여행마을"])]),
|
("last_car", ["경암동 철길마을", "일흥옥", "군산 시간여행마을"])]),
|
||||||
("항구만 도는 1박 2일", "1박 2일", "바다가 보고 싶은 손님",
|
("항구만 도는 1박 2일", "1박 2일", "바다가 보고 싶은 손님",
|
||||||
"부잔교에서 째보선창까지, 이틀 동안 물가만 걷습니다.",
|
"부잔교에서 째보선창까지, 이틀 동안 물가만 걷습니다.",
|
||||||
[("first", ["군산 내항 부잔교", "해망굴", "빈해원", "군산 내항"]),
|
[("first", ["군산 내항 부잔교", "해망굴", "빈해원", "군산 내항"]),
|
||||||
@ -422,15 +443,15 @@ THEMES = [
|
|||||||
("소설 『탁류』를 따라 걷는 1박 2일", "1박 2일", "채만식을 읽어 본 손님",
|
("소설 『탁류』를 따라 걷는 1박 2일", "1박 2일", "채만식을 읽어 본 손님",
|
||||||
"소설이 지나간 자리를 순서대로 밟습니다. 세관·선창·문학관이 한 줄로 이어집니다.",
|
"소설이 지나간 자리를 순서대로 밟습니다. 세관·선창·문학관이 한 줄로 이어집니다.",
|
||||||
[("first", ["째보선창", "군산 내항 부잔교", "한일옥", "군산 내항"]),
|
[("first", ["째보선창", "군산 내항 부잔교", "한일옥", "군산 내항"]),
|
||||||
("last", ["채만식문학관", "일흥옥", "구 군산세관 본관"])]),
|
("last_car", ["채만식문학관", "일흥옥", "구 군산세관 본관"])]),
|
||||||
("아이와 함께 1박 2일", "1박 2일", "아이를 데려온 손님",
|
("아이와 함께 1박 2일", "1박 2일", "아이를 데려온 손님",
|
||||||
"걷는 거리가 짧고, 아이가 지루해할 자리를 뺐습니다.",
|
"걷는 거리가 짧고, 아이가 지루해할 자리를 뺐습니다.",
|
||||||
[("first", ["초원사진관", "우체통거리", "만남스넥", "군산 내항"]),
|
[("first", ["초원사진관", "우체통거리", "만남스넥", "군산 내항"]),
|
||||||
("last", ["경암동 철길마을", "이성당", "월명호수"])]),
|
("last_car", ["경암동 철길마을", "이성당", "월명호수"])]),
|
||||||
("늦게 도착하는 1박 2일", "1박 2일", "퇴근하고 오는 손님",
|
("늦게 도착하는 1박 2일", "1박 2일", "퇴근하고 오는 손님",
|
||||||
"첫날은 저녁 한 끼로 끝냅니다. 대신 이튿날을 아침부터 저녁까지 씁니다.",
|
"첫날은 저녁 한 끼로 끝냅니다. 대신 이튿날을 아침부터 저녁까지 씁니다.",
|
||||||
[("first_late", ["명월갈비"]),
|
[("first_late", ["명월갈비"]),
|
||||||
("last_plus", ["군산근대역사박물관", "한일옥", "군산근대건축관", "해망굴"])]),
|
("last_plus_car", ["군산근대역사박물관", "한일옥", "군산근대건축관", "해망굴"])]),
|
||||||
|
|
||||||
("천천히 도는 2박 3일", "2박 3일", "쉬러 오는 손님",
|
("천천히 도는 2박 3일", "2박 3일", "쉬러 오는 손님",
|
||||||
"하루에 두어 곳만 봅니다. 마당에 앉아 있는 시간을 일정에 넣었습니다.",
|
"하루에 두어 곳만 봅니다. 마당에 앉아 있는 시간을 일정에 넣었습니다.",
|
||||||
@ -441,7 +462,7 @@ THEMES = [
|
|||||||
"가운데 날을 섬에 다 씁니다. 첫날과 마지막 날은 걸어서 도심입니다.",
|
"가운데 날을 섬에 다 씁니다. 첫날과 마지막 날은 걸어서 도심입니다.",
|
||||||
[("first", ["구 군산세관 본관", "군산근대건축관", "빈해원", "군산 내항"]),
|
[("first", ["구 군산세관 본관", "군산근대건축관", "빈해원", "군산 내항"]),
|
||||||
("mid_island", ["고군산군도 선유도", "장자도", "국제반점", "명월갈비"]),
|
("mid_island", ["고군산군도 선유도", "장자도", "국제반점", "명월갈비"]),
|
||||||
("last", ["초원사진관", "일흥옥", "이성당"])]),
|
("last_car", ["초원사진관", "일흥옥", "이성당"])]),
|
||||||
("골목을 세 번 나눠 걷는 2박 3일", "2박 3일", "차 없이 오는 손님",
|
("골목을 세 번 나눠 걷는 2박 3일", "2박 3일", "차 없이 오는 손님",
|
||||||
"원도심을 세 덩이로 나눠 하루에 하나씩 걷습니다. 같은 길을 두 번 걷지 않습니다.",
|
"원도심을 세 덩이로 나눠 하루에 하나씩 걷습니다. 같은 길을 두 번 걷지 않습니다.",
|
||||||
[("first", ["히로쓰 가옥", "초원사진관", "일흥옥", "군산 내항"]),
|
[("first", ["히로쓰 가옥", "초원사진관", "일흥옥", "군산 내항"]),
|
||||||
@ -451,22 +472,22 @@ THEMES = [
|
|||||||
"끼니가 중심입니다. 사이에 걸을 자리를 넣어 배를 비웁니다.",
|
"끼니가 중심입니다. 사이에 걸을 자리를 넣어 배를 비웁니다.",
|
||||||
[("first", ["물밀소", "초원사진관", "명월갈비", "군산 내항"]),
|
[("first", ["물밀소", "초원사진관", "명월갈비", "군산 내항"]),
|
||||||
("mid", ["군산 시간여행마을", "일흥옥", "이성당", "우체통거리", "빈해원"]),
|
("mid", ["군산 시간여행마을", "일흥옥", "이성당", "우체통거리", "빈해원"]),
|
||||||
("last", ["동국사", "국제반점", "군산근대역사박물관"])]),
|
("last_car", ["동국사", "국제반점", "군산근대역사박물관"])]),
|
||||||
("아무 데도 안 가는 2박 3일", "2박 3일", "쉬려고만 오는 손님",
|
("아무 데도 안 가는 2박 3일", "2박 3일", "쉬려고만 오는 손님",
|
||||||
"가운데 날은 대문 밖으로 두 번만 나갑니다. 나머지는 마당과 카페 공간에서 보냅니다.",
|
"가운데 날은 대문 밖으로 두 번만 나갑니다. 나머지는 마당과 카페 공간에서 보냅니다.",
|
||||||
[("first", ["히로쓰 가옥", "초원사진관", "도란", "군산 내항"]),
|
[("first", ["히로쓰 가옥", "초원사진관", "도란", "군산 내항"]),
|
||||||
("mid_rest", ["만남스넥", "일흥옥"]),
|
("mid_rest", ["만남스넥", "일흥옥"]),
|
||||||
("last", ["동국사", "물밀소", "월명호수"])]),
|
("last_car", ["동국사", "물밀소", "월명호수"])]),
|
||||||
("사진만 찍는 2박 3일", "2박 3일", "사진을 찍는 손님",
|
("사진만 찍는 2박 3일", "2박 3일", "사진을 찍는 손님",
|
||||||
"시간대에 맞춰 자리를 배치했습니다. 철길마을은 오전, 부잔교는 해 질 때입니다.",
|
"시간대에 맞춰 자리를 배치했습니다. 철길마을은 오전, 부잔교는 해 질 때입니다.",
|
||||||
[("first", ["초원사진관", "동국사", "도란", "군산 내항"]),
|
[("first", ["초원사진관", "동국사", "도란", "군산 내항"]),
|
||||||
("mid", ["경암동 철길마을", "만남스넥", "째보선창", "해망굴", "국제반점"]),
|
("mid", ["경암동 철길마을", "만남스넥", "째보선창", "해망굴", "국제반점"]),
|
||||||
("last", ["우체통거리", "일흥옥", "군산근대미술관"])]),
|
("last_car", ["우체통거리", "일흥옥", "군산근대미술관"])]),
|
||||||
("비가 와도 되는 2박 3일", "2박 3일", "비 예보를 보고 오는 손님",
|
("비가 와도 되는 2박 3일", "2박 3일", "비 예보를 보고 오는 손님",
|
||||||
"사흘 내리 비가 와도 도는 순서가 바뀌지 않습니다. 실내와 처마 밑만 잇습니다.",
|
"사흘 내리 비가 와도 도는 순서가 바뀌지 않습니다. 실내와 처마 밑만 잇습니다.",
|
||||||
[("first", ["군산근대역사박물관", "군산근대미술관", "빈해원", "군산 내항"]),
|
[("first", ["군산근대역사박물관", "군산근대미술관", "빈해원", "군산 내항"]),
|
||||||
("mid", ["군산근대건축관", "일흥옥", "째보선창 근처 카페", "동국사다온", "명월갈비"]),
|
("mid", ["군산근대건축관", "일흥옥", "째보선창 근처 카페", "동국사다온", "명월갈비"]),
|
||||||
("last", ["채만식문학관", "한일옥", "이성당"])]),
|
("last_car", ["채만식문학관", "한일옥", "이성당"])]),
|
||||||
("늦게 시작하는 2박 3일", "2박 3일", "아침이 힘든 손님",
|
("늦게 시작하는 2박 3일", "2박 3일", "아침이 힘든 손님",
|
||||||
"가운데 날을 열한 시에 엽니다. 오전을 비우고 저녁을 늦게까지 씁니다.",
|
"가운데 날을 열한 시에 엽니다. 오전을 비우고 저녁을 늦게까지 씁니다.",
|
||||||
[("first", ["히로쓰 가옥", "우체통거리", "도란", "군산 내항"]),
|
[("first", ["히로쓰 가옥", "우체통거리", "도란", "군산 내항"]),
|
||||||
@ -476,7 +497,7 @@ THEMES = [
|
|||||||
"가운데 날은 아침에 나가 저녁에 들어옵니다. 섬에서 하루를 통째로 씁니다.",
|
"가운데 날은 아침에 나가 저녁에 들어옵니다. 섬에서 하루를 통째로 씁니다.",
|
||||||
[("first", ["군산 내항 부잔교", "해망굴", "빈해원", "군산 내항"]),
|
[("first", ["군산 내항 부잔교", "해망굴", "빈해원", "군산 내항"]),
|
||||||
("mid_island", ["고군산군도 선유도", "장자도", "한일옥", "명월갈비"]),
|
("mid_island", ["고군산군도 선유도", "장자도", "한일옥", "명월갈비"]),
|
||||||
("last", ["군산근대건축관", "만남스넥", "경암동 철길마을"])]),
|
("last_car", ["군산근대건축관", "만남스넥", "경암동 철길마을"])]),
|
||||||
("소설 『탁류』를 따라 걷는 2박 3일", "2박 3일", "채만식을 읽어 본 손님",
|
("소설 『탁류』를 따라 걷는 2박 3일", "2박 3일", "채만식을 읽어 본 손님",
|
||||||
"소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다.",
|
"소설의 순서대로 사흘을 나눴습니다. 선창에서 시작해 문학관에서 끝냅니다.",
|
||||||
[("first", ["째보선창", "군산 내항 부잔교", "일흥옥", "군산 내항"]),
|
[("first", ["째보선창", "군산 내항 부잔교", "일흥옥", "군산 내항"]),
|
||||||
@ -486,7 +507,7 @@ THEMES = [
|
|||||||
"기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다.",
|
"기찻길과 물길을 하루씩 나눠 봅니다. 마지막 날은 건물만 봅니다.",
|
||||||
[("first", ["경암동 철길마을", "초원사진관", "국제반점", "군산 내항"]),
|
[("first", ["경암동 철길마을", "초원사진관", "국제반점", "군산 내항"]),
|
||||||
("mid", ["군산 내항 부잔교", "한일옥", "째보선창", "해망굴", "명월갈비"]),
|
("mid", ["군산 내항 부잔교", "한일옥", "째보선창", "해망굴", "명월갈비"]),
|
||||||
("last", ["구 군산세관 본관", "일흥옥", "군산근대미술관"])]),
|
("last_car", ["구 군산세관 본관", "일흥옥", "군산근대미술관"])]),
|
||||||
]
|
]
|
||||||
|
|
||||||
# 테마 이름 → (날짜별 시작 시각, 체류 배율). 시각과 배율이 그 테마의 성격이다 —
|
# 테마 이름 → (날짜별 시작 시각, 체류 배율). 시각과 배율이 그 테마의 성격이다 —
|
||||||
@ -522,11 +543,21 @@ for name, duration, audience, why, days in THEMES:
|
|||||||
built = [build_day(kind, places, DAY_LABEL[i],
|
built = [build_day(kind, places, DAY_LABEL[i],
|
||||||
(starts[i] if starts and i < len(starts) else None), pace)
|
(starts[i] if starts and i < len(starts) else None), pace)
|
||||||
for i, (kind, places) in enumerate(days)]
|
for i, (kind, places) in enumerate(days)]
|
||||||
items.append({"name": name, "duration": duration, "audience": audience, "why": why,
|
# ★ 차 필요 여부를 손님이 테마를 **고르기 전에** 보이게 한다 (2026-09-14 대표: "차로
|
||||||
"days": built})
|
# 가는거 아닌거는 대중교통 이용이라고 따로 표시해두는게 어때"). 이 신호는 원래
|
||||||
|
# 마지막 날 체크아웃 칸의 note 에만 있었다 — 카드를 펼쳐 마지막 정거장까지 봐야
|
||||||
|
# 나오는 자리라, 테마 이름만 보고 고르는 손님에게는 안 보였다.
|
||||||
|
# ★ '대중교통'이라고는 쓰지 않는다. 이 7개 테마는 버스·택시가 필요한 게 아니라
|
||||||
|
# 전 정거장이 머뭄에서 1.5km 안(WALK_LIMIT_KM)이라 걸어서 다 된다 — 실제로
|
||||||
|
# 안 쓰는 교통수단을 이름에 올리면 손님이 없는 버스편을 찾아보게 만든다.
|
||||||
|
needs_car = days[-1][0] in CAR_KINDS
|
||||||
|
tag = "차량 필요" if needs_car else "도보로 충분 (차 없이 가능)"
|
||||||
|
items.append({"name": name, "duration": duration, "audience": f"{audience} · {tag}",
|
||||||
|
"why": why, "days": built})
|
||||||
|
|
||||||
env = {"kind": "itinerary", "version": 1, "title": "추천 일정",
|
env = {"kind": "itinerary", "version": 1, "title": "추천 일정",
|
||||||
"subtitle": "머뭄에서 나서고 머뭄으로 돌아옵니다", "items": items}
|
"subtitle": "머뭄에서 나서고 머뭄으로 돌아옵니다 — 떠나는 날만 그대로 길을 나섭니다",
|
||||||
|
"items": items}
|
||||||
SECT["itinerary"]["data"] = json.dumps(env, ensure_ascii=False)
|
SECT["itinerary"]["data"] = json.dumps(env, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
@ -543,6 +574,8 @@ FOOD = {"빈해원", "일흥옥", "한일옥", "명월갈비", "국제반점", "
|
|||||||
INDOOR = {"군산근대역사박물관", "군산근대건축관", "군산근대미술관", "채만식문학관", STAY} | FOOD
|
INDOOR = {"군산근대역사박물관", "군산근대건축관", "군산근대미술관", "채만식문학관", STAY} | FOOD
|
||||||
NEEDS_CAR = {"고군산군도 선유도", "장자도", "채만식문학관", "경암동 철길마을", "월명호수", "은적사"}
|
NEEDS_CAR = {"고군산군도 선유도", "장자도", "채만식문학관", "경암동 철길마을", "월명호수", "은적사"}
|
||||||
REST_SLOTS = {"쉼", "머뭄에서 쉬는 날"}
|
REST_SLOTS = {"쉼", "머뭄에서 쉬는 날"}
|
||||||
|
# 마지막 날을 **복귀 없이** 끝내는 테마 — 차로 온 손님이다. 뼈대에서 곧바로 뽑는다(손으로 적지 않는다).
|
||||||
|
CAR_LAST_THEMES = {name for name, _dur, _aud, _why, days in THEMES if days[-1][0] in CAR_KINDS}
|
||||||
NIGHT_ONLY = {"군산 내항"}
|
NIGHT_ONLY = {"군산 내항"}
|
||||||
BAR = {"도란"}
|
BAR = {"도란"}
|
||||||
DAYS_OF = {"1박 2일": 2, "2박 3일": 3}
|
DAYS_OF = {"1박 2일": 2, "2박 3일": 3}
|
||||||
@ -564,13 +597,24 @@ def audit(built: list[dict]) -> list[str]:
|
|||||||
slots = [s["name"].split(" · ")[0] for s in day["stops"]]
|
slots = [s["name"].split(" · ")[0] for s in day["stops"]]
|
||||||
places = [s["name"].split(" · ")[-1] for s in day["stops"]]
|
places = [s["name"].split(" · ")[-1] for s in day["stops"]]
|
||||||
seen_all += [p for p in places if p != STAY]
|
seen_all += [p for p in places if p != STAY]
|
||||||
|
last_day = day is item["days"][-1]
|
||||||
|
car_end = last_day and item["name"] in CAR_LAST_THEMES
|
||||||
|
|
||||||
# ① 하루의 처음과 끝은 머뭄
|
# ① 하루의 처음과 끝은 머뭄 — 단, 차로 온 손님의 마지막 날은 짐을 싣고 떠난 뒤라 복귀가 없다
|
||||||
if places[0] != STAY:
|
if places[0] != STAY:
|
||||||
flag(item, day, f"1번이 머뭄이 아니다 ({places[0]})")
|
flag(item, day, f"1번이 머뭄이 아니다 ({places[0]})")
|
||||||
if places[-1] != STAY:
|
if places[-1] != STAY and not car_end:
|
||||||
flag(item, day, f"마지막이 머뭄이 아니다 ({places[-1]})")
|
flag(item, day, f"마지막이 머뭄이 아니다 ({places[-1]})")
|
||||||
|
|
||||||
|
# ⑰ 떠나는 날의 끝맺음이 테마와 맞는가 (2026-09-14)
|
||||||
|
# 차 테마인데 복귀 칸이 남아 있으면 짐을 차에 싣고 숙소로 다시 오는 일정이 되고,
|
||||||
|
# 도보 테마인데 복귀 칸이 없으면 맡긴 짐을 못 찾고 하루가 끝난다. 둘 다 화면만 봐서는
|
||||||
|
# 멀쩡해 보인다 — 뼈대를 바꿀 때 한쪽만 고치고 넘어가는 자리라 검사에 둔다.
|
||||||
|
if car_end and (places[-1] == STAY or slots[-1] == "마무리"):
|
||||||
|
flag(item, day, f"차 테마인데 복귀 칸이 남았다 ({slots[-1]})")
|
||||||
|
if last_day and not car_end and slots[-1] != "마무리":
|
||||||
|
flag(item, day, f"도보 테마인데 짐을 찾는 마무리 칸이 없다 ({slots[-1]})")
|
||||||
|
|
||||||
# ② 같은 자리가 연달아 서지 않는다
|
# ② 같은 자리가 연달아 서지 않는다
|
||||||
for i in range(len(places) - 1):
|
for i in range(len(places) - 1):
|
||||||
if places[i] == places[i + 1]:
|
if places[i] == places[i + 1]:
|
||||||
@ -596,7 +640,7 @@ def audit(built: list[dict]) -> list[str]:
|
|||||||
flag(item, day, f"{slots[i]} 도착 {arrive // 60}:{arrive % 60:02d} — 창 밖")
|
flag(item, day, f"{slots[i]} 도착 {arrive // 60}:{arrive % 60:02d} — 창 밖")
|
||||||
|
|
||||||
# ⑥ 마지막 날 첫 칸(아침 겸 퇴실)은 11시 전에 끝난다
|
# ⑥ 마지막 날 첫 칸(아침 겸 퇴실)은 11시 전에 끝난다
|
||||||
if i == 0 and slots[0] == "아침" and len(item["days"]) == want and day is item["days"][-1]:
|
if i == 0 and slots[0] == "아침" and last_day:
|
||||||
if arrive + stop["minutes"] > CHECK_OUT_BY:
|
if arrive + stop["minutes"] > CHECK_OUT_BY:
|
||||||
end = arrive + stop["minutes"]
|
end = arrive + stop["minutes"]
|
||||||
flag(item, day, f"퇴실이 {end // 60}:{end % 60:02d} — 11시를 넘긴다")
|
flag(item, day, f"퇴실이 {end // 60}:{end % 60:02d} — 11시를 넘긴다")
|
||||||
@ -678,4 +722,4 @@ for item in items:
|
|||||||
f"{clock // 60:02d}:{clock % 60:02d} {len(day['stops'])}칸")
|
f"{clock // 60:02d}:{clock % 60:02d} {len(day['stops'])}칸")
|
||||||
|
|
||||||
DST.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
DST.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||||
print(f"\n테마 {len(items)}개 · 규칙 16종 위반 0건 · payload {DST.stat().st_size:,}B")
|
print(f"\n테마 {len(items)}개 · 규칙 17종 위반 0건 · payload {DST.stat().st_size:,}B")
|
||||||
|
|||||||
301
solution/site/scripts/mockup/build_reading.py
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
"""군산 읽기 — 이 도시를 갈래로 묶어 카로셀로 보여주는 소재 더미.
|
||||||
|
|
||||||
|
★ 왜 만들었나 (2026-09-14 대표 의견: "오늘의 한 장으로 군산을 소개하기는 무리다,
|
||||||
|
하루에 하나씩밖에 안 알려 주니까 — 엑셀을 보고 다시 짜라")
|
||||||
|
일력(`build_daily.py`)은 오늘·어제·내일 **세 장만** 화면에 선다. 나머지는 문서에만 남아
|
||||||
|
검색·AI 는 읽어도 손님은 못 본다. 도시를 소개하는 자리로는 그릇이 작다.
|
||||||
|
그래서 받은 자료(`gunsan_365_story_db.xlsx`)의 주제를 **갈래로 묶는다.**
|
||||||
|
일력은 그대로 두되, 이제 '매일 바뀌는 한 장' 이지 소개 자리가 아니다.
|
||||||
|
|
||||||
|
★ 화면 구성은 이 파일이 정하지 않는다 — **payload 는 갈래 전체를 그대로 담고**,
|
||||||
|
'한 화면에 몇 개를 어떻게 보여줄지' 는 `inject.js` `startReading()` 이 정한다.
|
||||||
|
처음엔 31개를 전부 세로로 펼쳤는데 "스크롤 압박" 이 됐다(같은 날 대표 의견) —
|
||||||
|
갈래마다 3~4개를 무작위로 뽑아 카로셀 하나로 붙이는 방식을 거쳐, 지금은
|
||||||
|
**갈래를 가리지 않고 전체에서 5~6개만 무작위로** 뽑는다(2026-09-14 대표: "34개중
|
||||||
|
15~19개인데 왜 34개라고 써놔?" → "그냥 5-6개 정도만 보여주는거 어때?" → "랜덤으로").
|
||||||
|
이 파일은 후보 전체를 만들 뿐이고, 추리고 섞고 세우는 일은 화면 쪽 책임이다.
|
||||||
|
|
||||||
|
★ **꼭지 수는 확인되는 사실만큼이다** — 채우려고 지어내지 않는다(레포 절대 규칙).
|
||||||
|
인물 57 · 가요 25 처럼 이 목록도 출처를 댈 수 있는 것까지가 한계다. 2026-09-14 기준 34가지.
|
||||||
|
|
||||||
|
★ 엑셀 52주제를 그대로 옮기지 않는다 — **이미 이 페이지에 선 것은 뺀다.**
|
||||||
|
· 인물 35주제 → `people` 57명이 이미 레일로 다 보인다
|
||||||
|
· 대중가요 21주제 → `songs` 25곡
|
||||||
|
· 축제 14주제 → `local.festivals` 10개
|
||||||
|
· 진포대첩·개항·세관·부잔교·『탁류』 연재·하굿둑·새만금 → `chronicle` 연표 7건
|
||||||
|
· 히로쓰 가옥·초원사진관·근대미술관·철길마을·우체통거리 등 → `local.attractions` 12곳
|
||||||
|
· 이성당·짬뽕집 → `local.restaurants` 31곳
|
||||||
|
같은 것을 두 번 세우면 페이지만 길어지고 손님은 같은 말을 두 번 읽는다.
|
||||||
|
**남은 것**이 여기 선다 — 문학 · 섬과 바다 · 역사 · 장소 · 음식과 생활, 2026-09-14 기준 34가지.
|
||||||
|
|
||||||
|
★ 엑셀의 문장을 그대로 쓰지 않는다. 한 주제를 운영 슬롯 7가지로 복제해 둔 시드라
|
||||||
|
"…를 소개한다" 로 끝나는 **우리끼리 쓰는 메모**다. 여기 문장은 손님이 읽는 말로 다시 썼다.
|
||||||
|
|
||||||
|
★ 2026-09-14 대표: "내용이 너무 적다, 특히 2줄인 것들은 최소 3~5줄로." 두 문장짜리
|
||||||
|
21꼭지를 네 문장으로 늘렸다 — 새로 보탠 문장은 널리 알려진 배경 설명이고, 없는 숫자를
|
||||||
|
지어내지는 않는다.
|
||||||
|
|
||||||
|
★ 2026-09-14 대표: "확인필요 없애고 링크는 네이버 링크로." 기관·위키 링크를 항목마다
|
||||||
|
따로 골라 달던 방식을 접고, **네이버 검색 링크 하나**로 통일했다(`naver()`).
|
||||||
|
개별 문서 URL(`terms.naver.com/entry.naver?docId=...`)은 짐작해서 적으면 없는 문서로
|
||||||
|
이어질 위험이 있어(레포 절대 규칙: 없는 사실은 안 짓는다) 쓰지 않는다 — 검색 링크는
|
||||||
|
제목을 그대로 넘기니 항상 관련 결과로 뜬다. 이 결정과 함께 '확인/확인필요' 구분도
|
||||||
|
화면·데이터 양쪽에서 뺐다.
|
||||||
|
|
||||||
|
★ 렌더러에는 이 섹션이 없다. 목업이라 **주입분(`inject.js` `startReading()`)이 그린다** —
|
||||||
|
렌더러를 고치면 전 사이트 재굽기가 따라온다(README 2.1). 제품으로 옮길 자리는 README 1.3.
|
||||||
|
|
||||||
|
입력: stay-payload-new.json (build_story.py 뒤)
|
||||||
|
출력: 같은 파일에 `reading` 섹션을 넣는다. 화면은 `inject.js` 가 그린다.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
SP = Path(__file__).parent
|
||||||
|
SRC = SP / "stay-payload-new.json"
|
||||||
|
|
||||||
|
|
||||||
|
def naver(title):
|
||||||
|
"""개별 항목마다 실제 문서를 골라 링크하지 않는다 — 검색 링크 하나로 통일해서
|
||||||
|
'없는 문서를 짐작해 적는' 위험을 아예 없앤다(위 파일 머리 설명)."""
|
||||||
|
query = title if "군산" in title else f"{title} 군산"
|
||||||
|
return {"name": "네이버에서 찾아보기",
|
||||||
|
"url": "https://search.naver.com/search.naver?query=" + quote(query)}
|
||||||
|
|
||||||
|
|
||||||
|
# 갈래 — 화면에 이 순서로 선다.
|
||||||
|
GROUPS = [
|
||||||
|
("문학", "이 도시를 쓴 사람과 그 자리"),
|
||||||
|
("섬과 바다", "다리를 건너면 나오는 것"),
|
||||||
|
("역사", "연표에 안 들어간 이야기"),
|
||||||
|
("장소", "걸어서 또는 차로 닿는 자리"),
|
||||||
|
("음식과 생활", "이 도시가 먹는 것"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# (갈래, 제목, 글, 연도 또는 None)
|
||||||
|
# ★ 원문을 옮기지 않는다 — 시·소설·가사의 본문은 한 줄도 싣지 않고 제목과 배경만 쓴다.
|
||||||
|
ITEMS = [
|
||||||
|
# ── 문학 8 ────────────────────────────────────────────────────────────
|
||||||
|
("문학", "채만식 『탁류』",
|
||||||
|
"일제강점기 군산을 무대로 한 장편소설입니다. 쌀을 사고팔던 미두장을 중심으로, "
|
||||||
|
"가족을 지키려던 초봉이 시대의 소용돌이에 휩쓸려 무너지는 과정을 그렸습니다. "
|
||||||
|
"금강 하구의 흙탕물이라는 제목처럼, 개항장 군산의 실제 지명과 풍경 위에 이야기가 "
|
||||||
|
"펼쳐집니다.", 1937),
|
||||||
|
("문학", "백릉 채만식",
|
||||||
|
"1902년 옥구(지금의 군산) 임피에서 태어난 소설가입니다. 호는 백릉이고, 잡지에 "
|
||||||
|
"작품을 발표하며 문단에 나서 세태를 꼬집는 풍자 소설로 이름을 알렸습니다. "
|
||||||
|
"광복 뒤에는 고향 인근에 머물다 1950년 세상을 떠났습니다.", 1902),
|
||||||
|
("문학", "채만식문학관",
|
||||||
|
"금강 하구 옆, 작가가 나고 자란 임피 인근에 선 문학관입니다. 생애를 따라가는 "
|
||||||
|
"전시실과 『탁류』를 비롯한 작품의 여러 판본을 한자리에서 볼 수 있습니다. "
|
||||||
|
"군산이 낳은 다른 작가·시인을 소개하는 자리도 함께 있습니다.", None),
|
||||||
|
("문학", "『탁류』라는 제목",
|
||||||
|
"금강 하류의 흙탕물을 가리키는 말이자, 그 물살에 휩쓸려 가는 사람들을 함께 "
|
||||||
|
"가리키는 이중의 제목입니다. 맑았던 강물이 하구에 이르러 흐려지듯, 소설 속 "
|
||||||
|
"인물들도 도시의 욕망 앞에서 하나씩 무너져 갑니다. 신문에 회를 나눠 연재된 "
|
||||||
|
"소설이라, 그 시절 독자들은 다음 이야기를 기다리며 읽었습니다. 제목 하나에 "
|
||||||
|
"배경과 줄거리를 함께 담은 셈이라, 채만식문학관에서도 이 제목의 뜻부터 설명을 "
|
||||||
|
"시작합니다.", None),
|
||||||
|
("문학", "채만식의 풍자",
|
||||||
|
"웃기면서 찌르는 문장이 이 작가의 방식입니다. 『레디메이드 인생』·『태평천하』 같은 "
|
||||||
|
"다른 대표작에서도 이어지는 태도로, 식민지 도시의 부조리를 인물의 우스꽝스러운 "
|
||||||
|
"말과 행동으로 드러냅니다. 개항장 군산에서 본 것이 그 풍자의 바탕에 있습니다.",
|
||||||
|
None),
|
||||||
|
("문학", "탁류길",
|
||||||
|
"소설 속 공간을 실제 걷는 길로 이은 군산 스탬프투어 코스입니다. 미두장이 있던 "
|
||||||
|
"자리와 째보선창, 부잔교를 차례로 지나며 소설의 배경을 몸으로 확인합니다. "
|
||||||
|
"원도심 구간이라 머뭄에서 걸어 닿습니다.", None),
|
||||||
|
("문학", "조정래 『아리랑』과 아리랑길",
|
||||||
|
"구한말부터 광복까지, 만경강 들녘 농민들이 수탈에 맞서는 이야기를 그린 대하소설입니다. "
|
||||||
|
"군산·김제 일대가 무대라 스탬프투어에도 같은 이름의 코스가 있습니다. 여러 권에 "
|
||||||
|
"걸친 긴 이야기라, 한 도시가 아니라 호남평야 들녘 전체를 무대로 다룹니다. 아리랑길은 "
|
||||||
|
"실제 그 들녘과 마을 길을 따라 걷도록 짜여 있어, 소설이 그린 풍경을 걸음으로 "
|
||||||
|
"확인할 수 있습니다.", None),
|
||||||
|
("문학", "군산이 낳은 시인들",
|
||||||
|
"고은 · 이병훈 · 심호택 · 문효치 · 이연주 · 주하림. 일제강점기부터 지금까지 세대를 "
|
||||||
|
# ★ "아래 카드" 가 아니다 (2026-09-14 대표 지적). '인물 열전' 은 이 꼭지와 **형제 탭**이라
|
||||||
|
# 같은 줄 위쪽에 서 있다 — 아래로 내려가면 없다. 위치를 가리키는 말을 아예 뺀다:
|
||||||
|
# 탭 차림은 화면 폭과 탭 수에 따라 줄이 바뀌어서, 어느 방향으로도 단정하면 틀린다.
|
||||||
|
"이어 시인이 나온 도시입니다. 이름만으로는 알기 어려워, '인물 열전' 탭에서 "
|
||||||
|
"한 사람씩 생애와 활동을 봅니다.", None),
|
||||||
|
|
||||||
|
# ── 섬과 바다 6 ───────────────────────────────────────────────────────
|
||||||
|
("섬과 바다", "고군산군도",
|
||||||
|
"예순 남짓한 섬이 모인 군도입니다. 새만금 방조제가 육지와 신시도를 잇고, 그 뒤로 "
|
||||||
|
"무녀도 · 선유도 · 장자도가 다리로 차례로 이어져 차로 들어갈 수 있습니다. "
|
||||||
|
"섬마다 성격이 달라 하루로는 다 못 봅니다.", None),
|
||||||
|
("섬과 바다", "선유도",
|
||||||
|
"고군산군도의 대표 섬입니다. 명사십리라 불리는 백사장을 걷고, 망주봉 아래 해변에서 "
|
||||||
|
"스카이라인(짚라인)을 탑니다. 대장봉에 오르면 다도해로 지는 해를 한눈에 봅니다. "
|
||||||
|
"머뭄에서 차로 한 시간 남짓입니다.", None),
|
||||||
|
("섬과 바다", "장자도",
|
||||||
|
"선유도와 다리로 이어진 작은 섬입니다. 어선이 드나드는 포구를 끼고 있고, "
|
||||||
|
"마을 뒤 대장봉으로 오르는 등산로가 여기서 시작합니다. 산길이 가팔라도 길지 "
|
||||||
|
"않아 왕복 한두 시간이면 오르내릴 수 있습니다. 정상에 서면 고군산군도의 섬들이 "
|
||||||
|
"바다 위에 늘어선 모습이 한눈에 들어옵니다.", None),
|
||||||
|
("섬과 바다", "무녀도",
|
||||||
|
"고군산군도의 섬 하나입니다. 춤추는 무녀의 형상을 닮았다는 이야기가 이름의 유래로 "
|
||||||
|
"전해지고, 작은 해수욕장이 딸려 있습니다. 신시도에서 선유도로 넘어가는 다리 길목에 "
|
||||||
|
"있어, 고군산군도를 도는 길에 자연히 거쳐 가게 됩니다. 관광객이 몰리는 선유도보다 "
|
||||||
|
"한적해, 잠시 차를 세우고 바다를 보기에 좋습니다.", None),
|
||||||
|
("섬과 바다", "신시도",
|
||||||
|
"새만금 방조제가 육지와 처음 맞닿는 섬입니다. 고군산군도에서 몸집이 큰 섬 가운데 "
|
||||||
|
"하나로, 대각산에 오르면 방조제와 인근 섬들이 한눈에 들어옵니다. 방조제 도로를 타고 "
|
||||||
|
"들어오면 가장 먼저 만나는 섬이라, 고군산군도 여행이 시작되는 자리이기도 합니다. "
|
||||||
|
"마을과 해변이 함께 있어, 다른 섬으로 건너가기 전에 잠깐 들러 쉬어 가기도 합니다.",
|
||||||
|
None),
|
||||||
|
("섬과 바다", "비응항",
|
||||||
|
"새만금 방조제가 시작하는 항구입니다. 젓갈과 수산물을 파는 상가가 늘어서 있고, "
|
||||||
|
"서해 관광의 관문으로 씁니다. 어선이 실어 온 수산물이 그날그날 상가로 올라와, "
|
||||||
|
"싱싱한 해산물을 바로 사거나 맛볼 수 있습니다. 항구 너머로 방조제가 곧게 뻗어 "
|
||||||
|
"있어, 바다 쪽으로 나가기 전 들르기 좋은 자리입니다.", None),
|
||||||
|
|
||||||
|
# ── 역사 7 (연표에 없는 것만) ─────────────────────────────────────────
|
||||||
|
("역사", "미곡 수탈과 군산항",
|
||||||
|
"일제강점기 호남평야에서 거둔 쌀이 이 항구로 모여 일본으로 나갔습니다. 부잔교와 "
|
||||||
|
"그 옆의 대형 창고들이 그 통로였고, 쌀값을 매기던 미두장이 도시 경제의 중심에 "
|
||||||
|
"있었습니다. 『탁류』가 그 시절 풍경을 그대로 배경 삼았습니다.", None),
|
||||||
|
("역사", "째보선창",
|
||||||
|
"내항 옆 선창입니다. 언청이 뱃사공의 사연이 이름의 유래로 전해지고, 『탁류』의 "
|
||||||
|
"인물들이 오가던 자리이기도 합니다. 지금은 배 대신 낡은 창고가 남았습니다.",
|
||||||
|
None),
|
||||||
|
("역사", "해망굴",
|
||||||
|
"일제강점기에 뚫린 굴로, 시내와 항구를 잇습니다. 차 한 대가 겨우 지날 만큼 좁아 "
|
||||||
|
"지금은 사람이 걸어서 통과해 반대편 항구 쪽으로 나옵니다. 굴 안은 서늘하고 어둑해 "
|
||||||
|
"걸어 지나가는 동안 도시의 소리가 잠깐 끊깁니다. 반대편으로 나오면 바로 내항이 "
|
||||||
|
"보여, 원도심과 항구를 잇는 지름길로 지금도 쓰입니다.", None),
|
||||||
|
("역사", "경암동 철길",
|
||||||
|
"1944년에 놓인 철길입니다. 인근 제지공장의 화물을 나르던 산업철도였고, 지금은 "
|
||||||
|
"열차가 다니지 않지만 선로가 집 앞 골목을 그대로 지나가는 풍경만 남았습니다. "
|
||||||
|
"선로 양옆으로 집과 가게가 바짝 붙어 있어, 열차가 다니던 시절에는 문을 열면 바로 "
|
||||||
|
"철길이었습니다. 지금은 옛 교복을 빌려 입고 사진을 찍거나 선로를 따라 걷는 자리로 "
|
||||||
|
"쓰입니다.", 1944),
|
||||||
|
("역사", "군산항",
|
||||||
|
"1899년 개항 이후 근대 산업항으로 자란 항구입니다. 만남과 이별, 물류와 근대사가 "
|
||||||
|
"한자리에 겹치며 도시의 성격이 여기서 정해졌습니다. 개항 뒤로는 쌀과 물자가 이 "
|
||||||
|
"항구를 거쳐 오갔고, 그만큼 많은 사람이 들고 났습니다. 지금 원도심에 남은 근대 "
|
||||||
|
"건축물 대부분이 이 항구를 중심으로 들어선 것들입니다.", 1899),
|
||||||
|
("역사", "군산 3·5 만세운동",
|
||||||
|
"1919년 3월 5일, 영명학교 학생들이 중심이 되어 원도심에서 벌인 만세운동입니다. "
|
||||||
|
"전국 3·1운동이 퍼져 가던 무렵 이 도시가 어떻게 호응했는지를 보여주는 사건입니다. "
|
||||||
|
"학생들은 미리 태극기와 독립선언서를 준비해 장날에 맞춰 거리로 나섰습니다. 지금도 "
|
||||||
|
"원도심을 걷다 보면 이 사건을 알리는 표지판과 기록을 곳곳에서 만납니다.", 1919),
|
||||||
|
("역사", "임피역",
|
||||||
|
"옛 모습을 그대로 간직한 목조 간이역입니다. 근대 철도 건축의 흔적으로 등록문화재가 "
|
||||||
|
"됐고, 지금은 열차가 서지 않고 자리만 지킵니다. 작은 대합실과 승강장이 지어질 "
|
||||||
|
"당시 모습 그대로 남아 있어, 옛 기차역의 분위기를 눈으로 확인할 수 있습니다. "
|
||||||
|
"원도심에서 조금 떨어져 있어, 차로 이동하는 여정에 잠시 들르기 좋은 자리입니다.",
|
||||||
|
None),
|
||||||
|
|
||||||
|
# ── 장소 10 (주변 명소 12곳에 없는 것만) ──────────────────────────────
|
||||||
|
("장소", "군산근대역사박물관",
|
||||||
|
"해양물류의 역사와 근대 생활 풍경을 층별로 나눠 보여주는 박물관입니다. 3층부터 "
|
||||||
|
"내려오며 항구도시의 근대사를 훑고, 도시를 걷기 전에 여기서 한 번 배경을 "
|
||||||
|
"잡고 나섭니다. 전시실 안에는 당시 거리와 상점을 재현한 공간도 있어, 사진으로만 "
|
||||||
|
"보던 풍경을 실제 크기로 걸어 볼 수 있습니다. 원도심 답사의 시작점으로 삼기 좋은 "
|
||||||
|
"위치라, 관람을 마치고 바로 근처 근대문화유산으로 이어 걷습니다.", None),
|
||||||
|
("장소", "군산 내항과 부잔교",
|
||||||
|
"조수 간만의 차가 큰 서해에서 배를 대기 위해 만든 뜬다리(부잔교)입니다. "
|
||||||
|
"물이 오르내려도 다리 자체가 함께 움직여 배가 늘 수평으로 닿습니다. "
|
||||||
|
"다리 위를 끝까지 걸어 나갔다 돌아옵니다.", 1926),
|
||||||
|
("장소", "동국사",
|
||||||
|
"국내에 남은 몇 안 되는 일본식 사찰 건축입니다. 대웅전 지붕과 마당의 배치에서 "
|
||||||
|
"일본 사찰 양식이 그대로 드러나, 개항기 흔적을 건물로 보여주는 자리입니다. "
|
||||||
|
"지금도 실제로 예불을 올리는 절이라, 관람 중에는 조용히 둘러보는 것이 예의입니다. "
|
||||||
|
"마당의 소철과 종루까지 그 시절 배치가 크게 바뀌지 않아, 사진에 담기 좋은 자리로도 "
|
||||||
|
"알려져 있습니다.", None),
|
||||||
|
("장소", "월명공원",
|
||||||
|
"시내와 바다를 함께 내려다보는 공원입니다. 수시탑과 문인들의 시비가 산책로를 "
|
||||||
|
"따라 서 있고, 계절마다 오르는 길의 풍경이 달라집니다. 정상 부근까지 차로 오를 "
|
||||||
|
"수 있어, 가볍게 걷고 싶은 사람과 전망만 보고 싶은 사람 모두에게 맞습니다. 밤에는 "
|
||||||
|
"시내 불빛이 내려다보여, 낮과는 다른 풍경을 봅니다.", None),
|
||||||
|
("장소", "은파호수공원",
|
||||||
|
"호수를 두르는 산책길입니다. 겨울이면 조명을 켠 벚나무 길이 볼거리가 되고, "
|
||||||
|
"봄에는 같은 길이 벚꽃길로 바뀝니다. 저녁에 한 바퀴 돌기 좋습니다.",
|
||||||
|
None),
|
||||||
|
("장소", "금강하굿둑",
|
||||||
|
"금강과 서해가 만나는 자리에 놓은 둑입니다. 강과 바다의 경계를 사람 손으로 그은 "
|
||||||
|
"곳이고, 둑 위로 충남 서천과 전북 군산을 잇는 길이 나 있습니다. 둑을 따라 걷거나 "
|
||||||
|
"차로 건너면서 한쪽은 강물, 다른 한쪽은 바닷물이 나뉘는 모습을 눈으로 확인할 수 "
|
||||||
|
"있습니다. 철새가 많이 모이는 자리로도 알려져 있어, 계절에 따라 다른 새를 "
|
||||||
|
"봅니다.", 1990),
|
||||||
|
("장소", "말랭이마을",
|
||||||
|
"경사진 언덕을 따라 계단식으로 집이 들어선 마을입니다. 골목 담장마다 그림이 "
|
||||||
|
"그려져 있고, 도시재생 사업으로 되살아난 자리입니다. 계단과 골목이 좁고 가팔라, "
|
||||||
|
"걷는 속도를 늦추고 천천히 둘러보게 됩니다. 언덕 위에서는 원도심과 항구 쪽 풍경이 "
|
||||||
|
"함께 내려다보입니다.", None),
|
||||||
|
("장소", "장미갤러리",
|
||||||
|
"근대역사문화거리 안의 전시 공간입니다. 쌀을 쌓아 두던 옛 창고 건물을 고쳐 써서, "
|
||||||
|
"건물 자체가 그 시절 물류의 흔적입니다. 높은 천장과 두꺼운 벽 같은 창고의 구조를 "
|
||||||
|
"그대로 살려, 전시 공간이면서 건물 자체도 볼거리입니다. 인근의 다른 근대 건축물과 "
|
||||||
|
"나란히 있어, 걸어서 함께 둘러보기 좋습니다.", None),
|
||||||
|
("장소", "진포해양테마공원",
|
||||||
|
"진포대첩의 기억을 항구 옆에 펼쳐 둔 공원입니다. 퇴역한 군함과 항공기를 실제로 "
|
||||||
|
"가져다 놓아, 전시장이 아니라 야외에서 크기를 몸으로 느낄 수 있습니다. 고려 말 "
|
||||||
|
"왜구를 화포로 물리친 진포대첩의 무대가 바로 이 앞바다입니다. 아이와 함께라면 "
|
||||||
|
"직접 올라타 볼 수 있는 전시물도 있어, 역사 이야기를 몸으로 체험하게 됩니다.",
|
||||||
|
None),
|
||||||
|
("장소", "군산예술의전당",
|
||||||
|
"지역 공연을 올리는 문화 시설입니다. 원도심 답사에서 벗어나 공연 한 편을 함께 "
|
||||||
|
"일정에 넣고 싶을 때 확인해 볼 자리입니다. 크고 작은 공연장이 함께 있어, 음악회부터 "
|
||||||
|
"연극까지 다양한 공연이 열립니다. 원도심에서 차로 이동해야 하는 거리라, 일정에 "
|
||||||
|
"여유가 있을 때 넣기 좋습니다.", None),
|
||||||
|
|
||||||
|
# ── 음식과 생활 3 ─────────────────────────────────────────────────────
|
||||||
|
("음식과 생활", "군산 짬뽕",
|
||||||
|
"개항 이후 이 도시에 자리 잡은 화교 공동체가 남긴 음식 문화입니다. 빈해원 · "
|
||||||
|
"국제반점처럼 오래된 중식당이 아직 그 자리를 지키고, 짬뽕 한 그릇에 항구도시의 "
|
||||||
|
"내력이 담겨 있다고들 말합니다. 항구를 오가던 뱃사람과 노동자들이 빠르고 든든하게 "
|
||||||
|
"먹을 수 있는 음식으로 자리 잡았다고 전해집니다. 가게마다 국물 색과 맛이 조금씩 "
|
||||||
|
"달라, 여러 곳을 비교해 먹어 보는 재미도 있습니다.", None),
|
||||||
|
("음식과 생활", "박대와 군산 밥상",
|
||||||
|
"서해에서 나는 박대라는 생선을 말려 구워 냅니다. 살이 도톰하고 기름기가 적어 "
|
||||||
|
"짭조름하게 구우면 밥반찬으로 잘 맞고, 이 지역 백반집 상에 자주 오릅니다. 예전에는 "
|
||||||
|
"흔해 서민 밥상에 자주 오르던 생선이었다고 전해집니다. 말려서 굽는 조리법이라 "
|
||||||
|
"오래 두고 먹을 수 있어, 지금도 반찬이나 선물용으로 찾는 사람이 많습니다.", None),
|
||||||
|
("음식과 생활", "이성당",
|
||||||
|
"국내에서 손꼽히게 오래된 빵집 가운데 하나입니다. 단팥빵과 야채빵이 대표 메뉴로, "
|
||||||
|
"갓 나오는 시간에 맞추지 않으면 줄을 서야 할 만큼 찾는 사람이 많습니다. 일제강점기 "
|
||||||
|
"부터 이어진 자리에서 지금까지 영업을 이어 오고 있어, 군산을 대표하는 오래된 "
|
||||||
|
"가게로 꼽힙니다. 이른 아침부터 줄이 늘어서는 날이 많아, 여유 있게 들르려면 시간을 "
|
||||||
|
"넉넉히 잡는 편이 낫습니다.", None),
|
||||||
|
]
|
||||||
|
|
||||||
|
payload = json.loads(SRC.read_text(encoding="utf-8"))
|
||||||
|
SECTIONS = payload["theme"]["sections"]
|
||||||
|
|
||||||
|
seen = [name for name, _note in GROUPS]
|
||||||
|
for group, *_rest in ITEMS:
|
||||||
|
assert group in seen, f"갈래에 없는 이름: {group}"
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for group, title, body, year in ITEMS:
|
||||||
|
item = {"group": group, "title": title, "body": body, "source": naver(title)}
|
||||||
|
if year:
|
||||||
|
item["year"] = year
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
# 부제에 전체 개수를 내걸지 않는다 — 화면은 이 중 5~6개만 매번 무작위로 보여준다
|
||||||
|
# (2026-09-14 대표: "34개중 15~19개인데 왜 34개라고 써놔?"). 숫자 대신 "올 때마다
|
||||||
|
# 다른"으로 적어 실제 화면과 문구가 어긋나지 않게 한다.
|
||||||
|
env = {"kind": "reading", "version": 1, "title": "군산 읽기",
|
||||||
|
"subtitle": "문학 · 섬과 바다 · 역사 · 장소 · 음식과 생활 — 올 때마다 다른 몇 가지",
|
||||||
|
"groups": [{"name": name, "note": note} for name, note in GROUPS],
|
||||||
|
"items": items}
|
||||||
|
|
||||||
|
# 렌더러가 모르는 id 다 — payload 자체에는 이런 섹션이 없다. `#story` 탭 하나로 화면에
|
||||||
|
# 세우는 일은 inject.js `startReading()` 이 한다(2026-09-14부터, 독립 섹션이 아니라 탭이다).
|
||||||
|
# 여기 payload 로 두는 이유는 ① 고치는 자리가 스크립트 하나로 모이고 ② 문서(HTML)에 값이 남기 때문이다.
|
||||||
|
existing = next((s for s in SECTIONS if s["id"] == "reading"), None)
|
||||||
|
if existing is None:
|
||||||
|
existing = {"id": "reading", "name": "군산 읽기", "enabled": False, "locked": False}
|
||||||
|
SECTIONS.insert(next(i for i, s in enumerate(SECTIONS) if s["id"] == "story") + 1, existing)
|
||||||
|
existing["data"] = json.dumps(env, ensure_ascii=False)
|
||||||
|
|
||||||
|
SRC.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
counts = {name: sum(1 for i in items if i["group"] == name) for name, _ in GROUPS}
|
||||||
|
print("군산 읽기 " + str(len(items)) + "가지 — "
|
||||||
|
+ " · ".join(f"{k} {v}" for k, v in counts.items())
|
||||||
|
+ " (화면엔 매번 5~6개만 무작위)")
|
||||||
@ -121,6 +121,9 @@ PEOPLE = [
|
|||||||
("김수미", "김수미", "1949–2024", "배우", "「전원일기」 일용 엄니로 오래 기억되는 배우입니다."),
|
("김수미", "김수미", "1949–2024", "배우", "「전원일기」 일용 엄니로 오래 기억되는 배우입니다."),
|
||||||
("송새벽", "송새벽", "1979–", "배우", "군산 출신 배우입니다."),
|
("송새벽", "송새벽", "1979–", "배우", "군산 출신 배우입니다."),
|
||||||
("장신영", "장신영", "1984–", "배우", "군산 출신 배우입니다."),
|
("장신영", "장신영", "1984–", "배우", "군산 출신 배우입니다."),
|
||||||
|
# ★ 익산 출생이지만 군산에서 학교를 다녔다 — 그대로 적는다. 다른 줄처럼 "군산 출신" 으로
|
||||||
|
# 뭉뚱그리면 위키와 어긋나고, 이 목록의 규칙(문서에 적힌 관계만 담는다)도 깨진다.
|
||||||
|
("진희경", "진희경", "1968–", "배우", "익산에서 태어나 군산영광여고를 나온 배우입니다."),
|
||||||
("이서원", "이서원", "1997–", "배우", "군산 출신 배우입니다."),
|
("이서원", "이서원", "1997–", "배우", "군산 출신 배우입니다."),
|
||||||
("장재호 (배우)", "장재호", "1986–", "배우", "군산 출신 배우입니다."),
|
("장재호 (배우)", "장재호", "1986–", "배우", "군산 출신 배우입니다."),
|
||||||
("강민지 (배우)", "강민지", "1996–", "배우 · 모델", "모델로 시작해 드라마와 영화로 활동 범위를 넓혔습니다."),
|
("강민지 (배우)", "강민지", "1996–", "배우 · 모델", "모델로 시작해 드라마와 영화로 활동 범위를 넓혔습니다."),
|
||||||
|
|||||||
@ -89,17 +89,58 @@ for item in itin:
|
|||||||
out.append(f"| {item['name']} | {item['duration']} | {item['audience']} | {item['why']} |")
|
out.append(f"| {item['name']} | {item['duration']} | {item['audience']} | {item['why']} |")
|
||||||
out += [""]
|
out += [""]
|
||||||
|
|
||||||
|
# ── 군산 읽기 ───────────────────────────────────────────────────────────────
|
||||||
|
reading = env("reading")
|
||||||
|
out += [f"## 6. 군산 읽기 {len(reading['items'])}가지 — `build_reading.py` `ITEMS`", "",
|
||||||
|
"한 화면에 다 펴 두는 카드. 일력(7번)과 달리 손님이 훑어 내려가며 읽는 자리다.", ""]
|
||||||
|
for group in reading["groups"]:
|
||||||
|
rows = [x for x in reading["items"] if x["group"] == group["name"]]
|
||||||
|
out += [f"### {group['name']} {len(rows)}가지 — {group['note']}", "",
|
||||||
|
"| 제목 | 글 | 연도 | 출처 |", "|---|---|---|---|"]
|
||||||
|
for x in rows:
|
||||||
|
out.append(f"| {x['title']} | {x['body']} | {x.get('year', '')} | "
|
||||||
|
f"{x['source']['name']} |")
|
||||||
|
out += [""]
|
||||||
|
|
||||||
|
# ── 오늘의 한 장 ────────────────────────────────────────────────────────────
|
||||||
|
daily = env("daily")["items"]
|
||||||
|
out += [f"## 7. 오늘의 한 장 {len(daily)}장 — `build_daily.py`", "",
|
||||||
|
"날마다 한 장씩 넘어가는 일력. 가요는 가요 다방을 그대로 읽고, 시·소설은 손으로 적는다.", "",
|
||||||
|
"| 날짜 | 분류 | 제목 | 글 | 확인 | 출처 |", "|---|---|---|---|---|---|"]
|
||||||
|
for x in daily:
|
||||||
|
out.append(f"| {x['monthDay']} | {x['category']} | {x['title']} | {x['body']} | "
|
||||||
|
f"{x['verified']} | {x['source']['name']} |")
|
||||||
|
out += [""]
|
||||||
|
|
||||||
# ── 오늘의 엽서 ──────────────────────────────────────────────────────────────
|
# ── 오늘의 엽서 ──────────────────────────────────────────────────────────────
|
||||||
cards = env("postcard")["items"]
|
cards = env("postcard")["items"]
|
||||||
out += [f"## 6. 오늘의 엽서 {len(cards)}장 — 원본 payload (아직 다시 안 뽑음)", ""]
|
out += [f"## 8. 오늘의 엽서 {len(cards)}장 — 원본 payload (아직 다시 안 뽑음)", ""]
|
||||||
out += [f"- {c['line']} `{' '.join(c.get('hashtags', []))}`" for c in cards] + [""]
|
out += [f"- {c['line']} `{' '.join(c.get('hashtags', []))}`" for c in cards] + [""]
|
||||||
|
|
||||||
# ── 머뭄이 만든 노래 ─────────────────────────────────────────────────────────
|
# ── 머뭄이 만든 노래 ─────────────────────────────────────────────────────────
|
||||||
own = payload["narrative"]["ownSongs"]["items"]
|
own = payload["narrative"]["ownSongs"]["items"]
|
||||||
out += [f"## 7. 머뭄이 만든 노래 {len(own)}곡 — `patch_stay.py` `OWN_SONGS`", ""]
|
out += [f"## 9. 머뭄이 만든 노래 {len(own)}곡 — `patch_stay.py` `OWN_SONGS`", ""]
|
||||||
out += [f"- {s['title']} — `{s['audioUrl']}`" for s in own] + [""]
|
out += [f"- {s['title']} — `{s['audioUrl']}`" for s in own] + [""]
|
||||||
|
|
||||||
|
# ── 오늘의 날씨 ──────────────────────────────────────────────────────────────
|
||||||
|
# 하늘 다섯 · 기온대 다섯. 화면에는 케이스마다 한 줄만 서고 20초마다 자루에서 다음 것이 나온다.
|
||||||
|
wx = payload["local"]["weather"]
|
||||||
|
sky, temp = wx.get("noteSets", {}), wx.get("tempNoteSets", {})
|
||||||
|
nsky, ntemp = sum(map(len, sky.values())), sum(map(len, temp.values()))
|
||||||
|
out += [f"## 10. 오늘의 날씨 문구 {nsky + ntemp}개 — `patch_stay.py` `SKY_NOTES`·`TEMP_NOTES`", "",
|
||||||
|
"하늘 줄은 **집 안**에서 뭘 할지, 기온 줄은 **밖에서** 어디를 갈지다. 두 줄이 나란히 선다.",
|
||||||
|
f"### 하늘 {nsky}개 (`SKY_NOTES`)", ""]
|
||||||
|
for case, rows in sky.items():
|
||||||
|
out += [f"**{case}**"] + [f"- {t}" for t in rows] + [""]
|
||||||
|
out += [f"### 기온대 {ntemp}개 (`TEMP_NOTES`)", ""]
|
||||||
|
for band, rows in temp.items():
|
||||||
|
out += [f"**{band}**"] + [f"- {t}" for t in rows] + [""]
|
||||||
|
|
||||||
|
|
||||||
path = SP / "TEXT.md"
|
path = SP / "TEXT.md"
|
||||||
path.write_text("\n".join(out), encoding="utf-8")
|
path.write_text("\n".join(out), encoding="utf-8")
|
||||||
print(f"{path.name} 캐치프레이즈 {len(box)} · 노래 {len(songs)} · 인물 {len(people)} · "
|
print(f"{path.name} 캐치프레이즈 {len(box)} · 노래 {len(songs)} · 인물 {len(people)} · "
|
||||||
f"정거장 문구 {len(seen)} · 테마 {len(itin)} · 엽서 {len(cards)} · 자작곡 {len(own)}")
|
f"정거장 문구 {len(seen)} · 테마 {len(itin)} · 군산 읽기 {len(reading['items'])} · "
|
||||||
|
f"일력 {len(daily)} · 엽서 {len(cards)} · "
|
||||||
|
f"자작곡 {len(own)} · "
|
||||||
|
f"날씨 {nsky + ntemp}")
|
||||||
|
|||||||
99
solution/site/scripts/mockup/fetch_restaurants.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"""주변 맛집을 TourAPI 에서 다시 받아 `restaurants.json` 으로 떨어뜨린다. patch_stay.py 가 이 파일을 싣는다.
|
||||||
|
|
||||||
|
★ 왜 (2026-09-11, 사장님: "주변맛집 데이터가 너무 적은거같아")
|
||||||
|
원본 사이트의 맛집은 8곳이었고 전부 467m 안이었다. 그래서 '걸어서 10분 이상' 탭을 누르면
|
||||||
|
맛집 칸이 "이 거리 안에는 없습니다" 로 비었다. 같은 좌표 TourAPI 5km 반경에는 59곳이 있다
|
||||||
|
(제품은 5km 를 싣는다 — local_content_service.RESTAURANT_RADIUS_M).
|
||||||
|
★ 2km(걸어서 25분)까지만 싣는다. 그 밖은 차로 갈 거리라 '주변' 이라 부르기 어렵고,
|
||||||
|
레일에 59장이 서면 가까운 집이 묻힌다.
|
||||||
|
★ 원래 8곳은 그대로 둔다 — 소개문과 사진이 원본 사이트 것이다. 새로 붙는 곳만 TourAPI 에서 받는다.
|
||||||
|
★ 사진은 공공누리 상업 이용 가능분만 온다(tour_api._normalize 가 거른다). 받아서 img/mirror/ 에
|
||||||
|
둔다 — 시연본은 레포 안에서 자급자족한다(README 2.2). 사진 없는 집은 렌더러가 이름 활자로 세운다.
|
||||||
|
|
||||||
|
실행 (solution/backend 에서 — 백엔드 설정이 .env 를 거기서 읽는다):
|
||||||
|
.venv/bin/python ../site/scripts/mockup/fetch_restaurants.py
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
SP = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(SP.parents[2] / "backend"))
|
||||||
|
from services.external.tour_api import _call, fetch_nearby # noqa: E402
|
||||||
|
|
||||||
|
LAT, LNG = 35.9864175, 126.7061254 # payload.place — 스테이 머뭄
|
||||||
|
RADIUS_M = 2_000
|
||||||
|
|
||||||
|
|
||||||
|
def meters(text: str) -> float:
|
||||||
|
m = re.match(r"^([\d.]+)\s*(km|m)$", (text or "").strip(), re.I)
|
||||||
|
return float(m[1]) * (1000 if m[2].lower() == "km" else 1) if m else float("inf")
|
||||||
|
|
||||||
|
|
||||||
|
def distance_text(m: int) -> str:
|
||||||
|
"""site_payload 와 같은 표기 — 렌더러가 이 문자열을 다시 미터로 읽어 거리 탭을 가른다."""
|
||||||
|
return f"{m}m" if m < 1000 else f"{(m + 50) // 100 / 10:.1f}km"
|
||||||
|
|
||||||
|
|
||||||
|
def clean_name(title: str) -> str:
|
||||||
|
# "[백년가게]운정식당" · "[착한가게] 국일식당" — 인증 꼬리표는 이름이 아니다. 검색어에 붙으면 가게가 안 잡힌다.
|
||||||
|
return re.sub(r"^\[[^\]]*\]\s*", "", title).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def blurb(overview: str) -> str:
|
||||||
|
"""overview 앞 두 문장까지. 카드는 세 줄에서 자른다(line-clamp-3) — 원래 8곳도 두 문장 안팎이다."""
|
||||||
|
text = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", "", overview or "")).strip()
|
||||||
|
sentences = re.split(r"(?<=[.!?])\s+", text)
|
||||||
|
out = sentences[0] if sentences else ""
|
||||||
|
if len(sentences) > 1 and len(out) + len(sentences[1]) < 110:
|
||||||
|
out += " " + sentences[1]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def mirror(client: httpx.AsyncClient, url: str) -> str:
|
||||||
|
ext = Path(url.split("?")[0]).suffix.lower() or ".jpg"
|
||||||
|
name = hashlib.sha1(url.encode()).hexdigest()[:16] + ext
|
||||||
|
path = SP / "img" / "mirror" / name
|
||||||
|
if not path.exists():
|
||||||
|
res = await client.get(url)
|
||||||
|
res.raise_for_status()
|
||||||
|
path.write_bytes(res.content)
|
||||||
|
# patch_stay.py 가 /assets/mirror/ → /s/stay/img/mirror/ 로 바꿔 박는다(원래 8곳과 같은 길).
|
||||||
|
return f"/assets/mirror/{name}"
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
original = json.loads((SP / "stay-payload.json").read_text(encoding="utf-8"))["local"]["restaurants"]
|
||||||
|
have = {r["name"] for r in original}
|
||||||
|
out = list(original)
|
||||||
|
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
|
||||||
|
rows = await fetch_nearby(client, LAT, LNG, radius_m=RADIUS_M, content_type_id="39")
|
||||||
|
for row in rows:
|
||||||
|
name = clean_name(row["name"])
|
||||||
|
if name in have:
|
||||||
|
continue
|
||||||
|
items, _ = await _call(client, "detailCommon2", contentId=row["contentid"])
|
||||||
|
entry = {"name": name, "category": "맛집", "searchQuery": name,
|
||||||
|
"distanceText": distance_text(row["distance_m"])}
|
||||||
|
if row.get("imageUrl"):
|
||||||
|
entry["imageUrl"] = await mirror(client, row["imageUrl"])
|
||||||
|
desc = blurb(items[0].get("overview") if items else "")
|
||||||
|
if desc:
|
||||||
|
entry["description"] = desc
|
||||||
|
out.append(entry)
|
||||||
|
have.add(name)
|
||||||
|
# 거리순 — 레일 앞머리가 가장 가까운 집이어야 '주변' 이다.
|
||||||
|
out.sort(key=lambda r: meters(r.get("distanceText")))
|
||||||
|
(SP / "restaurants.json").write_text(json.dumps(out, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
||||||
|
far = sum(1 for r in out if meters(r.get("distanceText")) > 800)
|
||||||
|
photo = sum(1 for r in out if r.get("imageUrl"))
|
||||||
|
print(f"restaurants.json · {len(out)}곳 (원본 {len(original)} + TourAPI {len(out) - len(original)}) "
|
||||||
|
f"· 사진 {photo} · 800m 밖 {far}")
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
47
solution/site/scripts/mockup/fill_restaurant_photos.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""TourAPI 에 사진이 없는 주변 맛집에 네이버 이미지 검색으로 찾은 사진을 받아 둔다.
|
||||||
|
|
||||||
|
★ 왜 (2026-09-11, 사장님: "이미지가 없어?? 좀 가져와봐" · "다운로드 하던가")
|
||||||
|
fetch_restaurants.py 가 붙인 23곳 중 11곳은 TourAPI(위치 목록·추가 이미지·키워드 검색)와
|
||||||
|
위키미디어 어디에도 사진이 없었다. 레일에 이름 활자 카드가 11장 섞였다.
|
||||||
|
★ ⚠ 이 사진들은 공공누리가 아니다. 대한민국 구석구석·군산시청·블로그·네이버 플레이스에 올라온 것이고
|
||||||
|
권리는 각 게시자에게 있다. **시연본에만 쓴다 — 제품 수집 파이프라인으로 옮기지 않는다.**
|
||||||
|
제품의 규칙은 그대로 공공누리 Type1·Type3 뿐이다(tour_api._COMMERCIAL_OK_LICENSES).
|
||||||
|
★ 한 장씩 눈으로 보고 골랐다 — 간판이 보이거나 게시물 제목에 그 가게 이름이 있는 것만.
|
||||||
|
이름만 같은 다른 집(거목아리랑 · 국일복아구)은 버렸다. 출처 주소는 restaurant-photos.json 에 남긴다.
|
||||||
|
★ 긴 변 1600px 로 줄인다 — 원본이 10MB 인 것도 있다. 작은 사진은 키우지 않는다.
|
||||||
|
|
||||||
|
실행: python3 fill_restaurant_photos.py (macOS `sips` 로 줄인다)
|
||||||
|
굽기(patch_stay.py)는 여기서 받아 둔 파일만 읽는다 — 사진이 없는 집에만 덧대고, TourAPI 사진이 있으면 그쪽이 먼저다.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SP = Path(__file__).resolve().parent
|
||||||
|
LIST = SP / "restaurant-photos.json"
|
||||||
|
MAX_SIDE = 1600
|
||||||
|
|
||||||
|
photos = json.loads(LIST.read_text(encoding="utf-8"))
|
||||||
|
for name, ph in photos.items():
|
||||||
|
file = hashlib.sha1(ph["url"].encode()).hexdigest()[:16] + ".jpg"
|
||||||
|
path = SP / "img" / "mirror" / file
|
||||||
|
if not path.exists():
|
||||||
|
src = path.with_suffix(".src")
|
||||||
|
req = urllib.request.Request(ph["url"], headers={"User-Agent": "Mozilla/5.0", "Referer": "https://search.naver.com/"})
|
||||||
|
src.write_bytes(urllib.request.urlopen(req, timeout=30).read())
|
||||||
|
dims = subprocess.run(["sips", "-g", "pixelWidth", "-g", "pixelHeight", str(src)],
|
||||||
|
capture_output=True, text=True, check=True).stdout
|
||||||
|
side = max(int(v) for v in re.findall(r"pixel(?:Width|Height): (\d+)", dims))
|
||||||
|
cmd = ["sips", "-s", "format", "jpeg", "-s", "formatOptions", "80"]
|
||||||
|
if side > MAX_SIDE:
|
||||||
|
cmd += ["-Z", str(MAX_SIDE)]
|
||||||
|
subprocess.run(cmd + [str(src), "--out", str(path)], capture_output=True, check=True)
|
||||||
|
src.unlink()
|
||||||
|
ph["file"] = file
|
||||||
|
print(f" {name:<8} {path.stat().st_size // 1024:>5}KB {file}")
|
||||||
|
|
||||||
|
LIST.write_text(json.dumps(photos, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
||||||
|
print(f"restaurant-photos.json · {len(photos)}곳")
|
||||||
BIN
solution/site/scripts/mockup/img/mirror/00a8ac4ce017fb12.jpg
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
solution/site/scripts/mockup/img/mirror/067f6fe42c045a3a.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
solution/site/scripts/mockup/img/mirror/087900c82da3b2f5.jpg
Normal file
|
After Width: | Height: | Size: 619 KiB |
BIN
solution/site/scripts/mockup/img/mirror/1302460cb149975f.jpg
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
solution/site/scripts/mockup/img/mirror/24043ae4f5854f96.jpg
Normal file
|
After Width: | Height: | Size: 452 KiB |
BIN
solution/site/scripts/mockup/img/mirror/3e5b3daae33ea51c.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
solution/site/scripts/mockup/img/mirror/42e8d872198edaa9.jpg
Normal file
|
After Width: | Height: | Size: 520 KiB |
BIN
solution/site/scripts/mockup/img/mirror/483f52c2d2b7eebe.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
solution/site/scripts/mockup/img/mirror/55f57f45fcebb460.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
solution/site/scripts/mockup/img/mirror/5f92192c7b3e2cbf.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
solution/site/scripts/mockup/img/mirror/6102b164464dbfa4.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
solution/site/scripts/mockup/img/mirror/81438670a5485351.jpg
Normal file
|
After Width: | Height: | Size: 482 KiB |
BIN
solution/site/scripts/mockup/img/mirror/81bd76fa51ae9ad1.jpg
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
solution/site/scripts/mockup/img/mirror/83766fa52745ef37.jpg
Normal file
|
After Width: | Height: | Size: 529 KiB |
BIN
solution/site/scripts/mockup/img/mirror/92cb907d07db9953.jpg
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
solution/site/scripts/mockup/img/mirror/93146c8ae53252ff.jpg
Normal file
|
After Width: | Height: | Size: 418 KiB |
BIN
solution/site/scripts/mockup/img/mirror/98539e6361e317b1.jpg
Normal file
|
After Width: | Height: | Size: 357 KiB |
BIN
solution/site/scripts/mockup/img/mirror/aebe437fd281f9ad.jpg
Normal file
|
After Width: | Height: | Size: 420 KiB |
BIN
solution/site/scripts/mockup/img/mirror/b3518ee8ef2b4aac.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
solution/site/scripts/mockup/img/mirror/bf6bb6183c6f6246.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
solution/site/scripts/mockup/img/mirror/de286714af7faaba.jpg
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
solution/site/scripts/mockup/img/mirror/e13c8a2a1aa2762c.jpg
Normal file
|
After Width: | Height: | Size: 436 KiB |
BIN
solution/site/scripts/mockup/img/mirror/f9cb7e88415edd06.jpg
Normal file
|
After Width: | Height: | Size: 359 KiB |
@ -256,6 +256,9 @@
|
|||||||
두 문장이 같은 자리에서 겹쳐 보인다.
|
두 문장이 같은 자리에서 겹쳐 보인다.
|
||||||
★ 자리 흔들림: 처음 구워진 줄 높이를 min-height 로 박아 둔다(스크립트가 잰다).
|
★ 자리 흔들림: 처음 구워진 줄 높이를 min-height 로 박아 둔다(스크립트가 잰다).
|
||||||
문장 길이가 달라 한 줄↔두 줄이 오갈 때 히어로 아래가 튀는 것을 막는다. */
|
문장 길이가 달라 한 줄↔두 줄이 오갈 때 히어로 아래가 튀는 것을 막는다. */
|
||||||
|
/* 대표 문구 아래에서 도는 줄 (2026-09-11). 대표 문구보다 한 단 작고 옅게 둔다 —
|
||||||
|
둘이 같은 크기·같은 농도면 어느 쪽이 숙소의 한 줄인지 안 읽힌다. */
|
||||||
|
.w4d-sub { margin-top: 0.5rem; font-size: var(--fs-sm, 0.9375rem); line-height: 1.6; opacity: 0.75; }
|
||||||
.w4d-line { display: block; }
|
.w4d-line { display: block; }
|
||||||
.w4d-line--out {
|
.w4d-line--out {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -327,3 +330,295 @@
|
|||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
/* #weather .w4-sky { display: none; } */
|
/* #weather .w4-sky { display: none; } */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── 가요 다방 판꽂이 ──────────────────────────────────────────────────────
|
||||||
|
*
|
||||||
|
* ★ 되짚음 (2026-09-14 대표: "모바일 대응이 안 되어있는듯? 무슨 노래인지 모르잖아,
|
||||||
|
* 모바일에서는 hover가 없으니까"). 맞는 지적이고, **아래는 내가 만든 회귀였다.**
|
||||||
|
* 원래 렌더러(`SongsSection.tsx`)는 판 아래 곡명을 **항상** 보여준다 —
|
||||||
|
* `<span className="mt-2 block truncate text-[10px] ...">{song.title}</span>`,
|
||||||
|
* 숨기는 코드가 없다. 그런데 같은 날 앞선 지적("LP판이 많아져서 보기가 이상하잖아",
|
||||||
|
* 25곡이 `flex-wrap` 으로 3~9줄이 되던 문제)에 답하며 판을 겹쳐 꽂고 **곡명을
|
||||||
|
* hover 로만 띄우게** 고쳤다 — 데스크톱 벽은 없앴지만 손가락엔 hover 가 없으니
|
||||||
|
* 곡명이 아예 안 보이는 화면이 됐다. 데스크톱만 보고 고친 값이다.
|
||||||
|
* ★ 다시 짠다 — **곡명은 항상 보인다. 줄은 접지 않고 가로 한 줄로만 편다.**
|
||||||
|
* 판이 25곡이든 50곡이든 줄 수는 하나고, 안 보이는 나머지는 옆으로 스크롤한다.
|
||||||
|
* 이 페이지의 다른 레일(주변 명소·인물 열전)이 이미 쓰는 것과 같은 해법이라 —
|
||||||
|
* "hover 로 감추기" 는 애초에 필요 없던 처방이었다.
|
||||||
|
* ★ 마크업을 고치지 않는다(React 가 되돌린다). `:has()` 로 판이 든 상자만 집는다.
|
||||||
|
* 곡명 `<span>` 은 원래도 절대배치가 아니라 `block` 이라 손대지 않는다 — 화면에
|
||||||
|
* 보이려고 위치를 옮길 필요가 없다, 그냥 숨기지만 않으면 된다.
|
||||||
|
* ★ **화살표 + 실제 카로셀 라이브러리**(2026-09-14 대표: "옆으로 갈수있는 버튼도 주고
|
||||||
|
* slider.js 적용해줘"). `inject.js` `startSongsRail()` 이 이 판 목록을 감싸는 상자를
|
||||||
|
* 하나 만들어 `.slider-viewport` 로 표시하고(embla-carousel 코어를 그 위에 붙인다 —
|
||||||
|
* "군산 읽기" 탭과 같은 라이브러리, `vendor/embla-carousel.umd.js`), 그 안에 원래
|
||||||
|
* 판 목록(:has() 로 잡는 이 상자)을 그대로 넣는다. **가로 스크롤·snap 은 이제
|
||||||
|
* `.slider-viewport` 쪽 몫이다** — 그 클래스가 이미 그 CSS 를 갖고 있어(전역 번들)
|
||||||
|
* 여기서 다시 안 만든다. 이 규칙은 **안쪽 판 줄의 레이아웃(줄바꿈 금지·간격·판 크기)만** 잡는다.
|
||||||
|
*/
|
||||||
|
#songs div:has(> button > .w4-disc-mini) {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 6px 4px 14px;
|
||||||
|
/* 렌더러가 이 div 자체에 준 mt-4 를 지운다 — 지금은 이 div 가 `.slider-viewport` 안에
|
||||||
|
들어가 있어, 그 여백이 화살표 줄(위)이 아니라 스크롤 상자 안쪽 빈 공간이 됐다.
|
||||||
|
간격은 화살표 줄의 mt-4(inject.js `startSongsRail`)가 대신 준다. */
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#songs button:has(> .w4-disc-mini) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 68px; /* 원래 96px(`w-24`) — 한 줄로만 펴니 그만큼은 줄여도 된다 */
|
||||||
|
transition: transform 0.16s ease;
|
||||||
|
}
|
||||||
|
#songs button:has(> .w4-disc-mini) > .w4-disc-mini {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
/* 곡명 — 렌더러가 원래 그리는 그 글자다. 여기서는 줄어든 판 폭에 맞춰 여백만 좁힌다. */
|
||||||
|
#songs button:has(> .w4-disc-mini) > .w4-disc-mini + span {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
/* 고른 판만 살짝 들어 올린다 — "뽑아 든 판" 표시. 선택 자체(테두리)는 React 가 인라인
|
||||||
|
스타일로 이미 그린다(`boxShadow`); 여기선 그 위에 얹는 동작만 더한다. */
|
||||||
|
#songs button:has(> .w4-disc-mini):hover { transform: translateY(-4px); }
|
||||||
|
#songs button[aria-pressed='true']:has(> .w4-disc-mini) { transform: translateY(-6px); }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#songs button:has(> .w4-disc-mini) { transition: none; }
|
||||||
|
}
|
||||||
|
/* `#songs .slider-viewport` 로 스코프한다 — `.slider-viewport` 를 전부 건드리면
|
||||||
|
"군산 읽기" 카로셀과 이 페이지 다른 실제 레일까지 같이 바뀐다. */
|
||||||
|
#songs .slider-viewport {
|
||||||
|
-webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent 100%);
|
||||||
|
mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 28px), transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── 날씨 문구 전환 ────────────────────────────────────────────────────────
|
||||||
|
* 문장이 여러 벌이라 바뀌는 자리다(inject.js ⑦). 글자가 툭 갈리지 않게 한 번 접었다 편다.
|
||||||
|
*/
|
||||||
|
#weather [data-w4d-fade] { transition: opacity 0.3s ease; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#weather [data-w4d-fade] { transition: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── 엽서 쓰기 (inject.js ⑥) ───────────────────────────────────────────────
|
||||||
|
* `#location` 과 `#festival` 사이의 **독립 섹션**이다(2026-09-14 대표 지시로 '오늘의
|
||||||
|
* 엽서' 탭에서 빠져나왔다). 다른 실제 섹션과 같은 껍데기(`.shell` · `--section-space`
|
||||||
|
* 세로 여백)를 쓰고, 제목·부제도 `ItemSection` 이 쓰는 것과 같은 전역 클래스(`.h2`
|
||||||
|
* `.measure`)라 이 섹션만 따로 CSS 를 두지 않는다 — 안쪽 위젯(캔버스·폼)만 여기서 잡는다.
|
||||||
|
*/
|
||||||
|
#w4d-pm-section {
|
||||||
|
width: 100%;
|
||||||
|
padding-block: var(--section-space, 64px);
|
||||||
|
background: var(--tpl-surface, #fafafa);
|
||||||
|
color: var(--tpl-text, #1b1a15);
|
||||||
|
}
|
||||||
|
.w4d-pm-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.w4d-pm-canvas {
|
||||||
|
/* ★ 300px 이던 것을 키웠다(2026-09-14 대표: "엽서 작아서 깨지는데?"). 실제로 그리는
|
||||||
|
건 1080×1080 이라 화질 문제는 아니지만, 미리보기가 작으면 안의 문구·소인이 안 읽혀
|
||||||
|
"제대로 안 나온 것" 처럼 보인다 — 미리보기 크기를 실물 크기에 맞춘다. */
|
||||||
|
width: 420px;
|
||||||
|
max-width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border: 1px solid var(--tpl-border, #bcb49e);
|
||||||
|
background: var(--tpl-card, #efe7d3);
|
||||||
|
}
|
||||||
|
.w4d-pm-form { flex: 1 1 320px; min-width: 260px; }
|
||||||
|
/* 썸네일 줄 — `.slider-viewport`(뷰포트) 안에 `.slider-track`(트랙, 이 클래스 자체가
|
||||||
|
display:flex 를 준다)이 있다. 여기서는 간격·안쪽 여백만 얹는다(2026-09-14 대표:
|
||||||
|
"이미지는 다 가져와서 그냥 카로셀로 보여주든가 하셈" — 9장 전체를 후보로 두면서
|
||||||
|
한 줄에 다 못 앉으니 카로셀이 됐다). */
|
||||||
|
.w4d-pm-thumbs-viewport { margin-bottom: 12px; }
|
||||||
|
.w4d-pm-thumbs { gap: 8px; padding: 2px; }
|
||||||
|
.w4d-pm-thumb {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.w4d-pm-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||||
|
.w4d-pm-thumb[aria-pressed='true'] { border-color: var(--tpl-accent, #bf2f1b); }
|
||||||
|
.w4d-pm-text {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font: inherit;
|
||||||
|
font-family: var(--tpl-font-body, serif);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--tpl-text, #1b1a15);
|
||||||
|
background: var(--tpl-surface, #fafafa);
|
||||||
|
border: 1px solid var(--tpl-border, #bcb49e);
|
||||||
|
resize: none; /* 2026-09-14 대표: "textarea 크기 못바꾸게" — 늘리면 옆 캔버스와 높이가 어긋난다 */
|
||||||
|
}
|
||||||
|
.w4d-pm-count { margin: 4px 0 14px; font-size: 11px; text-align: right; opacity: 0.55; }
|
||||||
|
.w4d-pm-actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
.w4d-pm-actions button {
|
||||||
|
padding: 9px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid var(--tpl-primary, #1b1a15);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.w4d-pm-share { background: var(--tpl-primary, #1b1a15); color: var(--tpl-bg, #fff); }
|
||||||
|
.w4d-pm-save { background: transparent; color: var(--tpl-text, #1b1a15); }
|
||||||
|
.w4d-pm-hint { margin: 10px 0 0; font-size: 11.5px; opacity: 0.6; }
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.w4d-pm-canvas { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── 군산 읽기 (inject.js ③) ───────────────────────────────────────────────
|
||||||
|
* 렌더러에 없는 섹션이라 카드 내용 모양은 여기서 만든다. 색·서체는 전부 템플릿 변수에서
|
||||||
|
* 가져온다 — 값을 박으면 템플릿을 바꿀 때 이 섹션만 남의 집처럼 보인다(이 파일의 규약).
|
||||||
|
*
|
||||||
|
* ★ **카로셀 껍데기는 새로 만들지 않았다** — `.slider-viewport` · `.slider-track` 은
|
||||||
|
* 이 사이트가 이미 쓰는 클래스다(`site/src/lib/ui/Carousel.tsx`, 컴파일된 전역 CSS에
|
||||||
|
* 이미 있다). 여기서 정의하는 CSS 는 **카드 안 내용물**뿐이다. 점(dots)·간격도 그 컴포넌트가
|
||||||
|
* 쓰는 Tailwind 유틸리티(`h-1.5 rounded-full bg-current opacity-70` 등)를 그대로 쓴다.
|
||||||
|
* ★ **포스트 하나씩** (2026-09-14 대표). 슬라이드 하나 = 꼭지 하나, 전체 폭을 다 쓴다 —
|
||||||
|
* 3~4개를 욱여넣었더니 카드가 좁아져 본문이 한 줄로 접혔었다.
|
||||||
|
*/
|
||||||
|
/* '군산 이야기' 탭 하나로 접혀 들어왔다(2026-09-14) — 독립 섹션의 배경·제호는 없다.
|
||||||
|
나머지 다섯 탭 패널과 같은 폭(`.shell`)에, 위아래 여백만 얹는다. */
|
||||||
|
#w4d-reading-panel {
|
||||||
|
padding: 26px 0 4px;
|
||||||
|
color: var(--tpl-text, #1b1a15);
|
||||||
|
font-family: var(--tpl-font-body, serif);
|
||||||
|
}
|
||||||
|
/* .w4d-read-kicker 삭제 — 2026-09-14 대표: "군산읽기만 타이틀이 없서 통일 시켜",
|
||||||
|
커스텀 캡션 대신 다른 네 탭과 같은 h2.h2 + p.measure 로 바꿨다(inject.js startReading()). */
|
||||||
|
|
||||||
|
/* ── 슬라이드 폭 ── `CarouselSlide` 의 관례와 같다: 모바일은 다음 장이 살짝 걸치게(86%),
|
||||||
|
데스크톱은 한 장이 크게 온다(560px 안팎). "다음 장이 걸쳐 보이는 것" 이 옆에 더 있다는
|
||||||
|
유일한 시각 신호라는 그 컴포넌트의 원칙을 그대로 따른다. */
|
||||||
|
.w4d-read-basis { width: 86%; }
|
||||||
|
@media (min-width: 640px) { .w4d-read-basis { width: min(600px, 56%); } }
|
||||||
|
|
||||||
|
/* ── 포스트 카드 — 슬라이드 하나 = 꼭지 하나. */
|
||||||
|
.w4d-read-card {
|
||||||
|
height: 100%;
|
||||||
|
padding: 18px 20px 16px;
|
||||||
|
background: var(--tpl-card, #efe7d3);
|
||||||
|
border: 1px solid var(--tpl-border, #bcb49e);
|
||||||
|
border-radius: var(--tpl-radius, 0);
|
||||||
|
}
|
||||||
|
.w4d-read-page {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: var(--tpl-accent, #bf2f1b);
|
||||||
|
}
|
||||||
|
.w4d-read-year { font-weight: 400; letter-spacing: 0.06em; opacity: 0.7; }
|
||||||
|
.w4d-read-card h3 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid var(--tpl-primary, #1b1a15);
|
||||||
|
font-family: var(--tpl-font-heading, var(--tpl-font-body, serif));
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.w4d-read-body {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.8;
|
||||||
|
text-align: justify;
|
||||||
|
word-break: keep-all;
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
.w4d-read-src {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px dashed var(--tpl-border, #bcb49e);
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.w4d-read-src a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
||||||
|
/* .w4d-read-flag(확인필요 배지) 삭제 — 2026-09-14 대표: "확인필요 없애고 링크는
|
||||||
|
네이버 링크로", inject.js startReading() 도 같이 뺐다. */
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.w4d-read-card h3 { font-size: 18px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══ 사진 갤러리 쪽 넘김 (데스크톱만) ═══════════════════════════════════════
|
||||||
|
* ★ 왜 (2026-09-14 사장님: "사진갤러리 전체 렌더링 말고 페이징 좀, 너무 많아 데스크톱에서는")
|
||||||
|
* 갤러리는 좁은 화면에서 한 장씩 미는 캐러셀이지만 넓은 화면에서는 4열 격자로 **전부**
|
||||||
|
* 깔린다(`GallerySection.tsx` — `lg:grid lg:grid-cols-4`). 사진 37장이면 열 줄이 되어
|
||||||
|
* 페이지가 사진으로 끊긴다.
|
||||||
|
* ★ 좁은 화면은 그대로 둔다 — 거기서는 한 장이 폭을 다 쓰는 캐러셀이라 이미 한 번에 하나다.
|
||||||
|
* 그래서 숨김 규칙 전부가 `min-width: 1024px`(Tailwind `lg`) 안에 있다.
|
||||||
|
* ★ 마크업을 지우지 않고 **속성으로만** 가린다. React 가 payload 로 다시 그리면 잘라낸
|
||||||
|
* <li> 는 돌아오지만, 그때는 주입 JS 가 속성을 다시 찍는다(inject.js ⑧).
|
||||||
|
* 그리고 사진이 DOM 에 남아 있어야 alt 문장이 문서에 남는다 — 이미지 검색이 그걸 읽는다
|
||||||
|
* (같은 이유가 `GallerySection.tsx` 머리주석에도 있다).
|
||||||
|
*/
|
||||||
|
.w4d-gal-pager { display: none; }
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
#gallery ul[data-w4d-gal] > li[data-w4d-gal-on='0'] { display: none; }
|
||||||
|
|
||||||
|
.w4d-gal-pager {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
.w4d-gal-pager button {
|
||||||
|
min-width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid var(--tpl-border, #bcb49e);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease, opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
.w4d-gal-pager button:hover:not(:disabled) { background: color-mix(in srgb, currentColor 8%, transparent); }
|
||||||
|
.w4d-gal-pager button:disabled { opacity: 0.3; cursor: default; }
|
||||||
|
.w4d-gal-pager button[aria-current='true'] {
|
||||||
|
background: var(--tpl-primary, #1b1a15);
|
||||||
|
border-color: var(--tpl-primary, #1b1a15);
|
||||||
|
color: var(--tpl-bg, #fff);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.w4d-gal-total { margin-left: 8px; font-size: 11.5px; opacity: 0.55; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 768~1023px 에서 하단 탭바가 푸터 마지막 줄을 덮는 것 막기 ─────────────────
|
||||||
|
★ 실측(2026-09-14 대표 스크린샷, 태블릿 폭 Edge): 푸터(SiteFooter.tsx)는 768px
|
||||||
|
부터 아래 여백을 줄이는데(pb-28 → md:pb-14), 하단 고정 탭바(MobileTabBar.tsx)는
|
||||||
|
1024px 이 되기 전엔 그대로 떠 있다(lg:hidden). 그래서 768~1023px 사이에서만
|
||||||
|
푸터 마지막 줄이 탭바에 깔린다 — 두 컴포넌트가 서로 다른 기준(md·lg)으로
|
||||||
|
"이제 모바일 아니다"를 판단해 생기는 폭이다.
|
||||||
|
★ 진짜 수정은 렌더러 쪽(`solution/site/src/sections/SiteFooter.tsx`)에서
|
||||||
|
`md:pb-14` 를 탭바와 같은 `lg:pb-14` 로 맞추는 것인데, 그건 발행 사이트 전부에
|
||||||
|
영향을 주는 변경이라 다시 굽고(`docker compose restart solution-prerender`)
|
||||||
|
전부 재발행(`republish_all.py`)해야 한다 — 이 목업만 먼저 막는다. */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023.98px) {
|
||||||
|
footer { padding-bottom: 7rem !important; }
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,15 @@ from pathlib import Path
|
|||||||
|
|
||||||
SP = Path(__file__).parent
|
SP = Path(__file__).parent
|
||||||
|
|
||||||
|
# ── 최종 업데이트 시각 ────────────────────────────────────────────────────────
|
||||||
|
# 화면 아래 "최종 업데이트", head 의 dateModified, JSON-LD, payload 의 site.updatedAt ·
|
||||||
|
# local.syncedAt 이 전부 이 한 값을 쓴다. 구운 원본에는 2026-09-09 가 박혀 있어, 문구·맛집을
|
||||||
|
# 고친 뒤에도 화면에는 옛 날짜가 남았다 (2026-09-11 사장님: "이거 업뎃해야지").
|
||||||
|
# ★ now() 를 쓰지 않는다 — 내용을 안 고치고 다시 구워도 날짜가 올라가 버린다. 구글은 lastmod 를
|
||||||
|
# 페이지의 실제 수정과 대조해 맞을 때만 쓰고 어긋나면 그 필드를 무시한다(seo/directory.ts).
|
||||||
|
# 내용을 고친 날에 이 값을 손으로 올린다.
|
||||||
|
UPDATED_AT = "2026-09-14T07:42:00+00:00"
|
||||||
|
|
||||||
# ── 캐치프레이즈 100개 ────────────────────────────────────────────────────────
|
# ── 캐치프레이즈 100개 ────────────────────────────────────────────────────────
|
||||||
# 사실이 아니라 문구다(출처가 붙는 값이 아니다). 근거는 payload 의 시설·위치 값 —
|
# 사실이 아니라 문구다(출처가 붙는 값이 아니다). 근거는 payload 의 시설·위치 값 —
|
||||||
# 적산가옥 두 동 · 히로쓰 가옥 옆 · 마당과 정원 · 창고형 카페 · 매일 세탁하는 침구 · 최대 4인.
|
# 적산가옥 두 동 · 히로쓰 가옥 옆 · 마당과 정원 · 창고형 카페 · 매일 세탁하는 침구 · 최대 4인.
|
||||||
@ -92,6 +101,92 @@ WEATHER = {
|
|||||||
"눈": ["기와에 눈이 앉으면 골목이 통째로 조용해집니다", "눈 밟는 소리가 담 안에서 크게 들립니다"],
|
"눈": ["기와에 눈이 앉으면 골목이 통째로 조용해집니다", "눈 밟는 소리가 담 안에서 크게 들립니다"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── 오늘의 날씨 문구 ──────────────────────────────────────────────────────────
|
||||||
|
# ★ 케이스마다 한 줄뿐이었다 (2026-09-14 사장님: "케이스당 여러개로 만들어서 랜덤으로")
|
||||||
|
# 같은 하늘·같은 기온이면 늘 같은 문장이 떴다. 시연에서 새로고침해도 안 바뀐다.
|
||||||
|
# ★ 축을 둘로 갈라 둔 것은 그대로다(site-payload.ts WeatherSnapshot 주석) —
|
||||||
|
# 하늘은 **집 안에서** 뭘 할지, 기온은 **밖에서** 어디를 갈지다. 한 칸에 담으면
|
||||||
|
# 5×5 스물다섯 벌을 적어야 한다.
|
||||||
|
# ★ 이름을 대는 곳은 이 사이트에 **이미 있는 자리**만 쓴다(주변 안내 12곳 · 맛집 31곳).
|
||||||
|
# 화면에 없는 곳을 날씨 칸에서만 권하면 손님이 그걸 찾을 데가 없다.
|
||||||
|
# ★ 하늘은 다섯이다. 렌더러는 넷(맑음·흐림·비·눈)으로만 가르므로 구름많음과 그 외
|
||||||
|
# (안개·소나기·뇌우)가 `notes.흐림` 한 칸을 같이 본다 — 그 둘을 가르는 것은 inject.js ⑥ 다.
|
||||||
|
SKY_NOTES = {
|
||||||
|
"맑음": [
|
||||||
|
"마당 평상에 앉으면 새소리만 들립니다. 정원 쪽으로 그늘이 길게 눕는 시간입니다.",
|
||||||
|
"기와가 마르는 냄새가 나는 날입니다. 아침에 널어 두신 것은 오후면 다 마릅니다.",
|
||||||
|
"볕이 좋아 대문을 열어 둡니다. 마당 사진은 해가 기울기 시작할 때가 가장 곱습니다.",
|
||||||
|
"창을 다 열어도 좋은 날입니다. A동 장지문으로 드는 빛이 오전에 가장 깊습니다.",
|
||||||
|
"해가 길게 남는 날입니다. 짐을 풀고 골목부터 한 바퀴 돌고 오셔도 늦지 않습니다.",
|
||||||
|
],
|
||||||
|
"구름많음": [
|
||||||
|
"빛이 부드러운 날입니다. 골목 사진이 제일 잘 나오는 빛입니다.",
|
||||||
|
"그늘이 옅어 마당이 눈부시지 않습니다. 평상에 오래 앉아 계시기 좋습니다.",
|
||||||
|
"해가 들었다 숨었다 합니다. 창고형 CAFÉ 공간과 마당을 오가며 쉬시면 됩니다.",
|
||||||
|
"구름이 얇은 날입니다. 우체통거리까지 걷기에 덥지도 춥지도 않습니다.",
|
||||||
|
"빛이 고른 날입니다. 초원사진관까지 걸어서 삼 분, 사진은 오늘 같은 하늘이 낫습니다.",
|
||||||
|
],
|
||||||
|
"그외": [
|
||||||
|
"해가 숨은 날입니다. 기와와 담이 한 색으로 가라앉고, 마당 평상에 앉아도 눈이 부시지 않습니다.",
|
||||||
|
"하늘이 자주 바뀌는 날입니다. 나가실 때 우산을 챙기시는 편이 낫습니다.",
|
||||||
|
"안개가 낮게 깔리면 골목 끝이 흐려집니다. 서두르지 마시고 늦게 나가셔도 됩니다.",
|
||||||
|
"소나기가 지나갈 수 있는 하늘입니다. 창고형 CAFÉ 공간에서 한 차례 보내고 나가셔도 됩니다.",
|
||||||
|
"바람이 먼저 바뀌는 날입니다. 마당에 널어 두신 것은 들여놓으시는 편이 좋습니다.",
|
||||||
|
],
|
||||||
|
"비": [
|
||||||
|
"백 년 된 기와를 타고 내리는 소리가 다릅니다. 창고형 CAFÉ 공간에서 빗소리를 들으실 수 있습니다.",
|
||||||
|
"비 오는 골목에는 사람이 없습니다. 히로쓰 가옥 담길은 오늘 같은 날이 가장 조용합니다.",
|
||||||
|
"마당 디딤돌이 미끄럽습니다. 밤에 나가실 때는 정원 석등 쪽으로 도세요.",
|
||||||
|
"실내로 도는 편이 낫습니다. 근대건축관과 군산근대미술관이 걸어서 십 분 안에 나란히 있습니다.",
|
||||||
|
"우산은 대문 안쪽에 두시면 됩니다. 젖은 것은 테라스 쪽이 잘 마릅니다.",
|
||||||
|
],
|
||||||
|
"눈": [
|
||||||
|
"기와에 눈이 앉으면 골목이 통째로 조용해집니다. 정원 석등만 켜 두겠습니다.",
|
||||||
|
"눈 밟는 소리가 담 안에서 크게 들립니다. 마당은 아침에 한 번 쓸어 두겠습니다.",
|
||||||
|
"눈 오는 날의 원도심은 사람이 적습니다. 초원사진관 골목이 오늘 가장 한산합니다.",
|
||||||
|
"디딤돌에 눈이 얼 수 있습니다. 밤에 드나드실 때는 조심해서 딛으세요.",
|
||||||
|
"창을 닫아 두시는 편이 낫습니다. 창고형 CAFÉ 공간이 이런 날 가장 아늑합니다.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
# 기온대 경계는 렌더러가 정한다(`site/src/lib/derive.ts weatherBand`) — 30·25·20·10.
|
||||||
|
TEMP_NOTES = {
|
||||||
|
"혹서": [
|
||||||
|
"낮에는 군산근대미술관·근대건축관처럼 실내로 도는 편이 낫습니다. 월명호수 한 바퀴는 해가 넘어간 뒤가 시원합니다.",
|
||||||
|
"한낮 골목에는 그늘이 없습니다. 초원사진관·영화의 거리는 이른 아침이나 해 질 무렵에 도세요.",
|
||||||
|
"옛 군산세관과 근대건축관은 걸어서 이어집니다. 실내를 징검다리 삼아 도시면 덜 지칩니다.",
|
||||||
|
"경암동 철길마을은 그늘이 적습니다. 오늘은 저녁에 가시는 편이 낫습니다.",
|
||||||
|
"물은 나가시기 전에 챙기세요. 이성당까지는 걸어서 오백 미터, 해가 기운 뒤가 낫습니다.",
|
||||||
|
],
|
||||||
|
"더움": [
|
||||||
|
"선유도까지 다녀오기 좋은 기온입니다. 골목은 오후 늦게 도세요 — 경암동 철길마을은 해 질 때가 사람이 적습니다.",
|
||||||
|
"낮에는 월명호수 그늘길이 시원합니다. 물가를 따라 걷다 돌아오시면 됩니다.",
|
||||||
|
"우체통거리와 영화의 거리는 그늘이 이어집니다. 걸어서 도시기에 무리가 없습니다.",
|
||||||
|
"해가 길어 저녁이 넉넉합니다. 내항 쪽으로 나가 노을을 보고 오셔도 됩니다.",
|
||||||
|
"가장 더운 시간만 실내가 낫습니다. 군산근대미술관에서 한 차례 쉬어 가세요.",
|
||||||
|
],
|
||||||
|
"선선": [
|
||||||
|
"걷기에 가장 좋은 날입니다. 히로쓰 가옥에서 초원사진관·영화의 거리까지 걸어서 그대로 이어집니다.",
|
||||||
|
"원도심은 오늘 하루면 다 걸립니다. 군산 시간여행마을까지 천천히 다녀오셔도 됩니다.",
|
||||||
|
"차를 두고 걷는 편이 낫습니다. 우체통거리에서 옛 군산세관까지 골목이 계속 이어집니다.",
|
||||||
|
"해가 부담스럽지 않습니다. 경암동 철길마을까지 다녀오기에 알맞은 기온입니다.",
|
||||||
|
"낮에는 월명호수를 한 바퀴 돌고, 밤에는 마당에 앉아 계셔도 좋습니다.",
|
||||||
|
],
|
||||||
|
"쌀쌀": [
|
||||||
|
"월명호수를 한 바퀴 돌고 이성당이나 국제반점에서 따뜻한 것을 드시면 알맞습니다.",
|
||||||
|
"겉옷을 하나 더 챙기세요. 해가 지면 항구 쪽 바람이 먼저 찹니다.",
|
||||||
|
"실내와 골목을 섞어 도는 편이 낫습니다. 근대건축관에서 몸을 녹이고 다시 나오시면 됩니다.",
|
||||||
|
"따뜻한 국물이 도는 날입니다. 복성루·빈해원처럼 가까운 집부터 보세요.",
|
||||||
|
"오후 볕이 짧습니다. 초원사진관·영화의 거리를 먼저 돌고 저녁에 들어오세요.",
|
||||||
|
],
|
||||||
|
"추움": [
|
||||||
|
"밖은 바람이 매섭습니다. 근대건축관·근대미술관과 시간여행마을 실내를 묶어 도는 편이 낫습니다.",
|
||||||
|
"항구 바람이 매운 날입니다. 골목은 짧게 돌고 실내를 길게 잡으세요.",
|
||||||
|
"장갑과 목도리를 챙기세요. 옛 군산세관 앞은 바람이 그대로 지나갑니다.",
|
||||||
|
"따뜻한 것을 먼저 드시는 편이 낫습니다. 이성당·국제반점은 걸어서 오백 미터입니다.",
|
||||||
|
"일찍 들어오셔도 됩니다. 벽난로가 있는 거실이 이런 날 가장 좋습니다.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
items = [{"text": t, "kind": "general"} for t in GENERAL]
|
items = [{"text": t, "kind": "general"} for t in GENERAL]
|
||||||
items += [{"text": t, "kind": "season", "season": s} for s, lst in SEASON.items() for t in lst]
|
items += [{"text": t, "kind": "season", "season": s} for s, lst in SEASON.items() for t in lst]
|
||||||
items += [{"text": t, "kind": "month", "month": m} for m, lst in MONTH.items() for t in lst]
|
items += [{"text": t, "kind": "month", "month": m} for m, lst in MONTH.items() for t in lst]
|
||||||
@ -99,6 +194,71 @@ items += [{"text": t, "kind": "weather", "weather": w} for w, lst in WEATHER.ite
|
|||||||
|
|
||||||
payload = json.loads((SP / "stay-payload-new.json").read_text(encoding="utf-8"))
|
payload = json.loads((SP / "stay-payload-new.json").read_text(encoding="utf-8"))
|
||||||
payload["narrative"]["catchphrases"] = {"version": 1, "items": items}
|
payload["narrative"]["catchphrases"] = {"version": 1, "items": items}
|
||||||
|
|
||||||
|
# ── '오늘의 엽서' 탭을 없앤다 ─────────────────────────────────────────────────
|
||||||
|
# ★ 왜 (2026-09-14 대표: "오늘의 엽서 칸을 빼고 이 엽서 포맷으로 스테이 머뭄 관련
|
||||||
|
# 엽서를 쓰는 걸로 하자") — 도시 엽서 4장은 이제 안 쓴다. 대신 손님이 직접 쓰는
|
||||||
|
# 엽서 섹션을 새로 만들고(inject.js `postcardMaker`), 그건 '군산 이야기' 탭이
|
||||||
|
# 아니라 `#location`–`#festival` 사이의 **독립 섹션**이다.
|
||||||
|
# ★ 마크업을 안 건드리고 payload 만 비운다 — `StorySection.tsx` 는
|
||||||
|
# `sectionItems(payload,'postcard').items.length > 0` 인 탭만 세우므로, data 를
|
||||||
|
# 비우면 렌더러 코드는 그대로 두고도 탭이 저절로 사라진다(README 2.6 의 원칙과 같다:
|
||||||
|
# "마크업을 고쳐 지우지 않는다").
|
||||||
|
for _sec in payload["theme"]["sections"]:
|
||||||
|
if _sec["id"] == "postcard":
|
||||||
|
_sec["data"] = json.dumps({"kind": "postcard", "version": 1, "items": []}, ensure_ascii=False)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 굽는 판에는 케이스마다 **첫 문장**을 박는다 — 크롤러와 첫 화면이 보는 값이고,
|
||||||
|
# inject.js 가 DOM 에서 갈아 끼울 때 찾는 닻이기도 하다. 문장이 겹치면 엉뚱한 줄을 바꾼다.
|
||||||
|
_all_notes = [t for lst in SKY_NOTES.values() for t in lst] + \
|
||||||
|
[t for lst in TEMP_NOTES.values() for t in lst]
|
||||||
|
assert len(_all_notes) == len(set(_all_notes)), "날씨 문구가 겹친다 — DOM 에서 어느 줄인지 못 가른다"
|
||||||
|
payload["local"]["weather"].update({
|
||||||
|
# 렌더러가 아는 넷. 구름많음은 '흐림' 칸을 쓴다(그 외와 가르는 것은 inject.js ⑦).
|
||||||
|
"notes": {"맑음": SKY_NOTES["맑음"][0], "흐림": SKY_NOTES["구름많음"][0],
|
||||||
|
"비": SKY_NOTES["비"][0], "눈": SKY_NOTES["눈"][0]},
|
||||||
|
"tempNotes": {band: lines[0] for band, lines in TEMP_NOTES.items()},
|
||||||
|
# 렌더러가 모르는 칸이다(계약에 없어 그대로 지나간다). 주입분만 읽는다.
|
||||||
|
"noteSets": SKY_NOTES,
|
||||||
|
"tempNoteSets": TEMP_NOTES,
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── 주변 맛집 8곳 → 2km 안 31곳 ──────────────────────────────────────────────
|
||||||
|
# ★ 원본 8곳이 전부 467m 안이라 '걸어서 10분 이상' 탭에서 맛집 칸이 비었다 (2026-09-11 사장님:
|
||||||
|
# "주변맛집 데이터가 너무 적은거같아"). 목록은 fetch_restaurants.py 가 TourAPI 에서 받아 둔 것이다 —
|
||||||
|
# 여기서 API 를 부르지 않는다. 굽기가 네트워크·키에 매이면 다른 기계에서 못 굽는다.
|
||||||
|
payload["local"]["restaurants"] = json.loads((SP / "restaurants.json").read_text(encoding="utf-8"))
|
||||||
|
# 사진 없는 집에 시연본 전용 사진을 덧댄다(fill_restaurant_photos.py — 공공누리 아님, 제품으로 옮기지 않는다).
|
||||||
|
# TourAPI 사진이 있는 집은 건드리지 않는다.
|
||||||
|
_photos = json.loads((SP / "restaurant-photos.json").read_text(encoding="utf-8"))
|
||||||
|
for _r in payload["local"]["restaurants"]:
|
||||||
|
if not _r.get("imageUrl") and _photos.get(_r["name"], {}).get("file"):
|
||||||
|
_r["imageUrl"] = f"/assets/mirror/{_photos[_r['name']]['file']}"
|
||||||
|
# ── 군산 국가유산 야행 ───────────────────────────────────────────────────────
|
||||||
|
# ★ 왜 (2026-09-14 대표: 받은 `gunsan_365_story_db.xlsx` 52주제 대조 — 사이트에 없던 둘 중 하나)
|
||||||
|
# 나머지 축제는 TourAPI 가 준 것이라 이 목록에 없었다. 원도심 국가유산을 밤에 여는
|
||||||
|
# 행사라 머뭄(신흥동)에서 걸어 닿는 범위이고, 여름 저녁 일정과 맞는다.
|
||||||
|
# ★ **이름은 '문화재야행' 이 아니다.** 엑셀에는 옛 이름으로 적혀 있는데, 2024년 문화재청이
|
||||||
|
# 국가유산청으로 바뀌면서 행사명도 '국가유산 야행' 으로 갈렸다(군산시 문화예술과 공지 확인).
|
||||||
|
# 옛 이름으로 두면 검색 링크가 엉뚱한 해의 문서로 간다.
|
||||||
|
# ★ 날짜(period·startDate)는 **적지 않는다.** 확인한 공지는 2025년분이고 2026년 일정은
|
||||||
|
# 아직 공고 전이다 — 지어내지 않는다(레포 절대 규칙). 축제 10개 중 5개가 이미 날짜 없이
|
||||||
|
# 서 있어 화면도 그 경우를 감당한다.
|
||||||
|
payload["local"]["festivals"].append({
|
||||||
|
"name": "군산 국가유산 야행",
|
||||||
|
"month": "8월",
|
||||||
|
"season": "여름",
|
||||||
|
"location": "구 조선식량영단 등 원도심 국가유산 일원 · 걸어서 20분",
|
||||||
|
"description": "원도심의 국가유산을 밤에 여는 행사다. 야경·야로·야사 등 여덟 갈래로 나뉘어 "
|
||||||
|
"신흥동 일본식 가옥의 정원 조명, 스탬프 투어, 옛 건물에서 여는 공연과 야시장이 "
|
||||||
|
"저녁부터 밤까지 이어진다.",
|
||||||
|
"searchQuery": "군산 국가유산 야행",
|
||||||
|
})
|
||||||
|
|
||||||
|
payload["site"]["updatedAt"] = UPDATED_AT
|
||||||
|
# 주변 정보(맛집·명소)를 갈아 끼운 시각도 같다 — 이 스크립트가 그 둘을 같이 바꾼다.
|
||||||
|
payload["local"]["syncedAt"] = UPDATED_AT
|
||||||
(SP / "stay-payload-new.json").write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
(SP / "stay-payload-new.json").write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
# ── 머뭄이 만든 노래 5곡 ──────────────────────────────────────────────────────
|
# ── 머뭄이 만든 노래 5곡 ──────────────────────────────────────────────────────
|
||||||
@ -234,6 +394,35 @@ for unit in payload["units"]:
|
|||||||
# 옛 사진은 남기지 않는다 — 등록본이 그 객실에 붙여 놓은 사진만 그 순서로 세운다.
|
# 옛 사진은 남기지 않는다 — 등록본이 그 객실에 붙여 놓은 사진만 그 순서로 세운다.
|
||||||
unit["mediaIds"] = per_unit.get(room, [])
|
unit["mediaIds"] = per_unit.get(room, [])
|
||||||
|
|
||||||
|
# ── 화면에서 뺄 탭 ───────────────────────────────────────────────────────────
|
||||||
|
# ★ `enabled = False` 로는 안 꺼진다 (2026-09-14 사장님: "오늘의 한장은 주석처리해주쇼")
|
||||||
|
# 이 다섯은 '군산 이야기' 안의 탭이고, 탭은 **항목이 있는 것만** 선다(StorySection.tsx:41).
|
||||||
|
# enabled 는 아예 보지 않는다 — 실측: daily 가 enabled=False 인데도 탭이 서 있었다.
|
||||||
|
# ★ 그래서 항목을 비운다. 원값은 stay-payload-new.json 에 그대로 있으므로(위에서 이미 썼다)
|
||||||
|
# 되살리려면 아래 집합에서 id 만 빼면 된다. 다시 만들 필요 없다.
|
||||||
|
HIDE_TABS = {"daily"} # 오늘의 한 장
|
||||||
|
|
||||||
|
for _section in payload["theme"]["sections"]:
|
||||||
|
if _section["id"] not in HIDE_TABS:
|
||||||
|
continue
|
||||||
|
_env = json.loads(_section["data"])
|
||||||
|
_env["items"] = []
|
||||||
|
_section["data"] = json.dumps(_env, ensure_ascii=False)
|
||||||
|
_section["enabled"] = False
|
||||||
|
|
||||||
|
# ── 예약 안내를 켠다 ─────────────────────────────────────────────────────────
|
||||||
|
# ★ 왜 (2026-09-14 사장님: "예약 섹션을 안 가져다가 렌더링하네, 그거 구현되었으니 가져오셈")
|
||||||
|
# 원본 payload 의 booking 은 enabled=False 였다. 숙박이면 이 자리는 요금·인원·창구에
|
||||||
|
# 날짜/시간 목업까지 붙은 `StayBookingSection` 이 그린다(HomePage.tsx:69) — 회의록의
|
||||||
|
# "예약 페이지는 연동이 어려워 우선 목업 형태로" 가 이미 구현돼 있던 것이다.
|
||||||
|
# ★ 기본값(항목이 없으면 알아서 나가는 HomePage.tsx:122)에 기대지 못한다 — 항목이
|
||||||
|
# **있는데 꺼진** 상태라 `hasSection()` 이 참이고, 그 분기는 "사장님이 끈 것" 으로 존중한다.
|
||||||
|
# ★ 지어낸 값이 없다. 요금·인원·취소 규정은 payload 의 확인된 fact 뿐이고
|
||||||
|
# (`derive.ts stayBookingView`), 근거가 하나도 없으면 섹션째 안 그린다.
|
||||||
|
for _section in payload["theme"]["sections"]:
|
||||||
|
if _section["id"] == "booking":
|
||||||
|
_section["enabled"] = True
|
||||||
|
|
||||||
# ── index.html 조립 ──────────────────────────────────────────────────────────
|
# ── index.html 조립 ──────────────────────────────────────────────────────────
|
||||||
html = (SP / "orig" / "index.html").read_text(encoding="utf-8")
|
html = (SP / "orig" / "index.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
@ -244,6 +433,14 @@ html, n = re.subn(r"<script>window\.__SITE_PAYLOAD__=.*?</script>",
|
|||||||
html, count=1, flags=re.S)
|
html, count=1, flags=re.S)
|
||||||
assert n == 1, "payload 스크립트를 못 찾았다"
|
assert n == 1, "payload 스크립트를 못 찾았다"
|
||||||
|
|
||||||
|
# ★ 날짜는 payload 밖에도 박혀 있다 — head 의 dateModified · article:modified_time ·
|
||||||
|
# JSON-LD · 화면의 <time>. payload 만 고치면 **크롤러와 첫 화면은 옛 날짜를 본다**
|
||||||
|
# (React 가 다시 그리기 전까지, 그리고 JS 를 안 돌리는 크롤러에게는 영영).
|
||||||
|
OLD_UPDATED_AT = "2026-09-09T13:40:00+00:00"
|
||||||
|
html = html.replace(OLD_UPDATED_AT, UPDATED_AT)
|
||||||
|
html = html.replace(f">{OLD_UPDATED_AT[:10]}</time>", f">{UPDATED_AT[:10]}</time>")
|
||||||
|
assert OLD_UPDATED_AT not in html and OLD_UPDATED_AT[:10] not in html, "옛 날짜가 남았다"
|
||||||
|
|
||||||
# ★ 이용 정보 **맨 아래 구분선**을 지운다 (2026-09-10 사장님 지시).
|
# ★ 이용 정보 **맨 아래 구분선**을 지운다 (2026-09-10 사장님 지시).
|
||||||
# 항목이 끝난 자리에 선이 한 줄 더 그어져 있어, 빈 칸이 하나 더 있는 것처럼 보인다.
|
# 항목이 끝난 자리에 선이 한 줄 더 그어져 있어, 빈 칸이 하나 더 있는 것처럼 보인다.
|
||||||
# 항목(바비큐 이용) 자체는 그대로 둔다 — 지우라고 한 것은 선이다.
|
# 항목(바비큐 이용) 자체는 그대로 둔다 — 지우라고 한 것은 선이다.
|
||||||
@ -252,9 +449,23 @@ assert n == 1, "payload 스크립트를 못 찾았다"
|
|||||||
|
|
||||||
css = (SP / "inject.css").read_text(encoding="utf-8")
|
css = (SP / "inject.css").read_text(encoding="utf-8")
|
||||||
js = (SP / "inject.js").read_text(encoding="utf-8")
|
js = (SP / "inject.js").read_text(encoding="utf-8")
|
||||||
|
# 빌드 도장 — 콘솔에서 "지금 보는 게 언제 구운 것인가"를 바로 읽게 한다(위 ★ 주석, inject.js).
|
||||||
|
assert "__W4D_BUILD__" in js, "inject.js 에서 빌드 도장 자리가 사라졌다"
|
||||||
|
js = js.replace("__W4D_BUILD__", UPDATED_AT)
|
||||||
assert "</style>" not in css and "</script>" not in js
|
assert "</style>" not in css and "</script>" not in js
|
||||||
|
|
||||||
block = ("\n <style id=\"w4d-css\">\n" + css + "\n </style>\n"
|
# ★ 군산 읽기 카로셀은 이 사이트가 실제 쓰는 그 라이브러리(embla-carousel)로 움직인다
|
||||||
|
# (2026-09-14 대표: "기존 카로셀 쓰라고, 니가 뭔데 니맘대로 그지같이 구현해" — 맞는 말이다).
|
||||||
|
# `inject.js` 는 React 번들 밖의 순정 스크립트라 앱이 물고 있는 embla 인스턴스를 가져다
|
||||||
|
# 쓸 수 없다 — 그래서 **코어(라이브러리 자체, React 래퍼 없이)의 UMD 빌드**를 이 목업
|
||||||
|
# 전용으로 하나 더 들여왔다. 의존성 0 · 17.9KB(비압축, 사이트 자체 주석의 "~5KB gzip"과
|
||||||
|
# 같은 값) — `node_modules/embla-carousel/embla-carousel.umd.js` 를 그대로 복사한 것이고
|
||||||
|
# 손으로 고치지 않는다(라이선스 MIT, 버전은 site/package.json 의 embla-carousel-react 와 맞춘다).
|
||||||
|
assert (SP / "vendor" / "embla-carousel.umd.js").exists(), \
|
||||||
|
"vendor/embla-carousel.umd.js 가 없다 — node_modules/embla-carousel/embla-carousel.umd.js 를 복사한다"
|
||||||
|
|
||||||
|
block = ("\n <script src=\"/s/stay/vendor/embla-carousel.umd.js\"></script>\n"
|
||||||
|
" <style id=\"w4d-css\">\n" + css + "\n </style>\n"
|
||||||
" <script id=\"w4d-js\">\n" + js + "\n </script>\n ")
|
" <script id=\"w4d-js\">\n" + js + "\n </script>\n ")
|
||||||
# 치환문(block)에 JS 의 정규식(\s 등)이 섞여 있어 그대로 넘기면 파이썬이 이스케이프로 읽는다 — 람다로 넘긴다.
|
# 치환문(block)에 JS 의 정규식(\s 등)이 섞여 있어 그대로 넘기면 파이썬이 이스케이프로 읽는다 — 람다로 넘긴다.
|
||||||
html, n = re.subn(r"\n?\s*</body>", lambda _m: block + "</body>", html, count=1)
|
html, n = re.subn(r"\n?\s*</body>", lambda _m: block + "</body>", html, count=1)
|
||||||
@ -270,6 +481,28 @@ assert n == 1, "</body> 를 못 찾았다"
|
|||||||
# 그래서 둘 다 `img/mirror/` · `vendor/` 로 복사해 두고, 경로를 `/s/stay/…` 로 바꿔 박는다.
|
# 그래서 둘 다 `img/mirror/` · `vendor/` 로 복사해 두고, 경로를 `/s/stay/…` 로 바꿔 박는다.
|
||||||
# 이러면 nginx 가 `/s/` 를 파일로 주기만 하면 되고 번들 해시와 무관해진다.
|
# 이러면 nginx 가 `/s/` 를 파일로 주기만 하면 되고 번들 해시와 무관해진다.
|
||||||
# (대신 렌더러를 고쳐도 이 목업은 안 따라간다 — 목업은 원래 재굽기 대상이 아니라 그게 맞다.)
|
# (대신 렌더러를 고쳐도 이 목업은 안 따라간다 — 목업은 원래 재굽기 대상이 아니라 그게 맞다.)
|
||||||
|
# ★ 번들은 **레포에서 지금 구운 것**으로 갈아 끼운다 — `orig/index.html` 에 박힌 해시는
|
||||||
|
# 2026-09-09 판이라 그 뒤에 들어온 화면이 없다(실측: 숙박 예약 섹션 `StayBookingSection`
|
||||||
|
# 이 그 번들에 아예 없어서, payload 로 켜도 화면에 아무것도 안 그려졌다).
|
||||||
|
# 목업은 자기 번들을 `vendor/` 에 들고 다니므로(아래 주석) 해시를 맞출 상대가 없다 —
|
||||||
|
# `vendor/` 에 있는 한 벌이 정답이고, 원본의 옛 이름을 그 이름으로 바꾼다.
|
||||||
|
# 갱신 절차: `solution/site` 에서 `npm run build:client` → `dist/client/assets/index-*.{js,css}`
|
||||||
|
# 를 `vendor/` 로 복사하고 옛 파일을 지운다. (여기는 이름만 맞추므로 손댈 것이 없다.)
|
||||||
|
# ★ `<body class="site">` — 새 번들에서는 이게 없으면 35줄이 조용히 죽는다.
|
||||||
|
# 2026-09-09 의 `b1386dd`(에디터와 발행본을 한 렌더러로) 이후 발행본 공용 스타일이
|
||||||
|
# `:where(.site,.site-canvas) …` 로 스코프됐다. `orig/index.html` 은 그 전에 구운 것이라
|
||||||
|
# body 에 클래스가 없다 — 실측: 카로셀 상자(`.slider-viewport`)의 `overflow-x` 가 통째로
|
||||||
|
# 빠져 모바일에서 판 25장이 화면 밖으로 흘러 **가로 스크롤이 생겼다.**
|
||||||
|
# 프리렌더가 지금 내보내는 것과 같은 마크업으로 맞춘다(`scripts/prerender.ts:384`).
|
||||||
|
html, n = re.subn(r"<body(?![^>]*\bclass=)", '<body class="site"', html, count=1) if True else (html,1)
|
||||||
|
assert n == 1 or 'class="site"' in html, "body 에 site 클래스를 못 붙였다"
|
||||||
|
|
||||||
|
_vendor = {ext: sorted((SP / "vendor").glob(f"index-*.{ext}")) for ext in ("js", "css")}
|
||||||
|
for _ext, _found in _vendor.items():
|
||||||
|
assert len(_found) == 1, f"vendor/ 의 {_ext} 번들이 {len(_found)}개다 — 한 벌만 둔다: {_found}"
|
||||||
|
html = re.sub(r"/assets/index-[\w.-]+\.js", "/assets/" + _vendor["js"][0].name, html)
|
||||||
|
html = re.sub(r"/assets/index-[\w.-]+\.css", "/assets/" + _vendor["css"][0].name, html)
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
for name in sorted(set(re.findall(r"/assets/mirror/[0-9a-f]+\.\w+", html))):
|
for name in sorted(set(re.findall(r"/assets/mirror/[0-9a-f]+\.\w+", html))):
|
||||||
if not (SP / "img" / "mirror" / name.rsplit("/", 1)[1]).exists():
|
if not (SP / "img" / "mirror" / name.rsplit("/", 1)[1]).exists():
|
||||||
@ -289,6 +522,7 @@ out.mkdir(exist_ok=True)
|
|||||||
(out / "index.html").write_text(html, encoding="utf-8")
|
(out / "index.html").write_text(html, encoding="utf-8")
|
||||||
|
|
||||||
env = json.loads({s["id"]: s for s in payload["theme"]["sections"]}["itinerary"]["data"])
|
env = json.loads({s["id"]: s for s in payload["theme"]["sections"]}["itinerary"]["data"])
|
||||||
print(f"index.html {len(html):,}자 · 일정 {len(env['items'])}개 · 캐치프레이즈 {len(items)}개")
|
print(f"index.html {len(html):,}자 · 일정 {len(env['items'])}개 · 캐치프레이즈 {len(items)}개 "
|
||||||
|
f"· 맛집 {len(payload['local']['restaurants'])}곳")
|
||||||
print(" " + " · ".join(f"{k} {sum(1 for i in items if i['kind'] == k)}"
|
print(" " + " · ".join(f"{k} {sum(1 for i in items if i['kind'] == k)}"
|
||||||
for k in ("general", "season", "month", "weather")))
|
for k in ("general", "season", "month", "weather")))
|
||||||
|
|||||||
57
solution/site/scripts/mockup/restaurant-photos.json
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"군산복집": {
|
||||||
|
"url": "https://cdn.visitkorea.or.kr/img/call?cmd=VIEW&id=07337813-0fb6-4e60-a6c7-e130feff5a63",
|
||||||
|
"title": "(인쇄용) 군산복집 | 대한민국 구석구석",
|
||||||
|
"file": "067f6fe42c045a3a.jpg"
|
||||||
|
},
|
||||||
|
"진갈비": {
|
||||||
|
"url": "http://www.gunsan.go.kr/upload_data/board_data/BBS_0000054/140229349431468.jpg",
|
||||||
|
"title": "음식,숙박,쇼핑 > 음식점 > 군산맛집",
|
||||||
|
"file": "92cb907d07db9953.jpg"
|
||||||
|
},
|
||||||
|
"한주옥": {
|
||||||
|
"url": "https://d12zq4w4guyljn.cloudfront.net/20220816093444324_photo_3w9NMDVGF1oN.webp",
|
||||||
|
"title": "한주옥 - 군산 간장게장, 꽃게장정식 맛집 - 다이닝코드",
|
||||||
|
"file": "f9cb7e88415edd06.jpg"
|
||||||
|
},
|
||||||
|
"사골뚝배기": {
|
||||||
|
"url": "https://www.gunsan.go.kr/upload_data/board_data/FOOD/150838840634244.jpg",
|
||||||
|
"title": "게시글 목록 < 탕류 < 음식 < 음식/숙박/쇼핑 < 문화관광",
|
||||||
|
"file": "24043ae4f5854f96.jpg"
|
||||||
|
},
|
||||||
|
"스위트인디아": {
|
||||||
|
"url": "https://cdn.visitkorea.or.kr/img/call?cmd=VIEW&id=bd1e6e61-70f2-462d-a8f7-f7fc31d98443",
|
||||||
|
"title": "스위트인디아> 여행지 :대한민국 구석구석 스위트인디아> 여행지 :대한민국 구석구석 ",
|
||||||
|
"file": "bf6bb6183c6f6246.jpg"
|
||||||
|
},
|
||||||
|
"홍영장": {
|
||||||
|
"url": "https://pup-post-phinf.pstatic.net/MjAyNjA1MjNfMTYg/MDAxNzc5NTE2OTMxNDkz.SRqGW5cr3uqRcOaulY6pgfkM_b8UPyxL1QwZ_WN-bfUg.tDWdfAwPxIUmrComXVMgo-hSslTYtbmI3x3iQyG0XKAg.JPEG/PostEncodingTask.30343AE4-7E4E-47B8-A9C0-978AD991D374.jpg",
|
||||||
|
"title": "물짜장 맛집입니더!!! #가정의달 #오늘클립챌린지 #군산 #홍영장 #맛집",
|
||||||
|
"file": "087900c82da3b2f5.jpg"
|
||||||
|
},
|
||||||
|
"명동소바": {
|
||||||
|
"url": "https://cdn.visitkorea.or.kr/img/call?cmd=VIEW&id=26a5546a-6966-4391-b0eb-46703f24adb9",
|
||||||
|
"title": "명동소바> 여행지 :대한민국 구석구석 명동소바> 여행지 :대한민국 구석구석 ",
|
||||||
|
"file": "55f57f45fcebb460.jpg"
|
||||||
|
},
|
||||||
|
"유락식당": {
|
||||||
|
"url": "https://cdn.visitkorea.or.kr/img/call?cmd=VIEW&id=911b5e60-01e0-4eab-8579-4cd447b94dca",
|
||||||
|
"title": "(인쇄용) [백년가게]유락식당 | 대한민국 구석구석",
|
||||||
|
"file": "5f92192c7b3e2cbf.jpg"
|
||||||
|
},
|
||||||
|
"일풍식당": {
|
||||||
|
"url": "https://ldb-phinf.pstatic.net/20200103_164/1578048423036vc5I1_JPEG/lIOOzZx3sZZK9JUJgMsD3dHg.jpg",
|
||||||
|
"title": "일풍식당",
|
||||||
|
"file": "e13c8a2a1aa2762c.jpg"
|
||||||
|
},
|
||||||
|
"아리랑": {
|
||||||
|
"url": "https://pup-post-phinf.pstatic.net/MjAyNjA2MDhfNCAg/MDAxNzgwODg2MDI5NTY1.6vNfC_2hbbyXSjtxSEDSTzkn0f3j8rrEjU15_2DL2Kkg.dbYhyKh4ozdjjWXUUS-oYnCJp7-oFfh_2bU0oc1OBAog.JPEG/POST_IMAGE_ENC_20260608_113317_278.jpg",
|
||||||
|
"title": "#오늘클립챌린지#아리랑식당#비빔밥정식#군산맛집",
|
||||||
|
"file": "1302460cb149975f.jpg"
|
||||||
|
},
|
||||||
|
"국일식당": {
|
||||||
|
"url": "https://ldb-phinf.pstatic.net/20240406_119/1712415338890jfJDL_JPEG/IMG_3315.jpeg",
|
||||||
|
"title": "국일복아구",
|
||||||
|
"file": "6102b164464dbfa4.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
239
solution/site/scripts/mockup/restaurants.json
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "도란",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "도란",
|
||||||
|
"distanceText": "135m",
|
||||||
|
"imageUrl": "/assets/mirror/12938240987c1f78.jpg",
|
||||||
|
"description": "전라북도 군산시 월명동에 있는 요리주점이다. 와인과 함께 카페와 양식을 메뉴를 주로 다루고 있다. 대표 메뉴로는 감바스, 해산물 크림 스튜, 부채살 스테이크 등이 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "물밀소",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "물밀소",
|
||||||
|
"distanceText": "297m",
|
||||||
|
"imageUrl": "/assets/mirror/07600bb67b91dd15.jpg",
|
||||||
|
"description": "월명동에 자리한 베이커리 물밀소는 신생 빵집으로 입소문이 자자하다. 붉은 벽돌의 아담한 외관을 가졌고 매장 내부에는 저온숙성한 반죽으로 빚어낸 건강한 빵들이 진열돼 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "일흥옥",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "일흥옥",
|
||||||
|
"distanceText": "300m",
|
||||||
|
"imageUrl": "/assets/mirror/3420ff7e357ab7ef.jpg",
|
||||||
|
"description": "일흥옥은 군산 테디베어뮤지엄 인근에 위치한 국밥 전문점이다. 중소벤처기업부로부터 백년가게 인증을 받은 곳이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "군산복집",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "군산복집",
|
||||||
|
"distanceText": "305m",
|
||||||
|
"description": "군산복집은 싱싱한 재료만을 사용하는 복요리 전문점으로 복탕과 아구탕은 해장하기에 좋으며, 오래된 전통과 경력의 주방장이 내는 회 맛이 일품인 곳이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "명월갈비",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "명월갈비",
|
||||||
|
"distanceText": "351m",
|
||||||
|
"imageUrl": "/assets/mirror/881d1f7ff820942f.jpg",
|
||||||
|
"description": "군산 신창동에 있는 명월갈비는 군산 지역의 대표 양념 소갈비 전문 맛집이다. 플러스 등급의 한우 갈비를 비법 양념장으로 숙성한 단품 메뉴로 오랜 기간 동안 인기를 유지해오고 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "만남스넥",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "만남스넥",
|
||||||
|
"distanceText": "405m",
|
||||||
|
"imageUrl": "/assets/mirror/d2be5a053a944cfe.jpg",
|
||||||
|
"description": "만남스넥은 군산 현지인들이 좋아하는 메뉴인 잡탕을 파는 분식집이다. 잡탕은 떡, 만두, 라면, 어묵 등을 넣고 같이 끓인 메뉴인데, 떡볶이나 라볶이보다 국물이 많고 부재료가 풍부해 잡탕으로 불리게…"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "동국사다온",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "동국사다온",
|
||||||
|
"distanceText": "418m",
|
||||||
|
"imageUrl": "/assets/mirror/4db345bd2cb4f0d7.jpg",
|
||||||
|
"description": "전라북도 군산시 일본식 사찰인 동국사 사찰 내에 있는 테이크아웃 커피 전문점이다. 불교 용품과 함께 대추차, 오미자차, 오룡차 등 다양한 차 종류를 판매하고 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "진갈비",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "진갈비",
|
||||||
|
"distanceText": "426m",
|
||||||
|
"description": "소박해보이지만 오랜 세월의 내공으로 묵직하고 깊은 떡갈비를 선사하는 맛집이다. 군산 현지인들이 추천하는 유명 맛집이기도 하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "한주옥",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "한주옥",
|
||||||
|
"distanceText": "430m",
|
||||||
|
"description": "‘한주옥’은 전북 군산시 영화동에 위치한 꽃게장 전문 음식점이다. 꽃게장 백반/정식, 대하장 백반/정식을 맛볼 수 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "이성당",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "이성당",
|
||||||
|
"distanceText": "455m",
|
||||||
|
"imageUrl": "/assets/mirror/37c90494d28478e2.png",
|
||||||
|
"description": "일본 시마네현 이즈모시에 살다가 1906년 조선으로 건너온 히로세 야스타로라는 일본인이 ‘이즈모야’라는 이름으로 문을 열어 영업한 것이 시초이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "국제반점",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "국제반점",
|
||||||
|
"distanceText": "467m",
|
||||||
|
"imageUrl": "/assets/mirror/149c18951bdcaff8.jpg",
|
||||||
|
"description": "군산 영화동에 있는 국제반점은 1960년대 초 진흥반점으로 개업해 현재는 상호를 국제 반점으로 변경하여 성업 중인 중식당이다. 영화 <타짜>에 등장해 군산의 관광 명소가 되었다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "사골뚝배기",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "사골뚝배기",
|
||||||
|
"distanceText": "479m",
|
||||||
|
"description": "사골 뚝배기는 전북 군산시에 위치한 한식 전문점이다. 한우고기와 한우사골만을 써서 아주 진하고 맛깔스러운 국물을 우려내어 요리를 만든다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "영화원",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "영화원",
|
||||||
|
"distanceText": "495m",
|
||||||
|
"imageUrl": "/assets/mirror/42e8d872198edaa9.jpg",
|
||||||
|
"description": "군산 영화동에 있는 영화원은 1976년부터 지금까지 4대째 운영하는 오래된 중국 음식점이다. 대를 이어오면서 그 맛의 비결을 간직하고 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "스위트인디아",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "스위트인디아",
|
||||||
|
"distanceText": "539m",
|
||||||
|
"description": "전라북도 군산시에 위치한 스위트인디아는 인도 카레 맛집으로, 양고기, 소고기, 닭고기, 새우, 야채 총 5가지의 맛의 카레를 판매하고 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "틈(TEUM)",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "틈(TEUM)",
|
||||||
|
"distanceText": "573m",
|
||||||
|
"imageUrl": "/assets/mirror/81bd76fa51ae9ad1.jpg",
|
||||||
|
"description": "군산항 근처에 있는 옛 미곡 창고를 그대로 활용한 카페다. ‘틈’이란 이름처럼 입구가 좁은 골목길 틈에 위치해 있어 간판을 눈여겨보지 않으면 찾기 어렵다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "미즈커피",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "미즈커피",
|
||||||
|
"distanceText": "696m",
|
||||||
|
"imageUrl": "/assets/mirror/3e5b3daae33ea51c.jpg",
|
||||||
|
"description": "군산 근대문화 역사관 바로 옆에 자리한 카페로 일제강점기 무역회사였던 ‘미즈 상사’의 옛 사옥을 그대로 활용했다. 당시 일본인이 운영했던 미즈 상사는 식료품과 잡화 등을 수입해 판매하던 회사였다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "홍영장",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "홍영장",
|
||||||
|
"distanceText": "725m",
|
||||||
|
"description": "홍영장은 군산의 짬뽕거리에 위치한 중식당으로 방송에서 인정받은 맛집이다. 평소에 흔히 맛볼 수 없는 메뉴인 물짜장이 유명하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "아리랑",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "아리랑",
|
||||||
|
"distanceText": "732m",
|
||||||
|
"description": "군산에 위치한 향토음식점으로 군산 지역의 특산물을 맛볼 수 있는 음식점이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "운정식당",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "운정식당",
|
||||||
|
"distanceText": "748m",
|
||||||
|
"imageUrl": "/assets/mirror/00a8ac4ce017fb12.jpg",
|
||||||
|
"description": "군산 개복동 골목의 터줏대감으로 40년 가까이 한 자리를 지키고 있는 식당이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "빈해원",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "빈해원",
|
||||||
|
"distanceText": "749m",
|
||||||
|
"imageUrl": "/assets/mirror/483f52c2d2b7eebe.jpg",
|
||||||
|
"description": "군산 빈해원은 1965년 콘크리트와 벽돌을 사용하여 지은 2층 건물이다. 이 건물은 1~2층이 개방된 내부공간과 각층에 여러 개의 방이 있는 독특한 구조이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "국일식당",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "국일식당",
|
||||||
|
"distanceText": "935m",
|
||||||
|
"description": "전라북도 군산시에 위치한 국일식당은 콩나물의 아삭함과 매콤한 양념이 어우러진 아귀찜 전문점이다. 군산의 전통명가 답게 모범 착한업소로 선정된 곳이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "명동소바",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "명동소바",
|
||||||
|
"distanceText": "961m",
|
||||||
|
"description": "‘군산명동소바’는 30년 이상된 군산 최초 원조 소바집이다. 메밀면발도 직접 연구 개발하고 매장 자체적으로 기계로 면을 빼서 쫄깃하고 탱탱한 맛으로 현지인뿐만 아니라 여행객의 많은 발길이 이어진다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "복성루",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "복성루",
|
||||||
|
"distanceText": "1.3km",
|
||||||
|
"imageUrl": "/assets/mirror/b3518ee8ef2b4aac.png",
|
||||||
|
"description": "복성루는 빈해원 지린성과 함께 군산 3대 짬뽕 맛집으로 유명하며 싱싱한 오징어, 홍합, 꼬막 등 다양한 해산물과 야채를 넣고 우려낸 진한 짬뽕 국물이 일품인 가게로 줄 서서 맛보는 지역의 대표 짬뽕집으로 자리 잡고 있다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "유락식당",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "유락식당",
|
||||||
|
"distanceText": "1.3km",
|
||||||
|
"description": "김정례 대표가 1981년 문을 열어 40여 년 동안 지역을 대표하는 음식점으로 손님에게 최선의 맛과 서비스를 제공하고자 하는 곳이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "왕산반점",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "왕산반점",
|
||||||
|
"distanceText": "1.5km",
|
||||||
|
"imageUrl": "/assets/mirror/aebe437fd281f9ad.jpg",
|
||||||
|
"description": "특허받은 콩나물 짬뽕으로 유명한 중화요리 집이다. 짬뽕면 위에 푸짐하게 올라간 콩나물, 홍합, 그 위에 신선한 낙지 한 마리까지 먹기 전부터 시각을 자극한다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "리투스카페",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "리투스카페",
|
||||||
|
"distanceText": "1.6km",
|
||||||
|
"imageUrl": "/assets/mirror/81438670a5485351.jpg",
|
||||||
|
"description": "리투스카페는 군산 내항 부두 근처에 있는 오션 뷰 카페다. 특히 황금빛 일몰이 아름다워 일몰 카페로도 유명하다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "일풍식당",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "일풍식당",
|
||||||
|
"distanceText": "1.6km",
|
||||||
|
"description": "‘일풍식당’은 각종 TV매체에서 맛집으로 다룬 곳이다. 골목 사이에 위치해 있어 찾을 때 약간 주의를 해야 한다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "수송반점",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "수송반점",
|
||||||
|
"distanceText": "1.7km",
|
||||||
|
"imageUrl": "/assets/mirror/98539e6361e317b1.jpg",
|
||||||
|
"description": "수송 반점은 군산시 서흥남동, 서흥 중학교 근처에 있는 중식당이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "군산활어회센타",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "군산활어회센타",
|
||||||
|
"distanceText": "1.9km",
|
||||||
|
"imageUrl": "/assets/mirror/93146c8ae53252ff.jpg",
|
||||||
|
"description": "군산 문화동에 있는 '군산 활어회센터'는 가까이 있는 군산항 활어회 직판장에서 가져오는 활어회로 신선도와 식감 면에서 뛰어나다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "진성원",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "진성원",
|
||||||
|
"distanceText": "1.9km",
|
||||||
|
"imageUrl": "/assets/mirror/83766fa52745ef37.jpg",
|
||||||
|
"description": "진성원은 전라북도 군산시 경암동에 있는 중식당이다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "서우식당",
|
||||||
|
"category": "맛집",
|
||||||
|
"searchQuery": "서우식당",
|
||||||
|
"distanceText": "2.0km",
|
||||||
|
"imageUrl": "/assets/mirror/de286714af7faaba.jpg",
|
||||||
|
"description": "서우식당은 저렴한 가격에 푸짐한 한상차림으로 허영만의 백반 기행 등 TV 프로그램에도 소개된 한식집이다."
|
||||||
|
}
|
||||||
|
]
|
||||||
1
solution/site/scripts/mockup/vendor/embla-carousel.umd.js
vendored
Normal file
1
solution/site/scripts/mockup/vendor/index-C3fFNl3r.css
vendored
Normal file
340
solution/site/scripts/mockup/vendor/index-DDxwteyn.js
vendored
Normal file
22
solution/site/scripts/mockup/vendor/retired/README.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 물러난 번들 — 지우지 않는다
|
||||||
|
|
||||||
|
여기 있는 파일은 **더 이상 아무 HTML 도 가리키지 않는** 옛 번들이다. 그래도 서버에는 올린다.
|
||||||
|
|
||||||
|
## 왜
|
||||||
|
|
||||||
|
`/s/` 는 5분 캐시(`max-age=300`)다. 새 `index.html` 을 올려도 그동안 손님 브라우저에는
|
||||||
|
**옛 HTML** 이 남아 있고, 그 HTML 은 옛 해시 이름으로 css·js 를 부른다.
|
||||||
|
그 파일을 서버에서 지우면 **그 손님에게는 스타일도 스크립트도 404 다** — 화면은 뜨는데
|
||||||
|
헤더가 안 붙고 푸터 색이 빠진 민짜 문서가 된다.
|
||||||
|
|
||||||
|
실측(2026-09-14): 번들을 갈면서 옛 두 개를 `docker exec rm` 으로 지웠더니 바로 그 증상이
|
||||||
|
났다. "헤더가 스티키가 아니고 푸터가 투명해졌다" — 파일이 사라진 것이지 CSS 가 바뀐 게 아니다.
|
||||||
|
제품 쪽 프리렌더가 옛 자산을 30일 보관하는 것도 같은 이유다(`prerender.ts ASSET_RETENTION_DAYS`).
|
||||||
|
|
||||||
|
## 규칙
|
||||||
|
|
||||||
|
- 번들을 갈면 옛 파일을 **지우지 말고 이리로 옮긴다.** `vendor/` 뿌리에는 현재 한 벌만 둔다
|
||||||
|
(`patch_stay.py` 가 뿌리에 js·css 각 하나임을 확인한다).
|
||||||
|
- 배포할 때 뿌리와 이 폴더를 **둘 다** 서버 `vendor/` 에 넣는다:
|
||||||
|
`docker cp vendor/retired/. $C:/app/solution/site/out/s/stay/vendor/`
|
||||||
|
- 30일쯤 지나 캐시가 다 빠지면 지워도 된다. 급할 것 없다 — 합쳐서 450KB 다.
|
||||||