diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 62007ddee..11a316773 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -485,7 +485,7 @@ this file per §3.5 of the prior snapshot). | Semantic source rendering | ADR 0223 and migration 0221 give new paragraph, list, table, MathML formula, and caller-parsed conversation-turn units explicit persisted kinds without rewriting historical rows; image regions remain ordered normalized children under ADR 0091. This branch is candidate evidence, not protected-main delivery | Land the exact-head candidate, then prove an authorized semantic-only query retrieves each persisted unit kind and gather authenticated browser evidence that nesting, continuation alignment, formula units, and image regions retain source order | | Event and project semantics | Multi-project mentions, project-bound actions, 5W1H, requester/processor, and semantic relations exist in ADR 0036/0052/0100/0111/0129 and active stacks | Aggregate authenticated evidence must show distinct projects and events, explicit requester/processor and real R&R, normalized relative time, and product/entity relations without promoting attendance or co-occurrence | | Product semantic identity | ADR 0228 and migration 0228 define normalized product group/model/variant/trade-item identities, scoped GTIN/MPN keys, exact-span provenance, fail-closed unique/tie/missing/unavailable resolution, and foreign-key relations to existing project and operations facts. The worker candidate reuses the durable post-content queue and skips an unchanged authorized input digest. No authorized-corpus product counts or rendered acceptance evidence are recorded | Land the stack, add authorization-filtered Post/Dashboard relationship reads and SHACL projection, then verify aggregate-only backfill outcomes plus desktop/mobile Storybook screenshots without exposing identifying runtime rows | -| Voice semantic taxonomy | ADR/migration 0230 preserve the five-value source post scheme separately from the six-value post-scoped organization relationship scheme, retain source/derived disagreement and multi-membership, and provide authorized overlap-aware aggregate filters. Candidate Storybook evidence is synthetic; no private-corpus derived assertion count is recorded | Land the stack, run bounded orchestrator backfill, and verify aggregate-only source/derived/disagreement/unavailable counts at one declared cutoff without exposing record identities | +| Voice semantic taxonomy | ADRs 0244/0246 and migrations 0230/0235 preserve the twelve-value source-post scheme separately from the six-value post-scoped organization relationship scheme, retain source/derived disagreement and multi-membership, and provide authorized overlap-aware aggregate filters. The Dashboard API returns every persisted category dynamically; PR #736 exact `2f5d9ee8` still typed and labeled only `voc`/`vocc`/`voco`/`vom`/`vop`, so `vos`/`voe`/`vob`/`vor`/`voi`/`voso`/`vops` could not render. This stacked repair covers all twelve with locale and component tests. Candidate Storybook evidence remains synthetic; no private-corpus derived assertion count is recorded | Land the stack, run bounded orchestrator backfill, and verify aggregate-only source/derived/disagreement/unavailable counts at one declared cutoff without exposing record identities | | Knowledge Graph readability | The black evidence-node root cause is an undefined-token fallback; the design-token repair and long-label/evidence-table coverage remain only on closed, unmerged #490, not protected `main` | Recreate the token repair on a current base and deliver it through protected `main`, then verify light/dark contrast, keyboard graph navigation, full labels, and evidence tables in the authenticated rendered surface | | Source-code lookup UX | Source state/detail codes remain evidence-bearing machine values and current detail presentation is dense | Catalog-backed display labels with raw-code provenance, compact 5W1H/source-detail hierarchy, keyboard access, and no unsupported customer/project binding | | Calendar / Naruon | #355 delivered the projection contract; v2.17.0 wires operator consumption without forwarding the end-user token. Naruon producer, provider/consumer fixtures, and protected merge remain open (#336) | Verify observed events against the published schema without invented events; keep commitments available when the channel is unwired | diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 656927dd7..93c391ff2 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -132,7 +132,7 @@ export interface VoiceTaxonomySummary { disagreement: number; counts_overlap: boolean; category_memberships: Array<{ - voice_concept_code: "voc" | "vocc" | "voco" | "vom" | "vop"; + voice_concept_code: "voc" | "vocc" | "voco" | "vom" | "vop" | "vos" | "voe" | "vob" | "vor" | "voi" | "voso" | "vops"; post_count: number; eligible_percentage: number; }>; diff --git a/frontend/src/components/OperationsDashboard.test.tsx b/frontend/src/components/OperationsDashboard.test.tsx index 35820d4b7..59fabeea1 100644 --- a/frontend/src/components/OperationsDashboard.test.tsx +++ b/frontend/src/components/OperationsDashboard.test.tsx @@ -99,10 +99,10 @@ describe("OperationsDashboardView", () => { expect(screen.queryByText("분류 Event")).not.toBeInTheDocument(); }); - it("does not label a scoped external count with a corpus-wide rate", () => { + it("shows external information as a share of all visible posts in the GNB view", () => { render( undefined} />); - expect(screen.getByText("5건")).toBeInTheDocument(); - expect(screen.queryByText("5건 · 25.0%")).not.toBeInTheDocument(); + expect(screen.getByText("외부 정보 (전체 글 대비)")).toBeInTheDocument(); + expect(screen.getByText("5건 · 25.0%")).toBeInTheDocument(); }); it("labels a source-backed external relation by its semantic target", () => { @@ -208,7 +208,7 @@ describe("OperationsDashboardView", () => { vi.mocked(fetchVoiceTaxonomySummary).mockClear(); vi.mocked(fetchOperationsDashboard).mockReset().mockResolvedValue(data); render( undefined} />); - await screen.findByText("5건"); + await screen.findByText("5건 · 25.0%"); expect(fetchOperationsDashboard).toHaveBeenCalledWith("synthetic-token", "", "", true); expect(fetchVoiceTaxonomySummary).not.toHaveBeenCalled(); }); diff --git a/frontend/src/components/OperationsDashboard.tsx b/frontend/src/components/OperationsDashboard.tsx index dbd8bfb26..16aeb2b3b 100644 --- a/frontend/src/components/OperationsDashboard.tsx +++ b/frontend/src/components/OperationsDashboard.tsx @@ -116,7 +116,7 @@ export function OperationsDashboardView({ data, externalOnly = false, onOpenPost
{!externalOnly ?
전체 글
{data.total_post_count}
: null} {!externalOnly ?
분류 Event
{data.total_event_count}
: null} -
외부 정보
{data.external_post_count}건{externalOnly ? "" : ` · ${data.external_percent.toFixed(1)}%`}
+
외부 정보 (전체 글 대비)
{data.external_post_count}건 · {data.external_percent.toFixed(1)}%
{!externalOnly ?
분석 대기
{data.pending_analysis_count}
: null} {!externalOnly ?
분석 실패
{data.failed_analysis_count}
: null}
diff --git a/frontend/src/components/VoiceTaxonomySummary.stories.tsx b/frontend/src/components/VoiceTaxonomySummary.stories.tsx index 38821bd59..ec340eb2d 100644 --- a/frontend/src/components/VoiceTaxonomySummary.stories.tsx +++ b/frontend/src/components/VoiceTaxonomySummary.stories.tsx @@ -13,6 +13,8 @@ export const OverlappingEvidence: Story = { args: { data: { category_memberships: [ { voice_concept_code: "voc", post_count: 5, eligible_percentage: 41.7 }, { voice_concept_code: "vom", post_count: 4, eligible_percentage: 33.3 }, + { voice_concept_code: "vos", post_count: 2, eligible_percentage: 16.7 }, + { voice_concept_code: "voe", post_count: 1, eligible_percentage: 8.3 }, ], } } }; diff --git a/frontend/src/components/VoiceTaxonomySummary.test.tsx b/frontend/src/components/VoiceTaxonomySummary.test.tsx index 9e177bd2d..8f72e15a8 100644 --- a/frontend/src/components/VoiceTaxonomySummary.test.tsx +++ b/frontend/src/components/VoiceTaxonomySummary.test.tsx @@ -37,4 +37,18 @@ describe("VoiceTaxonomySummary", () => { expect(screen.getByText(/불일치와 글 유형 근거가 없는 기록을 확인한 뒤/)).toBeInTheDocument(); expect(screen.queryByText("Voice evidence overview")).not.toBeInTheDocument(); }); + + it("renders every accepted post voice category", () => { + const voiceConceptCodes = ["voc", "vocc", "voco", "vom", "vop", "vos", "voe", "vob", "vor", "voi", "voso", "vops"] as const; + render( ({ voice_concept_code, post_count: 1, eligible_percentage: 8.3 })), + }} />); + + for (const label of ["Voice of Customer", "Voice of Customer's customer", "Voice of Competitor", "Voice of Market", "Voice of Partner", "Voice of Supplier", "Voice of Employee", "Voice of Business", "Voice of Regulator", "Voice of Investor", "Voice of Society", "Voice of Process"]) { + expect(screen.getByText(label)).toBeInTheDocument(); + } + }); }); diff --git a/frontend/src/components/VoiceTaxonomySummary.tsx b/frontend/src/components/VoiceTaxonomySummary.tsx index d74cff0fa..e4dea9c02 100644 --- a/frontend/src/components/VoiceTaxonomySummary.tsx +++ b/frontend/src/components/VoiceTaxonomySummary.tsx @@ -7,6 +7,13 @@ const voiceLabels = { voco: "Voice of Competitor", vom: "Voice of Market", vop: "Voice of Partner", + vos: "Voice of Supplier", + voe: "Voice of Employee", + vob: "Voice of Business", + vor: "Voice of Regulator", + voi: "Voice of Investor", + voso: "Voice of Society", + vops: "Voice of Process", } as const; export function VoiceTaxonomySummary({ data }: { data: Summary }) { diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index fd050c3df..94a3b3ed5 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -148,6 +148,13 @@ const TRANSLATIONS: Partial>> = { "Voice of Customer's customer": "고객 고객사의 소리", "Voice of Competitor": "경쟁사의 소리", "Voice of Partner": "파트너의 소리", + "Voice of Supplier": "공급사의 소리", + "Voice of Employee": "임직원의 소리", + "Voice of Business": "사업의 소리", + "Voice of Regulator": "규제기관의 소리", + "Voice of Investor": "투자자의 소리", + "Voice of Society": "사회의 소리", + "Voice of Process": "프로세스의 소리", "Voice evidence overview": "글 유형 근거 현황", "Loading voice evidence...": "글 유형 근거를 불러오는 중입니다...", "Voice evidence could not be loaded.": "글 유형 근거를 불러오지 못했습니다.", @@ -707,6 +714,13 @@ const TRANSLATIONS: Partial>> = { "Voice of Customer's customer": "客户的客户之声", "Voice of Competitor": "竞争对手之声", "Voice of Partner": "合作伙伴之声", + "Voice of Supplier": "供应商之声", + "Voice of Employee": "员工之声", + "Voice of Business": "业务之声", + "Voice of Regulator": "监管机构之声", + "Voice of Investor": "投资者之声", + "Voice of Society": "社会之声", + "Voice of Process": "流程之声", "Voice evidence overview": "声音分类证据概览", "Loading voice evidence...": "正在加载声音分类证据...", "Voice evidence could not be loaded.": "无法加载声音分类证据。", @@ -1282,6 +1296,13 @@ const TRANSLATIONS: Partial>> = { "Voice of Customer's customer": "顧客の顧客の声", "Voice of Competitor": "競合の声", "Voice of Partner": "パートナーの声", + "Voice of Supplier": "サプライヤーの声", + "Voice of Employee": "従業員の声", + "Voice of Business": "事業の声", + "Voice of Regulator": "規制当局の声", + "Voice of Investor": "投資家の声", + "Voice of Society": "社会の声", + "Voice of Process": "プロセスの声", "Voice evidence overview": "声分類の証拠概要", "Loading voice evidence...": "声分類の証拠を読み込んでいます...", "Voice evidence could not be loaded.": "声分類の証拠を読み込めませんでした。", @@ -1836,6 +1857,13 @@ const TRANSLATIONS: Partial>> = { "Voice of Customer's customer": "Tiếng nói khách hàng của khách hàng", "Voice of Competitor": "Tiếng nói đối thủ", "Voice of Partner": "Tiếng nói đối tác", + "Voice of Supplier": "Tiếng nói nhà cung cấp", + "Voice of Employee": "Tiếng nói nhân viên", + "Voice of Business": "Tiếng nói doanh nghiệp", + "Voice of Regulator": "Tiếng nói cơ quan quản lý", + "Voice of Investor": "Tiếng nói nhà đầu tư", + "Voice of Society": "Tiếng nói xã hội", + "Voice of Process": "Tiếng nói quy trình", "Voice evidence overview": "Tổng quan bằng chứng phân loại tiếng nói", "Loading voice evidence...": "Đang tải bằng chứng phân loại tiếng nói...", "Voice evidence could not be loaded.": "Không thể tải bằng chứng phân loại tiếng nói.",