feat: 국가 메타 업데이트 기능 추가 및 관련 로직 구현
This commit is contained in:
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user