feat: 국가 메타 업데이트 기능 추가 및 관련 로직 구현

This commit is contained in:
2026-01-24 03:16:05 +00:00
parent 6e439f074d
commit cbee0d4c20
5 changed files with 169 additions and 49 deletions
+38 -23
View File
@@ -429,26 +429,35 @@ const assertNationEditable = (
}; };
const updateNationMeta = async ( const updateNationMeta = async (
ctx: Pick<GameApiContext, 'db'>, ctx: Pick<GameApiContext, 'turnDaemon'>,
nationId: number, nationId: number,
updates: Record<string, unknown> updates: Record<string, unknown>,
currentMeta: Record<string, unknown>
): Promise<InputJsonValue> => { ): Promise<InputJsonValue> => {
const nation = await ctx.db.nation.findUnique({ const expectedUpdatedAt = typeof currentMeta._updatedAt === 'string' ? currentMeta._updatedAt : undefined;
where: { id: nationId }, const result = await ctx.turnDaemon.requestCommand({
select: { meta: true }, type: 'setNationMeta',
nationId,
updates,
expectedUpdatedAt,
}); });
if (!nation) { if (!result || result.type !== 'setNationMeta') {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
} }
const meta = asRecord(nation.meta); if (!result.ok) {
const nextMeta = { ...meta, ...updates } as InputJsonValue; if (result.reason === 'CONFLICT') {
await ctx.db.nation.update({ throw new TRPCError({
where: { id: nationId }, code: 'CONFLICT',
data: { message: '다른 사용자가 정책을 변경했습니다. 재시도하거나 현재 상태로 갱신해주세요.',
meta: nextMeta, });
}, }
}); throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
return nextMeta; }
return {
...currentMeta,
...updates,
_updatedAt: result.updatedAt,
} as InputJsonValue;
}; };
const mapGeneralList = async ( const mapGeneralList = async (
@@ -1239,9 +1248,10 @@ export const nationRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
} }
assertNationEditable(me, nation.meta); assertNationEditable(me, nation.meta);
const nationMeta = asRecord(nation.meta);
await updateNationMeta(ctx, me.nationId, { await updateNationMeta(ctx, me.nationId, {
notice: input.msg, notice: input.msg,
}); }, nationMeta);
return { ok: true }; return { ok: true };
}), }),
setScoutMsg: authedProcedure setScoutMsg: authedProcedure
@@ -1261,9 +1271,10 @@ export const nationRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
} }
assertNationEditable(me, nation.meta); assertNationEditable(me, nation.meta);
const nationMeta = asRecord(nation.meta);
await updateNationMeta(ctx, me.nationId, { await updateNationMeta(ctx, me.nationId, {
infoText: input.msg, infoText: input.msg,
}); }, nationMeta);
return { ok: true }; return { ok: true };
}), }),
setRate: authedProcedure setRate: authedProcedure
@@ -1283,9 +1294,10 @@ export const nationRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
} }
assertNationEditable(me, nation.meta); assertNationEditable(me, nation.meta);
const nationMeta = asRecord(nation.meta);
await updateNationMeta(ctx, me.nationId, { await updateNationMeta(ctx, me.nationId, {
rate: input.amount, rate: input.amount,
}); }, nationMeta);
return { ok: true }; return { ok: true };
}), }),
setBill: authedProcedure setBill: authedProcedure
@@ -1305,9 +1317,10 @@ export const nationRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
} }
assertNationEditable(me, nation.meta); assertNationEditable(me, nation.meta);
const nationMeta = asRecord(nation.meta);
await updateNationMeta(ctx, me.nationId, { await updateNationMeta(ctx, me.nationId, {
bill: input.amount, bill: input.amount,
}); }, nationMeta);
return { ok: true }; return { ok: true };
}), }),
setSecretLimit: authedProcedure setSecretLimit: authedProcedure
@@ -1327,9 +1340,10 @@ export const nationRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
} }
assertNationEditable(me, nation.meta); assertNationEditable(me, nation.meta);
const nationMeta = asRecord(nation.meta);
await updateNationMeta(ctx, me.nationId, { await updateNationMeta(ctx, me.nationId, {
secretlimit: input.amount, secretlimit: input.amount,
}); }, nationMeta);
return { ok: true }; return { ok: true };
}), }),
setBlockWar: authedProcedure setBlockWar: authedProcedure
@@ -1359,7 +1373,7 @@ export const nationRouter = router({
await updateNationMeta(ctx, me.nationId, { await updateNationMeta(ctx, me.nationId, {
war: input.value ? 1 : 0, war: input.value ? 1 : 0,
available_war_setting_cnt: nextRemain, available_war_setting_cnt: nextRemain,
}); }, meta);
return { availableCnt: nextRemain }; return { availableCnt: nextRemain };
}), }),
setBlockScout: authedProcedure setBlockScout: authedProcedure
@@ -1379,9 +1393,10 @@ export const nationRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' }); throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
} }
assertNationEditable(me, nation.meta); assertNationEditable(me, nation.meta);
const nationMeta = asRecord(nation.meta);
await updateNationMeta(ctx, me.nationId, { await updateNationMeta(ctx, me.nationId, {
scout: input.value ? 1 : 0, scout: input.value ? 1 : 0,
}); }, nationMeta);
return { ok: true }; return { ok: true };
}), }),
getBattleCenter: authedProcedure.query(async ({ ctx }) => { getBattleCenter: authedProcedure.query(async ({ ctx }) => {
+49 -24
View File
@@ -8,6 +8,7 @@ import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js'
import { authedProcedure, router } from '../../trpc.js'; import { authedProcedure, router } from '../../trpc.js';
import { loadUnitSetDefinitionByName } from '../../battleSim/unitSetLoader.js'; import { loadUnitSetDefinitionByName } from '../../battleSim/unitSetLoader.js';
import type { GameApiContext } from '../../context.js';
import { getMyGeneral } from '../shared/general.js'; import { getMyGeneral } from '../shared/general.js';
import { resolveSecretPermission } from '../shared/secretPermission.js'; import { resolveSecretPermission } from '../shared/secretPermission.js';
@@ -42,6 +43,33 @@ type SetterInfo = {
date: string | null; date: string | null;
}; };
const updateNationMeta = async (
ctx: Pick<GameApiContext, 'turnDaemon'>,
nationId: number,
updates: Record<string, unknown>,
currentMeta: Record<string, unknown>
): Promise<void> => {
const expectedUpdatedAt = typeof currentMeta._updatedAt === 'string' ? currentMeta._updatedAt : undefined;
const result = await ctx.turnDaemon.requestCommand({
type: 'setNationMeta',
nationId,
updates,
expectedUpdatedAt,
});
if (!result || result.type !== 'setNationMeta') {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Unexpected response' });
}
if (!result.ok) {
if (result.reason === 'CONFLICT') {
throw new TRPCError({
code: 'CONFLICT',
message: '다른 사용자가 정책을 변경했습니다. 재시도하거나 현재 상태로 갱신해주세요.',
});
}
throw new TRPCError({ code: 'BAD_REQUEST', message: result.reason });
}
};
const DEFAULT_NATION_PRIORITY = [ const DEFAULT_NATION_PRIORITY = [
'불가침제의', '불가침제의',
'선전포고', '선전포고',
@@ -665,15 +693,14 @@ export const npcRouter = router({
valueSetTime: new Date().toISOString(), valueSetTime: new Date().toISOString(),
}; };
await ctx.db.nation.update({ await updateNationMeta(
where: { id: nation.id }, ctx,
data: { nation.id,
meta: { {
...nationMeta, npc_nation_policy: nextPolicyRoot,
npc_nation_policy: nextPolicyRoot,
},
}, },
}); nationMeta
);
return { ok: true }; return { ok: true };
}), }),
@@ -722,15 +749,14 @@ export const npcRouter = router({
prioritySetTime: new Date().toISOString(), prioritySetTime: new Date().toISOString(),
}; };
await ctx.db.nation.update({ await updateNationMeta(
where: { id: nation.id }, ctx,
data: { nation.id,
meta: { {
...nationMeta, npc_nation_policy: nextPolicyRoot,
npc_nation_policy: nextPolicyRoot,
},
}, },
}); nationMeta
);
return { ok: true }; return { ok: true };
}), }),
@@ -778,15 +804,14 @@ export const npcRouter = router({
prioritySetTime: new Date().toISOString(), prioritySetTime: new Date().toISOString(),
}; };
await ctx.db.nation.update({ await updateNationMeta(
where: { id: nation.id }, ctx,
data: { nation.id,
meta: { {
...nationMeta, npc_general_policy: nextPolicyRoot,
npc_general_policy: nextPolicyRoot,
},
}, },
}); nationMeta
);
return { ok: true }; return { ok: true };
}), }),
@@ -282,6 +282,18 @@ const normalizeCommand = (envelope: TurnDaemonCommandEnvelope): TurnDaemonComman
top4, top4,
}; };
} }
case 'setNationMeta': {
if (typeof command.nationId !== 'number' || !command.updates || typeof command.updates !== 'object') {
return null;
}
return {
type: 'setNationMeta',
requestId: envelope.requestId,
nationId: command.nationId,
updates: command.updates as Record<string, unknown>,
expectedUpdatedAt: typeof command.expectedUpdatedAt === 'string' ? command.expectedUpdatedAt : undefined,
};
}
case 'getStatus': { case 'getStatus': {
const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId; const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId;
return { type: 'getStatus', requestId }; return { type: 'getStatus', requestId };
@@ -41,6 +41,52 @@ interface TournamentRewardFinalizer {
finalize(command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>): Promise<TurnDaemonCommandResult>; finalize(command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>): Promise<TurnDaemonCommandResult>;
} }
async function handleSetNationMeta(
ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'setNationMeta' }>
): Promise<TurnDaemonCommandResult> {
const { world, hooks } = ctx;
const nation = world.getNationById(command.nationId);
if (!nation) {
return {
type: 'setNationMeta',
ok: false,
nationId: command.nationId,
reason: '국가 정보를 찾을 수 없습니다.',
};
}
const meta = (nation.meta ?? {}) as Record<string, unknown>;
const currentUpdatedAt = typeof meta._updatedAt === 'string' ? meta._updatedAt : undefined;
if (command.expectedUpdatedAt && currentUpdatedAt && command.expectedUpdatedAt !== currentUpdatedAt) {
return {
type: 'setNationMeta',
ok: false,
nationId: command.nationId,
reason: 'CONFLICT',
currentUpdatedAt,
};
}
const updatedAt = new Date().toISOString();
const nextMeta = {
...meta,
...command.updates,
_updatedAt: updatedAt,
};
world.updateNation(command.nationId, {
meta: nextMeta,
});
await flushWorld(world, hooks);
return {
type: 'setNationMeta',
ok: true,
nationId: command.nationId,
updatedAt,
};
}
async function handleTroopJoin( async function handleTroopJoin(
ctx: CommandHandlerContext, ctx: CommandHandlerContext,
command: Extract<TurnDaemonCommand, { type: 'troopJoin' }> command: Extract<TurnDaemonCommand, { type: 'troopJoin' }>
@@ -614,6 +660,8 @@ export const createTurnDaemonCommandHandler = (options: {
return handleTournamentBettingPayout(ctx, command); return handleTournamentBettingPayout(ctx, command);
case 'tournamentReward': case 'tournamentReward':
return handleTournamentReward(ctx, command); return handleTournamentReward(ctx, command);
case 'setNationMeta':
return handleSetNationMeta(ctx, command);
default: default:
return null; return null;
} }
+22 -2
View File
@@ -115,7 +115,14 @@ export type TurnDaemonCommand =
top16: number[]; top16: number[];
top8: number[]; top8: number[];
top4: number[]; top4: number[];
}; }
| {
type: 'setNationMeta';
requestId?: string;
nationId: number;
updates: Record<string, unknown>;
expectedUpdatedAt?: string;
};
export type TurnDaemonCommandResult = export type TurnDaemonCommandResult =
| { | {
@@ -207,7 +214,20 @@ export type TurnDaemonCommandResult =
winnerId: number; winnerId: number;
runnerUpId: number; runnerUpId: number;
reason: string; reason: string;
}; }
| {
type: 'setNationMeta';
ok: true;
nationId: number;
updatedAt: string;
}
| {
type: 'setNationMeta';
ok: false;
nationId: number;
reason: string;
currentUpdatedAt?: string;
};
export type TurnDaemonEvent = export type TurnDaemonEvent =
| { type: 'status'; requestId?: string; status: TurnDaemonStatus } | { type: 'status'; requestId?: string; status: TurnDaemonStatus }