독자 수준별 구조 문서와 게이머 입문·전술 안내를 재정비한다

This commit is contained in:
2026-09-29 08:53:30 +00:00
parent 71155186b7
commit 08c3d0e936
34 changed files with 1175 additions and 271 deletions
+6 -1
View File
@@ -1,5 +1,9 @@
# 파일 지도
먼저 증상이 보이는 화면이나 동작 하나를 고르고, 아래 시작점에서 호출을 따라가세요.
폴더 이름만으로 저장 책임이나 권한을 추측하지 않습니다. 읽는 순서는
[기초 안내](./first-steps.md)와 [경험자 안내](./system-walkthrough.md)에 있습니다.
## 최상위
```text
@@ -9,7 +13,8 @@ core2026/
│ ├─ gateway-api/
│ ├─ game-frontend/
│ ├─ game-api/
│ └─ game-engine/
│ ├─ game-engine/
│ └─ release-controller/
├─ packages/
│ ├─ common/
│ ├─ logic/
+34 -16
View File
@@ -1,16 +1,33 @@
# 도메인과 조립 지점
**도메인**은 장수·도시·전투처럼 이 게임이 다루는 개념과 규칙입니다. 이 문서는
객체 지향 문법을 설명하기보다 “규칙을 누가 계산하고 결과를 누가 적용하는가”를
따라갑니다. [기초 안내](./first-steps.md)의 모병 예시를 떠올리면 좋습니다.
```text
저장된 장수·도시 → loader → 메모리 세계
예약한 명령 → 입력 해석 → 조건 검사 → 규칙 계산 → 변경 결과
변경 결과 → 메모리 세계에 적용 → DB 저장 또는 실패 시 복원
```
## World entity
`packages/logic/src/domain/entities.ts`와 `world/types.ts`가 장수, 국가, 도시,
부대, 외교와 trigger state의 런타임 타입을 정의합니다. Prisma row는
`app/game-engine/src/turn/worldLoader.ts`가 이 타입으로 변환합니다.
`InMemoryTurnWorld`가 조회와 mutation을 제공하고 `EngineStateManager`가
transaction snapshot과 dirty state를 관리합니다.
메모리 snapshot과 실패 복원을 관리합니다. 실제 DB transaction은
`databaseHooks.ts`의 책임입니다. **dirty state**는 마지막 저장 이후 바뀌어 다시
저장해야 하는 상태를 뜻합니다.
## Command
장수 command는 `GeneralActionDefinition`으로 다음 계약을 가집니다.
명령(command)은 “모병” 같은 행동 한 종류입니다. 입력(args)은 병종·수량처럼
그 행동에 필요한 값입니다. **constraint**는 실행에 필요한 조건이고,
**state patch**는 병력·자원 등 바꿀 값의 묶음입니다. 실행 결과에는 patch뿐 아니라
플레이어에게 보일 로그와 후속 효과도 포함됩니다.
장수 command는 definition·command spec·resolver를 통해 다음 계약을 연결합니다.
- key와 사용자 표시 이름
- raw args parser
@@ -38,25 +55,26 @@ turn 소비와 side effect 계약을 보존합니다.
priority trigger와 의미 event는 각각 다른 interface를 사용합니다.
[행동 모듈 프로토콜](../architecture/action-module-protocol.md)을 따라 주세요.
## 주요 클래스
## 주요 클래스와 함수
| 클래스·함수 | 책임 |
| ------------------------- | -------------------------------------------------- |
| `GatewayOrchestrator` | profile operation과 process reconciliation |
| `createGameApiServer` | game transport, context, router와 worker lifecycle |
| `DatabaseTurnDaemonLease` | profile별 lease, heartbeat와 fencing |
| `TurnDaemonLifecycle` | schedule, pause/resume/run/shutdown loop |
| `InMemoryTurnWorld` | turn 실행 중 world state와 dirty tracking |
| `EngineStateManager` | snapshot, transaction flush와 rollback restore |
| `ReservedTurnHandler` | revision·lease 기반 예약 명령 claim과 실행 |
| `GeneralActionPipeline` | action module 계산·trigger·event 실행 |
| `WarEngine` | 전투 phase, RNG, 상태와 log 결과 |
| 클래스·함수 | 책임 |
| ----------------------------- | -------------------------------------------------- |
| `GatewayOrchestrator` | profile operation과 process reconciliation |
| `createGameApiServer` | game transport, context, router와 worker lifecycle |
| `DatabaseTurnDaemonLease` | profile별 lease, heartbeat와 fencing |
| `TurnDaemonLifecycle` | schedule, pause/resume/run/shutdown loop |
| `InMemoryTurnWorld` | turn 실행 중 world state와 dirty tracking |
| `EngineStateManager` | 메모리 snapshot, 실패 시 restore |
| `createReservedTurnHandler()` | revision·lease 기반 예약 명령 claim과 실행 |
| `GeneralActionPipeline` | action module 계산·trigger·event 실행 |
| `resolveWarBattle()` | 전투 phase, RNG, 상태와 log 결과 |
## 명령 추가
1. ref command의 예약·실행 constraint, args, RNG, log와 DB mutation을 찾습니다.
1. 계승 명령이면 Ref의 예약·실행 조건, 입력, RNG, 로그와 DB 변경을 찾습니다.
Core 전용 명령이면 요구사항과 의도한 결과를 먼저 정합니다.
2. command definition과 필요한 domain helper를 추가합니다.
3. engine registry, profile resource와 frontend args UI를 연결합니다.
4. state patch와 dirty field가 flush·reload되는지 확인합니다.
5. 정상·실패·경계 fixed-seed test와 ref 차등 fixture를 추가합니다.
5. 정상·실패·경계 fixed-seed test를 추가하고, 계승 계약은 Ref 차등 fixture로 확인합니다.
6. 생성 command catalog, 상위 mapping과 report를 갱신합니다.
+106
View File
@@ -0,0 +1,106 @@
# 코드가 처음인 사람을 위한 구조 안내
함수, 변수, 조건문은 알지만 웹 서버나 큰 프로젝트는 처음인 독자를 위한 글입니다.
이 문서를 읽고 나면 화면을 바꿀 파일, 게임 규칙을 바꿀 파일, 결과를 저장할 파일을
구분할 수 있습니다. 설치 전에 읽어도 됩니다.
## 먼저 게임을 한 문장으로 이해하기
플레이어는 장수 한 명의 행동을 미리 예약합니다. 서버는 장수의 차례가 되면
행동을 실행하고, 자원·도시·전투 결과를 저장합니다. 여러 플레이어와 컴퓨터 장수가
같은 세계에 참여하므로, 브라우저를 닫아도 세계는 계속 진행될 수 있습니다.
게임 자체가 낯설다면 [플레이어 첫 안내](../user/index.md)를 먼저 읽으세요.
## 화면과 서버는 다른 프로그램입니다
**프론트엔드**는 브라우저에서 실행되어 화면을 그리고 버튼 입력을 받는 프로그램입니다.
**백엔드**는 서버에서 실행되어 로그인과 권한을 확인하고 데이터를 읽거나 바꾸는
프로그램입니다. 버튼을 눌렀다고 브라우저가 직접 병력을 늘리는 것은 아닙니다.
브라우저는 서버에 요청하고, 서버가 허용한 결과를 받아 표시합니다.
**API**는 두 프로그램이 요청과 응답을 주고받는 약속입니다. 이 프로젝트는
TypeScript 타입으로 호출 형태를 연결하는 **tRPC**를 사용합니다. 타입이 맞더라도
사용자 권한과 실제 자원은 서버가 다시 검사해야 합니다.
예를 들어 “모병을 예약”하는 과정은 다음과 같습니다.
```text
브라우저: 병종과 수량을 고른다
↓ 저장 요청
게임 API: 로그인과 입력을 검사하고 예약 변경을 처리한다
↓ 예약 목록에 반영
턴 데몬: 내 차례가 되면 모병 조건을 다시 검사한다
↓ 실행 결과
데이터베이스: 병력, 자원, 로그와 남은 예약을 저장한다
↓ 알림을 받고 다시 조회
브라우저: 실제 결과를 보여 준다
```
예약 저장과 모병 실행은 서로 다른 일입니다. 예약 이후 돈을 쓰거나 도시를
잃었다면 실행에 실패할 수 있습니다. API를 읽을 때도 “요청을 받았다”와
“게임 행동이 끝났다”를 구분해야 합니다.
## 다섯 곳을 먼저 익히세요
| 위치 | 역할 | 찾아볼 때 |
| ------------------- | ------------------------------------- | -------------------------------------------- |
| `app/game-frontend` | Vue 화면과 사용자 입력 | 예턴 입력창이나 도시 화면을 이해하고 싶을 때 |
| `app/game-api` | 로그인·권한 검사, 조회·변경 요청 접수 | 누가 어떤 정보를 볼 수 있는지 궁금할 때 |
| `app/game-engine` | 시간에 맞춘 실행, 세계 상태와 저장 | 예약한 행동이 언제 실행되는지 궁금할 때 |
| `packages/logic` | 모병·내정·전투 등 게임 규칙 | 비용이나 성공 조건을 찾을 때 |
| `packages/infra` | 데이터베이스 구조와 연결 | 무엇을 영구히 저장하는지 찾을 때 |
`app`은 실행하는 프로그램, `packages`는 여러 프로그램이 함께 쓰는 코드 묶음입니다.
한 저장소에 여러 프로그램과 묶음이 있는 구성을 **모노레포**라고 합니다.
`pnpm`은 이 묶음들의 설치와 명령 실행을 관리합니다.
`resources`에는 지도, 병종, 시나리오 설정이 있습니다. 규칙 코드가 같아도 선택한
설정이 다르면 다른 게임이 됩니다. `tools`에는 검사·변환 같은 개발 도구가,
`docs`에는 지금 읽고 있는 문서가 있습니다.
## 로비와 게임을 나누는 이유
**Gateway**는 회원 계정과 로비를 담당합니다. 하나의 계정으로 여러 게임 서버에
들어갈 수 있지만, 각 게임의 장수·국가·진행 시점은 다릅니다. 운영 대상 게임을
코드에서는 **프로필(profile)**이라고 부릅니다. 프로필은 개인 프로필 사진과 다른
뜻입니다. **시나리오**는 그 게임에 적용할 시작 조건·지도·규칙 묶음입니다.
Gateway의 화면과 API는 `app/gateway-frontend`, `app/gateway-api`에 있습니다.
게임 업데이트와 실행 관리도 필요하므로 운영용 프로그램이 추가로 있습니다.
처음부터 이들을 모두 읽을 필요는 없습니다. [전체 구조](../architecture/overview.md)를
읽을 때 게임 진행 경로와 운영 경로를 나눠 보세요.
## 데이터는 어디에 남나요?
**메모리**는 실행 중인 프로그램이 빠르게 사용하는 작업 공간입니다. 프로그램을
다시 시작하면 사라질 수 있습니다. **PostgreSQL**은 세계 상태와 기록을 영구히
보관하는 데이터베이스입니다. **Prisma**는 코드에서 데이터베이스를 다루기 위한
도구이며, `packages/infra/prisma/game.prisma`는 게임 데이터 구조를 정의합니다.
턴 데몬은 세계를 메모리에 읽어 와 계산합니다. 계산이 끝나면 바뀐 값과 로그를
**트랜잭션(transaction)**으로 묶어 저장합니다. 모병으로 돈만 줄고 병력은 늘지
않는 식의 반쪽 저장을 막기 위한 경계입니다. 저장에 실패하면 메모리 상태도
되돌려야 다음 행동이 잘못된 세계에서 실행되지 않습니다.
**Redis**는 세션이나 작업 전달·알림 등에 쓰는 별도의 저장·통신 도구입니다.
게임 진행이 확정됐는지는 PostgreSQL 저장 결과를 기준으로 판단합니다.
**SSE**는 서버가 열린 브라우저로 변경 알림을 보내는 연결입니다. 알림이 늦었다고
곧바로 행동이 실패한 것은 아니므로 최신 데이터를 다시 조회합니다.
## 파일 하나에서 시작하는 읽기 연습
1. `packages/logic/src/actions/turn/general/che_모병.ts`를 엽니다.
입력값, 조건, 결과를 나눠 봅니다. 지금은 모든 계산을 외우지 않아도 됩니다.
2. `app/game-engine/src/turn/reservedTurnHandler.ts`를 읽으며 예약 행동이
실행되는 곳을 찾습니다. 규칙 파일과 실행 담당 파일이 다른 이유를 확인합니다.
3. `app/game-api/src/router/turns/`에서 예약을 바꾸는 요청을 찾습니다.
예약과 실행 사이에 시간이 흐른다는 점을 다시 확인합니다.
4. [파일 지도](./code-map.md)로 화면과 저장 담당 위치를 이어 봅니다.
파일명 뒤 `.ts`는 TypeScript 코드, `.vue`는 Vue 화면 구성요소, `.json`은 설정
데이터, `.prisma`는 데이터베이스 구조, `.test.ts`는 검증 코드입니다.
`import`는 다른 파일의 기능을 가져오는 선언입니다. 함수를 읽다가 모르는 이름을
만나면 선언 위치로 이동해 입력과 반환값부터 확인하세요.
다음은 [개발자 핸드북](./index.md)의 경험자 경로입니다. 실행 환경을 준비할 때는
저장소 `README.md`의 개발 환경과 [테스트 정책](../testing-policy.md)을 따릅니다.
@@ -4,9 +4,11 @@ Baseline: `main@b91dcbcaaac5acd4c7349cd3ed0996c547f58756`
Branch: `test/game-clock-reconciliation-20260903`
This plan is the status source for the long-running user test branch. A checked
item means code and focused automated evidence exist on this branch; it does not
mean deployment or production validation.
이 문서는 위 기준선·branch에서 진행한 구현 계획의 기록입니다. 체크 표시는 당시
코드와 집중 검증이 있었다는 뜻이며, 현재 main·배포·운영 검증 상태를 나타내지 않습니다.
현재 동작은 [게임 시계](../architecture/game-clock.md)와
[재정렬 계약](../architecture/game-clock-reconciliation.md), 장애 처리는
[복구 안내](./game-clock-recovery.md)를 읽으세요.
## Milestone 1 - authority and inventory
+47 -47
View File
@@ -1,61 +1,61 @@
# Game clock reconciliation recovery
# 게임 시계 재정렬 실패 복구
This runbook is intentionally fail-closed. Do not force a profile to `RUNNING`
or delete an outbox row merely because its process is alive.
이 문서는 운영 경험이 있는 개발자를 위한 절차입니다. 개념부터 읽으려면
[게임 시계](../architecture/game-clock.md), 계산·저장 계약은
[시계 재정렬](../architecture/game-clock-reconciliation.md)을 참고하세요.
프로세스가 살아 있다는 이유만으로 `RUNNING`을 강제하거나 outbox 행을 지우지 않습니다.
## Observe
## 먼저 관찰하기
Read only the target game schema. Record `world_state.clock_phase`,
`clock_revision`, `deadline_generation`, the latest `clock_suspension`, all its
participant checksums, and the matching `clock_projection_outbox`. Compare that
target revision with `sammo:{profile}:clock:active-revision`. Never print DB or
Redis credentials.
대상 게임 schema에서 `world_state.clock_phase`, `clock_revision`,
`deadline_generation`, 최신 `clock_suspension`, 참여자 checksum과 대응하는
`clock_projection_outbox`를 읽습니다. Redis의
`sammo:{profile}:clock:active-revision`과 목표 revision을 비교합니다.
DB·Redis 접속 비밀은 출력하지 않습니다.
## Status meaning
**outbox**는 DB에서 확정했지만 다른 저장소에 전달해야 할 일을 보관한 원장입니다.
**checksum**은 재정렬 대상이 예상한 상태인지 비교하기 위한 요약값입니다.
DB와 Redis를 하나의 트랜잭션으로 묶었다고 가정하지 말고 두 단계의 상태를 봅니다.
- `SUSPENDED`: the cut is durable; no alignment DB transaction has committed.
- `RECONCILING` with `PENDING`/`FAILED` outbox: DB schedules moved, Redis is not
authoritative yet, and gameplay must remain stopped.
- `RECONCILING` with `APPLIED` outbox: verify Redis active revision and all
participant checksums before finalizing.
- `RUNNING`: DB revision, deadline generation, and Redis active revision must
agree. A mismatch is an incident and workers must not dequeue.
## 상태를 해석하기
## Retry
| 상태 | 의미와 확인할 일 |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `SUSPENDED` | 중단 지점은 저장됐지만 일정 정렬 DB transaction은 아직 확정되지 않음 |
| `RECONCILING`, outbox `PENDING`·`FAILED` | DB 일정은 옮겼지만 Redis 전환이 끝나지 않음. 게임 진행을 열지 않음 |
| `RECONCILING`, outbox `APPLIED` | Redis 활성 revision과 모든 참여자 checksum을 확인하고 최종 확정 |
| `RUNNING` | DB revision·deadline generation·Redis 활성 revision이 일치해야 함. 불일치하면 worker가 작업을 꺼내서는 안 됨 |
Retry the same suspension ID and target revision through the clock-operation
service. The service must re-read participant checksums and either return the
already-applied result or resume the pending outbox. Never create a replacement
revision to hide a failed target revision.
## 같은 작업을 재시도하기
For `UNIFICATION_WAIT`, never rerun invader creation as a separate repair.
The input event, aligned schedules, optional rate, deterministic invader IDs,
reserved turns, and outbox committed together. A committed command with a
`RECONCILING` world therefore needs only the same outbox retry. If the command
transaction rolled back, the original prompt and source revision remain and
the same response can be retried without changing IDs or RNG results.
시계 작업 서비스를 통해 **같은 suspension ID와 목표 revision**으로 재시도합니다.
서비스는 checksum을 다시 확인하고, 이미 적용됐다면 그 결과를 사용하거나 남은
outbox 처리를 이어갑니다. 실패를 숨기려고 새 revision을 만들지 않습니다.
When an outbox row is `FAILED`, the profile must remain `RECONCILING`. A retry
is safe in both crash locations:
`UNIFICATION_WAIT`에서는 이민족 생성을 별도 복구로 다시 실행하지 않습니다.
입력 이벤트, 정렬된 일정, 선택적 간격 변경, 결정적인 이민족 ID, 예약 턴과 outbox가
함께 확정됩니다. 명령이 확정됐고 세계가 `RECONCILING`이라면 같은 outbox만 이어
처리합니다. transaction이 rollback됐다면 원래 질문·source revision이 남으므로
ID나 RNG 결과를 바꾸지 않고 같은 응답을 재시도할 수 있습니다.
- before Redis commit, the Lua operation reapplies from the source revision;
- after Redis commit but before DB finalization, the Lua operation returns the
already-active target result and DB finalization resumes without shifting any
tournament deadline twice.
실패 위치에 따른 처리는 다음과 같습니다.
`lastError` naming a legacy tournament deadline means the tournament lacks its
tick dual-write. Do not force the active revision; migrate or prove the
tournament inactive, then retry the same outbox.
- Redis 반영 전: Lua 작업이 원래 revision에서 적용합니다.
- Redis 반영 후·DB 최종 확정 전: Lua가 이미 활성화한 결과를 돌려주고 DB 확정을
이어갑니다. 토너먼트 기한을 두 번 이동하지 않습니다.
## Rollback
`lastError`가 구형 토너먼트 기한을 지목하면 tick 기록이 갖춰졌는지 확인합니다.
활성 revision을 강제하지 말고 migration이나 비활성 상태의 근거를 확인한 뒤 같은
outbox를 재시도합니다.
There is no blind inverse update. Before enabling exact reconciliation in an
environment, keep the normal database backup required for schema migrations.
If participant verification shows an unexpected mutation, stop the profile,
retain the ledger/outbox evidence, and restore the whole game schema from that
backup. Redis projections are then rebuilt from the restored DB revision.
## 되돌리기와 검증 범위
The conditional clock suite exercises `SUSPENDED`, `RECONCILING/PENDING`,
`RECONCILING/FAILED` before and after Redis commit, recovered `APPLIED`, and
final `RUNNING`. The admin status endpoint exposes the phase, revision,
participant checksums, and outbox error needed to choose the matching step.
모든 값을 반대로 이동하는 일괄 역연산은 복구 절차가 아닙니다. 운영 변경 전
정상 백업을 준비하고, 예상하지 못한 mutation이 확인되면 대상 profile을 멈춰
원장·outbox 증거를 보존합니다. 백업 복원이 필요하면 해당 시점 이후 데이터 손실
범위를 평가하고 승인된 운영 복구 절차로 game schema를 복원합니다. Redis 투영은
복원한 DB revision에서 다시 구성합니다.
조건부 시계 통합 suite는 `SUSPENDED`, Redis 적용 전·후의 실패, `APPLIED` 복구와
최종 `RUNNING`을 검사합니다. 실행에 필요한 DB·Redis가 없어 skip된 결과는 복구
검증 성공이 아닙니다. [테스트 정책](../testing-policy.md)과 해당 suite 설정을 확인하세요.
+13 -3
View File
@@ -2,9 +2,16 @@
## 읽기 순서
신규 관리자 플레이 감사의 확정 범위, 수집·조회 비용과 후속 구현 체크리스트는
[프로필별 플레이 감사 설계](../design/play-audit.md)에 있습니다. 해당 기능은
아직 미구현이며 아래의 현재 구조 설명과 상태를 구분합니다.
프로그래밍 기초만 안다면 [코드가 처음인 사람을 위한 구조 안내](./first-steps.md)에서
시작하세요. 화면·서버·저장을 익힌 뒤 아키텍처 개요와 파일 지도로 넘어갑니다.
개발 경험이 있다면 [경험자를 위한 시스템 읽기](./system-walkthrough.md) →
[아키텍처 개요](../architecture/overview.md) → [요청·턴·저장](./request-turn-persistence.md)
순서가 좋습니다. 이후 아래 표에서 맡은 기능을 고르세요. 런타임 문서는 배포·worker까지
포함하는 상세 참고서입니다.
관리자 감사는 [운영 안내](../play-audit-operations.md)에서 현재 기능을,
[설계](../design/play-audit.md)에서 요구사항과 배경을 확인합니다.
| 작업 | 문서 | 코드 시작점 |
| --------------------- | --------------------------------------------------------------- | ------------------------------ |
@@ -18,6 +25,9 @@
| action module | [행동 모듈 프로토콜](../architecture/action-module-protocol.md) | `actionModules/` |
| ref 비교 | [차등 검증](../architecture/turn-state-differential-testing.md) | `tools/integration-tests` |
[구조 문서 찾아보기](./reference-map.md)에는 시계·재시도·실시간 갱신·측정·운영 자료를
질문별로 모았습니다. 과거 계획과 현재 계약의 구분도 여기서 확인할 수 있습니다.
## 경계
- `packages/logic`은 계산과 규칙을 소유합니다.
+47
View File
@@ -0,0 +1,47 @@
# 구조 문서 찾아보기
[기초 안내](./first-steps.md)와 [경험자 안내](./system-walkthrough.md)가 학습 경로를,
아래 문서들은 세부 계약을 제공합니다. 목록·벤치마크·과거 계획을 처음부터 모두
읽기보다 현재 질문에 해당하는 문서를 고르세요.
## 현재 구조와 구현 계약
| 질문 | 문서 |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 무엇이 어디서 실행되나요? | [개요](../architecture/overview.md), [런타임](../architecture/runtime.md) |
| 어느 파일부터 읽나요? | [파일 지도](./code-map.md), [패키지 경계](../architecture/package-boundaries.md) |
| 한 행동이 어떻게 계산되나요? | [도메인과 조립](./domain-and-classes.md), [행동 모듈](../architecture/action-module-protocol.md) |
| 언제 저장되고 실패는 어떻게 처리하나요? | [요청·턴·저장](./request-turn-persistence.md), [API 재시도](../architecture/api-input-event-replay.md) |
| 시간 정지·재개는 어떻게 되나요? | [게임 시계](../architecture/game-clock.md), [시간 도메인 목록](../architecture/time-domains.md), [재정렬 계약](../architecture/game-clock-reconciliation.md) |
| 설정을 여러 파일에서 조합하나요? | [시나리오 합성](../architecture/scenario-composition.md) |
| 화면에 변경이 어떻게 전달되나요? | [실시간 변경 원장](../architecture/realtime-change-journal.md) |
| 전투 시뮬레이터가 어디서 계산하나요? | [브라우저 Worker](../architecture/battle-simulator-browser-worker.md) |
| Ref와 무엇을 비교하나요? | [차등 검증](../architecture/turn-state-differential-testing.md) |
| TypeScript 버전이 왜 둘인가요? | [도구 체인 정책](../architecture/typescript-version.md) |
## 변경 범위를 빠짐없이 확인하는 목록
[엔진 호출 procedure 목록](../architecture/game-api-daemon-procedure-inventory.md)과
[직접 변경·journal 목록](../architecture/game-api-direct-mutation-journal-inventory.md)은
읽기 교재보다 변경 누락을 찾는 검토 자료입니다. 표의 조사 날짜·기준선과 현재
router·검사 코드를 함께 확인하세요. 새 API가 늘어도 과거 조사 숫자가 자동으로
현재 개수가 되지는 않습니다.
시계 참여자 JSON·mutation evidence TSV는 기계가 사용하는 자료입니다. 형식과
검사 계약을 유지하며 해당 기능을 바꿀 때 갱신합니다.
## 측정·계획·운영 자료
| 자료 | 해석 범위 |
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [NPC 메모리 측정](../architecture/npc-lifecycle-memory-profile.md) | 지정 fixture의 메모리·생성/사망 부하. 운영 DB 처리량과 다름 |
| [NPC 천통 시간 측정](../architecture/npc-unification-timing-benchmark.md) | 인메모리 계산 시간. 실제 배포 성능 보장이 아님 |
| [시계 구현 계획 기록](./game-clock-reconciliation-plan.md) | 당시 branch의 milestone. 현재 운영 상태의 기준이 아님 |
| [시계 복구](./game-clock-recovery.md) | 상태·원장 확인과 같은 작업 재시도 |
| [릴리스 운영](../release-operations.md) | 실제 배포 작업과 준비 확인 |
| [관리자 콘솔](../admin-console.md), [플레이 감사 운영](../play-audit-operations.md) | 관리자 권한과 현재 조사 기능 |
| [테스트 정책](../testing-policy.md) | 실행 준비·검증 종류·skip의 해석 |
| [프론트엔드 CSS 구조](../frontend-css-architecture.md) | 화면 스타일의 소유권과 배치 계약 |
기능 설계의 목표, 코드에 있는 구현, 테스트로 확인한 범위, 실제 배포 상태는 서로
다릅니다. 문서의 “완료”는 함께 적힌 날짜와 검증 범위로 해석하세요.
+25 -14
View File
@@ -1,5 +1,10 @@
# 요청·턴·저장 흐름
이 문서는 데이터베이스와 트랜잭션을 아는 독자를 위한 실행 흐름입니다. 처음이라면
[기초 구조 안내](./first-steps.md)를 먼저 읽으세요. **mutation**은 상태 변경,
**flush**는 메모리에서 바뀐 내용을 저장소에 반영하는 작업입니다.
조회·즉시 변경·예약 수정·시간에 따른 턴 실행을 구분해서 읽습니다.
## 조회
```text
@@ -13,31 +18,35 @@ endpoint DTO에서 공개 field를 선택합니다.
## API transaction mutation
```text
request
-> requestId와 input 검증
-> session actor·권한 검사
-> InputEvent(target=API, PROCESSING)
-> Prisma transaction
-> domain row mutation
-> InputEvent(SUCCEEDED)
-> commit
-> notification
request → requestId·입력·actor·권한 검증
→ PostgreSQL transaction
→ API InputEvent 생성 또는 기존 row 잠금
→ identity 확인 → PROCESSING(attempts + 1)
→ savepoint 이후 업무 변경
→ SUCCEEDED + 실제 result → commit
→ notification
```
`app/game-api/src/inputEventBoundary.ts`의 `executeInputEvent()`가 이 경계를
제공합니다. 중복 request ID는 완료·처리 중 event를 다시 실행하지 않으며,
실패 event는 claim 조건을 만족할 때 attempts를 증가시킵니다.
제공합니다. identity는 event type, actor, payload digest를 포함합니다. 같은 요청의
완료 결과는 재사용하고, 다른 내용으로 같은 ID를 사용하면 충돌로 거부합니다.
`PENDING`·`FAILED`는 identity가 맞으면 재시도할 수 있고, `PROCESSING`은 임의로
다시 선점하지 않습니다.
업무 오류는 savepoint까지 되돌린 뒤 실패 상태를 저장합니다. DB transaction
자체가 실패하면 그 안의 변경은 rollback됩니다. 상세 상태 표와 HTTP 응답 계약은
[API 입력 재시도](../architecture/api-input-event-replay.md)를 따릅니다.
## Daemon mutation
```text
request
-> actor·input 검증
-> InputEvent(target=DAEMON)
-> InputEvent(target=ENGINE)
-> daemon transport
-> lease owner claim
-> in-memory world mutation
-> EngineStateManager transaction flush
-> EngineStateManager의 메모리 복원 경계 안에서 DB transaction flush
-> PostgreSQL event 결과 commit과 in-memory world checkpoint 확정
-> SSE/realtime
```
@@ -60,7 +69,9 @@ request
7. PostgreSQL transaction에서 dirty state, turn queue, log와 event를 flush하고,
같은 `EngineStateManager` 경계에서 world checkpoint를 확정합니다.
Transaction 실패 시 `EngineStateManager`가 in-memory snapshot을 복원합니다.
`databaseHooks.ts`가 PostgreSQL transaction을 담당하고, 이를 감싼
`EngineStateManager`가 실패 시 메모리 snapshot을 복원합니다. 이 관리자는 DB를
직접 알지 못합니다. DB rollback과 메모리 복원을 함께 유지해야 합니다.
Lease를 잃은 process는 fencing 검사에서 commit하지 못합니다.
`InMemoryTurnStateStore`는 checkpoint를 별도로 복제하지 않고 rollback 대상인
`InMemoryTurnWorld`에서 읽습니다. 따라서 flush 실패 뒤 다음 run도 복원된
+88
View File
@@ -0,0 +1,88 @@
# 경험자를 위한 시스템 읽기
웹 개발이나 컴퓨터공학 배경이 있고, 이 저장소의 책임과 상태 전이를 빠르게
이해하려는 독자를 위한 안내입니다. 웹 용어가 낯설다면 [기초 안내](./first-steps.md)를
먼저 읽으세요.
## 세 가지 흐름으로 나누기
| 흐름 | 시작과 끝 | 핵심 질문 |
| ----------- | -------------------------------------------------------- | --------------------------------------------------------- |
| 사용자 요청 | 브라우저 → API → 저장 또는 엔진 입력 → 응답 | actor, 입력 검증, 중복 요청, 공개 범위는 누가 결정하는가? |
| 게임 진행 | 스케줄 → 장수 턴·월간 처리 → 저장 → 알림 | 실행 순서, 난수 소비, 실패 후 복원이 보존되는가? |
| 운영 | 관리자 요청 → 작업 원장 → 빌드·프로세스 전환 → 준비 확인 | 게임 상태와 배포 상태를 혼동하지 않는가? |
HTTP 요청 하나가 게임의 한 턴인 것은 아닙니다. 조회, 예약 수정, 즉시 동작,
시간에 따라 실행하는 턴은 서로 다른 수명주기를 가집니다. [전체 구조](../architecture/overview.md)에서
구성요소를 본 뒤 [요청·턴·저장](./request-turn-persistence.md)에서 경계를 읽으세요.
## 조회: 인증한 사람과 보여 줄 대상을 분리합니다
`app/game-api/src/server.ts`, `trpc.ts`, `router.ts`에서 transport와 인증 context,
procedure 조립을 따라갑니다. Gateway 계정과 게임 장수는 같은 식별자가 아닙니다.
클라이언트가 보낸 장수 번호만으로 소유권을 결정하지 않습니다.
구체적인 예는 `app/game-api/src/router/world/index.ts`의 `getCurrentCity`입니다.
자신의 위치, 같은 국가 장수의 위치, 소유 도시, 첩보와 인접 여부를 조합해
정보 공개 수준을 정합니다. 응답에 없는 비밀 값을 프론트엔드에서 가리는 방식이
아닙니다. 데이터 전달 객체(DTO)는 저장 행 전체와 다를 수 있습니다.
## 변경: 입력 원장과 결과의 원자성을 읽습니다
`InputEvent`는 받아들인 변경 요청과 처리 상태를 기록하는 PostgreSQL 모델입니다.
실제 `InputEventTarget` 값은 `API`와 `ENGINE`입니다. 엔진을 설명할 때 쓰는
“daemon”은 프로세스 역할이며 DB enum 값이 아닙니다.
API 쪽 경계는 `app/game-api/src/inputEventBoundary.ts`입니다. request ID만
같다고 모든 요청을 동일시하지 않습니다. actor, event type, payload identity와
상태를 함께 읽고 재시도·충돌·완료 결과 재사용을 구분합니다. 엔진 쪽 입력은
`app/game-api/src/daemon/`에서 따라갑니다.
예약 변경이 저장된 뒤 실제 턴 실행 조건은 다시 달라질 수 있습니다. request
acceptance 검증과 command execution 검증을 하나로 합치지 않습니다. API에서
완료된 변경과 엔진이 메모리에 보유한 세계 사이의 동기화도 확인해야 합니다.
## 엔진: 단일 소유자라도 장애 경계는 필요합니다
`createTurnDaemonRuntime()`의 조립부터 읽으면 loader, registry, AI, 월간 handler,
저장 hook이 연결됩니다. 계산 중 상태는 `InMemoryTurnWorld`, 저장과 실패 복원은
`EngineStateManager`, 실행 순서는 `TurnDaemonLifecycle`에서 추적합니다.
**Lease**는 일정 기간 엔진 소유권을 인정하는 임대입니다. **Fencing token**은
소유권 세대를 구분하여, 멈췄다가 돌아온 과거 프로세스가 새 소유자의 상태를
덮어쓰지 못하게 합니다. “프로세스가 한 개일 것”이라는 운영 가정만으로 대체할 수
없습니다. DB 저장 실패에는 메모리 rollback도 필요합니다.
난수도 입력 상태의 일부입니다. seed가 같아도 분기·정렬·호출 횟수가 바뀌면
이후 결과가 달라집니다. 도메인 계산은 [행동 모듈](../architecture/action-module-protocol.md),
비교 범위는 [차등 검증](../architecture/turn-state-differential-testing.md)을 읽으세요.
## 시간을 하나의 Date로 생각하지 않습니다
게임 진행 시각과 인증 만료·lease 같은 현실 시각은 목적이 다릅니다. 일시정지나
턴 간격 변경이 있다고 로그인 만료와 소유권 heartbeat를 같은 방식으로 옮길 수는
없습니다. [시간 도메인](../architecture/time-domains.md), [게임 시계](../architecture/game-clock.md),
[복구 절차](./game-clock-recovery.md) 순으로 읽으면 계산과 운영 경계가 이어집니다.
## 모듈과 프로세스도 다릅니다
`packages/logic`은 게임 계산을 소유하며 DB·파일·네트워크 I/O를 직접 하지 않습니다.
필요한 외부 동작을 interface인 port로 표현하고, app/infra 쪽 adapter를 주입합니다.
이 구분은 단위 검증과 런타임 저장 경계를 분리하기 위한 것입니다.
`game-api`가 `game-engine`의 일부 loader를 import한다고 API 안에서 턴 데몬을
실행한다는 뜻은 아닙니다. 공개 subpath와 프로세스 entrypoint를 구분하세요.
프론트엔드의 backend router 타입 참조도 서버 실행 코드를 브라우저에 넣는 것과
다릅니다. [패키지 경계](../architecture/package-boundaries.md)는 이 규칙의 기준입니다.
## 다음 조사 위치를 선택하기
- 화면 상태·재접속 문제: frontend store → 조회 응답 → [실시간 변경 원장](../architecture/realtime-change-journal.md)
- 새 게임 규칙: [도메인 조립](./domain-and-classes.md) → 명령·constraint → 엔진 결과 적용·flush
- 설정에 따른 차이: [시나리오 합성](../architecture/scenario-composition.md) → 선택한 resource → 실제 loader
- 계정·배포 문제: [런타임](../architecture/runtime.md) → [릴리스 운영](../release-operations.md)
- 관리자 조사 기록: [플레이 감사 운영](../play-audit-operations.md) → 수집 정책·snapshot·조회
코드 검증은 [테스트 정책](../testing-policy.md)을 따릅니다. 순수 계산 테스트,
실제 DB 저장 검증, 브라우저 검증은 서로 다른 질문에 답합니다. Ref는 계승 규칙의
비교 기준이며, 별도로 결정된 Core 기능까지 예전 구현으로 되돌리는 기준은 아닙니다.