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

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), rect: rect(row),
cells: Array.from(row.querySelectorAll('td'), (cell) => ({ cells: Array.from(row.querySelectorAll('td'), (cell) => ({
field: cell.dataset.field, field: cell.dataset.field,
label: getComputedStyle(cell, '::before').content,
gridArea: getComputedStyle(cell).gridArea, gridArea: getComputedStyle(cell).gridArea,
rect: rect(cell), rect: rect(cell),
text: cell.innerText.replace(/\s+/g, ''), 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.setViewportSize({ width: 1000, height: 900 });
await page.goto(route); await page.goto(route);
const desktop = await measure(page, root, table, `${route.replace('/', '-')}-1000`); 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]) { for (const width of [500, 501, 800]) {
await page.setViewportSize({ width, height: 900 }); await page.setViewportSize({ width, height: 900 });
const mobile = await measure(page, root, table, `${route.replace('/', '-')}-${width}`); 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) { for (const row of mobile.rows) {
const turns = row.cells.find((cell) => cell.field === 'turns')!; const turns = row.cells.find((cell) => cell.field === 'turns')!;
expect(turns.rect.width).toBeCloseTo(166, 0); 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()) { for (const [index, cell] of row.cells.entries()) {
const reference = mobile.rows[0]!.cells[index]!; const reference = mobile.rows[0]!.cells[index]!;
expect(cell.gridArea).toBe(reference.gridArea); 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) => ({ const npcColorGenerals = npcColorStates.map((npcState) => ({
...general, ...general,
id: 100 + npcState, id: 100 + npcState,
name: `색상장수${npcState}`, name: `${({ 4: 'ⓖ', 5: '㉥', 6: 'ⓤ' } as Record<number, string>)[npcState] ?? ''}색상장수${npcState}`,
npcState, npcState,
})); }));
const npcColorSecretGenerals = npcColorStates.map((npcState) => ({ const npcColorSecretGenerals = npcColorStates.map((npcState) => ({
id: 100 + npcState, id: 100 + npcState,
name: `색상장수${npcState}`, name: `${({ 4: 'ⓖ', 5: '㉥', 6: 'ⓤ' } as Record<number, string>)[npcState] ?? ''}색상장수${npcState}`,
npcState, npcState,
injury: 0, injury: 0,
baseStats: { leadership: 70, strength: 60, intelligence: 50 }, 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) { for (const npcState of npcColorStates) {
const name = table.locator(`[data-npc-state="${npcState}"] [data-general-name]`); const name = table.locator(`[data-npc-state="${npcState}"] [data-general-name]`);
await expect(name).toBeVisible(); 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) { if (npcState === 0) {
expect(await name.evaluate((element) => (element as HTMLElement).style.color)).toBe(''); expect(await name.evaluate((element) => (element as HTMLElement).style.color)).toBe('');
} else { } else {
@@ -284,7 +284,7 @@ const commandBrief = (command: ReservedCommand): string =>
</td> </td>
<td data-field="defence" data-label="">{{ defenceTrainText(general.defenceTrain) }}</td> <td data-field="defence" data-label="">{{ defenceTrainText(general.defenceTrain) }}</td>
<td data-field="type" data-label="병종">{{ general.crewTypeName ?? '?' }}</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="train" data-label="">{{ general.train ?? '?' }}</td>
<td data-field="atmos" data-label="">{{ general.atmos ?? '?' }}</td> <td data-field="atmos" data-label="">{{ general.atmos ?? '?' }}</td>
<td data-field="turns" class="turns" :class="{ 'turns--reserved': general.turns.length > 0 }"> <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'] { .generals [data-field='crew'] {
grid-area: 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'] { .generals [data-field='train'] {
grid-area: train; grid-area: train;
} }
@@ -50,7 +50,7 @@ const generals = computed(() =>
); );
const closeWindow = () => window.close(); 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);
const injuryDescription = (general: Result['generals'][number]): string => const injuryDescription = (general: Result['generals'][number]): string =>
general.injury > 0 ? `부상 ${general.injury}% · ${injuryInfo(general.injury).text}` : ''; general.injury > 0 ? `부상 ${general.injury}% · ${injuryInfo(general.injury).text}` : '';
@@ -312,12 +312,12 @@ onMounted(load);
> >
</td> </td>
<td data-field="troop" data-label="부대">{{ general.troopName ?? '-' }}</td> <td data-field="troop" data-label="부대">{{ general.troopName ?? '-' }}</td>
<td data-field="gold" data-label="">{{ general.gold }}</td> <td data-field="gold" data-label="">{{ general.gold.toLocaleString('ko-KR') }}</td>
<td data-field="rice" data-label="">{{ general.rice }}</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="city" data-label="도시">{{ general.cityName ?? '-' }}</td>
<td data-field="defence" data-label="">{{ general.defenceTrainText }}</td> <td data-field="defence" data-label="">{{ general.defenceTrainText }}</td>
<td data-field="type" data-label="병종">{{ general.crewTypeName }}</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="train" data-label="">{{ general.train }}</td>
<td data-field="atmos" data-label="">{{ general.atmos }}</td> <td data-field="atmos" data-label="">{{ general.atmos }}</td>
<td data-field="turns" class="turns"> <td data-field="turns" class="turns">
@@ -571,12 +571,12 @@ th,
} }
.list tbody tr { .list tbody tr {
display: grid; display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr)) 166px; grid-template-columns: repeat(24, minmax(0, 1fr)) 166px;
grid-template-areas: grid-template-areas:
'name name name name name name stats stats stats stats stats stats 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 city city city city defence defence 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 rice rice rice rice time time time 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 crew crew crew train train atmos atmos kill kill 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; height: auto;
border: 1px solid gray; border: 1px solid gray;
border-top: 0; border-top: 0;
@@ -601,7 +601,11 @@ th,
display: none; display: none;
} }
.general-level { .general-level {
margin-left: 4px; margin-left: 3px;
font-size: 11px;
line-height: 1;
color: #bbb;
white-space: nowrap;
} }
.list tbody .turns { .list tbody .turns {
font-size: 11px; font-size: 11px;
@@ -637,6 +641,22 @@ th,
.list [data-field='crew'] { .list [data-field='crew'] {
grid-area: 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'] { .list [data-field='train'] {
grid-area: 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 with reserved commands; current-city portraits remain 64px. Both lists reserve
166px of the 500px table for commands and 332px for information (plus borders). 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 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 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 widths 1000, 500, 501 and 800. The mixed-list height must stay below 1.5 times the