diff --git a/app/game-frontend/e2e/playAudit.spec.ts b/app/game-frontend/e2e/playAudit.spec.ts
index 6aca2e1f..52bda458 100644
--- a/app/game-frontend/e2e/playAudit.spec.ts
+++ b/app/game-frontend/e2e/playAudit.spec.ts
@@ -61,7 +61,7 @@ const decision = {
coverage: 'PROCEDURES',
clockRevision: 1,
codeVersion: null,
- policyRefs: {},
+ policyRefs: { DEFENCE: 'a'.repeat(64) },
requestedAction: '휴식',
selectedAction: 'che_징병',
selectedReason: '징병 선택',
@@ -1154,3 +1154,30 @@ test('NPC decision detail retry preserves history and other general information'
expect(requests.filter((r) => r.operation === 'playAudit.decisionHistory')).toHaveLength(count);
await capture(page, 'desktop-npc-decision');
});
+
+test('NPC decision opens its immutable policy without querying policy history', async ({ page }) => {
+ const requests = await install(page);
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto(gamePath(`/play-audit?tab=generals&general=1&decision=${decision.id}`));
+ await expect(page.getByRole('list', { name: '판단 절차' })).toBeVisible();
+ expect(requests.some((r) => r.operation === 'playAudit.policyVersion')).toBe(false);
+ const before = requests.length;
+ await page.getByText('당시 정책 참조', { exact: true }).click();
+ await page.getByRole('button', { name: '국방 설정 당시 버전 조회', exact: true }).click();
+ await expect(page.getByText(/국가 #2 · 버전 1/)).toBeVisible();
+ expect(requests.slice(before).map((r) => r.operation)).toEqual(['playAudit.policyVersion']);
+ expect(requests.at(-1)?.input).toEqual({ id: 'a'.repeat(64) });
+ await expect(page.getByRole('button', { name: '이전 정책 버전', exact: true })).toHaveCount(0);
+ expect(await page.evaluate(() => Object.hasOwn(window, 'auditInjected'))).toBe(false);
+ await capture(page, 'mobile-decision-policy');
+ await page.reload();
+ await expect(page.getByText(/국가 #2 · 버전 1/)).toBeVisible();
+ expect(requests.some((r) => r.operation === 'playAudit.policyHistory')).toBe(false);
+ await page.getByRole('button', { name: '정책 상세 닫기', exact: true }).click();
+ await expect(page.getByRole('heading', { name: /선택 정책 버전/ })).toHaveCount(0);
+ const policyReads = requests.filter((r) => r.operation === 'playAudit.policyVersion').length;
+ await page.goto(gamePath(`/play-audit?tab=generals&general=1&decision=${decision.id}&policy=${'b'.repeat(64)}`));
+ await expect(page.getByRole('list', { name: '판단 절차' })).toBeVisible();
+ expect(requests.filter((r) => r.operation === 'playAudit.policyVersion')).toHaveLength(policyReads);
+ await expect(page.getByRole('heading', { name: /선택 정책 버전/ })).toHaveCount(0);
+});
diff --git a/app/game-frontend/src/components/playAudit/AuditGeneralDecisions.vue b/app/game-frontend/src/components/playAudit/AuditGeneralDecisions.vue
index d8656b96..4065ff9e 100644
--- a/app/game-frontend/src/components/playAudit/AuditGeneralDecisions.vue
+++ b/app/game-frontend/src/components/playAudit/AuditGeneralDecisions.vue
@@ -2,6 +2,7 @@
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { trpc } from '../../utils/trpc';
+import AuditPolicyVersion from './AuditPolicyVersion.vue';
const props = defineProps<{ generalId: number; month?: { year: number; month: number } }>();
const route = useRoute();
const router = useRouter();
@@ -17,6 +18,19 @@ const detailLoading = ref(false);
let generation = 0;
let detailGeneration = 0;
const selected = computed(() => (typeof route.query.decision === 'string' ? route.query.decision : null));
+const policyLabels = {
+ NPC_VALUES: 'NPC 설정 값',
+ NPC_NATION_PRIORITY: '수뇌 우선순위',
+ NPC_GENERAL_PRIORITY: '개인 우선순위',
+ DEFENCE: '국방 설정',
+};
+const selectedPolicy = computed(() => {
+ const id = route.query.policy;
+ return typeof id === 'string' && Object.values(detail.value?.decision.summary.policyRefs ?? {}).includes(id)
+ ? id
+ : null;
+});
+const selectPolicy = (id: string | null) => router.push({ query: { ...route.query, policy: id ?? undefined } });
const message = (cause: unknown) => (cause instanceof Error ? cause.message : 'NPC 결정 기록을 조회하지 못했습니다.');
const load = async (more = false) => {
if (loading.value) return;
@@ -191,8 +205,19 @@ watch(
확보된 정책 참조가 없습니다. {{ area }}: {{ id }} 선택 당시 저장된 국가 설정입니다. NPC별 합성 유효 값과 다를 수 있습니다.
+
+ 당시 정책 참조
정책 버전 조회 중…
-- {{ detailError }} -
- -- 국가 #{{ detail.version.nationId }} · 버전 {{ detail.version.revision }} · - {{ labels[detail.version.source] }} · {{ detail.version.year }}년 {{ detail.version.month }}월 -
-- 기록 시각 {{ detail.version.createdAt }} · tick {{ detail.version.tick ?? '미상' }} · 순번 - {{ detail.version.ordinal }} -
-- {{ detail.version.actor.name }} (#{{ detail.version.actor.generalId }}) · 당시 국가 #{{ - detail.version.actor.nationId - }} - · 직책 {{ detail.version.actor.officerLevel }} -
-- 이 버전은 관측 기준입니다. 이전 값과 변경 주체를 추정하지 않습니다. -
-- null은 개별 설정이 없음을 뜻합니다. 설정값을 기록하며 당시 NPC별 유효 값은 여기서 재계산하지 - 않습니다. -
-| 설정 | -변경 전 | -변경 후 | -
|---|---|---|
| - {{ fieldLabels[field.key] ?? field.key }} (변경) - | -
- {{ field.beforeJson ?? '관측하지 않음' }}
- |
-
- {{ field.afterJson }}
- |
-
요청 {{ detail.version.requestId ?? '해당 없음' }}
-입력 순번 {{ detail.version.inputSequence ?? '해당 없음' }}
-정책 버전 조회 중…
++ {{ detailError }} +
+ ++ 국가 #{{ detail.version.nationId }} · 버전 {{ detail.version.revision }} · + {{ labels[detail.version.source] }} · {{ detail.version.year }}년 {{ detail.version.month }}월 +
++ 기록 시각 {{ detail.version.createdAt }} · tick {{ detail.version.tick ?? '미상' }} · 순번 + {{ detail.version.ordinal }} +
++ {{ detail.version.actor.name }} (#{{ detail.version.actor.generalId }}) · 당시 국가 #{{ + detail.version.actor.nationId + }} + · 직책 {{ detail.version.actor.officerLevel }} +
++ 이 버전은 관측 기준입니다. 이전 값과 변경 주체를 추정하지 않습니다. +
++ null은 개별 설정이 없음을 뜻합니다. 설정값을 기록하며 당시 NPC별 유효 값은 여기서 재계산하지 않습니다. +
+| 설정 | +변경 전 | +변경 후 | +
|---|---|---|
| + {{ fieldLabels[field.key] ?? field.key }} (변경) + | +
+ {{ field.beforeJson ?? '관측하지 않음' }}
+ |
+
+ {{ field.afterJson }}
+ |
+
요청 {{ detail.version.requestId ?? '해당 없음' }}
+입력 순번 {{ detail.version.inputSequence ?? '해당 없음' }}
+