diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index b0ec3bf..423d0d4 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -5,6 +5,40 @@ --- +## 2026-09-01 — 붙여넣기 아이템 셋: 가요 다방 · 오늘의 한 장 · 반나절 산책 + +**한 일** +- 섹션 타입 3개 추가(`songs` · `daily` · `course`). 데이터가 수집(fact)이 아니라 + **사장님이 붙여넣은 JSON** 에서 온다 — 새 갈래다. +- `canvas/dataSpec.ts` 신설: 스키마·예시·프롬프트가 한 표에 모인다. 배리에이션 레지스트리와 같은 결이라 + 여기 한 줄을 더하면 캔버스·[콘텐츠] 탭·프롬프트가 동시에 는다. +- `SectionItem.data?: string` 추가. **파싱본이 아니라 원문 문자열**을 담는다. +- [콘텐츠] 탭에 JSON 칸 + [프롬프트 복사] [프롬프트 보기] [예시 넣기] [줄맞춤]. +- 업종 시드 넷 모두에 세 섹션을 **꺼진 채로** 넣었다. + +**왜 이 모양인가** +`gunsan_365_story_db.xlsx`(365행)를 분석한 결과 **고유 주제는 52개고 한 주제가 7회씩 돈다** +(접미사 10개만 회전). 날짜 축으로 카드를 늘어놓으면 이레마다 같은 카드가 돌아온다 — +그래서 묶는 축을 주제로 잡고, 날짜는 일력 한 장에만 썼다. +같은 시트 `DB_Guide` 가 **가사·현대문학 원문 전재를 금지**해서 가요 스키마에 `lyrics` 필드를 +아예 두지 않았다. 없는 칸은 채울 수 없다. + +**밟은 함정** +- **테마 상한 64KB**(`site_service._THEME_MAX_BYTES`). 세 섹션이 각자 JSON 을 채우면 넘고, + 거절은 발행 직전에야 드러난다. → `SECTION_DATA_MAX_CHARS`(12,000자)로 화면에서 먼저 끊는다. +- **`JSON.parse` 오류 메시지가 두 형식이다.** `position N (line L column C)` 형과, 위치 없이 + 깨진 조각만 인용하는 형. 앞의 것만 보면 후자에서 위치를 통째로 잃는다 — 조각을 원문에서 되찾아 센다. +- 파싱은 **절대 throw 하지 않는다.** 편집 중인 JSON 은 늘 깨져 있고, 깨진 순간 캔버스가 죽으면 못 고친다. + +**아직 안 한 것** +- 발행 사이트(`solution/site`)는 `variantId` 도 `data` 도 아직 안 읽는다. 지금은 빌더 캔버스 전용이다. +- 프롬프트는 상호·주소를 박아 내보낸다(빈칸을 남기면 사장님이 못 채우고 그대로 보낸다). + +**검증** — `tsc --noEmit` · `eslint` · `vite build` 통과(frontend·admin). 세 배리에이션 SSR 렌더 확인, +파서 경계 12건 확인. + +--- + ## 2026-09-01 — 설정을 `.env` 하나로 모았다 **한 일** diff --git a/solution/frontend/index.html b/solution/frontend/index.html index c243534..1e9c809 100644 --- a/solution/frontend/index.html +++ b/solution/frontend/index.html @@ -12,6 +12,11 @@ href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300..900&display=swap" rel="stylesheet" /> + +
이 레이아웃에는 직접 입력하는 문구가 없습니다. 수집된 데이터만 표시합니다.
@@ -160,6 +171,159 @@ function ContentTab() { ); } +/** + * 붙여넣기 아이템의 JSON 입력. + * + * ★ 원문을 그대로 저장한다. 깨진 JSON 도 담아 두고, 왜 깨졌는지만 아래에 말한다 — + * 저장을 막으면 사장님은 고칠 기회 없이 쓰던 걸 잃는다. + */ +function SectionDataPanel({sectionId, sectionType}: {sectionId: string; sectionType: string}) { + const spec = dataSpecFor(sectionType); + const sections = useBuilderStore((s) => s.sections); + const updateSectionData = useBuilderStore((s) => s.updateSectionData); + const storeName = useBuilderStore((s) => s.storeName); + const location = useBuilderStore((s) => s.location); + const industry = useBuilderStore((s) => s.industry); + const [copied, setCopied] = useState(false); + const [showPrompt, setShowPrompt] = useState(false); + + const section = sections.find((item) => item.id === sectionId); + if (!spec || !section) return null; + + const raw = section.data ?? ''; + const parsed = parseSectionData(sectionType, raw); + const tooLong = raw.length > SECTION_DATA_MAX_CHARS; + + // 상호·주소가 이미 박혀 있는 프롬프트. 사장님이 빈칸을 채울 일이 없어야 한다. + const promptText = buildPrompt(spec, { + storeName, + location, + industryLabel: INDUSTRY_CONFIGS[industry].name, + }); + + const copyPrompt = async () => { + const text = promptText; + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 1600); + } catch { + // 클립보드가 막힌 브라우저 — 프롬프트를 화면에 띄워 직접 긁을 수 있게 한다. + window.prompt('아래 프롬프트를 복사해 ChatGPT 에 붙여넣으세요', text); + } + }; + + /** 유효한 JSON 만 2칸 들여쓰기로 다시 쓴다. 깨져 있으면 손대지 않는다. */ + const tidy = () => { + try { + updateSectionData(sectionId, JSON.stringify(JSON.parse(raw), null, 2)); + } catch { + // 그대로 둔다 — 아래 오류 줄이 이미 어디가 깨졌는지 말하고 있다. + } + }; + + return ( ++ 프롬프트를 복사해 ChatGPT·Claude 에 그대로 붙여넣으면 JSON 을 줍니다. 받은 JSON 을 아래에 붙여넣으세요. +
+ ++ 아래 내용이 그대로 복사됩니다. 상호와 지역은 이미 채워져 있습니다. +
++ {no} +
++ {stop.note} +
+ )} + ++ 정거장이 아직 없습니다. JSON 의 stops 배열을 채워 주세요. +
+ ) : ( ++ {parsed.subtitle || section.description} +
+ )} +{month}月
++ {String(Number(day) || day)} +
+ {dow &&{dow}曜
} +{item.category}
+ )} ++ {item.body} +
+ )} + {item.tags && item.tags.length > 0 && ( +{item.tags.join(' ')}
+ )} ++ {parsed.subtitle || section.description} +
+ )} ++ 오늘 날짜에 맞는 장이 자동으로 펼쳐집니다 · 총 {items.length}장 + {parsed.unverified > 0 && ` · 확인 필요 ${parsed.unverified}장`} +
++ {verified && ( + + {verified} + + )} + {source?.name && ( + + 출처 ·{' '} + {source.url ? ( + event.stopPropagation()} + className="underline underline-offset-2" + > + {source.name} + + ) : ( + source.name + )} + + )} +
+ ); +} + +/** 가로 스크롤 + 화살표. 스크롤 컨테이너를 ref 로 잡아 한 화면의 80%씩 민다. */ +export function useCarousel{label} 내용이 아직 없습니다
++ 오른쪽 [콘텐츠] 탭에서 프롬프트를 복사해 ChatGPT 에 넣고, 받은 JSON 을 붙여넣으면 바로 여기에 그려집니다. +
+붙여넣은 JSON 을 읽지 못했습니다
+{message}
+33⅓ RPM
++ {parsed.subtitle || section.description} +
+ )} ++ A면 · {playing + 1} / {parsed.items.length} +
++ {[ + current.artist, + current.year ? String(current.year) : undefined, + current.lyricist || current.composer + ? `작사 ${current.lyricist ?? '미상'} / 작곡 ${current.composer ?? '미상'}` + : undefined, + current.label, + ] + .filter(Boolean) + .join(' · ')} +
+ {current.story && ( ++ {current.story} +
+ )} + {current.connection && ( ++ {current.connection} +
+ )} + + ◎ 가사 대신 이야기 — 원문은 싣지 않습니다 + +