From be8340a98575f445d5e5751d0b04d0bc7454a21a Mon Sep 17 00:00:00 2001 From: hided62 Date: Fri, 21 Aug 2026 15:54:26 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=ED=86=A0=EB=84=88=EB=A8=BC=ED=8A=B8?= =?UTF-8?q?=20=EB=8B=A8=EA=B3=84=EB=A5=BC=20=EC=88=9C=EC=B0=A8=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EA=B3=A0=20=EC=B0=B8=EA=B0=80=20=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20=ED=91=9C=EC=8B=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 예선, 본선, 16강 영역을 실제 진행 단계부터 누적해서 보여준다. 참가 신청이 성공하면 배정된 예선 조를 선택하고 해당 표로 이동한다. --- .../e2e/tournamentBracket.spec.ts | 79 ++++- .../src/utils/tournamentStatus.ts | 15 + .../src/views/TournamentView.vue | 320 ++++++++++-------- .../test/tournamentStatus.test.ts | 30 +- 4 files changed, 292 insertions(+), 152 deletions(-) diff --git a/app/game-frontend/e2e/tournamentBracket.spec.ts b/app/game-frontend/e2e/tournamentBracket.spec.ts index 84782d31..b0db345f 100644 --- a/app/game-frontend/e2e/tournamentBracket.spec.ts +++ b/app/game-frontend/e2e/tournamentBracket.spec.ts @@ -126,7 +126,15 @@ const persistScreenshot = async (page: Page, name: string, fallbackPath: string) await page.screenshot({ path: resolve(responsiveArtifactDir, `${name}.webp`), fullPage: true }); }; -const installFixture = async (page: Page, options: { applicationOpen?: boolean; tournamentType?: number } = {}) => { +const installFixture = async ( + page: Page, + options: { + applicationOpen?: boolean; + tournamentType?: number; + tournamentStage?: number; + joinedGroupId?: number; + } = {} +) => { let joined = false; await page.addInitScript((profile) => { window.localStorage.setItem('sammo-game-token', 'ga_tournament_bracket_playwright'); @@ -148,9 +156,11 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean; if (operation === 'general.me') return response({ general: { id: 1, name: names[0] } }); if (operation === 'tournament.getAdminStatus') return response({ ok: false }); if (operation === 'tournament.getSnapshot') { + const tournamentStage = options.tournamentStage ?? (options.applicationOpen ? 1 : 0); + const joinedGroupId = options.joinedGroupId ?? 0; return response({ state: { - stage: options.applicationOpen ? 1 : 0, + stage: tournamentStage, phase: 0, type: options.tournamentType ?? 0, auto: false, @@ -158,7 +168,7 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean; openMonth: 1, termSeconds: 60, nextAt: '2026-08-02T00:00:00.000Z', - winnerId: 1, + winnerId: tournamentStage === 0 ? 1 : undefined, }, participants: options.applicationOpen && !joined @@ -167,8 +177,10 @@ const installFixture = async (page: Page, options: { applicationOpen?: boolean; ? [ { ...participants[0], - groupId: 0, + groupId: joinedGroupId, groupNo: 0, + preliminaryGroupId: joinedGroupId, + preliminaryGroupNo: 0, win: 0, draw: 0, lose: 0, @@ -322,20 +334,36 @@ test('desktop bracket connects every real general slot to the next round', async 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 }) => { +test('join refresh shows the assigned preliminary group immediately with accessible controls', async ({ + page, +}, testInfo) => { await page.setViewportSize({ width: 390, height: 844 }); - await installFixture(page, { applicationOpen: true }); + await installFixture(page, { applicationOpen: true, joinedGroupId: 5 }); 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 expect(page.getByText('조별 예선 순위')).toBeVisible(); + await expect(page.getByText('조별 본선 순위')).toHaveCount(0); + await expect(page.getByLabel('토너먼트 대진표')).toHaveCount(0); await join.click(); - await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다.'); + await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다. 六조에 배정되었습니다.'); await expect(join).toBeDisabled(); - await expect(page.locator('.preliminary-grid .general-identity', { hasText: names[0] })).toBeVisible(); + const preliminaryTabs = page.getByRole('tablist', { name: '예선 조 선택' }); + await expect(preliminaryTabs.getByRole('tab').nth(5)).toHaveAttribute('aria-selected', 'true'); + const assignedGroup = page.locator('[data-preliminary-group="5"]'); + await expect(assignedGroup.locator('.general-identity', { hasText: names[0] })).toBeVisible(); + const assignedGroupBounds = await assignedGroup.boundingBox(); + expect(assignedGroupBounds?.y).toBeLessThan(844); + expect((assignedGroupBounds?.y ?? 0) + (assignedGroupBounds?.height ?? 0)).toBeGreaterThan(0); + await persistScreenshot( + page, + 'tournament-joined-group-mobile', + testInfo.outputPath('tournament-joined-group.webp') + ); for (const control of [refresh, join, close]) { const box = await control.boundingBox(); @@ -349,6 +377,41 @@ test('join refresh shows the assigned preliminary group immediately with accessi expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); }); +test('desktop join scrolls the assigned preliminary group into view without future sections', async ({ + page, +}, testInfo) => { + await page.setViewportSize({ width: 1365, height: 900 }); + await installFixture(page, { applicationOpen: true, joinedGroupId: 7 }); + await page.goto('tournament'); + + await expect(page.getByText('조별 본선 순위')).toHaveCount(0); + await expect(page.getByLabel('토너먼트 대진표')).toHaveCount(0); + await page.getByRole('button', { name: '참가' }).click(); + + await expect(page.getByRole('status')).toHaveText('참가 신청이 반영되었습니다. 八조에 배정되었습니다.'); + const assignedGroup = page.locator('[data-preliminary-group="7"]'); + await expect(assignedGroup.locator('.general-identity', { hasText: names[0] })).toBeVisible(); + const bounds = await assignedGroup.boundingBox(); + expect(bounds?.y).toBeLessThan(900); + expect((bounds?.y ?? 0) + (bounds?.height ?? 0)).toBeGreaterThan(0); + await persistScreenshot( + page, + 'tournament-joined-group-desktop', + testInfo.outputPath('tournament-joined-group.webp') + ); +}); + +test('final group section appears before the later knockout section', async ({ page }, testInfo) => { + await page.setViewportSize({ width: 390, height: 844 }); + await installFixture(page, { tournamentStage: 3 }); + await page.goto('tournament'); + + await expect(page.getByText('조별 예선 순위')).toBeVisible(); + await expect(page.getByText('조별 본선 순위')).toBeVisible(); + await expect(page.getByLabel('토너먼트 대진표')).toHaveCount(0); + await persistScreenshot(page, 'tournament-final-stage-mobile', testInfo.outputPath('tournament-final-stage.webp')); +}); + test('mobile bracket exposes every round through tabs with standard horizontal identities', async ({ page, }, testInfo) => { diff --git a/app/game-frontend/src/utils/tournamentStatus.ts b/app/game-frontend/src/utils/tournamentStatus.ts index 138501e7..529e8941 100644 --- a/app/game-frontend/src/utils/tournamentStatus.ts +++ b/app/game-frontend/src/utils/tournamentStatus.ts @@ -13,3 +13,18 @@ export const tournamentStageNames = [ ] as const; export const resolveTournamentStageName = (stage: number): string => tournamentStageNames[stage] ?? '상태 확인 중'; + +export interface TournamentSectionVisibility { + preliminary: boolean; + final: boolean; + knockout: boolean; +} + +export const resolveTournamentSectionVisibility = (stage: number, winnerId?: number): TournamentSectionVisibility => { + const completed = stage === 0 && winnerId !== undefined; + return { + preliminary: stage >= 1 || completed, + final: stage >= 3 || completed, + knockout: stage >= 5 || completed, + }; +}; diff --git a/app/game-frontend/src/views/TournamentView.vue b/app/game-frontend/src/views/TournamentView.vue index e21b9b16..c1ea2f71 100644 --- a/app/game-frontend/src/views/TournamentView.vue +++ b/app/game-frontend/src/views/TournamentView.vue @@ -1,11 +1,11 @@ - 명마{{ itemNames.horse ?? '-' }} - 무기{{ itemNames.weapon ?? '-' }} - 서적{{ itemNames.book ?? '-' }} + 명마 + + + {{ itemNames.horse ?? '-' }} + + + 무기 + + + {{ itemNames.weapon ?? '-' }} + + + 서적 + + + {{ itemNames.book ?? '-' }} + + { /> 자금{{ props.general.gold.toLocaleString('ko-KR') }} 군량{{ props.general.rice.toLocaleString('ko-KR') }} - 도구{{ itemNames.item ?? '-' }} + 도구 + + + {{ itemNames.item ?? '-' }} + + - 병종{{ props.general.crewTypeName ?? '-' }} + 병종 + + + {{ props.general.crewTypeName ?? '-' }} + + + 병사{{ props.general.crew.toLocaleString('ko-KR') }} - 성격{{ props.general.traits?.personal ?? '-' }} + 성격 + + + {{ props.general.traits?.personal ?? '-' }} + + 훈련{{ props.general.train }} 사기{{ props.general.atmos }} - 특기{{ specialText }} + 특기 + + + {{ props.general.traits?.specialDomestic ?? '-' }} + + / + + {{ props.general.traits?.specialWar ?? '-' }} + + Lv {{ props.general.progression?.experienceLevel ?? 0 }} diff --git a/app/game-frontend/src/components/main/NationBasicCard.vue b/app/game-frontend/src/components/main/NationBasicCard.vue index ba738b29..478df508 100644 --- a/app/game-frontend/src/components/main/NationBasicCard.vue +++ b/app/game-frontend/src/components/main/NationBasicCard.vue @@ -1,5 +1,6 @@ + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d89f226..3400c45f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,6 +207,9 @@ importers: pinia: specifier: ^3.0.4 version: 3.0.4(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)) + tippy.js: + specifier: 6.3.7 + version: 6.3.7 vue: specifier: ^3.5.26 version: 3.5.41(typescript@6.0.3) @@ -1347,6 +1350,9 @@ packages: '@pm2/pm2-version-check@1.0.4': resolution: {integrity: sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@prisma/adapter-pg@7.9.1': resolution: {integrity: sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==} @@ -4489,6 +4495,9 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5496,6 +5505,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@popperjs/core@2.11.8': {} + '@prisma/adapter-pg@7.9.1': dependencies: '@prisma/driver-adapter-utils': 7.9.1 @@ -8517,6 +8528,10 @@ snapshots: tinyrainbow@3.1.1: {} + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0