merge: 왕조 이전 기록 profile 경계를 main에 통합
This commit is contained in:
@@ -4,11 +4,13 @@ import { z } from 'zod';
|
|||||||
import { asRecord } from '@sammo-ts/common';
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
import { procedure, router } from '../../trpc.js';
|
import { procedure, router } from '../../trpc.js';
|
||||||
|
import type { LegacyEmperorRow } from '../../services/legacyArchiveStore.js';
|
||||||
import {
|
import {
|
||||||
findLegacyEmperor,
|
findLegacyEmperor,
|
||||||
findLegacyEmperors,
|
findLegacyEmperorsByProfile,
|
||||||
findLegacyGeneralsForServer,
|
findLegacyGeneralsForServer,
|
||||||
findLegacyNations,
|
findLegacyNations,
|
||||||
|
isLegacyArchiveProfile,
|
||||||
} from '../../services/legacyArchiveStore.js';
|
} from '../../services/legacyArchiveStore.js';
|
||||||
|
|
||||||
const zDynastyDetailInput = z.object({
|
const zDynastyDetailInput = z.object({
|
||||||
@@ -65,7 +67,7 @@ const firstText = (...values: unknown[]): string => {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const legacyEmperorListEntry = (row: Awaited<ReturnType<typeof findLegacyEmperors>>[number]) => {
|
const legacyEmperorListEntry = (row: LegacyEmperorRow) => {
|
||||||
const data = asRecord(row.data);
|
const data = asRecord(row.data);
|
||||||
return {
|
return {
|
||||||
id: Number(row.id),
|
id: Number(row.id),
|
||||||
@@ -132,7 +134,9 @@ const formatNationLevel = (level: number | null): string => {
|
|||||||
export const dynastyRouter = router({
|
export const dynastyRouter = router({
|
||||||
getList: procedure.input(zDynastyListInput).query(async ({ ctx, input }) => {
|
getList: procedure.input(zDynastyListInput).query(async ({ ctx, input }) => {
|
||||||
if ((input?.source ?? 'current') === 'legacy') {
|
if ((input?.source ?? 'current') === 'legacy') {
|
||||||
const rows = await findLegacyEmperors(ctx.db);
|
const rows = isLegacyArchiveProfile(ctx.profile.id)
|
||||||
|
? await findLegacyEmperorsByProfile(ctx.db, ctx.profile.id)
|
||||||
|
: [];
|
||||||
return {
|
return {
|
||||||
source: 'legacy' as const,
|
source: 'legacy' as const,
|
||||||
current: null,
|
current: null,
|
||||||
@@ -186,7 +190,12 @@ export const dynastyRouter = router({
|
|||||||
}),
|
}),
|
||||||
getDetail: procedure.input(zDynastyDetailInput).query(async ({ ctx, input }) => {
|
getDetail: procedure.input(zDynastyDetailInput).query(async ({ ctx, input }) => {
|
||||||
if (input.source === 'legacy') {
|
if (input.source === 'legacy') {
|
||||||
const archived = await findLegacyEmperor(ctx.db, input.emperorId);
|
const archived = isLegacyArchiveProfile(ctx.profile.id)
|
||||||
|
? await findLegacyEmperor(ctx.db, {
|
||||||
|
id: input.emperorId,
|
||||||
|
sourceProfile: ctx.profile.id,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
if (!archived) {
|
if (!archived) {
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: '이전 서버 왕조 정보를 찾을 수 없습니다.' });
|
throw new TRPCError({ code: 'NOT_FOUND', message: '이전 서버 왕조 정보를 찾을 수 없습니다.' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,7 +270,26 @@ export const findLegacyEmperors = async (
|
|||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findLegacyEmperor = async (db: LegacyArchiveDatabase, id: number): Promise<LegacyEmperorRow | null> => {
|
export const findLegacyEmperorsByProfile = async (
|
||||||
|
db: LegacyArchiveDatabase,
|
||||||
|
sourceProfile: LegacyArchiveProfile
|
||||||
|
): Promise<LegacyEmperorRow[]> =>
|
||||||
|
db.$queryRaw<LegacyEmperorRow[]>(GamePrisma.sql`
|
||||||
|
SELECT
|
||||||
|
"id",
|
||||||
|
"source_profile" AS "sourceProfile",
|
||||||
|
"legacy_id" AS "legacyId",
|
||||||
|
"server_id" AS "serverId",
|
||||||
|
"data"
|
||||||
|
FROM "legacy_archive"."emperor"
|
||||||
|
WHERE "source_profile" = ${sourceProfile}
|
||||||
|
ORDER BY "id" DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const findLegacyEmperor = async (
|
||||||
|
db: LegacyArchiveDatabase,
|
||||||
|
input: { id: number; sourceProfile: LegacyArchiveProfile }
|
||||||
|
): Promise<LegacyEmperorRow | null> => {
|
||||||
const rows = await db.$queryRaw<LegacyEmperorRow[]>(GamePrisma.sql`
|
const rows = await db.$queryRaw<LegacyEmperorRow[]>(GamePrisma.sql`
|
||||||
SELECT
|
SELECT
|
||||||
"id",
|
"id",
|
||||||
@@ -279,7 +298,8 @@ export const findLegacyEmperor = async (db: LegacyArchiveDatabase, id: number):
|
|||||||
"server_id" AS "serverId",
|
"server_id" AS "serverId",
|
||||||
"data"
|
"data"
|
||||||
FROM "legacy_archive"."emperor"
|
FROM "legacy_archive"."emperor"
|
||||||
WHERE "id" = ${id}
|
WHERE "id" = ${input.id}
|
||||||
|
AND "source_profile" = ${input.sourceProfile}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`);
|
`);
|
||||||
return rows[0] ?? null;
|
return rows[0] ?? null;
|
||||||
|
|||||||
@@ -120,20 +120,24 @@ const authFor = (userId: string, roles: string[] = []): GameSessionTokenPayload
|
|||||||
|
|
||||||
const buildContext = (
|
const buildContext = (
|
||||||
auth: GameSessionTokenPayload | null,
|
auth: GameSessionTokenPayload | null,
|
||||||
oldNations: Array<Record<string, unknown>> = [oldNation, deletedOldNation]
|
oldNations: Array<Record<string, unknown>> = [oldNation, deletedOldNation],
|
||||||
|
profileId = profile.id
|
||||||
): GameApiContext => {
|
): GameApiContext => {
|
||||||
|
const selectedProfile = { ...profile, id: profileId, name: `${profileId}:default` };
|
||||||
const db = {
|
const db = {
|
||||||
$queryRaw: async (query: { strings?: readonly string[] }) => {
|
$queryRaw: async (query: { strings?: readonly string[]; values?: unknown[] }) => {
|
||||||
const sql = query.strings?.join(' ') ?? '';
|
const sql = query.strings?.join(' ') ?? '';
|
||||||
if (sql.includes('legacy_archive"."emperor')) {
|
if (sql.includes('legacy_archive"."emperor')) {
|
||||||
|
if (!query.values?.includes(selectedProfile.id)) return [];
|
||||||
|
if (sql.includes('WHERE "id"') && !query.values.includes(101)) return [];
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 101n,
|
id: 101n,
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: selectedProfile.id,
|
||||||
legacyId: 7,
|
legacyId: 7,
|
||||||
serverId: emperor.serverId,
|
serverId: emperor.serverId,
|
||||||
data: {
|
data: {
|
||||||
phase: '이전 훼2기',
|
phase: `이전 ${selectedProfile.id.toUpperCase()} 2기`,
|
||||||
nation_count: emperor.nationCount,
|
nation_count: emperor.nationCount,
|
||||||
nation_name: emperor.nationName,
|
nation_name: emperor.nationName,
|
||||||
nation_hist: emperor.nationHist,
|
nation_hist: emperor.nationHist,
|
||||||
@@ -170,9 +174,10 @@ const buildContext = (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (sql.includes('legacy_archive"."nation')) {
|
if (sql.includes('legacy_archive"."nation')) {
|
||||||
|
if (!query.values?.includes(selectedProfile.id)) return [];
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: selectedProfile.id,
|
||||||
legacyId: oldNation.id,
|
legacyId: oldNation.id,
|
||||||
serverId: oldNation.serverId,
|
serverId: oldNation.serverId,
|
||||||
nation: oldNation.nation,
|
nation: oldNation.nation,
|
||||||
@@ -182,6 +187,7 @@ const buildContext = (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (sql.includes('legacy_archive"."general')) {
|
if (sql.includes('legacy_archive"."general')) {
|
||||||
|
if (!query.values?.includes(selectedProfile.id)) return [];
|
||||||
return [
|
return [
|
||||||
{ generalNo: 11, name: '유비', lastYearMonth: 21504 },
|
{ generalNo: 11, name: '유비', lastYearMonth: 21504 },
|
||||||
{ generalNo: 12, name: '제갈량', lastYearMonth: 21504 },
|
{ generalNo: 12, name: '제갈량', lastYearMonth: 21504 },
|
||||||
@@ -217,13 +223,13 @@ const buildContext = (
|
|||||||
db: db as unknown as DatabaseClient,
|
db: db as unknown as DatabaseClient,
|
||||||
turnDaemon: new InMemoryTurnDaemonTransport(),
|
turnDaemon: new InMemoryTurnDaemonTransport(),
|
||||||
battleSim: new InMemoryBattleSimTransport(),
|
battleSim: new InMemoryBattleSimTransport(),
|
||||||
profile,
|
profile: selectedProfile,
|
||||||
auth,
|
auth,
|
||||||
uploadDir: 'uploads',
|
uploadDir: 'uploads',
|
||||||
uploadPath: '/uploads',
|
uploadPath: '/uploads',
|
||||||
uploadPublicUrl: null,
|
uploadPublicUrl: null,
|
||||||
redis,
|
redis,
|
||||||
accessTokenStore: new RedisAccessTokenStore(redis, profile.name),
|
accessTokenStore: new RedisAccessTokenStore(redis, selectedProfile.name),
|
||||||
flushStore: new InMemoryFlushStore(),
|
flushStore: new InMemoryFlushStore(),
|
||||||
gameTokenSecret: 'test-secret',
|
gameTokenSecret: 'test-secret',
|
||||||
};
|
};
|
||||||
@@ -252,27 +258,31 @@ describe('dynasty public read model', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reads previous-server dynasties only when the archive source is selected', async () => {
|
it('scopes previous-server dynasties and detail to the request profile', async () => {
|
||||||
const caller = appRouter.createCaller(buildContext(null));
|
const cheCaller = appRouter.createCaller(buildContext(null));
|
||||||
const list = await caller.dynasty.getList({ source: 'legacy' });
|
const cheList = await cheCaller.dynasty.getList({ source: 'legacy' });
|
||||||
expect(list).toMatchObject({
|
expect(cheList).toMatchObject({
|
||||||
source: 'legacy',
|
source: 'legacy',
|
||||||
current: null,
|
current: null,
|
||||||
entries: [
|
entries: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 101,
|
id: 101,
|
||||||
source: 'legacy',
|
source: 'legacy',
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
phase: '이전 훼2기',
|
phase: '이전 CHE 2기',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const detail = await caller.dynasty.getDetail({ emperorId: 101, source: 'legacy' });
|
const staleListInput = { source: 'legacy' as const, sourceProfile: 'hwe' as const };
|
||||||
expect(detail).toMatchObject({
|
const staleList = await cheCaller.dynasty.getList(staleListInput);
|
||||||
|
expect(staleList.entries.map((entry) => entry.sourceProfile)).toEqual(['che']);
|
||||||
|
|
||||||
|
const cheDetail = await cheCaller.dynasty.getDetail({ emperorId: 101, source: 'legacy' });
|
||||||
|
expect(cheDetail).toMatchObject({
|
||||||
source: 'legacy',
|
source: 'legacy',
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
emperor: expect.objectContaining({ id: 101, phase: '이전 훼2기', name: '촉' }),
|
emperor: expect.objectContaining({ id: 101, phase: '이전 CHE 2기', name: '촉' }),
|
||||||
nations: [
|
nations: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: '촉',
|
name: '촉',
|
||||||
@@ -283,6 +293,22 @@ describe('dynasty public read model', () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const staleDetailInput = { emperorId: 101, source: 'legacy' as const, sourceProfile: 'hwe' as const };
|
||||||
|
const staleDetail = await cheCaller.dynasty.getDetail(staleDetailInput);
|
||||||
|
expect(staleDetail.sourceProfile).toBe('che');
|
||||||
|
|
||||||
|
const hweCaller = appRouter.createCaller(buildContext(null, undefined, 'hwe'));
|
||||||
|
const hweList = await hweCaller.dynasty.getList({ source: 'legacy' });
|
||||||
|
expect(hweList.entries).toEqual([expect.objectContaining({ sourceProfile: 'hwe', phase: '이전 HWE 2기' })]);
|
||||||
|
const hweDetail = await hweCaller.dynasty.getDetail({ emperorId: 101, source: 'legacy' });
|
||||||
|
expect(hweDetail.sourceProfile).toBe('hwe');
|
||||||
|
|
||||||
|
const developmentCaller = appRouter.createCaller(buildContext(null, undefined, 'development'));
|
||||||
|
await expect(developmentCaller.dynasty.getList({ source: 'legacy' })).resolves.toMatchObject({ entries: [] });
|
||||||
|
await expect(developmentCaller.dynasty.getDetail({ emperorId: 101, source: 'legacy' })).rejects.toMatchObject({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exposes the same public DTO to anonymous, general owners and admins', async () => {
|
it('exposes the same public DTO to anonymous, general owners and admins', async () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const isLegacyRequest = (route: Route): boolean =>
|
|||||||
|
|
||||||
const installArchiveViews = async (page: Page) => {
|
const installArchiveViews = async (page: Page) => {
|
||||||
const hallRequests: string[] = [];
|
const hallRequests: string[] = [];
|
||||||
|
const dynastyRequests: string[] = [];
|
||||||
await page.addInitScript((profile) => {
|
await page.addInitScript((profile) => {
|
||||||
localStorage.setItem('sammo-game-token', 'ga_archive_views');
|
localStorage.setItem('sammo-game-token', 'ga_archive_views');
|
||||||
localStorage.setItem('sammo-game-profile', profile);
|
localStorage.setItem('sammo-game-profile', profile);
|
||||||
@@ -21,6 +22,9 @@ const installArchiveViews = async (page: Page) => {
|
|||||||
if (operations.some((operation) => operation.startsWith('ranking.getHallOfFame'))) {
|
if (operations.some((operation) => operation.startsWith('ranking.getHallOfFame'))) {
|
||||||
hallRequests.push(decodeURIComponent(`${route.request().url()} ${route.request().postData() ?? ''}`));
|
hallRequests.push(decodeURIComponent(`${route.request().url()} ${route.request().postData() ?? ''}`));
|
||||||
}
|
}
|
||||||
|
if (operations.some((operation) => operation.startsWith('dynasty.'))) {
|
||||||
|
dynastyRequests.push(decodeURIComponent(`${route.request().url()} ${route.request().postData() ?? ''}`));
|
||||||
|
}
|
||||||
const results = operations.map((operation) => {
|
const results = operations.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: { id: 1, name: '기록장수' } });
|
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '기록장수' } });
|
||||||
@@ -67,8 +71,8 @@ const installArchiveViews = async (page: Page) => {
|
|||||||
{
|
{
|
||||||
id: legacy ? 101 : 1,
|
id: legacy ? 101 : 1,
|
||||||
source: legacy ? 'legacy' : 'current',
|
source: legacy ? 'legacy' : 'current',
|
||||||
sourceProfile: legacy ? 'hwe' : 'che',
|
sourceProfile: 'che',
|
||||||
serverId: legacy ? 'hwe-old-1' : 'che-current-1',
|
serverId: legacy ? 'che-old-1' : 'che-current-1',
|
||||||
phase: legacy ? '이전 1기' : '현재 1기',
|
phase: legacy ? '이전 1기' : '현재 1기',
|
||||||
name: '촉',
|
name: '촉',
|
||||||
year: 215,
|
year: 215,
|
||||||
@@ -93,10 +97,10 @@ const installArchiveViews = async (page: Page) => {
|
|||||||
if (operation === 'dynasty.getDetail') {
|
if (operation === 'dynasty.getDetail') {
|
||||||
return response({
|
return response({
|
||||||
source: 'legacy',
|
source: 'legacy',
|
||||||
sourceProfile: 'hwe',
|
sourceProfile: 'che',
|
||||||
emperor: {
|
emperor: {
|
||||||
id: 101,
|
id: 101,
|
||||||
serverId: 'hwe-old-1',
|
serverId: 'che-old-1',
|
||||||
winnerNationId: 1,
|
winnerNationId: 1,
|
||||||
phase: '이전 1기',
|
phase: '이전 1기',
|
||||||
nationCount: '1 / 2',
|
nationCount: '1 / 2',
|
||||||
@@ -189,7 +193,7 @@ const installArchiveViews = async (page: Page) => {
|
|||||||
});
|
});
|
||||||
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 { hallRequests };
|
return { dynastyRequests, hallRequests };
|
||||||
};
|
};
|
||||||
|
|
||||||
test('명예의 전당은 현재 profile의 현재·이전 서버 기록만 조회한다', async ({ page }, testInfo) => {
|
test('명예의 전당은 현재 profile의 현재·이전 서버 기록만 조회한다', async ({ page }, testInfo) => {
|
||||||
@@ -217,19 +221,36 @@ test('명예의 전당은 현재 profile의 현재·이전 서버 기록만 조
|
|||||||
await page.screenshot({ path: testInfo.outputPath('hall-profile-scope-mobile.png'), fullPage: true });
|
await page.screenshot({ path: testInfo.outputPath('hall-profile-scope-mobile.png'), fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('왕조 일람과 상세는 이전 서버 source와 profile을 유지한다', async ({ page }) => {
|
test('왕조 일람과 상세는 현재 profile의 이전 서버 기록만 조회한다', async ({ page }, testInfo) => {
|
||||||
await installArchiveViews(page);
|
const state = await installArchiveViews(page);
|
||||||
await page.setViewportSize({ width: 1200, height: 800 });
|
await page.setViewportSize({ width: 1200, height: 800 });
|
||||||
await page.goto('dynasty');
|
await page.goto('dynasty');
|
||||||
|
|
||||||
await expect(page.getByText('현재 1기')).toBeVisible();
|
await expect(page.getByText('현재 1기')).toBeVisible();
|
||||||
|
await page.getByLabel('기록 구분').focus();
|
||||||
|
await expect(page.getByLabel('기록 구분')).toBeFocused();
|
||||||
await page.getByLabel('기록 구분').selectOption('legacy');
|
await page.getByLabel('기록 구분').selectOption('legacy');
|
||||||
await expect(page.getByText(/이전 1기.*HWE 이전 서버/)).toBeVisible();
|
await expect(page.getByText(/이전 1기.*이전 서버/)).toBeVisible();
|
||||||
|
await expect(page.getByText(/CHE 이전 서버|HWE 이전 서버/)).toHaveCount(0);
|
||||||
|
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
||||||
|
await expect(page.locator('.dynasty-table')).toHaveCSS('height', '139px');
|
||||||
|
await expect(page.locator('.dynasty-table .phase-heading')).toHaveCSS('background-color', 'rgb(135, 206, 235)');
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('dynasty-list-profile-scope-desktop.png'), fullPage: true });
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('dynasty-list-profile-scope-mobile.png'), fullPage: true });
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1200, height: 800 });
|
||||||
const detailLink = page.getByRole('link', { name: '자세히' });
|
const detailLink = page.getByRole('link', { name: '자세히' });
|
||||||
await expect(detailLink).toHaveAttribute('href', /dynasty\/101\?source=legacy$/);
|
await expect(detailLink).toHaveAttribute('href', /dynasty\/101\?source=legacy$/);
|
||||||
await detailLink.click();
|
await detailLink.click();
|
||||||
await expect(page.getByText(/이전 1기.*HWE 이전 서버/)).toBeVisible();
|
await expect(page.getByText(/이전 1기.*이전 서버/)).toBeVisible();
|
||||||
|
await expect(page.getByText(/CHE 이전 서버|HWE 이전 서버/)).toHaveCount(0);
|
||||||
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
||||||
|
expect(state.dynastyRequests.some((request) => request.includes('legacy'))).toBe(true);
|
||||||
|
expect(state.dynastyRequests.every((request) => !request.includes('sourceProfile'))).toBe(true);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('dynasty-detail-profile-scope-desktop.png'), fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 글자를 사용한다', async ({ page }, testInfo) => {
|
test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 글자를 사용한다', async ({ page }, testInfo) => {
|
||||||
|
|||||||
@@ -92,9 +92,7 @@ onMounted(loadDetail);
|
|||||||
<td class="phase-heading centered" colspan="6">
|
<td class="phase-heading centered" colspan="6">
|
||||||
<span class="large-text">
|
<span class="large-text">
|
||||||
{{ data.emperor.phase }}
|
{{ data.emperor.phase }}
|
||||||
<template v-if="data.source === 'legacy'">
|
<template v-if="data.source === 'legacy'"> [이전 서버] </template>
|
||||||
[{{ data.sourceProfile.toUpperCase() }} 이전 서버]
|
|
||||||
</template>
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ watch(selectedSource, loadDynasty);
|
|||||||
<td class="phase-heading" colspan="8">
|
<td class="phase-heading" colspan="8">
|
||||||
<span class="large-text"
|
<span class="large-text"
|
||||||
>{{ entry.phase
|
>{{ entry.phase
|
||||||
}}<template v-if="entry.source === 'legacy'">
|
}}<template v-if="entry.source === 'legacy'"> [이전 서버]</template></span
|
||||||
[{{ entry.sourceProfile.toUpperCase() }} 이전 서버]</template
|
|
||||||
></span
|
|
||||||
>
|
>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{
|
:to="{
|
||||||
|
|||||||
Reference in New Issue
Block a user