merge: 최신 main을 tRPC JSON 본문 전송에 통합
This commit is contained in:
@@ -53,6 +53,18 @@ const inputOptions = {
|
||||
{ value: 1, label: '장수 (아국 · 업)' },
|
||||
{ value: 2, label: '관우 (아국 · 업)' },
|
||||
],
|
||||
generalTargets: {
|
||||
che_포상: [
|
||||
{ value: 1, label: '장수 (아국 · 업)' },
|
||||
{ value: 2, label: '관우 (아국 · 업)' },
|
||||
{ value: 3, label: '여포NPC (아국 · 업)' },
|
||||
],
|
||||
che_몰수: [
|
||||
{ value: 1, label: '장수 (아국 · 업)' },
|
||||
{ value: 2, label: '관우 (아국 · 업)' },
|
||||
{ value: 3, label: '여포NPC (아국 · 업)' },
|
||||
],
|
||||
},
|
||||
crewTypes: [{ value: 1100, label: '보병' }],
|
||||
armTypes: [{ value: 1, label: '보병' }],
|
||||
nationTypes: [{ value: 'che_도적', label: '도적', description: '금 수입 증가, 쌀 수입 감소' }],
|
||||
@@ -389,8 +401,8 @@ const chiefCenter = {
|
||||
maxTurns: 12,
|
||||
chiefs: [12, 10, 8, 6, 11, 9, 7, 5].map((officerLevel) => ({
|
||||
officerLevel,
|
||||
name: officerLevel === 5 ? '장수' : null,
|
||||
npcState: officerLevel === 5 ? 0 : null,
|
||||
name: officerLevel === 5 ? '장수' : `수뇌${officerLevel}`,
|
||||
npcState: officerLevel === 8 ? 2 : 0,
|
||||
turnTime: null,
|
||||
revision: 0,
|
||||
turns: turns(12),
|
||||
@@ -801,6 +813,92 @@ test('shows every Ref chief command in the exact category and command order', as
|
||||
await mobilePicker.screenshot({ path: test.info().outputPath('ref-chief-command-list-mobile-500.png') });
|
||||
});
|
||||
|
||||
test('shows all 12 advanced chief turns before the actions and uses the full mobile chief matrix', async ({ page }) => {
|
||||
await install(page);
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
await page.goto('/che/chief-center');
|
||||
|
||||
const editor = page.locator('[data-command-scope="nation"]:visible');
|
||||
await editor.getByRole('button', { name: '고급 모드', exact: true }).click();
|
||||
await expect(editor.locator('.index-column > button')).toHaveCount(12);
|
||||
await expect(editor.locator('.index-column > button').last()).toHaveText('12');
|
||||
await expect(editor.locator('.advanced-actions')).toContainText('선택한 턴을');
|
||||
await expect(editor.locator('.advanced-actions')).toContainText('명령 선택');
|
||||
|
||||
const frame = page.locator('.chief-overview-frame');
|
||||
await expect(frame.locator('.chief-overview-row')).toHaveCount(2);
|
||||
await expect(frame.locator('.overview-turn-index')).toHaveCount(4);
|
||||
for (const gutter of await frame.locator('.overview-turn-index').all()) {
|
||||
await expect(gutter.locator('span').filter({ hasText: /\d+/u })).toHaveText(
|
||||
Array.from({ length: 12 }, (_, index) => String(index + 1))
|
||||
);
|
||||
}
|
||||
await expect(frame.locator('.compact-name')).toHaveCount(8);
|
||||
|
||||
const geometry = await page.locator('.chief-page').evaluate((element) => {
|
||||
const editorElement = element.querySelector<HTMLElement>('[data-command-scope="nation"]')!;
|
||||
const queue = editorElement.querySelector<HTMLElement>('.queue-grid')!;
|
||||
const lastTurn = editorElement.querySelectorAll<HTMLElement>('.action-column > div')[11]!;
|
||||
const actions = editorElement.querySelector<HTMLElement>('.advanced-actions')!;
|
||||
const overviewFrame = element.querySelector<HTMLElement>('.chief-overview-frame')!;
|
||||
const firstOverviewRow = element.querySelector<HTMLElement>('.chief-overview-row')!;
|
||||
const overviewRows = [...element.querySelectorAll<HTMLElement>('.chief-overview-row')];
|
||||
const gutters = [...firstOverviewRow.querySelectorAll<HTMLElement>('.overview-turn-index')];
|
||||
const cards = [...firstOverviewRow.querySelectorAll<HTMLElement>('.chief-card')];
|
||||
const names = [...overviewFrame.querySelectorAll<HTMLElement>('.compact-name')];
|
||||
const frameRect = overviewFrame.getBoundingClientRect();
|
||||
const editorRect = editorElement.getBoundingClientRect();
|
||||
const actionsRect = actions.getBoundingClientRect();
|
||||
return {
|
||||
editorBottom: editorRect.bottom,
|
||||
editorHeight: editorRect.height,
|
||||
queueBottom: queue.getBoundingClientRect().bottom,
|
||||
lastTurnBottom: lastTurn.getBoundingClientRect().bottom,
|
||||
actionsTop: actionsRect.top,
|
||||
actionsBottom: actionsRect.bottom,
|
||||
frameTop: frameRect.top,
|
||||
frameWidth: frameRect.width,
|
||||
rowWidth: firstOverviewRow.getBoundingClientRect().width,
|
||||
rowEdges: overviewRows.map((item) => ({
|
||||
top: item.getBoundingClientRect().top - frameRect.top,
|
||||
bottom: item.getBoundingClientRect().bottom - frameRect.top,
|
||||
})),
|
||||
gutterWidths: gutters.map((item) => item.getBoundingClientRect().width),
|
||||
gutterEdges: gutters.map((item) => ({
|
||||
left: item.getBoundingClientRect().left - frameRect.left,
|
||||
right: item.getBoundingClientRect().right - frameRect.left,
|
||||
})),
|
||||
cardWidths: cards.map((item) => item.getBoundingClientRect().width),
|
||||
namesInsideFrame: names.every((item) => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
return rect.top >= frameRect.top && rect.bottom <= frameRect.bottom && rect.height > 0;
|
||||
}),
|
||||
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
|
||||
expect(geometry.lastTurnBottom).toBeLessThanOrEqual(geometry.actionsTop);
|
||||
expect(geometry.queueBottom).toBeLessThanOrEqual(geometry.actionsTop);
|
||||
expect(geometry.actionsBottom).toBeLessThanOrEqual(geometry.editorBottom);
|
||||
expect(geometry.frameTop).toBeGreaterThanOrEqual(geometry.editorBottom);
|
||||
expect(geometry.editorHeight).toBeGreaterThanOrEqual(404);
|
||||
expect(geometry.frameWidth).toBe(500);
|
||||
expect(geometry.rowWidth).toBe(500);
|
||||
expect(geometry.rowEdges).toEqual([
|
||||
{ top: 0, bottom: 155 },
|
||||
{ top: 155, bottom: 310 },
|
||||
]);
|
||||
expect(geometry.gutterWidths).toEqual([12, 12]);
|
||||
expect(geometry.gutterEdges).toEqual([
|
||||
{ left: 0, right: 12 },
|
||||
{ left: 488, right: 500 },
|
||||
]);
|
||||
expect(geometry.cardWidths).toEqual([119, 119, 119, 119]);
|
||||
expect(geometry.namesInsideFrame).toBe(true);
|
||||
expect(geometry.documentOverflow).toBeLessThanOrEqual(0);
|
||||
await page.screenshot({ path: test.info().outputPath('chief-advanced-mobile-500.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('enters general and nation command arguments and sends exact values', async ({ page }) => {
|
||||
const requests = await install(page);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
@@ -891,7 +989,13 @@ test('enters general and nation command arguments and sends exact values', async
|
||||
const chiefForm = chiefPicker.getByTestId('command-argument-form');
|
||||
await chiefForm.getByRole('button', { name: '쌀' }).click();
|
||||
await chiefForm.locator('input[type=number]').fill('300');
|
||||
await chiefForm.locator('select').selectOption('2');
|
||||
const chiefTarget = chiefForm.locator('select');
|
||||
await expect(chiefTarget.locator('option')).toHaveText([
|
||||
'장수 (아국 · 업)',
|
||||
'관우 (아국 · 업)',
|
||||
'여포NPC (아국 · 업)',
|
||||
]);
|
||||
await chiefTarget.selectOption('3');
|
||||
const geometry = await chiefForm.evaluate((element) => {
|
||||
const row = element.querySelector('.argument-row');
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -905,13 +1009,13 @@ test('enters general and nation command arguments and sends exact values', async
|
||||
});
|
||||
await chiefPicker.getByRole('button', { name: '입력', exact: true }).click();
|
||||
await expect(page.locator('[data-command-scope="nation"] .action-column > div').first()).toHaveText(
|
||||
'【관우】 쌀 300 포상'
|
||||
'【여포NPC】 쌀 300 포상'
|
||||
);
|
||||
|
||||
expect(JSON.stringify(requests)).toContain('"destCityId":2');
|
||||
expect(JSON.stringify(requests)).toContain('"isGold":false');
|
||||
expect(JSON.stringify(requests)).toContain('"amount":300');
|
||||
expect(JSON.stringify(requests)).toContain('"destGeneralId":2');
|
||||
expect(JSON.stringify(requests)).toContain('"destGeneralId":3');
|
||||
|
||||
expect(mapGeometry.width).toBeGreaterThan(650);
|
||||
expect(mapGeometry.height / mapGeometry.width).toBeCloseTo(5 / 7, 2);
|
||||
|
||||
@@ -875,6 +875,18 @@ test('current-city wraps dense general names and only shrinks reserved turns', a
|
||||
expect(pageStyle.fontFamily).toContain('Pretendard');
|
||||
expect(pageStyle.fontSize).toBe('14px');
|
||||
|
||||
const controlFonts = await page
|
||||
.locator('.city-page')
|
||||
.evaluate(() =>
|
||||
['#citySelector', '.back-link'].map(
|
||||
(selector) => getComputedStyle(document.querySelector(selector)!).fontFamily
|
||||
)
|
||||
);
|
||||
expect(controlFonts).toHaveLength(2);
|
||||
for (const fontFamily of controlFonts) {
|
||||
expect(fontFamily).toContain('Pretendard');
|
||||
}
|
||||
|
||||
const names = page.locator('.general-names');
|
||||
await expect(names).toHaveCSS('white-space', 'normal');
|
||||
const nameLineCount = await names.locator('span').evaluateAll((elements) => {
|
||||
|
||||
@@ -791,6 +791,97 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
|
||||
await persistParityArtifact(page, 'core-main-personal-battle-log-inline-mobile', mobileGeometry);
|
||||
});
|
||||
|
||||
test('메인 개인 기록의 공격·수비 시각은 Ref와 같은 90% 글자 크기로 표시한다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'head',
|
||||
myset: 3,
|
||||
settingMutations: [],
|
||||
accessPages: [],
|
||||
recentRecords: {
|
||||
global: [],
|
||||
general: [
|
||||
{
|
||||
id: 18703,
|
||||
text: '<C>●</>10월:천귀병으로 <Y>ⓝ염행</>의 보병을 <M>수비</>합니다.',
|
||||
createdAt: '2026-01-01T03:54:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 18702,
|
||||
text: '<C>●</>10월:천귀병으로 <Y>ⓝ염행</>의 보병을 <M>공격</>합니다.',
|
||||
createdAt: '2026-01-01T03:55:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 18701,
|
||||
text: '<C>●</>10월:이미 기록된 시각 <1>12:34</>',
|
||||
createdAt: '2026-01-01T03:56:00.000Z',
|
||||
},
|
||||
],
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
await install(page, state);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await page.goto('');
|
||||
|
||||
const inspect = async (selector: string) => {
|
||||
const lines = page.locator(selector);
|
||||
await expect(lines).toHaveCount(3);
|
||||
await expect(lines.nth(0)).toHaveText('●10월:천귀병으로 ⓝ염행의 보병을 수비합니다. 12:54');
|
||||
await expect(lines.nth(1)).toHaveText('●10월:천귀병으로 ⓝ염행의 보병을 공격합니다. 12:55');
|
||||
await expect(lines.nth(2)).toHaveText('●10월:이미 기록된 시각 12:34');
|
||||
|
||||
return lines.evaluateAll((elements) =>
|
||||
elements.map((element) => {
|
||||
const spans = [...element.querySelectorAll<HTMLElement>('span')];
|
||||
const time = spans.find((span) => /^\d{2}:\d{2}$/u.test(span.textContent ?? ''));
|
||||
const name = spans.find((span) => span.textContent === 'ⓝ염행');
|
||||
const action = spans.find((span) => span.textContent === '수비' || span.textContent === '공격');
|
||||
if (!time) throw new Error('개인 기록 시각 span을 찾지 못했습니다.');
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
text: element.textContent,
|
||||
row: {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
clientWidth: element.clientWidth,
|
||||
scrollWidth: element.scrollWidth,
|
||||
fontSize: getComputedStyle(element).fontSize,
|
||||
lineHeight: getComputedStyle(element).lineHeight,
|
||||
},
|
||||
time: {
|
||||
fontSize: getComputedStyle(time).fontSize,
|
||||
lineHeight: getComputedStyle(time).lineHeight,
|
||||
},
|
||||
nameFontSize: name ? getComputedStyle(name).fontSize : null,
|
||||
actionFontSize: action ? getComputedStyle(action).fontSize : null,
|
||||
timeSpanCount: spans.filter((span) => /^\d{2}:\d{2}$/u.test(span.textContent ?? '')).length,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
const assertFontContract = (measurements: Awaited<ReturnType<typeof inspect>>) => {
|
||||
expect(measurements.map((entry) => entry.row.fontSize)).toEqual(['14px', '14px', '14px']);
|
||||
expect(measurements.map((entry) => entry.row.lineHeight)).toEqual(['21px', '21px', '21px']);
|
||||
expect(measurements.map((entry) => entry.row.height)).toEqual([21, 21, 21]);
|
||||
expect(measurements.map((entry) => entry.time.fontSize)).toEqual(['12.6px', '12.6px', '12.6px']);
|
||||
expect(measurements.map((entry) => entry.timeSpanCount)).toEqual([1, 1, 1]);
|
||||
expect(measurements[0]?.nameFontSize).toBe('14px');
|
||||
expect(measurements[0]?.actionFontSize).toBe('14px');
|
||||
expect(measurements[1]?.nameFontSize).toBe('14px');
|
||||
expect(measurements[1]?.actionFontSize).toBe('14px');
|
||||
expect(measurements.every((entry) => entry.row.scrollWidth <= entry.row.clientWidth)).toBe(true);
|
||||
};
|
||||
|
||||
const desktop = await inspect('.record-zone [data-record-bucket="general"] .record-line');
|
||||
assertFontContract(desktop);
|
||||
await persistParityArtifact(page, 'core-main-personal-war-log-time-font-desktop', desktop);
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
const mobile = await inspect('.record-zone-mobile [data-record-bucket="general"] .record-line');
|
||||
assertFontContract(mobile);
|
||||
await persistParityArtifact(page, 'core-main-personal-war-log-time-font-mobile', mobile);
|
||||
});
|
||||
|
||||
test('접속량정보 keeps the legacy public 1016px chart geometry', async ({ page }) => {
|
||||
const state: FixtureState = { permission: 'member', myset: 0, settingMutations: [], accessPages: [] };
|
||||
await install(page, state);
|
||||
|
||||
@@ -118,7 +118,8 @@ const persistScreenshot = async (page: Page, name: string, fallbackPath: string)
|
||||
await page.screenshot({ path: resolve(responsiveArtifactDir, `${name}.webp`), fullPage: true });
|
||||
};
|
||||
|
||||
const installFixture = async (page: Page) => {
|
||||
const installFixture = async (page: Page, options: { applicationOpen?: boolean } = {}) => {
|
||||
let joined = false;
|
||||
await page.addInitScript((profile) => {
|
||||
window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright');
|
||||
window.localStorage.setItem('sammo-game-profile', profile);
|
||||
@@ -141,7 +142,7 @@ const installFixture = async (page: Page) => {
|
||||
if (operation === 'tournament.getSnapshot') {
|
||||
return response({
|
||||
state: {
|
||||
stage: 0,
|
||||
stage: options.applicationOpen ? 1 : 0,
|
||||
phase: 0,
|
||||
type: 0,
|
||||
auto: false,
|
||||
@@ -151,11 +152,32 @@ const installFixture = async (page: Page) => {
|
||||
nextAt: '2026-08-02T00:00:00.000Z',
|
||||
winnerId: 1,
|
||||
},
|
||||
participants,
|
||||
participants:
|
||||
options.applicationOpen && !joined
|
||||
? []
|
||||
: options.applicationOpen
|
||||
? [
|
||||
{
|
||||
...participants[0],
|
||||
groupId: 0,
|
||||
groupNo: 0,
|
||||
win: 0,
|
||||
draw: 0,
|
||||
lose: 0,
|
||||
gl: 0,
|
||||
seedRank: 0,
|
||||
finalRank: 0,
|
||||
},
|
||||
]
|
||||
: participants,
|
||||
matches,
|
||||
betCount: 16,
|
||||
});
|
||||
}
|
||||
if (operation === 'tournament.join') {
|
||||
joined = true;
|
||||
return response({ ok: true, count: 1 });
|
||||
}
|
||||
if (operation === 'tournament.getBettingSummary') {
|
||||
return response({
|
||||
totals: Object.fromEntries(
|
||||
@@ -245,9 +267,67 @@ test('desktop bracket connects every real general slot to the next round', async
|
||||
expect(geometry.horizontalIdentities).toBe(true);
|
||||
expect(Math.abs(geometry.firstParentY - geometry.firstPairAverageY)).toBeLessThan(1);
|
||||
|
||||
const controls = await page.locator('#tournament-container').evaluate((container) => {
|
||||
const bounds = (selector: string) => container.querySelector<HTMLElement>(selector)!.getBoundingClientRect();
|
||||
const refresh = bounds('.toolbar button:first-child');
|
||||
const join = bounds('.join-button');
|
||||
const close = bounds('.close-button');
|
||||
return {
|
||||
refresh: { width: refresh.width, height: refresh.height },
|
||||
join: { width: join.width, height: join.height },
|
||||
close: { width: close.width, height: close.height },
|
||||
};
|
||||
});
|
||||
expect(controls.refresh).toEqual({ width: 72, height: 44 });
|
||||
expect(controls.join).toEqual({ width: 72, height: 44 });
|
||||
expect(controls.close).toEqual({ width: 88, height: 44 });
|
||||
|
||||
const firstSlot = page.locator('.desktop-bracket-name').first();
|
||||
const oddsContainment = await firstSlot.evaluate((slot) => {
|
||||
const card = slot.getBoundingClientRect();
|
||||
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
|
||||
return {
|
||||
cardTop: card.top,
|
||||
cardBottom: card.bottom,
|
||||
oddsTop: odds.top,
|
||||
oddsBottom: odds.bottom,
|
||||
cardHeight: card.height,
|
||||
};
|
||||
});
|
||||
expect(oddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
|
||||
expect(oddsContainment.oddsTop).toBeGreaterThanOrEqual(oddsContainment.cardTop);
|
||||
expect(oddsContainment.oddsBottom).toBeLessThanOrEqual(oddsContainment.cardBottom);
|
||||
|
||||
await persistScreenshot(page, 'tournament-desktop', testInfo.outputPath('tournament-bracket-desktop.webp'));
|
||||
});
|
||||
|
||||
test('join refresh shows the assigned preliminary group immediately with accessible controls', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await installFixture(page, { applicationOpen: true });
|
||||
await page.goto('tournament');
|
||||
|
||||
const refresh = page.getByRole('button', { name: '갱신' });
|
||||
const join = page.getByRole('button', { name: '참가' });
|
||||
const close = page.getByRole('button', { name: '창 닫기' }).first();
|
||||
await expect(join).toBeEnabled();
|
||||
await join.click();
|
||||
|
||||
await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다.');
|
||||
await expect(join).toBeDisabled();
|
||||
await expect(page.locator('.preliminary-grid .general-identity', { hasText: names[0] })).toBeVisible();
|
||||
|
||||
for (const control of [refresh, join, close]) {
|
||||
const box = await control.boundingBox();
|
||||
expect(box?.height).toBe(44);
|
||||
expect(box?.width).toBeGreaterThanOrEqual(72);
|
||||
}
|
||||
await refresh.focus();
|
||||
await expect(refresh).toBeFocused();
|
||||
await refresh.hover();
|
||||
await expect(refresh).toHaveCSS('filter', 'brightness(1.25)');
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||
});
|
||||
|
||||
test('mobile bracket exposes every round through tabs with standard horizontal identities', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
@@ -325,6 +405,14 @@ test('mobile bracket exposes every round through tabs with standard horizontal i
|
||||
expect(identity.nameLeft).toBeGreaterThanOrEqual(identity.iconRight - 1);
|
||||
expect(identity.nameTop).toBeLessThan(identity.iconBottom);
|
||||
expect(identity.nameBottom).toBeGreaterThan(identity.iconTop);
|
||||
const firstMobileSlot = bracket.locator('.mobile-bracket-name').first();
|
||||
const mobileOddsContainment = await firstMobileSlot.evaluate((slot) => {
|
||||
const card = slot.getBoundingClientRect();
|
||||
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
|
||||
return { cardBottom: card.bottom, oddsBottom: odds.bottom, cardHeight: card.height };
|
||||
});
|
||||
expect(mobileOddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
|
||||
expect(mobileOddsContainment.oddsBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom);
|
||||
await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible();
|
||||
await page.getByRole('tab', { name: '二조' }).first().click();
|
||||
await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
|
||||
@import 'tailwindcss';
|
||||
@import './styles/tokens.css';
|
||||
@import './styles/legacy-controls.css';
|
||||
|
||||
@@ -685,7 +685,9 @@ const clickOutsideMenu = (event: Event) => {
|
||||
class="command-picker"
|
||||
:class="{ 'recruitment-picker': isRecruitmentCommand }"
|
||||
data-testid="command-picker"
|
||||
:style="isRecruitmentCommand || props.compact ? undefined : { top: quickPickerTop }"
|
||||
:style="
|
||||
isRecruitmentCommand || quickTarget === null || props.compact ? undefined : { top: quickPickerTop }
|
||||
"
|
||||
:role="isRecruitmentCommand ? 'dialog' : undefined"
|
||||
:aria-modal="isRecruitmentCommand ? 'true' : undefined"
|
||||
:aria-label="
|
||||
@@ -1146,12 +1148,15 @@ const clickOutsideMenu = (event: Event) => {
|
||||
}
|
||||
|
||||
.mobile.compact .editor-layout {
|
||||
height: 360px;
|
||||
min-height: 370px;
|
||||
display: grid;
|
||||
grid-template-columns: 109px 391px;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
.mobile.compact .control-pad {
|
||||
order: initial;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / -1;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1159,6 +1164,8 @@ const clickOutsideMenu = (event: Event) => {
|
||||
}
|
||||
.mobile.compact .queue-area {
|
||||
order: initial;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.mobile.compact .queue-grid {
|
||||
@@ -1189,8 +1196,8 @@ const clickOutsideMenu = (event: Event) => {
|
||||
overflow: visible;
|
||||
}
|
||||
.mobile.compact .advanced-actions {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 109px;
|
||||
position: static;
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -75,7 +75,10 @@ export const formatReservedCommandBrief = (
|
||||
const commandName = commandNames.get(action) ?? defaultCommandName(action);
|
||||
const cityName = optionLabel(input?.cities ?? [], firstValue(args, ['destCityId', 'destCityID']));
|
||||
const nationName = optionLabel(input?.nations ?? [], firstValue(args, ['destNationId', 'destNationID']));
|
||||
const generalName = optionLabel(input?.generals ?? [], firstValue(args, ['destGeneralId', 'destGeneralID']));
|
||||
const generalName = optionLabel(
|
||||
input?.generalTargets?.[action] ?? input?.generals ?? [],
|
||||
firstValue(args, ['destGeneralId', 'destGeneralID'])
|
||||
);
|
||||
|
||||
if (CITY_TO_COMMANDS.has(action) && cityName) {
|
||||
return `${wrappedWithParticle(cityName, '으로')} ${commandName}`;
|
||||
|
||||
@@ -95,6 +95,7 @@ export type CommandTable = {
|
||||
cities: CommandOption[];
|
||||
nations: CommandOption[];
|
||||
generals: CommandOption[];
|
||||
generalTargets?: Record<string, CommandOption[]>;
|
||||
crewTypes: CommandOption[];
|
||||
armTypes: CommandOption[];
|
||||
nationTypes: CommandOption[];
|
||||
|
||||
@@ -33,6 +33,9 @@ const visibleFields = computed(() => props.fields.filter((entry) => entry.kind !
|
||||
const optionsFor = (field: CommandInputField): CommandOption[] => {
|
||||
if (field.options) return field.options;
|
||||
if (!field.optionSource) return [];
|
||||
if (field.optionSource === 'generals') {
|
||||
return props.options.generalTargets?.[props.commandKey] ?? props.options.generals;
|
||||
}
|
||||
if (field.optionSource === 'items') {
|
||||
return props.options.items[String(values.itemType ?? '')] ?? [];
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const formattedLogs = computed(() =>
|
||||
.recent-log-list {
|
||||
min-width: 0;
|
||||
color: #fff;
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
@@ -28,8 +28,10 @@ const roundColumns = computed(() => [
|
||||
]);
|
||||
const desktopX = [110, 355, 600, 845, 1090];
|
||||
const cardWidth = 190;
|
||||
const desktopSlotHeight = 88;
|
||||
const desktopCanvasHeight = desktopSlotHeight * 16;
|
||||
const slotY = (columnIndex: number, slotIndex: number) => {
|
||||
const slotHeight = 72 * 2 ** columnIndex;
|
||||
const slotHeight = desktopSlotHeight * 2 ** columnIndex;
|
||||
return slotHeight / 2 + slotIndex * slotHeight;
|
||||
};
|
||||
const connections = computed(() =>
|
||||
@@ -77,8 +79,8 @@ const mobilePairs = computed(() => {
|
||||
<div class="desktop-round-labels" aria-hidden="true">
|
||||
<strong v-for="label in roundLabels" :key="label">{{ label }}</strong>
|
||||
</div>
|
||||
<div class="desktop-bracket-canvas">
|
||||
<svg viewBox="0 0 1200 1152" aria-hidden="true">
|
||||
<div class="desktop-bracket-canvas" :style="{ height: `${desktopCanvasHeight}px` }">
|
||||
<svg :viewBox="`0 0 1200 ${desktopCanvasHeight}`" aria-hidden="true">
|
||||
<g v-for="connection in connections" :key="connection.id">
|
||||
<path
|
||||
class="bracket-connector"
|
||||
@@ -177,7 +179,6 @@ const mobilePairs = computed(() => {
|
||||
.desktop-bracket-canvas {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 1152px;
|
||||
}
|
||||
.desktop-bracket-canvas svg {
|
||||
position: absolute;
|
||||
@@ -200,7 +201,7 @@ const mobilePairs = computed(() => {
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
width: clamp(140px, 16vw, 190px);
|
||||
min-height: 68px;
|
||||
min-height: 82px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
transform: translate(-50%, -50%);
|
||||
@@ -265,7 +266,7 @@ const mobilePairs = computed(() => {
|
||||
.mobile-bracket-name {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
min-height: 68px;
|
||||
min-height: 82px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #555;
|
||||
background: rgb(58 33 24 / 94%);
|
||||
|
||||
@@ -304,16 +304,16 @@ const placeBet = async (targetId: number) => {
|
||||
background: #142b42 var(--sammo-texture-blue);
|
||||
}
|
||||
.title {
|
||||
height: 55.6875px;
|
||||
min-height: 68px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 19.1875px;
|
||||
}
|
||||
.close-button {
|
||||
display: block;
|
||||
width: 62px;
|
||||
height: 35.5px;
|
||||
padding: 8px 12px;
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #375a7f;
|
||||
border-radius: 5.25px;
|
||||
background: #375a7f;
|
||||
@@ -323,10 +323,16 @@ const placeBet = async (targetId: number) => {
|
||||
text-decoration: none;
|
||||
}
|
||||
.toolbar {
|
||||
min-height: 36.5px;
|
||||
min-height: 46px;
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
}
|
||||
.toolbar button {
|
||||
min-width: 72px;
|
||||
height: 44px;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.error {
|
||||
min-height: 32px;
|
||||
padding: 5px;
|
||||
|
||||
@@ -252,9 +252,7 @@ onMounted(() => {
|
||||
padding: 8px;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
font:
|
||||
16px/normal 'Times New Roman',
|
||||
serif;
|
||||
font: 16px/normal var(--sammo-font-sans);
|
||||
}
|
||||
|
||||
.legacy-board-page {
|
||||
|
||||
@@ -352,22 +352,34 @@ const repeatTurns = async (amount: number) => {
|
||||
</div>
|
||||
<div class="chief-overview-frame">
|
||||
<div class="chief-overview">
|
||||
<template v-for="chief in chiefViews" :key="chief.officerLevel">
|
||||
<ChiefTurnCard
|
||||
v-if="chief.name"
|
||||
:officer-level-text="chief.officerLevelText"
|
||||
:name="chief.name"
|
||||
:npc-state="chief.npcState"
|
||||
:rows="chief.rows"
|
||||
:compact="true"
|
||||
:selected="chief.officerLevel === selectedChief?.officerLevel"
|
||||
:is-me="chief.officerLevel === data.me.officerLevel"
|
||||
:clickable="true"
|
||||
:turn-time-label="chief.rows[0]?.time"
|
||||
@select="selectedChiefLevel = chief.officerLevel"
|
||||
/>
|
||||
<div v-else class="empty-chief-slot" aria-hidden="true"></div>
|
||||
</template>
|
||||
<div
|
||||
v-for="(rowChiefs, rowIndex) in [chiefViews.slice(0, 4), chiefViews.slice(4, 8)]"
|
||||
:key="rowIndex"
|
||||
class="chief-overview-row"
|
||||
>
|
||||
<div class="overview-turn-index legacy-bg0" :aria-label="`${rowIndex + 1}행 턴 번호`">
|
||||
<span></span><span v-for="idx in data.maxTurns" :key="idx">{{ idx }}</span>
|
||||
</div>
|
||||
<template v-for="chief in rowChiefs" :key="chief.officerLevel">
|
||||
<ChiefTurnCard
|
||||
v-if="chief.name"
|
||||
:officer-level-text="chief.officerLevelText"
|
||||
:name="chief.name"
|
||||
:npc-state="chief.npcState"
|
||||
:rows="chief.rows"
|
||||
:compact="true"
|
||||
:selected="chief.officerLevel === selectedChief?.officerLevel"
|
||||
:is-me="chief.officerLevel === data.me.officerLevel"
|
||||
:clickable="true"
|
||||
:turn-time-label="chief.rows[0]?.time"
|
||||
@select="selectedChiefLevel = chief.officerLevel"
|
||||
/>
|
||||
<div v-else class="empty-chief-slot" aria-hidden="true"></div>
|
||||
</template>
|
||||
<div class="overview-turn-index legacy-bg0" :aria-label="`${rowIndex + 1}행 턴 번호`">
|
||||
<span></span><span v-for="idx in data.maxTurns" :key="idx">{{ idx }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -750,20 +762,25 @@ const repeatTurns = async (amount: number) => {
|
||||
.chief-overview-frame {
|
||||
width: 500px;
|
||||
height: 310px;
|
||||
margin-top: -3px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 11px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chief-overview {
|
||||
width: 445px;
|
||||
width: 500px;
|
||||
height: 310px;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.chief-overview-row {
|
||||
width: 500px;
|
||||
height: 155px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 111.25px);
|
||||
grid-auto-rows: 155px;
|
||||
grid-template-columns: 12px repeat(4, 119px) 12px;
|
||||
}
|
||||
.chief-overview :deep(.chief-card) {
|
||||
width: 111.25px;
|
||||
width: 119px;
|
||||
height: 155px;
|
||||
border: 0;
|
||||
border-left: 1px solid #fff;
|
||||
@@ -787,13 +804,34 @@ const repeatTurns = async (amount: number) => {
|
||||
box-sizing: border-box;
|
||||
height: 20px !important;
|
||||
min-height: 20px !important;
|
||||
grid-template-rows: none;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 10px 10px;
|
||||
line-height: 10px;
|
||||
}
|
||||
.chief-overview :deep(.compact-name),
|
||||
.chief-overview :deep(.compact-meta) {
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
}
|
||||
.chief-overview :deep(.row-time),
|
||||
.chief-overview :deep(.row-action) {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.overview-turn-index {
|
||||
display: grid;
|
||||
grid-template-rows: 20px repeat(12, 11.25px);
|
||||
width: 12px;
|
||||
height: 155px;
|
||||
color: #fff;
|
||||
font-size: 0.55rem;
|
||||
line-height: 11.25px;
|
||||
text-align: center;
|
||||
}
|
||||
.overview-turn-index span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.mobile-readonly {
|
||||
width: 404px;
|
||||
height: 420px;
|
||||
@@ -879,7 +917,7 @@ const repeatTurns = async (amount: number) => {
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.chief-overview {
|
||||
grid-template-columns: repeat(4, 111.25px);
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,8 +929,8 @@ const repeatTurns = async (amount: number) => {
|
||||
.chief-grid-row > .empty-chief-slot {
|
||||
height: 384px;
|
||||
}
|
||||
.chief-overview > .empty-chief-slot {
|
||||
width: 111.25px;
|
||||
.chief-overview-row > .empty-chief-slot {
|
||||
width: 119px;
|
||||
height: 155px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -366,7 +366,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
border: 1px solid #767676;
|
||||
background: #6b6b6b;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 13.3333px;
|
||||
}
|
||||
.selector {
|
||||
@@ -470,7 +470,7 @@ const generalImage = (general: General): string => resolveGeneralIconUrl(general
|
||||
background: #6c757d;
|
||||
color: #fff;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-family: Arial, sans-serif;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -305,7 +305,7 @@ onMounted(loadDetail);
|
||||
width: 1000px;
|
||||
margin: 8px auto 0;
|
||||
color: #fff;
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
}
|
||||
@@ -384,7 +384,7 @@ onMounted(loadDetail);
|
||||
padding: 1px 6px;
|
||||
background: buttonface;
|
||||
color: buttontext;
|
||||
font-family: Arial;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 13.3333px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
|
||||
@@ -231,7 +231,7 @@ onMounted(loadDynasty);
|
||||
padding: 1px 6px;
|
||||
background: buttonface;
|
||||
color: buttontext;
|
||||
font-family: Arial;
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 13.3333px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
|
||||
@@ -778,12 +778,7 @@ onMounted(() => {
|
||||
padding: 0 7px;
|
||||
color: #fff;
|
||||
height: 1597px;
|
||||
font:
|
||||
14px/21px Pretendard,
|
||||
'Apple SD Gothic Neo',
|
||||
'Noto Sans KR',
|
||||
'Malgun Gothic',
|
||||
sans-serif;
|
||||
font: 14px/21px var(--sammo-font-sans);
|
||||
}
|
||||
|
||||
.inherit-page.legacy-bg0 {
|
||||
|
||||
@@ -66,11 +66,12 @@ const nationAccess = computed(() => ({
|
||||
}));
|
||||
const nationColor = computed(() => nation.value?.color ?? '#000000');
|
||||
const voteActive = computed(() => Boolean(frontStatus.value?.latestVote));
|
||||
const recordTimeSuffixPattern = /\d{2}:\d{2}(?:<\/>)?\s*$/u;
|
||||
const formatRecord = (entry: { text: string; createdAt?: string | Date }, appendTime = false): string => {
|
||||
if (!appendTime || /\d{2}:\d{2}\s*$/u.test(entry.text)) return formatLog(entry.text);
|
||||
if (!appendTime || recordTimeSuffixPattern.test(entry.text)) return formatLog(entry.text);
|
||||
const time = formatServerDateTime(entry.createdAt, { format: 'hourMinute', fallback: '' });
|
||||
if (!time) return formatLog(entry.text);
|
||||
return formatLog(`${entry.text} ${time}`);
|
||||
return formatLog(`${entry.text} <1>${time}</>`);
|
||||
};
|
||||
|
||||
let surveyNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -451,7 +451,7 @@ onMounted(() => {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic';
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -522,12 +522,7 @@ onMounted(() => void loadPersonnel());
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font:
|
||||
14px/1.3 Pretendard,
|
||||
'Apple SD Gothic Neo',
|
||||
'Noto Sans KR',
|
||||
'Malgun Gothic',
|
||||
sans-serif;
|
||||
font: 14px/1.3 var(--sammo-font-sans);
|
||||
}
|
||||
.legacy-table {
|
||||
width: 1000px;
|
||||
|
||||
@@ -174,12 +174,7 @@ onMounted(load);
|
||||
.secret-page {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
font:
|
||||
14px Pretendard,
|
||||
'Apple SD Gothic Neo',
|
||||
'Noto Sans KR',
|
||||
'Malgun Gothic',
|
||||
sans-serif;
|
||||
font: 14px var(--sammo-font-sans);
|
||||
color: #fff;
|
||||
}
|
||||
.layout {
|
||||
|
||||
@@ -437,12 +437,7 @@ onMounted(() => void loadStratFinan());
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
background: var(--sammo-texture-walnut);
|
||||
font:
|
||||
14px/1.3 Pretendard,
|
||||
'Apple SD Gothic Neo',
|
||||
'Noto Sans KR',
|
||||
'Malgun Gothic',
|
||||
sans-serif;
|
||||
font: 14px/1.3 var(--sammo-font-sans);
|
||||
}
|
||||
.tiptap-compat-controls {
|
||||
display: none;
|
||||
|
||||
@@ -165,7 +165,7 @@ onMounted(() => {
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic';
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
@@ -515,8 +515,6 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
|
||||
|
||||
.select-pool-page {
|
||||
width: 1000px;
|
||||
min-width: 1000px;
|
||||
|
||||
@@ -423,7 +423,7 @@ onMounted(() => {
|
||||
.pageVote {
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic';
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -385,16 +385,16 @@ const start = async () => {
|
||||
background: #142b42 var(--sammo-texture-blue);
|
||||
}
|
||||
.legacy-title {
|
||||
height: 55.6875px;
|
||||
min-height: 68px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 19.1875px;
|
||||
}
|
||||
.close-button {
|
||||
display: block;
|
||||
width: 62px;
|
||||
height: 35.5px;
|
||||
padding: 8px 12px;
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #375a7f;
|
||||
border-radius: 5.25px;
|
||||
background: #375a7f;
|
||||
@@ -404,9 +404,15 @@ const start = async () => {
|
||||
text-decoration: none;
|
||||
}
|
||||
.toolbar {
|
||||
min-height: 36.5px;
|
||||
min-height: 46px;
|
||||
padding: 1px;
|
||||
}
|
||||
.toolbar button {
|
||||
min-width: 72px;
|
||||
height: 44px;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.operator-row,
|
||||
.state-row,
|
||||
.error-row,
|
||||
|
||||
@@ -361,7 +361,7 @@ onMounted(() => {
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic';
|
||||
font-family: var(--sammo-font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -378,10 +378,10 @@ onMounted(async () => {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.history-log:first-of-type {
|
||||
height: 128px;
|
||||
min-height: 128px;
|
||||
}
|
||||
.history-log:last-of-type {
|
||||
height: 65px;
|
||||
min-height: 65px;
|
||||
}
|
||||
.dropdown-compat-buttons {
|
||||
display: none;
|
||||
@@ -441,7 +441,7 @@ onMounted(async () => {
|
||||
order: 4;
|
||||
}
|
||||
.history-log:first-of-type {
|
||||
height: 149px;
|
||||
min-height: 149px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
const sourceRoot = path.resolve(import.meta.dirname, '../src');
|
||||
const fontFamilyDeclaration = /font-family\s*:\s*([^;]+);/g;
|
||||
const fontShorthandDeclaration = /(?:^|[\s{])font\s*:\s*([^;]+);/gm;
|
||||
|
||||
const listStyleSources = async (): Promise<string[]> => {
|
||||
const entries = await readdir(sourceRoot, { recursive: true, withFileTypes: true });
|
||||
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && (entry.name.endsWith('.css') || entry.name.endsWith('.vue')))
|
||||
.map((entry) => path.join(entry.parentPath, entry.name))
|
||||
.sort();
|
||||
};
|
||||
|
||||
void describe('game content font contract', () => {
|
||||
void it('loads Pretendard once from the global stylesheet entry', async () => {
|
||||
const importPattern =
|
||||
/@import url\(['"]https:\/\/cdn\.jsdelivr\.net\/gh\/orioncactus\/pretendard\/dist\/web\/static\/pretendard\.css['"]\);/g;
|
||||
const imports: string[] = [];
|
||||
|
||||
for (const file of await listStyleSources()) {
|
||||
const source = await readFile(file, 'utf8');
|
||||
if (importPattern.test(source)) {
|
||||
imports.push(path.relative(sourceRoot, file));
|
||||
}
|
||||
importPattern.lastIndex = 0;
|
||||
}
|
||||
|
||||
assert.deepEqual(imports, ['assets/main.css']);
|
||||
});
|
||||
|
||||
void it('uses the shared sans token for every explicit content font declaration', async () => {
|
||||
const violations: string[] = [];
|
||||
|
||||
for (const file of await listStyleSources()) {
|
||||
const source = await readFile(file, 'utf8');
|
||||
|
||||
for (const match of source.matchAll(fontFamilyDeclaration)) {
|
||||
const value = match[1]?.trim();
|
||||
if (value !== 'inherit' && value !== 'var(--sammo-font-sans)') {
|
||||
violations.push(`${path.relative(sourceRoot, file)}: font-family: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(fontShorthandDeclaration)) {
|
||||
const value = match[1]?.trim();
|
||||
if (value !== 'inherit' && !value?.includes('var(--sammo-font-sans)')) {
|
||||
violations.push(`${path.relative(sourceRoot, file)}: font: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(violations, []);
|
||||
});
|
||||
});
|
||||
@@ -81,6 +81,12 @@ const table: CommandTable = {
|
||||
{ value: 8, label: '손권 (오 · 단양)' },
|
||||
{ value: 9, label: '조조 (위 · 업)' },
|
||||
],
|
||||
generalTargets: {
|
||||
che_포상: [
|
||||
{ value: 8, label: '손권 (오 · 단양)' },
|
||||
{ value: 10, label: '여포NPC (오 · 단양)' },
|
||||
],
|
||||
},
|
||||
crewTypes: [{ value: 1100, label: '보병' }],
|
||||
armTypes: [
|
||||
{ value: 0, label: '보병' },
|
||||
@@ -165,6 +171,10 @@ test('국가 명령의 도시·국가·장수·자원 인자를 Ref brief로 표
|
||||
for (const [action, args, expected] of cases) {
|
||||
assert.equal(formatReservedCommandBrief('nation', action, args, table), expected, action);
|
||||
}
|
||||
assert.equal(
|
||||
formatReservedCommandBrief('nation', 'che_포상', { destGeneralId: 10, amount: 300, isGold: false }, table),
|
||||
'【여포NPC】 쌀 300 포상'
|
||||
);
|
||||
});
|
||||
|
||||
test('Ref가 getBrief를 재정의하지 않은 명령은 실제 표시명을 유지한다', () => {
|
||||
|
||||
Reference in New Issue
Block a user