feat: 플레이 감사 최종 국가 표본 별도 조회 구현
This commit is contained in:
@@ -114,6 +114,26 @@ const install = async (page: Page, denied = false) => {
|
||||
},
|
||||
],
|
||||
});
|
||||
case 'playAudit.nationSnapshot':
|
||||
return result({
|
||||
...world,
|
||||
collected: true,
|
||||
sample: { year: 190, month: 6, kind: 'FINAL', settlementsComplete: true },
|
||||
nation: {
|
||||
id: 2,
|
||||
name: '촉',
|
||||
color: '#ff0000',
|
||||
gold: 999,
|
||||
rice: 222,
|
||||
tech: 100,
|
||||
appliedRate: 20,
|
||||
incomeGold: 99,
|
||||
incomeRice: 0,
|
||||
paidGold: 0,
|
||||
paidRice: 0,
|
||||
populations: { human: population, npc: population, troopNpc: population },
|
||||
},
|
||||
});
|
||||
case 'playAudit.generals':
|
||||
return result({
|
||||
...world,
|
||||
@@ -283,3 +303,16 @@ test('back navigation during a slow read keeps the newer city view', async ({ pa
|
||||
await page.getByRole('button', { name: '조회', exact: true }).focus();
|
||||
await expect(page.getByRole('button', { name: '조회', exact: true })).toBeFocused();
|
||||
});
|
||||
|
||||
test('final nation snapshot is separate from the monthly series', async ({ page }) => {
|
||||
const requests = await install(page);
|
||||
await page.goto(gamePath('/play-audit?tab=nations&nation=2&at=final&year=190&month=6'));
|
||||
await expect(page.getByRole('heading', { name: '촉 · 190년 6월 최종 표본' })).toBeVisible();
|
||||
await expect(page.getByText('999 / 222', { exact: true })).toBeVisible();
|
||||
expect(requests.some((request) => request.operation === 'playAudit.nationSeries')).toBe(false);
|
||||
expect(requests.find((request) => request.operation === 'playAudit.nationSnapshot')?.input).toEqual({
|
||||
nationId: 2,
|
||||
at: { year: 190, month: 6, kind: 'FINAL' },
|
||||
});
|
||||
await capture(page, 'final-nation');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { trpc } from '../../utils/trpc';
|
||||
type Snapshot = Awaited<ReturnType<typeof trpc.playAudit.nationSnapshot.query>>;
|
||||
defineProps<{ data: Snapshot }>();
|
||||
const format = (value: number | null) =>
|
||||
value === null ? '자료 없음' : value.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
|
||||
const groups = [
|
||||
['human', '유저'],
|
||||
['npc', 'NPC'],
|
||||
['troopNpc', '부대장 NPC'],
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
<p v-if="!data.sample?.settlementsComplete">정산 수집이 불완전한 월입니다.</p>
|
||||
<dl class="nation-values">
|
||||
<div>
|
||||
<dt>국고 금 / 쌀</dt>
|
||||
<dd>{{ format(data.nation.gold) }} / {{ format(data.nation.rice) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>기술력 / 적용 세율</dt>
|
||||
<dd>{{ format(data.nation.tech) }} / {{ format(data.nation.appliedRate) }}%</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>실제 금 / 쌀 수입</dt>
|
||||
<dd>{{ format(data.nation.incomeGold) }} / {{ format(data.nation.incomeRice) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>지급 금 / 쌀</dt>
|
||||
<dd>{{ format(data.nation.paidGold) }} / {{ format(data.nation.paidRice) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="table-scroll" tabindex="0" aria-label="최종 국가 장수 집계">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>장수 집단</th>
|
||||
<th>인원</th>
|
||||
<th>보유 금 / 쌀 합계</th>
|
||||
<th>평균 금 / 쌀</th>
|
||||
<th>평균 숙련 (보 / 궁 / 기 / 귀 / 차)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="[key, label] in groups" :key="key">
|
||||
<th scope="row">{{ label }}</th>
|
||||
<td>{{ data.nation.populations[key].count }}</td>
|
||||
<td>
|
||||
{{ format(data.nation.populations[key].gold) }} /
|
||||
{{ format(data.nation.populations[key].rice) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ format(data.nation.populations[key].averageGold) }} /
|
||||
{{ format(data.nation.populations[key].averageRice) }}
|
||||
</td>
|
||||
<td>{{ Object.values(data.nation.populations[key].averageDex).map(format).join(' / ') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h3 {
|
||||
font-size: var(--sammo-font-size-normal);
|
||||
font-weight: bold;
|
||||
}
|
||||
.nation-values {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.nation-values dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
.nation-values dd {
|
||||
margin: 0;
|
||||
}
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 680px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 1px solid gray;
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import PanelCard from '../components/ui/PanelCard.vue';
|
||||
import AuditNationSeries from '../components/playAudit/AuditNationSeries.vue';
|
||||
import AuditNationSnapshot from '../components/playAudit/AuditNationSnapshot.vue';
|
||||
import { usePageExit } from '../composables/usePageExit';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
@@ -11,6 +12,7 @@ type Nations = Awaited<ReturnType<typeof trpc.playAudit.nations.query>>;
|
||||
type Generals = Awaited<ReturnType<typeof trpc.playAudit.generals.query>>;
|
||||
type Cities = Awaited<ReturnType<typeof trpc.playAudit.cities.query>>;
|
||||
type Series = Awaited<ReturnType<typeof trpc.playAudit.nationSeries.query>>;
|
||||
type NationSnapshot = Awaited<ReturnType<typeof trpc.playAudit.nationSnapshot.query>>;
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { pageExitLabel, exitPage } = usePageExit();
|
||||
@@ -19,6 +21,7 @@ const nations = ref<Nations | null>(null);
|
||||
const generals = ref<Generals | null>(null);
|
||||
const cities = ref<Cities | null>(null);
|
||||
const series = ref<Series | null>(null);
|
||||
const nationSnapshot = ref<NationSnapshot | null>(null);
|
||||
const authorized = ref(false);
|
||||
const profileName = ref('');
|
||||
const loading = ref(false);
|
||||
@@ -54,7 +57,13 @@ const scopeLabel = computed(() =>
|
||||
: `${route.query.year}년 ${route.query.month}월 ${route.query.at === 'final' ? '최종 표본' : '월말'}`
|
||||
);
|
||||
const result = computed(() =>
|
||||
tab.value === 'generals' ? generals.value : tab.value === 'cities' ? cities.value : series.value
|
||||
tab.value === 'generals'
|
||||
? generals.value
|
||||
: tab.value === 'cities'
|
||||
? cities.value
|
||||
: nationSnapshot.value
|
||||
? { ...nationSnapshot.value, nextCursor: null }
|
||||
: series.value
|
||||
);
|
||||
const readQuery = () => {
|
||||
tab.value = ['nations', 'generals', 'cities'].includes(String(route.query.tab))
|
||||
@@ -83,6 +92,7 @@ const load = async (append = false) => {
|
||||
generals.value = null;
|
||||
cities.value = null;
|
||||
series.value = null;
|
||||
nationSnapshot.value = null;
|
||||
}
|
||||
const filter = { at: at.value, nationId: nationId.value === '' ? undefined : Number(nationId.value), limit: 50 };
|
||||
try {
|
||||
@@ -111,6 +121,12 @@ const load = async (append = false) => {
|
||||
...response,
|
||||
items: append ? [...(cities.value?.items ?? []), ...response.items] : response.items,
|
||||
};
|
||||
} else if (nationId.value !== '' && moment.value === 'final') {
|
||||
const response = await trpc.playAudit.nationSnapshot.query({
|
||||
nationId: Number(nationId.value),
|
||||
at: { year: year.value, month: month.value, kind: 'FINAL' },
|
||||
});
|
||||
if (request === generation) nationSnapshot.value = response;
|
||||
} else if (nationId.value !== '') {
|
||||
const response = await trpc.playAudit.nationSeries.query({
|
||||
nationId: Number(nationId.value),
|
||||
@@ -292,7 +308,7 @@ onMounted(async () => {
|
||||
required
|
||||
/></label>
|
||||
<label>월<input v-model.number="month" type="number" min="1" max="12" required /></label>
|
||||
<template v-if="tab === 'nations'">
|
||||
<template v-if="tab === 'nations' && moment !== 'final'">
|
||||
<label
|
||||
>시작 연도<input
|
||||
v-model.number="fromYear"
|
||||
@@ -335,6 +351,7 @@ onMounted(async () => {
|
||||
국가를 선택하고 조회해 주세요. 멸망한 국가는 해당 월말 기준의 국가 목록에서 선택할 수 있습니다.
|
||||
</p>
|
||||
<AuditNationSeries v-if="series && tab === 'nations'" :data="series" />
|
||||
<AuditNationSnapshot v-if="nationSnapshot && tab === 'nations'" :data="nationSnapshot" />
|
||||
<template v-if="generals && tab === 'generals'">
|
||||
<p v-if="!generals.collected">선택한 시점의 표본이 없습니다.</p>
|
||||
<p v-else-if="!generals.items.length">조건에 맞는 장수가 없습니다.</p>
|
||||
|
||||
Reference in New Issue
Block a user