From 35f587768e2db1e4cc76df321645316774668fae Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 12 Sep 2026 16:15:10 +0000 Subject: [PATCH] =?UTF-8?q?=EC=95=94=ED=96=89=EB=B6=80=EC=99=80=20?= =?UTF-8?q?=ED=98=84=EC=9E=AC=20=EB=8F=84=EC=8B=9C=20=EB=AA=A8=EB=B0=94?= =?UTF-8?q?=EC=9D=BC=20=EB=AA=85=EB=A0=B9=20=ED=8F=AD=EC=9D=84=20=EC=A4=84?= =?UTF-8?q?=EC=9D=B4=EA=B3=A0=20=EC=9E=A5=EC=88=98=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/infoMobileLayout.spec.ts | 22 ++++- .../src/views/CurrentCityView.vue | 3 +- .../src/views/NationSecretView.vue | 86 ++----------------- docs/frontend-legacy-parity.md | 11 ++- 4 files changed, 39 insertions(+), 83 deletions(-) diff --git a/app/game-frontend/e2e/infoMobileLayout.spec.ts b/app/game-frontend/e2e/infoMobileLayout.spec.ts index 76068d2c..34003356 100644 --- a/app/game-frontend/e2e/infoMobileLayout.spec.ts +++ b/app/game-frontend/e2e/infoMobileLayout.spec.ts @@ -233,6 +233,8 @@ const measure = async (page: Page, root: string, table: string, name: string) => rows: rows.map((row) => ({ rect: rect(row), cells: Array.from(row.querySelectorAll('td'), (cell) => ({ + field: cell.dataset.field, + gridArea: getComputedStyle(cell).gridArea, rect: rect(cell), text: cell.innerText.replace(/\s+/g, ''), scrollWidth: cell.scrollWidth, @@ -287,6 +289,17 @@ for (const [route, root, table] of [ expect(mobile.rows.map((row) => row.cells.map((cell) => cell.text))).toEqual( desktop.rows.map((row) => row.cells.map((cell) => cell.text)) ); + // Every general shares the same columns, including NPCs and empty reservations. + for (const row of mobile.rows) { + const turns = row.cells.find((cell) => cell.field === 'turns')!; + expect(turns.rect.width).toBeCloseTo(166, 0); + for (const [index, cell] of row.cells.entries()) { + const reference = mobile.rows[0]!.cells[index]!; + expect(cell.gridArea).toBe(reference.gridArea); + expect(cell.rect.x).toBeCloseTo(reference.rect.x, 1); + expect(cell.rect.width).toBeCloseTo(reference.rect.width, 1); + } + } for (const row of mobile.rows) for (const cell of row.cells) { expect(cell.rect.width).toBeGreaterThan(0); @@ -378,7 +391,14 @@ test('idle and empty information lists stay compact; doubled text wraps without const desktop = await measure(page, root, table, `idle-${route.replace('/', '-')}-1000`); await page.setViewportSize({ width: 500, height: 900 }); const mobile = await measure(page, root, table, `idle-${route.replace('/', '-')}-500`); - expect(mobile.table.height / desktop.table.height).toBeLessThan(1.8); + if (route === 'nation/secret') { + // The uniform four-line layout intentionally replaces the former NPC-only two-line layout. + // Bound the ordinary row directly; desktop idle rows have no command height to share. + for (const row of mobile.rows.slice(1)) expect(row.rect.height).toBeLessThan(76); + expect(mobile.table.height).toBeLessThan(7600); + } else { + expect(mobile.table.height / desktop.table.height).toBeLessThan(1.8); + } expect(mobile.document.width).toBe(500); await page.unroute(gameTrpcRoute); await install(page); diff --git a/app/game-frontend/src/views/CurrentCityView.vue b/app/game-frontend/src/views/CurrentCityView.vue index 794d7c02..28b7bcc6 100644 --- a/app/game-frontend/src/views/CurrentCityView.vue +++ b/app/game-frontend/src/views/CurrentCityView.vue @@ -514,7 +514,7 @@ const commandBrief = (command: ReservedCommand): string => } .generals tbody tr { display: grid; - grid-template-columns: 64px 62px 68px 40px 40px minmax(0, 1fr); + grid-template-columns: 64px 74px 90px 52px 52px 166px; grid-template-areas: 'icon name name name name turns' 'icon lead str intel intel turns' @@ -539,6 +539,7 @@ const commandBrief = (command: ReservedCommand): string => content: attr(data-label); margin-right: 3px; font-size: 11px; + line-height: 1; color: #bbb; } .generals [data-field='train'], diff --git a/app/game-frontend/src/views/NationSecretView.vue b/app/game-frontend/src/views/NationSecretView.vue index 3f192c8a..16d571c4 100644 --- a/app/game-frontend/src/views/NationSecretView.vue +++ b/app/game-frontend/src/views/NationSecretView.vue @@ -241,7 +241,6 @@ onMounted(load); v-for="general in generals" :key="general.id" :data-general-id="general.id" - :class="{ 'has-commands': general.npcState < 2 && general.reservedCommands.length > 0 }" :data-npc-state="general.npcState" > @@ -525,8 +524,7 @@ th, } } -/* 500px에서는 명령을 숨기지 않고 그 옆 높이를 통계에 사용한다. - * 예약이 없는 장수는 전체 폭 2행으로 배치해 빈 명령 칸 때문에 길어지지 않는다. */ +/* 장수 종류와 예약 유무에 관계없이 정보 2/3, 명령 1/3의 같은 배치를 사용한다. */ @media (max-width: 939.98px) { .secret-page { width: 500px; @@ -573,20 +571,16 @@ th, } .list tbody tr { display: grid; - grid-template-columns: repeat(100, minmax(0, 1fr)); + grid-template-columns: repeat(12, 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'; height: auto; border: 1px solid gray; border-top: 0; } - .list tbody tr.has-commands { - grid-template-columns: 64px 70px 70px 70px minmax(0, 1fr); - grid-template-areas: - 'name name stats stats turns' - 'troop troop city city turns' - 'gold gold rice rice turns' - 'type crew train atmos turns' - 'defence kill time time turns'; - } .list tbody td { min-width: 0; padding: 0 2px; @@ -600,6 +594,7 @@ th, content: attr(data-label); margin-right: 3px; font-size: 11px; + line-height: 1; color: #bbb; } .list tbody td[data-field='name'] br { @@ -657,70 +652,5 @@ th, .list [data-field='time'] { grid-area: time; } - /* 예약이 없으면 서로 다른 열폭을 가진 2행을 100등분 grid 위에 놓는다. - * 첫 행: 이름/능력/부대/도시/NPC, 둘째 행: 자원/병력/훈사/턴. */ - .list tbody tr:not(.has-commands) td { - grid-area: auto; - padding-inline: 1px; - } - .list tbody tr:not(.has-commands) td::before { - margin-right: 2px; - } - .list tbody tr:not(.has-commands) [data-field='name'] { - grid-row: 1; - grid-column: 1 / span 27; - } - .list tbody tr:not(.has-commands) [data-field='stats'] { - grid-row: 1; - grid-column: 28 / span 28; - } - .list tbody tr:not(.has-commands) [data-field='troop'] { - grid-row: 1; - grid-column: 56 / span 18; - } - .list tbody tr:not(.has-commands) [data-field='city'] { - grid-row: 1; - grid-column: 74 / span 12; - } - .list tbody tr:not(.has-commands) [data-field='turns'] { - grid-row: 1; - grid-column: 86 / span 15; - } - .list tbody tr:not(.has-commands) [data-field='gold'] { - grid-row: 2; - grid-column: 1 / span 19; - } - .list tbody tr:not(.has-commands) [data-field='rice'] { - grid-row: 2; - grid-column: 20 / span 16; - } - .list tbody tr:not(.has-commands) [data-field='defence'] { - grid-row: 2; - grid-column: 36 / span 7; - } - .list tbody tr:not(.has-commands) [data-field='type'] { - grid-row: 2; - grid-column: 43 / span 11; - } - .list tbody tr:not(.has-commands) [data-field='crew'] { - grid-row: 2; - grid-column: 54 / span 14; - } - .list tbody tr:not(.has-commands) [data-field='train'] { - grid-row: 2; - grid-column: 68 / span 8; - } - .list tbody tr:not(.has-commands) [data-field='atmos'] { - grid-row: 2; - grid-column: 76 / span 8; - } - .list tbody tr:not(.has-commands) [data-field='kill'] { - grid-row: 2; - grid-column: 84 / span 6; - } - .list tbody tr:not(.has-commands) [data-field='time'] { - grid-row: 2; - grid-column: 90 / span 11; - } } diff --git a/docs/frontend-legacy-parity.md b/docs/frontend-legacy-parity.md index 465e9e1a..37364577 100644 --- a/docs/frontend-legacy-parity.md +++ b/docs/frontend-legacy-parity.md @@ -125,12 +125,17 @@ the selected radio option so the user can retry. `app/game-frontend/e2e/infoMobileLayout.spec.ts` verifies the intentional Core 500px layouts for `/nation/secret` and `/current-city`. Both keep every visible cell, command and image from the desktop dataset. Statistics share vertical space -with reserved commands; current-city portraits remain 64px. Empty reservation -cells in the secret list use a shorter full-width two-row layout. +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. 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 -same desktop list, and page height below 1.55 times; idle lists stay below 1.8 times. +same desktop list, and page height below 1.55 times. Idle current-city lists stay +below 1.8 times; ordinary idle secret rows stay below 76px and the 100-row fixture +below 7600px. The secret idle limit replaces the old NPC-specific two-row limit +to preserve consistent field positions, as requested in the follow-up review. These limits apply to the deterministic fixture, not arbitrary-length user text. It also verifies exact cell text preservation, no horizontal overflow, sorting, injury tooltip interaction, city selection, empty lists, doubled text, and actual