diff --git a/CLAUDE.md b/CLAUDE.md index 0037c2f..95103e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,9 +74,14 @@ Phase 5 [ ] 비주얼 폴리싱 & UX ← 그레이박스 테스트 완 **Phase 4 완료 시 남은 주의사항:** 엔딩 도감 localStorage 키를 `ss_endings_v2`로 교체했다 (기존 `ss_endings`는 제목 문자열 기반이라 취약했음). 이전 플레이테스트 기록은 새 키와 -호환되지 않는다. 내러티브 태그는 모든 카드가 아니라 선별된 카드(22/63장)만 키워드를 주는 -방식으로 재설계했고, 페이즈 전환 문턱도 4/8 → 2/4로 낮췄다. 자세한 내용은 `ROADMAP.md` -Phase 4 참고. +호환되지 않는다. 내러티브 태그는 재사용 가능한 태그 풀(`narrative_tags.json`, 10종) 방식으로, +선별된 카드의 좌/우 선택지에서만 얻는다. 페이즈 전환 문턱은 "고유 태그 3종/6종 획득". 자세한 +내용은 `ROADMAP.md` Phase 4 참고. + +**⚠️ 2026-07-09 병렬 브랜치 병합:** 이 폴더가 유일한 작업 사본이 아니라면, 작업 시작 전 +`git fetch` 후 `git log origin/main..HEAD`와 `git log HEAD..origin/main`으로 분기 여부를 +반드시 확인할 것. 과거에 다른 세션이 origin에 푸시한 커밋을 이 로컬이 받지 못한 채 Phase 2~4를 +독립적으로 재구현해 대형 충돌이 났던 적이 있다. 자세한 경위는 `ROADMAP.md` 상단 박스 참고. Phase 0~4 그레이박스 구간이 모두 완료됐다. 다음은 외부 플레이어 그레이박스 테스트이며, Phase 5(비주얼 폴리싱)는 그 피드백 반영 후에만 착수한다. diff --git a/ROADMAP.md b/ROADMAP.md index 1c10dca..d2dfde2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -42,6 +42,19 @@ Phase 5 [ ] 비주얼 폴리싱 & UX ← 그레이박스 테스트 완 --- +## ⚠️ 2026-07-09: 병렬 작업 브랜치 병합 (중요 — 반드시 읽을 것) + +이 로컬 작업 폴더는 2026-06-24~25에 **다른 세션에서 origin/main에 푸시된 4개 커밋**을 +받아오지 못한 채로 Phase 2~4 작업을 진행했다. 그 커밋들에는 이 로컬에서 독립적으로 +다시 구현한 것과 겹치는 기능(파산 문책, 내러티브 태그, 오염 카드, 자산)이 서로 다른 +방식으로 이미 존재했고, `git pull` 시점에 6개 파일에서 충돌이 나서 origin 쪽 구현을 +기준으로 병합했다. 원인·과정·잔여 이슈는 아래 각 Phase 절의 "병합 참고" 박스에 정리했다. + +**이 폴더가 유일한 작업 사본이 아니라면, 다음 세션을 시작하기 전에 반드시 `git fetch` + +`git log origin/main..HEAD` / `git log HEAD..origin/main`으로 분기 여부를 먼저 확인할 것.** + +--- + ## 다음 단계 **Phase 0~4 그레이박스 작업이 모두 완료됐다.** 이제 외부 플레이어 그레이박스 테스트를 진행할 차례다. @@ -108,7 +121,14 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한 > **참고:** 오염 카드 콘텐츠(`cards.csv`의 신화/조직저항/사회공황/이사회압박 16종)는 등급별 1종씩만 채운 **초안**이다. > 밸런스·문체 검토 후 디자이너가 등급별로 추가 카드를 늘리는 작업이 별도로 필요하다. > 또한 A급 이상 신화 카드가 '뉴스 이벤트' 카드를 추가하는 연쇄 규칙(기획서 92-95행)은 이번 범위에 포함하지 않았다. -> node/npm이 설치되지 않은 환경이라 `npm run dev` 실기 검증은 못 했고, 코드 리뷰로만 확인했다 — 실행 확인 필요. + +> **병합 참고 (2026-07-09):** origin/main에 미리 푸시되어 있던 다른 세션의 파산 문책 구현(정례회의 +> 라운드가 늘수록 결재 건수가 8→6→4→2→0으로 줄어들고, 파산 카운터 5회 이상이면 의회에서 전 파라미터를 +> 강제로 올리는 에스컬레이션 포함)이 더 완성도 높고 실제 사용자 피드백을 직접 반영한 것이었다. 그 버전을 +> 기준으로 병합하면서 상태명을 `bankruptcyCounter/bankruptcyWarned` → `bankruptcyCount/bankruptcyWarning`으로 +> 통일했다. 자산은 origin에 `assets.json`(이름 있는 개별 아이템 4종: 전임 국장의 만년필 등)이 준비돼 있었지만 +> 실제 효과 로직이 연결돼 있지 않아서, 이 로컬에서 만든 "성질 변환/시한부 격리/물리적 영구 소각/결재권 강제 +> 행사" 4종 기능 로직을 그대로 유지했다 — `assets.json`의 이름 있는 아이템 플레이버는 아직 미사용 상태다. ### 핵심 참조 문서 - `시스템 기획서` — 결재 단계, 덱 오염, 덱 압축, 자산 슬롯 @@ -130,18 +150,20 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한 - [x] 연대기(Chronicle) 팝업 - [x] **파벌 우호도 세부 조건** — 12개 글로벌 지부장 파벌이 각자 다른 공식으로 우호도를 계산한다(`GameScreen.jsx` `GLOBAL_FACTIONS`). 게임 시작 시 12개 중 5개를 무작위 선발(`factionRoster`), 매 의회마다 실시간 상태(토큰/태그/파라미터/자산/카운터)로 점수를 산출해 동맹/우호/중립/적대/파국 5단계 티어로 환산, 투표와 국장 개입 면역 여부를 결정한다(`CouncilScreen.jsx`). - [x] **파산 카운터 → 의회 문책 연동** — Phase 2 작업으로 완료됨 (`CouncilScreen.jsx`가 `bankruptcyCounter`/`bankruptcyWarned`를 직접 참조) -- [x] **스토리 페이징 카드 풀 전환** — 초기 덱은 일상/위기만 포함되고, 내러티브 태그 4개 도달 시 작전/극비 프로젝트 카드 풀이 실시간으로 덱에 개방된다(`GameScreen.jsx` `phase2Pool`/`phase2Unlocked`). +- [x] **스토리 페이징 카드 풀 전환** — 초기 덱은 일상/위기만 포함되고, 고유 내러티브 태그 3종 도달 시 작전/극비 프로젝트 카드 풀이 실시간으로 덱에 개방된다(`GameScreen.jsx` `phase2Pool`/`phase2Unlocked`). ### 완료 기준 (Done Criteria) - [x] 글로벌 지부장 12개 파벌의 우호도가 각자 다른 조건으로 개별 계산된다. - [x] 파산 카운터가 의회 문책 로직에 연동된다. -- [x] 내러티브 태그 4개 도달 시 등장 카드 풀이 실시간으로 전환된다. +- [x] 내러티브 태그 누적 시 등장 카드 풀이 실시간으로 전환된다. > **참고 및 남은 과제:** > - 상임 이사회 8명의 투표는 여전히 `params.risk` 기반 확률 처리다. 기획서상 이들은 국장의 '성향 태그'에 반응해야 하는데, 이 게임에는 아직 성향 태그 시스템 자체가 없어 범위 밖으로 남겨뒀다(§4.1 대응). -> - 태그 8개(페이즈3) 시점의 카드 풀은 기획서상 페이즈2와 동일한 유형(작전/극비/오염)이라 별도 전환 이벤트를 추가하지 않았다. 대신 오염 카드 등급 가중치가 파라미터 상승에 따라 자연스럽게 심해지는 방식으로 "압박 루프" 강도를 구현했다. > - Playwright로 실제 브라우저에서 플레이 검증 중 **`params.risk >= 5`가 직접 게임오버를 발동시키지만 `EndingScreen`에 해당 분기가 없어 "미확인 종말" 폴백으로 빠지는 버그**를 발견해 수정했다(PRD상 risk는 탄핵 기준치일 뿐 직접 붕괴 조건이 아님). +> **병합 참고 (2026-07-09):** 내러티브 태그 시스템 자체를 origin 버전(재사용 가능한 태그 풀 방식, 아래 참고)으로 +> 교체하면서 페이징 문턱도 "고유 태그 4개/8개" → "고유 태그 3개/6개"로 재조정했다(풀 크기 10종 기준). + ### 핵심 참조 문서 - `스토리 바이블` — §4.2 글로벌 지부장 12개 파벌 상세 - `시스템 기획서` — 주간 의회 단계, 토큰 경제 @@ -169,8 +191,9 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한 - [x] **변주된 붕괴 엔딩** — 자원 태그 변주 3종(쿠데타/은폐된 멸망/자본의 폭주) + 토큰 변주 3종(야반도주/정보 암흑/긴축의 폭동) - [x] **극단적 파라미터 조기 엔딩** — 단일 결재에서 위험 파라미터가 3단계 이상 급증하며 5단계에 도달하면 "돌이킬 수 없는 순간" 즉각 붕괴 엔딩 발동 - [x] **내러티브 연쇄 엔딩** — 오염 카드 4종을 지정 순서로 수집하면 해금, 이후 탄핵/붕괴 시 최우선 적용 -- [x] **도감 UI** — `EndingCodex.jsx` 신규 컴포넌트. 엔딩 화면에서 "📖 엔딩 도감 보기"로 열람, 해금 15종 중 몇 종 확인했는지 표시, 미해금은 "???"로 잠금 처리, `localStorage`(`ss_endings_v2`)에 엔딩 id 기준으로 영구 저장 -- [x] **연대기 팝업 완성** — 좌측 세로선 + 턴별 점(dot)으로 타임라인 형태 시각화(`ChroniclePopup.css`) +- [x] **임기 만료 엔딩** (병합으로 추가) — 라운드가 거듭될수록 정례회의까지의 결재 건수가 8→6→4→2→0으로 줄어들다 0이 되면 발동 +- [x] **도감 UI** — `EndingCodex.jsx` 신규 컴포넌트. 엔딩 화면에서 "📖 엔딩 도감 보기"로 열람, 해금 16종 중 몇 종 확인했는지 표시, 미해금은 "???"로 잠금 처리, `localStorage`(`ss_endings_v2`)에 엔딩 id 기준으로 영구 저장 +- [x] **연대기 팝업 완성** — 턴별 타임라인 + 새로 획득한 내러티브 태그를 아이콘/설명과 함께 인라인으로 강조 표시, 상단에 수집한 태그 전체를 칩으로 요약(`ChroniclePopup.jsx/css`) ### 완료 기준 (Done Criteria) - [x] 파라미터 5단계 도달 시 우선순위 판별기가 정확한 엔딩을 출력한다. (Playwright로 "군사 쿠데타" 변주 엔딩과 "정치적 탄핵" 기본 엔딩을 각각 실제로 트리거해 확인) @@ -179,16 +202,18 @@ Phase 5(비주얼 폴리싱)는 그 피드백을 반영한 뒤에만 착수한 - [x] 연대기 팝업에서 이번 회차 내러티브 히스토리를 확인할 수 있다. > **참고:** 엔딩 도감 localStorage 키를 `ss_endings`(제목 문자열 기반, 취약)에서 `ss_endings_v2`(고유 id 기반)로 교체했다. -> 이전에 수동 플레이테스트로 쌓인 `ss_endings` 데이터는 새 키와 호환되지 않아 브라우저에서 도감이 0/15로 보일 수 있다 — +> 이전에 수동 플레이테스트로 쌓인 `ss_endings` 데이터는 새 키와 호환되지 않아 브라우저에서 도감이 0/16으로 보일 수 있다 — > 프로토타입 단계라 마이그레이션 로직은 만들지 않았다. -> **내러티브 태그 시스템 재설계 (디자이너 피드백 반영):** 최초 구현은 결재 카드 한 장당 무조건 +1 되는 -> 카운터였는데, 이는 "내러티브 태그는 항상 얻는 것이 아니다"라는 기획 의도와 맞지 않았다. `cards.csv`에 -> `left_narrative` / `right_narrative` 컬럼을 추가해 선별된 카드(위기/작전/극비 22종 + 오염 카드 A·S등급 -> 8종, 총 22장/63장)의 좌/우 선택지에만 짧은 키워드(예: "은폐 공작 실행", "쿠데타 조짐")를 부여하도록 -> 바꿨다. 수집한 키워드는 연대기 팝업 상단과 엔딩 화면에서 칩(chip) 형태로 확인할 수 있다(기존 턴별 -> 결재 로그는 그대로 유지). 동시에 페이즈 전환 문턱을 4/8 → 2/4로 낮췄다(키워드가 희소해진 만큼, 그리고 -> 기존 4/8도 실제 플레이에서 길게 느껴진다는 피드백 반영). +> **병합 참고 (2026-07-09) — 내러티브 태그 시스템 최종본:** 이 Phase에서 한 번(카드당 자유 텍스트 키워드 +> `left_narrative`/`right_narrative`) 재설계했었는데, 이후 origin/main에 병합할 다른 세션의 구현을 발견했다. +> 그 버전은 `narrative_tags.json`(재사용 가능한 태그 10종 — 아이콘+이름+설명, 예: "쿠데타의 그림자" 🗡️)과 +> `cards.csv`의 `left_tag`/`right_tag` 컬럼(태그 id 참조) 조합으로, 처음 획득하는 태그일 때만 토스트 알림이 +> 뜨고 진행도에 반영되는 방식이었다 — 사용자 피드백을 더 직접 반영한 더 성숙한 설계라 최종적으로 이 방식을 +> 채택하고 자유 텍스트 방식은 폐기했다. 다만 origin의 `cards.csv`에는 태그 컬럼 자체가 비어 있어 실제로는 +> 작동하지 않는 상태였으므로, `update_csv_tags.js`의 기존 11개 매핑을 참고해 새로 채워 넣고 태그 풀도 +> 4종(쿠데타의 그림자/군중의 광기/정치적 압박/희생의 대가) 추가했다. 페이징 문턱도 "태그 풀 10종 중 고유 +> 3종/6종 획득"으로 재조정했다. > **이 Phase 완료 = 그레이박스 테스트 준비 완료.** > Phase 5는 테스트 피드백 반영이 끝난 후 착수한다. diff --git a/client/src/components/ChroniclePopup.css b/client/src/components/ChroniclePopup.css index 20d6e03..c268eea 100644 --- a/client/src/components/ChroniclePopup.css +++ b/client/src/components/ChroniclePopup.css @@ -4,7 +4,7 @@ left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.8); + background: rgba(0, 0, 0, 0.85); z-index: 300; display: flex; align-items: center; @@ -14,14 +14,14 @@ .chronicle-popup { background: var(--color-surface); width: 90%; - max-width: 400px; - max-height: 80%; + max-width: 500px; + max-height: 85%; border-radius: 16px; display: flex; flex-direction: column; overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 10px 50px rgba(0, 0, 0, 0.9); } .chronicle-header { @@ -29,14 +29,23 @@ align-items: center; justify-content: space-between; padding: 20px; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.6); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .chronicle-header h2 { margin: 0; font-family: var(--font-doc-title); - font-size: 20px; + font-size: 22px; +} + +.chronicle-tags-summary { + background: rgba(251, 191, 36, 0.2); + color: #fbbf24; + padding: 4px 12px; + border-radius: 12px; + font-size: 14px; + font-weight: bold; } .close-btn { @@ -45,6 +54,7 @@ color: #fff; font-size: 28px; cursor: pointer; + line-height: 1; } .keyword-section { @@ -85,7 +95,7 @@ } .chronicle-content { - padding: 20px; + padding: 24px; overflow-y: auto; flex: 1; } @@ -96,50 +106,106 @@ font-style: italic; } -.chronicle-list { - list-style: none; - padding: 0 0 0 14px; - margin: 0; - border-left: 2px solid rgba(255, 255, 255, 0.1); +.chronicle-timeline { + display: flex; + flex-direction: column; + gap: 20px; } -.chronicle-item { - position: relative; - margin-bottom: 20px; +.chronicle-entry { + display: flex; + gap: 16px; padding-bottom: 20px; - padding-left: 16px; border-bottom: 1px dashed rgba(255, 255, 255, 0.1); } -.chronicle-item::before { - content: ''; - position: absolute; - left: -21px; - top: 4px; - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--color-token-info); - box-shadow: 0 0 0 3px var(--color-surface); -} - -.chronicle-item:last-child { +.chronicle-entry:last-child { border-bottom: none; - margin-bottom: 0; padding-bottom: 0; } -.turn-number { - font-size: 12px; - color: var(--color-token-info); +.entry-turn { + font-size: 14px; font-weight: bold; - display: block; - margin-bottom: 5px; + color: #a1a1aa; + min-width: 60px; + padding-top: 2px; } -.chronicle-item p { - margin: 0; - font-size: 14px; - line-height: 1.5; - color: #ddd; +.entry-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; +} + +.entry-context { + display: flex; + gap: 8px; + align-items: center; +} + +.entry-type { + font-size: 12px; + color: var(--color-token-info); + font-family: var(--font-typewriter); +} + +.entry-keyword { + font-size: 15px; + font-weight: bold; + color: #fbbf24; +} + +.entry-text { + font-size: 14px; + color: #d4d4d8; + line-height: 1.5; +} + +.entry-decision { + font-size: 14px; + color: #4ade80; + font-weight: bold; +} + +.entry-tag-highlight { + margin-top: 12px; + background: rgba(251, 191, 36, 0.15); + border-left: 4px solid #fbbf24; + border-radius: 4px; + padding: 12px; + display: flex; + align-items: center; + gap: 12px; +} + +.tag-icon { + font-size: 32px; +} + +.tag-details { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tag-name { + font-size: 16px; + font-weight: bold; + color: #fbbf24; +} + +.tag-desc { + font-size: 13px; + color: #fff; + line-height: 1.4; +} + +.has-tag { + background: linear-gradient(90deg, rgba(251,191,36,0.05) 0%, transparent 100%); + border-radius: 8px; + padding: 12px; + border-bottom: none; + margin-bottom: 4px; } diff --git a/client/src/components/ChroniclePopup.jsx b/client/src/components/ChroniclePopup.jsx index 0d6464e..df2a7b2 100644 --- a/client/src/components/ChroniclePopup.jsx +++ b/client/src/components/ChroniclePopup.jsx @@ -1,23 +1,26 @@ import React from 'react'; import './ChroniclePopup.css'; -export default function ChroniclePopup({ chronicle, keywords = [], onClose }) { +export default function ChroniclePopup({ chronicle, acquiredTags = [], onClose }) { return (
아직 기록된 역사가 없습니다.
) : ( -{entry.text}
-지난 경고에도 불구하고 장부는 여전히 마이너스입니다. 이사회는 더 이상 변명을 듣지 않기로 했습니다.
-"숫자는 거짓말을 하지 않습니다. 당신의 자리도 이제 안녕입니다."
- -{bankruptPenalty}
+[{newTagNotification.name}]
+{card.text}
@@ -109,10 +111,22 @@ export default function SwipeCard({ card, onSwipe, phase }) { {/* Swipe Intents */}