# 모달 자동 검증 설계 ## 1. 목표 모든 모달에서 필수 입력값이 있는 경우: - 빈 필수 필드 아래에 경고 문구 표시 - 모든 필수 필드가 입력되기 전까지 저장/등록 버튼 비활성화 --- ## 2. 전체 구조 ``` ┌─────────────────────────────────────────────────────────────────┐ │ DialogContent (모든 모달의 공통 래퍼) │ │ │ │ useDialogAutoValidation(contentRef) │ │ │ │ │ ├─ 0단계: 모드 확인 │ │ │ └─ useTabStore.mode === "user" 일 때만 실행 │ │ │ (관리자 모드에서는 return → 나중에 필요 시 확장) │ │ │ │ │ ├─ 1단계: 필수 필드 탐지 │ │ │ └─ Label 내부 안에 * 문자 존재 여부 │ │ │ (라벨 텍스트 직접 매칭 X → span 태그 안의 * 만 감지) │ │ │ │ │ ├─ 2단계: 시각적 피드백 │ │ │ ├─ 빈 필수 필드 → 빨간 테두리 (border-destructive) │ │ │ └─ 필드 아래 에러 메시지 주입 ("필수 입력 항목입니다") │ │ │ │ │ └─ 3단계: 버튼 비활성화 │ │ │ │ │ ├─ 대상: data-variant="default" 인 버튼 │ │ │ (저장, 등록, 수정, 확인 등 — variant 미지정 = default) │ │ │ │ │ ├─ 제외: outline, ghost, destructive, secondary │ │ │ (취소, 닫기, X, 삭제 등) │ │ │ │ │ ├─ 빈 필수 필드 있음 → 버튼 반투명 + 클릭 차단 │ │ └─ 모든 필수 필드 입력됨 → 정상 활성화 │ │ │ │ 제외 조건: │ │ └─ 필수 필드가 0개인 모달 (자동 비활성) │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 3. 필수 필드 감지: span 기반 * 감지 ### 원리 화면 관리에서 필드를 "필수"로 체크하면 `component.required = true`가 저장된다. V2 컴포넌트가 렌더링할 때 `required = true`이면 Label 안에 `*`을 추가한다. 훅은 이 span 안의 `*`를 감지하여 필수 필드를 식별한다. ### 오탐 방지 관리자가 라벨 텍스트에 직접 `*`를 입력해도 span 안에 들어가지 않으므로 오탐이 발생하지 않는다. ``` required = true → → span 안에 * 있음 → 감지 O required = false → → span 없음 → 감지 X 라벨에 * 직접 입력 → → span 없이 텍스트에 * → 감지 X (오탐 방지) ``` ### 코드 ```typescript const hasRequiredMark = Array.from(label.querySelectorAll("span")) .some(span => span.textContent?.trim() === "*"); if (!hasRequiredMark) return; ``` --- ## 4. 버튼 식별: data-variant 속성 기반 ### 원리 shadcn Button 컴포넌트의 `variant` 값을 `data-variant` 속성으로 DOM에 노출한다. 텍스트 매칭 없이 버튼의 역할을 식별할 수 있다. ### 비활성화 대상 | data-variant | 용도 | 훅 동작 | |:---:|------|:---:| | `default` | 저장, 등록, 수정, 확인 | 비활성화 대상 | ### 제외 (건드리지 않음) | data-variant | 용도 | |:---:|------| | `outline` | 취소 | | `ghost` | 닫기, X 버튼 | | `destructive` | 삭제 | | `secondary` | 보조 액션 | --- ## 5. 동작 흐름 ``` 모달 열림 │ ▼ DialogContent 마운트 │ ▼ useDialogAutoValidation 실행 │ ▼ 모드 확인 (useTabStore.mode) │ ├─ mode !== "user"? → return (관리자 모드에서는 비활성) │ ▼ 필수 필드 탐지 (Label 내 span에서 * 감지) │ ├─ 필수 필드 0개? → return (비활성) │ ▼ 초기 검증 실행 (50ms 후) │ ├─ 빈 필수 필드 발견 │ ├─ 해당 input에 border-destructive 클래스 추가 │ ├─ input 아래에 "필수 입력 항목입니다" 에러 메시지 주입 │ └─ data-variant="default" 버튼 비활성화 │ ├─ 모든 필수 필드 입력됨 │ ├─ 에러 메시지 제거 │ ├─ border-destructive 제거 │ └─ 버튼 활성화 │ ▼ 이벤트 리스너 등록 ├─ input 이벤트 → 재검증 ├─ change 이벤트 → 재검증 ├─ click 캡처링 → 비활성 버튼 클릭 차단 └─ MutationObserver → DOM 변경 시 재검증 모달 닫힘 │ ▼ 클린업 ├─ 이벤트 리스너 제거 ├─ MutationObserver 해제 ├─ 주입된 에러 메시지 제거 ├─ 버튼 비활성화 상태 복원 └─ border-destructive 클래스 제거 ``` --- ## 6. 관련 파일 | 파일 | 역할 | |------|------| | `frontend/lib/hooks/useDialogAutoValidation.ts` | 자동 검증 훅 본체 | | `frontend/components/ui/button.tsx` | data-variant 속성 노출 | | `frontend/components/ui/dialog.tsx` | DialogContent에서 훅 호출 | --- ## 7. 적용 범위 ### 현재 (1단계): 사용자 모드만 | 모달 유형 | 동작 여부 | 이유 | |---------------------------------------|:---:|-------------------------------| | 사용자 모드 모달 (SaveModal 등) | O | mode === "user" + span * 있음 | | 관리자 모드 모달 (CodeFormModal 등) | X | mode !== "user" → return | | 확인/삭제 다이얼로그 (필수 필드 없음) | X | 필수 필드 0개 → 자동 제외 | ### 나중에 (2단계): 관리자 모드 확장 시 ```typescript // 1단계 (현재) if (mode !== "user") return; // 2단계 (확장) if (!["user", "admin"].includes(mode)) return; ```