diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 00000000..c6276912 --- /dev/null +++ b/design-qa.md @@ -0,0 +1,37 @@ +**Findings** +- No actionable P0/P1/P2 mismatches remain. + Location: `ExportModal` share/export component. + Evidence: Figma source node `29:143` and the rendered implementation both use a centered 640px modal, two-column desktop body, compact bordered sections, primary blue actions, neutral/success/error status strip treatment, and footer actions aligned to the right. + Impact: The implementation preserves the intended share/export hierarchy and does not block handoff. + Fix: None required. + +**Open Questions** +- The Figma source has three export artifact rows: SQL DDL, SVG image, and Mermaid. The implementation adds PlantUML because the current product already exposes PlantUML export. This is treated as an intentional product-capability extension. +- The implementation is localized in Korean while the Figma source is in English. The copy preserves the same intent and hierarchy. +- The close button shows a visible focus ring in the automated screenshots. This is expected accessibility behavior from the capture path and should not be removed. + +**Implementation Checklist** +- Source visual truth path: `docs/ui-ux/qa/2026-07-02-figma-share-export-modal.png`. +- Implementation screenshot path: `docs/ui-ux/qa/2026-07-02-implementation-share-export-modal.png`. +- Success-state screenshot path: `docs/ui-ux/qa/2026-07-02-implementation-share-export-success-modal.png`. +- Mobile screenshot path: `docs/ui-ux/qa/2026-07-02-implementation-share-export-mobile.png`. +- Full-view comparison evidence: `docs/ui-ux/qa/2026-07-02-share-export-comparison.png`. +- Viewport: desktop `1440x900`; mobile `390x844 @ 2x`. +- State: default ready state and copied success state in demo mode. +- Focused region comparison evidence: modal-only screenshots were captured because the component-level source and implementation are readable without additional crops. +- Fonts and typography: system UI stack maps acceptably to the app; hierarchy, weight, wrapping, and line height remain readable in desktop and mobile captures. +- Spacing and layout rhythm: modal width, two-column desktop layout, section grouping, status strip, and footer alignment match the source intent. Export row density was compacted during QA. +- Colors and visual tokens: primary blue, slate text, light borders, neutral ready state, green success state, and disabled secondary action match the source intent and app palette. +- Image quality and asset fidelity: no external images or replacement assets are part of this modal; all visible UI is native controls and text. +- Copy and content: Korean implementation copy is coherent and maps to the same source states. PlantUML is the only added product row. +- Responsiveness: mobile capture has no horizontal overflow; the modal collapses to one column and remains scrollable. + +**Patches Made Since Previous QA Pass** +- Reduced `exportModal` vertical gaps and padding. +- Reduced share/export section padding and minimum height. +- Tightened export artifact row gaps, padding, and button sizing. + +**Follow-up Polish** +- Consider adding a dedicated Figma variant with PlantUML so the source design exactly reflects all current product export types. + +final result: passed diff --git a/docs/ui-ux/qa/2026-07-02-figma-share-export-modal.png b/docs/ui-ux/qa/2026-07-02-figma-share-export-modal.png new file mode 100644 index 00000000..e5e36571 Binary files /dev/null and b/docs/ui-ux/qa/2026-07-02-figma-share-export-modal.png differ diff --git a/docs/ui-ux/qa/2026-07-02-implementation-share-export-mobile.png b/docs/ui-ux/qa/2026-07-02-implementation-share-export-mobile.png new file mode 100644 index 00000000..78bc44e7 Binary files /dev/null and b/docs/ui-ux/qa/2026-07-02-implementation-share-export-mobile.png differ diff --git a/docs/ui-ux/qa/2026-07-02-implementation-share-export-modal.png b/docs/ui-ux/qa/2026-07-02-implementation-share-export-modal.png new file mode 100644 index 00000000..00619051 Binary files /dev/null and b/docs/ui-ux/qa/2026-07-02-implementation-share-export-modal.png differ diff --git a/docs/ui-ux/qa/2026-07-02-implementation-share-export-success-modal.png b/docs/ui-ux/qa/2026-07-02-implementation-share-export-success-modal.png new file mode 100644 index 00000000..69b14917 Binary files /dev/null and b/docs/ui-ux/qa/2026-07-02-implementation-share-export-success-modal.png differ diff --git a/docs/ui-ux/qa/2026-07-02-share-export-comparison.png b/docs/ui-ux/qa/2026-07-02-share-export-comparison.png new file mode 100644 index 00000000..1708eb3d Binary files /dev/null and b/docs/ui-ux/qa/2026-07-02-share-export-comparison.png differ diff --git a/docs/ui-ux/qa/README.md b/docs/ui-ux/qa/README.md new file mode 100644 index 00000000..f08ce3de --- /dev/null +++ b/docs/ui-ux/qa/README.md @@ -0,0 +1,11 @@ +# UI/UX QA Evidence + +This folder stores visual QA captures used to compare Figma source designs against rendered frontend implementation states. + +## 2026-07-02 Share Export Modal + +- `2026-07-02-figma-share-export-modal.png`: Figma source node `29:143`. +- `2026-07-02-implementation-share-export-modal.png`: rendered default state at `1440x900`. +- `2026-07-02-implementation-share-export-success-modal.png`: rendered copied-link success state at `1440x900`. +- `2026-07-02-implementation-share-export-mobile.png`: rendered mobile state at `390x844 @ 2x`. +- `2026-07-02-share-export-comparison.png`: combined comparison evidence. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e99700c5..e1bde7d8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1425,36 +1425,38 @@ export default function App() { @@ -1518,9 +1520,9 @@ export default function App() { 0} + hasDiagramExport={nodes.length > 0} shareLinkUrl={shareLinkUrl} isCreatingShareLink={isCreatingShareLink} isShareLinkCopied={isShareLinkCopied} @@ -1528,6 +1530,9 @@ export default function App() { canCreateShareLink={Boolean(selectedProjectId)} onCloseExport={onCloseExport} onCopyExportDdl={onCopyExportDdl} + onDownloadSvg={onDownloadSvg} + onDownloadUml={onDownloadUml} + onDownloadMermaid={onDownloadMermaid} onCreateShareLink={onCreateShareLink} onCopyShareLink={onCopyShareLink} /> diff --git a/frontend/src/components/modals/ExportModal.test.tsx b/frontend/src/components/modals/ExportModal.test.tsx index d5d98c8a..be25c934 100644 --- a/frontend/src/components/modals/ExportModal.test.tsx +++ b/frontend/src/components/modals/ExportModal.test.tsx @@ -6,9 +6,9 @@ import { ExportModal } from './ExportModal'; const baseProps = { isOpen: true, - exportDdlText: 'create table users (id integer primary key);', isCopied: false, hasDdlExport: true, + hasDiagramExport: true, shareLinkUrl: '', isCreatingShareLink: false, isShareLinkCopied: false, @@ -16,6 +16,9 @@ const baseProps = { canCreateShareLink: true, onCloseExport: vi.fn(), onCopyExportDdl: vi.fn(), + onDownloadSvg: vi.fn(), + onDownloadUml: vi.fn(), + onDownloadMermaid: vi.fn(), onCreateShareLink: vi.fn(), onCopyShareLink: vi.fn(), }; @@ -25,7 +28,7 @@ afterEach(() => { }); describe('ExportModal', () => { - it('creates a project share link and exposes the current DDL', () => { + it('separates project share links from export artifacts', () => { const onCreateShareLink = vi.fn(); render( { fireEvent.click(screen.getByRole('button', { name: '링크 만들기' })); expect(onCreateShareLink).toHaveBeenCalledOnce(); - expect(screen.getByLabelText('DDL Export')).toHaveValue(baseProps.exportDdlText); + expect(screen.getByRole('heading', { name: '공유 링크' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '내보내기 산출물' })).toBeInTheDocument(); + expect(screen.getByText('SQL DDL')).toBeInTheDocument(); + expect(screen.getByText('SVG 이미지')).toBeInTheDocument(); + expect(screen.getByText('PlantUML')).toBeInTheDocument(); + expect(screen.getByText('Mermaid')).toBeInTheDocument(); }); it('copies an already generated share link', () => { @@ -58,6 +66,33 @@ describe('ExportModal', () => { expect(onCopyShareLink).toHaveBeenCalledOnce(); }); + it('runs each export artifact action from the modal', () => { + const onCopyExportDdl = vi.fn(); + const onDownloadSvg = vi.fn(); + const onDownloadUml = vi.fn(); + const onDownloadMermaid = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: 'SQL DDL 복사' })); + fireEvent.click(screen.getByRole('button', { name: 'SVG 이미지 내보내기' })); + fireEvent.click(screen.getByRole('button', { name: 'PlantUML 내보내기' })); + fireEvent.click(screen.getByRole('button', { name: 'Mermaid 내보내기' })); + + expect(onCopyExportDdl).toHaveBeenCalledOnce(); + expect(onDownloadSvg).toHaveBeenCalledOnce(); + expect(onDownloadUml).toHaveBeenCalledOnce(); + expect(onDownloadMermaid).toHaveBeenCalledOnce(); + }); + it('shows share link copy or creation errors', () => { render( { expect(screen.getByRole('alert')).toHaveTextContent('공유 링크 복사에 실패했습니다.'); }); - it('explains when DDL cannot be generated yet', () => { + it('explains when exports cannot be generated yet', () => { render( , ); - expect(screen.queryByLabelText('DDL Export')).not.toBeInTheDocument(); - expect(screen.getByText('DDL을 만들려면 먼저 스냅샷을 생성하거나 테이블을 추가하세요.')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'DDL 복사' })).toBeDisabled(); + expect(screen.getAllByText('먼저 테이블을 추가하세요')).toHaveLength(4); + expect(screen.getByRole('button', { name: 'SQL DDL 복사' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'SVG 이미지 내보내기' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'PlantUML 내보내기' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Mermaid 내보내기' })).toBeDisabled(); + }); + + it('exposes access-control guidance for disabled button', () => { + render(); + + expect(screen.getByText('접근 권한은 프로젝트 설정에서 별도로 관리합니다.')).toBeInTheDocument(); + const accessManagementButton = screen.getByRole('button', { name: '접근 관리' }); + expect(accessManagementButton).toBeDisabled(); + expect(accessManagementButton).toHaveAttribute('aria-describedby', 'share-export-access-hint'); }); }); diff --git a/frontend/src/components/modals/ExportModal.tsx b/frontend/src/components/modals/ExportModal.tsx index 5392d502..b898e65b 100644 --- a/frontend/src/components/modals/ExportModal.tsx +++ b/frontend/src/components/modals/ExportModal.tsx @@ -3,9 +3,9 @@ import { useDialogAccessibility } from './useDialogAccessibility'; interface ExportModalProps { isOpen: boolean; - exportDdlText: string; isCopied: boolean; hasDdlExport: boolean; + hasDiagramExport: boolean; shareLinkUrl: string; isCreatingShareLink: boolean; isShareLinkCopied: boolean; @@ -13,15 +13,27 @@ interface ExportModalProps { canCreateShareLink: boolean; onCloseExport: () => void; onCopyExportDdl: () => void; + onDownloadSvg: () => void; + onDownloadUml: () => void; + onDownloadMermaid: () => void; onCreateShareLink: () => void; onCopyShareLink: () => void; } +type ExportArtifact = { + label: string; + description: string; + buttonLabel: string; + disabled: boolean; + onExport: () => void; + ariaLabel: string; +}; + export function ExportModal({ isOpen, - exportDdlText, isCopied, hasDdlExport, + hasDiagramExport, shareLinkUrl, isCreatingShareLink, isShareLinkCopied, @@ -29,6 +41,9 @@ export function ExportModal({ canCreateShareLink, onCloseExport, onCopyExportDdl, + onDownloadSvg, + onDownloadUml, + onDownloadMermaid, onCreateShareLink, onCopyShareLink, }: ExportModalProps) { @@ -36,6 +51,50 @@ export function ExportModal({ if (!isOpen) return null; + const shareStatusKind = shareLinkError ? 'error' : isShareLinkCopied ? 'success' : 'neutral'; + const shareStatusRole = shareLinkError ? 'alert' : 'status'; + const shareStatusLive = shareLinkError ? 'assertive' : 'polite'; + const shareStatusMessage = shareLinkError + ? shareLinkError + : isShareLinkCopied + ? '링크가 복사되었습니다. 접근 권한이 있는 팀원이 최신 스냅샷을 열 수 있습니다.' + : '선택한 다이어그램을 공유하거나 산출물로 내보낼 준비가 되었습니다.'; + + const artifacts: ExportArtifact[] = [ + { + label: 'SQL DDL', + description: hasDdlExport ? '스키마 텍스트' : '먼저 테이블을 추가하세요', + buttonLabel: isCopied ? '복사 완료' : '내보내기', + disabled: !hasDdlExport, + onExport: onCopyExportDdl, + ariaLabel: 'SQL DDL 복사', + }, + { + label: 'SVG 이미지', + description: hasDiagramExport ? '다이어그램 파일' : '먼저 테이블을 추가하세요', + buttonLabel: '내보내기', + disabled: !hasDiagramExport, + onExport: onDownloadSvg, + ariaLabel: 'SVG 이미지 내보내기', + }, + { + label: 'PlantUML', + description: hasDiagramExport ? '텍스트 포맷' : '먼저 테이블을 추가하세요', + buttonLabel: '내보내기', + disabled: !hasDiagramExport, + onExport: onDownloadUml, + ariaLabel: 'PlantUML 내보내기', + }, + { + label: 'Mermaid', + description: hasDiagramExport ? '텍스트 포맷' : '먼저 테이블을 추가하세요', + buttonLabel: '내보내기', + disabled: !hasDiagramExport, + onExport: onDownloadMermaid, + ariaLabel: 'Mermaid 내보내기', + }, + ]; + return (

