[feat] site/mockup: /s/stay 에 엽서 쓰기 · 새 군산 읽기를 넣는다 — 탭 전환 버그 수정
'오늘의 엽서'는 미리 써 둔 문구만 보여줬다 — 손님이 사진 고르고 직접 한마디를 써서 카드를 만들고 저장·공유까지 하게 바꿨다. 군산 읽기는 xlsx 원본 대비 2줄짜리 항목이 많아 부실했고(gunsan_365_story_db.xlsx 365행은 52개 주제를 10가지 각도로 복제한 것 — 그대로 실으면 같은 말이 반복된다), 3~5줄로 다시 쓰고 확인필요 배지 대신 네이버 검색 링크로 바꿨다. 그 과정에서 군산 읽기 탭에서 다른 탭(시간의 골목)을 누르면 화면이 사라지는 버그를 잡았다 — React 가 no-op setState 를 건너뛰면서 직접 건드린 DOM 속성(hidden·aria-selected)이 안 돌아오던 것. - inject.js: 엽서 쓰기 섹션(사진 선택 → 캔버스 카드 → 저장/공유), 4줄 캡+글자 단위 줄바꿈, 공유는 이미지+링크만(캡션 중복 안 보냄) · 군산 읽기 탭 5~6개 무작위 노출 · 탭 전환 시 React 미관리 속성(style.display)으로 전환하고 클릭된 탭에 상태를 직접 강제해 no-op setState 버그 제거 - inject.css: 엽서 쓰기 버튼·썸네일 카로셀 스타일, 군산 읽기 확인필요 배지 제거, 768~1023px 구간에서 하단 탭바가 푸터 마지막 줄을 덮는 것 막는 보정 (SiteFooter.tsx 는 md: 768px 부터, MobileTabBar.tsx 는 lg: 1024px 부터 기준을 잡아 그 사이가 비었다 — 렌더러 쪽 근본 수정은 별도) - build_reading.py: 34꼭지 재작성(2줄짜리 21개를 3~5줄로), 출처를 네이버 검색 링크로 통일, verified 필드 제거 - build_daily.py · dump_text.py: 위에서 뺀 verified 필드를 읽던 자리 방어 처리 - README.md: 1부(T7 군산 읽기 · T8 엽서 쓰기)를 실제 구현 가이드로 다시 씀 — 히스토리·인용 없이 무엇을 만들어야 하는지만, 엽서는 AI/스키마 작업이 아니라 UI 그대로 옮기면 되는 것임을 명시 검증: Playwright 로 탭 재전환·엽서 줄바꿈 재현 확인. 킹서버 반영 후 `docker exec o2o-web4ai-solution-site md5sum` 로컬-킹 일치 확인. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYUb8ZfNiDd15tZRYrusCX
This commit is contained in:
parent
b2c8bb033e
commit
0500ab3a3f
@ -59,7 +59,7 @@
|
||||
| 파일 | 옮길 자리 |
|
||||
|---|---|
|
||||
| 이 문서 **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` |
|
||||
| `inject.js` (렌더러에 **없는** 기능 3개) | `site/src/sections/` |
|
||||
|
||||
@ -81,25 +81,26 @@
|
||||
| `photos` | 사진 갤러리 | `media[]` | 사장님이 올림 | |
|
||||
| `map` | 오시는 길 | `place` + `routes[]` | 수집 | |
|
||||
| `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** |
|
||||
| `story` | 군산 이야기 | 아래 6탭 껍데기 | — | |
|
||||
| `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** |
|
||||
| `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` | |
|
||||
| `faq` | 자주 묻는 질문 | `faqs[]` | LLM + 사장님 | |
|
||||
| `rules` | 이용 규정 | `facts[]` | 사장님이 적음 | |
|
||||
| `weather` | 날씨 | `local.weather` | open-meteo | |
|
||||
| `planner` | 계절별 추천 하루 | `data(kind=planner)` | **없음** | (시연본은 꺼 둠) |
|
||||
|
||||
섹션이 아닌 것 하나가 더 있다 — 헤더 음악 플레이어(**T4**).
|
||||
섹션이 아닌 것이 셋 더 있다 — 헤더 음악 플레이어(**T4**) · **군산 읽기**(**T7**, '군산 이야기'
|
||||
탭 묶음의 여섯 번째 탭) · **엽서 쓰기**(독립 섹션, `festival` 바로 앞).
|
||||
|
||||
---
|
||||
|
||||
## 1.3 할 일 여섯
|
||||
## 1.3 할 일 여덟
|
||||
|
||||
### T1 — 히어로 캐치프레이즈
|
||||
|
||||
@ -118,7 +119,10 @@
|
||||
weather?: '맑음'|'구름많음'|'흐림'|'비'|'눈';
|
||||
}[] };
|
||||
```
|
||||
2. 히어로가 **지금 조건에 맞는 것만 후보**로 두고 돌린다.
|
||||
2. **대표 문구(`tagline`)는 고정하고, 순환은 그 아래 줄에서 돈다** (2026-09-11 사장님 지시).
|
||||
대표 문구 자리를 갈아 끼우면 숙소의 한 줄이 첫 화면에서 사라진다. 아래 줄은 대표 문구보다
|
||||
한 단 작고 옅게, 높이는 가장 긴 문구에 맞춰 잡는다(히어로가 아래 정렬이라 줄 수가 바뀌면 튄다).
|
||||
히어로가 **지금 조건에 맞는 것만 후보**로 두고 돌린다.
|
||||
순환 순서는 `일반 → 계절 → 일반 → 월 → 일반 → 날씨` 한 바퀴를 만들고 그 바퀴를 돈다.
|
||||
일반은 자루에서 뽑아 쓰고 비면 다시 채운다(같은 문장이 연달아 안 나오게).
|
||||
계절은 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 ②** |
|
||||
| **② 좌표** | 코드 | 카카오 로컬로 `searchQuery` → 위경도. **LLM 이 적은 좌표는 쓰지 않는다** | — |
|
||||
| **③ 조립** | **코드** | 이동시간·도착시각·끼니 창 맞추기·규칙 16종 감사 | 없음 (`build_itinerary.py`) |
|
||||
| **③ 조립** | **코드** | 이동시간·도착시각·끼니 창 맞추기·규칙 17종 감사 | 없음 (`build_itinerary.py`) |
|
||||
| **④ 테마** | LLM | 테마 20개 × `{name, duration, audience, why, days[{kind, places[]}]}` | **1.4 ③** |
|
||||
|
||||
**코드가 하는 일(③)에서 반드시 옮겨야 하는 것** — 근거는 `build_itinerary.py` 줄번호
|
||||
@ -174,7 +178,7 @@
|
||||
| 창 맞추기 | 어긋나면 **앞 칸 체류를 범위(min~max) 안에서** 늘이고 줄인다. 이동시간을 고치지 않는다. 그래도 안 되면 시작 시각을 10분씩 옮기고, 그래도 안 되면 **그 조합을 만들지 않는다** | `plan_day()` |
|
||||
| 테마별 완급 | 시작 시각·체류 배율이 테마마다 다르다(쉬러 오면 1.25배, 사진 찍으면 0.8배) → 끝나는 시각이 13:35~21:00 로 퍼진다 | `:470` |
|
||||
| 끼니 간격 | 앞 끼니가 끝나고 **3시간**이 지나야 다음 끼니 창이 열린다(`MEAL_GAP`). 창만 지키면 통과하던 자리다 | `_window_of()` |
|
||||
| **감사 16종** | 하나라도 어기면 **payload 를 쓰지 않고 멈춘다** | `:526` · 목록은 **2.4** |
|
||||
| **감사 17종** | 하나라도 어기면 **payload 를 쓰지 않고 멈춘다** | `:575` · 목록은 **2.4** |
|
||||
|
||||
★ **감사는 생성기 안에 둔다.** 밖에 두면 뼈대를 고칠 때마다 "이번에 안 본 규칙"이 생긴다 —
|
||||
실제로 그래서 33건이 통과했다(**3.2**).
|
||||
@ -182,12 +186,18 @@
|
||||
**하루 뼈대** — `@` 는 숙소
|
||||
|
||||
```
|
||||
첫날 체크인@ · 오후 · 오후 · 저녁 · 밤산책 · 복귀@
|
||||
가운데날 아침@ · 오전 · 점심 · 오후 · 오후 · 쉼@ · 저녁 · 복귀@
|
||||
쉬는날 숙소에서 쉬는 날@ · 점심 · 쉼@ · 저녁 · 복귀@
|
||||
마지막날 아침(퇴실)@ · 오전 · 점심 · 오후 · 마무리@
|
||||
첫날 체크인@ · 오후 · 오후 · 저녁 · 밤산책 · 복귀@
|
||||
가운데날 아침@ · 오전 · 점심 · 오후 · 오후 · 쉼@ · 저녁 · 복귀@
|
||||
쉬는날 숙소에서 쉬는 날@ · 점심 · 쉼@ · 저녁 · 복귀@
|
||||
마지막날(도보) 아침(퇴실)@ · 오전 · 점심 · 오후 · 마무리@ ← 짐을 맡기고 저녁에 찾으러 온다
|
||||
마지막날(차) 아침(퇴실·차)@ · 오전 · 점심 · 오후 ← 짐을 싣고 나섰다, 복귀 칸이 없다
|
||||
```
|
||||
|
||||
★ **떠나는 날은 둘로 갈린다** (2026-09-14 대표 의견). 차로 온 손님에게 "짐을 맡기고 저녁에
|
||||
찾으러 오세요" 는 **한 번 더 들르라는 말**이다 — 퇴실할 때 실으면 끝날 일이다. 그래서 차 테마의
|
||||
마지막 날은 마지막 정거장에서 끝난다. 짐 보관 동선은 걸어 다니는 손님 것이고 그 테마만 남는다.
|
||||
뼈대를 두 벌로 적지 않고 **복귀 칸만 떼어** 만든다(`build_itinerary.py` `CAR_KINDS`).
|
||||
|
||||
**완료 기준** 새 업장에서 테마 20개가 나오고 `audit()` 위반 0건.
|
||||
하루가 숙소로 시작해 숙소로 끝나고, 끼니가 제 시각에 있고, 21시를 안 넘는다.
|
||||
|
||||
@ -220,9 +230,18 @@
|
||||
|
||||
**할 일** `MediaItem` 에 `kind:'audio'`(+`title` `artist` `durationSec`)를 넣고 발행 때 같이 올린다.
|
||||
헤더에 붙는 작은 플레이어를 만든다 — 아이콘 · 현재 곡명 · 재생/멈춤 · 목록.
|
||||
**자동재생은 넣지 않는다**(2026-09-11 지시). 순서는 **랜덤 시작 후 순차**.
|
||||
순서는 **랜덤 시작 후 순차**.
|
||||
목업 구현은 `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`)
|
||||
@ -254,6 +273,68 @@
|
||||
|
||||
---
|
||||
|
||||
### T7 — 도시를 펼쳐 보이는 자리
|
||||
|
||||
**증상** 도시 이야기를 담는 자리가 전부 한 번에 하나만 보인다. 하루에 한 장만 보여주는
|
||||
일력이나, 넉 장짜리 엽서 레일 정도로는 군산이라는 도시를 소개하기엔 부족하다.
|
||||
|
||||
**할 일** 문학·역사·섬과 바다·장소·음식처럼 갈래로 나눈 도시 이야기를, 한 화면에서
|
||||
옆으로 넘겨 가며 볼 수 있는 자리를 만든다.
|
||||
|
||||
1. 이 콘텐츠는 **사장님마다 새로 만드는 게 아니다.** 군산이라는 지역이 공유하는 고정
|
||||
자료다(계절별 축제·주변 명소와 같은 성격) — 사이트를 만들 때마다 AI 가 새로 글을
|
||||
지어내는 게 아니라, 군산에 있는 모든 사이트가 같은 이야기 목록을 나눠 쓴다.
|
||||
2. 항목마다 출처 링크를 단다. 정확한 문서 주소를 일일이 확인하기 어려우니, 제목으로
|
||||
검색되는 링크(예: 네이버 검색)를 걸어 둔다 — 없는 주소를 지어내 걸지 않는다.
|
||||
3. 옆으로 넘겨 보는 방식은 **이 사이트가 이미 쓰고 있는 것**을 그대로 쓴다(사진 갤러리·
|
||||
추천 일정에 쓰는 그 슬라이드). 새로 스크롤 상자를 만들지 않는다. 한 슬라이드에는
|
||||
이야기 하나만 크게 보여준다 — 여러 개를 한 슬라이드에 욱여넣지 않는다.
|
||||
4. 한 번 볼 때 전체 이야기를 다 보여주지 않는다. 그중 몇 개만 무작위로 뽑아 보여주고,
|
||||
다시 오거나 새로고침하면 다른 조합이 뜬다. 화면 문구에 "전체 몇 개"라고 숫자를
|
||||
박지 않는다 — 실제로 보이는 개수와 어긋나면 안 되기 때문이다.
|
||||
5. 화면 안에서는 '군산 이야기'(가요 다방·인물 열전 등이 모여 있는 탭 묶음)의 탭
|
||||
하나로 들어간다 — 독립된 섹션으로 따로 빼지 않는다. 다른 탭들처럼 탭 안에도
|
||||
제목을 그대로 보여준다.
|
||||
6. 이미 다른 자리에 나온 이야기는 다시 넣지 않는다 — 인물·가요·축제·연표·명소·맛집이
|
||||
각자 자리를 갖고 있다. 같은 이야기를 두 번 보여주면 페이지만 길어진다.
|
||||
|
||||
**완료 기준** 갈래를 가리지 않고 훑어볼 수 있고, 이야기마다 출처가 붙어 있고, 넘기는
|
||||
방식이 이 사이트에서 이미 쓰는 방식과 같다.
|
||||
|
||||
---
|
||||
|
||||
### T8 — 엽서 쓰기
|
||||
|
||||
**증상** '오늘의 엽서'는 미리 써 둔 문구를 보여주기만 했다. 손님이 직접 한마디를 써서
|
||||
자기만의 엽서를 만들고, 그걸 저장하거나 공유할 수 있게 바꾼다.
|
||||
|
||||
**이건 AI 가 글을 짓는 기능이 아니다.** 새 데이터 형식이나 문구를 설계할 필요 없이,
|
||||
이미 만들어 확인까지 끝낸 화면 기능 하나를 그대로 옮겨 붙이면 되는 일이다.
|
||||
|
||||
1. 숙소 사진(가진 사진 전체를 보여주고 손님이 그중 하나를 고르게 한다) + 손님이
|
||||
직접 쓴 한마디 + 우표·소인 모양을 합쳐서 한 장의 카드 이미지로 만든다.
|
||||
2. 문구는 최대 네 줄까지만 쓸 수 있게 막는다. 줄이 넘어가면 자동으로 다음 줄로
|
||||
넘어가는데, 띄어쓰기 없이 길게 이어 쓴 글자도 카드 밖으로 삐져나오지 않고
|
||||
글자 단위로 잘 접혀야 한다.
|
||||
3. 완성한 카드는 사진으로 저장하거나 공유할 수 있다. 공유할 때는 카드 이미지와
|
||||
함께, 지금 보고 있는 이 사이트 페이지로 가는 링크를 같이 보낸다. 손님이 쓴
|
||||
문구는 이미 카드 그림 안에 박혀 있으니, 공유 문구 자리에 그 글을 따로 한 번 더
|
||||
적지 않는다 — 같은 말이 두 번 보이면 안 된다. 서버에 새로 올리는 기능을 만들
|
||||
필요 없이 휴대폰·브라우저가 원래 갖고 있는 공유 기능을 그대로 쓰고, 그래서 이
|
||||
기능은 서버를 한 번도 부르지 않는다.
|
||||
4. 공유 버튼이나 안내 문구에 특정 메신저 이름을 못 박지 않는다 — 기기에 따라
|
||||
실제로 뜨는 공유 대상이 다르다(휴대폰은 설치된 메신저가 뜨지만, 컴퓨터 특히
|
||||
맥은 카카오톡이 그 목록에 안 뜬다). 버튼 문구는 손끝으로 조작하는 기기에서만
|
||||
"카카오톡 등으로 공유"라고 쓰고, 그 외에는 그냥 "공유하기"라고 쓴다.
|
||||
5. 상호명을 코드 안에 고정으로 박아 넣지 않는다 — 이 기능은 다른 숙소나 카페에서도
|
||||
그대로 가져다 써야 한다.
|
||||
6. 화면 위치는 "계절별 축제" 섹션 바로 앞에 독립된 섹션으로 둔다.
|
||||
|
||||
**완료 기준** 손님이 사진 고르고 글 쓰고 저장·공유하는 것까지 한 화면에서 끝난다.
|
||||
새 서버 기능이나 AI 프롬프트가 필요 없다.
|
||||
|
||||
---
|
||||
|
||||
## 1.4 프롬프트 전문
|
||||
|
||||
**프롬프트는 지금 두 파일에 나뉘어 있다. 이게 문제다.**
|
||||
@ -307,7 +388,7 @@ backend/services/prompts/section_prompts.json ← 산출물. 손으로 고치
|
||||
창고형 카페 공간 · 침구 매일 세탁 · 히로쓰 가옥 옆 · 초원사진관 도보 3분}
|
||||
|
||||
[해야 할 일]
|
||||
[업소] 의 히어로 문구를 100개 만든다. 상호 바로 아래에 한 줄로 뜨고 7초마다 바뀐다.
|
||||
[업소] 의 히어로 문구를 100개 만든다. 상호와 대표 문구 아래에 한 줄로 뜨고 7초마다 바뀐다.
|
||||
네 종류로 나눠 만든다.
|
||||
|
||||
· 일반 40개 — 언제 나와도 되는 문구
|
||||
@ -552,9 +633,11 @@ backend/services/prompts/section_prompts.json ← 산출물. 손으로 고치
|
||||
stay-payload.json 원본 payload (건드리지 않는다 · 되돌릴 때 기준)
|
||||
orig/index.html 원본 HTML (건드리지 않는다)
|
||||
│
|
||||
├─ build_itinerary.py 여행 일정을 계산해 넣는다 (+ 규칙 16종 감사)
|
||||
├─ build_itinerary.py 여행 일정을 계산해 넣는다 (+ 규칙 17종 감사)
|
||||
├─ 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 이그노어)
|
||||
@ -599,19 +682,27 @@ node audit-all.mjs
|
||||
### 내용을 바꾼 뒤 (전체 굽기)
|
||||
|
||||
```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`(원본)을 읽어
|
||||
`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
|
||||
docker cp build/index.html o2o-web4ai-solution-prerender:/app/solution/site/out/s/stay/index.html
|
||||
node audit-all.mjs # 34개 항목
|
||||
node rails-test.mjs # 카로셀 13개
|
||||
node audit-all.mjs # 47개 항목
|
||||
node rails-test.mjs # 카로셀 13개 (모바일은 +3)
|
||||
python3 audit_schedule.py # 일정 전수 검수표(SCHEDULE.md)
|
||||
```
|
||||
|
||||
`/s/` 는 **5분 캐시**(`max-age=300`)다 — 확인할 때 ⌘⇧R.
|
||||
@ -645,14 +736,32 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
|
||||
| 자리 | 줄 | 무엇 |
|
||||
|---|---|---|
|
||||
| `GENERAL` | `:20` | 일반 40개 |
|
||||
| `SEASON` | `:62` | 계절별 24개 — 봄/여름/가을/겨울 각 6 |
|
||||
| `MONTH` | `:72` | 월별 24개 — 1~12월 각 2 |
|
||||
| `WEATHER` | `:87` | 날씨별 12개 — 맑음/구름많음/흐림/비/눈 |
|
||||
| `GENERAL` | `:29` | 일반 40개 |
|
||||
| `SEASON` | `:71` | 계절별 24개 — 봄/여름/가을/겨울 각 6 |
|
||||
| `MONTH` | `:81` | 월별 24개 — 1~12월 각 2 |
|
||||
| `WEATHER` | `:96` | 날씨별 12개 — 맑음/구름많음/흐림/비/눈 |
|
||||
|
||||
문장만 더하거나 지우면 된다. 개수 제한은 없다.
|
||||
순환 순서·계절 구분·날씨 연동은 **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`
|
||||
|
||||
**사람은 "어느 자리에 어느 장소" 만 고르고, 시각은 스크립트가 계산한다.**
|
||||
@ -660,17 +769,55 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
| 자리 | 줄 | 무엇 |
|
||||
|---|---|---|
|
||||
| `PLACES` | `:40` | **장소 대장** — 좌표 · 지도 검색어 · 체류시간(min/base/max) · **거기서 무엇을 하는지** |
|
||||
| `STAY_ACTION` | `:108` | 머뭄 칸의 행위 — 체크인 · 아침 · 아침(퇴실) · 쉼 · 쉬는 날 · 머뭄으로 · 마무리 |
|
||||
| `WINDOW` | `:156` | 시간대가 정해진 자리 |
|
||||
| `SKELETON` | `:182` | 하루 뼈대. `@` 가 붙은 것은 머뭄 |
|
||||
| `THEMES` | `:368` | 테마 21개 |
|
||||
| `PACING` | `:470` | 테마별 시작 시각과 체류 배율 |
|
||||
| `STAY_ACTION` | `:108` | 머뭄 칸의 행위 — 체크인 · 아침 · 아침(퇴실) · **아침(퇴실·차)** · 쉼 · 쉬는 날 · 머뭄으로 · 마무리 |
|
||||
| `WINDOW` | `:162` | 시간대가 정해진 자리 |
|
||||
| `SKELETON` | `:194` | 하루 뼈대. `@` 가 붙은 것은 머뭄. **떠나는 날은 차·도보 두 벌**(`CAR_KINDS` 가 복귀 칸을 떼어 만든다) |
|
||||
| `THEMES` | `:414` | 테마 21개. 마지막 날 뼈대가 `last_car` 계열이면 **차로 떠나는 테마**다(지금 14개 — 1박 2일 7 · 2박 3일 7) |
|
||||
| `PACING` | `:516` | 테마별 시작 시각과 체류 배율 |
|
||||
|
||||
**새 장소** → `PLACES` 에 한 줄. 좌표는 카카오 로컬에서 받은 실제 값이어야 한다(지어내지 않는다).
|
||||
그다음 `THEMES` 의 원하는 자리에 이름을 적는다.
|
||||
**새 테마** → `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`
|
||||
|
||||
| 자리 | 줄 | 무엇 |
|
||||
@ -685,13 +832,13 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
- ★ 이름만으로 커먼즈를 검색하지 않는다(**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 파일을 쓰지 않고** 위반 목록을 찍고 멈춘다.
|
||||
|
||||
| | 규칙 | 이 규칙이 없어서 났던 일 |
|
||||
@ -712,24 +859,91 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
| 14 | 아이 동반 테마에 술집 금지 | |
|
||||
| **15** | **끼니 사이 3시간 이상** | 늦은 점심 15:47 종료 → 저녁 17:30. 창은 둘 다 합법인데 배가 안 꺼진다 |
|
||||
| **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. **캐치프레이즈 순환** — React 가 그린 `<p>` 를 찾아 단어 단위로 갈아 끼운다 → T1
|
||||
2. **헤더 미니 플레이어** — 카세트 아이콘 + 재생/멈춤 + 목록. 자작곡 5곡 → T4
|
||||
3. **링크 걸기** — 일정 정거장 187개 · 엽서 4개를 네이버 지도로
|
||||
4. **사진 저작자 표시** — 이 목업이 물고 있는 번들이 옛 판이라 `imageCredit` 을 안 그린다 → T3
|
||||
5. **카로셀 제어** — 아래
|
||||
1. **캐치프레이즈 순환** — 대표 문구 `<p>` 를 찾아 **그 아래에 줄(`#w4d-sub`)을 붙이고** 단어 단위로 갈아 끼운다. 대표 문구는 안 건드린다 → T1
|
||||
2. **헤더 미니 플레이어** — 카세트 아이콘 + 재생/멈춤 + 목록. 자작곡 5곡.
|
||||
**자동재생**은 걸어 보고, 브라우저가 막으면 첫 손길에 켠다(**1.3 T4**) → T4
|
||||
3. **군산 읽기** — 렌더러에 없는 섹션. '군산 이야기' 탭 묶음의 **여섯 번째 탭**으로 들어가
|
||||
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`)
|
||||
|
||||
@ -765,7 +979,7 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
★ **React 가 다시 그리면 사라진다.** 그래서
|
||||
- 플레이어 목록·패널은 `#root` **밖**(body 직속)에 둔다
|
||||
- 헤더에 꽂는 단추는 `MutationObserver` 로 감시해 다시 꽂는다
|
||||
- 캐치프레이즈는 노드를 들고 있지 않고 **매 순환마다 다시 찾는다**
|
||||
- 캐치프레이즈 줄은 노드를 들고 있지 않고 **매 순환마다 붙어 있는지 보고, 없으면 대표 문구를 다시 찾아 새로 붙인다**
|
||||
|
||||
★ **`window.load` 를 기다리지 않는다.** 경로 지도 때문에 OSM 타일 `<img>` 가 140장이라
|
||||
`load` 는 그게 다 끝나야 나온다 — 타일이 느리면 영원히 안 뜬다.
|
||||
@ -787,6 +1001,15 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
바꾸는 것이라, SSR 마크업과 `llms.txt` 는 구운 날 그대로다. 시연에는 문제없지만
|
||||
검색·AI 에 인용되는 값은 아니다.
|
||||
- **`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. **실행하지 않은 것을 됐다고 쓰지 않는다.** 코드에서 빠진 것을 보는 건 확인이 아니다.
|
||||
화면을 열어 세거나 명령을 돌려서 **숫자·출력**을 얻은 뒤에 말한다.
|
||||
2. **"했다" 옆에 무엇으로 확인했는지 같이 쓴다.** `audit-all.mjs 34/34` · `md5 일치` ·
|
||||
2. **"했다" 옆에 무엇으로 확인했는지 같이 쓴다.** `audit-all.mjs 38/38` · `md5 일치` ·
|
||||
`20초 동안 0px` 처럼. 근거를 못 쓰면 아직 안 끝난 것이다.
|
||||
3. **절차를 안내할 때는 그 절차를 먼저 밟아 본다.**
|
||||
4. **일부만 했으면 일부만 했다고 쓴다.** 못 한 것을 빼고 요약하지 않는다.
|
||||
@ -861,15 +1084,19 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
|---|---|
|
||||
| `README.md` | **이 문서.** 지침은 전부 여기 |
|
||||
| `TEXT.md` | 화면에 나가는 텍스트 전문 (생성물 — `python3 dump_text.py`) |
|
||||
| `build_itinerary.py` | 여행 일정 계산 + 규칙 16종 감사 |
|
||||
| `build_itinerary.py` | 여행 일정 계산 + 규칙 17종 감사 |
|
||||
| `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` 생성 |
|
||||
| `inject.css` · `inject.js` | 화면에 덧대는 다섯 가지 |
|
||||
| `audit-all.mjs` | 브라우저로 34개 항목 전수 점검 |
|
||||
| `rails-test.mjs` | 카로셀 13개 × 데스크톱·모바일 |
|
||||
| `audit-all.mjs` | 브라우저로 47개 항목 전수 점검 |
|
||||
| `rails-test.mjs` | 카로셀 13개 × 데스크톱·모바일. 모바일에는 이름 없는 가로 상자 3개가 더 잡힌다 — 그중 하나가 가요 다방 판꽂이다(자동으로 안 넘어가므로 전 칸 0 이 맞다) |
|
||||
| `kingcheck.mjs` | 킹서버 반영분 확인 |
|
||||
| `audit_schedule.py` | 일정 전수 검수표 생성 → `SCHEDULE.md` (규칙이 안 보는 논리 12가지) |
|
||||
| `audit_schedule.py` | 일정 전수 검수표 생성 → `SCHEDULE.md` (규칙이 안 보는 논리 13가지) |
|
||||
| `SCHEDULE.md` | 그 결과물 (생성물) |
|
||||
|
||||
### 원본 · 원자료 (건드리지 않는다)
|
||||
@ -878,6 +1105,8 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
|---|---|
|
||||
| `stay-payload.json` · `orig/index.html` | **원본.** 되돌릴 때 기준 |
|
||||
| `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-raw.json` · `people-rows.json` | 인물 수집 원자료 |
|
||||
| `coords.json` | 카카오 로컬에서 받은 좌표 |
|
||||
@ -890,7 +1119,7 @@ docker cp vendor/. \$C:/app/solution/site/out/s/stay/vendor/"
|
||||
| `img/people/` | 17장 | 위키 사진. 저작자 표시가 라이선스 조건이라 같이 관리한다 |
|
||||
| `img/mirror/` | 61장 | 히어로·소개·객실. 없으면 **다른 기계에서 흰 화면** |
|
||||
| `audio/` | 5곡 | 사장님 자작곡 |
|
||||
| `vendor/` | 2개 | 번들 css/js. 해시가 달라도 뜨게 하려고 |
|
||||
| `vendor/` | 3개 | 번들 css/js(해시가 달라도 뜨게 하려고) + `embla-carousel.umd.js`(군산 읽기·가요 다방 카로셀이 쓰는 코어, `node_modules/embla-carousel` 그대로 복사) |
|
||||
|
||||
합쳐서 약 77M 이다. 전부 **한 번 넣고 안 바뀌는 파일**이라 git 이 싫어하는 경우가 아니다.
|
||||
자산은 커밋을 따로 떼어 뒀다 — 히스토리에서 덩치를 걷어낼 일이 생기면 그 커밋만 건드린다.
|
||||
@ -935,4 +1164,7 @@ ssh King_admin "rm -rf ~/data2/o2o-site-AEO/tmp-stay-deploy"
|
||||
| 인물 사진 18/57 | 위키 문서에 사진이 있는 사람이 18명뿐이다. 동명이인 함정 때문에 이름 검색으로 늘리지 않는다 |
|
||||
| 렌더러의 카로셀 자동 넘김 | 운영 사이트에 그대로 있다. 고치려면 `use-rail-autoplay.ts` 를 고치고 **전체 재굽기 + `republish_all.py`** |
|
||||
| ~~일정 정거장의 체류 대비 이동~~ | **해결(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 에 안 남는다 |
|
||||
|
||||
@ -218,7 +218,7 @@
|
||||
| 56 | | 전미라 | 1978– | 테니스 선수 | 테니스 선수로 뛰었고 지금은 해설위원입니다. |
|
||||
| 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분 |
|
||||
| 빈해원 | 2층 홀에 앉아 짬뽕이나 잡채밥을 시킵니다. 건물 자체가 문화재입니다. | 70분 |
|
||||
| 스테이 머뭄 | 돌아와 마당 평상에 앉습니다. 항구 소리가 여기까지 옵니다. | 30분 |
|
||||
| 스테이 머뭄 | 맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다. | 40분 |
|
||||
| 스테이 머뭄 | 맡겨 둔 짐을 찾고, 마당 평상에 한 번 더 앉았다 나섭니다. | 42분 |
|
||||
| 스테이 머뭄 | 부엌에서 아침을 차려 먹고 마당에서 커피를 내려 마신 뒤 나섭니다. | 60분 |
|
||||
| 스테이 머뭄 | 숙소로 돌아와 마당 평상이나 창고형 카페 공간에서 쉽니다. | 173분 |
|
||||
| 스테이 머뭄 | 아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요. | 75분 |
|
||||
| 스테이 머뭄 | 아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 맡겨 두고 나섰다가 저녁에 찾아 가세요. | 79분 |
|
||||
| 스테이 머뭄 | 아침을 차려 먹고 11시 전에 퇴실합니다. 짐은 차에 싣고 그대로 나섭니다. | 75분 |
|
||||
| 스테이 머뭄 | 오늘은 나가지 않습니다. 마당 평상과 창고형 카페 공간에서 책을 읽고 낮잠을 잡니다. | 223분 |
|
||||
| 스테이 머뭄 | 짐을 풀고 마당과 창고형 카페 공간을 한 바퀴 돕니다. | 45분 |
|
||||
| 우체통거리 | 주민들이 손질해 내놓은 우체통을 따라 걷습니다. 엽서를 부치고 가도 됩니다. | 26분 |
|
||||
@ -285,17 +286,200 @@
|
||||
| 소설 『탁류』를 따라 걷는 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`
|
||||
- CHECK IN · 머뭄 — `/s/stay/audio/02-check-in.mp3`
|
||||
- Stay in 머뭄 — `/s/stay/audio/03-stay-in.mp3`
|
||||
- 오늘은 안 갈란다 — `/s/stay/audio/04-not-going.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`)
|
||||
|
||||
**혹서**
|
||||
- 낮에는 군산근대미술관·근대건축관처럼 실내로 도는 편이 낫습니다. 월명호수 한 바퀴는 해가 넘어간 뒤가 시원합니다.
|
||||
- 한낮 골목에는 그늘이 없습니다. 초원사진관·영화의 거리는 이른 아침이나 해 질 무렵에 도세요.
|
||||
- 옛 군산세관과 근대건축관은 걸어서 이어집니다. 실내를 징검다리 삼아 도시면 덜 지칩니다.
|
||||
- 경암동 철길마을은 그늘이 적습니다. 오늘은 저녁에 가시는 편이 낫습니다.
|
||||
- 물은 나가시기 전에 챙기세요. 이성당까지는 걸어서 오백 미터, 해가 기운 뒤가 낫습니다.
|
||||
|
||||
**더움**
|
||||
- 선유도까지 다녀오기 좋은 기온입니다. 골목은 오후 늦게 도세요 — 경암동 철길마을은 해 질 때가 사람이 적습니다.
|
||||
- 낮에는 월명호수 그늘길이 시원합니다. 물가를 따라 걷다 돌아오시면 됩니다.
|
||||
- 우체통거리와 영화의 거리는 그늘이 이어집니다. 걸어서 도시기에 무리가 없습니다.
|
||||
- 해가 길어 저녁이 넉넉합니다. 내항 쪽으로 나가 노을을 보고 오셔도 됩니다.
|
||||
- 가장 더운 시간만 실내가 낫습니다. 군산근대미술관에서 한 차례 쉬어 가세요.
|
||||
|
||||
**선선**
|
||||
- 걷기에 가장 좋은 날입니다. 히로쓰 가옥에서 초원사진관·영화의 거리까지 걸어서 그대로 이어집니다.
|
||||
- 원도심은 오늘 하루면 다 걸립니다. 군산 시간여행마을까지 천천히 다녀오셔도 됩니다.
|
||||
- 차를 두고 걷는 편이 낫습니다. 우체통거리에서 옛 군산세관까지 골목이 계속 이어집니다.
|
||||
- 해가 부담스럽지 않습니다. 경암동 철길마을까지 다녀오기에 알맞은 기온입니다.
|
||||
- 낮에는 월명호수를 한 바퀴 돌고, 밤에는 마당에 앉아 계셔도 좋습니다.
|
||||
|
||||
**쌀쌀**
|
||||
- 월명호수를 한 바퀴 돌고 이성당이나 국제반점에서 따뜻한 것을 드시면 알맞습니다.
|
||||
- 겉옷을 하나 더 챙기세요. 해가 지면 항구 쪽 바람이 먼저 찹니다.
|
||||
- 실내와 골목을 섞어 도는 편이 낫습니다. 근대건축관에서 몸을 녹이고 다시 나오시면 됩니다.
|
||||
- 따뜻한 국물이 도는 날입니다. 복성루·빈해원처럼 가까운 집부터 보세요.
|
||||
- 오후 볕이 짧습니다. 초원사진관·영화의 거리를 먼저 돌고 저녁에 들어오세요.
|
||||
|
||||
**추움**
|
||||
- 밖은 바람이 매섭습니다. 근대건축관·근대미술관과 시간여행마을 실내를 묶어 도는 편이 낫습니다.
|
||||
- 항구 바람이 매운 날입니다. 골목은 짧게 돌고 실내를 길게 잡으세요.
|
||||
- 장갑과 목도리를 챙기세요. 옛 군산세관 앞은 바람이 그대로 지나갑니다.
|
||||
- 따뜻한 것을 먼저 드시는 편이 낫습니다. 이성당·국제반점은 걸어서 오백 미터입니다.
|
||||
- 일찍 들어오셔도 됩니다. 벽난로가 있는 거실이 이런 날 가장 좋습니다.
|
||||
|
||||
152
solution/site/scripts/mockup/build_daily.py
Normal file
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']}")
|
||||
301
solution/site/scripts/mockup/build_reading.py
Normal file
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개만 무작위)")
|
||||
@ -89,17 +89,58 @@ for item in itin:
|
||||
out.append(f"| {item['name']} | {item['duration']} | {item['audience']} | {item['why']} |")
|
||||
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"]
|
||||
out += [f"## 6. 오늘의 엽서 {len(cards)}장 — 원본 payload (아직 다시 안 뽑음)", ""]
|
||||
out += [f"## 8. 오늘의 엽서 {len(cards)}장 — 원본 payload (아직 다시 안 뽑음)", ""]
|
||||
out += [f"- {c['line']} `{' '.join(c.get('hashtags', []))}`" for c in cards] + [""]
|
||||
|
||||
# ── 머뭄이 만든 노래 ─────────────────────────────────────────────────────────
|
||||
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] + [""]
|
||||
|
||||
# ── 오늘의 날씨 ──────────────────────────────────────────────────────────────
|
||||
# 하늘 다섯 · 기온대 다섯. 화면에는 케이스마다 한 줄만 서고 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.write_text("\n".join(out), encoding="utf-8")
|
||||
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}")
|
||||
|
||||
@ -256,6 +256,9 @@
|
||||
두 문장이 같은 자리에서 겹쳐 보인다.
|
||||
★ 자리 흔들림: 처음 구워진 줄 높이를 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--out {
|
||||
opacity: 0;
|
||||
@ -327,3 +330,295 @@
|
||||
animation: none !important;
|
||||
}
|
||||
/* #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; }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user