fix: 특수 유저 커맨드의 Ref 호환 경계를 보강한다

수뇌 국가 설정과 NPC 정책을 actor-bound ENGINE mutation으로 옮기고, 추방·등용·점령·멸망·아이템 폐기의 특수 분기를 Ref와 맞춘다.

요청 ID를 사용자·프로필별로 격리하고 토너먼트 손상 projection을 fail-closed하며 실제 DB 및 Ref 차등 회귀를 보강한다.
This commit is contained in:
2026-08-24 12:10:57 +00:00
parent 5389f8ed94
commit 630bc29100
74 changed files with 3893 additions and 1141 deletions
+13 -1
View File
@@ -24,7 +24,8 @@ import {
resolveRemainingMinutes,
} from '../../services/generalBasicCardProjection.js';
import { loadTraitNames } from '../nation/shared.js';
import { getMyGeneral } from '../shared/general.js';
import { getAuthenticatedUserId, getMyGeneral } from '../shared/general.js';
import { throwIfCommandRejected } from '../shared/turnDaemon.js';
const TROOP_PANEL_RECORD_TYPES = [
'firenum',
@@ -54,6 +55,7 @@ const assertCommandResult = <T extends 'troopCreate' | 'troopJoin' | 'troopExit'
result: TurnDaemonCommandResult | null,
expectedType: T
): never => {
throwIfCommandRejected(result);
if (!result) {
throw new TRPCError({ code: 'TIMEOUT', message: 'Turn daemon did not respond.' });
}
@@ -427,6 +429,7 @@ export const troopRouter = router({
};
}),
create: engineAuthedProcedure.input(z.object({ troopName: troopNameSchema })).mutation(async ({ ctx, input }) => {
const userId = getAuthenticatedUserId(ctx);
const me = await getMyGeneral(ctx);
const troopName = normalizeRequiredTroopName(input.troopName);
if (me.troopId !== 0) {
@@ -444,6 +447,7 @@ export const troopRouter = router({
const result = await ctx.turnDaemon.requestCommand({
type: 'troopCreate',
...(ctx.requestId ? { requestId: `${ctx.requestId}:troop.create:engine:0:troopCreate` } : {}),
userId,
generalId: me.id,
troopName,
});
@@ -458,10 +462,12 @@ export const troopRouter = router({
join: engineAuthedProcedure
.input(z.object({ troopId: z.number().int().positive() }))
.mutation(async ({ ctx, input }) => {
const userId = getAuthenticatedUserId(ctx);
const me = await getMyGeneral(ctx);
const result = await ctx.turnDaemon.requestCommand({
type: 'troopJoin',
...(ctx.requestId ? { requestId: `${ctx.requestId}:troop.join:engine:0:troopJoin` } : {}),
userId,
generalId: me.id,
troopId: input.troopId,
});
@@ -474,10 +480,12 @@ export const troopRouter = router({
return { ok: true };
}),
exit: engineAuthedProcedure.mutation(async ({ ctx }) => {
const userId = getAuthenticatedUserId(ctx);
const me = await getMyGeneral(ctx);
const result = await ctx.turnDaemon.requestCommand({
type: 'troopExit',
...(ctx.requestId ? { requestId: `${ctx.requestId}:troop.exit:engine:0:troopExit` } : {}),
userId,
generalId: me.id,
});
if (!result || result.type !== 'troopExit') {
@@ -496,6 +504,7 @@ export const troopRouter = router({
})
)
.mutation(async ({ ctx, input }) => {
const userId = getAuthenticatedUserId(ctx);
const me = await getMyGeneral(ctx);
if (me.id !== input.troopId || me.troopId !== me.id) {
throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다.' });
@@ -520,6 +529,7 @@ export const troopRouter = router({
const result = await ctx.turnDaemon.requestCommand({
type: 'troopKick',
...(ctx.requestId ? { requestId: `${ctx.requestId}:troop.kick:engine:0:troopKick` } : {}),
userId,
generalId: me.id,
troopId: input.troopId,
targetGeneralId: input.targetGeneralId,
@@ -541,6 +551,7 @@ export const troopRouter = router({
})
)
.mutation(async ({ ctx, input }) => {
const userId = getAuthenticatedUserId(ctx);
const me = await getMyGeneral(ctx);
const troopName = normalizeRequiredTroopName(input.troopName);
const nation = await ctx.db.nation.findUnique({
@@ -562,6 +573,7 @@ export const troopRouter = router({
const result = await ctx.turnDaemon.requestCommand({
type: 'troopRename',
...(ctx.requestId ? { requestId: `${ctx.requestId}:troop.rename:engine:0:troopRename` } : {}),
userId,
generalId: me.id,
troopId: input.troopId,
troopName,