merge: 최신 원격 main을 사령부 지도 복구에 통합

This commit is contained in:
2026-08-21 02:27:13 +00:00
19 changed files with 577 additions and 53 deletions
+37 -1
View File
@@ -751,7 +751,13 @@ const chiefCenter = {
npcState: officerLevel === 8 ? 2 : 0,
turnTime: null,
revision: 0,
turns: turns(12),
turns:
officerLevel === 12
? [
{ index: 0, action: 'che_포상', args: { destGeneralId: 2, isGold: false, amount: 300 } },
...turns(11).map((turn) => ({ ...turn, index: turn.index + 1 })),
]
: turns(12),
})),
};
@@ -1519,6 +1525,36 @@ test('enters general and nation command arguments and sends exact values', async
expect(Number.parseFloat(geometry.fontSize)).toBeGreaterThanOrEqual(10);
});
test('shows full nation command briefs in every chief card', async ({ page }) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('/che/chief-center');
const desktopSummary = page.locator('.layout-desktop .chief-card').first().locator('.row-action').first();
await expect(desktopSummary).toHaveText('【관우】 쌀 300 포상');
await expect(desktopSummary).toHaveAttribute('title', '【관우】 쌀 300 포상');
await page.screenshot({ path: test.info().outputPath('chief-card-command-brief-desktop-1200.png'), fullPage: true });
await page.setViewportSize({ width: 500, height: 900 });
const mobileSummary = page.locator('.chief-overview .chief-card').first().locator('.row-action').first();
await expect(mobileSummary).toHaveText('【관우】 쌀 300 포상');
await expect(mobileSummary).toHaveAttribute('title', '【관우】 쌀 300 포상');
const geometry = await mobileSummary.evaluate((element) => {
const rect = element.getBoundingClientRect();
const card = element.closest<HTMLElement>('.chief-card');
if (!card) throw new Error('chief card is missing');
return {
width: rect.width,
cardWidth: card.getBoundingClientRect().width,
horizontalOverflow: card.scrollWidth - card.clientWidth,
};
});
expect(geometry.width).toBeLessThanOrEqual(geometry.cardWidth);
expect(geometry.horizontalOverflow).toBeLessThanOrEqual(0);
await page.screenshot({ path: test.info().outputPath('chief-card-command-brief-mobile-500.png'), fullPage: true });
});
test('uses a Ref-style full recruitment page without horizontal overflow on desktop or mobile', async ({
page,
}, testInfo) => {
+58 -2
View File
@@ -48,6 +48,7 @@ type NavigationFixture = {
accessLimitAfterCalls?: number;
largeCommandTable?: boolean;
draftCommandTable?: boolean;
equipmentItemOptions?: Array<{ value: string; label: string; description?: string }>;
refCommandCategories?: boolean;
currentYear?: number;
currentMonth?: number;
@@ -235,6 +236,7 @@ const draftCommandGroups = [
options: [
{ value: 'horse', label: '명마' },
{ value: 'weapon', label: '무기' },
{ value: 'item', label: '도구' },
],
},
{
@@ -250,7 +252,13 @@ const draftCommandGroups = [
},
];
const commandTableFixture = (large: boolean, blockedCount = 0, refCategories = false, draftCommands = false) => ({
const commandTableFixture = (
large: boolean,
blockedCount = 0,
refCategories = false,
draftCommands = false,
equipmentItemOptions?: Array<{ value: string; label: string; description?: string }>
) => ({
general: draftCommands
? draftCommandGroups
: refCategories
@@ -309,6 +317,7 @@ const commandTableFixture = (large: boolean, blockedCount = 0, refCategories = f
{ value: 'None', label: '없음' },
{ value: '청룡언월도', label: '청룡언월도' },
],
item: equipmentItemOptions ?? [{ value: 'None', label: '없음' }],
}
: {},
context: draftCommands
@@ -595,7 +604,8 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
state.largeCommandTable === true,
state.commandBlockedCount,
state.refCommandCategories === true,
state.draftCommandTable === true
state.draftCommandTable === true,
state.equipmentItemOptions
),
}
: input.known.commandTable === currentCommandTableRevision
@@ -3455,6 +3465,52 @@ for (const viewport of [
});
}
for (const viewport of [
{ name: 'desktop', width: 1200, height: 900 },
{ name: 'mobile', width: 500, height: 900 },
] as const) {
test(`renders only scenario-scoped equipment items on ${viewport.name}`, async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
draftCommandTable: true,
equipmentItemOptions: [
{ value: 'None', label: '판매/해제' },
{
value: 'che_치료_환약',
label: '환약',
description: '현재 구입 가능 · 가격 100 · 부상 회복',
},
],
reservedTurns: Array.from({ length: 30 }, (_, index) => ({ index, action: '휴식', args: {} })),
};
await installFixture(page, state);
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await waitForMain(page);
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
const picker = page.getByTestId('command-picker');
await picker.getByRole('button', { name: '국가', exact: true }).click();
await picker.getByRole('button', { name: '장비 매매', exact: true }).click();
await picker.getByLabel('장비 종류', { exact: true }).selectOption('item');
const itemSelect = picker.getByLabel('장비', { exact: true });
await expect(itemSelect.locator('option')).toHaveText(['판매/해제', '환약']);
await expect(itemSelect.locator('option', { hasText: '비급' })).toHaveCount(0);
await itemSelect.selectOption('che_치료_환약');
await expect(picker).toContainText('현재 구입 가능 · 가격 100 · 부상 회복');
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth))
.toBeLessThanOrEqual(viewport.width);
await picker.screenshot({ path: test.info().outputPath(`scenario-item-shop-${viewport.name}.png`) });
});
}
test('realtime read-model events skip clock-only work, merge bursts, patch in place, and stop off-route', async ({
page,
}) => {
@@ -70,7 +70,7 @@ const handleClick = () => {
<div v-for="row in props.rows" :key="row.index" class="chief-row" :class="{ rest: row.isRest }">
<span class="row-index">#{{ row.index + 1 }}</span>
<span class="row-time">{{ row.time }}</span>
<span class="row-action">{{ row.action }}</span>
<span class="row-action" :title="row.action">{{ row.action }}</span>
</div>
</div>
</article>
@@ -8,6 +8,7 @@ import ChiefTurnCard from '../components/chief/ChiefTurnCard.vue';
import ChiefCommandEditor from '../components/chief/ChiefCommandEditor.vue';
import { trpc } from '../utils/trpc';
import { formatOfficerLevelText } from '../utils/nationFormat';
import { formatReservedCommandBrief } from '../components/command/reservedCommandBrief';
import type { CommandMapData, CommandMapLayout, CommandPatternEntry, CommandTable } from '../components/command/types';
type ChiefTurn = {
@@ -219,7 +220,10 @@ const buildTurnRows = (chief: ChiefEntry): TurnRow[] => {
? `${String(turnDate.getUTCHours()).padStart(2, '0')}:${String(turnDate.getUTCMinutes()).padStart(2, '0')}`
: `${String(turnDate.getUTCMinutes()).padStart(2, '0')}:${String(turnDate.getUTCSeconds()).padStart(2, '0')}`
: '--:--';
const actionLabel = labelMap.get(turn.action) ?? turn.action;
const actionLabel =
formatReservedCommandBrief('nation', turn.action, turn.args, commandTable.value) ??
labelMap.get(turn.action) ??
turn.action;
return {
index: turn.index,
time: timeLabel,