diff --git a/CHANGELOG.md b/CHANGELOG.md index 35613431a..512bb689c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- [FE] ♿ **내보내기 버튼 설명 연결**: ExportModal의 각 산출물 버튼을 대응 설명과 `aria-describedby`로 연결해 활성 상태에서는 산출물 맥락을, 비활성 상태에서는 생성 불가 사유를 스크린 리더가 함께 읽을 수 있도록 했습니다. 집중 테스트가 실제 설명 `id` 연결을 활성/비활성 상태에서 검증합니다. - [BE] 🔒 **Cryptography 50+ 보안 경계 갱신**: `pyproject.toml`과 두 hash-locked 요구사항 파일을 동일한 Cryptography 50+ 해석으로 정합화하여 PKCS#7 오류·타이밍 구분으로 인한 CVE-2026-69247 완화를 실제 설치·검증 경로에 반영했습니다. - [FE] ⚡ **검색 노드 참조 안정화 및 순차 스냅샷 폴링**: 같은 정규화 검색어와 원본 테이블 데이터에는 장식된 `node.data` 참조를 재사용하여 드래그 중 불필요한 하위 렌더링과 할당을 줄입니다. 스냅샷 폴링은 이전 요청이 끝난 뒤에만 다음 요청을 예약하며, 선택 변경·언마운트 후 도착한 오래된 성공 또는 실패 응답을 무시합니다. - [BE] 🔒 **공유 export 전 경로 redaction**: 공개 share의 SQL / index-design / reversing-spec export에서 코멘트·`example_value`를 제거합니다. 단위 테스트로 누출을 차단합니다. diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index c78944c31..41ba2e26e 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] ### Added +- **내보내기 접근성 설명 연결**: ExportModal의 각 산출물 버튼을 대응 설명과 `aria-describedby`로 연결하여 활성 상태의 산출물 맥락과 비활성 상태의 생성 불가 사유를 보조 기술에 함께 제공합니다. - **테이블 및 컬럼 편집 기능**: UI 패널을 통해 노드를 선택하고, 테이블의 이름/코멘트를 수정하며, 컬럼을 추가/수정/삭제하거나 테이블을 삭제할 수 있는 기능 추가. - **테스트 추가**: 프론트엔드 테스트 커버리지 100% 목표 달성을 위해 `cardinality.ts`, `types.ts`, `export.ts` 의 미달성 분기 및 함수 테스트 추가 (`cardinality_extra.test.ts` 등). - `.gitignore` 파일에 `coverage/` 폴더를 추가하여 불필요한 테스트 아티팩트가 커밋되지 않도록 보완. diff --git a/frontend/src/components/modals/ExportModal.ids.test.tsx b/frontend/src/components/modals/ExportModal.ids.test.tsx new file mode 100644 index 000000000..b769ecb7c --- /dev/null +++ b/frontend/src/components/modals/ExportModal.ids.test.tsx @@ -0,0 +1,56 @@ +import '@testing-library/jest-dom/vitest'; +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { ExportModal } from './ExportModal'; + +const baseProps = { + isOpen: true, + isCopied: false, + hasDdlExport: true, + hasDictionaryExport: true, + hasDiagramExport: true, + shareLinkUrl: '', + isCreatingShareLink: false, + isShareLinkCopied: false, + shareLinkError: null, + canCreateShareLink: true, + onCloseExport: vi.fn(), + onCopyExportDdl: vi.fn(), + onDownloadSvg: vi.fn(), + onDownloadUml: vi.fn(), + onDownloadMermaid: vi.fn(), + onExportDictionaryCsv: vi.fn(), + onExportDictionaryMarkdown: vi.fn(), + onDownloadDbml: vi.fn(), + onDownloadPrisma: vi.fn(), + onCreateShareLink: vi.fn(), + onCopyShareLink: vi.fn(), +}; + +afterEach(cleanup); + +describe('ExportModal artifact description ids', () => { + it('uses stable artifact identities rather than user-visible labels for aria relationships', () => { + render(); + + const expectedIds = new Map([ + ['SQL DDL 복사', 'export-artifact-desc-sql-ddl'], + ['SVG 이미지 내보내기', 'export-artifact-desc-svg'], + ['PlantUML 내보내기', 'export-artifact-desc-plantuml'], + ['Mermaid 내보내기', 'export-artifact-desc-mermaid'], + ['DBML 내보내기', 'export-artifact-desc-dbml'], + ['Prisma Schema 내보내기', 'export-artifact-desc-prisma'], + ['데이터 사전 CSV 내보내기', 'export-artifact-desc-dictionary-csv'], + ['데이터 사전 Markdown 내보내기', 'export-artifact-desc-dictionary-markdown'], + ]); + + for (const [buttonName, descriptionId] of expectedIds) { + expect(screen.getByRole('button', { name: buttonName })).toHaveAttribute( + 'aria-describedby', + descriptionId, + ); + expect(document.getElementById(descriptionId)).toBeInTheDocument(); + } + }); +}); diff --git a/frontend/src/components/modals/ExportModal.test.tsx b/frontend/src/components/modals/ExportModal.test.tsx index bd89c5384..52754d3c7 100644 --- a/frontend/src/components/modals/ExportModal.test.tsx +++ b/frontend/src/components/modals/ExportModal.test.tsx @@ -161,14 +161,36 @@ describe('ExportModal', () => { ); expect(screen.getAllByText('먼저 테이블을 추가하세요')).toHaveLength(8); - 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(); - expect(screen.getByRole('button', { name: 'DBML 내보내기' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Prisma Schema 내보내기' })).toBeDisabled(); - expect(screen.getByRole('button', { name: '데이터 사전 CSV 내보내기' })).toBeDisabled(); - expect(screen.getByRole('button', { name: '데이터 사전 Markdown 내보내기' })).toBeDisabled(); + const exportButtonNames = [ + 'SQL DDL 복사', + 'SVG 이미지 내보내기', + 'PlantUML 내보내기', + 'Mermaid 내보내기', + 'DBML 내보내기', + 'Prisma Schema 내보내기', + '데이터 사전 CSV 내보내기', + '데이터 사전 Markdown 내보내기', + ]; + + for (const name of exportButtonNames) { + const button = screen.getByRole('button', { name }); + expect(button).toBeDisabled(); + const descriptionId = button.getAttribute('aria-describedby'); + expect(descriptionId).toBeTruthy(); + expect(document.getElementById(descriptionId!)).toHaveTextContent( + '먼저 테이블을 추가하세요', + ); + } + }); + + it('describes enabled exports with their artifact context', () => { + render(); + + const ddlButton = screen.getByRole('button', { name: 'SQL DDL 복사' }); + expect(ddlButton).toBeEnabled(); + const descriptionId = ddlButton.getAttribute('aria-describedby'); + expect(descriptionId).toBeTruthy(); + expect(document.getElementById(descriptionId!)).toHaveTextContent('스키마 텍스트'); }); it('exposes access-control guidance for disabled button', () => { @@ -180,4 +202,4 @@ describe('ExportModal', () => { expect(accessManagementButton).toHaveAttribute('aria-describedby', 'share-export-access-hint'); expect(accessManagementButton).not.toHaveAttribute('title'); }); -}); +}); \ No newline at end of file diff --git a/frontend/src/components/modals/ExportModal.tsx b/frontend/src/components/modals/ExportModal.tsx index 995393ceb..5d2d1fb36 100644 --- a/frontend/src/components/modals/ExportModal.tsx +++ b/frontend/src/components/modals/ExportModal.tsx @@ -26,6 +26,7 @@ interface ExportModalProps { } type ExportArtifact = { + id: string; label: string; description: string; buttonLabel: string; @@ -72,6 +73,7 @@ export function ExportModal({ const artifacts: ExportArtifact[] = [ { + id: 'sql-ddl', label: 'SQL DDL', description: hasDdlExport ? '스키마 텍스트' : '먼저 테이블을 추가하세요', buttonLabel: isCopied ? '복사 완료' : '내보내기', @@ -80,6 +82,7 @@ export function ExportModal({ ariaLabel: 'SQL DDL 복사', }, { + id: 'svg', label: 'SVG 이미지', description: hasDiagramExport ? '다이어그램 파일' : '먼저 테이블을 추가하세요', buttonLabel: '내보내기', @@ -88,6 +91,7 @@ export function ExportModal({ ariaLabel: 'SVG 이미지 내보내기', }, { + id: 'plantuml', label: 'PlantUML', description: hasDiagramExport ? '텍스트 포맷' : '먼저 테이블을 추가하세요', buttonLabel: '내보내기', @@ -96,6 +100,7 @@ export function ExportModal({ ariaLabel: 'PlantUML 내보내기', }, { + id: 'mermaid', label: 'Mermaid', description: hasDiagramExport ? '텍스트 포맷' : '먼저 테이블을 추가하세요', buttonLabel: '내보내기', @@ -104,6 +109,7 @@ export function ExportModal({ ariaLabel: 'Mermaid 내보내기', }, { + id: 'dbml', label: 'DBML', description: hasDiagramExport ? '텍스트 포맷' : '먼저 테이블을 추가하세요', buttonLabel: '내보내기', @@ -112,6 +118,7 @@ export function ExportModal({ ariaLabel: 'DBML 내보내기', }, { + id: 'prisma', label: 'Prisma Schema', description: hasDiagramExport ? '텍스트 포맷' : '먼저 테이블을 추가하세요', buttonLabel: '내보내기', @@ -120,6 +127,7 @@ export function ExportModal({ ariaLabel: 'Prisma Schema 내보내기', }, { + id: 'dictionary-csv', label: 'Data Dictionary CSV', description: hasDictionaryExport ? '테이블/컬럼 목록' : '먼저 테이블을 추가하세요', buttonLabel: '내보내기', @@ -128,6 +136,7 @@ export function ExportModal({ ariaLabel: '데이터 사전 CSV 내보내기', }, { + id: 'dictionary-markdown', label: 'Data Dictionary MD', description: hasDictionaryExport ? '마크다운 문서' : '먼저 테이블을 추가하세요', buttonLabel: '내보내기', @@ -222,23 +231,27 @@ export function ExportModal({

- {artifacts.map((artifact) => ( -
-
- {artifact.label} - {artifact.description} + {artifacts.map((artifact) => { + const descId = `export-artifact-desc-${artifact.id}`; + return ( +
+
+ {artifact.label} + {artifact.description} +
+
- -
- ))} + ); + })}