diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index deb96ae77..a98d45225 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -21,7 +21,8 @@ operator-facing control you can click before changing product CSS. | `Workspace/WorkspaceCalendar` | Read observed Naruon events, or open a commitment to land on that post. Fail-closed copy stays `이 범위의 일정을 아직 받을 수 없습니다`. | `--color-chip-border`, `WorkspaceCalendar`, `EvidenceStatusMark` | | `Evidence/OntologyExplorer` | Distinguish Post, Person, Organization, and Team by shape and text, use the token-backed surface as a secondary cue, then open the exact-value table or cited evidence. Compare desktop, narrow, drawer, empty, truncated, denied, stale, and rejected states. | `--ontology-node-*-fill`, `OntologyExplorer` | | `Post/ProductEvidenceList` | Open the cited product span. If the identity is unresolved, review the product catalog before using the relationship. Compare catalog-linked and catalog-review-required states. | `--surface`, `--border`, `ProductEvidenceList` | -| `Dashboard/VoiceTaxonomySummary` | Compare source and semantic classifications, note overlapping memberships, then review disagreements and records waiting for evidence. | `--surface`, `--border`, `VoiceTaxonomySummary` | +| `Dashboard/VoiceTaxonomySummary` | Compare source and semantic classifications, note overlapping memberships, then review disagreements and records waiting for evidence; `KoreanMobile` verifies locale-complete customer copy in the narrow viewport. | `--surface`, `--border`, `VoiceTaxonomySummary` | +| `Navigation/WorkspaceNav` | Reach every workspace destination and the language action; `MobileAllDestinations` keeps all actions visible without horizontal clipping. | `--gnb-height`, `--size-control-min`, `WorkspaceNav` | Repeated web objects must use `frontend/src/styles/tokens.css` and a module under `frontend/src/components/`. Do not add a second Node package manager; diff --git a/frontend/src/App.css b/frontend/src/App.css index 11409d336..50a5d782d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1344,16 +1344,25 @@ /* Phone Breakpoint (<768px) */ .workspace-gnb { - overflow-x: auto; - overscroll-behavior-inline: contain; - gap: 0.75rem; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + height: auto; + gap: 0; padding: 0 1rem; - scrollbar-width: thin; } - .workspace-gnb-item, + .workspace-gnb-item { + min-height: var(--size-control-min); + justify-content: center; + padding: 0 0.25rem; + text-align: center; + } + .workspace-gnb-tools { - flex: 0 0 auto; + grid-column: 1 / -1; + min-height: var(--size-control-min); + margin-left: 0; + justify-content: flex-end; } .mobile-drawer-trigger { diff --git a/frontend/src/components/VoiceTaxonomySummary.stories.tsx b/frontend/src/components/VoiceTaxonomySummary.stories.tsx index 8665a6f6b..38821bd59 100644 --- a/frontend/src/components/VoiceTaxonomySummary.stories.tsx +++ b/frontend/src/components/VoiceTaxonomySummary.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { setLocale } from "../i18n"; import { VoiceTaxonomySummary } from "./VoiceTaxonomySummary"; const meta = { title: "Dashboard/VoiceTaxonomySummary", component: VoiceTaxonomySummary } satisfies Meta; @@ -14,3 +15,12 @@ export const OverlappingEvidence: Story = { args: { data: { { voice_concept_code: "vom", post_count: 4, eligible_percentage: 33.3 }, ], } } }; + +export const KoreanMobile: Story = { + ...OverlappingEvidence, + beforeEach: () => { + setLocale("ko"); + return () => setLocale("en"); + }, + globals: { viewport: { value: "mobile1", isRotated: false } }, +}; diff --git a/frontend/src/components/VoiceTaxonomySummary.test.tsx b/frontend/src/components/VoiceTaxonomySummary.test.tsx index e46a3925b..9e177bd2d 100644 --- a/frontend/src/components/VoiceTaxonomySummary.test.tsx +++ b/frontend/src/components/VoiceTaxonomySummary.test.tsx @@ -1,8 +1,11 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { act, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { setLocale } from "../i18n"; import { VoiceTaxonomySummary } from "./VoiceTaxonomySummary"; describe("VoiceTaxonomySummary", () => { + afterEach(() => setLocale("en")); + it("discloses overlapping counts and the next review action", () => { render( { expect(screen.getByText(/voice categories, so category counts can overlap/)).toBeInTheDocument(); expect(screen.getByText(/Review disagreements and records without voice evidence/)).toBeInTheDocument(); }); + + it("updates every visible label when the product locale changes", () => { + render(); + + act(() => setLocale("ko")); + + expect(screen.getByRole("heading", { name: "글 유형 근거 현황" })).toBeInTheDocument(); + expect(screen.getByText("기록된 근거")).toBeInTheDocument(); + expect(screen.getByText("글 유형 근거가 없는 기록")).toBeInTheDocument(); + expect(screen.getByText("고객의 소리")).toBeInTheDocument(); + expect(screen.getByText(/불일치와 글 유형 근거가 없는 기록을 확인한 뒤/)).toBeInTheDocument(); + expect(screen.queryByText("Voice evidence overview")).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/VoiceTaxonomySummary.tsx b/frontend/src/components/VoiceTaxonomySummary.tsx index adb067bad..d74cff0fa 100644 --- a/frontend/src/components/VoiceTaxonomySummary.tsx +++ b/frontend/src/components/VoiceTaxonomySummary.tsx @@ -1,5 +1,5 @@ import type { VoiceTaxonomySummary as Summary } from "../api"; -import { t, tf } from "../i18n"; +import { t, tf, useLocale } from "../i18n"; const voiceLabels = { voc: "Voice of Customer", @@ -10,6 +10,7 @@ const voiceLabels = { } as const; export function VoiceTaxonomySummary({ data }: { data: Summary }) { + useLocale(); return (

{t("Voice evidence overview")}

diff --git a/frontend/src/components/WorkspaceNav.stories.tsx b/frontend/src/components/WorkspaceNav.stories.tsx index a958b67af..b4b33ee54 100644 --- a/frontend/src/components/WorkspaceNav.stories.tsx +++ b/frontend/src/components/WorkspaceNav.stories.tsx @@ -34,3 +34,11 @@ export const WithTools: Story = { tools: , }, }; + +export const MobileAllDestinations: Story = { + args: { + destination: "dashboard", + tools: , + }, + globals: { viewport: { value: "mobile1", isRotated: false } }, +}; diff --git a/frontend/src/mobileNavigationCss.test.ts b/frontend/src/mobileNavigationCss.test.ts index bd2a37f52..c19848d3e 100644 --- a/frontend/src/mobileNavigationCss.test.ts +++ b/frontend/src/mobileNavigationCss.test.ts @@ -8,6 +8,10 @@ const css = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "App.css" it("keeps the workspace GNB reachable on mobile", () => { const mobile = css.match(/@media \(max-width: 768px\) \{([\s\S]*?)\n\}/)?.[1] ?? ""; expect(mobile).toContain(".workspace-gnb"); - expect(mobile).toContain("overflow-x: auto"); + expect(mobile).toContain("grid-template-columns: repeat(3, minmax(0, 1fr))"); + expect(mobile).toContain("height: auto"); + expect(mobile).toContain("grid-column: 1 / -1"); + expect(mobile).toContain("min-height: var(--size-control-min)"); + expect(mobile).not.toContain("overflow-x: auto"); expect(mobile).not.toContain(".workspace-gnb {\n display: none"); });