fix(gateway): show legacy season states
This commit is contained in:
@@ -45,6 +45,7 @@ export const zWorldStateMeta = z.object({
|
||||
turntime: z.string().optional(),
|
||||
otherTextInfo: z.string().optional(),
|
||||
isUnited: z.number().optional(),
|
||||
isunited: z.number().optional(),
|
||||
autorun_user: z
|
||||
.object({
|
||||
limit_minutes: z.number().optional(),
|
||||
|
||||
@@ -52,7 +52,7 @@ export const lobbyRouter = router({
|
||||
opentime: worldState.meta.opentime ?? '',
|
||||
turntime: worldState.meta.turntime ?? '',
|
||||
otherTextInfo: worldState.meta.otherTextInfo ?? '',
|
||||
isUnited: worldState.meta.isUnited ?? 0,
|
||||
isUnited: worldState.meta.isunited ?? worldState.meta.isUnited ?? 0,
|
||||
selectionPoolEnabled: isSelectionPoolWorld(rawWorldState),
|
||||
npcPossessionEnabled: worldState.config.npcMode === 1,
|
||||
myGeneral,
|
||||
|
||||
@@ -88,7 +88,7 @@ const loadWorldTrendSnapshot = async (ctx: GameApiContext): Promise<WorldTrendSn
|
||||
opentime: meta.opentime ?? '',
|
||||
turntime: meta.turntime ?? '',
|
||||
otherTextInfo: meta.otherTextInfo ?? '',
|
||||
isUnited: meta.isUnited ?? 0,
|
||||
isUnited: meta.isunited ?? meta.isUnited ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -178,11 +178,13 @@ const globalGenerals = [
|
||||
}),
|
||||
];
|
||||
|
||||
const createContext = (options: {
|
||||
auth?: GameSessionTokenPayload | null;
|
||||
me?: GeneralRow | null;
|
||||
isUnited?: number;
|
||||
} = {}): { context: GameApiContext; requestCommand: ReturnType<typeof vi.fn> } => {
|
||||
const createContext = (
|
||||
options: {
|
||||
auth?: GameSessionTokenPayload | null;
|
||||
me?: GeneralRow | null;
|
||||
isUnited?: number;
|
||||
} = {}
|
||||
): { context: GameApiContext; requestCommand: ReturnType<typeof vi.fn> } => {
|
||||
const token = options.auth === undefined ? auth() : options.auth;
|
||||
const me = options.me === undefined ? actor({ userId: token?.user.id ?? 'user-1' }) : options.me;
|
||||
const requestCommand = vi.fn();
|
||||
@@ -280,12 +282,7 @@ describe('legacy global nation/general directories', () => {
|
||||
ambassadorNames: ['군주', '외교관'],
|
||||
auditorCount: 1,
|
||||
});
|
||||
expect(result[1]?.generals.map((general) => general.name)).toEqual([
|
||||
'군주',
|
||||
'외교관',
|
||||
'제재외교관',
|
||||
'조언자',
|
||||
]);
|
||||
expect(result[1]?.generals.map((general) => general.name)).toEqual(['군주', '외교관', '제재외교관', '조언자']);
|
||||
expect(result[1]?.officers[0]).toMatchObject({
|
||||
officerLevel: 12,
|
||||
general: { id: 10, name: '군주' },
|
||||
@@ -311,9 +308,9 @@ describe('legacy global nation/general directories', () => {
|
||||
expect(defaultResult.generals.find((general) => general.id === 10)?.ownerName).toBeNull();
|
||||
});
|
||||
|
||||
it('reveals only the legacy owner display name after unification', async () => {
|
||||
it.each([1, 2, 3])('reveals only the legacy owner display name in united state %i', async (isUnited) => {
|
||||
const result = await appRouter
|
||||
.createCaller(createContext({ isUnited: 1 }).context)
|
||||
.createCaller(createContext({ isUnited }).context)
|
||||
.world.getGeneralDirectory({ sort: 1 });
|
||||
expect(result.generals.find((general) => general.id === 10)?.ownerName).toBe('통일유저');
|
||||
expect(JSON.stringify(result)).not.toContain('user-1');
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { DatabaseClient, GameApiContext } from '../src/context.js';
|
||||
import { appRouter } from '../src/router.js';
|
||||
|
||||
const buildContext = (meta: Record<string, unknown>): GameApiContext =>
|
||||
({
|
||||
auth: null,
|
||||
db: {
|
||||
worldState: {
|
||||
findFirst: vi.fn(async () => ({
|
||||
id: 1,
|
||||
scenarioCode: 'default',
|
||||
currentYear: 200,
|
||||
currentMonth: 1,
|
||||
tickSeconds: 3_600,
|
||||
config: {},
|
||||
meta,
|
||||
updatedAt: new Date('2026-07-31T00:00:00.000Z'),
|
||||
})),
|
||||
},
|
||||
general: {
|
||||
count: vi.fn(async () => 0),
|
||||
},
|
||||
nation: {
|
||||
count: vi.fn(async () => 0),
|
||||
},
|
||||
} as unknown as DatabaseClient,
|
||||
}) as GameApiContext;
|
||||
|
||||
describe('lobby season state', () => {
|
||||
it.each([0, 1, 2, 3])('returns legacy isunited state %i', async (isunited) => {
|
||||
const result = await appRouter
|
||||
.createCaller(buildContext({ isUnited: isunited === 0 ? 2 : 0, isunited }))
|
||||
.lobby.info();
|
||||
|
||||
expect(result.isUnited).toBe(isunited);
|
||||
});
|
||||
});
|
||||
@@ -155,7 +155,7 @@ export const createRaiseInvaderHandler = (options: {
|
||||
}
|
||||
npcEachCount = Math.max(10, toInteger(npcEachCount));
|
||||
|
||||
world.updateWorldMeta({ isunited: 1 });
|
||||
world.updateWorldMeta({ isunited: 1, isUnited: 1 });
|
||||
const totalGeneralCount = npcEachCount * invaderCities.length + world.listGenerals().length;
|
||||
const maxGeneralsPerMinute =
|
||||
options.maxGeneralsPerMinute ?? readNumber(world.getState().meta.maxGeneralsPerMinute, 1_000);
|
||||
@@ -531,6 +531,7 @@ export const createInvaderEndingHandler = (options: {
|
||||
}
|
||||
world.updateWorldMeta({
|
||||
isunited: 3,
|
||||
isUnited: 3,
|
||||
refreshLimit: readNumber(meta.refreshLimit) * 100,
|
||||
});
|
||||
world.removeEvent(environment.currentEventID);
|
||||
|
||||
@@ -255,6 +255,7 @@ describe('invader monthly actions', () => {
|
||||
]);
|
||||
expect(harness.world.getState().meta).toMatchObject({
|
||||
isunited: 1,
|
||||
isUnited: 1,
|
||||
block_change_scout: false,
|
||||
lastNationId: 10,
|
||||
});
|
||||
@@ -363,7 +364,7 @@ describe('invader monthly actions', () => {
|
||||
|
||||
await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'));
|
||||
|
||||
expect(world.getState().meta).toMatchObject({ isunited: 3, refreshLimit: 300 });
|
||||
expect(world.getState().meta).toMatchObject({ isunited: 3, isUnited: 3, refreshLimit: 300 });
|
||||
expect(world.listEvents()).toHaveLength(0);
|
||||
expect(world.peekDirtyState().logs.map((log) => log.text)).toEqual([
|
||||
'<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!',
|
||||
|
||||
@@ -443,7 +443,7 @@ integration('RaiseInvader database persistence', () => {
|
||||
})
|
||||
).toBe(3);
|
||||
expect(await db.worldState.findUniqueOrThrow({ where: { id: stateRow.id } })).toMatchObject({
|
||||
meta: expect.objectContaining({ isunited: 1, block_change_scout: false }),
|
||||
meta: expect.objectContaining({ isunited: 1, isUnited: 1, block_change_scout: false }),
|
||||
});
|
||||
if (referenceTrace) {
|
||||
expect(referenceTrace.phases.afterRaise).toMatchObject({
|
||||
@@ -451,6 +451,7 @@ integration('RaiseInvader database persistence', () => {
|
||||
diplomacyCountsPerNation: [2],
|
||||
diplomacyStates: ['1:24'],
|
||||
isunited: 1,
|
||||
isUnited: 1,
|
||||
blockChangeScout: false,
|
||||
});
|
||||
}
|
||||
@@ -539,7 +540,7 @@ integration('RaiseInvader database persistence', () => {
|
||||
|
||||
expect(await db.event.findUnique({ where: { id: createdEventIds[1]! } })).toBeNull();
|
||||
expect(await db.worldState.findUniqueOrThrow({ where: { id: stateRow.id } })).toMatchObject({
|
||||
meta: expect.objectContaining({ isunited: 3, refreshLimit: 300 }),
|
||||
meta: expect.objectContaining({ isunited: 3, isUnited: 3, refreshLimit: 300 }),
|
||||
});
|
||||
expect(
|
||||
await db.logEntry.findMany({
|
||||
@@ -556,6 +557,7 @@ integration('RaiseInvader database persistence', () => {
|
||||
result: 'Deleted',
|
||||
endingEventPresent: false,
|
||||
isunited: 3,
|
||||
isUnited: 3,
|
||||
refreshLimit: 300,
|
||||
logs: [
|
||||
'<C>●</>200년 4월:<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
const response = (data: unknown) => ({ result: { data } });
|
||||
const artifactRoot = process.env.GATEWAY_STATUS_ARTIFACT_DIR;
|
||||
|
||||
if (artifactRoot) {
|
||||
mkdirSync(artifactRoot, { recursive: true });
|
||||
}
|
||||
|
||||
const operationNames = (route: Route): string[] => {
|
||||
const url = new URL(route.request().url());
|
||||
@@ -19,6 +27,7 @@ const fulfillTrpc = async (route: Route, results: unknown[]): Promise<void> => {
|
||||
};
|
||||
|
||||
type LobbyFixtureOptions = {
|
||||
authenticated?: boolean;
|
||||
canCreateGeneral?: boolean;
|
||||
myGeneral?: {
|
||||
name: string;
|
||||
@@ -29,10 +38,16 @@ type LobbyFixtureOptions = {
|
||||
npcPossessionEnabled?: boolean;
|
||||
userCnt?: number;
|
||||
maxUserCnt?: number;
|
||||
nationCnt?: number;
|
||||
isUnited?: number;
|
||||
starttime?: string;
|
||||
opentime?: string;
|
||||
turntime?: string;
|
||||
};
|
||||
|
||||
const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) => {
|
||||
const {
|
||||
authenticated = true,
|
||||
canCreateGeneral = true,
|
||||
myGeneral = {
|
||||
name: '선택장수',
|
||||
@@ -43,22 +58,33 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
||||
npcPossessionEnabled = false,
|
||||
userCnt = 1,
|
||||
maxUserCnt = 500,
|
||||
nationCnt = 0,
|
||||
isUnited = 0,
|
||||
starttime = '2026-07-30 00:00:00',
|
||||
opentime = '2026-07-30 00:00:00',
|
||||
turntime = '2026-07-30 00:05:00',
|
||||
} = options;
|
||||
const gameOperations: Array<{ operation: string; authorization: string | undefined }> = [];
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('sammo-session-token', 'gateway-lobby-session');
|
||||
});
|
||||
if (authenticated) {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('sammo-session-token', 'gateway-lobby-session');
|
||||
});
|
||||
}
|
||||
await page.route('**/gateway/api/trpc/**', async (route) => {
|
||||
const results = operationNames(route).map((operation) => {
|
||||
if (operation === 'me') {
|
||||
return response({
|
||||
id: 'lobby-user',
|
||||
username: 'lobby-user',
|
||||
displayName: '로비사용자',
|
||||
roles: ['user'],
|
||||
kakaoVerified: true,
|
||||
createdAt: '2026-07-30T00:00:00.000Z',
|
||||
});
|
||||
return response(
|
||||
authenticated
|
||||
? {
|
||||
id: 'lobby-user',
|
||||
username: 'lobby-user',
|
||||
displayName: '로비사용자',
|
||||
roles: ['user'],
|
||||
kakaoVerified: true,
|
||||
createdAt: '2026-07-30T00:00:00.000Z',
|
||||
}
|
||||
: null
|
||||
);
|
||||
}
|
||||
if (operation === 'lobby.notice') {
|
||||
return response('');
|
||||
@@ -119,14 +145,14 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
||||
userCnt,
|
||||
maxUserCnt,
|
||||
npcCnt: 0,
|
||||
nationCnt: 0,
|
||||
nationCnt,
|
||||
turnTerm: 5,
|
||||
fictionMode: '가상',
|
||||
starttime: '2026-07-30 00:00:00',
|
||||
opentime: '2026-07-30 00:00:00',
|
||||
turntime: '2026-07-30 00:05:00',
|
||||
starttime,
|
||||
opentime,
|
||||
turntime,
|
||||
otherTextInfo: '',
|
||||
isUnited: 0,
|
||||
isUnited,
|
||||
selectionPoolEnabled,
|
||||
npcPossessionEnabled,
|
||||
myGeneral,
|
||||
@@ -231,3 +257,99 @@ test('shows registration closed instead of acquisition actions at the Ref capaci
|
||||
await expect(row.getByRole('button', { name: '장수생성' })).toHaveCount(0);
|
||||
await expect(row.getByRole('button', { name: '장수빙의' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
for (const season of [
|
||||
{
|
||||
name: 'competition',
|
||||
isUnited: 0,
|
||||
opentime: '2000-01-01 00:00:00',
|
||||
label: '<4국 경쟁중>',
|
||||
period: '2026-07-30 00:00:00 ~',
|
||||
},
|
||||
{
|
||||
name: 'preopen',
|
||||
isUnited: 0,
|
||||
opentime: '2099-01-01 00:00:00',
|
||||
label: '-가오픈 중-',
|
||||
period: '2026-07-30 00:00:00 ~',
|
||||
},
|
||||
{
|
||||
name: 'event running',
|
||||
isUnited: 1,
|
||||
opentime: '2099-01-01 00:00:00',
|
||||
label: '§이벤트 진행중§',
|
||||
period: '2026-07-30 00:00:00 ~',
|
||||
},
|
||||
{
|
||||
name: 'united',
|
||||
isUnited: 2,
|
||||
opentime: '2099-01-01 00:00:00',
|
||||
label: '§천하통일§',
|
||||
period: '2026-07-30 00:00:00\n~ 2026-07-30 00:05:00',
|
||||
},
|
||||
{
|
||||
name: 'event finished',
|
||||
isUnited: 3,
|
||||
opentime: '2099-01-01 00:00:00',
|
||||
label: '§이벤트 종료§',
|
||||
period: '2026-07-30 00:00:00\n~ 2026-07-30 00:05:00',
|
||||
},
|
||||
] as const) {
|
||||
test(`renders the Ref ${season.name} season status without changing entry actions`, async ({ page }) => {
|
||||
await installFixture(page, {
|
||||
isUnited: season.isUnited,
|
||||
opentime: season.opentime,
|
||||
nationCnt: 4,
|
||||
});
|
||||
|
||||
await page.goto('lobby');
|
||||
const row = page.locator('tbody tr').filter({ hasText: 'hwe섭' });
|
||||
const status = row.getByText(season.label, { exact: true });
|
||||
await expect(status).toBeVisible();
|
||||
await expect(row.locator('td').first().locator('[title]')).toHaveAttribute('title', season.period);
|
||||
await expect(row.getByRole('button', { name: '입장' })).toBeVisible();
|
||||
|
||||
if (artifactRoot) {
|
||||
const [viewport, rowGeometry, statusGeometry] = await Promise.all([
|
||||
page.evaluate(() => ({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
})),
|
||||
row.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||
}),
|
||||
status.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: style.color,
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
lineHeight: style.lineHeight,
|
||||
textAlign: style.textAlign,
|
||||
};
|
||||
}),
|
||||
]);
|
||||
const geometry = { viewport, row: rowGeometry, status: statusGeometry };
|
||||
const slug = season.name.replaceAll(' ', '-');
|
||||
writeFileSync(resolve(artifactRoot, `gateway-${slug}.json`), `${JSON.stringify(geometry, null, 2)}\n`);
|
||||
await page.screenshot({ path: resolve(artifactRoot, `gateway-${slug}.png`), fullPage: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('renders the same Ref season status on the public gateway page', async ({ page }) => {
|
||||
await installFixture(page, { authenticated: false, isUnited: 3, nationCnt: 4 });
|
||||
|
||||
await page.goto('');
|
||||
const status = page.locator('.season-status');
|
||||
await expect(status).toHaveText('§이벤트 종료§');
|
||||
await expect(status).toHaveAttribute('title', '2026-07-30 00:00:00\n~ 2026-07-30 00:05:00');
|
||||
await expect(page.getByRole('button', { name: '현황 새로고침' })).toBeEnabled();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface ServerSeasonStatusInput {
|
||||
isUnited: number;
|
||||
nationCnt: number;
|
||||
opentime: string;
|
||||
starttime: string;
|
||||
turntime: string;
|
||||
}
|
||||
|
||||
export interface ServerSeasonStatus {
|
||||
code: 'COMPETING' | 'PREOPEN' | 'EVENT_RUNNING' | 'UNITED' | 'EVENT_FINISHED';
|
||||
label: string;
|
||||
period: string;
|
||||
}
|
||||
|
||||
export const resolveServerSeasonStatus = (info: ServerSeasonStatusInput, now = new Date()): ServerSeasonStatus => {
|
||||
const finishedPeriod = `${info.starttime}\n~ ${info.turntime}`;
|
||||
if (info.isUnited === 3) {
|
||||
return { code: 'EVENT_FINISHED', label: '§이벤트 종료§', period: finishedPeriod };
|
||||
}
|
||||
if (info.isUnited === 1) {
|
||||
return { code: 'EVENT_RUNNING', label: '§이벤트 진행중§', period: `${info.starttime} ~` };
|
||||
}
|
||||
if (info.isUnited === 2) {
|
||||
return { code: 'UNITED', label: '§천하통일§', period: finishedPeriod };
|
||||
}
|
||||
|
||||
const openAt = new Date(info.opentime);
|
||||
if (Number.isFinite(openAt.getTime()) && openAt.getTime() > now.getTime()) {
|
||||
return { code: 'PREOPEN', label: '-가오픈 중-', period: `${info.starttime} ~` };
|
||||
}
|
||||
return { code: 'COMPETING', label: `<${info.nationCnt}국 경쟁중>`, period: `${info.starttime} ~` };
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { createGameTrpc, type GameRouter } from '../utils/gameTrpc';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { formatLog } from '../utils/formatLog';
|
||||
import { sealPassword } from '../utils/passwordEnvelope';
|
||||
import { resolveServerSeasonStatus } from '../utils/serverSeasonStatus';
|
||||
|
||||
type GatewayOutput = inferRouterOutputs<AppRouter>;
|
||||
type GameOutput = inferRouterOutputs<GameRouter>;
|
||||
@@ -37,6 +38,7 @@ const dateText = computed(() => {
|
||||
}
|
||||
return `西紀 ${info.value.year}年 ${info.value.month}月`;
|
||||
});
|
||||
const seasonStatus = computed(() => (info.value ? resolveServerSeasonStatus(info.value) : null));
|
||||
|
||||
const loadPublicStatus = async (): Promise<void> => {
|
||||
statusLoading.value = true;
|
||||
@@ -153,9 +155,7 @@ const handlePasswordReset = async (): Promise<void> => {
|
||||
<button class="login-button" type="submit" :disabled="loginLoading">
|
||||
{{ loginLoading ? '로그인 중…' : '로그인' }}
|
||||
</button>
|
||||
<button class="reset-button" type="button" @click="handlePasswordReset">
|
||||
비밀번호 초기화
|
||||
</button>
|
||||
<button class="reset-button" type="button" @click="handlePasswordReset">비밀번호 초기화</button>
|
||||
<RouterLink class="signup-button" to="/signup">아이디로 회원가입</RouterLink>
|
||||
</form>
|
||||
<p v-if="loginError" class="login-error" role="alert">{{ loginError }}</p>
|
||||
@@ -174,7 +174,10 @@ const handlePasswordReset = async (): Promise<void> => {
|
||||
</div>
|
||||
<ul v-if="info" class="status-summary">
|
||||
<li>서버: {{ profile?.korName }} / 시나리오: {{ profile?.scenario }}</li>
|
||||
<li>유저 {{ info.userCnt }}명 · NPC {{ info.npcCnt }}명 · {{ info.nationCnt }}국 경쟁중</li>
|
||||
<li>
|
||||
유저 {{ info.userCnt }}명 · NPC {{ info.npcCnt }}명 ·
|
||||
<span class="season-status" :title="seasonStatus?.period">{{ seasonStatus?.label }}</span>
|
||||
</li>
|
||||
<li>{{ info.turnTerm }}분 턴 서버</li>
|
||||
</ul>
|
||||
<div v-if="mapData?.history?.length" class="status-history">
|
||||
|
||||
@@ -8,6 +8,7 @@ import MapPreview from '../components/MapPreview.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { createGameTrpc } from '../utils/gameTrpc';
|
||||
import type { GameRouter } from '../utils/gameTrpc';
|
||||
import { resolveServerSeasonStatus } from '../utils/serverSeasonStatus';
|
||||
|
||||
type GatewayRouterOutput = inferRouterOutputs<AppRouter>;
|
||||
type GameRouterOutput = inferRouterOutputs<GameRouter>;
|
||||
@@ -43,6 +44,7 @@ const userIconBaseUrl = import.meta.env.VITE_GATEWAY_USER_ICON_BASE_URL ?? '/gat
|
||||
|
||||
const formatGraceEndsAt = (value: string | null | undefined): string =>
|
||||
value ? new Date(value).toLocaleString('ko-KR') : '';
|
||||
const serverSeasonStatus = (info: LobbyInfo) => resolveServerSeasonStatus(info);
|
||||
const encodeLegacyIconPath = (value: string): string =>
|
||||
value
|
||||
.split('/')
|
||||
@@ -273,15 +275,18 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
|
||||
:style="{ color: profile.color }"
|
||||
class="text-lg font-bold cursor-help"
|
||||
:title="
|
||||
profileDetails[profile.profileName]?.starttime
|
||||
? `시작일: ${profileDetails[profile.profileName]?.starttime}`
|
||||
profileDetails[profile.profileName]
|
||||
? serverSeasonStatus(profileDetails[profile.profileName]!).period
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ profile.korName }}섭
|
||||
</div>
|
||||
<div v-if="profileDetails[profile.profileName]" class="text-xs text-zinc-500 mt-1">
|
||||
<{{ profileDetails[profile.profileName]?.nationCnt }}국 경쟁중>
|
||||
<div
|
||||
v-if="profileDetails[profile.profileName]"
|
||||
class="season-status mt-1 whitespace-nowrap text-xs text-zinc-500"
|
||||
>
|
||||
{{ serverSeasonStatus(profileDetails[profile.profileName]!).label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
|
||||
Reference in New Issue
Block a user