merge: 최신 main을 개인 봉급 표시에 통합한다

This commit is contained in:
2026-08-21 17:12:21 +00:00
3 changed files with 60 additions and 6 deletions
+3 -2
View File
@@ -79,7 +79,7 @@ export const troopRouter = router({
troopLeaderIds.length === 0 troopLeaderIds.length === 0
? [] ? []
: await ctx.db.generalTurn.findMany({ : await ctx.db.generalTurn.findMany({
where: { generalId: { in: troopLeaderIds } }, where: { generalId: { in: troopLeaderIds }, turnIdx: { lt: 5 } },
select: { generalId: true, turnIdx: true, actionCode: true }, select: { generalId: true, turnIdx: true, actionCode: true },
orderBy: [{ generalId: 'asc' }, { turnIdx: 'asc' }], orderBy: [{ generalId: 'asc' }, { turnIdx: 'asc' }],
}); });
@@ -94,7 +94,8 @@ export const troopRouter = router({
: 30; : 30;
for (const turn of turns) { for (const turn of turns) {
const list = reservedByLeader.get(turn.generalId) ?? []; const list = reservedByLeader.get(turn.generalId) ?? [];
list.push(turn.actionCode); // Ref 부대 편성은 앞쪽 슬롯이 집합인지 여부만 공개하고 다른 명령은 가립니다.
list.push(turn.actionCode === 'che_집합' ? '집합' : '-');
reservedByLeader.set(turn.generalId, list); reservedByLeader.set(turn.generalId, list);
} }
+28 -2
View File
@@ -77,10 +77,12 @@ const buildContext = (options: {
auth?: GameSessionTokenPayload | null; auth?: GameSessionTokenPayload | null;
requestId?: string; requestId?: string;
transaction?: ReturnType<typeof vi.fn>; transaction?: ReturnType<typeof vi.fn>;
turns?: Array<{ generalId: number; turnIdx: number; actionCode: string }>;
result: Awaited<ReturnType<TurnDaemonTransport['requestCommand']>>; result: Awaited<ReturnType<TurnDaemonTransport['requestCommand']>>;
}) => { }) => {
const me = options.me ?? buildGeneral(); const me = options.me ?? buildGeneral();
const requestCommand = vi.fn(async () => options.result); const requestCommand = vi.fn(async () => options.result);
const generalTurnFindMany = vi.fn(async () => options.turns ?? []);
const db = { const db = {
...(options.transaction ? { $transaction: options.transaction } : {}), ...(options.transaction ? { $transaction: options.transaction } : {}),
general: { general: {
@@ -110,7 +112,7 @@ const buildContext = (options: {
}, },
city: { findMany: vi.fn(async () => [{ id: 1, name: '북평' }]) }, city: { findMany: vi.fn(async () => [{ id: 1, name: '북평' }]) },
worldState: { findFirst: vi.fn(async () => ({ config: { const: { upgradeLimit: 20 } } })) }, worldState: { findFirst: vi.fn(async () => ({ config: { const: { upgradeLimit: 20 } } })) },
generalTurn: { findMany: vi.fn(async () => []) }, generalTurn: { findMany: generalTurnFindMany },
}; };
const accessTokenStore = new RedisAccessTokenStore( const accessTokenStore = new RedisAccessTokenStore(
{ {
@@ -134,7 +136,7 @@ const buildContext = (options: {
flushStore: new InMemoryFlushStore(), flushStore: new InMemoryFlushStore(),
gameTokenSecret: 'test-secret', gameTokenSecret: 'test-secret',
}; };
return { context, requestCommand }; return { context, requestCommand, generalTurnFindMany };
}; };
describe('troop router permissions and mutations', () => { describe('troop router permissions and mutations', () => {
@@ -175,6 +177,30 @@ describe('troop router permissions and mutations', () => {
}); });
}); });
it('returns only the first five Ref-redacted troop command labels without exposing action codes', async () => {
const fixture = buildContext({
me: buildGeneral({ troopId: 1 }),
turns: [
{ generalId: 1, turnIdx: 0, actionCode: 'che_집합' },
{ generalId: 1, turnIdx: 1, actionCode: 'che_이동' },
{ generalId: 1, turnIdx: 2, actionCode: 'che_징병' },
{ generalId: 1, turnIdx: 3, actionCode: '휴식' },
{ generalId: 1, turnIdx: 4, actionCode: 'che_화계' },
],
result: null,
});
const result = await appRouter.createCaller(fixture.context).troop.getList();
expect(result.troops[0]?.reservedCommands).toEqual(['집합', '-', '-', '-', '-']);
expect(JSON.stringify(result.troops)).not.toContain('che_');
expect(fixture.generalTurnFindMany).toHaveBeenCalledWith({
where: { generalId: { in: [1] }, turnIdx: { lt: 5 } },
select: { generalId: true, turnIdx: true, actionCode: true },
orderBy: [{ generalId: 'asc' }, { turnIdx: 'asc' }],
});
});
it('creates a troop only for the general owned by the authenticated user', async () => { it('creates a troop only for the general owned by the authenticated user', async () => {
const { context, requestCommand } = buildContext({ const { context, requestCommand } = buildContext({
result: { type: 'troopCreate', ok: true, generalId: 1, troopId: 1, troopName: '백마대' }, result: { type: 'troopCreate', ok: true, generalId: 1, troopId: 1, troopName: '백마대' },
+29 -2
View File
@@ -80,7 +80,7 @@ const baseTroops = (): TroopFixture[] => [
name: '백마대', name: '백마대',
nationId: 1, nationId: 1,
turnTime: '2026-07-25T08:20:30.000Z', turnTime: '2026-07-25T08:20:30.000Z',
reservedCommands: ['che_집합', 'che_이동'], reservedCommands: ['집합', '-'],
leader: { leader: {
id: 1, id: 1,
name: '공손찬', name: '공손찬',
@@ -96,7 +96,7 @@ const baseTroops = (): TroopFixture[] => [
name: '청룡대', name: '청룡대',
nationId: 1, nationId: 1,
turnTime: '2026-07-25T08:30:30.000Z', turnTime: '2026-07-25T08:30:30.000Z',
reservedCommands: ['che_징병'], reservedCommands: ['-'],
leader: { leader: {
id: 2, id: 2,
name: '관우', name: '관우',
@@ -224,6 +224,27 @@ const installApiFixture = async (page: Page, state: FixtureState) => {
}); });
}; };
test('shows only the Ref-safe first-five troop command labels on desktop and mobile', async ({ page }) => {
await installApiFixture(page, {
me: { id: 1, troopId: 1 },
permission: 4,
troops: baseTroops(),
});
for (const viewport of [
{ width: 1000, height: 800 },
{ width: 500, height: 800 },
]) {
await page.setViewportSize(viewport);
await gotoTroop(page);
const firstTroopCommands = page.locator('.troopReservedCommand').first();
await expect(firstTroopCommands).toContainText('1: 집합');
await expect(firstTroopCommands).toContainText('2: -');
await expect(page.locator('#troopList')).not.toContainText('che_');
}
});
test('renders the legacy desktop grid with matching computed geometry and states', async ({ page }) => { test('renders the legacy desktop grid with matching computed geometry and states', async ({ page }) => {
await installApiFixture(page, { await installApiFixture(page, {
me: { id: 1, troopId: 1 }, me: { id: 1, troopId: 1 },
@@ -233,6 +254,9 @@ test('renders the legacy desktop grid with matching computed geometry and states
await page.setViewportSize({ width: 1000, height: 800 }); await page.setViewportSize({ width: 1000, height: 800 });
await gotoTroop(page); await gotoTroop(page);
await expect(page.locator('.troopInfo').filter({ hasText: '백마대' })).toBeVisible(); await expect(page.locator('.troopInfo').filter({ hasText: '백마대' })).toBeVisible();
await expect(page.locator('.troopReservedCommand').first()).toContainText('1: 집합');
await expect(page.locator('.troopReservedCommand').first()).toContainText('2: -');
await expect(page.locator('#troopList')).not.toContainText('che_');
const geometry = await page const geometry = await page
.locator('.troopItem') .locator('.troopItem')
@@ -319,6 +343,9 @@ test('matches the legacy 500px responsive placement', async ({ page }) => {
await page.setViewportSize({ width: 500, height: 800 }); await page.setViewportSize({ width: 500, height: 800 });
await gotoTroop(page); await gotoTroop(page);
await expect(page.locator('.troopInfo').filter({ hasText: '백마대' })).toBeVisible(); await expect(page.locator('.troopInfo').filter({ hasText: '백마대' })).toBeVisible();
await expect(page.locator('.troopReservedCommand').first()).toContainText('1: 집합');
await expect(page.locator('.troopReservedCommand').first()).toContainText('2: -');
await expect(page.locator('#troopList')).not.toContainText('che_');
const geometry = await page const geometry = await page
.locator('.troopItem') .locator('.troopItem')