메인 연결 페이지의 탭 종료와 복귀 동작 통일
This commit is contained in:
@@ -209,6 +209,22 @@ const installApi = async (page: Page, state: BoardFixture) => {
|
|||||||
});
|
});
|
||||||
return response({ id: 22 });
|
return response({ id: 22 });
|
||||||
}
|
}
|
||||||
|
if (operation === 'dashboard.getContextBundleDelta') {
|
||||||
|
const snapshot = (data: unknown, revision: string) => ({
|
||||||
|
kind: 'snapshot',
|
||||||
|
revision,
|
||||||
|
sourceRevision: revision,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return response({
|
||||||
|
context: snapshot(generalContext, 'board-context'),
|
||||||
|
commandTable: snapshot({ general: [], nation: [] }, 'board-commands'),
|
||||||
|
boardAccess: snapshot(
|
||||||
|
{ permission: state.permission, canMeeting: state.canMeeting, canSecret: state.canSecret },
|
||||||
|
'board-access'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
if (operation === 'general.me') return response(generalContext);
|
if (operation === 'general.me') return response(generalContext);
|
||||||
if (operation === 'world.getMapLayout') {
|
if (operation === 'world.getMapLayout') {
|
||||||
return response({ mapName: 'che', cityList: [], regionMap: {}, levelMap: {} });
|
return response({ mapName: 'che', cityList: [], regionMap: {}, levelMap: {} });
|
||||||
@@ -498,7 +514,8 @@ test('denies direct secret-room rendering and disables its in-game menu for an o
|
|||||||
expect(state.requests).not.toContain('board.getArticles');
|
expect(state.requests).not.toContain('board.getArticles');
|
||||||
|
|
||||||
state.requests.length = 0;
|
state.requests.length = 0;
|
||||||
await page.goto('');
|
await page.getByRole('button', { name: '돌아가기', exact: true }).click();
|
||||||
|
await expect(page).toHaveURL(new URL(gamePath('/'), page.url()).href);
|
||||||
const nationMenu = page.locator('.main-nation-menu').first();
|
const nationMenu = page.locator('.main-nation-menu').first();
|
||||||
await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', gamePath('/board'));
|
await expect(nationMenu.locator('[data-navigation-id="meeting"]')).toHaveAttribute('href', gamePath('/board'));
|
||||||
await expect(nationMenu.locator('[data-navigation-id="secret-board"]')).toHaveAttribute('aria-disabled', 'true');
|
await expect(nationMenu.locator('[data-navigation-id="secret-board"]')).toHaveAttribute('aria-disabled', 'true');
|
||||||
|
|||||||
@@ -807,7 +807,7 @@ test('nation directory reuses only the public general-directory row on hover and
|
|||||||
expect(await foreignTrigger.getAttribute('aria-expanded')).toBe('true');
|
expect(await foreignTrigger.getAttribute('aria-expanded')).toBe('true');
|
||||||
expect(await foreignTrigger.evaluate((element) => getComputedStyle(element).outlineStyle)).toBe('dashed');
|
expect(await foreignTrigger.evaluate((element) => getComputedStyle(element).outlineStyle)).toBe('dashed');
|
||||||
|
|
||||||
await page.getByRole('button', { name: '창 닫기' }).first().focus();
|
await page.getByRole('button', { name: '돌아가기' }).first().focus();
|
||||||
await expect(preview).toHaveCount(0);
|
await expect(preview).toHaveCount(0);
|
||||||
|
|
||||||
if (artifactRoot) {
|
if (artifactRoot) {
|
||||||
|
|||||||
@@ -273,6 +273,8 @@ test('왕조 일람과 상세는 현재 profile의 이전 서버 기록만 조
|
|||||||
expect(state.dynastyRequests.some((request) => request.includes('legacy'))).toBe(true);
|
expect(state.dynastyRequests.some((request) => request.includes('legacy'))).toBe(true);
|
||||||
expect(state.dynastyRequests.every((request) => !request.includes('sourceProfile'))).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 });
|
await page.screenshot({ path: testInfo.outputPath('dynasty-detail-profile-scope-desktop.png'), fullPage: true });
|
||||||
|
await page.getByRole('button', { name: '돌아가기', exact: true }).last().click();
|
||||||
|
await expect(page).toHaveURL(new URL('./', testInfo.project.use.baseURL as string).href);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 글자를 사용한다', async ({ page }, testInfo) => {
|
test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 글자를 사용한다', async ({ page }, testInfo) => {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { buildEquipmentTradeItemOptions, loadEquipmentTradeItemOrder } from '../
|
|||||||
import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js';
|
import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js';
|
||||||
import { ITEM_KEYS, loadItemModules } from '@sammo-ts/logic/items/index.js';
|
import { ITEM_KEYS, loadItemModules } from '@sammo-ts/logic/items/index.js';
|
||||||
|
|
||||||
|
// 브라우저 보조키로 생성한 탭까지 검증하므로 경량 headless shell 대신 전체 Chromium을 사용한다.
|
||||||
|
test.use({ channel: 'chromium' });
|
||||||
|
|
||||||
const response = (data: unknown) => ({ result: { data } });
|
const response = (data: unknown) => ({ result: { data } });
|
||||||
const runtimeNavigation = JSON.parse(
|
const runtimeNavigation = JSON.parse(
|
||||||
await readFile(new URL('../../../resources/navigation.json', import.meta.url), 'utf8')
|
await readFile(new URL('../../../resources/navigation.json', import.meta.url), 'utf8')
|
||||||
@@ -29,6 +32,7 @@ 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(',');
|
||||||
|
|
||||||
type NavigationFixture = {
|
type NavigationFixture = {
|
||||||
|
pageExitAudit?: boolean;
|
||||||
decoratedNpcChiefs?: boolean;
|
decoratedNpcChiefs?: boolean;
|
||||||
officerLevel: number;
|
officerLevel: number;
|
||||||
permission: number;
|
permission: number;
|
||||||
@@ -567,6 +571,7 @@ const generalContext = (state: NavigationFixture) => ({
|
|||||||
const installFixture = async (page: Page | BrowserContext, state: NavigationFixture) => {
|
const installFixture = async (page: Page | BrowserContext, state: NavigationFixture) => {
|
||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
({ profile }) => {
|
({ profile }) => {
|
||||||
|
if (!window.location.protocol.startsWith('http')) return;
|
||||||
localStorage.setItem('sammo-game-token', 'ga_navigation');
|
localStorage.setItem('sammo-game-token', 'ga_navigation');
|
||||||
localStorage.setItem('sammo-game-profile', profile);
|
localStorage.setItem('sammo-game-profile', profile);
|
||||||
},
|
},
|
||||||
@@ -838,6 +843,12 @@ const installFixture = async (page: Page | BrowserContext, state: NavigationFixt
|
|||||||
winnerId: state.tournamentWinnerId,
|
winnerId: state.tournamentWinnerId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (state.pageExitAudit) return errorResponse(operation, 'Navigation audit fixture: data unavailable');
|
||||||
|
if (operation === 'tournament.getRankings') return response([]);
|
||||||
|
if (operation === 'tournament.getSnapshot') return response({ state: null, participants: [], matches: [] });
|
||||||
|
if (operation === 'tournament.getBettingSummary')
|
||||||
|
return response({ totals: {}, myTotals: {}, totalAmount: 0, myAmount: 0 });
|
||||||
|
if (operation === 'tournament.getAdminStatus') return response({ ok: false });
|
||||||
return response({ ok: true });
|
return response({ ok: true });
|
||||||
});
|
});
|
||||||
operations.forEach((operation, index) => {
|
operations.forEach((operation, index) => {
|
||||||
@@ -6881,3 +6892,246 @@ test('NPC prefixes stay single in the main nation card', async ({ page }, testIn
|
|||||||
await card.screenshot({ path: testInfo.outputPath(`npc-main-prefix-${width}.png`) });
|
await card.screenshot({ path: testInfo.outputPath(`npc-main-prefix-${width}.png`) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('page exit navigation', () => {
|
||||||
|
for (const width of [1000, 500]) {
|
||||||
|
test(`page exit policy distinguishes ordinary, modifier, copied and same-tab links at ${width}`, async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}, testInfo) => {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await installFixture(context, {
|
||||||
|
officerLevel: 12,
|
||||||
|
permission: 4,
|
||||||
|
nationLevel: 1,
|
||||||
|
stage: 0,
|
||||||
|
npcMode: 1,
|
||||||
|
generalMeCalls: 0,
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
await waitForMain(page);
|
||||||
|
const mainUrl = page.url();
|
||||||
|
const survey = page.locator('[data-menu-position="top"] [data-navigation-id="survey"]');
|
||||||
|
const href = await survey.getAttribute('href');
|
||||||
|
for (const method of ['control', 'middle', 'copy'] as const) {
|
||||||
|
const created = context.waitForEvent('page');
|
||||||
|
if (method === 'copy') {
|
||||||
|
const copied = await context.newPage();
|
||||||
|
await copied.goto(href!);
|
||||||
|
} else {
|
||||||
|
await survey.click(method === 'control' ? { modifiers: ['Control'] } : { button: 'middle' });
|
||||||
|
}
|
||||||
|
const tab = await created;
|
||||||
|
await tab.bringToFront();
|
||||||
|
await tab.waitForLoadState('domcontentloaded');
|
||||||
|
await expect(tab).toHaveURL(`${basePath}/survey`);
|
||||||
|
await expect(tab.locator('.back_bar .back_btn')).toHaveText('돌아가기');
|
||||||
|
await tab.reload();
|
||||||
|
await expect(tab.locator('.bottom_bar .back_btn')).toHaveText('돌아가기');
|
||||||
|
expect(context.pages()).toHaveLength(2);
|
||||||
|
await tab.locator('.bottom_bar .back_btn').click();
|
||||||
|
await expect(tab).toHaveURL(mainUrl);
|
||||||
|
expect(context.pages()).toHaveLength(2);
|
||||||
|
await tab.close();
|
||||||
|
}
|
||||||
|
// The status title intentionally uses the current tab, unlike its menu entry.
|
||||||
|
await page.locator('.tournament-status a').click();
|
||||||
|
await expect(page).toHaveURL(`${basePath}/tournament`);
|
||||||
|
expect(context.pages()).toHaveLength(1);
|
||||||
|
await expect(page.getByRole('button', { name: '돌아가기', exact: true }).first()).toBeVisible();
|
||||||
|
await page.screenshot({ path: testInfo.outputPath(`same-tab-${width}.png`), fullPage: true });
|
||||||
|
await page.getByRole('button', { name: '돌아가기', exact: true }).last().click();
|
||||||
|
await expect(page).toHaveURL(mainUrl);
|
||||||
|
expect(context.pages()).toHaveLength(1);
|
||||||
|
|
||||||
|
// Retain the auxiliary identity through SPA navigation and a reload.
|
||||||
|
const opened = page.waitForEvent('popup');
|
||||||
|
await page.locator('[data-navigation-id="tournament"]').click();
|
||||||
|
const tab = await opened;
|
||||||
|
await expect(tab.getByRole('button', { name: '창 닫기', exact: true }).first()).toBeVisible();
|
||||||
|
await tab.getByRole('tab', { name: '베팅장', exact: true }).click();
|
||||||
|
await expect(tab).toHaveURL(`${basePath}/betting`);
|
||||||
|
await tab.reload();
|
||||||
|
await expect(tab.getByRole('button', { name: '창 닫기', exact: true }).last()).toBeVisible();
|
||||||
|
const closed = tab.waitForEvent('close');
|
||||||
|
await tab
|
||||||
|
.getByRole('button', { name: '창 닫기', exact: true })
|
||||||
|
.last()
|
||||||
|
.click()
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (!tab.isClosed()) throw error;
|
||||||
|
});
|
||||||
|
await closed;
|
||||||
|
expect(context.pages()).toHaveLength(1);
|
||||||
|
expect(page.url()).toBe(mainUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitAuditEntries = [
|
||||||
|
'nation-betting',
|
||||||
|
'nation-list',
|
||||||
|
'general-list',
|
||||||
|
'best-general',
|
||||||
|
'yearbook',
|
||||||
|
'battle-simulator',
|
||||||
|
'hall-of-fame',
|
||||||
|
'dynasty',
|
||||||
|
'traffic',
|
||||||
|
'npc-list',
|
||||||
|
'survey',
|
||||||
|
'meeting',
|
||||||
|
'secret-board',
|
||||||
|
'troop',
|
||||||
|
'diplomacy',
|
||||||
|
'personnel',
|
||||||
|
'finance',
|
||||||
|
'chief-center',
|
||||||
|
'npc-control',
|
||||||
|
'nation-secret',
|
||||||
|
'tournament',
|
||||||
|
'tournament-menu',
|
||||||
|
'betting',
|
||||||
|
'nation-info',
|
||||||
|
'nation-cities',
|
||||||
|
'nation-generals',
|
||||||
|
'global-info',
|
||||||
|
'current-city',
|
||||||
|
'battle-center',
|
||||||
|
'inherit',
|
||||||
|
'my-page',
|
||||||
|
'auction-resource',
|
||||||
|
'auction-resource-menu',
|
||||||
|
'auction-unique',
|
||||||
|
'my-settings',
|
||||||
|
];
|
||||||
|
for (const width of [1000, 500]) {
|
||||||
|
test(`all main page exits obey tab policy even when page data fails at ${width}`, async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}, testInfo) => {
|
||||||
|
test.setTimeout(180_000);
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await installFixture(context, {
|
||||||
|
pageExitAudit: true,
|
||||||
|
officerLevel: 12,
|
||||||
|
permission: 4,
|
||||||
|
nationLevel: 1,
|
||||||
|
stage: 0,
|
||||||
|
npcMode: 1,
|
||||||
|
generalMeCalls: 0,
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
await waitForMain(page);
|
||||||
|
const mainUrl = page.url();
|
||||||
|
const evidence: unknown[] = [];
|
||||||
|
for (const id of exitAuditEntries) {
|
||||||
|
let link = page.locator(`[data-navigation-id="${id}"]`).first();
|
||||||
|
if (!(await link.isVisible())) {
|
||||||
|
const group = ['tournament-menu', 'betting'].includes(id)
|
||||||
|
? 'tournament-betting'
|
||||||
|
: ['auction-resource-menu', 'auction-unique'].includes(id)
|
||||||
|
? 'auction'
|
||||||
|
: 'game-info';
|
||||||
|
await page.locator(`[data-menu-id="${group}"]`).first().click();
|
||||||
|
}
|
||||||
|
link = page.locator(`[data-navigation-id="${id}"]`).first();
|
||||||
|
const href = await link.getAttribute('href');
|
||||||
|
const newTab = (await link.getAttribute('target')) === '_blank';
|
||||||
|
const opened = newTab ? page.waitForEvent('popup') : null;
|
||||||
|
await link.click();
|
||||||
|
const target = opened ? await opened : page;
|
||||||
|
await target.setViewportSize({ width, height: 900 });
|
||||||
|
await target.waitForURL(new URL(href!, mainUrl).href);
|
||||||
|
const exit = target.getByRole('button', { name: newTab ? '창 닫기' : '돌아가기', exact: true }).first();
|
||||||
|
await expect(exit, id).toBeVisible();
|
||||||
|
expect(context.pages(), id).toHaveLength(newTab ? 2 : 1);
|
||||||
|
await target.evaluate(() => document.fonts.ready);
|
||||||
|
await exit.focus();
|
||||||
|
await exit.hover();
|
||||||
|
evidence.push({
|
||||||
|
id,
|
||||||
|
href,
|
||||||
|
newTab,
|
||||||
|
width,
|
||||||
|
...(await exit.evaluate((el) => ({
|
||||||
|
viewportWidth: window.innerWidth,
|
||||||
|
rect: el.getBoundingClientRect().toJSON(),
|
||||||
|
font: getComputedStyle(el).font,
|
||||||
|
border: getComputedStyle(el).border,
|
||||||
|
text: el.textContent,
|
||||||
|
}))),
|
||||||
|
});
|
||||||
|
await target.screenshot({ path: testInfo.outputPath(`${id}-${width}.png`), fullPage: true });
|
||||||
|
await writeFile(
|
||||||
|
testInfo.outputPath(`${id}-${width}.html`),
|
||||||
|
await target.locator('#app').evaluate((el) => el.outerHTML)
|
||||||
|
);
|
||||||
|
if (newTab) {
|
||||||
|
const closed = target.waitForEvent('close');
|
||||||
|
await exit.click().catch((error: unknown) => {
|
||||||
|
if (!target.isClosed()) throw error;
|
||||||
|
});
|
||||||
|
await closed;
|
||||||
|
// Opening the original href from the browser does not inherit auxiliary identity.
|
||||||
|
const direct = await context.newPage();
|
||||||
|
await direct.setViewportSize({ width, height: 900 });
|
||||||
|
await direct.goto(href!);
|
||||||
|
const back = direct.getByRole('button', { name: '돌아가기', exact: true }).first();
|
||||||
|
await expect(back, id).toBeVisible();
|
||||||
|
await back.click();
|
||||||
|
await expect(direct).toHaveURL(mainUrl);
|
||||||
|
expect(context.pages()).toHaveLength(2);
|
||||||
|
await direct.close();
|
||||||
|
} else {
|
||||||
|
await exit.click();
|
||||||
|
await expect(page).toHaveURL(mainUrl);
|
||||||
|
}
|
||||||
|
expect(context.pages(), id).toHaveLength(1);
|
||||||
|
expect(page.url(), id).toBe(mainUrl);
|
||||||
|
}
|
||||||
|
await writeFile(testInfo.outputPath(`exit-audit-${width}.json`), JSON.stringify(evidence, null, 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('keyboard activation opens a closable tab and blocked popups use the current tab', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
await installFixture(context, {
|
||||||
|
officerLevel: 0,
|
||||||
|
permission: 0,
|
||||||
|
nationLevel: 0,
|
||||||
|
stage: 0,
|
||||||
|
npcMode: 1,
|
||||||
|
generalMeCalls: 0,
|
||||||
|
operations: [],
|
||||||
|
});
|
||||||
|
await waitForMain(page);
|
||||||
|
const mainUrl = page.url();
|
||||||
|
const link = page.locator('[data-menu-position="top"] [data-navigation-id="survey"]');
|
||||||
|
await link.focus();
|
||||||
|
const opened = page.waitForEvent('popup');
|
||||||
|
await link.press('Enter');
|
||||||
|
const tab = await opened;
|
||||||
|
await expect(tab.getByRole('button', { name: '창 닫기', exact: true }).first()).toBeVisible();
|
||||||
|
expect(await tab.evaluate(() => document.referrer)).toBe('');
|
||||||
|
const closed = tab.waitForEvent('close');
|
||||||
|
await tab
|
||||||
|
.getByRole('button', { name: '창 닫기', exact: true })
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (!tab.isClosed()) throw error;
|
||||||
|
});
|
||||||
|
await closed;
|
||||||
|
expect(context.pages()).toHaveLength(1);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.open = () => null;
|
||||||
|
});
|
||||||
|
await link.click();
|
||||||
|
await expect(page).toHaveURL(`${basePath}/survey`);
|
||||||
|
await page.getByRole('button', { name: '돌아가기', exact: true }).first().click();
|
||||||
|
await expect(page).toHaveURL(mainUrl);
|
||||||
|
expect(context.pages()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ test('join refresh shows the assigned preliminary group immediately with accessi
|
|||||||
|
|
||||||
const refresh = page.getByRole('button', { name: '갱신' });
|
const refresh = page.getByRole('button', { name: '갱신' });
|
||||||
const join = page.getByRole('button', { name: '참가' });
|
const join = page.getByRole('button', { name: '참가' });
|
||||||
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
const close = page.getByRole('button', { name: '돌아가기' }).first();
|
||||||
await expect(join).toBeEnabled();
|
await expect(join).toBeEnabled();
|
||||||
await expect(page.getByText('조별 예선 순위')).toBeVisible();
|
await expect(page.getByText('조별 예선 순위')).toBeVisible();
|
||||||
await expect(page.getByText('조별 본선 순위')).toHaveCount(0);
|
await expect(page.getByText('조별 본선 순위')).toHaveCount(0);
|
||||||
@@ -797,7 +797,7 @@ test('tournament and betting pages expose same-row navigation tabs beside close'
|
|||||||
const navigation = page.getByRole('tablist', { name: '토너먼트와 베팅장 이동' });
|
const navigation = page.getByRole('tablist', { name: '토너먼트와 베팅장 이동' });
|
||||||
const tournamentTab = navigation.getByRole('tab', { name: '토너먼트' });
|
const tournamentTab = navigation.getByRole('tab', { name: '토너먼트' });
|
||||||
const bettingTab = navigation.getByRole('tab', { name: '베팅장' });
|
const bettingTab = navigation.getByRole('tab', { name: '베팅장' });
|
||||||
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
const close = page.getByRole('button', { name: '돌아가기' }).first();
|
||||||
await expect(tournamentTab).toHaveAttribute('aria-selected', 'true');
|
await expect(tournamentTab).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
const headerCenters = await Promise.all(
|
const headerCenters = await Promise.all(
|
||||||
@@ -945,29 +945,7 @@ test('betting realtime refresh is shared across tabs and preserves local interac
|
|||||||
await expect(follower.locator('.mobile-bracket').getByLabel('관우 베팅 금액', { exact: true })).toHaveValue('50');
|
await expect(follower.locator('.mobile-bracket').getByLabel('관우 베팅 금액', { exact: true })).toHaveValue('50');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tournament and betting close only their script-opened popup window', async ({ page }, testInfo) => {
|
// Actual main-link popup closure and tab counts are covered in mainNavigation.spec.ts.
|
||||||
const baseURL = testInfo.project.use.baseURL;
|
|
||||||
expect(typeof baseURL).toBe('string');
|
|
||||||
await page.goto('about:blank');
|
|
||||||
|
|
||||||
for (const route of ['tournament', 'betting'] as const) {
|
|
||||||
const popupPromise = page.waitForEvent('popup');
|
|
||||||
await page.evaluate(() => window.open('about:blank', '_blank', 'noopener'));
|
|
||||||
const popup = await popupPromise;
|
|
||||||
await installFixture(popup, { tournamentStage: route === 'betting' ? 6 : 1 });
|
|
||||||
await popup.goto(new URL(route, baseURL as string).href);
|
|
||||||
|
|
||||||
await expect(popup.getByRole('button', { name: '창 닫기' }).first()).toBeVisible();
|
|
||||||
expect(await popup.evaluate(() => window.opener)).toBeNull();
|
|
||||||
|
|
||||||
const closed = popup.waitForEvent('close');
|
|
||||||
await popup.getByRole('button', { name: '창 닫기' }).first().click();
|
|
||||||
await closed;
|
|
||||||
|
|
||||||
expect(page.isClosed()).toBe(false);
|
|
||||||
expect(page.url()).toBe('about:blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('betting bracket shows intelligence for debate tournament candidates', async ({ page }) => {
|
test('betting bracket shows intelligence for debate tournament candidates', async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../../composables/usePageExit';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
activePage: 'tournament' | 'betting';
|
activePage: 'tournament' | 'betting';
|
||||||
title: string;
|
title: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const closeWindow = (): void => window.close();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -40,9 +42,9 @@ const closeWindow = (): void => window.close();
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeWindow"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
창 닫기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { isAuxiliaryWindow } from '../utils/auxiliaryNavigation';
|
||||||
|
|
||||||
|
export const usePageExit = (): { pageExitLabel: '창 닫기' | '돌아가기'; exitPage: () => void } => {
|
||||||
|
const router = useRouter();
|
||||||
|
const canClose = isAuxiliaryWindow();
|
||||||
|
const pageExitLabel = canClose ? '창 닫기' : '돌아가기';
|
||||||
|
const exitPage = (): void => {
|
||||||
|
if (canClose) {
|
||||||
|
window.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void router.push('/');
|
||||||
|
};
|
||||||
|
return { pageExitLabel, exitPage };
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { installAuxiliaryNavigation } from './utils/auxiliaryNavigation';
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
@@ -7,6 +8,7 @@ import { applyStoredCustomCss } from './utils/customCss';
|
|||||||
import { installImageAssetCssVariables } from './utils/imageAssets';
|
import { installImageAssetCssVariables } from './utils/imageAssets';
|
||||||
import { installScreenModeViewport } from './utils/screenModeViewport';
|
import { installScreenModeViewport } from './utils/screenModeViewport';
|
||||||
|
|
||||||
|
installAuxiliaryNavigation();
|
||||||
installImageAssetCssVariables();
|
installImageAssetCssVariables();
|
||||||
installScreenModeViewport();
|
installScreenModeViewport();
|
||||||
applyStoredCustomCss();
|
applyStoredCustomCss();
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { gameFrontendRuntimeConfig } from '../config/runtimeConfig';
|
||||||
|
|
||||||
|
// 이름은 앱이 직접 연 탭에만 부여한다. URL/sessionStorage 표식은 링크 복사나
|
||||||
|
// 브라우저의 새 탭 열기에도 전달될 수 있으므로 닫기 권한 판정에 사용하지 않는다.
|
||||||
|
const auxiliaryWindowPrefix = 'sammo-auxiliary:';
|
||||||
|
|
||||||
|
export const isAuxiliaryWindow = (): boolean => window.name.startsWith(auxiliaryWindowPrefix);
|
||||||
|
|
||||||
|
export const installAuxiliaryNavigation = (): void => {
|
||||||
|
document.addEventListener(
|
||||||
|
'click',
|
||||||
|
(event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
event.defaultPrevented ||
|
||||||
|
event.button !== 0 ||
|
||||||
|
event.ctrlKey ||
|
||||||
|
event.metaKey ||
|
||||||
|
event.shiftKey ||
|
||||||
|
event.altKey
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const link = event.target instanceof Element ? event.target.closest('a') : null;
|
||||||
|
if (!link || link.target !== '_blank' || link.hasAttribute('download')) return;
|
||||||
|
const url = new URL(link.href, window.location.href);
|
||||||
|
const basePath = gameFrontendRuntimeConfig.appBasePath;
|
||||||
|
if (url.origin !== window.location.origin || !url.pathname.startsWith(basePath)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const child = window.open('about:blank', '_blank');
|
||||||
|
if (!child) {
|
||||||
|
// 팝업 차단 시 현재 탭에서 열어 돌아가기 동작을 제공한다.
|
||||||
|
window.location.assign(url.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
child.name = `${auxiliaryWindowPrefix}${crypto.randomUUID()}`;
|
||||||
|
child.opener = null;
|
||||||
|
// 새 문서 소유의 링크로 이동해야 원본 링크의 referrer 정책도 적용된다.
|
||||||
|
const destination = child.document.createElement('a');
|
||||||
|
destination.href = url.href;
|
||||||
|
destination.rel = link.rel;
|
||||||
|
destination.referrerPolicy = link.referrerPolicy;
|
||||||
|
child.document.body.append(destination);
|
||||||
|
destination.click();
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
@@ -9,6 +11,8 @@ import { formatLog } from '../utils/formatLog';
|
|||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type AuctionOverview = Awaited<ReturnType<typeof trpc.auction.getOverview.query>>;
|
type AuctionOverview = Awaited<ReturnType<typeof trpc.auction.getOverview.query>>;
|
||||||
type ResourceAuction = AuctionOverview['resourceAuctions'][number];
|
type ResourceAuction = AuctionOverview['resourceAuctions'][number];
|
||||||
type UniqueAuction = AuctionOverview['uniqueAuctions'][number];
|
type UniqueAuction = AuctionOverview['uniqueAuctions'][number];
|
||||||
@@ -177,8 +181,6 @@ const bidUniqueAuction = (): Promise<void> =>
|
|||||||
showSuccessToast('입찰이 완료되었습니다.');
|
showSuccessToast('입찰이 완료되었습니다.');
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeWindow = (): void => window.close();
|
|
||||||
|
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, (tab) => {
|
||||||
error.value = null;
|
error.value = null;
|
||||||
if (tab === 'unique' && !selectedUnique.value && ongoingUnique.value[0]) {
|
if (tab === 'unique' && !selectedUnique.value && ongoingUnique.value[0]) {
|
||||||
@@ -201,9 +203,9 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeWindow"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
창 닫기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height reload-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height reload-button"
|
||||||
@@ -499,9 +501,9 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeWindow"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
창 닫기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
@@ -16,6 +18,8 @@ import { trpc } from '../utils/trpc';
|
|||||||
import { getNpcColor } from '../utils/npcColor';
|
import { getNpcColor } from '../utils/npcColor';
|
||||||
import { formatLog } from '../utils/formatLog';
|
import { formatLog } from '../utils/formatLog';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type BattleCenterResponse = Awaited<ReturnType<typeof trpc.nation.getBattleCenter.query>>;
|
type BattleCenterResponse = Awaited<ReturnType<typeof trpc.nation.getBattleCenter.query>>;
|
||||||
type GeneralEntry = BattleCenterResponse['generals'][number];
|
type GeneralEntry = BattleCenterResponse['generals'][number];
|
||||||
|
|
||||||
@@ -237,15 +241,13 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<main class="ref-shell battle-page">
|
<main class="ref-shell battle-page">
|
||||||
<header class="battle-top legacy-bg0">
|
<header class="battle-top legacy-bg0">
|
||||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
<button
|
||||||
<button
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height battle-nav"
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height battle-nav"
|
type="button"
|
||||||
type="button"
|
@click="exitPage"
|
||||||
@click="navigate"
|
>
|
||||||
>
|
{{ pageExitLabel }}
|
||||||
창 닫기
|
</button>
|
||||||
</button>
|
|
||||||
</RouterLink>
|
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height battle-nav"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height battle-nav"
|
||||||
@click="loadBattleCenter"
|
@click="loadBattleCenter"
|
||||||
@@ -318,15 +320,13 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="battle-footer legacy-bg0">
|
<footer class="battle-footer legacy-bg0">
|
||||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
<button
|
||||||
<button
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height battle-nav"
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height battle-nav"
|
type="button"
|
||||||
type="button"
|
@click="exitPage"
|
||||||
@click="navigate"
|
>
|
||||||
>
|
{{ pageExitLabel }}
|
||||||
창 닫기
|
</button>
|
||||||
</button>
|
|
||||||
</RouterLink>
|
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, shallowRef } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, shallowRef } from 'vue';
|
||||||
import type { BattleSimRequestPayload, BattleSimResultPayload } from '@sammo-ts/game-api';
|
import type { BattleSimRequestPayload, BattleSimResultPayload } from '@sammo-ts/game-api';
|
||||||
import PanelCard from '../components/ui/PanelCard.vue';
|
import PanelCard from '../components/ui/PanelCard.vue';
|
||||||
@@ -12,6 +13,8 @@ import type { BattleSimOptions, GeneralDraft, InheritBuff } from '../utils/battl
|
|||||||
import { BattleSimulatorWorkerClient } from '../utils/battleSimulatorWorkerClient';
|
import { BattleSimulatorWorkerClient } from '../utils/battleSimulatorWorkerClient';
|
||||||
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type GeneralExport = Omit<GeneralDraft, 'id'>;
|
type GeneralExport = Omit<GeneralDraft, 'id'>;
|
||||||
|
|
||||||
type BattleExport = {
|
type BattleExport = {
|
||||||
@@ -1015,6 +1018,9 @@ const shouldShowUI = computed(() => !loading.value && !!options.value);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="battle-simulator">
|
<main class="battle-simulator">
|
||||||
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
<!-- Ref renders several equivalent controls as input/button tags. Keep
|
<!-- Ref renders several equivalent controls as input/button tags. Keep
|
||||||
that DOM signature without duplicating the visible Vue controls. -->
|
that DOM signature without duplicating the visible Vue controls. -->
|
||||||
<div class="legacy-control-signature" hidden>
|
<div class="legacy-control-signature" hidden>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import RichTooltip from '../components/ui/RichTooltip.vue';
|
import RichTooltip from '../components/ui/RichTooltip.vue';
|
||||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type RankEntry = {
|
type RankEntry = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -43,7 +46,6 @@ type BestGeneralPayload = {
|
|||||||
uniqueItems: UniqueItemSection[];
|
uniqueItems: UniqueItemSection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const viewMode = ref<'user' | 'npc'>('user');
|
const viewMode = ref<'user' | 'npc'>('user');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
@@ -51,14 +53,6 @@ const data = ref<BestGeneralPayload | null>(null);
|
|||||||
|
|
||||||
const imageUrl = (entry: { picture: string | null; imageServer: number }): string => resolveGeneralIconUrl(entry);
|
const imageUrl = (entry: { picture: string | null; imageServer: number }): string => resolveGeneralIconUrl(entry);
|
||||||
|
|
||||||
const closePage = async (): Promise<void> => {
|
|
||||||
if (window.opener) {
|
|
||||||
window.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = async (): Promise<void> => {
|
const refresh = async (): Promise<void> => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
@@ -84,7 +78,7 @@ watch(viewMode, () => {
|
|||||||
<main id="best-general-container" class="legacy-ranking-page legacy-bg0">
|
<main id="best-general-container" class="legacy-ranking-page legacy-bg0">
|
||||||
<div class="legacy-ranking-title">
|
<div class="legacy-ranking-title">
|
||||||
명 장 일 람<br />
|
명 장 일 람<br />
|
||||||
<button class="legacy-button" type="button" @click="closePage">창 닫기</button>
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="view-selector" role="group" aria-label="장수 유형">
|
<div class="view-selector" role="group" aria-label="장수 유형">
|
||||||
@@ -175,7 +169,7 @@ watch(viewMode, () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="legacy-ranking-bottom">
|
<div class="legacy-ranking-bottom">
|
||||||
<button class="legacy-button" type="button" @click="closePage">창 닫기</button>
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
</div>
|
</div>
|
||||||
<footer class="legacy-banner">
|
<footer class="legacy-banner">
|
||||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD /
|
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD /
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -10,6 +12,8 @@ import { useTournamentPagesStore } from '../stores/tournamentPages';
|
|||||||
import type { TournamentBracketSlot } from '../utils/tournamentBracket';
|
import type { TournamentBracketSlot } from '../utils/tournamentBracket';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
const tournamentPages = useTournamentPagesStore();
|
const tournamentPages = useTournamentPagesStore();
|
||||||
const { snapshot, betting: summary, rankings, loading, error } = storeToRefs(tournamentPages);
|
const { snapshot, betting: summary, rankings, loading, error } = storeToRefs(tournamentPages);
|
||||||
const amounts = ref<Record<number, number | string>>({});
|
const amounts = ref<Record<number, number | string>>({});
|
||||||
@@ -266,11 +270,9 @@ const placeBet = async (target: TournamentBracketSlot) => {
|
|||||||
ㆍ소지금 500원 이하일땐 베팅이 불가능합니다.
|
ㆍ소지금 500원 이하일땐 베팅이 불가능합니다.
|
||||||
</section>
|
</section>
|
||||||
<footer class="betting-footer bg0">
|
<footer class="betting-footer bg0">
|
||||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
<button class="legacy-button legacy-button--navigation close-button" type="button" @click="exitPage">
|
||||||
<button class="legacy-button legacy-button--navigation close-button" type="button" @click="navigate">
|
{{ pageExitLabel }}
|
||||||
창 닫기
|
</button>
|
||||||
</button>
|
|
||||||
</RouterLink>
|
|
||||||
<small>
|
<small>
|
||||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com) / Credit
|
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com) / Credit
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type BoardArticle = Awaited<ReturnType<typeof trpc.board.getArticles.query>>[number];
|
type BoardArticle = Awaited<ReturnType<typeof trpc.board.getArticles.query>>[number];
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
const { error: showErrorToast } = useGameFeedback();
|
const { error: showErrorToast } = useGameFeedback();
|
||||||
const isSecretBoard = computed(() => route.name === 'board-secret');
|
const isSecretBoard = computed(() => route.name === 'board-secret');
|
||||||
const title = computed(() => (isSecretBoard.value ? '기밀실' : '회의실'));
|
const title = computed(() => (isSecretBoard.value ? '기밀실' : '회의실'));
|
||||||
const closeBoard = () => router.push('/');
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const accessChecked = ref(false);
|
const accessChecked = ref(false);
|
||||||
@@ -123,15 +125,20 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="accessChecked && !canAccess" class="legacy-raw-access-error" role="alert">{{ errorMessage }}</div>
|
<div v-if="accessChecked && !canAccess" class="legacy-raw-access-error">
|
||||||
|
<span role="alert">{{ errorMessage }}</span>
|
||||||
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<main v-else id="container" class="legacy-board-page">
|
<main v-else id="container" class="legacy-board-page">
|
||||||
<header class="top-back-bar bg0">
|
<header class="top-back-bar bg0">
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height back-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height back-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeBoard"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<div></div>
|
<div></div>
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
@@ -244,9 +251,9 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height back-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height back-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeBoard"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useMediaQuery } from '@vueuse/core';
|
import { useMediaQuery } from '@vueuse/core';
|
||||||
import { addMinutes } from 'date-fns';
|
import { addMinutes } from 'date-fns';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
||||||
import ChiefTurnCard from '../components/chief/ChiefTurnCard.vue';
|
import ChiefTurnCard from '../components/chief/ChiefTurnCard.vue';
|
||||||
import ChiefCommandEditor from '../components/chief/ChiefCommandEditor.vue';
|
import ChiefCommandEditor from '../components/chief/ChiefCommandEditor.vue';
|
||||||
@@ -13,6 +14,8 @@ import { formatOfficerLevelText } from '../utils/nationFormat';
|
|||||||
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
||||||
import type { CommandMapData, CommandMapLayout, CommandPatternEntry, CommandTable } from '../components/command/types';
|
import type { CommandMapData, CommandMapLayout, CommandPatternEntry, CommandTable } from '../components/command/types';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type ReservationCompletion = (success: boolean) => void;
|
type ReservationCompletion = (success: boolean) => void;
|
||||||
|
|
||||||
type ChiefTurn = {
|
type ChiefTurn = {
|
||||||
@@ -91,7 +94,6 @@ const worldMap = ref<CommandMapData | null>(null);
|
|||||||
const mapLayout = ref<CommandMapLayout | null>(null);
|
const mapLayout = ref<CommandMapLayout | null>(null);
|
||||||
|
|
||||||
const selectedChiefLevel = ref<number | null>(null);
|
const selectedChiefLevel = ref<number | null>(null);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||||
|
|
||||||
@@ -330,9 +332,9 @@ const repeatTurns = async (amount: number) => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height chief-nav"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height chief-nav"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height chief-nav"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height chief-nav"
|
||||||
@@ -465,9 +467,9 @@ const repeatTurns = async (amount: number) => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height chief-nav"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height chief-nav"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
||||||
@@ -9,6 +11,8 @@ import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIc
|
|||||||
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof trpc.world.getCurrentCity.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.world.getCurrentCity.query>>;
|
||||||
type General = Result['generals'][number];
|
type General = Result['generals'][number];
|
||||||
type ReservedCommand = General['turns'][number];
|
type ReservedCommand = General['turns'][number];
|
||||||
@@ -95,9 +99,9 @@ const commandBrief = (command: ReservedCommand): string =>
|
|||||||
도 시 정 보<br /><button
|
도 시 정 보<br /><button
|
||||||
class="legacy-button legacy-button--navigation back-link"
|
class="legacy-button legacy-button--navigation back-link"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -313,9 +317,9 @@ const commandBrief = (command: ReservedCommand): string =>
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation back-link"
|
class="legacy-button legacy-button--navigation back-link"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { EditorContent, useEditor } from '@tiptap/vue-3';
|
import { EditorContent, useEditor } from '@tiptap/vue-3';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
@@ -6,12 +8,13 @@ import Image from '@tiptap/extension-image';
|
|||||||
import Link from '@tiptap/extension-link';
|
import Link from '@tiptap/extension-link';
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import Underline from '@tiptap/extension-underline';
|
import Underline from '@tiptap/extension-underline';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl } from '../utils/generalIcon';
|
||||||
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
||||||
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type DiplomacyResponse = Awaited<ReturnType<typeof trpc.diplomacy.getLetters.query>>;
|
type DiplomacyResponse = Awaited<ReturnType<typeof trpc.diplomacy.getLetters.query>>;
|
||||||
type DiplomacyLetter = DiplomacyResponse['letters'][number];
|
type DiplomacyLetter = DiplomacyResponse['letters'][number];
|
||||||
|
|
||||||
@@ -19,7 +22,6 @@ const loading = ref(false);
|
|||||||
const errorMessage = ref<string | null>(null);
|
const errorMessage = ref<string | null>(null);
|
||||||
const data = ref<DiplomacyResponse | null>(null);
|
const data = ref<DiplomacyResponse | null>(null);
|
||||||
const historyOpen = ref<Record<number, boolean>>({});
|
const historyOpen = ref<Record<number, boolean>>({});
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const editable = computed(() => (data.value?.permission ?? 0) >= 4);
|
const editable = computed(() => (data.value?.permission ?? 0) >= 4);
|
||||||
|
|
||||||
@@ -263,9 +265,9 @@ onBeforeUnmount(() => {
|
|||||||
외 교 부<br /><button
|
외 교 부<br /><button
|
||||||
class="legacy-button legacy-button--navigation"
|
class="legacy-button legacy-button--navigation"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -634,8 +636,8 @@ onBeforeUnmount(() => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<button class="legacy-button legacy-button--navigation" type="button" @click="router.push('/')">
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
돌아가기</button
|
{{ pageExitLabel }}</button
|
||||||
><br /><br />
|
><br /><br />
|
||||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com)
|
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com)
|
||||||
/
|
/
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { formatLog } from '../utils/formatLog';
|
import { formatLog } from '../utils/formatLog';
|
||||||
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type DynastyDetailPayload = Awaited<ReturnType<typeof trpc.dynasty.getDetail.query>>;
|
type DynastyDetailPayload = Awaited<ReturnType<typeof trpc.dynasty.getDetail.query>>;
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const data = ref<DynastyDetailPayload | null>(null);
|
const data = ref<DynastyDetailPayload | null>(null);
|
||||||
@@ -23,14 +26,6 @@ const emperorId = computed(() => {
|
|||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const closePage = async (): Promise<void> => {
|
|
||||||
if (window.opener) {
|
|
||||||
window.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadDetail = async (): Promise<void> => {
|
const loadDetail = async (): Promise<void> => {
|
||||||
if (emperorId.value === null) {
|
if (emperorId.value === null) {
|
||||||
data.value = null;
|
data.value = null;
|
||||||
@@ -62,7 +57,7 @@ onMounted(loadDetail);
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
역 대 왕 조<br />
|
역 대 왕 조<br />
|
||||||
<button class="native-button" type="button" @click="closePage">창 닫기</button>
|
<button class="native-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
<span class="all-link">
|
<span class="all-link">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ path: '/dynasty', query: source === 'legacy' ? { source: 'legacy' } : {} }"
|
:to="{ path: '/dynasty', query: source === 'legacy' ? { source: 'legacy' } : {} }"
|
||||||
@@ -290,7 +285,9 @@ onMounted(loadDetail);
|
|||||||
<table class="legacy-table legacy-bg0 footer-table">
|
<table class="legacy-table legacy-bg0 footer-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button class="native-button" type="button" @click="closePage">창 닫기</button><br /></td>
|
<td>
|
||||||
|
<button class="native-button" type="button" @click="exitPage">{{ pageExitLabel }}</button><br />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="banner">삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD</td>
|
<td class="banner">삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD</td>
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type DynastyListPayload = Awaited<ReturnType<typeof trpc.dynasty.getList.query>>;
|
type DynastyListPayload = Awaited<ReturnType<typeof trpc.dynasty.getList.query>>;
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const data = ref<DynastyListPayload | null>(null);
|
const data = ref<DynastyListPayload | null>(null);
|
||||||
const selectedSource = ref<'current' | 'legacy'>(route.query.source === 'legacy' ? 'legacy' : 'current');
|
const selectedSource = ref<'current' | 'legacy'>(route.query.source === 'legacy' ? 'legacy' : 'current');
|
||||||
|
|
||||||
const closePage = async (): Promise<void> => {
|
|
||||||
if (window.opener) {
|
|
||||||
window.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadDynasty = async (): Promise<void> => {
|
const loadDynasty = async (): Promise<void> => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
@@ -45,7 +40,7 @@ watch(selectedSource, loadDynasty);
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
역 대 왕 조<br />
|
역 대 왕 조<br />
|
||||||
<button class="native-button" type="button" @click="closePage">창 닫기</button><br />
|
<button class="native-button" type="button" @click="exitPage">{{ pageExitLabel }}</button><br />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -162,7 +157,9 @@ watch(selectedSource, loadDynasty);
|
|||||||
<table class="legacy-table legacy-bg0 footer-table spaced-table">
|
<table class="legacy-table legacy-bg0 footer-table spaced-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button class="native-button" type="button" @click="closePage">창 닫기</button><br /></td>
|
<td>
|
||||||
|
<button class="native-button" type="button" @click="exitPage">{{ pageExitLabel }}</button><br />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="banner">삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD</td>
|
<td class="banner">삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD</td>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import GeneralDirectoryTable from '../components/directory/GeneralDirectoryTable.vue';
|
import GeneralDirectoryTable from '../components/directory/GeneralDirectoryTable.vue';
|
||||||
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
||||||
@@ -14,6 +15,8 @@ import {
|
|||||||
} from '../utils/generalDirectorySort';
|
} from '../utils/generalDirectorySort';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
const sortOptions: Array<{ value: GeneralDirectorySortKey; label: string }> = [
|
const sortOptions: Array<{ value: GeneralDirectorySortKey; label: string }> = [
|
||||||
{ value: 0, label: '이름' },
|
{ value: 0, label: '이름' },
|
||||||
{ value: 1, label: '국가' },
|
{ value: 1, label: '국가' },
|
||||||
@@ -38,7 +41,6 @@ const sourceGenerals = ref<GeneralDirectoryGeneral[]>([]);
|
|||||||
const sortCriteria = ref<GeneralDirectorySortCriterion[]>([]);
|
const sortCriteria = ref<GeneralDirectorySortCriterion[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const router = useRouter();
|
|
||||||
const { info: showInfoToast } = useGameFeedback();
|
const { info: showInfoToast } = useGameFeedback();
|
||||||
|
|
||||||
const generals = computed(() => sortGeneralDirectory(sourceGenerals.value, sortCriteria.value));
|
const generals = computed(() => sortGeneralDirectory(sourceGenerals.value, sortCriteria.value));
|
||||||
@@ -81,7 +83,7 @@ onMounted(() => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
장 수 일 람<br />
|
장 수 일 람<br />
|
||||||
<button class="legacy-button" type="button" @click="router.push('/')">창 닫기</button>
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
<button
|
<button
|
||||||
class="legacy-button"
|
class="legacy-button"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -118,7 +120,9 @@ onMounted(() => {
|
|||||||
<table class="directory-table title-table legacy-bg0">
|
<table class="directory-table title-table legacy-bg0">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button class="legacy-button" type="button" @click="router.push('/')">창 닫기</button></td>
|
<td>
|
||||||
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><small>삼국지 모의전투 HiDCHe</small></td>
|
<td><small>삼국지 모의전투 HiDCHe</small></td>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import MapViewer from '../components/main/MapViewer.vue';
|
import MapViewer from '../components/main/MapViewer.vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof trpc.world.getGlobalInfo.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.world.getGlobalInfo.query>>;
|
||||||
type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
|
type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
|
||||||
const data = ref<Result | null>(null);
|
const data = ref<Result | null>(null);
|
||||||
@@ -12,8 +15,6 @@ const layout = ref<Layout | null>(null);
|
|||||||
const error = ref('');
|
const error = ref('');
|
||||||
const matrixElement = ref<HTMLTableElement | null>(null);
|
const matrixElement = ref<HTMLTableElement | null>(null);
|
||||||
const matrixHeight = ref<number | null>(null);
|
const matrixHeight = ref<number | null>(null);
|
||||||
const router = useRouter();
|
|
||||||
const goBack = () => router.push('/');
|
|
||||||
const state = (value: number) => ({ 0: '★', 1: '▲', 2: '', 7: '@' })[value] ?? 'ㆍ';
|
const state = (value: number) => ({ 0: '★', 1: '▲', 2: '', 7: '@' })[value] ?? 'ㆍ';
|
||||||
const stateClass = (value: number) => `state-${value}`;
|
const stateClass = (value: number) => `state-${value}`;
|
||||||
const nationMap = computed(() => new Map(data.value?.nations.map((nation) => [nation.id, nation]) ?? []));
|
const nationMap = computed(() => new Map(data.value?.nations.map((nation) => [nation.id, nation]) ?? []));
|
||||||
@@ -56,9 +57,9 @@ onMounted(async () => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height"
|
||||||
type="button"
|
type="button"
|
||||||
@click="goBack"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기</button
|
{{ pageExitLabel }}</button
|
||||||
><strong>중원 정보</strong>
|
><strong>중원 정보</strong>
|
||||||
</header>
|
</header>
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
@@ -157,9 +158,9 @@ onMounted(async () => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height"
|
||||||
type="button"
|
type="button"
|
||||||
@click="goBack"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
<button class="legacy-compat-button" type="button" tabindex="-1" aria-hidden="true" />
|
<button class="legacy-compat-button" type="button" tabindex="-1" aria-hidden="true" />
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type HallOption = {
|
type HallOption = {
|
||||||
sourceProfile: string;
|
sourceProfile: string;
|
||||||
season: number;
|
season: number;
|
||||||
@@ -39,7 +42,6 @@ type HallPayload = {
|
|||||||
sections: HallSection[];
|
sections: HallSection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const options = ref<HallOption[]>([]);
|
const options = ref<HallOption[]>([]);
|
||||||
@@ -64,14 +66,6 @@ const selection = computed({
|
|||||||
|
|
||||||
const imageUrl = (entry: HallEntry): string => resolveGeneralIconUrl(entry);
|
const imageUrl = (entry: HallEntry): string => resolveGeneralIconUrl(entry);
|
||||||
|
|
||||||
const closePage = async (): Promise<void> => {
|
|
||||||
if (window.opener) {
|
|
||||||
window.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadOptions = async (): Promise<void> => {
|
const loadOptions = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
options.value = await trpc.ranking.getHallOfFameOptions.query({ source: selectedSource.value });
|
options.value = await trpc.ranking.getHallOfFameOptions.query({ source: selectedSource.value });
|
||||||
@@ -121,7 +115,7 @@ onMounted(loadOptions);
|
|||||||
<main id="container" class="legacy-hall-page legacy-bg0">
|
<main id="container" class="legacy-hall-page legacy-bg0">
|
||||||
<div class="legacy-hall-title">
|
<div class="legacy-hall-title">
|
||||||
명 예 의 전 당<br />
|
명 예 의 전 당<br />
|
||||||
<button class="legacy-button" type="button" @click="closePage">창 닫기</button>
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="archive-source">
|
<label class="archive-source">
|
||||||
@@ -189,7 +183,7 @@ onMounted(loadOptions);
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="legacy-hall-bottom">
|
<div class="legacy-hall-bottom">
|
||||||
<button class="legacy-button" type="button" @click="closePage">창 닫기</button>
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
</div>
|
</div>
|
||||||
<footer class="legacy-banner">
|
<footer class="legacy-banner">
|
||||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD /
|
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD /
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type InheritStatus = Awaited<ReturnType<typeof trpc.inherit.getStatus.query>>;
|
type InheritStatus = Awaited<ReturnType<typeof trpc.inherit.getStatus.query>>;
|
||||||
type InheritLog = Awaited<ReturnType<typeof trpc.inherit.getLogs.query>>[number];
|
type InheritLog = Awaited<ReturnType<typeof trpc.inherit.getLogs.query>>[number];
|
||||||
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
||||||
@@ -452,9 +456,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="top-back-bar legacy-bg0">
|
<header class="top-back-bar legacy-bg0">
|
||||||
<RouterLink class="top-button legacy-button legacy-button--navigation legacy-button--fixed-height" to="/"
|
<button
|
||||||
>돌아가기</RouterLink
|
class="top-button legacy-button legacy-button--navigation legacy-button--fixed-height"
|
||||||
|
type="button"
|
||||||
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
<strong>유산 관리</strong>
|
<strong>유산 관리</strong>
|
||||||
<button
|
<button
|
||||||
class="top-button legacy-button legacy-button--navigation legacy-button--fixed-height"
|
class="top-button legacy-button legacy-button--navigation legacy-button--fixed-height"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
@@ -11,6 +13,8 @@ import GeneralInformationPanel from '../components/main/GeneralInformationPanel.
|
|||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
import { gameFrontendRuntimeConfig } from '../config/runtimeConfig';
|
import { gameFrontendRuntimeConfig } from '../config/runtimeConfig';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
const PENDING_DIE_ON_PRESTART_KEY = 'sam.pending.dieOnPrestart';
|
const PENDING_DIE_ON_PRESTART_KEY = 'sam.pending.dieOnPrestart';
|
||||||
const { error: showErrorToast, showDialog } = useGameFeedback();
|
const { error: showErrorToast, showDialog } = useGameFeedback();
|
||||||
type LogType = 'generalHistory' | 'battleDetail' | 'battleResult' | 'generalAction';
|
type LogType = 'generalHistory' | 'battleDetail' | 'battleResult' | 'generalAction';
|
||||||
@@ -340,7 +344,9 @@ onMounted(() => {
|
|||||||
<span>내 정 보</span>
|
<span>내 정 보</span>
|
||||||
<div class="title-actions">
|
<div class="title-actions">
|
||||||
<div class="navigation-actions">
|
<div class="navigation-actions">
|
||||||
<RouterLink class="legacy-button legacy-button--navigation" to="/">돌아가기</RouterLink>
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
<button class="legacy-button legacy-button--navigation" type="button" @click="() => loadPage()">
|
<button class="legacy-button legacy-button--navigation" type="button" @click="() => loadPage()">
|
||||||
새로고침
|
새로고침
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { mode: clockDisplayMode } = useClockDisplay();
|
const { mode: clockDisplayMode } = useClockDisplay();
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
@@ -23,6 +25,8 @@ import {
|
|||||||
type ScreenMode,
|
type ScreenMode,
|
||||||
} from '../utils/screenModeViewport';
|
} from '../utils/screenModeViewport';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
const { success: showSuccessToast, error: showErrorToast } = useGameFeedback();
|
const { success: showSuccessToast, error: showErrorToast } = useGameFeedback();
|
||||||
type MyGeneralResponse = Awaited<ReturnType<typeof trpc.general.me.query>>;
|
type MyGeneralResponse = Awaited<ReturnType<typeof trpc.general.me.query>>;
|
||||||
const data = ref<MyGeneralResponse | null>(null);
|
const data = ref<MyGeneralResponse | null>(null);
|
||||||
@@ -134,7 +138,9 @@ onBeforeUnmount(() => {
|
|||||||
<main id="interface-settings" class="legacy-page bg0 interface-settings-page">
|
<main id="interface-settings" class="legacy-page bg0 interface-settings-page">
|
||||||
<div class="title-row">
|
<div class="title-row">
|
||||||
<span>환 경 설 정</span>
|
<span>환 경 설 정</span>
|
||||||
<RouterLink class="legacy-button legacy-button--navigation" to="/">돌아가기</RouterLink>
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="settings-grid">
|
<section class="settings-grid">
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type BettingListItem = {
|
type BettingListItem = {
|
||||||
id: number;
|
id: number;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -333,15 +337,13 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<main id="nation-betting-container" class="nation-betting-page legacy-bg0">
|
<main id="nation-betting-container" class="nation-betting-page legacy-bg0">
|
||||||
<header class="legacy-top-bar">
|
<header class="legacy-top-bar">
|
||||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
<button
|
||||||
<button
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height"
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height"
|
type="button"
|
||||||
type="button"
|
@click="exitPage"
|
||||||
@click="navigate"
|
>
|
||||||
>
|
{{ pageExitLabel }}
|
||||||
돌아가기
|
</button>
|
||||||
</button>
|
|
||||||
</RouterLink>
|
|
||||||
<div></div>
|
<div></div>
|
||||||
<h1>국가 베팅장</h1>
|
<h1>국가 베팅장</h1>
|
||||||
<div></div>
|
<div></div>
|
||||||
@@ -441,7 +443,9 @@ onMounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="betting-footer">
|
<footer class="betting-footer">
|
||||||
<RouterLink class="legacy-button legacy-button--navigation" to="/">돌아가기</RouterLink>
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
||||||
import type { CommandTable } from '../components/command/types';
|
import type { CommandTable } from '../components/command/types';
|
||||||
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
||||||
@@ -14,6 +15,8 @@ import { legacyNationTextColor } from '../utils/legacyNationColor';
|
|||||||
import { cityLevelMap, regionMap } from '../utils/nationFormat';
|
import { cityLevelMap, regionMap } from '../utils/nationFormat';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof trpc.nation.getCityOverview.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.nation.getCityOverview.query>>;
|
||||||
type SecretResult = Awaited<ReturnType<typeof trpc.nation.getSecretGeneralList.query>>;
|
type SecretResult = Awaited<ReturnType<typeof trpc.nation.getSecretGeneralList.query>>;
|
||||||
type PersonnelResult = Awaited<ReturnType<typeof trpc.nation.getPersonnelInfo.query>>;
|
type PersonnelResult = Awaited<ReturnType<typeof trpc.nation.getPersonnelInfo.query>>;
|
||||||
@@ -45,7 +48,6 @@ const extraSort = ref<
|
|||||||
| 'generalCount'
|
| 'generalCount'
|
||||||
| null
|
| null
|
||||||
>(null);
|
>(null);
|
||||||
const router = useRouter();
|
|
||||||
const { error: showErrorToast, info: showInfoToast, success: showSuccessToast } = useGameFeedback();
|
const { error: showErrorToast, info: showInfoToast, success: showSuccessToast } = useGameFeedback();
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
'기본',
|
'기본',
|
||||||
@@ -303,9 +305,9 @@ onMounted(async () => {
|
|||||||
세 력 도 시<br /><button
|
세 력 도 시<br /><button
|
||||||
class="legacy-button legacy-button--navigation back-button"
|
class="legacy-button legacy-button--navigation back-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -716,9 +718,9 @@ onMounted(async () => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation back-button"
|
class="legacy-button legacy-button--navigation back-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
|
||||||
import type { CommandTable } from '../components/command/types';
|
import type { CommandTable } from '../components/command/types';
|
||||||
import { formatOfficerLevelText } from '../utils/nationFormat';
|
import { formatOfficerLevelText } from '../utils/nationFormat';
|
||||||
@@ -33,6 +34,8 @@ import {
|
|||||||
} from '../utils/nationGeneralGrid';
|
} from '../utils/nationGeneralGrid';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof trpc.nation.getGeneralList.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.nation.getGeneralList.query>>;
|
||||||
type General = Result['generals'][number];
|
type General = Result['generals'][number];
|
||||||
type CellValue = string | number | null;
|
type CellValue = string | number | null;
|
||||||
@@ -218,7 +221,6 @@ const layout: LayoutItem[] = [
|
|||||||
const columnById = new Map(columns.map((column) => [column.id, column]));
|
const columnById = new Map(columns.map((column) => [column.id, column]));
|
||||||
const data = ref<Result | null>(null);
|
const data = ref<Result | null>(null);
|
||||||
const commandTable = ref<CommandTable | null>(null);
|
const commandTable = ref<CommandTable | null>(null);
|
||||||
const router = useRouter();
|
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const viewMenuOpen = ref(false);
|
const viewMenuOpen = ref(false);
|
||||||
@@ -709,9 +711,9 @@ onBeforeUnmount(() => {
|
|||||||
<span class="left-actions">
|
<span class="left-actions">
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height top-button nation-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height top-button nation-button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height top-button nation-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height top-button nation-button"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { formatLog } from '../utils/formatLog';
|
import { formatLog } from '../utils/formatLog';
|
||||||
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
import { formatNationLevelText } from '../utils/nationFormat';
|
import { formatNationLevelText } from '../utils/nationFormat';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type Result = Awaited<ReturnType<typeof trpc.nation.getNationInfo.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.nation.getNationInfo.query>>;
|
||||||
const data = ref<Result | null>(null);
|
const data = ref<Result | null>(null);
|
||||||
const router = useRouter();
|
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const number = (value: number) => value.toLocaleString('ko-KR');
|
const number = (value: number) => value.toLocaleString('ko-KR');
|
||||||
const diff = (value: number) => `${value > 0 ? '+' : ''}${number(value)}`;
|
const diff = (value: number) => `${value > 0 ? '+' : ''}${number(value)}`;
|
||||||
@@ -31,9 +33,9 @@ onMounted(async () => {
|
|||||||
세 력 정 보<br /><button
|
세 력 정 보<br /><button
|
||||||
class="legacy-button legacy-button--navigation"
|
class="legacy-button legacy-button--navigation"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -119,8 +121,8 @@ onMounted(async () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<button class="legacy-button legacy-button--navigation" type="button" @click="router.push('/')">
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import GeneralDirectoryTable from '../components/directory/GeneralDirectoryTable.vue';
|
import GeneralDirectoryTable from '../components/directory/GeneralDirectoryTable.vue';
|
||||||
@@ -9,6 +11,8 @@ import { getNpcColor } from '../utils/npcColor';
|
|||||||
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type Directory = Awaited<ReturnType<typeof trpc.world.getNationDirectory.query>>;
|
type Directory = Awaited<ReturnType<typeof trpc.world.getNationDirectory.query>>;
|
||||||
type Nation = Directory[number];
|
type Nation = Directory[number];
|
||||||
|
|
||||||
@@ -58,7 +62,6 @@ const displayAmbassadorName = (nation: Nation, name: string) => {
|
|||||||
return general ? displayGeneralName(general) : name;
|
return general ? displayGeneralName(general) : name;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeWindow = () => window.close();
|
|
||||||
const officerLevelAt = (row: number, column: number, columns: number) => 13 - ((row - 1) * columns + column);
|
const officerLevelAt = (row: number, column: number, columns: number) => 13 - ((row - 1) * columns + column);
|
||||||
|
|
||||||
const positionGeneralPreview = async (): Promise<void> => {
|
const positionGeneralPreview = async (): Promise<void> => {
|
||||||
@@ -165,8 +168,8 @@ onBeforeUnmount(() => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
세 력 일 람<br /><button class="legacy-button" type="button" @click="closeWindow">
|
세 력 일 람<br /><button class="legacy-button" type="button" @click="exitPage">
|
||||||
창 닫기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -400,7 +403,9 @@ onBeforeUnmount(() => {
|
|||||||
<table class="directory-table title-table footer-table legacy-bg0">
|
<table class="directory-table title-table footer-table legacy-bg0">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button class="legacy-button" type="button" @click="closeWindow">창 닫기</button></td>
|
<td>
|
||||||
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ import { trpc } from '../utils/trpc';
|
|||||||
import { cityLevelMap, formatOfficerLevelText, getNationChiefLevel, regionMap } from '../utils/nationFormat';
|
import { cityLevelMap, formatOfficerLevelText, getNationChiefLevel, regionMap } from '../utils/nationFormat';
|
||||||
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
import { legacyNationTextColor } from '../utils/legacyNationColor';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type PersonnelResponse = Awaited<ReturnType<typeof trpc.nation.getPersonnelInfo.query>>;
|
type PersonnelResponse = Awaited<ReturnType<typeof trpc.nation.getPersonnelInfo.query>>;
|
||||||
type GeneralEntry = PersonnelResponse['generals'][number];
|
type GeneralEntry = PersonnelResponse['generals'][number];
|
||||||
type OfficerLevel = 2 | 3 | 4;
|
type OfficerLevel = 2 | 3 | 4;
|
||||||
@@ -41,7 +44,6 @@ const selectionContext = ref<SelectionContext | null>(null);
|
|||||||
const kickTargetId = ref(0);
|
const kickTargetId = ref(0);
|
||||||
const ambassadorSelection = ref<number[]>([]);
|
const ambassadorSelection = ref<number[]>([]);
|
||||||
const auditorSelection = ref<number[]>([]);
|
const auditorSelection = ref<number[]>([]);
|
||||||
const router = useRouter();
|
|
||||||
const { success: showSuccessToast, error: showErrorToast, confirm: showConfirm } = useGameFeedback();
|
const { success: showSuccessToast, error: showErrorToast, confirm: showConfirm } = useGameFeedback();
|
||||||
|
|
||||||
const resolveErrorMessage = (value: unknown): string =>
|
const resolveErrorMessage = (value: unknown): string =>
|
||||||
@@ -311,9 +313,9 @@ onMounted(() => void loadPersonnel());
|
|||||||
인 사 부<br /><button
|
인 사 부<br /><button
|
||||||
class="legacy-button legacy-button--navigation"
|
class="legacy-button legacy-button--navigation"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -569,12 +571,8 @@ onMounted(() => void loadPersonnel());
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
class="legacy-button legacy-button--navigation"
|
{{ pageExitLabel }}
|
||||||
type="button"
|
|
||||||
@click="router.push('/')"
|
|
||||||
>
|
|
||||||
돌아가기
|
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
@@ -9,6 +11,8 @@ import DirectoryTooltip from '../components/directory/DirectoryTooltip.vue';
|
|||||||
import { getNpcColor } from '../utils/npcColor';
|
import { getNpcColor } from '../utils/npcColor';
|
||||||
import { generalInjuryPresentation } from '../utils/generalInjury';
|
import { generalInjuryPresentation } from '../utils/generalInjury';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
type Result = Awaited<ReturnType<typeof trpc.nation.getSecretGeneralList.query>>;
|
type Result = Awaited<ReturnType<typeof trpc.nation.getSecretGeneralList.query>>;
|
||||||
type ReservedCommand = Result['generals'][number]['reservedCommands'][number];
|
type ReservedCommand = Result['generals'][number]['reservedCommands'][number];
|
||||||
type Sort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
type Sort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||||
@@ -48,7 +52,6 @@ const generals = computed(() =>
|
|||||||
return b.troopId - a.troopId || a.id - b.id;
|
return b.troopId - a.troopId || a.id - b.id;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const closeWindow = () => window.close();
|
|
||||||
const displayName = (general: { name: string; npcState: number }) =>
|
const displayName = (general: { name: string; npcState: number }) =>
|
||||||
general.npcState > 0 && !/^[ⓜⓝⓖ㉥ⓤⓞⓧ]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
general.npcState > 0 && !/^[ⓜⓝⓖ㉥ⓤⓞⓧ]/u.test(general.name) ? `ⓝ${general.name}` : general.name;
|
||||||
const injuryInfo = (injury: number) => generalInjuryPresentation(injury);
|
const injuryInfo = (injury: number) => generalInjuryPresentation(injury);
|
||||||
@@ -86,7 +89,9 @@ onMounted(load);
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
암 행 부<br /><button class="close-button" type="button" @click="closeWindow">창 닫기</button>
|
암 행 부<br /><button class="close-button" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -343,7 +348,9 @@ onMounted(load);
|
|||||||
<table class="layout legacy-bg0 footer">
|
<table class="layout legacy-bg0 footer">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button class="close-button" type="button" @click="closeWindow">창 닫기</button></td>
|
<td>
|
||||||
|
<button class="close-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="legacy-banner">
|
<td class="legacy-banner">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
@@ -7,6 +9,8 @@ import { legacyNationTextColor } from '../utils/legacyNationColor';
|
|||||||
import LegacyHtmlEditor from '../components/ui/LegacyHtmlEditor.vue';
|
import LegacyHtmlEditor from '../components/ui/LegacyHtmlEditor.vue';
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type StratFinanResponse = Awaited<ReturnType<typeof trpc.nation.getStratFinan.query>>;
|
type StratFinanResponse = Awaited<ReturnType<typeof trpc.nation.getStratFinan.query>>;
|
||||||
type NationEntry = StratFinanResponse['nationsList'][number];
|
type NationEntry = StratFinanResponse['nationsList'][number];
|
||||||
|
|
||||||
@@ -172,9 +176,13 @@ onMounted(() => void loadStratFinan());
|
|||||||
<template>
|
<template>
|
||||||
<main id="finance-container" class="page-finance">
|
<main id="finance-container" class="page-finance">
|
||||||
<nav class="top-back-bar">
|
<nav class="top-back-bar">
|
||||||
<RouterLink class="legacy-button legacy-button--navigation legacy-button--fixed-height" to="/"
|
<button
|
||||||
>돌아가기</RouterLink
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height"
|
||||||
|
type="button"
|
||||||
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
<span />
|
<span />
|
||||||
<strong>내무부</strong>
|
<strong>내무부</strong>
|
||||||
<span />
|
<span />
|
||||||
@@ -451,7 +459,9 @@ onMounted(() => void loadStratFinan());
|
|||||||
<input v-for="index in 4" :key="`compat-input-${index}`" type="hidden" />
|
<input v-for="index in 4" :key="`compat-input-${index}`" type="hidden" />
|
||||||
</div>
|
</div>
|
||||||
<footer class="bottom-bar">
|
<footer class="bottom-bar">
|
||||||
<RouterLink class="legacy-button legacy-button--navigation" to="/">돌아가기</RouterLink>
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import SortableStringList from '../components/ui/SortableStringList';
|
import SortableStringList from '../components/ui/SortableStringList';
|
||||||
@@ -6,6 +8,8 @@ import { useGameFeedback } from '../composables/useGameFeedback';
|
|||||||
import { npcPriorityHelp } from '../utils/npcPriorityHelp';
|
import { npcPriorityHelp } from '../utils/npcPriorityHelp';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type NpcPolicyResponse = Awaited<ReturnType<typeof trpc.npc.getPolicy.query>>;
|
type NpcPolicyResponse = Awaited<ReturnType<typeof trpc.npc.getPolicy.query>>;
|
||||||
type NationPolicy = NpcPolicyResponse['currentNationPolicy'];
|
type NationPolicy = NpcPolicyResponse['currentNationPolicy'];
|
||||||
type NumericPolicyKey = Exclude<keyof NationPolicy, 'CombatForce' | 'SupportForce' | 'DevelopForce'>;
|
type NumericPolicyKey = Exclude<keyof NationPolicy, 'CombatForce' | 'SupportForce' | 'DevelopForce'>;
|
||||||
@@ -363,9 +367,13 @@ const submitPriority = async (section: PrioritySectionKey) => {
|
|||||||
<template>
|
<template>
|
||||||
<main id="npc-policy-page" class="npc-page">
|
<main id="npc-policy-page" class="npc-page">
|
||||||
<nav class="top-back-bar legacy-bg0">
|
<nav class="top-back-bar legacy-bg0">
|
||||||
<RouterLink class="legacy-button legacy-button--navigation legacy-button--fixed-height back-button" to="/"
|
<button
|
||||||
>돌아가기</RouterLink
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height back-button"
|
||||||
|
type="button"
|
||||||
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
<strong>NPC 정책</strong>
|
<strong>NPC 정책</strong>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
import LegacySortControls from '../components/ui/LegacySortControls.vue';
|
||||||
import { getNpcColor } from '../utils/npcColor';
|
import { getNpcColor } from '../utils/npcColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type NpcList = Awaited<ReturnType<typeof trpc.public.getNpcList.query>>;
|
type NpcList = Awaited<ReturnType<typeof trpc.public.getNpcList.query>>;
|
||||||
type NpcListSort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
type NpcListSort = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||||
|
|
||||||
@@ -40,7 +44,6 @@ const load = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeWindow = () => window.close();
|
|
||||||
const updateSort = (value: number): void => {
|
const updateSort = (value: number): void => {
|
||||||
sort.value = value as NpcListSort;
|
sort.value = value as NpcListSort;
|
||||||
};
|
};
|
||||||
@@ -63,7 +66,7 @@ onMounted(() => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
빙 의 일 람<br />
|
빙 의 일 람<br />
|
||||||
<button class="legacy-close" type="button" @click="closeWindow">창닫기</button>
|
<button class="legacy-close" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -226,7 +229,9 @@ onMounted(() => {
|
|||||||
<table class="legacy-table footer-table legacy-bg0">
|
<table class="legacy-table footer-table legacy-bg0">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button class="legacy-close" type="button" @click="closeWindow">창닫기</button></td>
|
<td>
|
||||||
|
<button class="legacy-close" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="banner">
|
<td class="banner">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +22,8 @@ import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
|||||||
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type Archive = Awaited<ReturnType<typeof trpc.archive.myPastPlays.query>>;
|
type Archive = Awaited<ReturnType<typeof trpc.archive.myPastPlays.query>>;
|
||||||
type ArchiveSeason = Archive['seasons'][number] & {
|
type ArchiveSeason = Archive['seasons'][number] & {
|
||||||
sourceProfile?: string;
|
sourceProfile?: string;
|
||||||
@@ -233,7 +237,7 @@ onMounted(() => {
|
|||||||
<header class="title-row legacy-bg1">
|
<header class="title-row legacy-bg1">
|
||||||
<h1>내 지난 플레이 보기</h1>
|
<h1>내 지난 플레이 보기</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink>
|
<button class="legacy-button" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
<button class="legacy-button" type="button" :disabled="loading" @click="loadArchive">새로고침</button>
|
<button class="legacy-button" type="button" :disabled="loading" @click="loadArchive">새로고침</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
@@ -10,6 +11,8 @@ import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIc
|
|||||||
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
||||||
type Reservation = Awaited<ReturnType<typeof trpc.join.getSelectionPool.mutate>>;
|
type Reservation = Awaited<ReturnType<typeof trpc.join.getSelectionPool.mutate>>;
|
||||||
type Candidate = Reservation['candidates'][number];
|
type Candidate = Reservation['candidates'][number];
|
||||||
@@ -235,6 +238,10 @@ async function loadPage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const goBack = (): void => {
|
const goBack = (): void => {
|
||||||
|
if (hasGeneral.value) {
|
||||||
|
exitPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
router.back();
|
router.back();
|
||||||
return;
|
return;
|
||||||
@@ -260,7 +267,9 @@ onBeforeUnmount(() => {
|
|||||||
<main class="select-pool-page legacy-bg0">
|
<main class="select-pool-page legacy-bg0">
|
||||||
<header class="page-title with-border">
|
<header class="page-title with-border">
|
||||||
장 수 선 택<br />
|
장 수 선 택<br />
|
||||||
<button class="legacy-button" type="button" @click="goBack">돌아가기</button>
|
<button class="legacy-button" type="button" @click="goBack">
|
||||||
|
{{ hasGeneral ? pageExitLabel : '돌아가기' }}
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<table v-if="serverInfo" class="server-info-table legacy-bg0">
|
<table v-if="serverInfo" class="server-info-table legacy-bg0">
|
||||||
@@ -479,7 +488,9 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<footer class="page-footer">
|
<footer class="page-footer">
|
||||||
<div class="footer-back with-border">
|
<div class="footer-back with-border">
|
||||||
<button class="legacy-button" type="button" @click="goBack">돌아가기</button>
|
<button class="legacy-button" type="button" @click="goBack">
|
||||||
|
{{ hasGeneral ? pageExitLabel : '돌아가기' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-banner with-border">
|
<div class="footer-banner with-border">
|
||||||
<small>
|
<small>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type VoteListResponse = Awaited<ReturnType<typeof trpc.vote.getVoteList.query>>;
|
type VoteListResponse = Awaited<ReturnType<typeof trpc.vote.getVoteList.query>>;
|
||||||
type VoteDetail = Awaited<ReturnType<typeof trpc.vote.getVoteDetail.query>>;
|
type VoteDetail = Awaited<ReturnType<typeof trpc.vote.getVoteDetail.query>>;
|
||||||
type PollSummary = VoteListResponse['polls'][number];
|
type PollSummary = VoteListResponse['polls'][number];
|
||||||
@@ -24,7 +28,6 @@ const myComment = ref('');
|
|||||||
const newVoteTitle = ref('');
|
const newVoteTitle = ref('');
|
||||||
const newVoteOptionsText = ref('');
|
const newVoteOptionsText = ref('');
|
||||||
const newVoteMultipleOptions = ref(1);
|
const newVoteMultipleOptions = ref(1);
|
||||||
const closeWindow = (): void => window.close();
|
|
||||||
const { success: showSuccessToast, error: showErrorToast } = useGameFeedback();
|
const { success: showSuccessToast, error: showErrorToast } = useGameFeedback();
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown): string => {
|
const getErrorMessage = (error: unknown): string => {
|
||||||
@@ -208,9 +211,9 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height back_btn"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height back_btn"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeWindow"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
창 닫기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height reload_btn"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height reload_btn"
|
||||||
@@ -408,9 +411,9 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height back_btn"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height back_btn"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeWindow"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
창 닫기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -12,6 +14,8 @@ import { formatLog } from '../utils/formatLog';
|
|||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { resolveTournamentSectionVisibility, resolveTournamentStageName } from '../utils/tournamentStatus';
|
import { resolveTournamentSectionVisibility, resolveTournamentStageName } from '../utils/tournamentStatus';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
const tournamentPages = useTournamentPagesStore();
|
const tournamentPages = useTournamentPagesStore();
|
||||||
const { snapshot, betting, loading, error } = storeToRefs(tournamentPages);
|
const { snapshot, betting, loading, error } = storeToRefs(tournamentPages);
|
||||||
type Snapshot = NonNullable<typeof snapshot.value>;
|
type Snapshot = NonNullable<typeof snapshot.value>;
|
||||||
@@ -354,11 +358,9 @@ const start = async () => {
|
|||||||
</section>
|
</section>
|
||||||
<input type="hidden" name="tournamentAction" value="join" />
|
<input type="hidden" name="tournamentAction" value="join" />
|
||||||
<footer class="tournament-footer bg0">
|
<footer class="tournament-footer bg0">
|
||||||
<RouterLink v-slot="{ navigate }" custom to="/">
|
<button class="legacy-button legacy-button--navigation close-button" type="button" @click="exitPage">
|
||||||
<button class="legacy-button legacy-button--navigation close-button" type="button" @click="navigate">
|
{{ pageExitLabel }}
|
||||||
창 닫기
|
</button>
|
||||||
</button>
|
|
||||||
</RouterLink>
|
|
||||||
<small>
|
<small>
|
||||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com) / Credit
|
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com) / Credit
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type TrafficData = Awaited<ReturnType<typeof trpc.public.getTraffic.query>>;
|
type TrafficData = Awaited<ReturnType<typeof trpc.public.getTraffic.query>>;
|
||||||
|
|
||||||
const data = ref<TrafficData | null>(null);
|
const data = ref<TrafficData | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown): string => {
|
const getErrorMessage = (error: unknown): string => {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -73,7 +75,7 @@ onMounted(() => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
트 래 픽 정 보<br />
|
트 래 픽 정 보<br />
|
||||||
<button class="legacy-close" type="button" @click="router.push('/')">돌아가기</button>
|
<button class="legacy-close" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -198,7 +200,9 @@ onMounted(() => {
|
|||||||
<table class="legacy-table footer-table legacy-bg0">
|
<table class="legacy-table footer-table legacy-bg0">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><button class="legacy-close" type="button" @click="router.push('/')">돌아가기</button></td>
|
<td>
|
||||||
|
<button class="legacy-close" type="button" @click="exitPage">{{ pageExitLabel }}</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="banner">
|
<td class="banner">
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { useClockDisplay } from '../composables/useClockDisplay';
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
||||||
const { formatTime: formatGameTime } = useClockDisplay();
|
const { formatTime: formatGameTime } = useClockDisplay();
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
@@ -10,6 +11,8 @@ import LegacyGeneralProgress from '../components/ui/LegacyGeneralProgress.vue';
|
|||||||
import GeneralInformationPanel from '../components/main/GeneralInformationPanel.vue';
|
import GeneralInformationPanel from '../components/main/GeneralInformationPanel.vue';
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type TroopList = Awaited<ReturnType<typeof trpc.troop.getList.query>>;
|
type TroopList = Awaited<ReturnType<typeof trpc.troop.getList.query>>;
|
||||||
type Troop = TroopList['troops'][number];
|
type Troop = TroopList['troops'][number];
|
||||||
type Member = Troop['members'][number];
|
type Member = Troop['members'][number];
|
||||||
@@ -25,7 +28,6 @@ const dialogKind = ref<DialogKind>(null);
|
|||||||
const dialogTroopId = ref(0);
|
const dialogTroopId = ref(0);
|
||||||
const popupMember = ref<Member | null>(null);
|
const popupMember = ref<Member | null>(null);
|
||||||
const popupTop = ref(0);
|
const popupTop = ref(0);
|
||||||
const router = useRouter();
|
|
||||||
const { success: showSuccessToast, error: showErrorToast, confirm: showConfirm } = useGameFeedback();
|
const { success: showSuccessToast, error: showErrorToast, confirm: showConfirm } = useGameFeedback();
|
||||||
|
|
||||||
const me = computed(() => data.value?.me ?? null);
|
const me = computed(() => data.value?.me ?? null);
|
||||||
@@ -185,9 +187,9 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height legacyNavButton backLink"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height legacyNavButton backLink"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height legacyNavButton reloadButton"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height legacyNavButton reloadButton"
|
||||||
@@ -335,9 +337,9 @@ onMounted(() => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacyNavButton backLink"
|
class="legacy-button legacy-button--navigation legacyNavButton backLink"
|
||||||
type="button"
|
type="button"
|
||||||
@click="router.push('/')"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
돌아가기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<div></div>
|
<div></div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { usePageExit } from '../composables/usePageExit';
|
||||||
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import MapViewer from '../components/main/MapViewer.vue';
|
import MapViewer from '../components/main/MapViewer.vue';
|
||||||
import { formatLog } from '../utils/formatLog';
|
import { formatLog } from '../utils/formatLog';
|
||||||
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
|
|
||||||
|
const { pageExitLabel, exitPage } = usePageExit();
|
||||||
|
|
||||||
type YearbookRange = Awaited<ReturnType<typeof trpc.yearbook.getRange.query>>;
|
type YearbookRange = Awaited<ReturnType<typeof trpc.yearbook.getRange.query>>;
|
||||||
type MapLayout = Awaited<ReturnType<typeof trpc.public.getMapLayout.query>>;
|
type MapLayout = Awaited<ReturnType<typeof trpc.public.getMapLayout.query>>;
|
||||||
type HistoryData = {
|
type HistoryData = {
|
||||||
@@ -26,7 +30,6 @@ type HistoryData = {
|
|||||||
globalAction: string[];
|
globalAction: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
@@ -61,14 +64,6 @@ const availableYearMonths = computed(() => {
|
|||||||
return values;
|
return values;
|
||||||
});
|
});
|
||||||
|
|
||||||
const closePage = async (): Promise<void> => {
|
|
||||||
if (window.opener) {
|
|
||||||
window.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await router.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadHistory = async (): Promise<void> => {
|
const loadHistory = async (): Promise<void> => {
|
||||||
if (selectedYearMonth.value === null) {
|
if (selectedYearMonth.value === null) {
|
||||||
return;
|
return;
|
||||||
@@ -140,9 +135,9 @@ onMounted(async () => {
|
|||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
class="legacy-button legacy-button--navigation legacy-button--fixed-height close-button"
|
||||||
type="button"
|
type="button"
|
||||||
@click="closePage"
|
@click="exitPage"
|
||||||
>
|
>
|
||||||
창 닫기
|
{{ pageExitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<span class="settings-menu">
|
<span class="settings-menu">
|
||||||
<button
|
<button
|
||||||
@@ -242,7 +237,9 @@ onMounted(async () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="yearbook-footer">
|
<footer class="yearbook-footer">
|
||||||
<button class="legacy-button legacy-button--navigation" type="button" @click="closePage">창 닫기</button>
|
<button class="legacy-button legacy-button--navigation" type="button" @click="exitPage">
|
||||||
|
{{ pageExitLabel }}
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
<div class="dropdown-compat-buttons" aria-hidden="true">
|
<div class="dropdown-compat-buttons" aria-hidden="true">
|
||||||
<button type="button" tabindex="-1" /><button type="button" tabindex="-1" />
|
<button type="button" tabindex="-1" /><button type="button" tabindex="-1" />
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ void describe('shared Lumen button family', () => {
|
|||||||
nationGenerals,
|
nationGenerals,
|
||||||
/legacy-button legacy-button--info legacy-button--fixed-height top-button columns-button/u
|
/legacy-button legacy-button--info legacy-button--fixed-height top-button columns-button/u
|
||||||
);
|
);
|
||||||
for (const label of ['돌아가기', '갱신', '보기 모드⌄', '열 선택⌄']) {
|
for (const label of ['pageExitLabel', '갱신', '보기 모드⌄', '열 선택⌄']) {
|
||||||
assert.match(nationGenerals, new RegExp(label, 'u'));
|
assert.match(nationGenerals, new RegExp(label, 'u'));
|
||||||
}
|
}
|
||||||
for (const label of ['토너먼트', '베팅장', '창 닫기']) {
|
for (const label of ['토너먼트', '베팅장', 'pageExitLabel']) {
|
||||||
assert.match(tournamentHeader, new RegExp(`legacy-button[\\s\\S]{0,260}${label}`, 'u'));
|
assert.match(tournamentHeader, new RegExp(`legacy-button[\\s\\S]{0,260}${label}`, 'u'));
|
||||||
}
|
}
|
||||||
assert.match(tournamentHeader, /const closeWindow = \(\): void => window\.close\(\)/u);
|
assert.match(tournamentHeader, /@click="exitPage"/u);
|
||||||
assert.doesNotMatch(tournamentHeader, /custom to="\/"/u);
|
assert.doesNotMatch(tournamentHeader, /custom to="\/"/u);
|
||||||
assert.match(reservedEditor, /legacy-button legacy-button--info legacy-button--fixed-height select-command/u);
|
assert.match(reservedEditor, /legacy-button legacy-button--info legacy-button--fixed-height select-command/u);
|
||||||
assert.match(reservedEditor, /명령 선택 ▾/u);
|
assert.match(reservedEditor, /명령 선택 ▾/u);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 게임 페이지의 돌아가기와 창 닫기
|
||||||
|
|
||||||
|
## 제품 계약
|
||||||
|
|
||||||
|
2026-09-14부터 게임 메인에서 연결되는 내부 페이지는 페이지 이름이나 `opener`가
|
||||||
|
아니라 **탭을 연 방식**으로 종료 동작을 결정합니다. 기존 Ref의 메인 복귀와 별도
|
||||||
|
창 종료 개념을 계승하면서, 직접 접속과 브라우저 새 탭 동작을 구분하는 Core UX 정책입니다.
|
||||||
|
|
||||||
|
| 진입 방식 | 표시 | 버튼 결과 |
|
||||||
|
| --------------------------------------------------------------------- | -------- | ------------------------------------------------- |
|
||||||
|
| 현재 탭에서 페이지 이동 | 돌아가기 | 같은 탭의 게임 메인으로 이동 |
|
||||||
|
| `newTab` 링크를 일반 클릭 또는 Enter로 실행 | 창 닫기 | 새로 열린 탭 종료, 원래 탭 유지 |
|
||||||
|
| Ctrl/Meta/Shift 클릭, 가운데 클릭, 브라우저 새 탭 열기, URL 직접 접속 | 돌아가기 | 해당 탭을 유지하고 게임 메인으로 이동 |
|
||||||
|
| 앱이 연 탭 안에서 페이지 이동 또는 새로고침 | 창 닫기 | 같은 보조 탭 종료 |
|
||||||
|
| 브라우저가 앱의 새 창 열기를 차단 | 돌아가기 | 현재 탭에서 목적지를 열고 같은 탭의 메인으로 복귀 |
|
||||||
|
|
||||||
|
‘창 닫기’는 `window.close()`만 호출하며 메인 이동으로 대체하지 않습니다.
|
||||||
|
‘돌아가기’는 브라우저 history의 직전 페이지가 아닌 현재 게임 prefix의 `/`로 이동합니다.
|
||||||
|
버튼의 원래 위치·스타일·상하단 구성은 유지합니다. 전투 시뮬레이터에는 빠져 있던
|
||||||
|
페이지 종료 버튼을 상단에 추가했습니다. 게시판 조회·접근 실패 화면에도 복귀 버튼을 제공합니다.
|
||||||
|
|
||||||
|
## 구현
|
||||||
|
|
||||||
|
- `app/game-frontend/src/main.ts`에서 `installAuxiliaryNavigation()`을 설치합니다.
|
||||||
|
- `app/game-frontend/src/utils/auxiliaryNavigation.ts`는 게임 prefix 안의 `target="_blank"` 링크에
|
||||||
|
대한 일반 클릭만 처리합니다. 따라서 공통·국가 메뉴, 모바일 메뉴, 설문 상태 제목과
|
||||||
|
새 설문 알림도 동일한 진입 처리를 사용합니다.
|
||||||
|
- 앱이 실제로 연 빈 탭에 고유한 `window.name`을 지정하고 `opener`를 분리한 뒤
|
||||||
|
목적지로 이동합니다. `noreferrer`도 새 문서의 referrer 정책으로 보존합니다.
|
||||||
|
- `app/game-frontend/src/composables/usePageExit.ts`가 표식 유무로 문구와 동작을 함께 결정합니다.
|
||||||
|
이 표식은 UI 상태이며 인증·권한 근거가 아닙니다. URL, localStorage 또는
|
||||||
|
sessionStorage를 표식으로 사용하지 않아 복사한 링크와 사용자가 연 탭에 전파하지 않습니다.
|
||||||
|
- 외부 웹사이트 및 다른 앱 prefix의 링크는 해당 앱의 탐색 정책을 따릅니다.
|
||||||
|
게임 안의 대화상자·선택창 ‘닫기’는 해당 대화상자를 닫는 기존 동작입니다.
|
||||||
|
- 장수 선택의 경우 이미 장수가 있는 메인 진입 흐름에는 이 정책을 사용하고,
|
||||||
|
아직 장수가 없는 가입 흐름의 복귀 경로는 유지합니다.
|
||||||
|
|
||||||
|
## 적용 화면
|
||||||
|
|
||||||
|
공통 메뉴: 천통국 베팅, 세력일람, 장수일람, 명장일람, 연감, 전투 시뮬레이터,
|
||||||
|
명예의전당, 왕조일람·상세, 접속량정보, 빙의일람, 설문조사.
|
||||||
|
|
||||||
|
국가 메뉴: 회의실·기밀실, 부대 편성, 외교부, 인사부, 내무부, 사령부, NPC 정책,
|
||||||
|
암행부, 토너먼트·베팅장, 세력 정보·도시·장수, 중원 정보, 현재 도시, 감찰부,
|
||||||
|
유산 관리, 내 정보, 금/쌀·유니크 경매장, 환경 설정.
|
||||||
|
|
||||||
|
후속 화면인 과거 플레이 기록 및 기존 장수의 장수 선택에도 같은 정책을 적용합니다.
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
`e2e/mainNavigation.spec.ts`에서 메인의 실제 링크 클릭으로 팝업을 얻고,
|
||||||
|
상하단 종료, 원래 탭 URL 유지와 `context.pages()` 수를 검증합니다.
|
||||||
|
직접 접속·Ctrl 클릭·가운데 클릭, 같은 탭 이동, 보조 탭 내부 이동·새로고침을
|
||||||
|
구분합니다. 전체 메뉴 순회는 API 조회 실패 시에도 종료 동작을 제공하는지 확인합니다.
|
||||||
|
정상 데이터 렌더링은 기존 페이지별 fixture suite와 함께 검증합니다.
|
||||||
|
|
||||||
|
실행 예:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PLAYWRIGHT_FRONTEND_PORT=15261 PLAYWRIGHT_FRONTEND_MODE=production \
|
||||||
|
pnpm --filter @sammo-ts/game-frontend exec playwright test mainNavigation.spec.ts \
|
||||||
|
--config e2e/playwright.config.mjs \
|
||||||
|
--grep 'page exit policy|all main page exits|closes the survey popup'
|
||||||
|
```
|
||||||
|
|
||||||
|
브라우저의 종료 제한 근거:
|
||||||
|
[MDN Window.close](https://developer.mozilla.org/en-US/docs/Web/API/Window/close).
|
||||||
Reference in New Issue
Block a user