From 83b2e18c1ce5809cd3979d877fbebf6b0628623b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:01:43 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat(a11y):=20add=20aria-labels=20to=20gene?= =?UTF-8?q?ric=20modal=20action=20buttons\n\nAdd=20explicit=20aria-labels?= =?UTF-8?q?=20to=20'=EC=B7=A8=EC=86=8C',=20'=EC=A0=80=EC=9E=A5',=20and=20'?= =?UTF-8?q?=EC=82=AD=EC=A0=9C'=20buttons=20in=20AddTableModal,=20EditTable?= =?UTF-8?q?Modal,=20and=20EditEdgeModal=20to=20provide=20context=20for=20s?= =?UTF-8?q?creen=20reader=20users.=20Updated=20relevant=20unit=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .Jules/palette.md | 6 ++++++ frontend/src/components/modals/AddTableModal.tsx | 3 ++- .../components/modals/DialogAccessibility.test.tsx | 2 +- frontend/src/components/modals/EditEdgeModal.tsx | 4 +++- frontend/src/components/modals/EditTableModal.tsx | 3 ++- .../src/components/modals/ModalCoverage.test.tsx | 12 ++++++------ 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index 7577a1ca..34dc037f 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -56,3 +56,9 @@ ## 2024-07-14 - Native Keyboard Submission with Forms for Modals **Learning:** Modals designed with plain `
` elements as wrappers instead of `
` lack native keyboard submission support, forcing users to switch from keyboard to mouse to confirm actions like "Save". **Action:** When designing modals or popups containing inputs, always use a `` element to wrap the content, handle the `onSubmit` event (calling `e.preventDefault()`), and set the primary confirmation button to `type="submit"` to enable seamless Enter-key submission for keyboard users. + + + +## 2024-07-16 - Add ARIA Labels to Generic Modal Action Buttons +**Learning:** In a canvas-based data modeling tool (like this ERD app) containing many separate modals for tasks like table editing, edge editing, and grouping, generic button text like "취소" (Cancel), "저장" (Save), and "삭제" (Delete) causes significant ambiguity for screen reader users when isolated from visual context. When navigating forms, screen reader users might not know *which* modal's action they are triggering. +**Action:** Added explicit context to generic buttons in `EditTableModal`, `AddTableModal`, and `EditEdgeModal` using `aria-label`s (e.g., `aria-label="테이블 편집 취소"` or `aria-label="a에서 b 관계 삭제"`). Always remember to update the corresponding tests (e.g. `getByRole`) whenever `aria-label` is changed. diff --git a/frontend/src/components/modals/AddTableModal.tsx b/frontend/src/components/modals/AddTableModal.tsx index 024bde03..15f2d57d 100644 --- a/frontend/src/components/modals/AddTableModal.tsx +++ b/frontend/src/components/modals/AddTableModal.tsx @@ -75,10 +75,11 @@ export function AddTableModal({ className="row" style={{ justifyContent: "flex-end", marginTop: 8 }} > - +
- + diff --git a/frontend/src/components/modals/EditTableModal.tsx b/frontend/src/components/modals/EditTableModal.tsx index 998929d4..a2a44726 100644 --- a/frontend/src/components/modals/EditTableModal.tsx +++ b/frontend/src/components/modals/EditTableModal.tsx @@ -223,11 +223,12 @@ export function EditTableModal({
- + diff --git a/frontend/src/components/modals/ModalCoverage.test.tsx b/frontend/src/components/modals/ModalCoverage.test.tsx index aa9dac53..be74abc2 100644 --- a/frontend/src/components/modals/ModalCoverage.test.tsx +++ b/frontend/src/components/modals/ModalCoverage.test.tsx @@ -69,7 +69,7 @@ describe('modal behavior coverage', () => { fireEvent.submit(screen.getByRole('dialog')) expect(setNewTableName).toHaveBeenCalledWith('users') expect(onSubmit).not.toHaveBeenCalled() - fireEvent.click(screen.getByRole('button', { name: '취소' })) + fireEvent.click(screen.getByRole('button', { name: '테이블 추가 취소' })) expect(onCancel).toHaveBeenCalledOnce() rerender( @@ -83,7 +83,7 @@ describe('modal behavior coverage', () => { ) fireEvent.submit(screen.getByRole('dialog')) expect(onSubmit).toHaveBeenCalledOnce() - expect(screen.getByRole('button', { name: '저장' })).toBeEnabled() + expect(screen.getByRole('button', { name: '테이블 추가 저장' })).toBeEnabled() }) it('covers EditEdgeModal visibility and actions', () => { @@ -116,9 +116,9 @@ describe('modal behavior coverage', () => { fireEvent.change(screen.getByLabelText('제약조건 이름 (Label)'), { target: { value: 'fk_changed' }, }) - fireEvent.click(screen.getByRole('button', { name: '삭제' })) - fireEvent.click(screen.getByRole('button', { name: '취소' })) - fireEvent.click(screen.getByRole('button', { name: '저장' })) + fireEvent.click(screen.getByRole('button', { name: 'a에서 b 관계 삭제' })) + fireEvent.click(screen.getByRole('button', { name: '관계 설정 취소' })) + fireEvent.click(screen.getByRole('button', { name: '관계 설정 저장' })) expect(setRelLabel).toHaveBeenCalledWith('fk_changed') expect(onDelete).toHaveBeenCalledOnce() expect(onCancel).toHaveBeenCalledOnce() @@ -191,7 +191,7 @@ describe('modal behavior coverage', () => { data: { title: 'public.users_copy' }, }) expect(duplicated.data.columns).not.toBe(tableNode.data.columns) - fireEvent.click(screen.getByRole('button', { name: '취소' })) + fireEvent.click(screen.getByRole('button', { name: '테이블 편집 취소' })) fireEvent.click(screen.getByRole('button', { name: '닫기' })) expect(onSubmit).toHaveBeenCalledOnce() expect(onDeleteTable).toHaveBeenCalledOnce() From 18f1bb4a929eb9933ea0013517cb58d2f40ffa18 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:08:23 +0000 Subject: [PATCH 2/5] =?UTF-8?q?feat(a11y):=20add=20aria-labels=20to=20gene?= =?UTF-8?q?ric=20modal=20action=20buttons\n\nAdd=20explicit=20aria-labels?= =?UTF-8?q?=20to=20'=EC=B7=A8=EC=86=8C',=20'=EC=A0=80=EC=9E=A5',=20and=20'?= =?UTF-8?q?=EC=82=AD=EC=A0=9C'=20buttons=20in=20AddTableModal,=20EditTable?= =?UTF-8?q?Modal,=20and=20EditEdgeModal=20to=20provide=20context=20for=20s?= =?UTF-8?q?creen=20reader=20users.=20Updated=20relevant=20unit=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 02439a97..9d7379b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,5 +36,6 @@ }, "overrides": { "esbuild": "^0.25.0" - } + }, + "packageManager": "pnpm@10.30.3" } From fa7ad0b88ce3bbda209a384ea93f6c2a71747ab2 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:45:14 +0000 Subject: [PATCH 3/5] =?UTF-8?q?feat(a11y):=20add=20aria-labels=20to=20gene?= =?UTF-8?q?ric=20modal=20action=20buttons\n\nAdd=20explicit=20aria-labels?= =?UTF-8?q?=20to=20'=EC=B7=A8=EC=86=8C',=20'=EC=A0=80=EC=9E=A5',=20and=20'?= =?UTF-8?q?=EC=82=AD=EC=A0=9C'=20buttons=20in=20AddTableModal,=20EditTable?= =?UTF-8?q?Modal,=20and=20EditEdgeModal=20to=20provide=20context=20for=20s?= =?UTF-8?q?creen=20reader=20users.=20Updated=20relevant=20unit=20tests.\n\?= =?UTF-8?q?nFixes=20an=20issue=20with=20coverage-evidence=20by=20explicitl?= =?UTF-8?q?y=20declaring=20packageManager:=20pnpm@10.30.3=20in=20the=20fro?= =?UTF-8?q?ntend=20package.json,=20which=20was=20causing=20the=20CI=20scri?= =?UTF-8?q?pt=20to=20fail=20with=20'Coverage=20package=20runner=20pnpm=20r?= =?UTF-8?q?equires=20an=20exact=20packageManager=20version'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 2992b5bb4b6c73f1bc17021ade0729a299fbe354 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:21:18 +0000 Subject: [PATCH 4/5] =?UTF-8?q?feat(a11y):=20add=20aria-labels=20to=20gene?= =?UTF-8?q?ric=20modal=20action=20buttons\n\nAdd=20explicit=20aria-labels?= =?UTF-8?q?=20to=20'=EC=B7=A8=EC=86=8C',=20'=EC=A0=80=EC=9E=A5',=20and=20'?= =?UTF-8?q?=EC=82=AD=EC=A0=9C'=20buttons=20in=20AddTableModal,=20EditTable?= =?UTF-8?q?Modal,=20and=20EditEdgeModal=20to=20provide=20context=20for=20s?= =?UTF-8?q?creen=20reader=20users.=20Updated=20relevant=20unit=20tests.\n\?= =?UTF-8?q?nFixes=20an=20issue=20with=20coverage-evidence=20by=20explicitl?= =?UTF-8?q?y=20declaring=20packageManager:=20pnpm@10.30.3=20in=20the=20fro?= =?UTF-8?q?ntend=20package.json,=20which=20was=20causing=20the=20CI=20scri?= =?UTF-8?q?pt=20to=20fail=20with=20'Coverage=20package=20runner=20pnpm=20r?= =?UTF-8?q?equires=20an=20exact=20packageManager=20version'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From fcaf8f0a071ed0ca84beef8b5afeeb3e96737ac9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:37:45 +0000 Subject: [PATCH 5/5] =?UTF-8?q?feat(a11y):=20add=20aria-labels=20to=20gene?= =?UTF-8?q?ric=20modal=20action=20buttons\n\nAdd=20explicit=20aria-labels?= =?UTF-8?q?=20to=20'=EC=B7=A8=EC=86=8C',=20'=EC=A0=80=EC=9E=A5',=20and=20'?= =?UTF-8?q?=EC=82=AD=EC=A0=9C'=20buttons=20in=20AddTableModal,=20EditTable?= =?UTF-8?q?Modal,=20and=20EditEdgeModal=20to=20provide=20context=20for=20s?= =?UTF-8?q?creen=20reader=20users.=20Updated=20relevant=20unit=20tests.\n\?= =?UTF-8?q?nFixes=20an=20issue=20with=20coverage-evidence=20by=20explicitl?= =?UTF-8?q?y=20declaring=20packageManager:=20pnpm@10.30.3=20in=20the=20fro?= =?UTF-8?q?ntend=20package.json,=20which=20was=20causing=20the=20CI=20scri?= =?UTF-8?q?pt=20to=20fail=20with=20'Coverage=20package=20runner=20pnpm=20r?= =?UTF-8?q?equires=20an=20exact=20packageManager=20version'.\n\nFixes=20an?= =?UTF-8?q?=20issue=20found=20by=20STRIX=20where=20EditTableModal=20failed?= =?UTF-8?q?=20to=20snapshot=20initial=20state,=20allowing=20unsubmitted=20?= =?UTF-8?q?changes=20to=20persist=20upon=20cancellation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .Jules/palette.md | 4 ++ .../src/components/modals/EditTableModal.tsx | 48 ++++++++++++------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/.Jules/palette.md b/.Jules/palette.md index 34dc037f..ab9c1610 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -62,3 +62,7 @@ ## 2024-07-16 - Add ARIA Labels to Generic Modal Action Buttons **Learning:** In a canvas-based data modeling tool (like this ERD app) containing many separate modals for tasks like table editing, edge editing, and grouping, generic button text like "취소" (Cancel), "저장" (Save), and "삭제" (Delete) causes significant ambiguity for screen reader users when isolated from visual context. When navigating forms, screen reader users might not know *which* modal's action they are triggering. **Action:** Added explicit context to generic buttons in `EditTableModal`, `AddTableModal`, and `EditEdgeModal` using `aria-label`s (e.g., `aria-label="테이블 편집 취소"` or `aria-label="a에서 b 관계 삭제"`). Always remember to update the corresponding tests (e.g. `getByRole`) whenever `aria-label` is changed. + +## 2026-07-16 - Safe Component Unmounting in React Modals +**Learning:** When a modal is closed, unsubmitted form state can inadvertently persist in parent component structures if state reset logic is absent. In `EditTableModal.tsx`, editing the graph (e.g. adding or removing columns) mutates `setNodes` directly, but clicking 'Cancel' only dismissed the modal. This creates a state desync where canceled changes remain in the diagram. Strix security scanners flag this as a business-logic integrity flaw. +**Action:** For modals that perform optimistic state updates on shared state objects (like nodes in a canvas), always take a deep-copy snapshot of the initial state upon opening. Ensure `onCancel` handles reverting the parent state back to this snapshot before dismissal. diff --git a/frontend/src/components/modals/EditTableModal.tsx b/frontend/src/components/modals/EditTableModal.tsx index a2a44726..c512662a 100644 --- a/frontend/src/components/modals/EditTableModal.tsx +++ b/frontend/src/components/modals/EditTableModal.tsx @@ -22,7 +22,27 @@ export function EditTableModal({ onEditTableSubmit, onDeleteTable, }: EditTableModalProps) { - const dialogRef = useDialogAccessibility(isOpen && Boolean(editingNode), onEditTableCancel); + // Snapshot the original node so we can restore it on cancel if needed + const [originalNodeSnapshot, setOriginalNodeSnapshot] = React.useState | null>(null); + + React.useEffect(() => { + if (isOpen && editingNode && !originalNodeSnapshot) { + setOriginalNodeSnapshot(JSON.parse(JSON.stringify(editingNode))); + } + if (!isOpen) { + setOriginalNodeSnapshot(null); + } + }, [isOpen, editingNode, originalNodeSnapshot]); + + const handleCancel = () => { + if (originalNodeSnapshot) { + setNodes((nds) => nds.map((n) => n.id === originalNodeSnapshot.id ? originalNodeSnapshot : n)); + setEditingNode(originalNodeSnapshot); + } + onEditTableCancel(); + }; + + const dialogRef = useDialogAccessibility(isOpen && Boolean(editingNode), handleCancel); if (!isOpen || !editingNode) return null; @@ -31,7 +51,7 @@ export function EditTableModal({

테이블 편집

- +
@@ -61,6 +81,12 @@ export function EditTableModal({
- +