-
+
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({