Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c9b9d87
feat(ux): Add visual indicators for required form fields
seonghobae Aug 19, 2026
b472f26
fix: enforce required modal fields
seonghobae Aug 19, 2026
66bb435
feat(ux): Add visual indicators for required form fields
seonghobae Aug 19, 2026
2906c22
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 19, 2026
4a64e03
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 19, 2026
c3b0de9
Merge branch 'main' into palette-ux-required-fields-2840570704280580839
seonghobae Aug 20, 2026
ec24bc0
Merge branch 'main' into palette-ux-required-fields-2840570704280580839
seonghobae Aug 20, 2026
aaffad0
fix(a11y): enforce required modal fields
seonghobae Aug 20, 2026
2349db9
test: cover required modal fields and frontend advisory
seonghobae Aug 20, 2026
57b673c
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 20, 2026
1cf1a6a
test: close frontend coverage gaps
seonghobae Aug 20, 2026
ae5ab8f
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
7f168b4
fix: retain required field coverage and guards
seonghobae Aug 20, 2026
cb17746
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
2543dff
fix: enforce required modal input validation
seonghobae Aug 20, 2026
be5a839
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
8829b36
fix: restore required modal validation coverage
seonghobae Aug 20, 2026
4f37dbf
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
78c8edc
fix: restore modal validation and complete coverage
seonghobae Aug 20, 2026
06e562a
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
3e4088e
Revert "test(ux): fix failing test cases by matching label correctly …
seonghobae Aug 20, 2026
48f2f48
test: stabilize frontend coverage suite
seonghobae Aug 20, 2026
55d530e
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
e9cd0cf
Revert "test(ux): fix failing test cases by matching label correctly …
seonghobae Aug 20, 2026
cc66e59
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
9f3879c
Revert "test(ux): fix failing test cases by matching label correctly …
seonghobae Aug 20, 2026
0dd3ae1
fix(ux): restore required-field validation and dependency pins
seonghobae Aug 20, 2026
30a52df
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
812e2b8
Revert "test(ux): fix failing test cases by matching label correctly …
seonghobae Aug 20, 2026
eaa426d
test(ux): allow complete editor coverage run
seonghobae Aug 20, 2026
778f08c
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
591e0c9
fix(ux): preserve dependency security and required field coverage
seonghobae Aug 20, 2026
6ad886a
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
6b7eed0
fix(ux): reject whitespace-only modal fields
seonghobae Aug 20, 2026
d81c331
Merge remote-tracking branch 'origin/palette-ux-required-fields-28405…
seonghobae Aug 20, 2026
ec395cd
fix(ux): restore required-field release and dependency guards
seonghobae Aug 20, 2026
b373b49
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 20, 2026
786ba51
fix: enforce required relation labels
seonghobae Aug 21, 2026
4280922
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 21, 2026
7cb8854
test(ux): fix failing test cases by matching label correctly with regex
seonghobae Aug 21, 2026
5dd6967
test(a11y): pin required modal validation contract
seonghobae Aug 30, 2026
94737f9
fix(a11y): align required modal validation and remove drift
seonghobae Aug 30, 2026
4ee42a5
fix(ux): explain invalid group names and document modal APIs
seonghobae Aug 30, 2026
ec67fdd
test(a11y): pin empty required-field guidance
seonghobae Aug 30, 2026
0e18ea2
fix(a11y): preserve guidance for empty required fields
seonghobae Aug 30, 2026
1dd642d
test(a11y): clean up DOM after required field validations tests
seonghobae Aug 30, 2026
7752a7a
test(a11y): clean up DOM after required field validations tests
seonghobae Aug 30, 2026
d2fbf9b
docs(test): diagnose duplicate Testing Library matches accurately
seonghobae Aug 31, 2026
b92823c
docs(palette): update learning to treat multiple elements as query sc…
seonghobae Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,6 @@
## 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-08-30 - Fix RTL DOM Bleed Causing Extraneous Rendered Components
**Learning:** In frontend Vitest tests, the guidance now treats "Found multiple elements" as a duplicate-match symptom rather than proof of failed auto-cleanup, requires inspecting the rendered DOM/query scope first, and uses explicit cleanup only when cross-test bleed is actually verified.
**Action:** Always inspect the rendered DOM and query scope when debugging multiple-elements errors, applying explicit cleanup only as a verified last resort for actual state bleed.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- [FE] ♿ **필수 모달 입력 검증 정합화**: 필수 입력 표시를 공유 `--color-danger` 토큰 기반 컴포넌트로 통일하고, 관계 제약조건/업무 그룹 이름의 공백-only 입력을 저장하지 않으며 고객이 다음에 입력해야 할 값을 브라우저 검증 메시지로 안내합니다.
- [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`를 제거합니다. 단위 테스트로 누출을 차단합니다.
Expand Down
35 changes: 35 additions & 0 deletions docs/doctoring/required-modal-field-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Required modal field validation

## Buyer problem

Several modal forms displayed a visual required marker while the executable constraint did not match it. In particular, relationship labels and business-group names could contain only whitespace: HTML `required` treats the empty string as missing, but whitespace is still a non-empty value. That mismatch made the UI promise stricter validation than the form actually enforced.

## Product contract

- Native `required` remains the semantic baseline for every mandatory text input in this slice.
- Relationship labels and business-group names additionally reject whitespace-only values before the mutation callback runs.
- Empty relationship and group names receive the same actionable Korean guidance through the input's native invalid event; whitespace-only values receive equivalent guidance through the explicit trim guard.
- The business-group form also renders linked guidance while its Add action is disabled, so the customer can see the next action without needing to trigger an unavailable submit control.
- Changing either value clears the stale custom validity message immediately.
- The visual `*` is centralized in `RequiredIndicator`, uses the existing `--color-danger` token, and is `aria-hidden="true"` because the input itself carries the required semantics.
- No API, database, persistence, authorization, or deployment contract changes.

## Design and accessibility authority

Fresh Figma evidence on 2026-08-30 confirms file `csnpEEJfmqFWB0vNUoTkWA`, page `190:2` (`Design System — Storybook Contract`), frame `190:40` (`Design Token Authority`), including `--color-danger = #b91c1c` and the instruction to change runtime source first, keep Storybook exact-value tests green, then resync Figma. The paired executable Storybook token inventory is currently owned by PR #944 at exact head `ffcf653e78306563797262678a4313c8215a5932` and renders exact token values as text while marking decorative previews `aria-hidden`.

This PR reuses that shared token contract rather than adding a local color. It does not claim a current Storybook component-state story for required-field validation: the protected base does not yet contain #944. Therefore design acceptance remains fail-closed until the required-field state is represented against the then-current paired Figma and Storybook authority before merge.

## Verification

Focused tests pin semantic `required` state for add-table, relationship, group, table-title, column-name, and column-type inputs; empty and whitespace-only validation guidance; whitespace-only mutation blocking; linked visible group guidance; clearing stale custom validity; and the decorative marker's exclusion from the accessibility tree. Existing modal coverage continues to exercise valid submit/cancel/delete flows.

## Standards traceability

The HTML Standard defines `required` as a boolean constraint and treats a required mutable input whose value is the empty string as missing. The same standard exposes `setCustomValidity()` and `reportValidity()` for constraints beyond the native empty-string rule. WAI-ARIA 1.2 specifies that elements with `aria-hidden="true"` should not be included in the accessibility tree; the marker is non-focusable decoration, not the semantic source of required state.

### References

WHATWG. (2026). *HTML Standard*. Retrieved August 30, 2026, from https://html.spec.whatwg.org/

World Wide Web Consortium. (2023). *Accessible Rich Internet Applications (WAI-ARIA) 1.2* (W3C Recommendation, June 6, 2023). https://www.w3.org/TR/wai-aria-1.2/
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
3 changes: 3 additions & 0 deletions frontend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
- **테스트 추가**: 프론트엔드 테스트 커버리지 100% 목표 달성을 위해 `cardinality.ts`, `types.ts`, `export.ts` 의 미달성 분기 및 함수 테스트 추가 (`cardinality_extra.test.ts` 등).
- `.gitignore` 파일에 `coverage/` 폴더를 추가하여 불필요한 테스트 아티팩트가 커밋되지 않도록 보완.

### Fixed
- 필수 모달 필드의 시각적 표시와 실제 `required`/공백 검증을 정합화하고, 유효하지 않은 관계·그룹 이름에는 고객이 다음에 입력할 값을 설명하는 검증 메시지를 제공합니다.

### Security
- `postcss` 트랜지티브 의존성을 `overrides`로 `^8.5.18`(8.5.25로 해석)에 고정해 GHSA-r28c-9q8g-f849(High, source-map 자동 로딩의 경로 순회를 통한 임의 `.map` 노출)을 제거했습니다. postcss는 Vite 빌드 툴체인 전용 트랜지티브 의존성으로 배포 런타임 경로에 없으며, 조치 후 `npm audit --audit-level=high` → High 이상 취약점 0건. 변경 범위는 `frontend/package.json`의 `overrides` 맵(`postcss` 항목 추가)과 재생성된 `frontend/package-lock.json`이며, 애플리케이션 소스는 수정하지 않았습니다.
6 changes: 5 additions & 1 deletion frontend/src/components/modals/AddTableModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { RequiredIndicator } from './RequiredIndicator';
import { useDialogAccessibility } from './useDialogAccessibility';

interface AddTableModalProps {
Expand All @@ -9,6 +10,7 @@ interface AddTableModalProps {
onAddTableSubmit: () => void;
}

/** Renders the add-table dialog with native required-name semantics. */
export function AddTableModal({
isOpen,
newTableName,
Expand Down Expand Up @@ -61,7 +63,9 @@ export function AddTableModal({
>
<h3 id="add-table-title">테이블 추가</h3>
<div className="field">
<label htmlFor="new-table-name">테이블 이름</label>
<label htmlFor="new-table-name">
테이블 이름 <RequiredIndicator />
</label>
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
<input
id="new-table-name"
value={newTableName}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/modals/DialogAccessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('modal dialog accessibility', () => {
const opener = screen.getByRole('button', { name: 'Open group manager' });
await user.click(opener);

await waitFor(() => expect(screen.getByLabelText('그룹 이름')).toHaveFocus());
await waitFor(() => expect(screen.getByLabelText(/그룹 이름/)).toHaveFocus());
await user.keyboard('{Escape}');
expect(onCloseGroupManager).toHaveBeenCalledOnce();

Expand All @@ -77,7 +77,7 @@ describe('modal dialog accessibility', () => {
/>,
);

const tableNameInput = screen.getByLabelText('테이블 이름');
const tableNameInput = screen.getByLabelText(/테이블 이름/);
const saveButton = screen.getByRole('button', { name: '저장' });

await waitFor(() => expect(tableNameInput).toHaveFocus());
Expand Down
26 changes: 24 additions & 2 deletions frontend/src/components/modals/EditEdgeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import type { Edge } from "@xyflow/react";
import { RequiredIndicator } from './RequiredIndicator';
import { useDialogAccessibility } from './useDialogAccessibility';

interface EditEdgeModalProps {
Expand All @@ -11,6 +12,7 @@ interface EditEdgeModalProps {
onRelSubmit: () => void;
}

/** Renders relationship editing and blocks blank constraint names before mutation. */
export function EditEdgeModal({
editingEdge,
relLabel,
Expand All @@ -20,6 +22,7 @@ export function EditEdgeModal({
onRelSubmit,
}: EditEdgeModalProps) {
const dialogRef = useDialogAccessibility<HTMLFormElement>(Boolean(editingEdge), onRelCancel);
const relLabelInputRef = React.useRef<HTMLInputElement>(null);

if (!editingEdge) return null;

Expand Down Expand Up @@ -48,6 +51,13 @@ export function EditEdgeModal({
tabIndex={-1}
onSubmit={(e) => {
e.preventDefault();
const input = relLabelInputRef.current;
if (!relLabel.trim()) {
input?.setCustomValidity("제약조건 이름을 입력하세요.");
input?.reportValidity();
return;
}
input?.setCustomValidity("");
onRelSubmit();
}}
style={{
Expand All @@ -66,13 +76,25 @@ export function EditEdgeModal({
To: {editingEdge.target}
</div>
<div className="field">
<label htmlFor="rel-label">제약조건 이름 (Label)</label>
<label htmlFor="rel-label">
제약조건 이름 (Label) <RequiredIndicator />
</label>
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
seonghobae marked this conversation as resolved.
Resolved
<input
id="rel-label"
ref={relLabelInputRef}
value={relLabel}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
onChange={(e) => setRelLabel(e.target.value)}
onInvalid={(e) => {
if (!e.currentTarget.value.trim()) {
e.currentTarget.setCustomValidity("제약조건 이름을 입력하세요.");
}
}}
Comment thread
seonghobae marked this conversation as resolved.
Resolved
onChange={(e) => {
e.currentTarget.setCustomValidity("");
setRelLabel(e.target.value);
}}
placeholder="fk_constraint_name"
autoFocus
required
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
/>
</div>
<div
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/components/modals/EditTableModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import type { Node } from "@xyflow/react";
import type { TableNodeData } from "../../erd/convert";
import { RequiredIndicator } from './RequiredIndicator';
import { useDialogAccessibility } from './useDialogAccessibility';

interface EditTableModalProps {
Expand All @@ -13,6 +14,7 @@ interface EditTableModalProps {
onDeleteTable: () => void;
}

/** Renders table and column editing with native required-field semantics. */
export function EditTableModal({
isOpen,
editingNode,
Expand All @@ -36,12 +38,15 @@ export function EditTableModal({
<div style={{ overflowY: "auto", padding: "0 4px", flex: 1 }}>
<form id="editTableForm" onSubmit={onEditTableSubmit} className="col" style={{ gap: 12 }}>
<div className="col">
<label htmlFor="editTableTitle">테이블명 (schema.table)</label>
<label htmlFor="editTableTitle">
테이블명 (schema.table) <RequiredIndicator />
</label>
<input
id="editTableTitle"
name="title"
defaultValue={editingNode.data.title}
placeholder="public.users"
required
Comment thread
seonghobae marked this conversation as resolved.
Resolved
autoFocus
/>
</div>
Expand Down Expand Up @@ -114,15 +119,17 @@ export function EditTableModal({
type="text"
name={`col_name_${idx}`}
defaultValue={col.column_name}
placeholder="컬럼명"
placeholder="컬럼명 (필수)"
required
style={{ flex: 2 }}
aria-label={`${col.column_name} 컬럼명`}
/>
<input
type="text"
name={`col_type_${idx}`}
defaultValue={col.data_type}
placeholder="데이터 타입"
placeholder="데이터 타입 (필수)"
required
style={{ flex: 1.5 }}
aria-label={`${col.column_name} 데이터 타입`}
/>
Expand Down
45 changes: 42 additions & 3 deletions frontend/src/components/modals/GroupModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import type { Node } from "@xyflow/react";
import type { TableNodeData } from "../../erd/convert";
import { BUSINESS_GROUP_COLORS, type BusinessGroup } from "../../erd/businessGroups";
import { RequiredIndicator } from './RequiredIndicator';
import { useDialogAccessibility } from './useDialogAccessibility';

interface GroupModalProps {
Expand All @@ -18,6 +19,7 @@ interface GroupModalProps {
onAssignBusinessGroup: (nodeId: string, groupId: string) => void;
}

/** Renders business-group management and prevents blank group creation. */
export function GroupModal({
isOpen,
businessGroups,
Expand All @@ -32,6 +34,11 @@ export function GroupModal({
onAssignBusinessGroup,
}: GroupModalProps) {
const dialogRef = useDialogAccessibility(isOpen, onCloseGroupManager);
const groupNameInputRef = React.useRef<HTMLInputElement>(null);
const hasMissingGroupName = !newGroupName.trim();
const groupNameGuidance = newGroupName.length === 0
? "그룹 이름을 입력하세요."
: "공백이 아닌 그룹 이름을 입력하세요.";
Comment thread
seonghobae marked this conversation as resolved.
Resolved

if (!isOpen) return null;

Expand All @@ -56,16 +63,48 @@ export function GroupModal({
</button>
</div>

<form className="groupManager__create" onSubmit={(e) => { e.preventDefault(); if (newGroupName.trim()) { onCreateBusinessGroup(); } }}>
<form
className="groupManager__create"
onSubmit={(e) => {
e.preventDefault();
const input = groupNameInputRef.current;
if (!newGroupName.trim()) {
input?.setCustomValidity("그룹 이름을 입력하세요.");
input?.reportValidity();
return;
}
input?.setCustomValidity("");
onCreateBusinessGroup();
}}
>
<div className="field">
<label htmlFor="business-group-name">그룹 이름</label>
<label htmlFor="business-group-name">
그룹 이름 <RequiredIndicator />
</label>
<input
autoFocus
id="business-group-name"
ref={groupNameInputRef}
value={newGroupName}
onChange={(event) => setNewGroupName(event.target.value)}
onInvalid={(event) => {
if (!event.currentTarget.value.trim()) {
event.currentTarget.setCustomValidity("그룹 이름을 입력하세요.");
}
}}
onChange={(event) => {
event.currentTarget.setCustomValidity("");
setNewGroupName(event.target.value);
}}
placeholder="Billing"
required
aria-invalid={hasMissingGroupName || undefined}
aria-describedby={hasMissingGroupName ? "business-group-name-error" : undefined}
/>
{hasMissingGroupName ? (
<div id="business-group-name-error" className="field-hint" role="status">
{groupNameGuidance}
</div>
) : null}
</div>
<div
className="groupManager__swatches"
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/modals/ModalCoverage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('modal behavior coverage', () => {
onAddTableSubmit={onSubmit}
/>,
)
fireEvent.change(screen.getByLabelText('테이블 이름'), { target: { value: 'users' } })
fireEvent.change(screen.getByLabelText(/테이블 이름/), { target: { value: 'users' } })
fireEvent.submit(screen.getByRole('dialog'))
expect(setNewTableName).toHaveBeenCalledWith('users')
expect(onSubmit).not.toHaveBeenCalled()
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('modal behavior coverage', () => {
/>,
)
expect(screen.getByText(/From: a/)).toBeInTheDocument()
fireEvent.change(screen.getByLabelText('제약조건 이름 (Label)'), {
fireEvent.change(screen.getByLabelText(/제약조건 이름 \(Label\)/), {
target: { value: 'fk_changed' },
})
vi.spyOn(window, 'confirm').mockReturnValueOnce(true)
Expand Down Expand Up @@ -261,7 +261,7 @@ describe('modal behavior coverage', () => {
onAssignBusinessGroup={onAssign}
/>,
)
fireEvent.change(screen.getByLabelText('그룹 이름'), { target: { value: 'New' } })
fireEvent.change(screen.getByLabelText(/그룹 이름/), { target: { value: 'New' } })
fireEvent.click(screen.getAllByRole('button', { name: /^색상 / })[1]!)
fireEvent.click(screen.getByRole('button', { name: '추가' }))
vi.spyOn(window, 'confirm').mockReturnValueOnce(true)
Expand Down
Loading
Loading