플레이 감사에 장수 수치 정렬과 시점별 도시 지도를 추가한다

This commit is contained in:
2026-09-26 04:52:18 +00:00
parent e35679335c
commit 51d0317a69
7 changed files with 514 additions and 8 deletions
+67 -1
View File
@@ -546,7 +546,11 @@ const install = async (
...world,
collected: !input.at || (input.at as { month: number }).month !== 7,
sample: input.at ?? null,
nextCursor: input.cursor ? null : 1,
nextCursor: input.cursor
? null
: input.sort && input.sort !== 'id'
? { id: 1, value: 1200 }
: 1,
items:
input.at && (input.at as { month: number }).month === 7
? []
@@ -567,6 +571,27 @@ const install = async (
nation: { id: 2, name: '촉' },
city: { id: 3, name: '성도' },
});
case 'playAudit.cityMap':
return result({
...world,
collected: true,
sample: input.at ?? null,
nextCursor: null,
unmappedCityIds: [],
layout: {
mapName: 'che',
cityList: [{ id: 3, name: '성도', level: 4, region: 1, x: 200, y: 180, path: [] }],
regionMap: { 1: '익주' },
levelMap: { 4: '이' },
},
map: {
year: 190,
month: (input.at as { month: number } | undefined)?.month ?? 7,
startYear: 190,
cityList: [[3, 4, 0, 2, 1, 1]],
nationList: [[2, '촉', '#ff0000', 0]],
},
});
case 'playAudit.cityDetail':
return result({
...world,
@@ -1455,3 +1480,44 @@ for (const width of [1440, 390]) {
if (width < 1200) await expect(detail).toBeInViewport();
});
}
for (const width of [1280, 390]) {
test(`audit historical map and ranked generals at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.route('https://sam-image.hided.net/**', (route) =>
route.fulfill({
contentType: 'image/svg+xml',
body: '<svg xmlns="http://www.w3.org/2000/svg" width="700" height="500"><rect width="700" height="500" fill="#203c2d"/></svg>',
})
);
const requests = await install(page);
await page.goto(gamePath('/play-audit?tab=cities&at=month&year=190&month=6'));
expect(requests.some((request) => request.operation === 'playAudit.cityMap')).toBe(false);
await page.getByRole('link', { name: '도시 지도', exact: true }).click();
await expect(page.getByRole('button', { name: '성도', exact: true })).toBeVisible();
await page.getByRole('button', { name: '성도', exact: true }).click();
await expect(page).toHaveURL(/cityRecord=3/);
await expect(page.getByRole('heading', { name: '성도 (#3) · 촉' })).toBeVisible();
await capture(page, `audit-map-${width}`);
await page.reload();
await expect(page.getByRole('button', { name: '성도', exact: true })).toBeVisible();
await page.getByRole('button', { name: '이 시점의 모든 국가 주둔 장수', exact: true }).click();
await expect(page).toHaveURL(/city=3/);
await page.getByLabel('정렬 기준', { exact: true }).selectOption('gold');
await page.getByLabel('정렬 방향', { exact: true }).selectOption('desc');
await page.getByRole('button', { name: '조회', exact: true }).click();
await expect(page).toHaveURL(/sort=gold/);
await page.getByRole('button', { name: '다음 50개 불러오기' }).click();
await expect(page.getByRole('rowheader', { name: /다음장수/ })).toBeVisible();
expect(requests.filter((request) => request.operation === 'playAudit.generals').at(-1)?.input).toMatchObject({
sort: 'gold',
order: 'desc',
cityId: 3,
at: { year: 190, month: 6, kind: 'MONTH_END' },
cursor: { id: 1, value: 1200 },
});
await page.reload();
await expect(page.getByLabel('정렬 기준', { exact: true })).toHaveValue('gold');
await capture(page, `audit-ranked-${width}`);
});
}
+82 -3
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router';
import MapViewer from '../components/main/MapViewer.vue';
import PanelCard from '../components/ui/PanelCard.vue';
import AuditNationSeries from '../components/playAudit/AuditNationSeries.vue';
import AuditNationSnapshot from '../components/playAudit/AuditNationSnapshot.vue';
@@ -24,6 +25,27 @@ const coverage = ref<Coverage | null>(null);
const nations = ref<Nations | null>(null);
const generals = ref<Generals | null>(null);
const cities = ref<Cities | null>(null);
const cityMap = ref<Awaited<ReturnType<typeof trpc.playAudit.cityMap.query>> | null>(null);
const cityView = computed(() => (route.query.view === 'map' ? 'map' : 'list'));
const sortOptions = [
['id', '장수 번호'],
['gold', '금'],
['rice', '쌀'],
['crew', '병력'],
['train', '훈련'],
['atmos', '사기'],
['leadership', '통솔'],
['strength', '무력'],
['intelligence', '지력'],
['experience', '경험'],
['dedication', '공헌'],
['dex1', '보병 숙련'],
['dex2', '궁병 숙련'],
['dex3', '기병 숙련'],
['dex4', '귀병 숙련'],
['dex5', '차병 숙련'],
] as const;
const generalSort = ref<(typeof sortOptions)[number][0]>('id');
const series = ref<Series | null>(null);
const nationSnapshot = ref<NationSnapshot | null>(null);
const authorized = ref(false);
@@ -151,7 +173,9 @@ const result = computed(() =>
tab.value === 'generals'
? generals.value
: tab.value === 'cities'
? cities.value
? cityView.value === 'map'
? cityMap.value
: cities.value
: nationSnapshot.value
? { ...nationSnapshot.value, nextCursor: null }
: series.value
@@ -231,6 +255,7 @@ const readQuery = () => {
cityId.value = route.query.city ? String(numeric(route.query.city, 0)) : '';
generalName.value = typeof route.query.name === 'string' ? route.query.name : '';
generalOrder.value = route.query.order === 'desc' ? 'desc' : 'asc';
generalSort.value = sortOptions.find(([key]) => key === route.query.sort)?.[0] ?? 'id';
population.value = ['human', 'npc', 'troopNpc'].includes(String(route.query.population))
? String(route.query.population)
: '';
@@ -254,6 +279,7 @@ const load = async (append = false) => {
if (!append) {
generals.value = null;
cities.value = null;
cityMap.value = null;
series.value = null;
nationSnapshot.value = null;
}
@@ -265,6 +291,7 @@ const load = async (append = false) => {
cityId: cityId.value === '' ? undefined : Number(cityId.value),
name: generalName.value.trim() || undefined,
order: generalOrder.value,
sort: generalSort.value,
population:
population.value === 'human' || population.value === 'npc' || population.value === 'troopNpc'
? population.value
@@ -276,6 +303,9 @@ const load = async (append = false) => {
...response,
items: append ? [...(generals.value?.items ?? []), ...response.items] : response.items,
};
} else if (tab.value === 'cities' && cityView.value === 'map') {
const response = await trpc.playAudit.cityMap.query({ at: at.value });
if (request === generation) cityMap.value = response;
} else if (tab.value === 'cities') {
const response = await trpc.playAudit.cities.query({
...filter,
@@ -337,6 +367,8 @@ const apply = async () => {
population: population.value || undefined,
name: tab.value === 'generals' ? generalName.value.trim() || undefined : undefined,
order: tab.value === 'generals' ? generalOrder.value : undefined,
sort: tab.value === 'generals' && generalSort.value !== 'id' ? generalSort.value : undefined,
view: tab.value === 'cities' ? cityView.value : undefined,
at: moment.value,
year: String(year.value),
month: String(month.value),
@@ -602,14 +634,26 @@ onMounted(async () => {
</select></label
>
<template v-if="tab === 'generals'">
<label
>정렬 기준<select
v-model="generalSort"
class="legacy-sort-select"
aria-label="정렬 기준"
>
<option v-for="[key, label] in sortOptions" :key="key" :value="key">
{{ label }}
</option>
</select></label
>
<label
>장수 이름<input v-model="generalName" maxlength="64" placeholder="이름 부분 검색"
/></label>
<label
>장수 번호 정렬<select
>{{ generalSort === 'id' ? '장수 번호 정렬' : '정렬 방향'
}}<select
class="legacy-sort-select"
v-model="generalOrder"
aria-label="장수 번호 정렬"
:aria-label="generalSort === 'id' ? '장수 번호 정렬' : '정렬 방향'"
>
<option value="asc">오름차순</option>
<option value="desc">내림차순</option>
@@ -737,6 +781,41 @@ onMounted(async () => {
</table>
</div>
</template>
<nav v-if="tab === 'cities'" class="menu-row" aria-label="도시 표시 방식">
<RouterLink
class="legacy-button"
:aria-current="cityView === 'list' ? 'page' : undefined"
:to="{ query: { ...route.query, view: 'list' } }"
>도시 목록</RouterLink
>
<RouterLink
class="legacy-button"
:aria-current="cityView === 'map' ? 'page' : undefined"
:to="{ query: { ...route.query, view: 'map' } }"
>도시 지도</RouterLink
>
</nav>
<template v-if="cityMap && tab === 'cities' && cityView === 'map'">
<p>
지도는 선택 시점의 모든 국가 도시를 표시합니다. 도시를 눌러 상세와 주둔 장수를 확인하세요.
</p>
<p v-if="!cityMap.collected">선택한 시점의 표본이 없습니다.</p>
<p v-else-if="cityMap.unmappedCityIds.length">
지도 위치가 없는 도시: {{ cityMap.unmappedCityIds.join(', ') }}. 도시 목록에서 조회할 수
있습니다.
</p>
<MapViewer
v-if="cityMap.map"
:map-data="cityMap.map"
:map-layout="cityMap.layout"
:loading="loading"
:selected-city-id="selectedCity"
:detail-mode="false"
fit-container
:show-current-city-marker="false"
@select-city="selectCity"
/>
</template>
<template v-if="cities && tab === 'cities'">
<p v-if="!cities.collected">선택한 시점의 표본이 없습니다.</p>
<p v-else-if="!cities.items.length">조건에 맞는 도시가 없습니다.</p>