diff --git a/CHANGELOG.json b/CHANGELOG.json index dd8b91c..96a3962 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -1,11 +1,26 @@ { "metadata": { - "lastUpdated": "2026-08-08T12:45:23Z", - "currentVersion": "1.4.15", + "lastUpdated": "2026-08-08T13:27:17Z", + "currentVersion": "1.4.16", "projectType": "project_type: react # spring, flutter, react, react-native, react-native-expo, node, basic", - "totalReleases": 30 + "totalReleases": 31 }, "releases": [ + { + "version": "1.4.16", + "project_type": "project_type: react # spring, flutter, react, react-native, react-native-expo, node, basic", + "date": "2026-08-08", + "pr_number": 118, + "raw_summary": "Summary by CodeRabbit\n릴리스 노트\n\n개선\n\n스토리 분기 규칙을 데이터로 분리해 이야기 추가·수정이 쉬워졌습니다", + "parsed_changes": { + "개선": { + "title": "개선", + "items": [ + "스토리 분기 규칙을 데이터로 분리해 이야기 추가·수정이 쉬워졌습니다" + ] + } + } + }, { "version": "1.4.15", "project_type": "project_type: react # spring, flutter, react, react-native, react-native-expo, node, basic", diff --git a/CHANGELOG.md b/CHANGELOG.md index 588de83..5ada628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ # Changelog -**현재 버전:** 1.4.15 -**마지막 업데이트:** 2026-08-08T12:45:23Z +**현재 버전:** 1.4.16 +**마지막 업데이트:** 2026-08-08T13:27:17Z + +--- + +## [1.4.16] - 2026-08-08 + +**PR:** #118 + +**개선** +- 스토리 분기 규칙을 데이터로 분리해 이야기 추가·수정이 쉬워졌습니다 --- diff --git a/README.md b/README.md index b3adae5..dcdfce6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ -## 최신 버전 : v1.4.14 (2026-08-08) +## 최신 버전 : v1.4.15 (2026-08-08) [전체 업데이트 내역 보기](CHANGELOG.md) diff --git a/claude.md b/claude.md index 46a2a36..48cfd60 100644 --- a/claude.md +++ b/claude.md @@ -90,7 +90,7 @@ CI=false npm run build # 빌드 | 1 | 인증/API 레이어 통일 및 레거시 제거 | **완료** | | 2 | 게임 라우팅 및 진행 상태 정합성 수정 | **완료** | | 3 | 디자인 시스템 재확립 및 대형 화면 분해 | 진행 중 | -| 4 | 스토리 분기 엔진 데이터화 | 예정 | +| 4 | 스토리 분기 엔진 데이터화 | **완료** | | 5 | 문서 최종 동기화 및 배포 | 예정 | 각 Phase는 **1 이슈 = 1 브랜치 = 1 PR**로 진행한다. diff --git a/docs/GAME-FLOW.md b/docs/GAME-FLOW.md index a6994a6..c8b0163 100644 --- a/docs/GAME-FLOW.md +++ b/docs/GAME-FLOW.md @@ -54,8 +54,19 @@ Main2(카페)의 마지막 씬 "바닥에 돈이 떨어져 있는 것을 발견 ## 점수 임계치 -`src/game/storyFlow.js`의 `resolveNextStep()`에 정의되어 있다. -아래는 **현재 실제 동작**이며, `src/game/storyFlow.test.js`가 이 표를 그대로 검증한다. +**`src/data/stageRules.json`이 단일 출처다.** `src/game/storyFlow.js`는 이 규칙을 평가만 한다. +아래 표는 그 데이터의 내용이며, `src/game/storyFlow.test.js`가 그대로 검증한다. + +```json +"0": [ + { "when": { "scoreMin": 15, "scoreMax": 40 }, "to": 1 }, + { "when": { "scoreMax": 15 }, "to": 2 }, + { "to": 3 } +] +``` + +규칙 배열을 위에서부터 평가해 첫 번째로 조건을 만족하는 대상으로 이동한다. +`when`이 없는 규칙은 기본값 역할을 한다. | 스테이지 | 조건 | 분기 | | -------- | ----------------------- | ------------------------------------------------------------- | @@ -97,16 +108,17 @@ Main2(카페)의 마지막 씬 "바닥에 돈이 떨어져 있는 것을 발견 이 절의 내용은 `docs/superpowers/specs/2026-07-27-codebase-standardization-design.md` 1.2절의 진단과 대응한다. -### 1. 컴포넌트가 스토리 구조를 알고 있다 — **부분 해소** +### 1. 컴포넌트가 스토리 구조를 알고 있다 — **해소됨** 씬 전이 규칙이 `scene`/`index` 숫자 조건문 약 60줄로 컴포넌트에 박혀 있었다. -`src/game/storyFlow.js`로 **순수 함수 추출을 완료**했다. 컴포넌트에서 `scene ===` 조건문이 -사라졌고, 전이 규칙은 이제 테스트 가능한 함수다. `src/game/storyFlow.test.js`의 25개 -테스트가 **현재 동작의 기대값 표** 역할을 한다. +**해소 방식** + +1. `src/game/storyFlow.js`로 순수 함수 추출 (컴포넌트의 `scene ===` 조건문 0건) +2. `src/game/storyFlow.test.js`에 전 경로 회귀 테스트 25개 — **현재 동작의 기대값 표** +3. 규칙을 `src/data/stageRules.json`으로 이관. 위 테스트가 데이터화 전후 모두 통과함을 확인 -다만 규칙은 여전히 **코드**에 있다. JSON 데이터로 옮기는 작업이 남아 있으며, -그때 위 테스트가 회귀 안전망이 된다. +이제 **씬을 추가하거나 점수 임계치를 바꾸려면 JSON만 고치면 된다.** 코드는 손대지 않는다. ### 2. 우연히 동작하는 분기 diff --git a/package.json b/package.json index b13f816..39d7472 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "re-wave", - "version": "1.4.15", + "version": "1.4.16", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.17.0", diff --git a/src/data/stageRules.json b/src/data/stageRules.json new file mode 100644 index 0000000..7517003 --- /dev/null +++ b/src/data/stageRules.json @@ -0,0 +1,67 @@ +{ + "_comment": [ + "스토리 전이 규칙. 원래는 컴포넌트에 숫자 조건문 60줄로 박혀 있었다.", + "", + "구조: stages[스테이지번호].scenes[씬인덱스] = 전이 규칙 배열", + " - 배열을 위에서부터 평가해 첫 번째로 조건을 만족하는 규칙을 따른다", + " - when 없는 규칙은 항상 참 (기본값 역할)", + " - when.scoreMin / when.scoreMax 로 점수 구간 지정 (Max는 미만)", + " - to: 다음 씬 인덱스 / exit: 화면 이동", + "", + "규칙이 없는 씬은 index + 1 로 순서대로 진행한다.", + "stageExit: 스테이지를 마쳤을 때 선택지에 따라 갈리는 다음 스테이지", + "", + "값의 근거는 docs/GAME-FLOW.md, 검증은 src/game/storyFlow.test.js" + ], + + "stages": { + "2": { + "scenes": { + "0": [ + { "when": { "scoreMin": 15, "scoreMax": 40 }, "to": 1 }, + { "when": { "scoreMax": 15 }, "to": 2 }, + { "to": 3 } + ], + "1": [{ "to": 3 }], + "2": [{ "to": 3 }] + }, + "stageExit": { + "_comment": "카페에서 '돈을 줍는다'(첫 선택지)를 고르면 우산 경로가 열린다", + "byChoice": { "0": "/main4" }, + "default": "/main3" + } + }, + + "3": { + "scenes": { + "1": [ + { "when": { "scoreMin": 60 }, "to": 2 }, + { "when": { "scoreMin": 20 }, "to": 3 }, + { "to": 4 } + ], + "2": [{ "to": 5 }], + "3": [{ "to": 5 }], + "4": [{ "to": 5 }] + } + }, + + "4": { + "scenes": { + "2": [ + { + "_comment": "데이터의 love:60 선언과 달리 실제 임계치는 70이다 (동작 유지)", + "when": { "scoreMin": 70 }, + "to": 3 + }, + { "to": 4 } + ] + } + }, + + "5": { + "scenes": { + "2": [{ "exit": "/result" }] + } + } + } +} diff --git a/src/game/storyFlow.js b/src/game/storyFlow.js index 2bb7163..c8ee9a1 100644 --- a/src/game/storyFlow.js +++ b/src/game/storyFlow.js @@ -1,22 +1,33 @@ import { ROUTES } from "../constants/routes"; +import stageRules from "../data/stageRules.json"; /** - * 스토리 전이 규칙 + * 스토리 전이 엔진 * - * 이 규칙은 원래 `BagelSelectPageComponent`의 `onSubClicked()` 안에 - * `scene`/`index` 숫자 조건문 약 60줄로 박혀 있었다. 컴포넌트가 스토리 구조를 - * 알고 있어서, 씬을 하나 추가하려면 데이터가 아니라 컴포넌트를 고쳐야 했다. + * 규칙은 `src/data/stageRules.json`에 있다. 이 파일은 그 규칙을 **평가**만 한다. + * 씬을 추가하거나 점수 임계치를 바꾸려면 JSON만 고치면 되고, 이 코드는 손대지 않는다. * - * 여기서는 **동작을 그대로 유지한 채** 순수 함수로만 떼어냈다. - * 순수 함수가 되면 전 경로를 테스트로 검증할 수 있고, 그 테스트가 - * 이후 데이터화(JSON 스키마 전환)의 회귀 안전망이 된다. + * 원래는 `BagelSelectPageComponent`의 `onSubClicked()`에 숫자 조건문 60줄로 박혀 있었다. + * 컴포넌트가 스토리 구조를 알고 있어서, 씬 하나를 추가하려면 컴포넌트를 고쳐야 했다. * - * 값은 바꾸지 않았다. 임계치가 이상해 보여도 그것이 현재 동작이다. - * 근거는 docs/GAME-FLOW.md에 기록되어 있다. + * 동작은 그대로다. 값의 근거는 docs/GAME-FLOW.md, + * 회귀 검증은 storyFlow.test.js의 전 경로 기대값 표가 담당한다. */ -/** 스테이지 종료를 뜻하는 신호. 씬 인덱스 대신 이 값을 반환한다. */ -export const STAGE_END = "STAGE_END"; +/** + * 전이 규칙의 조건을 평가한다. + * `when`이 없으면 항상 참 — 규칙 배열의 마지막 기본값으로 쓴다. + * + * @param {Object} [when] `{ scoreMin, scoreMax }`. scoreMax는 **미만** + * @param {number} score + * @returns {boolean} + */ +const matches = (when, score) => { + if (!when) return true; + if (when.scoreMin !== undefined && score < when.scoreMin) return false; + if (when.scoreMax !== undefined && score >= when.scoreMax) return false; + return true; +}; /** * 현재 씬을 마쳤을 때 다음에 무엇을 할지 결정한다. @@ -24,48 +35,22 @@ export const STAGE_END = "STAGE_END"; * @param {number} scene 스테이지 번호 (1~5) * @param {number} index 방금 마친 씬 인덱스 * @param {number} score 현재까지 누적 점수 - * @returns {{type: "scene", index: number} | {type: "navigate", to: string} | {type: "end"}} + * @returns {{type: "scene", index: number} | {type: "navigate", to: string}} */ export const resolveNextStep = (scene, index, score) => { - const toScene = (i) => ({ type: "scene", index: i }); + const rules = stageRules.stages?.[String(scene)]?.scenes?.[String(index)]; - if (scene === 2) { - if (index === 0) { - if (score >= 15 && score < 40) return toScene(1); - if (score < 15) return toScene(2); - return toScene(3); + if (rules) { + const matched = rules.find((rule) => matches(rule.when, score)); + if (matched) { + if (matched.exit) return { type: "navigate", to: matched.exit }; + return { type: "scene", index: matched.to }; } - // 점수 분기로 1 또는 2에 들어왔다면 공통 씬(3)으로 합류한다 - if (index === 1 || index === 2) return toScene(3); - return toScene(index + 1); } - if (scene === 3) { - if (index === 1) { - if (score >= 60) return toScene(2); - if (score >= 20) return toScene(3); - return toScene(4); - } - if (index === 2 || index === 3 || index === 4) return toScene(5); - return toScene(index + 1); - } - - if (scene === 4) { - // 점수가 높으면 특수 씬(맥주)을 거친다. - // 데이터에 `love: 60`이 선언돼 있으나 실제 동작은 70이다 (docs/GAME-FLOW.md 참조) - if (index === 2) { - return score >= 70 ? toScene(3) : toScene(4); - } - return toScene(index + 1); - } - - if (scene === 5) { - // 마지막 스테이지를 마치면 결과 화면으로 나간다 - if (index === 2) return { type: "navigate", to: ROUTES.RESULT }; - return toScene(index + 1); - } - - return toScene(index + 1); + // 규칙이 없는 씬은 순서대로 진행한다. + // 씬 인덱스가 데이터 길이를 넘으면 컴포넌트가 "다음 스테이지로" 버튼을 띄운다. + return { type: "scene", index: index + 1 }; }; /** @@ -76,16 +61,19 @@ export const resolveNextStep = (scene, index, score) => { * * @param {number} scene 스테이지 번호 * @param {number} choiceIndex 고른 선택지 인덱스 - * @param {string} defaultRoute 분기가 없는 스테이지의 기본 목적지 + * @param {string} defaultRoute 분기 규칙이 없는 스테이지의 기본 목적지 * @returns {string} 다음 스테이지 경로 */ export const resolveStageExit = (scene, choiceIndex, defaultRoute) => { - if (scene === 2) { - return choiceIndex === 0 ? ROUTES.MAIN4 : ROUTES.MAIN3; - } - return defaultRoute; + const exitRule = stageRules.stages?.[String(scene)]?.stageExit; + if (!exitRule) return defaultRoute; + + return exitRule.byChoice?.[String(choiceIndex)] ?? exitRule.default ?? defaultRoute; }; -const storyFlow = { STAGE_END, resolveNextStep, resolveStageExit }; +/** 결과 화면 경로. 데이터의 exit 값과 일치해야 한다 */ +export const RESULT_ROUTE = ROUTES.RESULT; + +const storyFlow = { resolveNextStep, resolveStageExit, RESULT_ROUTE }; export default storyFlow; diff --git a/version.yml b/version.yml index aece267..a0b2d80 100644 --- a/version.yml +++ b/version.yml @@ -6,7 +6,7 @@ # GitHub Actions 워크플로우가 이 파일을 읽어 자동으로 버전을 관리 # # 사용법: -# 1. version: "1.4.15" +# 1. version: "1.4.16" # 2. project_type: 프로젝트 타입 지정 # # 자동 버전 업데이트: @@ -15,7 +15,7 @@ # # 프로젝트 타입별 동기화 파일: # - spring: build.gradle (version = "x.y.z") -# - flutter: pubspec.yaml (version: "1.4.15" +# - flutter: pubspec.yaml (version: "1.4.16" # - react/node: package.json ("version": "x.y.z") # - react-native: iOS Info.plist 또는 Android build.gradle # - react-native-expo: app.json (expo.version) @@ -26,8 +26,8 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.4.15" +version: "1.4.16" project_type: react # spring, flutter, react, react-native, react-native-expo, node, basic metadata: - last_updated: "2026-08-08 12:44:58" + last_updated: "2026-08-08 13:26:43" last_updated_by: "Cassiiopeia"