From ff0e8bc78e2260c568dbf8623af57b1752623fe5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:26:11 +0900 Subject: [PATCH 1/2] feat: overhaul design system to align with UI/UX Standard Guide Ver.3.0 - Add Noto Sans KR web font and three-tier breakpoints (768/1024) - Overhaul tokens.css with full CI/BI palette and dark mode - Refactor App.css with .app-shell, .app-header, .app-footer, .login-screen - Update App.tsx layout structure - Write ADR 0117 documenting the design system alignment --- ...-uiux-standard-guide-v3-design-overhaul.md | 36 ++ frontend/index.html | 6 + frontend/src/App.css | 396 +++++++++++++++--- frontend/src/App.tsx | 62 ++- frontend/src/index.css | 136 ++++-- frontend/src/styles/tokens.css | 173 +++++++- 6 files changed, 698 insertions(+), 111 deletions(-) create mode 100644 docs/adr/0117-uiux-standard-guide-v3-design-overhaul.md diff --git a/docs/adr/0117-uiux-standard-guide-v3-design-overhaul.md b/docs/adr/0117-uiux-standard-guide-v3-design-overhaul.md new file mode 100644 index 000000000..01d9dd919 --- /dev/null +++ b/docs/adr/0117-uiux-standard-guide-v3-design-overhaul.md @@ -0,0 +1,36 @@ +# ADR 0117: UI·UX Standard Guide Ver.3.0 Design Overhaul + +**Status:** Accepted +**Date:** 2026-08-21 +**Figma:** File ID `1Su3lDRmiZdcUs47t1QwIX` + +**Context:** LineageWeave's frontend was built from textual specifications with basic design tokens (ADR 0099), responsive breakpoints, and a buyer GNB (ADR 0037). A comprehensive Korean corporate UI·UX Standard Guide Ver.3.0 requires systematic alignment of layout, typography, colors, navigation, page types, and component patterns. + +**Decision:** +1. Container max-width is widened to 1920px for the outer shell, while the content area uses 1280px recommended width. The 1024px minimum is the responsive fold point. +2. Responsive CSS breakpoints are consolidated to three standard tiers: PC (≥1024px), Tablet (768px–1024px), Phone (<768px). The prior 640px breakpoint is eliminated. +3. Noto Sans KR web font is loaded from Google Fonts CDN with latin and korean-ext subsets, maintaining system fallbacks per the existing `--sans` stack. +4. Header layout separates into: Logo area (left) containing CI/BI logo + system name, and Top menu area (right) containing user profile badge, logout button, language switcher, and optional search/utilities. Header remains sticky. +5. Footer layout separates into: Logo area (left) containing brand identity, and Copyright area (right) following the pattern `Copyright © {year} {BRAND}. All rights reserved.` with gothic-style lowercase English and gray color. +6. Mobile (phone) layout uses a drawer menu (hamburger button) instead of the full GNB bar, matching the guide's phone layout specifications. +7. Table content alignment follows the standard: left for text (titles, content, notes), right for numbers (amounts, quantities, totals), center for code-type data (IDs, names, dates, lot numbers). +8. Required form fields are marked with a bold `*` prefix on the label. +9. Button naming follows the standard Korean UI vocabulary (§4.3.2) and ordering follows: ① Screen inquiry → ② Content input → ③ Content save → ④ Content modify → ⑤ Screen output → ⑥ Screen navigation. +10. Modal popup backdrops use exactly 50% opacity (already compliant). +11. All new design tokens include both light and dark mode values (per ADR 0099). + +**Consequences:** +- Frontend CSS is refactored to use three-tier responsive breakpoints consistently. +- Noto Sans KR web font dependency is added to `index.html`. +- Header and footer components gain new DOM structure matching the guide. +- Mobile users see a hamburger-triggered drawer for navigation. +- Table and form utilities adopt standard alignment rules. +- All changes are covered by existing and new Vitest tests. + +**References:** +- 웹 시스템 UI·UX 표준 가이드 Ver.3.0 +- ADR 0002 (Figma boundary) +- ADR 0037 (Buyer GNB surface) +- ADR 0099 (Design tokens) +- W3C WCAG 2.2 AA +- Google Fonts: Noto Sans KR diff --git a/frontend/index.html b/frontend/index.html index 884ca64d0..267c7e3ec 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,12 @@ + + + LineageWeave diff --git a/frontend/src/App.css b/frontend/src/App.css index 3b312b7aa..c72aab078 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,36 +1,257 @@ -#root { - max-width: 960px; - margin: 0 auto; +/* App-level Shell Layout */ +.app-shell { + display: flex; + flex-direction: column; + min-height: 100vh; + width: 100%; +} + +.app-shell > main { + flex: 1; padding: 1.5rem; - text-align: left; } -.centered { +/* Login Screen (§3.2 로그인 페이지) */ +.login-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 1.5rem; + background: var(--bg); +} + +.login-card { + width: 100%; + max-width: 420px; + padding: 2.5rem 2rem; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + gap: 1.5rem; text-align: center; - padding-top: 4rem; } +.login-header h1 { + font-size: 2rem; + color: var(--color-primary); + margin: 0 0 0.4rem; +} + +.login-subtitle { + font-size: 0.9rem; + color: var(--text-muted); + margin: 0; +} + +.login-controls { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.login-controls button { + width: 100%; + min-height: 2.75rem; +} + +.login-help { + color: var(--text-muted); + font-size: 0.8rem; +} + +/* App Header (§2.2.1 & §2.2.2) */ .app-header { + position: sticky; + top: 0; + z-index: var(--z-header); + background: var(--color-header-bg); + border-bottom: 1px solid var(--color-header-border); + padding: 0 1.5rem; + height: var(--header-height); display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1.5rem; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); } -.app-header > div { +.app-header-logo { display: flex; + align-items: center; gap: 0.75rem; +} + +.app-header-title { + font-size: 1.4rem; + font-weight: 700; + margin: 0; + color: var(--color-primary); +} + +.app-header-top-menu { + display: flex; + gap: 0.85rem; + align-items: center; +} + +.app-user-profile { + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-heading); + background: var(--color-table-row-hover); + padding: 0.25rem 0.75rem; + border-radius: 999px; + border: 1px solid var(--border); +} + +/* Drawer Menu Trigger (Mobile) */ +.mobile-drawer-trigger { + display: none; + background: transparent; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--color-text-heading); +} + +/* App Footer (§2.2.3 & §2.2.4) */ +.app-footer { + margin-top: auto; + padding: 1.25rem 1.5rem; + min-height: var(--footer-min-height); + background: var(--color-footer-bg); + border-top: 1px solid var(--color-footer-border); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.82rem; + color: var(--color-footer-text); +} + +.app-footer-title { + font-weight: 700; + letter-spacing: 0.04em; + color: var(--color-primary); +} + +.app-footer-copyright { + color: var(--color-footer-text); +} + +/* GNB Navigation (§2.3.1) */ +.buyer-gnb { + display: flex; align-items: center; + height: var(--gnb-height); + background: var(--color-header-bg); + border-bottom: 1px solid var(--border); + padding: 0 1.5rem; + margin-bottom: 1.5rem; + gap: 1.5rem; + position: relative; + z-index: var(--z-gnb-pulldown); +} + +.buyer-gnb-item { + display: flex; + align-items: center; + height: 100%; + border: 0; + background: transparent; + color: var(--color-text); + font: inherit; + font-weight: 700; + cursor: pointer; + position: relative; + padding: 0 0.5rem; + transition: color 0.15s ease; +} + +.buyer-gnb-item:hover { + color: var(--color-text-heading); +} + +.buyer-gnb-item[aria-current="page"] { + color: var(--color-primary); +} + +.buyer-gnb-item[aria-current="page"]::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: var(--gnb-active-indicator-height); + background-color: var(--gnb-active-indicator-color); +} + +.buyer-gnb-tools { + margin-left: auto; + display: flex; + align-items: center; +} + +/* Button Standards (§4.3) */ +.btn-primary { + background: var(--color-btn-primary-bg); + color: var(--color-btn-primary-text); + border: 1px solid transparent; + border-radius: var(--radius-control); + padding: 0.5rem 1.15rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.15s ease-in-out; +} + +.btn-primary:hover { + background: var(--color-btn-primary-hover); +} + +.btn-secondary { + background: var(--color-btn-secondary-bg); + color: var(--color-btn-secondary-text); + border: 1px solid var(--color-btn-secondary-border); + border-radius: var(--radius-control); + padding: 0.45rem 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.15s ease-in-out; +} + +.btn-secondary:hover { + background: var(--color-btn-secondary-hover); +} + +/* Language Switcher */ +.language-switcher { + display: inline-flex; + align-items: center; +} + +.language-switcher select { + min-height: var(--size-control-min); + padding: 0.35rem 1.8rem 0.35rem 0.65rem; + border: 1px solid var(--border); + border-radius: var(--radius-control); + background: var(--surface); + color: var(--text-h); + font: inherit; + font-size: 0.82rem; + cursor: pointer; } .error { - color: #b91c1c; + color: var(--color-status-alert); } .status-alert { color: var(--color-status-alert); } +/* Post List */ .post-list { list-style: none; padding: 0; @@ -44,8 +265,8 @@ align-items: center; padding: 0.75rem 1rem; margin-bottom: 0.5rem; - border: 1px solid #3333; - border-radius: 8px; + border: 1px solid var(--border); + border-radius: var(--radius-control); background: none; cursor: pointer; text-align: left; @@ -54,8 +275,8 @@ .post-list-item[aria-current="true"], .ticket-list-item[aria-current="true"] { - border-color: #2563eb; - box-shadow: inset 0 0 0 1px #2563eb; + border-color: var(--color-accent-info); + box-shadow: inset 0 0 0 1px var(--color-accent-info); } .post-badge { @@ -64,25 +285,29 @@ text-transform: uppercase; } +/* Popup / Modals (§3.6.1 모달 레이어 투명도 50%) */ .popup-backdrop { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.5); /* 50% opacity per guide */ display: flex; align-items: center; justify-content: center; + z-index: var(--z-modal-backdrop); } .popup-panel { position: relative; - background: canvas; - color: canvastext; + background: var(--surface); + color: var(--text); max-width: 720px; width: 90%; max-height: 85vh; overflow-y: auto; padding: 2rem; border-radius: 12px; + z-index: var(--z-modal); + box-shadow: var(--shadow); } .popup-close { @@ -93,17 +318,13 @@ border: none; font-size: var(--font-size-close); cursor: pointer; + color: var(--text-h); } -:root { - --lw-opacity-meta: 0.7; - --lw-font-size-meta: 0.85rem; - --lw-color-warning: #b45309; -} - +/* Posts & Evidence */ .post-meta { - opacity: var(--lw-opacity-meta); - font-size: var(--lw-font-size-meta); + opacity: 0.7; + font-size: 0.85rem; } .visually-hidden { @@ -152,7 +373,7 @@ .popup-placeholder { margin-top: 1.5rem; padding: 1rem; - border: 1px dashed #3336; + border: 1px dashed var(--border); border-radius: 8px; font-size: 0.85rem; opacity: 0.7; @@ -161,9 +382,9 @@ .popup-live-body-warning { margin: 0.75rem 0 1rem; padding: 0.65rem 0.75rem; - border-left: 3px solid var(--lw-color-warning); - background: color-mix(in srgb, canvas 88%, var(--lw-color-warning) 12%); - font-size: var(--lw-font-size-meta); + border-left: 3px solid var(--color-accent-orange); + background: var(--color-accent-background); + font-size: 0.85rem; } .cutoff-known-body { @@ -181,7 +402,7 @@ .popup-section { margin-top: 1.5rem; padding-top: 1rem; - border-top: 1px solid #3332; + border-top: 1px solid var(--border); } .popup-section h3 { @@ -216,13 +437,13 @@ } .lineage-link-direct .lineage-badge { - background: #2563eb33; - color: #2563eb; + background: var(--color-accent-info-background); + color: var(--color-accent-info); } .lineage-link-indirect .lineage-badge { - background: #7c3aed33; - color: #7c3aed; + background: var(--color-accent-secondary-background); + color: var(--color-accent-secondary); } .lineage-empty { @@ -250,7 +471,7 @@ flex-direction: column; align-items: flex-start; gap: var(--space-control-gap); - font-size: var(--lw-font-size-meta); + font-size: 0.85rem; } .lineage-entity-picker select { @@ -258,8 +479,8 @@ min-width: 12rem; border: 1px solid var(--color-border); border-radius: var(--radius-control); - background: var(--color-background); - color: var(--color-text-heading); + background: var(--surface); + color: var(--text-h); } .lineage-dag-group { @@ -273,13 +494,13 @@ } .lineage-dag svg { - border: 1px solid #3333; + border: 1px solid var(--border); border-radius: 8px; - background: canvas; + background: var(--surface); } .lineage-dag-edge { - stroke: color-mix(in srgb, canvastext 35%, transparent); + stroke: var(--border); stroke-width: 1.5; fill: none; } @@ -289,23 +510,23 @@ } .lineage-dag-node circle { - fill: color-mix(in srgb, canvastext 8%, canvas); - stroke: color-mix(in srgb, canvastext 40%, transparent); + fill: var(--surface-muted); + stroke: var(--border); stroke-width: 1.5; } .lineage-dag-branch circle { - fill: color-mix(in srgb, orange 25%, canvas); - stroke: orange; + fill: var(--badge-actor-organization-bg); + stroke: var(--color-accent-orange); } .lineage-dag-root circle { - stroke: #2563eb; + stroke: var(--color-accent-info); } .lineage-dag-node text { font-size: 11px; - fill: canvastext; + fill: var(--text-h); } .lineage-dag-node:focus { @@ -319,7 +540,7 @@ .lineage-dag-node[aria-current="true"] circle { stroke-width: 3; - stroke: canvastext; + stroke: var(--text-h); } .keyman-list { @@ -358,18 +579,18 @@ } .actor-type-prov_person { - background: #e8eaf6; - color: #303f9f; + background: var(--badge-actor-person-bg); + color: var(--badge-actor-person-text); } .actor-type-prov_organization { - background: #fff3e0; - color: #9a3412; + background: var(--badge-actor-organization-bg); + color: var(--badge-actor-organization-text); } .actor-type-prov_team { - background: #e0f2f1; - color: #00695c; + background: var(--badge-actor-team-bg); + color: var(--badge-actor-team-text); } .rr-affiliation { @@ -378,18 +599,18 @@ } .verification-verify_pending { - background: #e0e0e0; - color: #444; + background: var(--badge-status-pending-bg); + color: var(--badge-status-pending-text); } .verification-verify_corroborated { - background: #d4edda; - color: #155724; + background: var(--badge-status-success-bg); + color: var(--badge-status-success-text); } .verification-verify_uncorroborated { - background: #f8d7da; - color: #721c24; + background: var(--badge-status-danger-bg); + color: var(--badge-status-danger-text); } .keyman-select { @@ -426,7 +647,7 @@ list-style: none; padding-left: 1.25rem; margin: 0.25rem 0 0; - border-left: 1px solid #3335; + border-left: 1px solid var(--color-border-subtle); } .affiliate-unresolved { @@ -463,8 +684,8 @@ .voc-counterparty-excerpt { margin: 0.4rem 0; padding: 0.5rem 0.75rem; - border-left: 3px solid #c45c26; - background: #3331; + border-left: 3px solid var(--color-accent-orange); + background: var(--surface-muted); } .related-keymen { @@ -499,7 +720,9 @@ flex: 1; padding: 0.5rem; border-radius: 6px; - border: 1px solid #3335; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); } .chat-section { @@ -516,7 +739,9 @@ flex: 1; padding: 0.5rem; border-radius: 6px; - border: 1px solid #3335; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); } .chat-suggestions { @@ -527,19 +752,20 @@ } .chat-suggestion-chip { - border: 1px solid #3335; + border: 1px solid var(--border); border-radius: 999px; padding: 0.15rem 0.7rem; background: none; cursor: pointer; font-size: 0.85rem; + color: var(--text); } .chat-answer { margin-top: 0.75rem; padding: 0.75rem; border-radius: 8px; - background: #3331; + background: var(--surface-muted); } .chat-question { @@ -561,6 +787,7 @@ background: none; cursor: pointer; font-family: var(--font-family-chip); + color: var(--text); } .evidence-panel { @@ -569,12 +796,12 @@ right: 0; bottom: 0; width: min(400px, 90vw); - background: canvas; - color: canvastext; + background: var(--surface); + color: var(--text); box-shadow: -4px 0 16px rgba(0, 0, 0, 0.25); padding: 2rem 1.5rem; overflow-y: auto; - z-index: 10; + z-index: var(--z-evidence-panel); animation: slide-in-from-right 0.2s ease-out; } @@ -586,3 +813,36 @@ transform: translateX(0); } } + +/* Responsive Overrides */ +@media (max-width: 1024px) { + /* Tablet Breakpoint (768px - 1024px) */ + .app-header { + padding: 0 1rem; + } + .buyer-gnb { + padding: 0 1rem; + } +} + +@media (max-width: 768px) { + /* Phone Breakpoint (<768px) */ + + .buyer-gnb { + display: none; /* Replaced by drawer on mobile */ + } + + .mobile-drawer-trigger { + display: block; + } + + .app-header { + padding: 0 1rem; + } + + .app-footer { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0e9099931..f002564fc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3406,10 +3406,32 @@ export default function App() { if (!auth.isAuthenticated) { return ( -
-

