인사부 도시 관직 자원과 기본 처리 및 수뇌 알림 추가
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user