공유 및 내보내기

- 프로젝트 공유 링크를 만들고 현재 ERD의 DDL을 복사합니다. + 공유 링크를 만들거나 현재 ERD를 작업 가능한 산출물로 내보냅니다.

- +
-
-
-
- -

읽기 가능한 스냅샷과 내보내기 API로 연결되는 프로젝트 링크입니다.

-
- -
+
+
+ +

+ 팀원이 검토할 수 있는 API 기반 프로젝트 링크를 생성합니다. 복사 + 피드백은 작업 후에도 확인할 수 있게 유지합니다. +

+ + - {shareLinkUrl ? ( -
- +
+ {shareLinkUrl ? ( + + ) : ( + + )} +

+ 접근 권한은 프로젝트 설정에서 별도로 관리합니다. +

- ) : ( - - 프로젝트가 선택되면 서버에서 새 공유 링크를 발급할 수 있습니다. - - )} +
- {shareLinkError ? ( -
{shareLinkError}
- ) : null} -
+
+

내보내기 산출물

+

+ 공유 링크와 즉시 다운로드되는 캔버스 산출물을 분리해 무엇이 + 프로젝트 밖으로 나가는지 명확히 합니다. +

-
-
-
-

DDL 내보내기

-

현재 캔버스의 테이블, 컬럼, 관계를 SQL DDL 텍스트로 복사합니다.

+
+ {artifacts.map((artifact) => ( +
+
+ {artifact.label} + {artifact.description} +
+ +
+ ))}
- -
+
+ - {hasDdlExport ? ( -