merge: 지난 플레이 현재 프로필 조회 경계 통합
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
|||||||
findLegacyGeneralsByOwner,
|
findLegacyGeneralsByOwner,
|
||||||
findLegacyGames,
|
findLegacyGames,
|
||||||
findLegacyNations,
|
findLegacyNations,
|
||||||
LEGACY_ARCHIVE_PROFILES,
|
isLegacyArchiveProfile,
|
||||||
type LegacyArchiveProfile,
|
type LegacyArchiveProfile,
|
||||||
type LegacyGeneralHallRow,
|
type LegacyGeneralHallRow,
|
||||||
} from '../../services/legacyArchiveStore.js';
|
} from '../../services/legacyArchiveStore.js';
|
||||||
@@ -50,15 +50,10 @@ const canonicalSnapshot = (value: unknown, fallbackName: string): ArchivedGenera
|
|||||||
|
|
||||||
const zPastPlayDetailInput = z.object({
|
const zPastPlayDetailInput = z.object({
|
||||||
source: z.enum(['current', 'legacy']).default('current'),
|
source: z.enum(['current', 'legacy']).default('current'),
|
||||||
sourceProfile: z.enum(LEGACY_ARCHIVE_PROFILES).optional(),
|
|
||||||
serverId: z.string().trim().min(1).max(64),
|
serverId: z.string().trim().min(1).max(64),
|
||||||
generalNo: z.number().int().positive(),
|
generalNo: z.number().int().positive(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const zPastPlaysInput = z.object({
|
|
||||||
sourceProfile: z.enum(LEGACY_ARCHIVE_PROFILES),
|
|
||||||
});
|
|
||||||
|
|
||||||
type ArchiveSource = 'current' | 'legacy';
|
type ArchiveSource = 'current' | 'legacy';
|
||||||
|
|
||||||
interface GeneralArchiveEntry {
|
interface GeneralArchiveEntry {
|
||||||
@@ -232,18 +227,17 @@ const buildGeneralDetail = async (entry: GeneralArchiveEntry, nation: ReturnType
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const archiveRouter = router({
|
export const archiveRouter = router({
|
||||||
myPastPlays: readOnlyAuthedProcedure.input(zPastPlaysInput).query(async ({ ctx, input }) => {
|
myPastPlays: readOnlyAuthedProcedure.query(async ({ ctx }) => {
|
||||||
const owner = ctx.auth?.user.id;
|
const owner = ctx.auth?.user.id;
|
||||||
if (!owner) throw new Error('Authenticated archive query is missing its user identity');
|
if (!owner) throw new Error('Authenticated archive query is missing its user identity');
|
||||||
|
const legacyProfile = isLegacyArchiveProfile(ctx.profile.id) ? ctx.profile.id : null;
|
||||||
|
|
||||||
const [legacyRows, currentRows] = await Promise.all([
|
const [legacyRows, currentRows] = await Promise.all([
|
||||||
findLegacyGeneralsByOwner(ctx.db, { owner, sourceProfile: input.sourceProfile }),
|
legacyProfile ? findLegacyGeneralsByOwner(ctx.db, { owner, sourceProfile: legacyProfile }) : [],
|
||||||
input.sourceProfile === ctx.profile.id
|
ctx.db.oldGeneral.findMany({
|
||||||
? ctx.db.oldGeneral.findMany({
|
where: { owner },
|
||||||
where: { owner },
|
orderBy: [{ lastYearMonth: 'desc' }, { serverId: 'desc' }, { generalNo: 'asc' }],
|
||||||
orderBy: [{ lastYearMonth: 'desc' }, { serverId: 'desc' }, { generalNo: 'asc' }],
|
}),
|
||||||
})
|
|
||||||
: [],
|
|
||||||
]);
|
]);
|
||||||
const legacyIdentity = new Set(
|
const legacyIdentity = new Set(
|
||||||
legacyRows.map((row) => `${row.sourceProfile}:${row.serverId}:${row.generalNo}`)
|
legacyRows.map((row) => `${row.sourceProfile}:${row.serverId}:${row.generalNo}`)
|
||||||
@@ -488,10 +482,7 @@ export const archiveRouter = router({
|
|||||||
myPastPlayDetail: readOnlyAuthedProcedure.input(zPastPlayDetailInput).query(async ({ ctx, input }) => {
|
myPastPlayDetail: readOnlyAuthedProcedure.input(zPastPlayDetailInput).query(async ({ ctx, input }) => {
|
||||||
const owner = ctx.auth?.user.id;
|
const owner = ctx.auth?.user.id;
|
||||||
if (!owner) throw new Error('Authenticated archive query is missing its user identity');
|
if (!owner) throw new Error('Authenticated archive query is missing its user identity');
|
||||||
const sourceProfile = input.sourceProfile ?? ctx.profile.id;
|
const sourceProfile = ctx.profile.id;
|
||||||
if (input.source === 'current' && sourceProfile !== ctx.profile.id) {
|
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: '지난 장수 기록을 찾을 수 없습니다.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry: GeneralArchiveEntry | null = null;
|
let entry: GeneralArchiveEntry | null = null;
|
||||||
let nationRows: ArchiveNationEntry[] = [];
|
let nationRows: ArchiveNationEntry[] = [];
|
||||||
@@ -500,10 +491,10 @@ export const archiveRouter = router({
|
|||||||
let battleResultAvailable = false;
|
let battleResultAvailable = false;
|
||||||
let hallBattle = legacyHallBattleSummary([]);
|
let hallBattle = legacyHallBattleSummary([]);
|
||||||
if (input.source === 'legacy') {
|
if (input.source === 'legacy') {
|
||||||
if (!LEGACY_ARCHIVE_PROFILES.includes(sourceProfile as LegacyArchiveProfile)) {
|
if (!isLegacyArchiveProfile(sourceProfile)) {
|
||||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '지원하지 않는 이전 서버 프로필입니다.' });
|
throw new TRPCError({ code: 'NOT_FOUND', message: '지난 장수 기록을 찾을 수 없습니다.' });
|
||||||
}
|
}
|
||||||
const profile = sourceProfile as LegacyArchiveProfile;
|
const profile: LegacyArchiveProfile = sourceProfile;
|
||||||
const row = await findLegacyGeneral(ctx.db, {
|
const row = await findLegacyGeneral(ctx.db, {
|
||||||
owner,
|
owner,
|
||||||
sourceProfile: profile,
|
sourceProfile: profile,
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ const context = (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (sql.includes('legacy_archive"."general')) {
|
if (sql.includes('legacy_archive"."general')) {
|
||||||
if (!query.values?.includes('hwe')) return [];
|
if (!query.values?.includes('che')) return [];
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
serverId: 'hwe_archive_1',
|
serverId: 'che_archive_1',
|
||||||
generalNo: 21,
|
generalNo: 21,
|
||||||
legacyId: 21,
|
legacyId: 21,
|
||||||
owner: 'user-1',
|
owner: 'user-1',
|
||||||
@@ -146,8 +146,8 @@ const context = (
|
|||||||
if (sql.includes('legacy_archive"."game_history')) {
|
if (sql.includes('legacy_archive"."game_history')) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
serverId: 'hwe_archive_1',
|
serverId: 'che_archive_1',
|
||||||
legacyId: 1,
|
legacyId: 1,
|
||||||
openedAt: new Date('2019-09-21T00:00:00.000Z'),
|
openedAt: new Date('2019-09-21T00:00:00.000Z'),
|
||||||
completedAt: null,
|
completedAt: null,
|
||||||
@@ -164,9 +164,9 @@ const context = (
|
|||||||
if (sql.includes('legacy_archive"."nation')) {
|
if (sql.includes('legacy_archive"."nation')) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
legacyId: 1,
|
legacyId: 1,
|
||||||
serverId: 'hwe_archive_1',
|
serverId: 'che_archive_1',
|
||||||
nation: 2,
|
nation: 2,
|
||||||
data: { name: '이전국', color: '#0000ff', level: 7 },
|
data: { name: '이전국', color: '#0000ff', level: 7 },
|
||||||
archivedAt: new Date('2020-01-02T00:00:00.000Z'),
|
archivedAt: new Date('2020-01-02T00:00:00.000Z'),
|
||||||
@@ -174,7 +174,7 @@ const context = (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (sql.includes('legacy_archive"."emperor')) {
|
if (sql.includes('legacy_archive"."emperor')) {
|
||||||
return [{ id: 99n, sourceProfile: 'hwe', legacyId: 1, serverId: 'hwe_archive_1', data: {} }];
|
return [{ id: 99n, sourceProfile: 'che', legacyId: 1, serverId: 'che_archive_1', data: {} }];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
@@ -314,12 +314,10 @@ const context = (
|
|||||||
|
|
||||||
describe('archive.myPastPlays', () => {
|
describe('archive.myPastPlays', () => {
|
||||||
it('requires authentication and returns only the authenticated owner archive', async () => {
|
it('requires authentication and returns only the authenticated owner archive', async () => {
|
||||||
await expect(
|
await expect(appRouter.createCaller(context(null)).archive.myPastPlays()).rejects.toMatchObject({
|
||||||
appRouter.createCaller(context(null)).archive.myPastPlays({ sourceProfile: 'che' })
|
|
||||||
).rejects.toMatchObject({
|
|
||||||
code: 'UNAUTHORIZED',
|
code: 'UNAUTHORIZED',
|
||||||
});
|
});
|
||||||
const result = await appRouter.createCaller(context(auth)).archive.myPastPlays({ sourceProfile: 'che' });
|
const result = await appRouter.createCaller(context(auth)).archive.myPastPlays();
|
||||||
expect(result.seasons).toEqual([
|
expect(result.seasons).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
source: 'current',
|
source: 'current',
|
||||||
@@ -397,9 +395,7 @@ describe('archive.myPastPlays', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('labels a retained cancellation as an unnumbered abandoned game without a dynasty link', async () => {
|
it('labels a retained cancellation as an unnumbered abandoned game without a dynasty link', async () => {
|
||||||
const result = await appRouter
|
const result = await appRouter.createCaller(context(auth, false, true)).archive.myPastPlays();
|
||||||
.createCaller(context(auth, false, true))
|
|
||||||
.archive.myPastPlays({ sourceProfile: 'che' });
|
|
||||||
|
|
||||||
expect(result.seasons).toEqual([
|
expect(result.seasons).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -415,12 +411,12 @@ describe('archive.myPastPlays', () => {
|
|||||||
|
|
||||||
it('returns normalized previous-server detail from the dedicated archive without exposing raw data', async () => {
|
it('returns normalized previous-server detail from the dedicated archive without exposing raw data', async () => {
|
||||||
const caller = appRouter.createCaller(context(auth, true));
|
const caller = appRouter.createCaller(context(auth, true));
|
||||||
const list = await caller.archive.myPastPlays({ sourceProfile: 'hwe' });
|
const list = await caller.archive.myPastPlays();
|
||||||
expect(list.seasons).toContainEqual(
|
expect(list.seasons).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
source: 'legacy',
|
source: 'legacy',
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
serverId: 'hwe_archive_1',
|
serverId: 'che_archive_1',
|
||||||
openedAt: '2019-09-21T00:00:00.000Z',
|
openedAt: '2019-09-21T00:00:00.000Z',
|
||||||
dynastyId: 99,
|
dynastyId: 99,
|
||||||
generals: [expect.objectContaining({ name: '이전서버장수', nationName: '이전국' })],
|
generals: [expect.objectContaining({ name: '이전서버장수', nationName: '이전국' })],
|
||||||
@@ -429,13 +425,12 @@ describe('archive.myPastPlays', () => {
|
|||||||
|
|
||||||
const detail = await caller.archive.myPastPlayDetail({
|
const detail = await caller.archive.myPastPlayDetail({
|
||||||
source: 'legacy',
|
source: 'legacy',
|
||||||
sourceProfile: 'hwe',
|
serverId: 'che_archive_1',
|
||||||
serverId: 'hwe_archive_1',
|
|
||||||
generalNo: 21,
|
generalNo: 21,
|
||||||
});
|
});
|
||||||
expect(detail).toMatchObject({
|
expect(detail).toMatchObject({
|
||||||
source: 'legacy',
|
source: 'legacy',
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
dynastyPath: '/dynasty/99?source=legacy',
|
dynastyPath: '/dynasty/99?source=legacy',
|
||||||
nation: { id: 2, name: '이전국', color: '#0000ff' },
|
nation: { id: 2, name: '이전국', color: '#0000ff' },
|
||||||
general: expect.objectContaining({
|
general: expect.objectContaining({
|
||||||
@@ -473,16 +468,18 @@ describe('archive.myPastPlays', () => {
|
|||||||
expect(JSON.stringify(detail)).not.toContain('raw_data');
|
expect(JSON.stringify(detail)).not.toContain('raw_data');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('limits the archive list to the requested source profile', async () => {
|
it('combines only the current profile Core and PHP archives without a profile selector', async () => {
|
||||||
const caller = appRouter.createCaller(context(auth, true));
|
const caller = appRouter.createCaller(context(auth, true));
|
||||||
|
|
||||||
const che = await caller.archive.myPastPlays({ sourceProfile: 'che' });
|
const result = await caller.archive.myPastPlays();
|
||||||
expect(che.seasons).toHaveLength(1);
|
expect(result.seasons).toHaveLength(2);
|
||||||
expect(che.seasons[0]?.sourceProfile).toBe('che');
|
expect(result.seasons.map((season) => season.sourceProfile)).toEqual(['che', 'che']);
|
||||||
|
expect(result.seasons.map((season) => season.source).sort()).toEqual(['current', 'legacy']);
|
||||||
|
|
||||||
const hwe = await caller.archive.myPastPlays({ sourceProfile: 'hwe' });
|
const queryWithInjectedProfile = caller.archive.myPastPlays as unknown as (input: {
|
||||||
expect(hwe.seasons).toHaveLength(1);
|
sourceProfile: string;
|
||||||
expect(hwe.seasons[0]?.sourceProfile).toBe('hwe');
|
}) => ReturnType<typeof caller.archive.myPastPlays>;
|
||||||
expect(hwe.seasons[0]?.source).toBe('legacy');
|
const attemptedCrossProfile = await queryWithInjectedProfile({ sourceProfile: 'hwe' });
|
||||||
|
expect(attemptedCrossProfile.seasons.map((season) => season.sourceProfile)).toEqual(['che', 'che']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,29 +5,27 @@ import { expect, test, type Page, type Route } from '@playwright/test';
|
|||||||
import { gamePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
import { gamePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||||
|
|
||||||
const response = (data: unknown) => ({ result: { data } });
|
const response = (data: unknown) => ({ result: { data } });
|
||||||
|
const gameProfileId = gameProfile.split(':', 1)[0] ?? 'che';
|
||||||
const operationNames = (route: Route) =>
|
const operationNames = (route: Route) =>
|
||||||
decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');
|
decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');
|
||||||
|
|
||||||
const installArchive = async (page: Page, options: { battleAvailable?: boolean; abandoned?: boolean } = {}) => {
|
const installArchive = async (page: Page, options: { battleAvailable?: boolean; abandoned?: boolean } = {}) => {
|
||||||
const requestedProfiles: string[] = [];
|
const archiveRequestBodies: string[] = [];
|
||||||
await page.addInitScript((profile) => {
|
await page.addInitScript((profile) => {
|
||||||
localStorage.setItem('sammo-game-token', 'ga_archive');
|
localStorage.setItem('sammo-game-token', 'ga_archive');
|
||||||
localStorage.setItem('sammo-game-profile', profile);
|
localStorage.setItem('sammo-game-profile', profile);
|
||||||
}, gameProfile);
|
}, gameProfile);
|
||||||
await page.route(gameTrpcRoute, async (route) => {
|
await page.route(gameTrpcRoute, async (route) => {
|
||||||
const requestBody = route.request().postData() ?? '';
|
const requestBody = route.request().postData() ?? '';
|
||||||
const requestedProfile = ['che', 'kwe', 'pwe', 'twe', 'nya', 'pya', 'hwe'].find((profile) =>
|
|
||||||
requestBody.includes(`"sourceProfile":"${profile}"`)
|
|
||||||
);
|
|
||||||
const results = operationNames(route).map((operation) => {
|
const results = operationNames(route).map((operation) => {
|
||||||
if (operation === 'auth.status') return response({ ok: true });
|
if (operation === 'auth.status') return response({ ok: true });
|
||||||
if (operation === 'lobby.info') return response({ myGeneral: null });
|
if (operation === 'lobby.info') return response({ myGeneral: null });
|
||||||
if (operation === 'archive.myPastPlays') {
|
if (operation === 'archive.myPastPlays') {
|
||||||
requestedProfiles.push(requestedProfile ?? 'missing');
|
archiveRequestBodies.push(requestBody);
|
||||||
return response({
|
return response({
|
||||||
seasons: [
|
seasons: [
|
||||||
{
|
{
|
||||||
sourceProfile: requestedProfile ?? 'che',
|
sourceProfile: gameProfileId,
|
||||||
source: options.abandoned ? 'current' : 'legacy',
|
source: options.abandoned ? 'current' : 'legacy',
|
||||||
serverId: 'che_2024_01',
|
serverId: 'che_2024_01',
|
||||||
openedAt: '2024-01-31T00:00:00.000Z',
|
openedAt: '2024-01-31T00:00:00.000Z',
|
||||||
@@ -155,26 +153,17 @@ const installArchive = async (page: Page, options: { battleAvailable?: boolean;
|
|||||||
});
|
});
|
||||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
|
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
|
||||||
});
|
});
|
||||||
return { requestedProfiles };
|
return { archiveRequestBodies };
|
||||||
};
|
};
|
||||||
|
|
||||||
test('과거 장수 기록을 체·퀘·풰·퉤·냐·퍄·훼 서버별로 나누어 조회한다', async ({ page }) => {
|
test('현재 게임 profile 기록만 조회하고 교차 profile 선택기를 노출하지 않는다', async ({ page }) => {
|
||||||
const state = await installArchive(page);
|
const state = await installArchive(page);
|
||||||
await page.goto('past-plays');
|
await page.goto('past-plays');
|
||||||
|
|
||||||
const tabs = page.getByRole('navigation', { name: '과거 장수 서버 선택' });
|
await expect(page.getByRole('navigation', { name: '과거 장수 서버 선택' })).toHaveCount(0);
|
||||||
await expect(tabs.getByRole('button')).toHaveCount(7);
|
|
||||||
await expect(tabs.getByRole('button')).toHaveText(['체', '퀘', '풰', '퉤', '냐', '퍄', '훼']);
|
|
||||||
await expect(tabs.getByRole('button', { name: '체 서버' })).toHaveAttribute('aria-pressed', 'true');
|
|
||||||
await expect(tabs.getByRole('button', { name: '체 서버' })).toHaveCSS('color', 'rgb(135, 206, 235)');
|
|
||||||
await expect(page.locator('.season-identity').getByText('체', { exact: true })).toBeVisible();
|
await expect(page.locator('.season-identity').getByText('체', { exact: true })).toBeVisible();
|
||||||
|
expect(state.archiveRequestBodies).toHaveLength(1);
|
||||||
await tabs.getByRole('button', { name: '훼 서버' }).hover();
|
expect(state.archiveRequestBodies[0]).not.toContain('sourceProfile');
|
||||||
await expect(tabs.getByRole('button', { name: '훼 서버' })).toHaveCSS('color', 'rgb(135, 206, 235)');
|
|
||||||
await tabs.getByRole('button', { name: '훼 서버' }).click();
|
|
||||||
await expect(tabs.getByRole('button', { name: '훼 서버' })).toHaveAttribute('aria-pressed', 'true');
|
|
||||||
await expect(page.locator('.season-identity').getByText('훼', { exact: true })).toBeVisible();
|
|
||||||
expect(state.requestedProfiles).toEqual(['che', 'hwe']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('지난 플레이 관직은 숫자 대신 저장된 Ref 표시명으로 나타난다', async ({ page }) => {
|
test('지난 플레이 관직은 숫자 대신 저장된 Ref 표시명으로 나타난다', async ({ page }) => {
|
||||||
@@ -350,8 +339,6 @@ test('past plays keeps the legacy-width table scrollable on a mobile viewport',
|
|||||||
const detailMetrics = await page.locator('.detail-shell').evaluate((element) => ({
|
const detailMetrics = await page.locator('.detail-shell').evaluate((element) => ({
|
||||||
width: element.getBoundingClientRect().width,
|
width: element.getBoundingClientRect().width,
|
||||||
scrollWidth: element.scrollWidth,
|
scrollWidth: element.scrollWidth,
|
||||||
profileTabsWidth: document.querySelector('.profile-tabs')!.getBoundingClientRect().width,
|
|
||||||
profileTabsScrollWidth: document.querySelector('.profile-tabs')!.scrollWidth,
|
|
||||||
hallRecordWidth: element.querySelector('[data-hall-battle-record]')!.getBoundingClientRect().width,
|
hallRecordWidth: element.querySelector('[data-hall-battle-record]')!.getBoundingClientRect().width,
|
||||||
hallRecordScrollWidth: element.querySelector('[data-hall-battle-record]')!.scrollWidth,
|
hallRecordScrollWidth: element.querySelector('[data-hall-battle-record]')!.scrollWidth,
|
||||||
hallColumns: getComputedStyle(element.querySelector('[data-hall-battle-record] dl')!).gridTemplateColumns,
|
hallColumns: getComputedStyle(element.querySelector('[data-hall-battle-record] dl')!).gridTemplateColumns,
|
||||||
@@ -360,8 +347,6 @@ test('past plays keeps the legacy-width table scrollable on a mobile viewport',
|
|||||||
}));
|
}));
|
||||||
expect(detailMetrics.width).toBe(498);
|
expect(detailMetrics.width).toBe(498);
|
||||||
expect(detailMetrics.scrollWidth).toBe(498);
|
expect(detailMetrics.scrollWidth).toBe(498);
|
||||||
expect(detailMetrics.profileTabsWidth).toBe(500);
|
|
||||||
expect(detailMetrics.profileTabsScrollWidth).toBeLessThanOrEqual(detailMetrics.profileTabsWidth);
|
|
||||||
expect(detailMetrics.hallRecordWidth).toBeGreaterThan(0);
|
expect(detailMetrics.hallRecordWidth).toBeGreaterThan(0);
|
||||||
expect(detailMetrics.hallRecordScrollWidth).toBeLessThanOrEqual(detailMetrics.hallRecordWidth);
|
expect(detailMetrics.hallRecordScrollWidth).toBeLessThanOrEqual(detailMetrics.hallRecordWidth);
|
||||||
expect(detailMetrics.hallColumns.split(' ')).toHaveLength(3);
|
expect(detailMetrics.hallColumns.split(' ')).toHaveLength(3);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { isLegacyArchiveProfile, LEGACY_ARCHIVE_PROFILES, type LegacyArchiveProfile } from '@sammo-ts/common';
|
import { isLegacyArchiveProfile, type LegacyArchiveProfile } from '@sammo-ts/common';
|
||||||
|
|
||||||
import GeneralBasicCard from '../components/main/GeneralBasicCard.vue';
|
import GeneralBasicCard from '../components/main/GeneralBasicCard.vue';
|
||||||
import GeneralBattleSummary, { type GeneralBattleSummaryData } from '../components/main/GeneralBattleSummary.vue';
|
import GeneralBattleSummary, { type GeneralBattleSummaryData } from '../components/main/GeneralBattleSummary.vue';
|
||||||
@@ -98,7 +98,6 @@ type PastPlayDetail = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type PastPlayDetailInput = {
|
type PastPlayDetailInput = {
|
||||||
sourceProfile: string;
|
|
||||||
source: string;
|
source: string;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
generalNo: number;
|
generalNo: number;
|
||||||
@@ -117,11 +116,6 @@ const profileLabels: Record<LegacyArchiveProfile, string> = {
|
|||||||
pya: '퍄',
|
pya: '퍄',
|
||||||
hwe: '훼',
|
hwe: '훼',
|
||||||
};
|
};
|
||||||
const profileOptions = LEGACY_ARCHIVE_PROFILES.map((profile) => ({ profile, label: profileLabels[profile] }));
|
|
||||||
const configuredProfile = import.meta.env.VITE_GAME_PROFILE?.trim();
|
|
||||||
const selectedProfile = ref<LegacyArchiveProfile>(
|
|
||||||
configuredProfile && isLegacyArchiveProfile(configuredProfile) ? configuredProfile : 'che'
|
|
||||||
);
|
|
||||||
|
|
||||||
const archive = ref<Archive | null>(null);
|
const archive = ref<Archive | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -130,35 +124,20 @@ const selectedKey = ref<string | null>(null);
|
|||||||
const detail = ref<PastPlayDetail | null>(null);
|
const detail = ref<PastPlayDetail | null>(null);
|
||||||
const detailLoading = ref(false);
|
const detailLoading = ref(false);
|
||||||
const detailError = ref<string | null>(null);
|
const detailError = ref<string | null>(null);
|
||||||
let archiveRequestId = 0;
|
|
||||||
|
|
||||||
const loadArchive = async () => {
|
const loadArchive = async () => {
|
||||||
const requestId = ++archiveRequestId;
|
if (loading.value) return;
|
||||||
const sourceProfile = selectedProfile.value;
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const result = await trpc.archive.myPastPlays.query({ sourceProfile });
|
archive.value = await trpc.archive.myPastPlays.query();
|
||||||
if (requestId === archiveRequestId) archive.value = result;
|
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (requestId === archiveRequestId) {
|
error.value = cause instanceof Error ? cause.message : '지난 플레이를 불러오지 못했습니다.';
|
||||||
error.value = cause instanceof Error ? cause.message : '지난 플레이를 불러오지 못했습니다.';
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
if (requestId === archiveRequestId) loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectProfile = (profile: LegacyArchiveProfile): void => {
|
|
||||||
if (selectedProfile.value === profile) return;
|
|
||||||
selectedProfile.value = profile;
|
|
||||||
archive.value = null;
|
|
||||||
selectedKey.value = null;
|
|
||||||
detail.value = null;
|
|
||||||
detailError.value = null;
|
|
||||||
void loadArchive();
|
|
||||||
};
|
|
||||||
|
|
||||||
const yearMonth = (value: number): string => `${Math.floor(value / 100)}년 ${value % 100}월`;
|
const yearMonth = (value: number): string => `${Math.floor(value / 100)}년 ${value % 100}월`;
|
||||||
const valueOrDash = (value: number | string | null): string => (value === null || value === '' ? '-' : String(value));
|
const valueOrDash = (value: number | string | null): string => (value === null || value === '' ? '-' : String(value));
|
||||||
const hallNumber = (value: number | null): string =>
|
const hallNumber = (value: number | null): string =>
|
||||||
@@ -204,7 +183,6 @@ const selectGeneral = async (season: ArchiveSeason, generalNo: number): Promise<
|
|||||||
detailLoading.value = true;
|
detailLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await queryPastPlayDetail({
|
const result = await queryPastPlayDetail({
|
||||||
sourceProfile: seasonSourceProfile(season),
|
|
||||||
source: seasonSource(season),
|
source: seasonSource(season),
|
||||||
serverId: season.serverId,
|
serverId: season.serverId,
|
||||||
generalNo,
|
generalNo,
|
||||||
@@ -257,20 +235,6 @@ onMounted(() => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="page-note">종료된 기수와 관리자가 보존한 취소 게임의 내 장수 기록입니다.</p>
|
<p class="page-note">종료된 기수와 관리자가 보존한 취소 게임의 내 장수 기록입니다.</p>
|
||||||
<nav class="profile-tabs legacy-bg1" aria-label="과거 장수 서버 선택">
|
|
||||||
<button
|
|
||||||
v-for="option in profileOptions"
|
|
||||||
:key="option.profile"
|
|
||||||
class="profile-tab"
|
|
||||||
:class="{ selected: selectedProfile === option.profile }"
|
|
||||||
type="button"
|
|
||||||
:aria-label="`${option.label} 서버`"
|
|
||||||
:aria-pressed="selectedProfile === option.profile"
|
|
||||||
@click="selectProfile(option.profile)"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
<p v-if="error" class="error-row">{{ error }}</p>
|
<p v-if="error" class="error-row">{{ error }}</p>
|
||||||
<p v-else-if="loading && !archive" class="empty-row">불러오는 중...</p>
|
<p v-else-if="loading && !archive" class="empty-row">불러오는 중...</p>
|
||||||
<p v-else-if="archive?.seasons.length === 0" class="empty-row">보관된 지난 플레이가 없습니다.</p>
|
<p v-else-if="archive?.seasons.length === 0" class="empty-row">보관된 지난 플레이가 없습니다.</p>
|
||||||
@@ -510,7 +474,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-note,
|
.page-note,
|
||||||
.profile-tabs,
|
|
||||||
.empty-row,
|
.empty-row,
|
||||||
.error-row {
|
.error-row {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -519,31 +482,6 @@ onMounted(() => {
|
|||||||
border-bottom: 1px solid #666;
|
border-bottom: 1px solid #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tabs {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
||||||
gap: 4px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-tab {
|
|
||||||
min-height: 30px;
|
|
||||||
border: 1px solid #777;
|
|
||||||
color: #ddd;
|
|
||||||
background: #222;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-tab:hover,
|
|
||||||
.profile-tab:focus-visible,
|
|
||||||
.profile-tab.selected {
|
|
||||||
border-color: skyblue;
|
|
||||||
color: skyblue;
|
|
||||||
background: #143a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-note,
|
.page-note,
|
||||||
.season-heading span {
|
.season-heading span {
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
|
|||||||
Reference in New Issue
Block a user