From 7dc167093514e966acd6ba4d100ad818564b579a Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 15 Sep 2026 07:42:37 +0000 Subject: [PATCH] =?UTF-8?q?=EA=B0=90=EC=B0=B0=EB=B6=80=20=EA=B0=9C?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=A1=9D=20=EA=B6=8C=ED=95=9C=20=EA=B1=B0?= =?UTF-8?q?=EB=B6=80=20=EC=8B=9C=20=ED=97=88=EC=9A=A9=EB=90=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=ED=91=9C=EC=8B=9C=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/inGameMenuPermissions.test.ts | 46 ++++++++++++++ app/game-frontend/e2e/inGameMenus.spec.ts | 61 +++++++++++++++++-- .../src/views/BattleCenterView.vue | 15 ++++- 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/app/game-api/test/inGameMenuPermissions.test.ts b/app/game-api/test/inGameMenuPermissions.test.ts index de714871..2bff98f5 100644 --- a/app/game-api/test/inGameMenuPermissions.test.ts +++ b/app/game-api/test/inGameMenuPermissions.test.ts @@ -945,6 +945,52 @@ describe('in-game my information ownership', () => { }); describe('battle-center general and user permissions', () => { + it.each([ + { role: '연수 미달', officerLevel: 1, belong: 2, permission: 'normal', penalty: {}, level: 0 }, + { role: '연수 충족', officerLevel: 1, belong: 3, permission: 'normal', penalty: {}, level: 1 }, + { role: '연수 초과', officerLevel: 1, belong: 4, permission: 'normal', penalty: {}, level: 1 }, + { role: '지방 관직', officerLevel: 2, belong: 0, permission: 'normal', penalty: {}, level: 1 }, + { role: '수뇌', officerLevel: 5, belong: 0, permission: 'normal', penalty: {}, level: 2 }, + { role: '군주', officerLevel: 12, belong: 0, permission: 'normal', penalty: {}, level: 4 }, + { role: '외교권자', officerLevel: 1, belong: 0, permission: 'ambassador', penalty: {}, level: 4 }, + { role: '조언자', officerLevel: 1, belong: 0, permission: 'auditor', penalty: {}, level: 3 }, + { role: '수뇌 금지', officerLevel: 12, belong: 4, permission: 'normal', penalty: { noChief: true }, level: 0 }, + { + role: '기밀 제한', + officerLevel: 12, + belong: 4, + permission: 'normal', + penalty: { noTopSecret: true }, + level: 1, + }, + { + role: '외교 제한', + officerLevel: 12, + belong: 4, + permission: 'normal', + penalty: { noAmbassador: true }, + level: 2, + }, + ])('Ref 감찰부 권한: $role', async ({ officerLevel, belong, permission, penalty, level }) => { + const me = buildGeneral({ officerLevel, meta: { belong, permission }, penalty }); + const other = buildGeneral({ id: 8, userId: 'user-8' }); + const fixture = createContext({ me, targets: [me, other], nationMeta: { secretLimit: 3 } }); + const caller = appRouter.createCaller(fixture.context); + if (level < 1) { + await expect(caller.nation.getBattleCenter()).rejects.toMatchObject({ code: 'FORBIDDEN' }); + } else { + await expect(caller.nation.getBattleCenter()).resolves.toMatchObject({ me: { permissionLevel: level } }); + } + for (const type of ['generalHistory', 'battleDetail', 'battleResult', 'generalAction'] as const) { + const request = caller.nation.getGeneralLog({ generalId: other.id, type }); + if (level < 1 || (type === 'generalAction' && level < 2)) { + await expect(request).rejects.toMatchObject({ code: 'FORBIDDEN' }); + } else { + await expect(request).resolves.toMatchObject({ generalId: other.id, type }); + } + } + }); + it('distinguishes an ordinary member, a tenured member, and an auditor', async () => { const ordinary = createContext({ me: buildGeneral({ officerLevel: 1, meta: { belong: 1, permission: 'normal' } }), diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 9cd9f019..1a5dbe28 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -61,6 +61,7 @@ const readGeneralSummaryRows = async (summary: Locator) => type FixtureState = { permission: 'head' | 'member'; + battleTenured?: boolean; myset: number; scenarioEffect?: string | null; instantRetreatEnabled?: boolean; @@ -295,7 +296,7 @@ const battleCenter = (state: FixtureState) => ({ me: { id: 7, officerLevel: state.permission === 'head' ? 9 : 1, - permissionLevel: state.permission === 'head' ? 2 : 0, + permissionLevel: state.battleTenured ? 1 : state.permission === 'head' ? 2 : 0, }, nation: { id: 1, name: '위', color: '#777777', level: 3 }, currentYear: 185, @@ -344,7 +345,7 @@ const battleCenter = (state: FixtureState) => ({ { id: 8, name: '다른장수', - npcState: 2, + npcState: state.battleTenured ? 0 : 2, officerLevel: 1, officerLevelText: '일반', cityId: 1, @@ -644,7 +645,7 @@ const install = async (page: Page, state: FixtureState) => { return response({ recorded: true }); } if (operation === 'nation.getBattleCenter') { - if (state.permission === 'member') { + if (state.permission === 'member' && !state.battleTenured) { return { error: { message: '권한이 부족합니다.', @@ -661,9 +662,18 @@ const install = async (page: Page, state: FixtureState) => { ['generalHistory', 'battleDetail', 'battleResult', 'generalAction'].includes(jsonInput.type) ? jsonInput.type : 'generalAction'; + if (state.battleTenured && jsonInput.generalId === 8 && type === 'generalAction') { + return { + error: { + message: '권한이 부족합니다. 유저 장수의 개인 기록은 수뇌만 열람 가능합니다.', + code: -32000, + data: { code: 'FORBIDDEN', httpStatus: 403, path: operation }, + }, + }; + } return response({ type, - generalId: 7, + generalId: jsonInput.generalId, logs: [{ id: 1, text: state.hiddenSeedLogText ?? `${type} 감찰 기록` }], }); } @@ -2574,3 +2584,46 @@ test('감찰부 하단 창 닫기 버튼은 상단 버튼과 같은 크기를 const mobile = await measure(); expect(mobile.bottom).toEqual(mobile.top); }); + +test('감찰부 사관 연수를 채운 일반 장수는 개인 기록 거부에도 열전과 전투 기록을 본다', async ({ page }) => { + const state: FixtureState = { + permission: 'member', + battleTenured: true, + myset: 3, + settingMutations: [], + accessPages: [], + }; + await install(page, state); + for (const width of [1000, 500]) { + await page.setViewportSize({ width, height: 900 }); + await page.goto('battle-center'); + const selector = page.locator('.selector-row select').nth(1); + await expect(selector).toHaveValue('8'); + for (const type of ['generalHistory', 'battleResult', 'battleDetail']) { + await expect(page.locator(`[data-log-type="${type}"]`)).toContainText(`${type} 감찰 기록`); + } + await expect(page.getByRole('alert')).toContainText('개인 기록은 수뇌만'); + await expect(page.locator('[data-log-type="generalAction"]')).not.toContainText('generalAction 감찰 기록'); + await expect(page.locator('.battle-general-card')).toBeVisible(); + await waitForVisualAssets(page); + await persistParityArtifact( + page, + `core-battle-permission-${width}`, + await page.locator('.log-block').evaluateAll((elements) => + elements.map((element) => ({ + type: element.getAttribute('data-log-type'), + text: element.textContent, + rect: element.getBoundingClientRect().toJSON(), + font: getComputedStyle(element).font, + })) + ) + ); + await selector.selectOption('7'); + await expect(page.locator('[data-log-type="generalAction"]')).toContainText('generalAction 감찰 기록'); + await expect(page.getByRole('alert')).toHaveCount(0); + await selector.selectOption('8'); + await expect(page.getByRole('alert')).toContainText('개인 기록은 수뇌만'); + await expect(page.locator('[data-log-type="generalAction"]')).not.toContainText('generalAction 감찰 기록'); + await expect(page.locator('[data-log-type="battleResult"]')).toContainText('battleResult 감찰 기록'); + } +}); diff --git a/app/game-frontend/src/views/BattleCenterView.vue b/app/game-frontend/src/views/BattleCenterView.vue index 4362e72e..e60ca0a8 100644 --- a/app/game-frontend/src/views/BattleCenterView.vue +++ b/app/game-frontend/src/views/BattleCenterView.vue @@ -153,16 +153,27 @@ const loadLogs = async (generalId: number) => { return; } logLoading.value = true; + error.value = null; + // 대상 전환 시 이전 장수의 기록이 새 장수 정보와 섞이지 않게 비운다. + for (const type of logTypes) { + logs[type] = []; + } const requestId = (logRequestId += 1); try { - const responses = await Promise.all( + const responses = await Promise.allSettled( logTypes.map((type) => trpc.nation.getGeneralLog.query({ generalId, type })) ); if (requestId !== logRequestId || selectedGeneralId.value !== generalId) { return; } - for (const response of responses) { + // Ref처럼 개인 기록의 권한 거부가 열전·전투 기록까지 지우지 않게 한다. + for (const result of responses) { + if (result.status === 'rejected') { + error.value = resolveErrorMessage(result.reason); + continue; + } + const response = result.value; const formatted = response.logs.map((entry) => { const eventTime = response.type === 'generalAction'