feat: expose archived general histories
This commit is contained in:
@@ -24,6 +24,7 @@ const installArchive = async (page: Page) => {
|
||||
season: 51,
|
||||
scenario: 2,
|
||||
scenarioName: '천하쟁패',
|
||||
dynastyId: 7,
|
||||
generals: [
|
||||
{
|
||||
generalNo: 17,
|
||||
@@ -41,12 +42,22 @@ const installArchive = async (page: Page) => {
|
||||
personal: '대담',
|
||||
special: '상재',
|
||||
special2: '신산',
|
||||
historyCount: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (operation === 'archive.myPastPlayDetail') {
|
||||
return response({
|
||||
serverId: 'che_2024_01',
|
||||
generalNo: 17,
|
||||
name: '관우',
|
||||
lastYearMonth: 21403,
|
||||
history: ['<C>●</>214년 3월: 촉에 임관', '<Y>●</>214년 1월: 성도에서 거병'],
|
||||
});
|
||||
}
|
||||
return { error: { message: `unhandled ${operation}`, data: { code: 'BAD_REQUEST' } } };
|
||||
});
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
|
||||
@@ -65,6 +76,12 @@ test('past plays is available without a current general and preserves desktop in
|
||||
await expect(page.getByRole('heading', { name: '내 지난 플레이 보기' })).toBeVisible();
|
||||
await expect(page.getByText('천하쟁패 · 51기')).toBeVisible();
|
||||
await expect(page.locator('.general-name')).toHaveText('관우');
|
||||
await expect(page.getByRole('link', { name: '이 기수 국가 정보' })).toHaveAttribute('href', '/che/dynasty/7');
|
||||
const historyToggle = page.locator('.history-toggle');
|
||||
await expect(historyToggle).toHaveText('보기 (2)');
|
||||
await historyToggle.click();
|
||||
await expect(page.getByText('214년 3월: 촉에 임관')).toBeVisible();
|
||||
await expect(historyToggle).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
const geometry = await root.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -117,6 +134,6 @@ test('past plays keeps the legacy-width table scrollable on a mobile viewport',
|
||||
overflowX: getComputedStyle(element).overflowX,
|
||||
}));
|
||||
// The shared legacy shell keeps its historical 500 px minimum canvas.
|
||||
expect(metrics).toEqual({ clientWidth: 500, scrollWidth: 820, overflowX: 'auto' });
|
||||
expect(metrics).toEqual({ clientWidth: 500, scrollWidth: 940, overflowX: 'auto' });
|
||||
await expect(page.locator('.title-row')).toHaveCSS('flex-direction', 'column');
|
||||
});
|
||||
|
||||
@@ -4,10 +4,18 @@ import { onMounted, ref } from 'vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
type Archive = Awaited<ReturnType<typeof trpc.archive.myPastPlays.query>>;
|
||||
type PastPlayDetail = Awaited<ReturnType<typeof trpc.archive.myPastPlayDetail.query>>;
|
||||
type DetailState = {
|
||||
open: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
detail: PastPlayDetail | null;
|
||||
};
|
||||
|
||||
const archive = ref<Archive | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const details = ref<Record<string, DetailState>>({});
|
||||
|
||||
const loadArchive = async () => {
|
||||
if (loading.value) return;
|
||||
@@ -24,6 +32,43 @@ const loadArchive = async () => {
|
||||
|
||||
const yearMonth = (value: number): string => `${Math.floor(value / 100)}년 ${value % 100}월`;
|
||||
const valueOrDash = (value: number | string | null): string => (value === null || value === '' ? '-' : String(value));
|
||||
const plainLog = (value: string): string => value.replace(/<[^>]+>/g, '');
|
||||
const detailKey = (serverId: string, generalNo: number): string => `${serverId}:${generalNo}`;
|
||||
|
||||
const toggleHistory = async (serverId: string, generalNo: number): Promise<void> => {
|
||||
const key = detailKey(serverId, generalNo);
|
||||
const current = details.value[key];
|
||||
if (current?.open) {
|
||||
details.value = { ...details.value, [key]: { ...current, open: false } };
|
||||
return;
|
||||
}
|
||||
if (current?.detail) {
|
||||
details.value = { ...details.value, [key]: { ...current, open: true } };
|
||||
return;
|
||||
}
|
||||
|
||||
details.value = {
|
||||
...details.value,
|
||||
[key]: { open: true, loading: true, error: null, detail: null },
|
||||
};
|
||||
try {
|
||||
const detail = await trpc.archive.myPastPlayDetail.query({ serverId, generalNo });
|
||||
details.value = {
|
||||
...details.value,
|
||||
[key]: { open: true, loading: false, error: null, detail },
|
||||
};
|
||||
} catch (cause) {
|
||||
details.value = {
|
||||
...details.value,
|
||||
[key]: {
|
||||
open: true,
|
||||
loading: false,
|
||||
error: cause instanceof Error ? cause.message : '장수 열전을 불러오지 못했습니다.',
|
||||
detail: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
void loadArchive();
|
||||
@@ -48,10 +93,19 @@ onMounted(() => {
|
||||
<section v-for="season in archive?.seasons ?? []" :key="season.serverId" class="season-card">
|
||||
<div class="season-heading legacy-bg2">
|
||||
<strong>{{ season.serverId }}</strong>
|
||||
<span>
|
||||
{{ season.scenarioName ?? '시나리오 미상' }}
|
||||
<template v-if="season.season !== null"> · {{ season.season }}기</template>
|
||||
</span>
|
||||
<div class="season-actions">
|
||||
<span>
|
||||
{{ season.scenarioName ?? '시나리오 미상' }}
|
||||
<template v-if="season.season !== null"> · {{ season.season }}기</template>
|
||||
</span>
|
||||
<RouterLink
|
||||
v-if="season.dynastyId !== null"
|
||||
class="legacy-button nation-archive-link"
|
||||
:to="`/dynasty/${season.dynastyId}`"
|
||||
>
|
||||
이 기수 국가 정보
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
@@ -67,28 +121,82 @@ onMounted(() => {
|
||||
<th>성격</th>
|
||||
<th>내정 특기</th>
|
||||
<th>전투 특기</th>
|
||||
<th>장수 열전</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="general in season.generals" :key="general.generalNo">
|
||||
<td class="general-name">{{ general.name }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="nation-name"
|
||||
:style="{ backgroundColor: general.nationColor, color: '#ffffff' }"
|
||||
>
|
||||
{{ general.nationName }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ yearMonth(general.lastYearMonth) }}</td>
|
||||
<td>{{ valueOrDash(general.leadership) }}</td>
|
||||
<td>{{ valueOrDash(general.strength) }}</td>
|
||||
<td>{{ valueOrDash(general.intel) }}</td>
|
||||
<td>{{ valueOrDash(general.officerLevel) }}</td>
|
||||
<td>{{ valueOrDash(general.personal) }}</td>
|
||||
<td>{{ valueOrDash(general.special) }}</td>
|
||||
<td>{{ valueOrDash(general.special2) }}</td>
|
||||
</tr>
|
||||
<template v-for="general in season.generals" :key="general.generalNo">
|
||||
<tr>
|
||||
<td class="general-name">{{ general.name }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="nation-name"
|
||||
:style="{ backgroundColor: general.nationColor, color: '#ffffff' }"
|
||||
>
|
||||
{{ general.nationName }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ yearMonth(general.lastYearMonth) }}</td>
|
||||
<td>{{ valueOrDash(general.leadership) }}</td>
|
||||
<td>{{ valueOrDash(general.strength) }}</td>
|
||||
<td>{{ valueOrDash(general.intel) }}</td>
|
||||
<td>{{ valueOrDash(general.officerLevel) }}</td>
|
||||
<td>{{ valueOrDash(general.personal) }}</td>
|
||||
<td>{{ valueOrDash(general.special) }}</td>
|
||||
<td>{{ valueOrDash(general.special2) }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="legacy-button history-toggle"
|
||||
type="button"
|
||||
:aria-expanded="
|
||||
details[detailKey(season.serverId, general.generalNo)]?.open ?? false
|
||||
"
|
||||
@click="toggleHistory(season.serverId, general.generalNo)"
|
||||
>
|
||||
{{
|
||||
details[detailKey(season.serverId, general.generalNo)]?.open
|
||||
? '접기'
|
||||
: `보기 (${general.historyCount})`
|
||||
}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="details[detailKey(season.serverId, general.generalNo)]?.open" class="history-row">
|
||||
<td colspan="11">
|
||||
<p
|
||||
v-if="details[detailKey(season.serverId, general.generalNo)]?.loading"
|
||||
role="status"
|
||||
>
|
||||
장수 열전을 불러오는 중...
|
||||
</p>
|
||||
<p
|
||||
v-else-if="details[detailKey(season.serverId, general.generalNo)]?.error"
|
||||
class="history-error"
|
||||
role="alert"
|
||||
>
|
||||
{{ details[detailKey(season.serverId, general.generalNo)]?.error }}
|
||||
</p>
|
||||
<p
|
||||
v-else-if="
|
||||
details[detailKey(season.serverId, general.generalNo)]?.detail?.history
|
||||
.length === 0
|
||||
"
|
||||
>
|
||||
보관된 장수 열전이 없습니다.
|
||||
</p>
|
||||
<ol v-else class="history-list">
|
||||
<li
|
||||
v-for="(entry, index) in details[
|
||||
detailKey(season.serverId, general.generalNo)
|
||||
]?.detail?.history ?? []"
|
||||
:key="index"
|
||||
>
|
||||
{{ plainLog(entry) }}
|
||||
</li>
|
||||
</ol>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -190,13 +298,24 @@ onMounted(() => {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.season-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nation-archive-link {
|
||||
min-height: 24px;
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 820px;
|
||||
min-width: 940px;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
background: #191919;
|
||||
@@ -229,11 +348,45 @@ th {
|
||||
text-shadow: 0 1px 1px #000;
|
||||
}
|
||||
|
||||
.history-toggle {
|
||||
min-height: 24px;
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
.history-row td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
background: #101010;
|
||||
}
|
||||
|
||||
.history-row p {
|
||||
margin: 0;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.history-error {
|
||||
color: #ff8d8d !important;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
margin: 0;
|
||||
padding-left: 26px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.title-row,
|
||||
.season-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.season-actions {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user