docs: add rendered developer and player handbook

This commit is contained in:
2026-07-28 23:59:52 +00:00
parent 1181f6f4e0
commit bf3f73708e
18 changed files with 2216 additions and 3 deletions
+83
View File
@@ -0,0 +1,83 @@
# 파일 지도와 변경 절차
## 최상위 지도
```text
core2026/
├─ app/
│ ├─ gateway-frontend/ 계정·로비·관리 UI
│ ├─ gateway-api/ 인증·profile·operation·orchestrator
│ ├─ game-frontend/ profile 게임 SPA
│ ├─ game-api/ tRPC/SSE와 mutation 수신
│ └─ game-engine/ turn daemon과 persistence orchestration
├─ packages/
│ ├─ common/ 공통 타입·직렬화·RNG
│ ├─ logic/ 명령·constraint·전투·trigger·scenario
│ ├─ infra/ Prisma·PostgreSQL·Redis
│ └─ tools-scripts/ resource schema 도구
├─ resources/ scenario·map·unitset·명령 profile
├─ tools/
│ ├─ integration-tests/ DB/Redis 및 ref 차등
│ ├─ frontend-legacy-parity/ 실제 Chromium 비교
│ ├─ legacy-db-migration/ 장기보존 데이터 CLI
│ ├─ build-scripts/ profile resource 복사 기반 build 도구
│ └─ docs/ 문서 생성 도구
└─ docs/ VitePress 소스와 상세 설계 문서
```
## 기능에서 파일로
| 기능 | 시작점 | 핵심 하위 경계 |
| -------------- | --------------------------------------------- | ------------------------------------- |
| 로그인·session | `gateway-api/src/router.ts` | `auth/*`, `account/router.ts`, Redis |
| profile 운영 | `gateway-api/src/adminRouter.ts` | `orchestrator/*`, gateway Prisma |
| 게임 인증 | `game-api/src/context.ts`, `router/auth` | session actor, profile·sanction |
| 메인 턴 입력 | `game-frontend/src/stores/mainDashboard.ts` | `router/turns`, `turns/*` |
| command 실행 | `game-engine/src/turn/reservedTurnHandler.ts` | `packages/logic/src/actions/turn` |
| 월간 lifecycle | `game-engine/src/turn/turnDaemon.ts` | `monthly*Handler.ts`, scenario events |
| 전투 | `actions/turn/general/che_출병.ts` | `packages/logic/src/war` |
| DB load/flush | `game-engine/src/turn/worldLoader.ts` | `databaseHooks.ts`, `packages/infra` |
| 공개/국가 정보 | `game-api/src/router/public`, `router/nation` | DTO와 redaction |
| 화면 parity | `game-frontend/src/views`, `styles` | `tools/frontend-legacy-parity` |
## 변경 절차
### API나 화면
router의 input, auth procedure, transaction과 response를 먼저 정한 뒤 frontend 호출부와 오류·loading 상태를
연결합니다. public prefix에서 direct navigation, asset, tRPC와 SSE URL을 확인합니다. UI를 바꾸면 실제
Chromium에서 ref와 geometry·computed style·interaction을 비교합니다.
### 도메인 로직
ref entry point부터 SQL·로그까지 호출 순서를 찾고, `packages/logic` 계산과 engine context/persistence를 함께
수정합니다. unit test만으로 끝내지 않고 fixed seed, 전체 state, RNG trace, 실패 side effect와 가능한
ref 차등을 확인합니다.
### DB
기존 migration을 고치지 않고 새 migration을 만듭니다. 빈 DB 전체 적용, 기존 DB 증분 적용, 재실행 no-op,
constraint/index와 runtime query, backup/restore 또는 rollback 경로를 확인합니다.
## 문서 사이트
```sh
pnpm install
pnpm docs:generate
pnpm docs:dev
pnpm docs:build
pnpm docs:preview
```
`docs:dev``docs:build`는 먼저 커맨드 목록을 생성합니다. 정적 결과는 `docs/.vitepress/dist`에 생기며
Git에 포함하지 않습니다. 문서만 바꿔도 Prettier, 생성 결과의 clean diff, VitePress build와 내부 링크를
검증해 주세요.
## 리팩터링 체크포인트
- [문서 기준선](../reference-baseline.md)과 현재 commit의 diff를 먼저 봅니다.
- 파일 이동만 했는지 소유권·transaction·호출 순서까지 바뀌었는지 구분합니다.
- public API, DB schema, action key와 resource format은 내부 파일명보다 강한 계약입니다.
- `rg`로 이 페이지의 이전 경로가 남았는지 확인합니다.
- command key를 바꿨다면 저장된 예약 턴과 profile resource의 migration/호환 경로가 필요합니다.
- 문서의 광범위한 “완료” 표현은 관련 integration·ref 차등·Chromium 증거가 있을 때만 사용합니다.
+106
View File
@@ -0,0 +1,106 @@
# 도메인 로직과 핵심 클래스
## 핵심 entity
`packages/logic/src/domain/entities.ts`가 HTTP나 Prisma row에 종속되지 않은 `General`, `City`, `Nation`,
`Troop`, diplomacy와 trigger 상태를 정의합니다. engine 전용 `TurnGeneral`, `TurnWorldState`,
`TurnEvent``app/game-engine/src/turn/types.ts`에서 실행 시간·예약 턴·월간 상태를 더합니다.
| entity | 핵심 책임 |
| ------- | -------------------------------------------------------------------------- |
| General | 능력치, 경험·공헌, 소속·도시·부대, 병력·훈련·사기, 자원, 특기·아이템, meta |
| City | 소유 국가, 규모, 인구·농업·상업·치안, 수비·성벽, 보급·전선 상태 |
| Nation | 수도, 국고·군량, 등급·국가 타입, 기술과 국가 meta |
| Troop | 부대장·구성원과 부대 상태 |
| World | 현재 연·월, 최근 턴 시각, scenario config/meta와 전체 entity collection |
Prisma row를 곧바로 게임 규칙에 넘기지 않습니다. `worldLoader.ts`와 API의 row mapper가 DB 표현을 domain
표현으로 바꾸고, flush 계층이 반대 변환을 담당합니다.
## 명령 정의
`GeneralActionDefinition`은 장수·국가 예약 명령이 공유하는 계약입니다.
- `key`, `name`: 저장 key와 화면 표시명
- `parseArgs`: 외부 입력을 실행 인자로 변환
- `buildPermissionConstraints`: 예약 입력 자체를 허용할지 판단
- `buildMinConstraints`: command table에서 현재 가능한지 사전 판단
- `buildConstraints`: 실행 시점의 전체 조건
- `getPreReqTurn`, `getPostReqTurn`: 연속 실행과 재사용 대기
- `resolve`: domain state와 effect를 계산
각 파일의 `commandSpec`은 category, 인자 필요 여부, schema와 definition factory를 등록합니다.
`GENERAL_TURN_COMMAND_KEYS`, `NATION_TURN_COMMAND_KEYS`가 전체 key 집합이며, `TurnCommandProfile`
profile별 subset을 선택합니다.
## Constraint 시스템
`packages/logic/src/constraints`는 “무엇이 필요한가”와 “현재 view가 무엇을 알고 있는가”를 분리합니다.
`ConstraintContext`에는 actor, city, nation, args, env와 평가 mode가 있고 `StateView`가 entity와 대상
정보를 제공합니다.
평가 결과는 다음 셋입니다.
- `allow`: 현재 정보로 조건을 만족합니다.
- `deny`: 이유가 확정된 실패입니다.
- `unknown`: 대상 입력이나 추가 state가 없어 아직 판정할 수 없습니다.
API command table은 `unknown`의 missing requirement가 대상 입력뿐이면 `needsInput`, 그 밖이면
`unknown`으로 보여 줍니다. 예약 뒤 실제 실행에서는 전체 context로 다시 판단합니다.
## 핵심 클래스와 조립 지점
### TurnDaemonLifecycle
`app/game-engine/src/lifecycle/turnDaemonLifecycle.ts`에 있습니다. clock, control queue, hook과 run budget을
조정하며 pause/resume/manual/scheduled run의 상태 전이를 소유합니다.
### DatabaseTurnDaemonLease
`app/game-engine/src/lifecycle/databaseTurnDaemonLease.ts`에 있습니다. profile별 단일 active owner와 fencing을
관리합니다. daemon 계산이 맞아도 lease를 잃었다면 결과를 저장하면 안 됩니다.
### InMemoryTurnWorld
`app/game-engine/src/turn/inMemoryWorld.ts`에 있습니다. entity map, dirty/create/delete set, log, message,
event, checkpoint와 월 변경을 소유합니다. `peekDirtyState()`는 저장할 변경을 보여 주고 성공한 flush 뒤
정리됩니다.
### EngineStateManager
`app/game-engine/src/turn/engineStateManager.ts`에 있습니다. world와 예약 턴 store 같은 mutable participant를
등록하고 계산 단위의 capture/restore/transaction을 제공합니다. PostgreSQL transaction을 대신하지 않고
실패한 계산의 메모리 rollback을 담당합니다.
### InMemoryReservedTurnStore와 ReservedTurnHandler
`reservedTurnStore.ts`는 장수 30칸·국가 12칸 예약 queue를 메모리에 유지합니다.
`reservedTurnHandler.ts`는 명령 loading, constraint, action context, AI fallback, progress/cooldown,
효과·로그와 queue rotation을 연결합니다.
### GeneralActionPipeline과 trigger module
`packages/logic/src/actions/engine.ts`, `triggers/*`는 명령 본체 전후의 특기·아이템·국가 특성 효과를
일정한 우선순위로 적용합니다. 같은 module 목록이라도 실행 순서가 결과와 RNG 소비를 바꿀 수 있습니다.
### WarEngine
`packages/logic/src/war/engine.ts`가 전투 resolution을, `war/actions.ts`와 trigger module이 확장 효과를,
`war/aftermath.ts`가 피해·점령·외교·후속 state를 계산합니다. `che_출병.ts`가 map, unit set, diplomacy,
time, seed와 aftermath를 조립하는 실제 장수 명령 entry입니다.
### GatewayOrchestrator
`app/gateway-api/src/orchestrator/gatewayOrchestrator.ts`가 DB의 profile desired state를 process state에
맞춥니다. `workspaceManager.ts`, `buildRunner.ts`, `seedProfileDatabase.ts`, `pm2ProcessManager.ts`
commit worktree 준비부터 build, seed, start/stop을 나눕니다.
## 새 명령을 추가할 때
1. 가장 가까운 ref command의 constraint, 실행 순서, RNG, 로그와 DB side effect를 조사합니다.
2. `packages/logic/src/actions/turn/{general,nation}`에 definition과 `commandSpec`을 작성합니다.
3. 해당 `*_TURN_COMMAND_KEYS`와 필요한 `resources/turn-commands` profile에 key를 등록합니다.
4. 인자가 있으면 Zod schema와 `app/game-api/src/turns/commandInput.ts`의 화면 입력 field를 연결합니다.
5. engine action context가 대상 entity·map·unit set·시간·seed를 완전하게 공급하는지 확인합니다.
6. permission/min/full 실패, 성공, 연속 턴, cooldown과 persistence를 테스트합니다.
7. `pnpm docs:generate`로 플레이어 커맨드 목록을 갱신하고 ref 매핑 문서를 함께 수정합니다.
+29
View File
@@ -0,0 +1,29 @@
# 개발자 핸드북
이 핸드북은 새 기능을 어디에 넣을지뿐 아니라 요청이 어떤 경계를 지나 상태로 남는지 설명합니다. 먼저
[문서 기준선](../reference-baseline.md)을 확인하고, 변경 성격에 따라 다음 순서로 읽어 주세요.
| 변경하려는 것 | 먼저 읽을 문서 | 주로 확인할 코드 |
| -------------------------- | ---------------------------------------------------- | -------------------------------------------- |
| 화면·라우팅·조회 API | [시스템 아키텍처](./system-architecture.md) | `app/*-frontend`, `app/*-api` |
| 턴 입력·게임 상태 mutation | [요청·턴·저장 흐름](./request-turn-persistence.md) | `app/game-api`, `app/game-engine` |
| 명령·전투·월간 로직 | [도메인 로직과 핵심 클래스](./domain-and-classes.md) | `packages/logic`, `app/game-engine/src/turn` |
| 새 파일 위치·검증 범위 | [파일 지도와 변경 절차](./code-map.md) | package manifest, test, docs |
## 읽을 때 지켜야 할 경계
- `packages/logic`의 순수 계산과 `app/game-engine`의 scheduling·persistence orchestration을 구분합니다.
- game API가 mutation을 받는 것과 engine이 world mutation을 확정하는 것은 다른 단계입니다.
- PostgreSQL `input_event`가 내구성 있는 작업 경계이며 Redis는 realtime fan-out과 일부 보조 worker
통신에 사용됩니다.
- 로그인한 사용자, 게임 장수, 국가 직책은 같은 개념이 아닙니다. actor와 소유권은 session에서
서버가 결정합니다.
- `resources/`의 scenario·map·unit set·turn-command profile이 런타임 구성을 바꿉니다. 기본 TypeScript
목록만 보고 실제 profile을 단정하지 않습니다.
- ref 호환 변경은 결과뿐 아니라 판정·정렬·반올림·RNG 소비·로그·저장 순서를 비교합니다.
## 기존 상세 문서와의 관계
이 핸드북은 탐색용 상위 지도입니다. 상세한 호환 근거와 테스트 절차는 `docs/architecture/*`,
`docs/integration-tests.md`, `docs/frontend-legacy-parity.md`에 유지합니다. 상위 작업공간의
`../docs/ref-core2026-mapping.md`는 ref entry point와 core 구현의 end-to-end 대응 인덱스입니다.
@@ -0,0 +1,99 @@
# 요청·턴·저장 흐름
## 조회 요청
일반 query는 다음 경로를 따릅니다.
```text
Vue view/store
-> tRPC client
-> game-api router
-> session actor + 입력 validation
-> Prisma query
-> 권한에 맞춘 DTO/redaction
-> 화면 상태
```
조회는 engine의 in-memory object를 직접 공유하지 않습니다. 따라서 daemon이 transaction을 commit하기 전의
중간 계산은 API query에 노출되지 않습니다.
## API가 직접 끝내는 mutation
예약 턴, 메시지처럼 API가 DB에서 완결할 수 있는 변경도 `input_event`를 사용합니다.
1. `Idempotency-Key`와 tRPC path로 요청 identity를 만듭니다.
2. `app/game-api/src/inputEventBoundary.ts`가 중복·처리 상태를 확인합니다.
3. 같은 PostgreSQL transaction에서 대상 row와 input event 결과를 저장합니다.
4. commit 뒤 응답하고 필요한 realtime 갱신을 알립니다.
동일 revision을 전제로 한 예약 턴 수정은 다른 탭이나 요청이 먼저 갱신했으면 충돌합니다. frontend는 최신
목록을 다시 불러와 사용자의 변경을 덮어쓰지 않게 해야 합니다.
## engine이 처리하는 mutation
```text
game-api mutation
-> input_event PENDING
-> daemon claim (FOR UPDATE SKIP LOCKED)
-> lease/fencing 확인
-> EngineStateManager savepoint
-> command/turn/monthly handler가 InMemoryTurnWorld 변경
-> world + 예약 턴 + log + message + event 결과 flush
-> input_event COMPLETED를 같은 DB transaction으로 commit
-> commit 이후 realtime 신호
```
계산이나 DB 쓰기가 실패하면 `EngineStateManager`가 등록된 in-memory participant를 savepoint로 되돌립니다.
DB transaction도 commit되지 않아 메모리와 DB의 부분 진행을 피합니다. lease를 잃은 worker는 stale 결과를
commit할 수 없어야 합니다.
## 한 tick의 처리
`TurnDaemonLifecycle`은 clock과 schedule에서 다음 실행 시점을 구합니다. 턴을 시작하면
`InMemoryTurnProcessor``InMemoryTurnWorld``turnTime`, 그다음 `general.id` 순서로 실행 대상을
결정합니다. checkpoint는 재시작 시 이미 처리한 동일 시점의 장수를 건너뛰는 기준입니다.
장수 한 명의 예약 명령은 대략 다음 순서입니다.
1. `InMemoryReservedTurnStore`에서 첫 예약 명령을 읽습니다.
2. 명령 key를 `GeneralTurnCommandLoader` 또는 `NationTurnCommandLoader`로 불러옵니다.
3. `actionContextBuilder`가 대상 도시·국가·장수, map, unit set, 외교, 시간과 RNG를 구성합니다.
4. permission/min/full constraint를 목적에 맞게 평가합니다.
5. 선행 턴이 있으면 진행 상태를 쌓고, 완성된 시점에 `resolve()`를 실행합니다.
6. effect와 직접 변경을 world에 반영하고 로그·메시지·후속 턴 시간을 기록합니다.
7. 실행된 queue를 당기고 끝에 기본 `휴식`을 채웁니다.
예약 시 통과와 실행 시 성공은 같지 않습니다. 그 사이 자원, 도시 소유, 외교, 직책이 바뀔 수 있으므로 full
constraint는 실행 순간 다시 평가됩니다.
## 월 변경 경계
`InMemoryTurnWorld.advanceMonth()`는 다음 순서를 보존합니다.
1. 다음 연·월을 계산합니다.
2. `beforeMonthChanged` handler를 등록 순서대로 실행합니다.
3. world의 현재 연·월을 바꿉니다.
4. `onMonthChanged` handler를 등록 순서대로 실행합니다.
5. 연도가 바뀌었으면 `onYearChanged`를 실행합니다.
`turnDaemon.ts``composeCalendarHandlers()` 순서에는 월간 event, 수입, 연감, PRE_MONTH 상태 정리,
국가 명령, 국가 통계, 외교, 전쟁 설정, 방랑, 국가 수, 통일, 토너먼트, 경매, 전선 상태가 포함됩니다.
이 순서는 ref의 관찰 가능한 결과와 RNG·persistence에 영향을 주므로 리팩터링 시 단순 정렬하지 않습니다.
## RNG 경계
게임 결과에 영향을 주는 난수는 `LiteHashDRBG``RandUtil`을 사용합니다. seed에는 hidden seed와
action/month/general 같은 context가 직렬화됩니다. main RNG의 소비 순서를 유지해야 하는 로직과 독립된
재현 가능 substream을 써야 하는 fallback을 구분합니다. authoritative path에 `Math.random()`을 넣지
않습니다.
## 장애를 추적할 위치
| 증상 | 우선 확인 |
| ----------------------------- | --------------------------------------------------------------------- |
| 같은 mutation이 두 번 보임 | idempotency key, `input_event` 상태·attempt |
| 요청은 성공했지만 화면이 늦음 | DB commit 결과, Redis/SSE fan-out |
| daemon이 처리하지 않음 | profile gate, pause 상태, lease owner, PENDING claim |
| 재시작 뒤 일부 턴 반복 | checkpoint와 general turn ordering |
| DB와 메모리가 다름 | `EngineStateManager`, `databaseHooks`, flush 대상 누락 |
| ref와 결과가 다름 | constraint 순서, action context, RNG trace, rounding, log/effect 순서 |
+99
View File
@@ -0,0 +1,99 @@
# 시스템 아키텍처
## 런타임 구성
```text
브라우저
├─ /gateway/ ─ gateway-frontend ─ tRPC ─ gateway-api
│ ├─ PostgreSQL public schema
│ ├─ Redis session
│ └─ orchestrator ─ Git worktree / build / PM2
└─ /{profile}/ ─ game-frontend ─ tRPC/SSE ─ game-api
├─ profile별 PostgreSQL schema
├─ Redis realtime/battle worker
└─ input_event ─ game-engine
├─ in-memory world
└─ transactional flush
```
Gateway는 계정·session·profile lifecycle을, game 계층은 한 profile의 플레이와 턴 진행을 소유합니다.
`/gateway`, `/che`, `/hwe` 같은 외부 prefix와 `/image/*`의 Caddy 소유권은 애플리케이션 밖의 배포
계약입니다.
## 애플리케이션
### gateway-frontend
`app/gateway-frontend/src/main.ts`가 Vue 앱과 router를 시작합니다. 가입·로그인·계정·로비·관리자 화면은
`src/views`에 있고, profile 선택 뒤 game frontend로 이동합니다. 브라우저에 보이는 `VITE_*` 값은
공개 설정이며 secret이 아닙니다.
### gateway-api
`app/gateway-api/src/server.ts``router.ts`가 HTTP/tRPC 경계입니다.
- `auth/*`, `account/router.ts`: Kakao·로컬 계정, session 발급·폐기, 사용자 저장소
- `lobby/profileStatusService.ts`: 사용자에게 보여 줄 profile 상태
- `adminRouter.ts`, `adminAuth.ts`: 관리자 권한과 operation 입력
- `orchestrator/*`: 원하는 profile 상태를 Git worktree, build, seed, PM2 프로세스에 반영
Gateway DB는 기본 `public` schema를 사용합니다. game profile DB를 직접 게임 로직의 source of truth로
대체하지 않습니다.
### game-frontend
`app/game-frontend/src/main.ts`가 profile base path 아래 Vue SPA를 시작합니다. `src/views`가 공개 정보,
메인 턴 입력, 국가 운영, 경매·토너먼트·기록 화면을 나누고 `src/stores/mainDashboard.ts`가 메인 화면의
query, 예약 턴 revision, mutation과 realtime refresh를 조정합니다.
UI는 서버가 반환한 command table의 `available`, `blocked`, `needsInput`, `unknown` 상태를 사용합니다.
클라이언트가 장수 ID나 직책을 보냈다는 이유만으로 권한이 생기지 않습니다.
### game-api
`app/game-api/src/server.ts``router.ts`가 query/mutation을 공개합니다. router는 기능 단위로
`src/router/*`에 나뉩니다.
- 읽기: world, public, directory, ranking, yearbook, dynasty 등은 권한·redaction을 거쳐 DB에서 조회합니다.
- 플레이: turns, join, nation, troop, diplomacy, messages, auction, betting, tournament 등이 있습니다.
- mutation: `inputEventBoundary.ts`가 idempotency와 PostgreSQL 작업 경계를 만듭니다.
- realtime: SSE와 Redis 알림은 commit 이후 화면 갱신 신호입니다.
### game-engine
`app/game-engine/src/index.ts`가 daemon entry point이고 `src/turn/turnDaemon.ts`가 profile resource,
world snapshot, command registry, calendar handler, persistence hook과 lease를 조립합니다.
한 daemon owner가 `TurnDaemonLifecycle`을 통해 정해진 tick과 durable inbox를 처리합니다. 실제 장수·도시·국가
상태는 `InMemoryTurnWorld`에서 계산하고, 성공한 작업만 `databaseHooks.ts`를 통해 PostgreSQL에 flush합니다.
lease/fencing은 오래된 daemon owner의 commit을 막습니다.
## 공유 package
| package | 책임 | 넣지 말아야 할 것 |
| ------------------------ | ------------------------------------------------------------------------------- | ---------------------------------- |
| `packages/common` | 공통 type, 직렬화, `LiteHashDRBG`, `RandUtil`, session/sanction 유틸리티 | profile DB orchestration |
| `packages/logic` | entity, constraint, command, battle, trigger, scenario parsing 같은 도메인 계산 | HTTP·Vue·Prisma transaction 소유권 |
| `packages/infra` | Prisma client, PostgreSQL·Redis connector, log와 turn-engine DB adapter | 게임 규칙 결정 |
| `packages/tools-scripts` | resource schema 생성·검증 | 런타임 요청 처리 |
## 데이터와 구성
- `packages/infra/prisma/schema.gateway.prisma`: 계정·profile·operation 같은 gateway 모델
- `packages/infra/prisma/schema.game.prisma`: profile별 world·general·city·nation·turn·event·log 모델
- `resources/scenario`: 시작 연도, 상수, 월간 event 등 scenario 정의
- `resources/map`, `resources/unitset`: 지형과 병종 정의
- `resources/turn-commands`: profile별 허용 명령 목록
새 persistence field는 schema와 새 migration만으로 끝나지 않습니다. domain type, loader, in-memory dirty
tracking, transaction flush, reload 검증까지 연결해야 합니다.
## 인증·권한 모델
Gateway session은 사용자 identity를, game token은 profile과 게임 역할을 전달합니다. game API는 session으로
내 장수를 조회한 뒤 그 장수의 국가·직책·sanction과 대상 resource의 관계를 판단합니다. 공개 endpoint도
비공개 국가 정보, 타 사용자 archive, 비밀 명령을 DTO에서 제거해야 합니다.
권한 변경을 검증할 때는 무인증, 일반 사용자, 본인, 같은 국가, 다른 국가, NPC, 직책 보유자, sanction
적용자를 필요한 범위에서 나눕니다. 거부된 mutation은 world와 queue에 side effect를 남기지 않아야 합니다.