NPC 결정에서 당시 정책 버전을 조회하고 기존 상세 화면 재사용
This commit is contained in:
@@ -61,7 +61,7 @@ const decision = {
|
|||||||
coverage: 'PROCEDURES',
|
coverage: 'PROCEDURES',
|
||||||
clockRevision: 1,
|
clockRevision: 1,
|
||||||
codeVersion: null,
|
codeVersion: null,
|
||||||
policyRefs: {},
|
policyRefs: { DEFENCE: 'a'.repeat(64) },
|
||||||
requestedAction: '휴식',
|
requestedAction: '휴식',
|
||||||
selectedAction: 'che_징병',
|
selectedAction: 'che_징병',
|
||||||
selectedReason: '징병 선택',
|
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);
|
expect(requests.filter((r) => r.operation === 'playAudit.decisionHistory')).toHaveLength(count);
|
||||||
await capture(page, 'desktop-npc-decision');
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { trpc } from '../../utils/trpc';
|
import { trpc } from '../../utils/trpc';
|
||||||
|
import AuditPolicyVersion from './AuditPolicyVersion.vue';
|
||||||
const props = defineProps<{ generalId: number; month?: { year: number; month: number } }>();
|
const props = defineProps<{ generalId: number; month?: { year: number; month: number } }>();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -17,6 +18,19 @@ const detailLoading = ref(false);
|
|||||||
let generation = 0;
|
let generation = 0;
|
||||||
let detailGeneration = 0;
|
let detailGeneration = 0;
|
||||||
const selected = computed(() => (typeof route.query.decision === 'string' ? route.query.decision : null));
|
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 message = (cause: unknown) => (cause instanceof Error ? cause.message : 'NPC 결정 기록을 조회하지 못했습니다.');
|
||||||
const load = async (more = false) => {
|
const load = async (more = false) => {
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
@@ -191,8 +205,19 @@ watch(
|
|||||||
<details>
|
<details>
|
||||||
<summary>당시 정책 참조</summary>
|
<summary>당시 정책 참조</summary>
|
||||||
<p v-if="!Object.keys(detail.decision.summary.policyRefs).length">확보된 정책 참조가 없습니다.</p>
|
<p v-if="!Object.keys(detail.decision.summary.policyRefs).length">확보된 정책 참조가 없습니다.</p>
|
||||||
<p v-for="(id, area) in detail.decision.summary.policyRefs" :key="area">{{ area }}: {{ id }}</p>
|
<p>선택 당시 저장된 국가 설정입니다. NPC별 합성 유효 값과 다를 수 있습니다.</p>
|
||||||
|
<p v-for="(id, area) in detail.decision.summary.policyRefs" :key="area">
|
||||||
|
<button class="legacy-button" @click="selectPolicy(id ?? null)">
|
||||||
|
{{ policyLabels[area] }} 당시 버전 조회
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
<AuditPolicyVersion
|
||||||
|
v-if="selectedPolicy"
|
||||||
|
:id="selectedPolicy"
|
||||||
|
:allow-previous="false"
|
||||||
|
@select="selectPolicy"
|
||||||
|
/>
|
||||||
<ol aria-label="판단 절차">
|
<ol aria-label="판단 절차">
|
||||||
<template v-for="chunk in detail.chunks" :key="chunk.ordinal"
|
<template v-for="chunk in detail.chunks" :key="chunk.ordinal"
|
||||||
><li v-for="step in chunk.steps" :key="step.sequence" :value="step.sequence + 1">
|
><li v-for="step in chunk.steps" :key="step.sequence" :value="step.sequence + 1">
|
||||||
@@ -221,6 +246,9 @@ watch(
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
.audit-decisions > section {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.table-scroll {
|
.table-scroll {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { trpc } from '../../utils/trpc';
|
import { trpc } from '../../utils/trpc';
|
||||||
|
import AuditPolicyVersion from './AuditPolicyVersion.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
nationId: number;
|
nationId: number;
|
||||||
@@ -10,47 +11,13 @@ const props = defineProps<{
|
|||||||
to: { year: number; month: number };
|
to: { year: number; month: number };
|
||||||
}>();
|
}>();
|
||||||
type History = Awaited<ReturnType<typeof trpc.playAudit.policyHistory.query>>;
|
type History = Awaited<ReturnType<typeof trpc.playAudit.policyHistory.query>>;
|
||||||
type Detail = Awaited<ReturnType<typeof trpc.playAudit.policyVersion.query>>;
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const data = ref<History | null>(null);
|
const data = ref<History | null>(null);
|
||||||
const detail = ref<Detail | null>(null);
|
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const detailError = ref('');
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const detailLoading = ref(false);
|
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
let detailGeneration = 0;
|
|
||||||
const selected = computed(() => (typeof route.query.policy === 'string' ? route.query.policy : null));
|
const selected = computed(() => (typeof route.query.policy === 'string' ? route.query.policy : null));
|
||||||
const fieldLabels: Record<string, string> = {
|
|
||||||
reqNationGold: '국가 권장 금',
|
|
||||||
reqNationRice: '국가 권장 쌀',
|
|
||||||
reqHumanWarUrgentGold: '유저전투장 긴급포상 금',
|
|
||||||
reqHumanWarUrgentRice: '유저전투장 긴급포상 쌀',
|
|
||||||
reqHumanWarRecommandGold: '유저전투장 권장 금',
|
|
||||||
reqHumanWarRecommandRice: '유저전투장 권장 쌀',
|
|
||||||
reqHumanDevelGold: '유저내정장 권장 금',
|
|
||||||
reqHumanDevelRice: '유저내정장 권장 쌀',
|
|
||||||
reqNPCWarGold: 'NPC전투장 권장 금',
|
|
||||||
reqNPCWarRice: 'NPC전투장 권장 쌀',
|
|
||||||
reqNPCDevelGold: 'NPC내정장 권장 금',
|
|
||||||
reqNPCDevelRice: 'NPC내정장 권장 쌀',
|
|
||||||
minimumResourceActionAmount: '포상/몰수/헌납/삼/팜 최소 단위',
|
|
||||||
maximumResourceActionAmount: '포상/몰수/헌납/삼/팜 최대 단위',
|
|
||||||
minWarCrew: '최소 전투 가능 병력 수',
|
|
||||||
minNPCRecruitCityPopulation: 'NPC 최소 징병 가능 인구 수',
|
|
||||||
safeRecruitCityPopulationRatio: '제자리 징병 허용 인구율 (비율)',
|
|
||||||
minNPCWarLeadership: 'NPC 전투 참여 통솔 기준',
|
|
||||||
properWarTrainAtmos: '훈련/사기진작 목표치',
|
|
||||||
cureThreshold: '요양 기준',
|
|
||||||
CombatForce: '전투 부대 편성',
|
|
||||||
SupportForce: '지원 부대 편성',
|
|
||||||
DevelopForce: '내정 부대 편성',
|
|
||||||
priority: '행동 우선순위',
|
|
||||||
war: '전쟁 금지 설정',
|
|
||||||
scout: '임관 권유 설정',
|
|
||||||
secretlimit: '기밀 공개 기준 (년)',
|
|
||||||
};
|
|
||||||
const labels = { BASELINE: '최초 관측', CHANGE: '실제 변경', OBSERVED_GAP: '관측 누락 이후 기준' };
|
const labels = { BASELINE: '최초 관측', CHANGE: '실제 변경', OBSERVED_GAP: '관측 누락 이후 기준' };
|
||||||
const message = (cause: unknown) => (cause instanceof Error ? cause.message : '정책 이력을 조회하지 못했습니다.');
|
const message = (cause: unknown) => (cause instanceof Error ? cause.message : '정책 이력을 조회하지 못했습니다.');
|
||||||
const load = async (append = false) => {
|
const load = async (append = false) => {
|
||||||
@@ -75,22 +42,6 @@ const load = async (append = false) => {
|
|||||||
if (request === generation) loading.value = false;
|
if (request === generation) loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const loadDetail = async () => {
|
|
||||||
const request = ++detailGeneration;
|
|
||||||
detail.value = null;
|
|
||||||
detailError.value = '';
|
|
||||||
detailLoading.value = false;
|
|
||||||
if (!selected.value) return;
|
|
||||||
detailLoading.value = true;
|
|
||||||
try {
|
|
||||||
const response = await trpc.playAudit.policyVersion.query({ id: selected.value });
|
|
||||||
if (request === detailGeneration) detail.value = response;
|
|
||||||
} catch (cause) {
|
|
||||||
if (request === detailGeneration) detailError.value = message(cause);
|
|
||||||
} finally {
|
|
||||||
if (request === detailGeneration) detailLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const select = (id: string | null) => router.push({ query: { ...route.query, policy: id ?? undefined } });
|
const select = (id: string | null) => router.push({ query: { ...route.query, policy: id ?? undefined } });
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
@@ -106,13 +57,6 @@ watch(
|
|||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
watch(
|
|
||||||
selected,
|
|
||||||
() => {
|
|
||||||
void loadDetail();
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -153,72 +97,7 @@ watch(
|
|||||||
다음 정책 50개
|
다음 정책 50개
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<section v-if="selected" aria-label="선택 정책 버전">
|
<AuditPolicyVersion v-if="selected" :id="selected" @select="select" />
|
||||||
<h3>선택 정책 버전 <button class="legacy-button" @click="select(null)">정책 상세 닫기</button></h3>
|
|
||||||
<p v-if="detailLoading" role="status">정책 버전 조회 중…</p>
|
|
||||||
<p v-if="detailError" role="alert">
|
|
||||||
{{ detailError }} <button class="legacy-button" @click="loadDetail">버전 다시 조회</button>
|
|
||||||
</p>
|
|
||||||
<template v-if="detail">
|
|
||||||
<p>
|
|
||||||
국가 #{{ detail.version.nationId }} · 버전 {{ detail.version.revision }} ·
|
|
||||||
{{ labels[detail.version.source] }} · {{ detail.version.year }}년 {{ detail.version.month }}월
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
기록 시각 {{ detail.version.createdAt }} · tick {{ detail.version.tick ?? '미상' }} · 순번
|
|
||||||
{{ detail.version.ordinal }}
|
|
||||||
</p>
|
|
||||||
<p v-if="detail.version.actor">
|
|
||||||
{{ detail.version.actor.name }} (#{{ detail.version.actor.generalId }}) · 당시 국가 #{{
|
|
||||||
detail.version.actor.nationId
|
|
||||||
}}
|
|
||||||
· 직책 {{ detail.version.actor.officerLevel }}
|
|
||||||
</p>
|
|
||||||
<p v-if="detail.version.source !== 'CHANGE'">
|
|
||||||
이 버전은 관측 기준입니다. 이전 값과 변경 주체를 추정하지 않습니다.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
null은 개별 설정이 없음을 뜻합니다. 설정값을 기록하며 당시 NPC별 유효 값은 여기서 재계산하지
|
|
||||||
않습니다.
|
|
||||||
</p>
|
|
||||||
<div class="table-scroll" tabindex="0" aria-label="정책 전후 값">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>설정</th>
|
|
||||||
<th>변경 전</th>
|
|
||||||
<th>변경 후</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="field in detail.version.fields" :key="field.key">
|
|
||||||
<th scope="row">
|
|
||||||
{{ fieldLabels[field.key] ?? field.key }} <span v-if="field.changed">(변경)</span>
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<pre>{{ field.beforeJson ?? '관측하지 않음' }}</pre>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<pre>{{ field.afterJson }}</pre>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="detail.version.previousId"
|
|
||||||
class="legacy-button"
|
|
||||||
@click="select(detail.version.previousId)"
|
|
||||||
>
|
|
||||||
이전 정책 버전
|
|
||||||
</button>
|
|
||||||
<details>
|
|
||||||
<summary>요청 연결</summary>
|
|
||||||
<p>요청 {{ detail.version.requestId ?? '해당 없음' }}</p>
|
|
||||||
<p>입력 순번 {{ detail.version.inputSequence ?? '해당 없음' }}</p>
|
|
||||||
</details>
|
|
||||||
</template>
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { trpc } from '../../utils/trpc';
|
||||||
|
const props = withDefaults(defineProps<{ id: string; allowPrevious?: boolean }>(), { allowPrevious: true });
|
||||||
|
const emit = defineEmits<{ select: [id: string | null] }>();
|
||||||
|
type Detail = Awaited<ReturnType<typeof trpc.playAudit.policyVersion.query>>;
|
||||||
|
const detail = ref<Detail | null>(null);
|
||||||
|
const detailError = ref('');
|
||||||
|
const detailLoading = ref(false);
|
||||||
|
let detailGeneration = 0;
|
||||||
|
const select = (id: string | null) => emit('select', id);
|
||||||
|
const message = (cause: unknown) => (cause instanceof Error ? cause.message : '정책 버전을 조회하지 못했습니다.');
|
||||||
|
const fieldLabels: Record<string, string> = {
|
||||||
|
reqNationGold: '국가 권장 금',
|
||||||
|
reqNationRice: '국가 권장 쌀',
|
||||||
|
reqHumanWarUrgentGold: '유저전투장 긴급포상 금',
|
||||||
|
reqHumanWarUrgentRice: '유저전투장 긴급포상 쌀',
|
||||||
|
reqHumanWarRecommandGold: '유저전투장 권장 금',
|
||||||
|
reqHumanWarRecommandRice: '유저전투장 권장 쌀',
|
||||||
|
reqHumanDevelGold: '유저내정장 권장 금',
|
||||||
|
reqHumanDevelRice: '유저내정장 권장 쌀',
|
||||||
|
reqNPCWarGold: 'NPC전투장 권장 금',
|
||||||
|
reqNPCWarRice: 'NPC전투장 권장 쌀',
|
||||||
|
reqNPCDevelGold: 'NPC내정장 권장 금',
|
||||||
|
reqNPCDevelRice: 'NPC내정장 권장 쌀',
|
||||||
|
minimumResourceActionAmount: '포상/몰수/헌납/삼/팜 최소 단위',
|
||||||
|
maximumResourceActionAmount: '포상/몰수/헌납/삼/팜 최대 단위',
|
||||||
|
minWarCrew: '최소 전투 가능 병력 수',
|
||||||
|
minNPCRecruitCityPopulation: 'NPC 최소 징병 가능 인구 수',
|
||||||
|
safeRecruitCityPopulationRatio: '제자리 징병 허용 인구율 (비율)',
|
||||||
|
minNPCWarLeadership: 'NPC 전투 참여 통솔 기준',
|
||||||
|
properWarTrainAtmos: '훈련/사기진작 목표치',
|
||||||
|
cureThreshold: '요양 기준',
|
||||||
|
CombatForce: '전투 부대 편성',
|
||||||
|
SupportForce: '지원 부대 편성',
|
||||||
|
DevelopForce: '내정 부대 편성',
|
||||||
|
priority: '행동 우선순위',
|
||||||
|
war: '전쟁 금지 설정',
|
||||||
|
scout: '임관 권유 설정',
|
||||||
|
secretlimit: '기밀 공개 기준 (년)',
|
||||||
|
};
|
||||||
|
const labels = { BASELINE: '최초 관측', CHANGE: '실제 변경', OBSERVED_GAP: '관측 누락 이후 기준' };
|
||||||
|
const loadDetail = async () => {
|
||||||
|
const request = ++detailGeneration;
|
||||||
|
detail.value = null;
|
||||||
|
detailError.value = '';
|
||||||
|
detailLoading.value = false;
|
||||||
|
if (!props.id) return;
|
||||||
|
detailLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await trpc.playAudit.policyVersion.query({ id: props.id });
|
||||||
|
if (request === detailGeneration) detail.value = response;
|
||||||
|
} catch (cause) {
|
||||||
|
if (request === detailGeneration) detailError.value = message(cause);
|
||||||
|
} finally {
|
||||||
|
if (request === detailGeneration) detailLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(
|
||||||
|
() => props.id,
|
||||||
|
() => {
|
||||||
|
void loadDetail();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<section aria-label="선택 정책 버전">
|
||||||
|
<h3>선택 정책 버전 <button class="legacy-button" @click="select(null)">정책 상세 닫기</button></h3>
|
||||||
|
<p v-if="detailLoading" role="status">정책 버전 조회 중…</p>
|
||||||
|
<p v-if="detailError" role="alert">
|
||||||
|
{{ detailError }} <button class="legacy-button" @click="loadDetail">버전 다시 조회</button>
|
||||||
|
</p>
|
||||||
|
<template v-if="detail">
|
||||||
|
<p>
|
||||||
|
국가 #{{ detail.version.nationId }} · 버전 {{ detail.version.revision }} ·
|
||||||
|
{{ labels[detail.version.source] }} · {{ detail.version.year }}년 {{ detail.version.month }}월
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
기록 시각 {{ detail.version.createdAt }} · tick {{ detail.version.tick ?? '미상' }} · 순번
|
||||||
|
{{ detail.version.ordinal }}
|
||||||
|
</p>
|
||||||
|
<p v-if="detail.version.actor">
|
||||||
|
{{ detail.version.actor.name }} (#{{ detail.version.actor.generalId }}) · 당시 국가 #{{
|
||||||
|
detail.version.actor.nationId
|
||||||
|
}}
|
||||||
|
· 직책 {{ detail.version.actor.officerLevel }}
|
||||||
|
</p>
|
||||||
|
<p v-if="detail.version.source !== 'CHANGE'">
|
||||||
|
이 버전은 관측 기준입니다. 이전 값과 변경 주체를 추정하지 않습니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
null은 개별 설정이 없음을 뜻합니다. 설정값을 기록하며 당시 NPC별 유효 값은 여기서 재계산하지 않습니다.
|
||||||
|
</p>
|
||||||
|
<div class="table-scroll" tabindex="0" aria-label="정책 전후 값">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>설정</th>
|
||||||
|
<th>변경 전</th>
|
||||||
|
<th>변경 후</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="field in detail.version.fields" :key="field.key">
|
||||||
|
<th scope="row">
|
||||||
|
{{ fieldLabels[field.key] ?? field.key }} <span v-if="field.changed">(변경)</span>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<pre>{{ field.beforeJson ?? '관측하지 않음' }}</pre>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<pre>{{ field.afterJson }}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="allowPrevious && detail.version.previousId"
|
||||||
|
class="legacy-button"
|
||||||
|
@click="select(detail.version.previousId)"
|
||||||
|
>
|
||||||
|
이전 정책 버전
|
||||||
|
</button>
|
||||||
|
<details>
|
||||||
|
<summary>요청 연결</summary>
|
||||||
|
<p>요청 {{ detail.version.requestId ?? '해당 없음' }}</p>
|
||||||
|
<p>입력 순번 {{ detail.version.inputSequence ?? '해당 없음' }}</p>
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.table-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 640px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid gray;
|
||||||
|
padding: 6px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
max-width: 400px;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: var(--sammo-font-size-normal);
|
||||||
|
}
|
||||||
|
[role='alert'] {
|
||||||
|
color: #ffb9b9;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,6 +23,10 @@ migration58은 기존 장수 인덱스를 `(server, general, year, month, tick,
|
|||||||
버튼·표 스타일을 재사용한다. 열기/상세 선택/더 보기는 명시적으로 수행하고 polling하지
|
버튼·표 스타일을 재사용한다. 열기/상세 선택/더 보기는 명시적으로 수행하고 polling하지
|
||||||
않는다. 결정 URL 복원과 상세 재시도는 상위 장수 목록을 다시 읽지 않는다.
|
않는다. 결정 URL 복원과 상세 재시도는 상위 장수 목록을 다시 읽지 않는다.
|
||||||
절차 coverage와 미수집 코드 버전을 표시하며 전체 후보 조건·유효 정책 연결은 남는다.
|
절차 coverage와 미수집 코드 버전을 표시하며 전체 후보 조건·유효 정책 연결은 남는다.
|
||||||
|
당시 정책 참조는 공용 `AuditPolicyVersion`으로 연결했다. 기존 정책 이력의 버전 표시·
|
||||||
|
필드 한국어 이름·실패 재시도를 재사용하며 클릭 시 버전1건만 읽는다. 결정의 참조 ID에
|
||||||
|
포함되지 않은 URL policy는 해당 결정의 정책으로 읽거나 표시하지 않는다.
|
||||||
|
국가의 저장 설정과 NPC별 합성 유효 값은 구분하며 현재 설정으로 보충하지 않는다.
|
||||||
|
|
||||||
### NPC 결정 저장·복구 기반
|
### NPC 결정 저장·복구 기반
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
| 도시 | 현재/월말 소유·내정 상태와 국가별 주둔 장수 상세 연결 | 월말 주둔은 그달 모든 방문자가 아님. 지도 기반 탐색은 미완성 |
|
| 도시 | 현재/월말 소유·내정 상태와 국가별 주둔 장수 상세 연결 | 월말 주둔은 그달 모든 방문자가 아님. 지도 기반 탐색은 미완성 |
|
||||||
| 외교 | 국가쌍·기간별 문서 제안/승인/철회/파기, 즉시 합의와 월간·명령 관계 전이, 생성/소멸 관계 | 문서 내용과 실제 관계는 별개. 최초 관측 이전 사건은 복원하지 않음 |
|
| 외교 | 국가쌍·기간별 문서 제안/승인/철회/파기, 즉시 합의와 월간·명령 관계 전이, 생성/소멸 관계 | 문서 내용과 실제 관계는 별개. 최초 관측 이전 사건은 복원하지 않음 |
|
||||||
| NPC 결정 | 장수별 개인·수뇌 판단, 절차 시도/차단, 관측한 RNG 결과와 선택·실제 실행 | 새 실행부터 수집하며 후보 내부 조건 전체는 미완성 |
|
| NPC 결정 | 장수별 개인·수뇌 판단, 절차 시도/차단, 관측한 RNG 결과와 선택·실제 실행 | 새 실행부터 수집하며 후보 내부 조건 전체는 미완성 |
|
||||||
| 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면과 NPC 결정의 정책 참조 연결은 아직 없음 |
|
| 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면은 아직 없음. NPC 결정에서 저장된 당시 버전을 직접 조회 가능 |
|
||||||
|
|
||||||
목록/그래프와 선택 상세를 분리해 읽는다. 표는 기본50건이며 더 보기를 명시적으로
|
목록/그래프와 선택 상세를 분리해 읽는다. 표는 기본50건이며 더 보기를 명시적으로
|
||||||
누른다. 현재 상태는 수동으로 조회하며 백그라운드 polling은 하지 않는다. 필터·월·선택
|
누른다. 현재 상태는 수동으로 조회하며 백그라운드 polling은 하지 않는다. 필터·월·선택
|
||||||
@@ -91,7 +91,7 @@ Gateway의 새 진입/권한 catalog도 사용하려면 같은 commit의 Gateway
|
|||||||
- NPC/유저 자동턴의 개인·수뇌 절차, 정책 차단, RNG utility 결과와 최종 실행 결과는
|
- NPC/유저 자동턴의 개인·수뇌 절차, 정책 차단, RNG utility 결과와 최종 실행 결과는
|
||||||
migration 이후 새 실행부터 저장한다. 후보 내부 조건 전체는 아직 없으며 `PROCEDURES`
|
migration 이후 새 실행부터 저장한다. 후보 내부 조건 전체는 아직 없으며 `PROCEDURES`
|
||||||
coverage로 구분한다. 장수 상세의 **NPC 결정 기록 조회**에서 선택 월의 목록과 순서별 상세를 읽는다. 과거 결정은 역산하지 않는다.
|
coverage로 구분한다. 장수 상세의 **NPC 결정 기록 조회**에서 선택 월의 목록과 순서별 상세를 읽는다. 과거 결정은 역산하지 않는다.
|
||||||
- 결정은 당시 확보된 정책 참조를 보존한다. 합성된 유효 정책 상세와 코드 버전 연결은
|
- 결정은 당시 확보된 정책 참조를 보존하며 **당시 정책 참조**에서 해당 불변 버전을 바로 조회한다. 합성된 유효 정책 상세와 코드 버전 연결은
|
||||||
아직 미완성이다. 코드 버전이 주입되지 않은 실행은 null로 남기며 현재 버전으로 메우지 않는다.
|
아직 미완성이다. 코드 버전이 주입되지 않은 실행은 null로 남기며 현재 버전으로 메우지 않는다.
|
||||||
- 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진
|
- 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진
|
||||||
버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다.
|
버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다.
|
||||||
|
|||||||
Reference in New Issue
Block a user