Files
ProjectSS/docs/architecture_design.md
kyoung5seo db1051c08c 자산(엔진) 시스템 v1 + 오염 카드 압박 강화
자산 시스템 (docs/작업지시서/자산시스템_작업지시서.md 반영, 이전 세션 작업분 커밋):
- 발라트로형 조커 레이어 엔진 빌딩 도입. assets.json 8종(트리거→효과→성장 스키마) +
  engine/assetEngine.js(이벤트 버스, 연쇄 깊이 상한 10)로 기존 수동 클릭형 자산 4종 전면 대체
- AssetInfoPopup 추가 — 자산 슬롯 클릭 시 트리거/효과/스택/충전 상태 표시, 발동 로그 피드 노출

오염 카드 압박 강화 (그레이박스 위기감 피드백 반영):
- engine/pollution.js 신설 — 오염 주입 규칙(타입 매핑·등급 추첨·물량)을 단일 모듈로 분리해
  사전 예고(SwipeCard) · 실제 주입(GameScreen) · 사후 알림이 같은 계산을 공유하도록 함
- 위험 파라미터를 올리는 결재는 정례회의를 기다리지 않고 그 자리에서 덱에 오염 카드를 주입
- UI 3종: 스와이프 시 오염 유입 사전 경고, 결재 후 유입 토스트, 상단 덱 오염 상시 게이지
- currentCardIndex를 매 결재마다 덱 길이로 정규화(주입/소각으로 덱 길이가 바뀌므로 필요)

기타:
- docs/architecture_design.md를 실제 구현(plain JS/CSS, useState, engine/ 순수 함수 경계)
  기준으로 전면 재작성 — 기존 문서는 채택되지 않은 초기 제안(Tailwind/Zustand/TS)이었음
- playtest.bat 추가 — 더블클릭으로 dev 서버 실행 (ASCII 전용, chcp로 인한 배치 파싱 오류 회피)
- CLAUDE.md 상태 갱신 및 실기 플레이 결과 기록

실기 플레이로 정상 동작 확인. 자산 획득 경로(극비 카드 스와이프)는 플레이 중 한 번도 발생하지
않아 원인 미조사 상태로 남김 — CLAUDE.md에 후속 확인 항목으로 기록.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:59:30 +09:00

11 KiB

Project SS — 아키텍처 (구현 실태)

이 문서는 "제안"이 아니라 "현재 코드가 실제로 어떻게 생겼는가"의 기록이다. 2026-07-23 기준으로 실제 구현을 확인해 전면 재작성했다. 초기 제안서(React + Vite + Tailwind + Zustand + TypeScript)는 상당 부분 채택되지 않았고, 그 내역은 문서 하단 "채택하지 않은 초기 제안"에 남겨 뒀다.

게임 규칙·수치의 원본은 이 문서가 아니라 docs/ 하위 기획서들이다. 여기엔 코드 구조만 적는다.


1. 기술 스택 (실제)

client/package.json 의존성이 전부다. 목록에 없는 것은 쓰지 않는다.

영역 채택 비고
프레임워크 React 19 함수 컴포넌트만, 클래스 컴포넌트 없음
빌드 Vite 8 client/가 프로젝트 루트. npm 명령은 cd client 후 실행
언어 plain JavaScript (.jsx) TypeScript 미도입
스타일 컴포넌트별 개별 .css 글로벌 클래스명. CSS Modules 아님, Tailwind 아님
상태관리 useState / useEffect 외부 상태관리 라이브러리 없음
애니메이션 Framer Motion 카드 드래그/스와이프
아이콘 lucide-react
CSV 파싱 papaparse 런타임 파싱 (빌드 타임 변환 아님)
영구 저장 localStorage 엔딩 도감 전용

디자인 토큰(컬러/폰트)은 src/index.css의 CSS 변수로 정의한다 (--color-entropy-critical, --font-typewriter 등). 컴포넌트 CSS는 이 변수를 참조한다.


2. 디렉토리 구조 (실제)

