feat: 서버 준비 전에 감사 초기 상태와 정책 기준을 저장
This commit is contained in:
@@ -11,6 +11,7 @@ const world = {
|
||||
serverId: 'audit-fixture',
|
||||
tick: '100',
|
||||
asOf: '2026-09-16T00:00:00.000Z',
|
||||
collectionStart: { year: 190, month: 1, tick: '0', observedAt: '2026-09-16T00:00:00.000Z' },
|
||||
};
|
||||
const dex = { dex1: 100, dex2: 200, dex3: 300, dex4: 400, dex5: 500 };
|
||||
const population = { count: 2, gold: 200, rice: 400, dex, averageGold: 100, averageRice: 200, averageDex: dex };
|
||||
@@ -201,7 +202,12 @@ const install = async (page: Page, denied = false) => {
|
||||
return result({
|
||||
...world,
|
||||
collected: true,
|
||||
sample: { year: 190, month: 6, kind: 'FINAL', settlementsComplete: true },
|
||||
sample: {
|
||||
year: 190,
|
||||
month: 6,
|
||||
kind: (input.at as { kind: string }).kind,
|
||||
settlementsComplete: (input.at as { kind: string }).kind !== 'INITIAL',
|
||||
},
|
||||
nation: {
|
||||
id: 2,
|
||||
name: '촉',
|
||||
@@ -463,6 +469,7 @@ test('selected general reads detail on demand and separates current reservations
|
||||
await expect(page.getByRole('heading', { name: '선택 장수 상세' })).toHaveCount(0);
|
||||
await page.goto(gamePath('/play-audit?tab=generals&general=1&at=month&year=190&month=6'));
|
||||
await expect(page.getByRole('heading', { name: '과거감사장수 (#1)' })).toBeVisible();
|
||||
await expect(page.getByText('과거 예약 명령은 상태 표본에 포함되지 않습니다.', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '현재 예약 명령 조회', exact: true })).toHaveCount(0);
|
||||
expect(requests.filter((request) => request.operation === 'playAudit.generalTurns')).toHaveLength(1);
|
||||
});
|
||||
@@ -682,3 +689,25 @@ test('policy filter drafts do not read until applied, including default dates',
|
||||
from: { year: 190, month: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
test('initial observation is separate from month-end and final snapshots', async ({ page }) => {
|
||||
const requests = await install(page);
|
||||
await page.goto(gamePath('/play-audit?tab=nations&nation=2&at=initial&year=190&month=6'));
|
||||
await expect(page.getByRole('heading', { name: '촉 · 190년 6월 수집 시작 기준' })).toBeVisible();
|
||||
await expect(page.getByText(/상태·정책 수집 시작: 190년 1월/)).toBeVisible();
|
||||
expect(requests.some(({ operation }) => operation === 'playAudit.nationSeries')).toBe(false);
|
||||
expect(requests.find(({ operation }) => operation === 'playAudit.nationSnapshot')?.input).toMatchObject({
|
||||
at: { kind: 'INITIAL' },
|
||||
});
|
||||
await page.getByLabel('조회 대상').selectOption('generals');
|
||||
await page.getByRole('button', { name: '조회', exact: true }).click();
|
||||
await page.getByRole('button', { name: '감사장수 (#1)', exact: true }).click();
|
||||
await expect(page.getByText('190년 6월 수집 시작 기준', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '과거감사장수 (#1)' })).toBeVisible();
|
||||
expect(requests.filter(({ operation }) => operation === 'playAudit.generalDetail').at(-1)?.input).toMatchObject({
|
||||
at: { kind: 'INITIAL' },
|
||||
});
|
||||
await capture(page, 'initial-observation');
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await capture(page, 'mobile-initial-observation');
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import PanelCard from '../ui/PanelCard.vue';
|
||||
import { trpc } from '../../utils/trpc';
|
||||
const props = defineProps<{ cityId: number; at?: { year: number; month: number; kind: 'MONTH_END' | 'FINAL' } }>();
|
||||
const props = defineProps<{
|
||||
cityId: number;
|
||||
at?: { year: number; month: number; kind: 'MONTH_END' | 'FINAL' | 'INITIAL' };
|
||||
}>();
|
||||
defineEmits<{ close: []; generals: [cityId: number] }>();
|
||||
type Detail = Awaited<ReturnType<typeof trpc.playAudit.cityDetail.query>>;
|
||||
const data = ref<Detail | null>(null);
|
||||
@@ -37,7 +40,11 @@ watch(
|
||||
<template>
|
||||
<PanelCard
|
||||
title="선택 도시 상세"
|
||||
:subtitle="at ? `${at.year}년 ${at.month}월 ${at.kind === 'FINAL' ? '최종 표본' : '월말'}` : '현재 상태'"
|
||||
:subtitle="
|
||||
at
|
||||
? `${at.year}년 ${at.month}월 ${at.kind === 'FINAL' ? '최종 표본' : at.kind === 'INITIAL' ? '수집 시작 기준' : '월말'}`
|
||||
: '현재 상태'
|
||||
"
|
||||
>
|
||||
<template #actions><button class="legacy-button" @click="$emit('close')">상세 닫기</button></template>
|
||||
<p v-if="loading" role="status">도시 조회 중…</p>
|
||||
|
||||
@@ -3,7 +3,10 @@ import { ref, watch } from 'vue';
|
||||
import PanelCard from '../ui/PanelCard.vue';
|
||||
import AuditGeneralLogs from './AuditGeneralLogs.vue';
|
||||
import { trpc } from '../../utils/trpc';
|
||||
const props = defineProps<{ generalId: number; at?: { year: number; month: number; kind: 'MONTH_END' | 'FINAL' } }>();
|
||||
const props = defineProps<{
|
||||
generalId: number;
|
||||
at?: { year: number; month: number; kind: 'MONTH_END' | 'FINAL' | 'INITIAL' };
|
||||
}>();
|
||||
defineEmits<{ close: [] }>();
|
||||
type Detail = Awaited<ReturnType<typeof trpc.playAudit.generalDetail.query>>;
|
||||
type Turns = Awaited<ReturnType<typeof trpc.playAudit.generalTurns.query>>;
|
||||
@@ -69,7 +72,11 @@ watch(
|
||||
<template>
|
||||
<PanelCard
|
||||
title="선택 장수 상세"
|
||||
:subtitle="at ? `${at.year}년 ${at.month}월 ${at.kind === 'FINAL' ? '최종 표본' : '월말'}` : '현재 상태'"
|
||||
:subtitle="
|
||||
at
|
||||
? `${at.year}년 ${at.month}월 ${at.kind === 'FINAL' ? '최종 표본' : at.kind === 'INITIAL' ? '수집 시작 기준' : '월말'}`
|
||||
: '현재 상태'
|
||||
"
|
||||
>
|
||||
<template #actions><button class="legacy-button" @click="$emit('close')">상세 닫기</button></template>
|
||||
<p v-if="loading" role="status">상세 조회 중…</p>
|
||||
@@ -93,7 +100,7 @@ watch(
|
||||
{{ format(data.general.dedication) }}
|
||||
</p>
|
||||
<p>숙련 (보 / 궁 / 기 / 귀 / 차): {{ Object.values(data.general.dex).map(format).join(' / ') }}</p>
|
||||
<p v-if="at">과거 예약 명령은 월말 표본에 포함되지 않습니다.</p>
|
||||
<p v-if="at">과거 예약 명령은 상태 표본에 포함되지 않습니다.</p>
|
||||
<button v-else class="legacy-button" :disabled="turnsLoading" @click="loadTurns()">
|
||||
현재 예약 명령 조회
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { trpc } from '../../utils/trpc';
|
||||
type Snapshot = Awaited<ReturnType<typeof trpc.playAudit.nationSnapshot.query>>;
|
||||
defineProps<{ data: Snapshot }>();
|
||||
const props = defineProps<{ data: Snapshot }>();
|
||||
const label = computed(() => (props.data.sample?.kind === 'INITIAL' ? '수집 시작 기준' : '최종 표본'));
|
||||
const format = (value: number | null) =>
|
||||
value === null ? '자료 없음' : value.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
|
||||
const groups = [
|
||||
@@ -12,13 +14,11 @@ const groups = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p v-if="!data.collected">선택한 시점의 최종 표본이 없습니다.</p>
|
||||
<p v-else-if="!data.nation">최종 표본에 해당 국가가 없습니다.</p>
|
||||
<p v-if="!data.collected">선택한 시점의 표본이 없습니다.</p>
|
||||
<p v-else-if="!data.nation">표본에 해당 국가가 없습니다.</p>
|
||||
<template v-else>
|
||||
<h3>{{ data.nation.name }} · {{ data.sample?.year }}년 {{ data.sample?.month }}월 최종 표본</h3>
|
||||
<p>
|
||||
최종 표본은 월말 시계열과 별도로 표시합니다. 아래 정산은 이 표본을 수집할 때까지 해당 월에 관측한 값입니다.
|
||||
</p>
|
||||
<h3>{{ data.nation.name }} · {{ data.sample?.year }}년 {{ data.sample?.month }}월 {{ label }}</h3>
|
||||
<p>월말 시계열과 구분되는 표본입니다. 아래 정산은 이 표본을 수집할 때까지 해당 월에 관측한 값입니다.</p>
|
||||
<p v-if="!data.sample?.settlementsComplete">정산 수집이 불완전한 월입니다.</p>
|
||||
<dl class="nation-values">
|
||||
<div>
|
||||
@@ -38,7 +38,7 @@ const groups = [
|
||||
<dd>{{ format(data.nation.paidGold) }} / {{ format(data.nation.paidRice) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="table-scroll" tabindex="0" aria-label="최종 국가 장수 집계">
|
||||
<div class="table-scroll" tabindex="0" :aria-label="`${label} 국가 장수 집계`">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -68,11 +68,16 @@ const selectedGeneral = computed(() =>
|
||||
typeof route.query.general === 'string' && /^\d+$/.test(route.query.general) ? Number(route.query.general) : null
|
||||
);
|
||||
const selectedAt = computed(() =>
|
||||
route.query.at === 'month' || route.query.at === 'final'
|
||||
route.query.at === 'month' || route.query.at === 'final' || route.query.at === 'initial'
|
||||
? {
|
||||
year: numeric(route.query.year, coverage.value?.year ?? 0),
|
||||
month: numeric(route.query.month, coverage.value?.month ?? 1),
|
||||
kind: route.query.at === 'final' ? ('FINAL' as const) : ('MONTH_END' as const),
|
||||
kind:
|
||||
route.query.at === 'final'
|
||||
? ('FINAL' as const)
|
||||
: route.query.at === 'initial'
|
||||
? ('INITIAL' as const)
|
||||
: ('MONTH_END' as const),
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
@@ -93,16 +98,21 @@ const at = computed(() =>
|
||||
: {
|
||||
year: year.value,
|
||||
month: month.value,
|
||||
kind: moment.value === 'final' ? ('FINAL' as const) : ('MONTH_END' as const),
|
||||
kind:
|
||||
moment.value === 'final'
|
||||
? ('FINAL' as const)
|
||||
: moment.value === 'initial'
|
||||
? ('INITIAL' as const)
|
||||
: ('MONTH_END' as const),
|
||||
}
|
||||
);
|
||||
const format = (value: number) => value.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
|
||||
const nationName = (id: number) =>
|
||||
nations.value?.items.find((item) => item.id === id)?.name ?? (id === 0 ? '무소속' : `국가 #${id}`);
|
||||
const scopeLabel = computed(() =>
|
||||
route.query.at !== 'month' && route.query.at !== 'final'
|
||||
route.query.at !== 'month' && route.query.at !== 'final' && route.query.at !== 'initial'
|
||||
? '현재 상태'
|
||||
: `${route.query.year}년 ${route.query.month}월 ${route.query.at === 'final' ? '최종 표본' : '월말'}`
|
||||
: `${route.query.year}년 ${route.query.month}월 ${route.query.at === 'final' ? '최종 표본' : route.query.at === 'initial' ? '수집 시작 기준' : '월말'}`
|
||||
);
|
||||
const result = computed(() =>
|
||||
tab.value === 'generals'
|
||||
@@ -123,7 +133,7 @@ const readQuery = () => {
|
||||
population.value = ['human', 'npc', 'troopNpc'].includes(String(route.query.population))
|
||||
? String(route.query.population)
|
||||
: '';
|
||||
moment.value = ['month', 'final'].includes(String(route.query.at)) ? String(route.query.at) : 'current';
|
||||
moment.value = ['month', 'final', 'initial'].includes(String(route.query.at)) ? String(route.query.at) : 'current';
|
||||
year.value = numeric(route.query.year, coverage.value?.year ?? 0);
|
||||
month.value = numeric(route.query.month, coverage.value?.month ?? 1);
|
||||
const defaultStart = Math.max(
|
||||
@@ -175,10 +185,10 @@ const load = async (append = false) => {
|
||||
};
|
||||
} else if (tab.value === 'policies') {
|
||||
// 정책 목록/상세는 해당 component가 필요한 요청만 실행한다.
|
||||
} else if (nationId.value !== '' && moment.value === 'final') {
|
||||
} else if (nationId.value !== '' && (moment.value === 'final' || moment.value === 'initial')) {
|
||||
const response = await trpc.playAudit.nationSnapshot.query({
|
||||
nationId: Number(nationId.value),
|
||||
at: { year: year.value, month: month.value, kind: 'FINAL' },
|
||||
at: { year: year.value, month: month.value, kind: moment.value === 'initial' ? 'INITIAL' : 'FINAL' },
|
||||
});
|
||||
if (request === generation) nationSnapshot.value = response;
|
||||
} else if (nationId.value !== '') {
|
||||
@@ -310,6 +320,10 @@ onMounted(async () => {
|
||||
<p v-else-if="coverage.status === 'NOT_COLLECTED'">
|
||||
아직 수집된 월별 표본이 없습니다. 현재 상태는 조회할 수 있습니다.
|
||||
</p>
|
||||
<p v-if="coverage.collectionStart">
|
||||
상태·정책 수집 시작: {{ coverage.collectionStart.year }}년 {{ coverage.collectionStart.month }}월 ·
|
||||
{{ coverage.collectionStart.observedAt }}. 이전 상태를 소급 복원하지 않습니다.
|
||||
</p>
|
||||
<form class="filters" @submit.prevent="apply">
|
||||
<label
|
||||
>조회 대상<select class="legacy-sort-select" v-model="tab">
|
||||
@@ -358,6 +372,7 @@ onMounted(async () => {
|
||||
<option value="current">현재</option>
|
||||
<option value="month">월말</option>
|
||||
<option value="final">최종 표본</option>
|
||||
<option value="initial">수집 시작 기준</option>
|
||||
</select></label
|
||||
>
|
||||
<label
|
||||
@@ -377,7 +392,9 @@ onMounted(async () => {
|
||||
:max="year === coverage.year ? coverage.month : 12"
|
||||
required
|
||||
/></label>
|
||||
<template v-if="(tab === 'nations' && moment !== 'final') || tab === 'policies'">
|
||||
<template
|
||||
v-if="(tab === 'nations' && moment !== 'final' && moment !== 'initial') || tab === 'policies'"
|
||||
>
|
||||
<label
|
||||
>시작 연도<input
|
||||
v-model.number="fromYear"
|
||||
|
||||
Reference in New Issue
Block a user