플레이 감사에 장수 수치 정렬과 시점별 도시 지도를 추가한다
This commit is contained in:
@@ -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}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user