From e6e3c93f9f303153de844089e2d9e4c47413cb65 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:33:36 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0]=20=EB=82=B4=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20aria-describedby=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก What: ExportModal์˜ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ๋“ค์— `aria-describedby` ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์˜†์— ์žˆ๋Š” ์„ค๋ช… ํ…์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๐ŸŽฏ Why: ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์„ ๋•Œ(์˜ˆ: "๋จผ์ € ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”"), ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž๊ฐ€ ์™œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ์ˆ˜ ์—†๋Š”์ง€ ์ด์œ ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋งฅ๋ฝ์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ๐Ÿ“ธ Before/After: - Before: `` - After: `๋จผ์ € ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š” ` โ™ฟ Accessibility: ๋น„ํ™œ์„ฑํ™”๋œ ์ปจํŠธ๋กค์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ์‚ฌ์œ ๋ฅผ ๋ณด์กฐ ๊ธฐ๊ธฐ์— ์ „๋‹ฌํ•˜์—ฌ WCAG์˜ ํผ ์ปจํŠธ๋กค ์ง€์นจ๊ณผ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์„ฑ์„ ํ–ฅ์ƒ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. --- .jules/palette.md | 4 +++ .../src/components/modals/ExportModal.tsx | 36 ++++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index bd0f73248..fcd2da1c1 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -57,3 +57,7 @@ ## 2026-07-30 - Add window.confirm for destructive actions **Learning:** Destructive actions like deleting groups and edge relationships previously occurred immediately without user confirmation. **Action:** Always wrap delete operations with window.confirm() dialogs and ensure corresponding tests successfully mock window.confirm. + +## 2026-10-25 - ๋‚ด๋ณด๋‚ด๊ธฐ ๋ชจ๋‹ฌ ๋ฒ„ํŠผ ์ ‘๊ทผ์„ฑ ๊ฐœ์„  +**Learning:** ExportModal์˜ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์„ ๋•Œ, ์‹œ๊ฐ์ ์œผ๋กœ๋Š” "๋จผ์ € ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”"๋ผ๋Š” ์„ค๋ช…์ด ๋ณด์ด์ง€๋งŒ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์ด ๋งฅ๋ฝ์ด ์ „๋‹ฌ๋˜์ง€ ์•Š์•„ ์™œ ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. +**Action:** ๋น„ํ™œ์„ฑํ™”๋  ์ˆ˜ ์žˆ๋Š” ๋ฒ„ํŠผ ๊ณ์— ์žˆ๋Š” ์„ค๋ช… ํ…์ŠคํŠธ ์š”์†Œ์— id๋ฅผ ๋ถ€์—ฌํ•˜๊ณ , ๋ฒ„ํŠผ์— `aria-describedby`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ๋น„ํ™œ์„ฑํ™” ์‚ฌ์œ ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋„๋ก ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. diff --git a/frontend/src/components/modals/ExportModal.tsx b/frontend/src/components/modals/ExportModal.tsx index 995393ceb..aff876900 100644 --- a/frontend/src/components/modals/ExportModal.tsx +++ b/frontend/src/components/modals/ExportModal.tsx @@ -222,23 +222,27 @@ export function ExportModal({

