diff --git a/app/game-api/src/router/general/index.ts b/app/game-api/src/router/general/index.ts index a6d92a27..dbd481d8 100644 --- a/app/game-api/src/router/general/index.ts +++ b/app/game-api/src/router/general/index.ts @@ -414,10 +414,10 @@ export const generalRouter = router({ generalId: me.id, scope: LogScope.GENERAL, category: categoryMap[input.type], - ...(input.beforeId ? { id: { lt: input.beforeId } } : {}), + ...(input.type !== 'generalHistory' && input.beforeId ? { id: { lt: input.beforeId } } : {}), }, orderBy: { id: 'desc' }, - take: 24, + ...(input.type === 'generalHistory' ? {} : { take: 24 }), }); return { diff --git a/app/game-api/src/router/nation/endpoints/getGeneralLog.ts b/app/game-api/src/router/nation/endpoints/getGeneralLog.ts index aa472502..a9c612de 100644 --- a/app/game-api/src/router/nation/endpoints/getGeneralLog.ts +++ b/app/game-api/src/router/nation/endpoints/getGeneralLog.ts @@ -12,6 +12,7 @@ export const getGeneralLog = authedProcedure z.object({ generalId: z.number().int().positive(), type: zGeneralLogType, + beforeId: z.number().int().positive().optional(), }) ) .query(async ({ ctx, input }) => { @@ -43,12 +44,7 @@ export const getGeneralLog = authedProcedure if (target.nationId !== me.nationId) { throw new TRPCError({ code: 'FORBIDDEN', message: '같은 나라의 장수가 아닙니다.' }); } - if ( - input.type === 'generalAction' && - target.npcState < 2 && - target.id !== me.id && - permissionLevel < 2 - ) { + if (input.type === 'generalAction' && target.npcState < 2 && target.id !== me.id && permissionLevel < 2) { throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다. 유저 장수의 개인 기록은 수뇌만 열람 가능합니다.', @@ -67,9 +63,10 @@ export const getGeneralLog = authedProcedure generalId: target.id, scope: LogScope.GENERAL, category: categoryMap[input.type], + ...(input.type !== 'generalHistory' && input.beforeId ? { id: { lt: input.beforeId } } : {}), }, orderBy: { id: 'desc' }, - take: 30, + ...(input.type === 'generalHistory' ? {} : { take: 30 }), }); return { diff --git a/app/game-api/test/inGameMenuPermissions.test.ts b/app/game-api/test/inGameMenuPermissions.test.ts index fdd0dc31..6480029c 100644 --- a/app/game-api/test/inGameMenuPermissions.test.ts +++ b/app/game-api/test/inGameMenuPermissions.test.ts @@ -79,6 +79,7 @@ const createContext = (options: { nationMeta?: Record; requestCommand?: ReturnType; accessToken?: string; + logs?: Array<{ id: number; text: string }>; }) => { const me = options.me === undefined ? buildGeneral() : options.me; const targets = options.targets ?? (me ? [me] : []); @@ -118,7 +119,12 @@ const createContext = (options: { }, logEntry: { groupBy: vi.fn(async () => []), - findMany: vi.fn(async () => [{ id: 1, text: '기록' }]), + findMany: vi.fn(async (query?: { where?: { id?: { lt?: number } }; take?: number }) => { + const source = options.logs ?? [{ id: 1, text: '기록' }]; + const beforeId = query?.where?.id?.lt; + const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source; + return query?.take ? filtered.slice(0, query.take) : filtered; + }), }, }; const redisClient = { get: async () => null, set: async () => null }; @@ -192,6 +198,19 @@ describe('in-game my information ownership', () => { ); }); + it('returns the complete personal history while preserving bounded action pages', async () => { + const logs = Array.from({ length: 61 }, (_, index) => ({ id: 61 - index, text: `기록-${61 - index}` })); + const fixture = createContext({ logs }); + const caller = appRouter.createCaller(fixture.context); + + await expect(caller.general.getMyLog({ type: 'generalHistory' })).resolves.toMatchObject({ + logs, + }); + await expect(caller.general.getMyLog({ type: 'generalAction' })).resolves.toMatchObject({ + logs: logs.slice(0, 24), + }); + }); + it('returns the three legacy front-page record streams for the session-owned general', async () => { const fixture = createContext({}); const caller = appRouter.createCaller(fixture.context); @@ -436,4 +455,26 @@ describe('battle-center general and user permissions', () => { .nation.getGeneralLog({ generalId: otherUser.id, type: 'generalAction' }) ).resolves.toMatchObject({ generalId: otherUser.id }); }); + + it('returns all nation history and paginates action logs in legacy 30-row pages', async () => { + const logs = Array.from({ length: 61 }, (_, index) => ({ id: 61 - index, text: `기록-${61 - index}` })); + const fixture = createContext({ + me: buildGeneral({ officerLevel: 5 }), + logs, + }); + const caller = appRouter.createCaller(fixture.context); + + await expect(caller.nation.getGeneralLog({ generalId: 7, type: 'generalHistory' })).resolves.toMatchObject({ + logs, + }); + await expect(caller.nation.getGeneralLog({ generalId: 7, type: 'generalAction' })).resolves.toMatchObject({ + logs: logs.slice(0, 30), + }); + await expect( + caller.nation.getGeneralLog({ generalId: 7, type: 'generalAction', beforeId: 32 }) + ).resolves.toMatchObject({ logs: logs.slice(30, 60) }); + await expect( + caller.nation.getGeneralLog({ generalId: 7, type: 'generalAction', beforeId: 2 }) + ).resolves.toMatchObject({ logs: logs.slice(60) }); + }); });