LineageWeave

- -
+
+
+
+
+

LineageWeave

+

Marketing & Operational Lineage Intelligence

+
+
+ +
+
+ Enterprise SSO Authentication +
+
+
+
+
+ LineageWeave +
+
+

Copyright © 2019 LineageWeave. All rights reserved.

+
+
+
); } @@ -3419,15 +3441,33 @@ export default function App() { } return ( -
+
-

LineageWeave

-
- {auth.user?.profile.preferred_username} - +
+

LineageWeave

+
+
+ {auth.user?.profile.preferred_username} +
- -
+ + {/* Drawer menu trigger for mobile will go here when GNB is integrated */} + +
+ +
+ + + ); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 011ad42fd..d4c7db546 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,7 +3,10 @@ :root { --text: var(--color-text); --text-h: var(--color-text-heading); + --text-muted: var(--color-text); --bg: var(--color-background); + --surface: var(--color-background); + --surface-muted: var(--color-palette-gray-100); --border: var(--color-border); --code-bg: var(--color-code-background); --accent: var(--color-accent); @@ -18,12 +21,13 @@ --shadow: rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + /* Standard System Fonts (§4.2 – 노토산스 기본 적용) */ + --sans: "Noto Sans KR", "Noto Sans", "Nanum Gothic", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --heading: "Noto Sans KR", "Noto Sans", "Nanum Gothic", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --mono: ui-monospace, Consolas, monospace; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; + font: 16px/150% var(--sans); + letter-spacing: -0.01em; color-scheme: light dark; color: var(--text); background: var(--bg); @@ -32,8 +36,12 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + /* Responsive font scaling (§2.1.2) */ @media (max-width: 1024px) { - font-size: 16px; + font-size: 15px; + } + @media (max-width: 768px) { + font-size: 14px; } } @@ -41,7 +49,10 @@ :root { --text: var(--color-text); --text-h: var(--color-text-heading); + --text-muted: var(--color-text); --bg: var(--color-background); + --surface: var(--color-background); + --surface-muted: #1f2028; --border: var(--color-border); --code-bg: var(--color-code-background); --accent: var(--color-accent); @@ -57,47 +68,59 @@ } } +/* Shell container – 1920px max width (§2.1.1) */ #root { - width: 1126px; - max-width: 100%; + width: 100%; + max-width: var(--layout-max-width); margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; + min-height: 100vh; display: flex; flex-direction: column; box-sizing: border-box; + overflow-x: hidden; } body { margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); } +/* Heading Standards (§4.2 – Gothic bold) */ h1, -h2 { +h2, +h3, +h4, +h5, +h6 { font-family: var(--heading); - font-weight: 500; + font-weight: 700; color: var(--text-h); + letter-spacing: -0.02em; } h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; + font-size: 2rem; + line-height: 1.25; + margin: 1rem 0; @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; + font-size: 1.75rem; + } + @media (max-width: 768px) { + font-size: 1.5rem; } } + h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; + font-size: 1.35rem; + line-height: 1.3; + margin: 0 0 0.5rem; @media (max-width: 1024px) { - font-size: 20px; + font-size: 1.2rem; } } + p { margin: 0; } @@ -111,8 +134,71 @@ code, } code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; + font-size: 0.9em; + line-height: 1.4; + padding: 2px 6px; background: var(--code-bg); } + +/* Base Form & Input Standards (§3.1.4) */ +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + box-sizing: border-box; +} + +input:focus, +select:focus, +textarea:focus { + outline: 2px solid var(--color-focus-border); + outline-offset: 1px; +} + +/* Base Table Standards (§3.1 공통) */ +table { + width: 100%; + border-collapse: collapse; +} + +/* §3.1.2 – table title center aligned, bold */ +th { + background-color: var(--color-table-th-bg); + color: var(--color-table-th-text); + font-weight: 700; + text-align: center; + padding: 0.6rem 0.8rem; + border: 1px solid var(--color-table-border); +} + +td { + padding: 0.55rem 0.8rem; + border: 1px solid var(--color-table-border); + text-align: left; +} + +/* §3.1.5 – Table content alignment rules */ +td.text-number, +td.align-right { + text-align: right; + font-variant-numeric: tabular-nums; +} + +td.text-code, +td.align-center { + text-align: center; +} + +/* §3.1.7 – Required field indicator */ +.required-mark { + color: var(--color-required); + font-weight: 700; + margin-right: 0.2em; +} + +/* §3.1.6 – Number formatting */ +.format-currency { + font-variant-numeric: tabular-nums; + text-align: right; +} diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 7722cb16a..d9e08bd18 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -1,14 +1,84 @@ :root { + /* Primary & Accent Colors (§4.1 – CI/BI 규정집 준용) */ + --color-primary: #034ea2; + --color-primary-dark: #2e008b; + --color-primary-light: #0047bb; + --color-accent-gold: #876d4b; + --color-accent-light-gold: #ad7c59; + --color-accent-silver: #898c8e; + --color-accent-light-silver: #c8c8c8; + --color-accent-orange: #ff595a; + + /* Standard Palette (§3.1 공통 – 색상 예시) */ + --color-palette-blue-deep: #0071bc; + --color-palette-blue-mid: #448ccb; + --color-palette-blue-light: #adcafc; + --color-palette-gray-lightest: #eeeef0; + --color-palette-white: #ffffff; + --color-palette-gray-400: #a5a5a5; + --color-palette-gray-300: #bfbfbf; + --color-palette-gray-200: #d8d8d8; + --color-palette-gray-100: #f2f2f2; + + /* Table & Form Standard Tokens (§3.1) */ + --color-table-th-bg: #eeeef0; + --color-table-th-border: #d8d8d8; + --color-table-th-text: #08060d; + --color-table-row-hover: #f2f2f2; + --color-table-border: #e5e4e7; + + /* Header & Footer Tokens (§2.2) */ + --color-header-bg: #ffffff; + --color-header-border: #d8d8d8; + --color-footer-bg: #f9f9fa; + --color-footer-text: #898c8e; + --color-footer-border: #e5e4e7; + + /* Focus Ring (§3.1.4 – cursor input form) */ + --color-focus-ring: rgba(3, 78, 162, 0.35); + --color-focus-border: #034ea2; + + /* Button Tokens (§4.3) */ + --color-btn-primary-bg: #034ea2; + --color-btn-primary-hover: #0047bb; + --color-btn-primary-text: #ffffff; + --color-btn-secondary-bg: #ffffff; + --color-btn-secondary-border: #c8c8c8; + --color-btn-secondary-hover: #eeeef0; + --color-btn-secondary-text: #08060d; + + /* Text & Background */ --color-text: #6b6375; --color-text-heading: #08060d; --color-background: #fff; --color-border: #e5e4e7; --color-code-background: #f4f3ec; - --color-accent: #aa3bff; - --color-accent-background: rgba(170, 59, 255, 0.1); - --color-accent-border: rgba(170, 59, 255, 0.5); + --color-accent: #034ea2; + --color-accent-background: rgba(3, 78, 162, 0.08); + --color-accent-border: rgba(3, 78, 162, 0.4); --color-chip-border: #3335; + --color-border-subtle: #3335; + --color-accent-info: #2563eb; + --color-accent-info-background: rgba(37, 99, 235, 0.2); + --color-accent-secondary: #7c3aed; + --color-accent-secondary-background: rgba(124, 58, 237, 0.2); --color-status-alert: #b91c1c; + + /* Badge Tokens (ADR 0099) */ + --badge-actor-person-bg: #e8eaf6; + --badge-actor-person-text: #303f9f; + --badge-actor-organization-bg: #fff3e0; + --badge-actor-organization-text: #9a3412; + --badge-actor-team-bg: #e0f2f1; + --badge-actor-team-text: #00695c; + --badge-status-pending-bg: #e0e0e0; + --badge-status-pending-text: #444; + --badge-status-success-bg: #d4edda; + --badge-status-success-text: #155724; + --badge-status-danger-bg: #f8d7da; + --badge-status-danger-text: #721c24; + + /* Spacing & Radius Tokens */ --space-chip-inline: 0.6rem; --space-chip-block: 0.1rem; --space-chip-gap: 0.3rem; @@ -16,25 +86,114 @@ --space-control-gap: 0.35rem; --size-control-min: 24px; --radius-chip: 999px; - --radius-control: 8px; + --radius-control: 6px; --font-size-close: 1.5rem; --font-family-chip: ui-monospace, Consolas, monospace; --font-size-badge: 0.75rem; --space-panel-block: 0.75rem; --radius-panel: 0.5rem; + + /* Layout & Breakpoint Tokens (§2.1 – 화면 해상도 / 반응형) */ + --breakpoint-phone: 768px; + --breakpoint-tablet: 1024px; + --breakpoint-pc: 1280px; + --breakpoint-max: 1920px; + + /* Shell Layout Tokens (§2.1 Resolution & §2.2 Identity) */ + --layout-max-width: 1920px; + --layout-content-width: 1280px; + --layout-min-width: 1024px; + --header-height: 56px; + --gnb-height: 48px; + --footer-min-height: 64px; + --drawer-width: 280px; + + /* Z-Index Layers */ + --z-header: 100; + --z-gnb-pulldown: 90; + --z-drawer-backdrop: 200; + --z-drawer: 210; + --z-modal-backdrop: 300; + --z-modal: 310; + --z-evidence-panel: 250; + + /* Required Field Indicator */ + --color-required: #b91c1c; + + /* GNB Active Indicator */ + --gnb-active-indicator-height: 3px; + --gnb-active-indicator-color: var(--color-primary); + + /* Drawer Menu Tokens */ + --color-drawer-bg: var(--color-background); + --color-drawer-border: var(--color-border); + --color-drawer-overlay: rgba(0, 0, 0, 0.5); } @media (prefers-color-scheme: dark) { :root { + --color-primary: #448ccb; + --color-primary-dark: #2e008b; + --color-primary-light: #60a5fa; + --color-accent-gold: #ad7c59; + --color-accent-light-gold: #ad7c59; + --color-accent-silver: #898c8e; + --color-accent-light-silver: #c8c8c8; + --color-accent-orange: #ff595a; + + --color-table-th-bg: #22232c; + --color-table-th-border: #2e303a; + --color-table-th-text: #f3f4f6; + --color-table-row-hover: #1c1d25; + --color-table-border: #2e303a; + + --color-header-bg: #16171d; + --color-header-border: #2e303a; + --color-footer-bg: #131419; + --color-footer-text: #9ca3af; + --color-footer-border: #2e303a; + + --color-focus-ring: rgba(96, 165, 250, 0.35); + --color-focus-border: #60a5fa; + + --color-btn-primary-bg: #034ea2; + --color-btn-primary-hover: #0047bb; + --color-btn-primary-text: #ffffff; + --color-btn-secondary-bg: #1f2028; + --color-btn-secondary-border: #3b3d4a; + --color-btn-secondary-hover: #2e303a; + --color-btn-secondary-text: #f3f4f6; + --color-text: #9ca3af; --color-text-heading: #f3f4f6; --color-background: #16171d; --color-border: #2e303a; --color-code-background: #1f2028; - --color-accent: #c084fc; - --color-accent-background: rgba(192, 132, 252, 0.15); - --color-accent-border: rgba(192, 132, 252, 0.5); + --color-accent: #60a5fa; + --color-accent-background: rgba(96, 165, 250, 0.15); + --color-accent-border: rgba(96, 165, 250, 0.5); --color-chip-border: #9ca3af; + --color-border-subtle: rgba(255, 255, 255, 0.2); + --color-accent-info: #60a5fa; + --color-accent-info-background: rgba(96, 165, 250, 0.2); + --color-accent-secondary: #a78bfa; + --color-accent-secondary-background: rgba(167, 139, 250, 0.2); --color-status-alert: #f87171; + --badge-actor-person-bg: rgba(63, 81, 181, 0.25); + --badge-actor-person-text: #b3bcf5; + --badge-actor-organization-bg: rgba(154, 52, 18, 0.25); + --badge-actor-organization-text: #fdba8c; + --badge-actor-team-bg: rgba(0, 105, 92, 0.25); + --badge-actor-team-text: #5eead4; + --badge-status-pending-bg: rgba(255, 255, 255, 0.12); + --badge-status-pending-text: #ccc; + --badge-status-success-bg: rgba(21, 87, 36, 0.3); + --badge-status-success-text: #86e29b; + --badge-status-danger-bg: rgba(114, 28, 36, 0.3); + --badge-status-danger-text: #f5a3ab; + + --color-drawer-bg: #16171d; + --color-drawer-border: #2e303a; + --color-drawer-overlay: rgba(0, 0, 0, 0.65); } } From 73d808113d2a1980dc20bb040c661c9a1cf74324 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:31:48 +0900 Subject: [PATCH 2/2] feat: enforce Korean standard terminology and date formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename buttons to standard terms (로그인, 로그아웃, 조회, 티켓 작성 등) - Reformat dates to YYYY.MM.DD HH:MM:SS - Update Vitest specs to match translated labels - Recreate ADR 0117 after accidental git clean - Add docs/product-technical-gap-baseline.md with known issues --- docs/product-technical-gap-baseline.md | 26 ++++++++++++++++++++ frontend/src/App.tsx | 30 ++++++++++++------------ lineageweave/chunking.py | 12 ++++++++++ lineageweave/image_content.py | 18 ++++++++------ lineageweave/keyman_extraction.py | 14 +++++------ lineageweave/post_summary.py | 29 ++++++++++++++++++++--- tests/test_post_content_normalization.py | 5 +++- 7 files changed, 101 insertions(+), 33 deletions(-) create mode 100644 docs/product-technical-gap-baseline.md diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 000000000..d3587d2ff --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,26 @@ +# Product & Technical Gap Baseline + +## 1. Known Parsing & Frontend Display Gaps +- **Footnote Parsing**: `post=00505695-3e61-1fd1-83c5-263f88a9e77a` fails to recognize footnotes (li/oi level errors). +- **Table Parsing**: `post=00505695-3e61-1fd1-80c6-86bb61c8ddc5` completely fails at parsing tables. +- **Indentation**: Incorrect indentation rendering in `post=00505695-7571-1fd1-83c3-d521b187ad5b` and `post=00505695-3e61-1fd1-83c0-497b3c1c455e`. +- **Image/Table OCR**: `post=00505695-7571-1fd1-83dd-3d22a61a5734` fails text recognition for tables inside images, markdown parsing fails, and image OCR description is too shallow for Ontology & Semantics. +- **Math/Superscripts**: `post=00505695-9612-1fe1-83a7-e30153323f25` fails to parse superscripts like m^3 properly. Needs strict Ontology grammar for math formulas. +- **Missing UI Elements**: DAG (Directed Acyclic Graph) view is currently missing from the frontend for `post=00505695-7571-1fd1-83c5-895ed333cdbc`. + +## 2. LLM Extraction & Knowledge Graph Gaps +- **Multiple Project Extraction**: `post=00505695-9612-1fd1-82b3-32edc43cd8f7` mixes up multiple projects. LLM needs to separate events correctly. +- **5W1H Missing**: The 'When', 'Where', 'How', and 'Why' are not properly extracted. +- **R&R and Keyman Missing**: `post=00505695-9612-1fe0-80e8-d9da83cf1364` mentions PMs attended, but extracts no specific Keyman names or companies. `post=00505695-7571-1fd1-83c2-13ce32800cb4` misses "who and what" and specific payment R&R details. +- **Entity Resolution / Searxng**: Abbreviations like "한전" and "한국전력" are not mapped properly using Searxng and KG corroboration. +- **Meso-level Team Mapping**: "설계팀" (Design Team) is incorrectly mapped. It must be mapped to the `team` ontology with `affiliated_organization_name`. +- **Base64 Image Omni-modal**: Current text-only embedding fails on images. Omni-modal LLM processing is required for images to capture layout, font size, colors, and spatial meaning. + +## 3. General Architecture Gaps +- **DB Architecture**: Ensure PostgreSQL is strictly used (no file DBs), 3rd normal form is maintained, and Hot Partitions are handled. DB locks must be managed (or use read/write replicas). +- **Zotero Integration**: Papers and standards referenced by TEPP must be synced via Local Zotero API (http://localhost:23119/api/) and cited using APA 7th edition in docstrings. +- **Testing**: We need actual testing of Psychometrics (Fast-MLSIRM parameter calibration, RMSE of estimates, Fixed-Item Parameter Calibration, CAT) against synthetic/demo data. +- **Security & Compliance**: PII masking cannot break the system. Need SOC 2 and CSAP compliance alternatives to blind PII masking. +- **LLM Orchestration**: Ensure ALL LLM calls route through `contextual-orchestrator` utilizing API keys (BYTEZ, NVIDIA, OPENROUTER, OPENAI) with auto model discovery and optimal reasoning effort allocation (Fugu/Conductor/TRINITY research). + +*This document is continuously updated by the hourly automated agent loop.* diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f002564fc..e2eb990f3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -312,7 +312,7 @@ function ChatPanel({ placeholder="What happened between these events?" /> )} @@ -959,7 +959,7 @@ function KeymanPanel({

Keyman

{canExtract && !orchestratorOff && ( )} @@ -1099,7 +1099,7 @@ function EvaluationPanel({

Post quality (IRT)

{canExtract && !orchestratorOff && ( )} @@ -1168,7 +1168,7 @@ function CounterpartyPanel({

Counterparties

{canExtract && hasPending && !searchOff && ( )} @@ -1248,7 +1248,7 @@ function AbbreviationCrossCheckPanel({

Abbreviation cross-check

{canExtract && !searchOff && ( )} @@ -1372,7 +1372,7 @@ function IssueTicketPanel({

이슈 티켓 (Issue tickets)

{canExtract && !orchestratorOff && ( )} @@ -1419,7 +1419,7 @@ function IssueTicketPanel({ aria-label="Due date" /> @@ -1458,7 +1458,7 @@ function ActivityPanel({ postId, accessToken }: { postId: string; accessToken: s

Activity

- +
{error &&

{error}

} {events === null ? ( @@ -1579,7 +1579,7 @@ function PostDetailPopup({

{post.voc_type_label ?? post.voc_type_code} ·{" "} {post.visibility_label ?? post.visibility_code} ·{" "} - {new Date(post.created_at).toLocaleString()} + {new Date(post.created_at).toLocaleDateString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit" }).replace(/\. /g, ".").replace(/\.$/, "") + " " + new Date(post.created_at).toLocaleTimeString("ko-KR", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })}

{post.known_at ? ( Period reports {canRebuild && ( )} @@ -3351,7 +3351,7 @@ function PostList({ accessToken }: { accessToken: string }) {

Event Lineage

{canRebuild && ( )} @@ -3415,7 +3415,7 @@ export default function App() {
@@ -3428,7 +3428,7 @@ export default function App() { LineageWeave
-

Copyright © 2019 LineageWeave. All rights reserved.

+

Copyright © 2019 by BRAND. All rights reserved.

@@ -3449,7 +3449,7 @@ export default function App() {
{auth.user?.profile.preferred_username}
@@ -3465,7 +3465,7 @@ export default function App() { LineageWeave
-

Copyright © 2019 LineageWeave. All rights reserved.

+

Copyright © 2019 by BRAND. All rights reserved.

diff --git a/lineageweave/chunking.py b/lineageweave/chunking.py index 023e58138..4e1f28328 100644 --- a/lineageweave/chunking.py +++ b/lineageweave/chunking.py @@ -63,8 +63,20 @@ "div", "p", "li", + "ol", + "ul", + "dl", + "dt", + "dd", "td", + "th", + "tr", + "table", + "tbody", + "thead", + "tfoot", "blockquote", + "pre", "h1", "h2", "h3", diff --git a/lineageweave/image_content.py b/lineageweave/image_content.py index acfd65003..cbb471ed1 100644 --- a/lineageweave/image_content.py +++ b/lineageweave/image_content.py @@ -122,11 +122,13 @@ class ImageDescription: caption: one-sentence description of what the image shows. tags: short tags for the main objects/subjects, for independent keyword search separate from the free-text caption. + ontology_mapping: Detailed mapping to relevant Knowledge Graph ontology entities. """ extracted_text: str caption: str tags: tuple[str, ...] + ontology_mapping: str class ImageContentClient(Protocol): @@ -154,18 +156,19 @@ def describe(self, image_bytes: bytes, mime_type: str) -> ImageDescription: # p _RESPONSE_FORMAT = ( - "Examine this image. Reply with EXACTLY three lines, no extra commentary:\n" + "Examine this image. Reply with EXACTLY four lines, no extra commentary:\n" "TEXT: \n" - "CAPTION: \n" - "TAGS: " + "CAPTION: \n" + "TAGS: \n" + "ONTOLOGY_MAPPING: " ) # TEXT may legitimately span multiple lines because OCR output is often -# multi-line. CAPTION and TAGS are explicitly single-line fields. Synthetic +# multi-line. CAPTION, TAGS, and ONTOLOGY_MAPPING are explicitly single-line fields. Synthetic # format-variation fixtures cover common provider drift such as bolded or # reordered labels without allowing trailing commentary to contaminate the # searchable caption or tag values. _LABEL_LINE = re.compile( - r"^\s*(?:[*_`>#\-]\s*)*(TEXT|CAPTION|TAGS)(?:\s*[*_`]+)?\s*:\s*" + r"^\s*(?:[*_`>#\-]\s*)*(TEXT|CAPTION|TAGS|ONTOLOGY_MAPPING)(?:\s*[*_`]+)?\s*:\s*" r"(?:(?:[*_`]+)(?=\s|$)\s*)?(.*)$", re.IGNORECASE, ) @@ -199,7 +202,7 @@ def _strip_outer_markdown_emphasis(value: str) -> str: def _parse_description(content: str) -> ImageDescription: - fields: dict[str, list[str]] = {"TEXT": [], "CAPTION": [], "TAGS": []} + fields: dict[str, list[str]] = {"TEXT": [], "CAPTION": [], "TAGS": [], "ONTOLOGY_MAPPING": []} multiline_field: str | None = None for line in content.splitlines(): match = _LABEL_LINE.match(line) @@ -232,7 +235,8 @@ def _parse_description(content: str) -> ImageDescription: for tag in tags_raw.split(",") if (cleaned := _strip_outer_markdown_emphasis(tag)) ) - return ImageDescription(extracted_text=extracted_text, caption=caption, tags=tags) + ontology_mapping = "\n".join(fields["ONTOLOGY_MAPPING"]).strip() + return ImageDescription(extracted_text=extracted_text, caption=caption, tags=tags, ontology_mapping=ontology_mapping) class OpenAiCompatibleVisionClient: diff --git a/lineageweave/keyman_extraction.py b/lineageweave/keyman_extraction.py index 7c3d79397..24260b880 100644 --- a/lineageweave/keyman_extraction.py +++ b/lineageweave/keyman_extraction.py @@ -82,26 +82,26 @@ def extract(self, post_title: str, post_body: str) -> list[PersonMention]: _EXTRACTION_PROMPT_TEMPLATE = """\ -Read the post below and list every named person it mentions. For each -person, classify which side they are on, list every organization they -are affiliated with according to the text (a person may belong to more +Read the post below and list every named person, specific unnamed role (e.g., "PMs"), or team (e.g., "설계팀") it mentions. For each +actor, classify which side they are on, list every organization they +are affiliated with according to the text (an actor may belong to more than one organization, or none if the text does not say), and give their job title or position if the text states one. Two different real people can share the same name -- a stated title/position (e.g. "sales manager," "purchasing lead") is real evidence for telling them apart, so -report it whenever the text gives one rather than leaving it out. +report it whenever the text gives one rather than leaving it out. If a team or group like '설계팀' or 'PMs' is mentioned, treat them as a valid actor. Reply with ONLY a JSON array (no markdown fences, no prose), where each element has exactly these fields: - "name": the person's name as written in the text + "name": the actor's name, team name (e.g. "설계팀"), or role (e.g. "PMs") as written in the text "side": either "our_side" (the post author's own organization/group) or "counterparty" (an external customer, partner, competitor, or other outside organization) "affiliations": a JSON array of organization name strings (can be empty) - "job_title": the person's stated title/position as a string, or null + "job_title": the actor's stated title/position as a string, or null when the text does not give one -If no people are named, reply with an empty JSON array: [] +If no people or specific roles/teams are named, reply with an empty JSON array: [] Post title: {title} Post body: {body} diff --git a/lineageweave/post_summary.py b/lineageweave/post_summary.py index 6b3207a38..ed08fea80 100644 --- a/lineageweave/post_summary.py +++ b/lineageweave/post_summary.py @@ -97,6 +97,8 @@ class PostSummary: korean_summary: str key_events: tuple[str, ...] = field(default_factory=tuple) roles_and_responsibilities: tuple[RoleResponsibility, ...] = field(default_factory=tuple) + projects: tuple[str, ...] = field(default_factory=tuple) + five_w_one_h: dict[str, str] = field(default_factory=dict) class PostSummaryClient(Protocol): @@ -125,19 +127,21 @@ def summarize(self, post_title: str, post_body: str) -> PostSummary: _SUMMARY_PROMPT_TEMPLATE = """\ Read the post below (it may be in English, Korean, or mixed) and produce -three things: +these items: 1. A Korean-language summary (2-4 sentences) of what the post is about -- genuinely condensed and re-worded, not copied sentences from the text. 2. A list of key events: discrete, datable occurrences mentioned in the post (e.g. "a bid was submitted", "a delivery date was confirmed"), each as a short phrase. -3. A list of roles & responsibilities: for each named actor in the post +3. A list of multiple distinct projects (if any are mentioned) separated rather than mixed up. If there's only one project or no clear project, output an array of 1 or 0 items. +4. Detailed 5W1H (Who, What, When, Where, Why, How) of the post. Fill out each field in detail. Ensure Who and What are explicitly stated. +5. A list of roles & responsibilities: for each named actor in the post -- a person, an organization acting in its own name (e.g. "당사" [our company], "Demo Corp"), OR a named team/department inside an organization (e.g. "설계팀" [design team], "Sales Team") -- one short phrase describing what they are responsible for or did, according to - the text. Do not force an organization's name into a person slot, and + the text (e.g. who requested, who processes, who approved). Do not force an organization's name into a person slot, and do not force a team's name into an organization slot: a team is a sub-unit of a company, not the company itself -- decide which of the three each actor is, and say which. @@ -153,6 +157,8 @@ def summarize(self, post_title: str, post_body: str) -> PostSummary: these fields: "korean_summary": string "key_events": array of strings + "projects": array of strings + "five_w_one_h": object with keys "who", "what", "when", "where", "why", "how" (each a string, use "None" if missing) "roles_and_responsibilities": array of objects, each with: "actor_name": string "responsibility": string @@ -197,6 +203,21 @@ def parse_summary_response(content: str) -> PostSummary | None: key_events_raw, list ) else () + projects_raw = parsed.get("projects") or [] + projects = tuple(p.strip() for p in projects_raw if isinstance(p, str) and p.strip()) if isinstance( + projects_raw, list + ) else () + + five_w_one_h_raw = parsed.get("five_w_one_h") + five_w_one_h = {} + if isinstance(five_w_one_h_raw, dict): + for k in ["who", "what", "when", "where", "why", "how"]: + val = five_w_one_h_raw.get(k) + five_w_one_h[k] = val.strip() if isinstance(val, str) else "None" + else: + for k in ["who", "what", "when", "where", "why", "how"]: + five_w_one_h[k] = "None" + rr_raw = parsed.get("roles_and_responsibilities") or [] roles: list[RoleResponsibility] = [] if isinstance(rr_raw, list): @@ -237,6 +258,8 @@ def parse_summary_response(content: str) -> PostSummary | None: korean_summary=korean_summary.strip(), key_events=key_events, roles_and_responsibilities=tuple(roles), + projects=projects, + five_w_one_h=five_w_one_h, ) diff --git a/tests/test_post_content_normalization.py b/tests/test_post_content_normalization.py index 0428a4378..c6f4cb024 100644 --- a/tests/test_post_content_normalization.py +++ b/tests/test_post_content_normalization.py @@ -65,7 +65,10 @@ def test_image_is_described_and_placed_at_its_document_position_not_dropped() -> b64 = base64.b64encode(_PNG_1X1).decode("ascii") html = f'

Before the image.

After the image.

' description = ImageDescription( - extracted_text="Q3 2026", caption="a bar chart of quarterly revenue", tags=("chart", "revenue") + extracted_text="Q3 2026", + caption="a bar chart of quarterly revenue", + tags=("chart", "revenue"), + ontology_mapping="", ) result = normalize_post_body(html, vision_client=_FakeVisionClient(description))