모바일 장수 정보 묶음과 숫자 가독성 및 의병 표기 개선

This commit is contained in:
2026-09-12 16:38:32 +00:00
parent 35f587768e
commit 23f1573bfc
5 changed files with 77 additions and 14 deletions
@@ -234,6 +234,7 @@ const measure = async (page: Page, root: string, table: string, name: string) =>
rect: rect(row),
cells: Array.from(row.querySelectorAll('td'), (cell) => ({
field: cell.dataset.field,
label: getComputedStyle(cell, '::before').content,
gridArea: getComputedStyle(cell).gridArea,
rect: rect(cell),
text: cell.innerText.replace(/\s+/g, ''),
@@ -276,6 +277,12 @@ for (const [route, root, table] of [
await page.setViewportSize({ width: 1000, height: 900 });
await page.goto(route);
const desktop = await measure(page, root, table, `${route.replace('/', '-')}-1000`);
const firstGeneral = page.locator(`${table} tbody tr`).first();
await expect(firstGeneral.locator('[data-field="crew"]')).toHaveText('12,345');
if (route === 'nation/secret') {
await expect(firstGeneral.locator('[data-field="gold"]')).toHaveText('123,456,789');
await expect(firstGeneral.locator('[data-field="rice"]')).toHaveText('234,567');
}
for (const width of [500, 501, 800]) {
await page.setViewportSize({ width, height: 900 });
const mobile = await measure(page, root, table, `${route.replace('/', '-')}-${width}`);
@@ -293,6 +300,18 @@ for (const [route, root, table] of [
for (const row of mobile.rows) {
const turns = row.cells.find((cell) => cell.field === 'turns')!;
expect(turns.rect.width).toBeCloseTo(166, 0);
const train = row.cells.find((cell) => cell.field === 'train')!;
const atmos = row.cells.find((cell) => cell.field === 'atmos')!;
expect(train.label).toBe('"훈/사"');
expect(atmos.label).toBe('"/"');
expect(train.rect.y).toBe(atmos.rect.y);
expect(train.rect.x + train.rect.width).toBeCloseTo(atmos.rect.x, 1);
if (route === 'nation/secret') {
const gold = row.cells.find((cell) => cell.field === 'gold')!;
const rice = row.cells.find((cell) => cell.field === 'rice')!;
expect(gold.rect.width).toBeCloseTo(rice.rect.width, 1);
expect(gold.rect.y).toBe(rice.rect.y);
}
for (const [index, cell] of row.cells.entries()) {
const reference = mobile.rows[0]!.cells[index]!;
expect(cell.gridArea).toBe(reference.gridArea);
@@ -126,12 +126,12 @@ const npcColorStates = [0, 1, 2, 4, 5, 6] as const;
const npcColorGenerals = npcColorStates.map((npcState) => ({
...general,
id: 100 + npcState,
name: `색상장수${npcState}`,
name: `${({ 4: 'ⓖ', 5: '㉥', 6: 'ⓤ' } as Record<number, string>)[npcState] ?? ''}색상장수${npcState}`,
npcState,
}));
const npcColorSecretGenerals = npcColorStates.map((npcState) => ({
id: 100 + npcState,
name: `색상장수${npcState}`,
name: `${({ 4: 'ⓖ', 5: '㉥', 6: 'ⓤ' } as Record<number, string>)[npcState] ?? ''}색상장수${npcState}`,
npcState,
injury: 0,
baseStats: { leadership: 70, strength: 60, intelligence: 50 },
@@ -292,6 +292,9 @@ test('NPC general names use the complete Ref palette in secret and nation lists
for (const npcState of npcColorStates) {
const name = table.locator(`[data-npc-state="${npcState}"] [data-general-name]`);
await expect(name).toBeVisible();
if (npcState === 4) await expect(name).toHaveText('ⓖ색상장수4');
if (npcState === 5) await expect(name).toHaveText('㉥색상장수5');
if (npcState === 6) await expect(name).toHaveText('ⓤ색상장수6');
if (npcState === 0) {
expect(await name.evaluate((element) => (element as HTMLElement).style.color)).toBe('');
} else {
@@ -284,7 +284,7 @@ const commandBrief = (command: ReservedCommand): string =>
</td>
<td data-field="defence" data-label="">{{ defenceTrainText(general.defenceTrain) }}</td>
<td data-field="type" data-label="병종">{{ general.crewTypeName ?? '?' }}</td>
<td data-field="crew" data-label="병사">{{ general.crew ?? '?' }}</td>
<td data-field="crew" data-label="병사">{{ show(general.crew) }}</td>
<td data-field="train" data-label="">{{ general.train ?? '?' }}</td>
<td data-field="atmos" data-label="">{{ general.atmos ?? '?' }}</td>
<td data-field="turns" class="turns" :class="{ 'turns--reserved': general.turns.length > 0 }">
@@ -590,6 +590,22 @@ const commandBrief = (command: ReservedCommand): string =>
.generals [data-field='crew'] {
grid-area: crew;
}
/* 같은 척도의 훈련·사기는 한 쌍으로 읽되 원래 데이터 셀과 값을 보존한다. */
.generals tbody td[data-field='train'] {
text-align: right;
padding-right: 0;
}
.generals tbody td[data-field='train']::before {
content: '훈/사';
}
.generals tbody td[data-field='atmos'] {
text-align: left;
padding-left: 0;
}
.generals tbody td[data-field='atmos']::before {
content: '/';
margin-inline: 3px;
}
.generals [data-field='train'] {
grid-area: train;
}
@@ -50,7 +50,7 @@ const generals = computed(() =>
);
const closeWindow = () => window.close();
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 injuryDescription = (general: Result['generals'][number]): string =>
general.injury > 0 ? `부상 ${general.injury}% · ${injuryInfo(general.injury).text}` : '';
@@ -312,12 +312,12 @@ onMounted(load);
>
</td>
<td data-field="troop" data-label="부대">{{ general.troopName ?? '-' }}</td>
<td data-field="gold" data-label="">{{ general.gold }}</td>
<td data-field="rice" data-label="">{{ general.rice }}</td>
<td data-field="gold" data-label="">{{ general.gold.toLocaleString('ko-KR') }}</td>
<td data-field="rice" data-label="">{{ general.rice.toLocaleString('ko-KR') }}</td>
<td data-field="city" data-label="도시">{{ general.cityName ?? '-' }}</td>
<td data-field="defence" data-label="">{{ general.defenceTrainText }}</td>
<td data-field="type" data-label="병종">{{ general.crewTypeName }}</td>
<td data-field="crew" data-label="병사">{{ general.crew }}</td>
<td data-field="crew" data-label="병사">{{ general.crew.toLocaleString('ko-KR') }}</td>
<td data-field="train" data-label="">{{ general.train }}</td>
<td data-field="atmos" data-label="">{{ general.atmos }}</td>
<td data-field="turns" class="turns">
@@ -571,12 +571,12 @@ th,
}
.list tbody tr {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr)) 166px;
grid-template-columns: repeat(24, minmax(0, 1fr)) 166px;
grid-template-areas:
'name name name name name name stats stats stats stats stats stats turns'
'troop troop troop troop troop troop city city city city defence defence turns'
'gold gold gold gold gold rice rice rice rice time time time turns'
'type type type crew crew crew train train atmos atmos kill kill turns';
'name name name name name name name name name name name name stats stats stats stats stats stats stats stats stats stats stats stats turns'
'troop troop troop troop troop troop troop troop troop troop troop troop city city city city city city city city defence defence defence defence turns'
'gold gold gold gold gold gold gold gold gold rice rice rice rice rice rice rice rice rice time time time time time time turns'
'type type type type type type crew crew crew crew crew crew train train train train train atmos atmos atmos kill kill kill kill turns';
height: auto;
border: 1px solid gray;
border-top: 0;
@@ -601,7 +601,11 @@ th,
display: none;
}
.general-level {
margin-left: 4px;
margin-left: 3px;
font-size: 11px;
line-height: 1;
color: #bbb;
white-space: nowrap;
}
.list tbody .turns {
font-size: 11px;
@@ -637,6 +641,22 @@ th,
.list [data-field='crew'] {
grid-area: crew;
}
/* 같은 척도의 훈련·사기는 한 쌍으로 읽되 원래 데이터 셀과 값을 보존한다. */
.list tbody td[data-field='train'] {
text-align: right;
padding-right: 0;
}
.list tbody td[data-field='train']::before {
content: '훈/사';
}
.list tbody td[data-field='atmos'] {
text-align: left;
padding-left: 0;
}
.list tbody td[data-field='atmos']::before {
content: '/';
margin-inline: 3px;
}
.list [data-field='train'] {
grid-area: train;
}
+6 -1
View File
@@ -128,7 +128,12 @@ cell, command and image from the desktop dataset. Statistics share vertical spac
with reserved commands; current-city portraits remain 64px. Both lists reserve
166px of the 500px table for commands and 332px for information (plus borders).
Secret rows use the same four-line field positions for players, NPCs and empty
reservations. Long content wraps without truncation.
reservations. Long content wraps without truncation. Gold and rice have equal
widths; training and morale form a compact `훈/사 value / value` pair in both
views. The secret level stays visible as secondary 11px text. Existing decorated
NPC names, including volunteer `ⓖ` and `ⓤ`, are not prefixed with an extra `ⓝ`.
Crew, gold and rice values use Korean-locale thousands separators in both modes;
redacted current-city crew values remain `?`.
The fixture measures 100 generals with five-command and NPC rows at CSS viewport
widths 1000, 500, 501 and 800. The mixed-list height must stay below 1.5 times the