인사부 도시 관직 자원과 기본 처리 및 수뇌 알림 추가

This commit is contained in:
2026-09-29 09:41:01 +00:00
parent cd6ded4970
commit 5bafdfc1e7
36 changed files with 2238 additions and 20 deletions
@@ -135,3 +135,10 @@ fence를 가진 채 ENGINE 결과를 기다리던 교착을 제거했다. 투표
`adjustAccountIconForUser`, `requestImmediateAction`, `requestJoinCreateCommand`,
`requestNpcPossessionCommand` caller 검색을 함께 실행해 helper 경유 route를 놓치지
않는다.
### 도시 관직 자원 (2026-09-29)
`nation.cityOfficeRequest`는 `engineAuthedProcedure`로 session actor를 전달한다.
`cityOfficeRequests.ts`의 ENGINE handler가 현재 소유자·소속·수뇌 권한·대상과
기한을 다시 검사하고 요청/임명/결과 로그를 같은 durable transaction으로 저장한다.
세부 기본값과 clock 정책은 [도시 관직 자원](../design/city-office-requests.md)을 따른다.
@@ -3,7 +3,7 @@
## 범위와 판정 규칙
`app/game-api/src/router/**`에서 `.mutation()`으로 선언하고 실제 `appRouter`에 mount한
87개 route를 2026-08-24 기준으로 전수 분류한다. 이 목록은 “mutation transport를
89개 route를 2026-09-29 기준으로 전수 분류한다. 이 목록은 “mutation transport를
사용한다”와 “game DB를 변경한다”를 구분한다. 신규 route가 추가되거나 선언한 router가
mount되지 않으면
`app/game-api/test/directMutationJournalInventory.test.ts`가 실패하므로 소유권과
@@ -37,7 +37,7 @@ writer reconciliation을 포함한다. rolling deployment가 끝난 뒤에만
| durable journal | 19 | `betting.bet`; `diplomacy.destroyLetter`, `diplomacy.respondLetter`, `diplomacy.rollbackLetter`, `diplomacy.sendLetter`; `inherit.checkOwner`; `messages.delete`, `messages.respond`, `messages.send`; `turns.reserved.repeatGeneral`, `turns.reserved.setGeneral`, `turns.reserved.setGeneralBulk`, `turns.reserved.setNation`, `turns.reserved.setNationBulk`, `turns.reserved.shiftGeneral`; `vote.closePoll`, `vote.createPoll`, `vote.submitVote`, `vote.updatePoll` |
| separate access journal | 1 | `public.recordAccess` |
| explicit no realtime consumer | 7 | `board.writeArticle`, `board.writeComment`; `join.listPossessCandidates`; `messages.readLatest`; `turns.reserved.repeatNation`, `turns.reserved.shiftNation`; `vote.addComment` |
| engine owned | 38 | `auction.bidBuyRice`, `auction.bidSellRice`, `auction.bidUnique`, `auction.openBuyRice`, `auction.openSellRice`, `auction.openUnique`; `general.adjustIcon`, `general.buildNationCandidate`, `general.dieOnPrestart`, `general.dropItem`, `general.ensureDieOnPrestartStatus`, `general.instantRetreat`, `general.setMySetting`, `general.vacation`; `inherit.openUniqueAuction`; `join.createGeneral`, `join.getSelectionPool`, `join.possessGeneral`, `join.reselectPoolGeneral`, `join.selectPoolGeneral`; `nation.appoint`, `nation.changePermission`, `nation.kick`, `nation.setBill`, `nation.setBlockScout`, `nation.setBlockWar`, `nation.setNotice`, `nation.setRate`, `nation.setScoutMsg`, `nation.setSecretLimit`; `npc.setGeneralPriority`, `npc.setNationPolicy`, `npc.setNationPriority`; `troop.create`, `troop.exit`, `troop.join`, `troop.kick`, `troop.rename` |
| engine owned | 39 | `auction.bidBuyRice`, `auction.bidSellRice`, `auction.bidUnique`, `auction.openBuyRice`, `auction.openSellRice`, `auction.openUnique`; `general.adjustIcon`, `general.buildNationCandidate`, `general.dieOnPrestart`, `general.dropItem`, `general.ensureDieOnPrestartStatus`, `general.instantRetreat`, `general.setMySetting`, `general.vacation`; `inherit.openUniqueAuction`; `join.createGeneral`, `join.getSelectionPool`, `join.possessGeneral`, `join.reselectPoolGeneral`, `join.selectPoolGeneral`; `nation.appoint`, `nation.changePermission`, `nation.cityOfficeRequest`, `nation.kick`, `nation.setBill`, `nation.setBlockScout`, `nation.setBlockWar`, `nation.setNotice`, `nation.setRate`, `nation.setScoutMsg`, `nation.setSecretLimit`; `npc.setGeneralPriority`, `npc.setNationPolicy`, `npc.setNationPriority`; `troop.create`, `troop.exit`, `troop.join`, `troop.kick`, `troop.rename` |
| mixed saga | 9 | `inherit.buyHiddenBuff`, `inherit.buyRandomUnique`, `inherit.resetSpecialWar`, `inherit.resetStat`, `inherit.resetTurnTime`, `inherit.setNextSpecialWar`; `tournament.cancel`, `tournament.join`, `tournament.placeBet` |
| Redis projection | 6 | `tournament.patchState`, `tournament.seedParticipants`, `tournament.setBettingEntries`, `tournament.setMatches`, `tournament.setParticipants`, `tournament.setState` |
| operational | 3 | `turnDaemon.pause`, `turnDaemon.resume`, `turnDaemon.run` |
@@ -95,3 +95,7 @@ public dashboard event로 내보내지 않는다. browser wake-up은 정밀 enti
4. 따라서 dashboard/map coverage v1 코드 조건은 충족했지만 migration은 rolling deploy
안전을 위해 0을 유지한다. 전체 writer binary 배포가 확인된 뒤에만 activation command를
실행하며, 문제 시 meta를 0으로 내려 즉시 full-compute fallback한다.
도시 관직 자원은 ENGINE 입력 이벤트로 접수·처리하며, `general.meta.cityOfficeRequest`
변경을 front-status journal/SSE에 연결한다. [처리 정책](../design/city-office-requests.md)을
따르며, HTTP actor 검증과 실제 ENGINE PostgreSQL rollback/reload를 별도로 확인한다.
@@ -44,6 +44,7 @@ messages.respond durable-journal direct-endpoint session-user-db-general dynamic
messages.send durable-journal direct-endpoint session-user-db-general endpoint-unit app/game-api/test/messagesRouter.test.ts no-route-actual-db
nation.appoint engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationPersonnelRouter.test.ts no-route-actual-db
nation.changePermission engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationPersonnelRouter.test.ts no-route-actual-db
nation.cityOfficeRequest engine-owned core-only session-user-engine-general actual-db app/game-engine/test/cityOfficeRequestPersistence.integration.test.ts no-route-actual-db
nation.kick engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationPersonnelRouter.test.ts no-route-actual-db
nation.setBill engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationSettingRouter.test.ts no-route-actual-db
nation.setBlockScout engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/scoutBlockRouter.test.ts no-route-actual-db
1 route owner_boundary ref_basis actor_source strongest_evidence evidence_path remaining_gap
44 messages.send durable-journal direct-endpoint session-user-db-general endpoint-unit app/game-api/test/messagesRouter.test.ts no-route-actual-db
45 nation.appoint engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationPersonnelRouter.test.ts no-route-actual-db
46 nation.changePermission engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationPersonnelRouter.test.ts no-route-actual-db
47 nation.cityOfficeRequest engine-owned core-only session-user-engine-general actual-db app/game-engine/test/cityOfficeRequestPersistence.integration.test.ts no-route-actual-db
48 nation.kick engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationPersonnelRouter.test.ts no-route-actual-db
49 nation.setBill engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/nationSettingRouter.test.ts no-route-actual-db
50 nation.setBlockScout engine-owned domain-command session-user-engine-general endpoint-unit app/game-api/test/scoutBlockRouter.test.ts no-route-actual-db
@@ -289,6 +289,21 @@
],
"projectionFields": [],
"owner": "game-engine/clock-operation"
},
{
"key": "city-office-request-occurrence",
"policy": "KEEP",
"authorityFields": ["general.meta.cityOfficeRequest.createdTick"],
"projectionFields": [],
"owner": "game-engine/city-office-requests"
},
{
"key": "city-office-request-deadline",
"policy": "SHIFT",
"authorityFields": ["general.meta.cityOfficeRequest.dueTick"],
"projectionFields": [],
"rowScope": "pending requests at or after the reconciliation cut",
"owner": "game-engine/city-office-requests"
}
],
"redis": [
+76
View File
@@ -0,0 +1,76 @@
# 도시 관직 자원
## 정책
인사부에서 현재 체류 중인 아국 도시의 태수·군사·종사에 자원한다. 기존 수동
임명은 유지하며, 이 요청 절차는 Core 전용 기능이다.
| 상태 | 기한까지 미처리한 요청 |
| -------------------------------------------- | ------------------------- |
| 공석 | 승인 |
| 소유자가 없는 N장(npcState=2)·M장(3) 재직 | 승인 |
| 다른 도시에 체류하는 유저장·빙의장 재직 | 거부 |
| 신청자의 소속연수가 국가 기밀 공개 기준 미달 | 위 조건보다 우선하여 거부 |
| 유저장·빙의장이 해당 도시에 체류 | 신청 불가 |
수뇌는 기본값과 관계없이 승인·거부할 수 있다. 명시적 승인은 즉시 임명한다.
승인 시 기존 지방 관직의 P1 기밀 권한을 받는다. 기본 거부 요청은 소속연수가
늘어도 자동 승인으로 전환하지 않는다. 대기 중 국가의 기밀 공개 기준이 강화되면
자동 승인 요청도 자동 거부로 제한한다.
능력치 기준은 기존 임명과 같다(태수 무력, 군사 지력은 scenario chiefMin 이상,
종사는 별도 능력치 하한 없음). 일반·도시 관직의 유저장과 빙의장만 자원할 수 있고,
noChief 벌점 및 도시 임명 lock을 검사한다. 군주·수뇌는 기존 수동 임명을 이용한다.
계급·사관 비교로 다른 유저장을 자동 교체하지 않는다.
## 기한과 충돌
- 장수당 분기 1회 접수한다. 철회·거부도 횟수를 소비한다. 대기 요청은 장수당 1건,
도시·관직당 1건이다.
- 요청 생성의 논리 tick에서 최소 한 달(`GAME_TICKS_PER_TURN`) 뒤 첫 월 경계를
마감으로 저장한다. 월말 직전 요청도 검토 기간이 단축되지 않는다. 정지 중에는
논리 tick이 진행하지 않으며, 턴 시간 변경은 남은 tick을 유지한다. 운영 시계
재정렬과 backlog 복구는 접수 시각을 보존하고 대기 마감만 이동한다.
`game-clock-participants.json`에 occurrence KEEP/deadline SHIFT로 등록한다.
- 수뇌의 수동 임명은 요청 때문에 막히지 않는다. 수동 임명 후 조건이 바뀐 요청은
취소한다. 자동 임명도 기존 `officer_set`을 설정한다.
- 분기 lock 초기화 전에는 무효 요청을 취소하고, 월 처리 후에는 기한이 된 요청을
판정한다. 소속·소유자·체류 도시·도시 소유·재직자 ID·능력치·벌점·lock을 다시
확인한다. 같은 슬롯의 재직자가 바뀌면 새 재직자를 덮어쓰지 않는다.
- 신청·승인·거부·철회 명령도 실행 시점 actor와 durable 입력 이벤트 소유자를 검사한다.
## 저장과 알림
`general.meta.cityOfficeRequest`에 장수당 마지막 요청 하나만 저장한다. 요청에는
신청자·소유자·국가·도시·관직·기존 재직자, 접수 분기, 생성/마감 tick, 기본 처리와
이유, 상태 및 결과 이유가 포함된다. `pending`에서 `approved`, `rejected`,
`withdrawn`, `cancelled` 중 하나로 종료한다. 기존 JSON metadata의 additive 확장으로
새 테이블이나 migration은 없다. 기존 입력 이벤트 idempotency와 ENGINE
transaction, dirty state flush 및 worldLoader 재시작 경로를 사용한다.
API는 일반 장수에게 본인 요청만, 수뇌에게 같은 국가의 대기 요청도 제공한다.
userId와 원본 metadata는 응답에 포함하지 않는다. 처리 결과는 개인·국가 로그에
기록한다.
메인 `getFrontStatus.cityOfficeRequests`에는 수뇌에게 대기 요청 ID만 제공한다.
요청 metadata 및 수뇌 권한 변경은 front-status projection을 무효화하여 기존
SSE read-model 갱신을 따른다. 알림 cursor는 시즌·장수·국가별로 분리한다. 새 요청은
60초간 닫을 수 있는 알림과 인사부 링크로 표시하며, 닫은 같은 요청은 재알림하지
않는다. 대기 건수 링크는 알림을 닫아도 남는다. 외부 Web Push는 이 기능의 범위가
아니다.
인사부는 기본값·이유·마감과 버튼을 짧은 항목으로 표시한다. 긴 설명문을 상시
출력하지 않는다. 기밀 조건에 해당하는 명시적 승인 확인창에는 기밀 열람 가능을
표시한다. 목록은 활성 화면에서 15초 간격 및 focus 복귀 때 갱신한다.
## 구현과 검증 진입점
- `packages/common/src/personnel/officeRequest.ts`: 영속 요청 타입·검증
- `packages/logic/src/personnel/officeRequest.ts`: 공유 자원 조건
- `app/game-engine/src/turn/cityOfficeRequests.ts`: 명령·월 처리·결과 로그
- `app/game-api/src/router/nation/endpoints/cityOfficeRequests.ts`: 인증 API·read model
- `CityOfficeRequests.vue`, `CityOfficeRequestNotice.vue`: 인사부·메인 알림
- `cityOfficeRequests.test.ts`, `cityOfficeRequestPersistence.integration.test.ts`:
정책·기한·충돌·rollback·재시작
- `nationPersonnelRouter.test.ts`, `mainFrontStatusRouter.test.ts`: DTO 권한과 HTTP 경계
- `nationOffices.spec.ts`, `mainNavigation.spec.ts`: production Chromium 조작·알림·geometry