- {artifacts.map((artifact) => ( -
-
- {artifact.label} - {artifact.description} + {artifacts.map((artifact) => { + const descId = `export-artifact-desc-${artifact.label.replace(/\s+/g, '-')}`; + return ( +
+
+ {artifact.label} + {artifact.description} +
+
- -
- ))} + ); + })}
From 71e01947d11ec9f6bc888e520a0c48e2aa0b6566 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 10:58:54 +0900 Subject: [PATCH 2/9] fix(docs): remove future-dated unverified export doctrine --- .jules/palette.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index fcd2da1c1..bd0f73248 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -57,7 +57,3 @@ ## 2026-07-30 - Add window.confirm for destructive actions **Learning:** Destructive actions like deleting groups and edge relationships previously occurred immediately without user confirmation. **Action:** Always wrap delete operations with window.confirm() dialogs and ensure corresponding tests successfully mock window.confirm. - -## 2026-10-25 - ๋‚ด๋ณด๋‚ด๊ธฐ ๋ชจ๋‹ฌ ๋ฒ„ํŠผ ์ ‘๊ทผ์„ฑ ๊ฐœ์„  -**Learning:** ExportModal์˜ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์„ ๋•Œ, ์‹œ๊ฐ์ ์œผ๋กœ๋Š” "๋จผ์ € ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”"๋ผ๋Š” ์„ค๋ช…์ด ๋ณด์ด์ง€๋งŒ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์ด ๋งฅ๋ฝ์ด ์ „๋‹ฌ๋˜์ง€ ์•Š์•„ ์™œ ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. -**Action:** ๋น„ํ™œ์„ฑํ™”๋  ์ˆ˜ ์žˆ๋Š” ๋ฒ„ํŠผ ๊ณ์— ์žˆ๋Š” ์„ค๋ช… ํ…์ŠคํŠธ ์š”์†Œ์— id๋ฅผ ๋ถ€์—ฌํ•˜๊ณ , ๋ฒ„ํŠผ์— `aria-describedby`๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์Šคํฌ๋ฆฐ ๋ฆฌ๋”๊ฐ€ ๋น„ํ™œ์„ฑํ™” ์‚ฌ์œ ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋„๋ก ์—ฐ๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. From 5f7bbac24801147e0b00aad3653a1edf903f338c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:08:07 +0900 Subject: [PATCH 3/9] test(a11y): verify export buttons expose disabled reasons --- .../components/modals/ExportModal.test.tsx | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/modals/ExportModal.test.tsx b/frontend/src/components/modals/ExportModal.test.tsx index bd89c5384..71ea63f5b 100644 --- a/frontend/src/components/modals/ExportModal.test.tsx +++ b/frontend/src/components/modals/ExportModal.test.tsx @@ -161,14 +161,26 @@ 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('exposes access-control guidance for disabled button', () => { @@ -180,4 +192,4 @@ describe('ExportModal', () => { expect(accessManagementButton).toHaveAttribute('aria-describedby', 'share-export-access-hint'); expect(accessManagementButton).not.toHaveAttribute('title'); }); -}); +}); \ No newline at end of file From aa934156fa5bbe66ae04e93fe69396ed6b219ae9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:09:01 +0900 Subject: [PATCH 4/9] test(a11y): cover enabled export descriptions --- frontend/src/components/modals/ExportModal.test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/components/modals/ExportModal.test.tsx b/frontend/src/components/modals/ExportModal.test.tsx index 71ea63f5b..52754d3c7 100644 --- a/frontend/src/components/modals/ExportModal.test.tsx +++ b/frontend/src/components/modals/ExportModal.test.tsx @@ -183,6 +183,16 @@ describe('ExportModal', () => { } }); + 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', () => { render(); From 0118deeba43d61b66f226e90e436bd1ff8e6c4c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:09:13 +0900 Subject: [PATCH 5/9] docs(frontend): record export accessibility descriptions --- frontend/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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/` ํด๋”๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ ์•„ํ‹ฐํŒฉํŠธ๊ฐ€ ์ปค๋ฐ‹๋˜์ง€ ์•Š๋„๋ก ๋ณด์™„. From c2fdfd8b06cab86f684d8ae4542b4eb6a9779103 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:09:28 +0900 Subject: [PATCH 6/9] docs(changelog): record export a11y descriptions --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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`๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๋ˆ„์ถœ์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. From 39a7e5dd9822aa5359a2954a396850fb5c982f9e Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:49:33 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0]=20=EB=82=B4=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20aria-describedby=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ’ก What: ExportModal์˜ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ๋“ค์— `aria-describedby` ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์˜†์— ์žˆ๋Š” ์„ค๋ช… ํ…์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๐ŸŽฏ Why: ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ์„ ๋•Œ(์˜ˆ: "๋จผ์ € ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”"), ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž๊ฐ€ ์™œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ์ˆ˜ ์—†๋Š”์ง€ ์ด์œ ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋งฅ๋ฝ์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ๐Ÿ“ธ Before/After: - Before: `` - After: `๋จผ์ € ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š” ` โ™ฟ Accessibility: ๋น„ํ™œ์„ฑํ™”๋œ ์ปจํŠธ๋กค์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ์‚ฌ์œ ๋ฅผ ๋ณด์กฐ ๊ธฐ๊ธฐ์— ์ „๋‹ฌํ•˜์—ฌ WCAG์˜ ํผ ์ปจํŠธ๋กค ์ง€์นจ๊ณผ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์„ฑ์„ ํ–ฅ์ƒ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. From 26ff208c3da3f44f898a020ad432165414bd8c6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 16:20:59 +0900 Subject: [PATCH 8/9] test(a11y): require stable export artifact description ids --- .../modals/ExportModal.ids.test.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 frontend/src/components/modals/ExportModal.ids.test.tsx 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(); + } + }); +}); From 87516b519717ee7ab4307a06a31f978f74b6b7cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 16:21:57 +0900 Subject: [PATCH 9/9] fix(a11y): decouple description ids from visible labels --- frontend/src/components/modals/ExportModal.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/modals/ExportModal.tsx b/frontend/src/components/modals/ExportModal.tsx index aff876900..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: '๋‚ด๋ณด๋‚ด๊ธฐ', @@ -223,9 +232,9 @@ export function ExportModal({
{artifacts.map((artifact) => { - const descId = `export-artifact-desc-${artifact.label.replace(/\s+/g, '-')}`; + const descId = `export-artifact-desc-${artifact.id}`; return ( -
+
{artifact.label} {artifact.description} @@ -236,7 +245,7 @@ export function ExportModal({ disabled={artifact.disabled} aria-label={artifact.ariaLabel} aria-describedby={descId} - aria-live={artifact.label === 'SQL DDL' ? 'polite' : undefined} + aria-live={artifact.id === 'sql-ddl' ? 'polite' : undefined} > {artifact.buttonLabel}