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}
+
+
-
-
- ))}
+ );
+ })}