From b470e32e810c53bba07569070ba7791efdb85b75 Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 16 Sep 2026 08:20:29 +0000 Subject: [PATCH] =?UTF-8?q?NPC=20=EA=B2=B0=EC=A0=95=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=B9=EC=8B=9C=20=EC=A0=95=EC=B1=85=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=98=EA=B3=A0=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EC=9E=AC?= =?UTF-8?q?=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-frontend/e2e/playAudit.spec.ts | 29 ++- .../playAudit/AuditGeneralDecisions.vue | 30 +++- .../playAudit/AuditPolicyHistory.vue | 125 +------------ .../playAudit/AuditPolicyVersion.vue | 165 ++++++++++++++++++ docs/design/play-audit-implementation.md | 4 + docs/play-audit-operations.md | 4 +- 6 files changed, 230 insertions(+), 127 deletions(-) create mode 100644 app/game-frontend/src/components/playAudit/AuditPolicyVersion.vue 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별 합성 유효 값과 다를 수 있습니다.

+

+ +

+
    diff --git a/app/game-frontend/src/components/playAudit/AuditPolicyVersion.vue b/app/game-frontend/src/components/playAudit/AuditPolicyVersion.vue new file mode 100644 index 00000000..e1c4fd26 --- /dev/null +++ b/app/game-frontend/src/components/playAudit/AuditPolicyVersion.vue @@ -0,0 +1,165 @@ + + + diff --git a/docs/design/play-audit-implementation.md b/docs/design/play-audit-implementation.md index 604e4fb9..e4fa75c0 100644 --- a/docs/design/play-audit-implementation.md +++ b/docs/design/play-audit-implementation.md @@ -23,6 +23,10 @@ migration58은 기존 장수 인덱스를 `(server, general, year, month, tick, 버튼·표 스타일을 재사용한다. 열기/상세 선택/더 보기는 명시적으로 수행하고 polling하지 않는다. 결정 URL 복원과 상세 재시도는 상위 장수 목록을 다시 읽지 않는다. 절차 coverage와 미수집 코드 버전을 표시하며 전체 후보 조건·유효 정책 연결은 남는다. +당시 정책 참조는 공용 `AuditPolicyVersion`으로 연결했다. 기존 정책 이력의 버전 표시· +필드 한국어 이름·실패 재시도를 재사용하며 클릭 시 버전1건만 읽는다. 결정의 참조 ID에 +포함되지 않은 URL policy는 해당 결정의 정책으로 읽거나 표시하지 않는다. +국가의 저장 설정과 NPC별 합성 유효 값은 구분하며 현재 설정으로 보충하지 않는다. ### NPC 결정 저장·복구 기반 diff --git a/docs/play-audit-operations.md b/docs/play-audit-operations.md index 94cc3b6a..1f91faf9 100644 --- a/docs/play-audit-operations.md +++ b/docs/play-audit-operations.md @@ -16,7 +16,7 @@ | 도시 | 현재/월말 소유·내정 상태와 국가별 주둔 장수 상세 연결 | 월말 주둔은 그달 모든 방문자가 아님. 지도 기반 탐색은 미완성 | | 외교 | 국가쌍·기간별 문서 제안/승인/철회/파기, 즉시 합의와 월간·명령 관계 전이, 생성/소멸 관계 | 문서 내용과 실제 관계는 별개. 최초 관측 이전 사건은 복원하지 않음 | | NPC 결정 | 장수별 개인·수뇌 판단, 절차 시도/차단, 관측한 RNG 결과와 선택·실제 실행 | 새 실행부터 수집하며 후보 내부 조건 전체는 미완성 | -| 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면과 NPC 결정의 정책 참조 연결은 아직 없음 | +| 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면은 아직 없음. NPC 결정에서 저장된 당시 버전을 직접 조회 가능 | 목록/그래프와 선택 상세를 분리해 읽는다. 표는 기본50건이며 더 보기를 명시적으로 누른다. 현재 상태는 수동으로 조회하며 백그라운드 polling은 하지 않는다. 필터·월·선택 @@ -91,7 +91,7 @@ Gateway의 새 진입/권한 catalog도 사용하려면 같은 commit의 Gateway - NPC/유저 자동턴의 개인·수뇌 절차, 정책 차단, RNG utility 결과와 최종 실행 결과는 migration 이후 새 실행부터 저장한다. 후보 내부 조건 전체는 아직 없으며 `PROCEDURES` coverage로 구분한다. 장수 상세의 **NPC 결정 기록 조회**에서 선택 월의 목록과 순서별 상세를 읽는다. 과거 결정은 역산하지 않는다. -- 결정은 당시 확보된 정책 참조를 보존한다. 합성된 유효 정책 상세와 코드 버전 연결은 +- 결정은 당시 확보된 정책 참조를 보존하며 **당시 정책 참조**에서 해당 불변 버전을 바로 조회한다. 합성된 유효 정책 상세와 코드 버전 연결은 아직 미완성이다. 코드 버전이 주입되지 않은 실행은 null로 남기며 현재 버전으로 메우지 않는다. - 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진 버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다.