ProjectSS/
├── playtest.bat               # 더블클릭 → npm install(최초 1회) + dev 서버 + 브라우저 자동 오픈
│                                # ASCII 전용 — 한글/chcp를 넣으면 cmd.exe가 배치 파싱을 깨뜨린다
├── CLAUDE.md                 # 작업 규칙 · Phase 상태 (실질적인 단일 진실 공급원)
├── ROADMAP.md
├── docs/                     # 기획서 · 스토리 바이블 · 작업지시서 (게임 규칙의 원본)
└── client/                   # ★ 실제 앱. npm 명령은 여기서 실행
    ├── update_csv.js         # ⛔ 보호 파일 — cards.csv 전처리
    ├── update_csv_tags.js    # ⛔ 보호 파일 — 내러티브 태그 매핑
    └── src/
        ├── main.jsx                 (10줄)  진입점
        ├── App.jsx                  (13줄)  GameScreen 하나만 렌더 — 라우터 없음
        ├── index.css                        디자인 토큰(CSS 변수) + 리셋
        ├── assets/                          hero.png 등 정적 파일
        ├── components/                      ★ flat 구조. 하위 폴더 없음
        │   ├── GameScreen.jsx      (628줄) ★ 상태 허브 + 화면 전환
        │   ├── SwipeCard.jsx       (156줄)  카드 드래그 · 선택지 사전 예고
        │   ├── CouncilScreen.jsx   (220줄)  정례회의(투표 · 개입 액션)
        │   ├── EndingScreen.jsx     (46줄)  엔딩 출력 + 도감 저장
        │   ├── EndingCodex.jsx      (33줄)  엔딩 도감 (localStorage 읽기)
        │   ├── ChroniclePopup.jsx   (63줄)  연대기 + 태그 히스토리
        │   ├── AssetInfoPopup.jsx   (43줄)  자산 상세 · 수동 발동
        │   └── *.css                        컴포넌트당 1개씩 짝을 이룸
        ├── engine/                          ★ React를 모르는 순수 로직
        │   ├── assetEngine.js      (226줄)  자산 트리거→효과→연쇄
        │   └── pollution.js        (154줄)  오염 카드 주입 규칙 · 밸런스 상수
        └── data/
            ├── cards.csv                    카드 원본 (런타임 papaparse 파싱)
            ├── assets.json                  자산 8종 정의
            ├── narrative_tags.json          재사용 태그 풀 10종
            ├── endings.js          (123줄)  엔딩 카탈로그 + 판별 함수
            └── pollution_cards.json         ⚠️ 어디서도 import하지 않음 (레거시)

3. 핵심 아키텍처 원칙

3.1 상태는 GameScreen.jsx 한 곳에 모은다

전역 스토어가 없다. 게임 상태 전부가 GameScreenuseState로 존재하고, 하위 컴포넌트는 props로 값과 콜백을 받는 프레젠테이션 계층이다.

GameScreen (상태 소유)
 ├── SwipeCard        ← card, onSwipe, phase
 ├── CouncilScreen    ← params/tokens/tags + setter, onResolve   ※ setter를 직접 넘기는 유일한 예외
 ├── EndingScreen     ← ending, narrativeTags
 ├── ChroniclePopup   ← chronicle, acquiredTags
 └── AssetInfoPopup   ← instance, def, onActivate

트레이드오프: 파일이 628줄까지 커졌고 관심사가 섞여 있다. 다만 게임 규칙이 아직 확정되지 않아 잦은 수정이 예상되는 그레이박스 단계에서는 "한 파일만 보면 전체 흐름을 안다"는 이점이 분할 비용보다 컸다. Phase 5 이후 재검토 대상이다.

3.2 규칙 계산은 engine/의 순수 함수로 뺀다

engine/은 React·훅·JSX를 전혀 import하지 않는다. 평범한 객체를 받아 평범한 객체를 돌려준다.

이 경계 덕분에 게임을 띄우지 않고 밸런스를 검증할 수 있다. 실제로 오염 카드 물량 조정 시 node 스크립트로 pollution.js를 직접 import해 600회 시뮬레이션을 돌려 변경 전후를 비교했다. 새 규칙 모듈을 만들 때도 이 성질을 깨지 말 것.

3.3 world 스냅샷 패턴 (자산 엔진)

assetEngine은 상태를 직접 만지지 않는다. GameScreen이 현재 state를 복사한 world 객체를 만들어 넘기고, 엔진이 변형한 결과를 다시 state로 커밋한다.

const world = buildAssetWorld();      // state 복사 → { cards, tokens, tags, params, owned, quarantine, logs }
AssetEngine.onPollutionDraw(world, card.type);
AssetEngine.onApprovalTick(world, nextTurns);
commitAssetWorld(world);              // 결과를 setState로 일괄 반영

⚠️ 반드시 지킬 것: 한 이벤트 처리 안에서 buildAssetWorld/commitAssetWorld 쌍을 정확히 한 번만 쓴다. 두 번 짝지어 부르면 두 번째 build가 아직 리렌더되지 않은 낡은 클로저 state를 복사하므로, 나중 커밋이 앞의 변경을 덮어써 버린다.

