From 690a296f6ad57683bc737cc7133861b9db87b451 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 22 Aug 2026 08:20:46 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=97=B0=EA=B0=90=20=EC=9E=A5=EC=88=98?= =?UTF-8?q?=EB=8F=99=ED=96=A5=20=EC=88=98=EC=A7=91=20=EB=B6=84=EB=A5=98?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=94=EB=A1=9C=EC=9E=A1=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-api/src/router/yearbook/index.ts | 42 +++++++--- .../test/yearbookArchiveRouter.test.ts | 81 ++++++++++++++++++- .../src/turn/yearbookPersistence.ts | 2 +- ...BoundaryPrePersistence.integration.test.ts | 13 ++- .../test/yearbookPersistence.test.ts | 48 +++++++++++ 5 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 app/game-engine/test/yearbookPersistence.test.ts diff --git a/app/game-api/src/router/yearbook/index.ts b/app/game-api/src/router/yearbook/index.ts index 1f672934..162ca048 100644 --- a/app/game-api/src/router/yearbook/index.ts +++ b/app/game-api/src/router/yearbook/index.ts @@ -226,8 +226,22 @@ const buildNationSnapshot = async (ctx: GameApiContext) => { }); }; -const buildLogs = async (ctx: GameApiContext, year: number, month: number) => { - const [historyLogs, actionLogs] = await Promise.all([ +const readGlobalActionLogs = async (ctx: GameApiContext, year: number, month: number) => { + const actionLogs = await ctx.db.logEntry.findMany({ + where: { + scope: LogScope.SYSTEM, + category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] }, + year, + month, + }, + orderBy: { id: 'desc' }, + }); + + return actionLogs.map((entry) => entry.text); +}; + +const readLogs = async (ctx: GameApiContext, year: number, month: number) => { + const [historyLogs, globalAction] = await Promise.all([ ctx.db.logEntry.findMany({ where: { scope: LogScope.SYSTEM, @@ -237,20 +251,16 @@ const buildLogs = async (ctx: GameApiContext, year: number, month: number) => { }, orderBy: { id: 'desc' }, }), - ctx.db.logEntry.findMany({ - where: { - scope: LogScope.SYSTEM, - category: LogCategory.ACTION, - year, - month, - }, - orderBy: { id: 'desc' }, - }), + readGlobalActionLogs(ctx, year, month), ]); const globalHistory = historyLogs.map((entry) => entry.text); - const globalAction = actionLogs.map((entry) => entry.text); + return { globalHistory, globalAction }; +}; + +const buildLogs = async (ctx: GameApiContext, year: number, month: number) => { + const { globalHistory, globalAction } = await readLogs(ctx, year, month); return { globalHistory: globalHistory.length ? globalHistory : [`●${month}월: 기록 없음`], globalAction: globalAction.length ? globalAction : [`●${month}월: 기록 없음`], @@ -387,7 +397,13 @@ export const yearbookRouter = router({ const map = asRecord(row.map) as BaseMapResult; const nations = parseYearbookNations(row.nations); const globalHistory = normalizeArchivedLogs(row.globalHistory, input.month); - const globalAction = normalizeArchivedLogs(row.globalAction, input.month); + let globalAction = normalizeArchivedLogs(row.globalAction, input.month); + if (target.isCurrentProfile) { + const liveGlobalAction = await readGlobalActionLogs(ctx, input.year, input.month); + if (liveGlobalAction.length) { + globalAction = liveGlobalAction; + } + } const data = { year: input.year, month: input.month, diff --git a/app/game-api/test/yearbookArchiveRouter.test.ts b/app/game-api/test/yearbookArchiveRouter.test.ts index b75bd22a..46f4d9d6 100644 --- a/app/game-api/test/yearbookArchiveRouter.test.ts +++ b/app/game-api/test/yearbookArchiveRouter.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken'; import type { RedisConnector } from '@sammo-ts/infra'; +import { LogCategory } from '@sammo-ts/logic'; import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js'; import { InMemoryFlushStore } from '../src/auth/flushStore.js'; @@ -92,17 +93,49 @@ const authFor = (userId: string): GameSessionTokenPayload => ({ const buildContext = ( auth: GameSessionTokenPayload | null, - options: { hasGeneral?: boolean; worldMeta?: unknown } = {} + options: { + hasGeneral?: boolean; + worldMeta?: unknown; + liveLogs?: { history: string[]; action: string[] }; + } = {} ): GameApiContext => { const db = { + $queryRaw: async () => [], general: { findFirst: async ({ where }: { where: { userId: string } }) => options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId }, + findMany: async () => [], + }, + city: { + findMany: async () => [], + }, + nation: { + findMany: async () => [], + }, + logEntry: { + findMany: async ({ where }: { where: { category: unknown } }) => { + if (where.category === LogCategory.HISTORY) { + return (options.liveLogs?.history ?? []).map((text) => ({ text })); + } + const categories = + typeof where.category === 'object' && where.category !== null && 'in' in where.category + ? (where.category as { in: unknown }).in + : null; + if ( + Array.isArray(categories) && + categories.includes(LogCategory.SUMMARY) && + categories.includes(LogCategory.ACTION) + ) { + return (options.liveLogs?.action ?? []).map((text) => ({ text })); + } + return []; + }, }, worldState: { findFirst: async () => ({ currentYear: 220, currentMonth: 1, + config: {}, meta: options.worldMeta ?? { serverId: currentServerId }, }), }, @@ -247,4 +280,50 @@ describe('historical yearbook access from dynasty', () => { }, }); }); + + it('recovers an already archived current-generation month from retained summary logs', async () => { + const caller = appRouter.createCaller( + buildContext(authFor('owner-a'), { + liveLogs: { + history: ['복구한 현재 기수 과거 정세'], + action: ['복구한 현재 기수 장수 동향'], + }, + }) + ); + + const result = await caller.yearbook.getHistory({ + serverID: currentServerId, + year: 219, + month: 12, + }); + + expect(result).toMatchObject({ + notModified: false, + data: { + globalHistory: ['저장된 현재 기수 과거 기록'], + globalAction: ['복구한 현재 기수 장수 동향'], + }, + }); + }); + + it('returns summary and compatible action logs as the live month general trend', async () => { + const caller = appRouter.createCaller( + buildContext(authFor('owner-a'), { + liveLogs: { + history: ['현재 천하 동향'], + action: ['최신 호환 행동', '최신 장수 동향'], + }, + }) + ); + + const result = await caller.yearbook.getHistory({ year: 220, month: 1 }); + + expect(result).toMatchObject({ + notModified: false, + data: { + globalHistory: ['현재 천하 동향'], + globalAction: ['최신 호환 행동', '최신 장수 동향'], + }, + }); + }); }); diff --git a/app/game-engine/src/turn/yearbookPersistence.ts b/app/game-engine/src/turn/yearbookPersistence.ts index e57ff8fb..71e8a9ed 100644 --- a/app/game-engine/src/turn/yearbookPersistence.ts +++ b/app/game-engine/src/turn/yearbookPersistence.ts @@ -27,7 +27,7 @@ export const persistYearbookSnapshot = async ( transaction.logEntry.findMany({ where: { scope: LogScope.SYSTEM, - category: LogCategory.ACTION, + category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] }, year: snapshot.year, month: snapshot.month, }, diff --git a/app/game-engine/test/monthlyBoundaryPrePersistence.integration.test.ts b/app/game-engine/test/monthlyBoundaryPrePersistence.integration.test.ts index 40229c53..73700093 100644 --- a/app/game-engine/test/monthlyBoundaryPrePersistence.integration.test.ts +++ b/app/game-engine/test/monthlyBoundaryPrePersistence.integration.test.ts @@ -17,7 +17,7 @@ const cityIds = [991_201, 991_202, 991_203, 991_204, 991_205, 991_206, 991_207]; const nationId = 991_201; const yearbookProfile = 'monthly-boundary-pre-persistence'; const yearbookServerId = 'monthly-boundary-generation-20260731'; -const archivedLogTexts = ['월경계 과거 정세', '월경계 과거 행동']; +const archivedLogTexts = ['월경계 과거 정세', '월경계 과거 장수 동향', '월경계 과거 호환 행동']; integration('monthly pre-update persistence', () => { let db: GamePrismaClient; @@ -78,11 +78,18 @@ integration('monthly pre-update persistence', () => { }, { scope: LogScope.SYSTEM, - category: LogCategory.ACTION, + category: LogCategory.SUMMARY, year: 200, month: 12, text: archivedLogTexts[1]!, }, + { + scope: LogScope.SYSTEM, + category: LogCategory.ACTION, + year: 200, + month: 12, + text: archivedLogTexts[2]!, + }, ], }); await db.city.createMany({ @@ -256,7 +263,7 @@ integration('monthly pre-update persistence', () => { ); expect(cityIds.map((id) => yearbookStates.get(id))).toEqual([31, 32, 33, 34, 41, 42, 43]); expect(yearbookRow.globalHistory).toEqual([archivedLogTexts[0]]); - expect(yearbookRow.globalAction).toEqual([archivedLogTexts[1]]); + expect(yearbookRow.globalAction).toEqual([archivedLogTexts[2], archivedLogTexts[1]]); expect(await db.yearbookHistory.count({ where: { profileName: yearbookProfile } })).toBe(0); } finally { await hooks.close(); diff --git a/app/game-engine/test/yearbookPersistence.test.ts b/app/game-engine/test/yearbookPersistence.test.ts new file mode 100644 index 00000000..f30104a6 --- /dev/null +++ b/app/game-engine/test/yearbookPersistence.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { GamePrisma } from '@sammo-ts/infra'; +import { LogCategory, LogScope } from '@sammo-ts/logic'; + +import { persistYearbookSnapshot } from '../src/turn/yearbookPersistence.js'; + +describe('persistYearbookSnapshot', () => { + it('archives canonical summary logs and compatible action logs together in descending ID order', async () => { + const findMany = vi + .fn() + .mockResolvedValueOnce([{ text: '천하 동향' }]) + .mockResolvedValueOnce([{ text: '호환 행동' }, { text: '장수 동향' }]); + const upsert = vi.fn().mockResolvedValue(undefined); + const transaction = { + logEntry: { findMany }, + yearbookHistory: { upsert }, + } as unknown as GamePrisma.TransactionClient; + + await persistYearbookSnapshot(transaction, { + serverId: 'hwe:generation', + sourceId: 0, + year: 195, + month: 1, + map: { year: 195, month: 1 }, + nations: [], + }); + + expect(findMany).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + where: { + scope: LogScope.SYSTEM, + category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] }, + year: 195, + month: 1, + }, + orderBy: { id: 'desc' }, + }) + ); + expect(upsert).toHaveBeenCalledWith( + expect.objectContaining({ + create: expect.objectContaining({ globalAction: ['호환 행동', '장수 동향'] }), + update: expect.objectContaining({ globalAction: ['호환 행동', '장수 동향'] }), + }) + ); + }); +});