엔진 훅 지점은 4곳이다: onApprovalTick(결재마다) · onPollutionDraw(오염 카드 등장) · onSwipeLeft(좌 스와이프 격리) · onCouncilEnd(정례회의 종료). 연쇄 발동은 fireEvent가 깊이 상한 10으로 처리한다.

3.4 예고 · 실행 · 알림은 같은 함수를 공유한다

플레이어에게 미리 보여준 숫자와 실제 결과가 어긋나면 정보 자체를 신뢰하지 않게 된다. 그래서 오염 주입량은 pollution.jspreviewPollution() 하나가 단일 기준이다.

SwipeCard  "☣ 오염 카드 +2" 사전 경고  ─┐
GameScreen 실제 덱 주입                 ─┼─ 모두 previewPollution() 사용
GameScreen "2건 유입" 사후 토스트        ─┘

이 함수를 우회해 오염 카드를 만들지 말 것. 밸런스 수치는 pollution.js 상단 상수 블록에 모여 있다.


4. 데이터 흐름

cards.csv ──(?raw import)──> papaparse ──> buildCardFromRow() ──> 덱 배열
                                                                    │
narrative_tags.json ──> left_tag/right_tag id로 조인 ────────────────┘
  • 카드 데이터는 런타임에 파싱한다. import cardsCsvRaw from '../data/cards.csv?raw' 후 브라우저에서 papaparse로 처리한다. 빌드 타임 변환 단계가 없으므로 CSV를 고치면 새로고침만으로 반영된다.
  • 덱은 인덱스를 순환(modulo)하는 풀이다. 소진되어 없어지지 않고 계속 돈다. currentCardIndex는 매 결재마다 최종 덱 길이로 정규화해 범위 안에 유지한다 — 주입/소각으로 길이가 변하기 때문이다.
  • 카드 풀은 페이즈별로 분리한다: 페이즈1(일상/위기)로 시작하고, 내러티브 태그 3종 획득 시 페이즈2(작전/극비) 풀이 덱에 삽입된다.
  • 오염 카드(신화/조직저항/사회공황/이사회압박)는 contaminationTemplates로 따로 보관하다가 주입 시점에 복제해 고유 id를 붙인다.

5. 화면 전환

라우터가 없다. GameScreen이 상태를 보고 조기 return으로 화면을 고른다.

if (gameOver)    return <EndingScreen ... />;
if (showCouncil) return <CouncilScreen ... />;
if (cards.length === 0) return <div>데이터를 불러오는 ...</div>;
return ( /* 결재 화면 */ );

팝업(연대기·자산 정보·도감)은 화면을 갈아끼우지 않고 조건부 오버레이로 렌더한다.


6. 영구 저장

localStorage에 저장하는 것은 엔딩 도감뿐이다. 세이브/로드 기능은 없고, 게임오버 시 window.location.reload()로 재시작한다.

  • 키: ss_endings_v2 (EndingCodex.jsx에서 ENDINGS_STORAGE_KEY로 export)
  • 값: 해금된 엔딩 id 배열. 구버전 ss_endings는 제목 문자열 기반이라 폐기했고 호환되지 않는다.

7. 채택하지 않은 초기 제안

초기 제안서에 있었으나 의도적으로 도입하지 않은 것들이다. 되살리려면 CLAUDE.md 규칙부터 바꿔야 한다.

제안 현재 사유
Tailwind CSS 컴포넌트별 .css 관료제 다크 UI는 유틸리티 클래스보다 커스텀 CSS가 손에 맞았다
Zustand useState 상태가 한 컴포넌트에 모여 있어 전역 스토어의 값어치가 없다. 도입 금지
TypeScript plain JS 그레이박스 단계의 잦은 스키마 변경 대비 타입 유지비가 컸다
store/ hooks/ types/ utils/ constants/ 없음 규칙 로직은 engine/으로 통합
components/{common,dashboard,gameplay,council}/ flat 컴포넌트 7개뿐이라 계층이 불필요
8대 파벌 도넛 차트 텍스트 목록 파벌은 12종 풀에서 5명 선발 방식으로 변경됨

8. 알려진 부채

  • GameScreen.jsx 628줄 — 상태 허브 + 화면 전환 + 스와이프 처리 + 파벌 계산이 한 파일에 있다.
  • data/pollution_cards.json 미사용 — 어디서도 import하지 않는 레거시. 오염 카드는 cards.csv에서 온다.
  • 테스트 코드 없음 — 검증은 engine/ 모듈을 직접 부르는 일회성 node 스크립트로 해 왔다.
  • window.location.reload() 재시작 — 상태 초기화 로직이 따로 없다.