2026-03-03 18:30:56 +09:00
|
|
|
# 모달 필수 입력 검증 설계
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
## 1. 목표
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
모든 모달에서 필수 입력값이 빈 상태로 저장 버튼을 클릭하면:
|
|
|
|
|
- 첫 번째 빈 필수 필드로 포커스 이동 + 하이라이트
|
|
|
|
|
- 우측 상단에 토스트 알림 ("○○ 항목을 입력해주세요")
|
|
|
|
|
- 버튼은 항상 활성 상태 (비활성화하지 않음)
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 2. 전체 구조
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
|
|
|
│ DialogContent (모든 모달의 공통 래퍼) │
|
|
|
|
|
│ │
|
|
|
|
|
│ useDialogAutoValidation(contentRef) │
|
|
|
|
|
│ │ │
|
|
|
|
|
│ ├─ 0단계: 모드 확인 │
|
|
|
|
|
│ │ └─ useTabStore.mode === "user" 일 때만 실행 │
|
|
|
|
|
│ │ │
|
|
|
|
|
│ ├─ 1단계: 필수 필드 탐지 │
|
|
|
|
|
│ │ └─ Label 내부 <span> 안에 * 문자 존재 여부 │
|
|
|
|
|
│ │ (라벨 텍스트 직접 매칭 X → span 태그 안의 * 만 감지) │
|
|
|
|
|
│ │ │
|
2026-03-03 18:30:56 +09:00
|
|
|
│ └─ 2단계: 저장 버튼 클릭 인터셉트 │
|
2026-03-03 12:07:12 +09:00
|
|
|
│ │ │
|
2026-03-03 18:30:56 +09:00
|
|
|
│ ├─ 저장/수정/확인 버튼 클릭 감지 │
|
|
|
|
|
│ │ (data-action-type="save"/"submit" │
|
|
|
|
|
│ │ 또는 data-variant="default") │
|
2026-03-03 12:07:12 +09:00
|
|
|
│ │ │
|
2026-03-03 18:30:56 +09:00
|
|
|
│ ├─ 빈 필수 필드 있음: │
|
|
|
|
|
│ │ ├─ 클릭 이벤트 차단 (stopPropagation + preventDefault) │
|
|
|
|
|
│ │ ├─ 첫 번째 빈 필드로 포커스 이동 │
|
|
|
|
|
│ │ ├─ 해당 필드 빨간 테두리 + 하이라이트 애니메이션 │
|
|
|
|
|
│ │ └─ 토스트 알림: "{필드명} 항목을 입력해주세요" │
|
2026-03-03 12:07:12 +09:00
|
|
|
│ │ │
|
2026-03-03 18:30:56 +09:00
|
|
|
│ └─ 모든 필수 필드 입력됨: │
|
|
|
|
|
│ └─ 클릭 이벤트 통과 (정상 저장 진행) │
|
2026-03-03 12:07:12 +09:00
|
|
|
│ │
|
|
|
|
|
│ 제외 조건: │
|
2026-03-03 18:30:56 +09:00
|
|
|
│ └─ 필수 필드가 0개인 모달 → 인터셉트 없음 │
|
2026-03-03 12:07:12 +09:00
|
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 3. 필수 필드 감지: span 기반 * 감지
|
|
|
|
|
|
|
|
|
|
### 원리
|
|
|
|
|
|
|
|
|
|
화면 관리에서 필드를 "필수"로 체크하면 `component.required = true`가 저장된다.
|
|
|
|
|
V2 컴포넌트가 렌더링할 때 `required = true`이면 Label 안에 `<span>*</span>`을 추가한다.
|
|
|
|
|
훅은 이 span 안의 `*`를 감지하여 필수 필드를 식별한다.
|
|
|
|
|
|
|
|
|
|
### 오탐 방지
|
|
|
|
|
|
|
|
|
|
관리자가 라벨 텍스트에 직접 `*`를 입력해도 span 안에 들어가지 않으므로 오탐이 발생하지 않는다.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
required = true → <label>품목코드<span class="text-orange-500">*</span></label>
|
|
|
|
|
→ span 안에 * 있음 → 감지 O
|
|
|
|
|
|
|
|
|
|
required = false → <label>품목코드</label>
|
|
|
|
|
→ span 없음 → 감지 X
|
|
|
|
|
|
|
|
|
|
라벨에 * 직접 입력 → <label>품목코드*</label>
|
|
|
|
|
→ span 없이 텍스트에 * → 감지 X (오탐 방지)
|
|
|
|
|
```
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
### 지원 필드 타입
|
2026-03-03 12:07:12 +09:00
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
| V2 컴포넌트 | 렌더링 요소 | 빈값 판정 |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| V2Input | `<input>`, `<textarea>` | `value.trim() === ""` |
|
|
|
|
|
| V2Select | `<button role="combobox">` | `querySelector("[data-placeholder]")` 존재 |
|
|
|
|
|
| V2Date | `<input>` (날짜/시간) | `value.trim() === ""` |
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
## 4. 저장 버튼 클릭 인터셉트
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
### 원리
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
버튼을 비활성화하지 않고, 클릭 이벤트를 캡처링 단계에서 가로챈다.
|
|
|
|
|
빈 필수 필드가 있으면 이벤트를 차단하고, 없으면 통과시킨다.
|
2026-03-03 12:07:12 +09:00
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
### 인터셉트 대상 버튼
|
2026-03-03 12:07:12 +09:00
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
| 조건 | 예시 |
|
2026-03-03 14:54:41 +09:00
|
|
|
|------|------|
|
2026-03-03 18:30:56 +09:00
|
|
|
| `data-action-type="save"` | ButtonPrimary 저장 버튼 |
|
|
|
|
|
| `data-action-type="submit"` | ButtonPrimary 제출 버튼 |
|
|
|
|
|
| `data-variant="default"` | shadcn Button 기본 (저장/확인/등록) |
|
2026-03-03 14:54:41 +09:00
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
### 인터셉트하지 않는 버튼
|
2026-03-03 14:54:41 +09:00
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
| 조건 | 예시 |
|
2026-03-03 14:54:41 +09:00
|
|
|
|------|------|
|
2026-03-03 18:30:56 +09:00
|
|
|
| `data-variant` = outline/ghost/destructive/secondary | 취소, 닫기, 삭제 |
|
|
|
|
|
| `role` = combobox/tab/switch 등 | 폼 컨트롤 |
|
|
|
|
|
| `data-action-type` != save/submit | 기타 액션 버튼 |
|
|
|
|
|
| `data-dialog-close` | 모달 닫기 X 버튼 |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 5. 시각적 피드백
|
|
|
|
|
|
|
|
|
|
### 포커스 이동
|
|
|
|
|
|
|
|
|
|
첫 번째 빈 필수 필드로 커서를 이동한다.
|
|
|
|
|
- `<input>`, `<textarea>`: `input.focus()`
|
|
|
|
|
- `<button role="combobox">` (V2Select): `button.click()` → 드롭다운 열기
|
|
|
|
|
|
|
|
|
|
### 하이라이트 애니메이션
|
|
|
|
|
|
|
|
|
|
빈 필수 필드에 빨간 테두리 + 흔들림 효과를 준다.
|
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
@keyframes validationShake {
|
|
|
|
|
0%, 100% { transform: translateX(0); }
|
|
|
|
|
20%, 60% { transform: translateX(-4px); }
|
|
|
|
|
40%, 80% { transform: translateX(4px); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-validation-highlight] {
|
|
|
|
|
border-color: hsl(var(--destructive)) !important;
|
|
|
|
|
animation: validationShake 400ms ease-in-out;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
애니메이션 종료 후 `data-validation-highlight` 속성 제거 (일회성).
|
|
|
|
|
|
|
|
|
|
### 토스트 알림
|
|
|
|
|
|
|
|
|
|
우측 상단에 토스트 메시지를 표시한다.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
toast.error(`${fieldLabel} 항목을 입력해주세요`);
|
|
|
|
|
```
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
## 6. 동작 흐름
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
```
|
|
|
|
|
모달 열림
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
DialogContent 마운트
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
useDialogAutoValidation 실행
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
모드 확인 (useTabStore.mode)
|
|
|
|
|
│
|
2026-03-03 18:30:56 +09:00
|
|
|
├─ mode !== "user"? → return
|
2026-03-03 12:07:12 +09:00
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
필수 필드 탐지 (Label 내 span에서 * 감지)
|
|
|
|
|
│
|
2026-03-03 18:30:56 +09:00
|
|
|
├─ 필수 필드 0개? → return
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
클릭 이벤트 리스너 등록 (캡처링 단계)
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
사용자가 저장 버튼 클릭
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
인터셉트 대상 버튼인가?
|
|
|
|
|
│
|
|
|
|
|
├─ 아니오 → 클릭 통과
|
2026-03-03 12:07:12 +09:00
|
|
|
│
|
|
|
|
|
▼
|
2026-03-03 18:30:56 +09:00
|
|
|
빈 필수 필드 검사
|
2026-03-03 12:07:12 +09:00
|
|
|
│
|
2026-03-03 18:30:56 +09:00
|
|
|
├─ 모두 입력됨 → 클릭 통과 (정상 저장)
|
2026-03-03 12:07:12 +09:00
|
|
|
│
|
2026-03-03 18:30:56 +09:00
|
|
|
├─ 빈 필드 있음:
|
|
|
|
|
│ ├─ e.stopPropagation() + e.preventDefault()
|
|
|
|
|
│ ├─ 첫 번째 빈 필드에 포커스 이동
|
|
|
|
|
│ ├─ 해당 필드에 data-validation-highlight 속성 추가
|
|
|
|
|
│ ├─ 애니메이션 종료 후 속성 제거
|
|
|
|
|
│ └─ toast.error("{필드명} 항목을 입력해주세요")
|
2026-03-03 12:07:12 +09:00
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
모달 닫힘
|
|
|
|
|
│
|
|
|
|
|
▼
|
|
|
|
|
클린업
|
|
|
|
|
├─ 이벤트 리스너 제거
|
2026-03-03 18:30:56 +09:00
|
|
|
└─ 하이라이트 속성 제거
|
2026-03-03 12:07:12 +09:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
## 7. 관련 파일
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
| 파일 | 역할 |
|
|
|
|
|
|------|------|
|
2026-03-03 18:30:56 +09:00
|
|
|
| `frontend/lib/hooks/useDialogAutoValidation.ts` | 검증 훅 본체 |
|
|
|
|
|
| `frontend/components/ui/dialog.tsx` | DialogContent에서 훅 호출 |
|
2026-03-03 12:07:12 +09:00
|
|
|
| `frontend/components/ui/button.tsx` | data-variant 속성 노출 |
|
2026-03-03 14:54:41 +09:00
|
|
|
| `frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx` | data-action-type 속성 노출 |
|
2026-03-03 18:30:56 +09:00
|
|
|
| `frontend/app/globals.css` | 하이라이트 애니메이션 |
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
## 8. 적용 범위
|
2026-03-03 12:07:12 +09:00
|
|
|
|
|
|
|
|
### 현재 (1단계): 사용자 모드만
|
|
|
|
|
|
|
|
|
|
| 모달 유형 | 동작 여부 | 이유 |
|
|
|
|
|
|---------------------------------------|:---:|-------------------------------|
|
|
|
|
|
| 사용자 모드 모달 (SaveModal 등) | O | mode === "user" + span * 있음 |
|
|
|
|
|
| 관리자 모드 모달 (CodeFormModal 등) | X | mode !== "user" → return |
|
|
|
|
|
| 확인/삭제 다이얼로그 (필수 필드 없음) | X | 필수 필드 0개 → 자동 제외 |
|
|
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
---
|
2026-03-03 12:07:12 +09:00
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
## 9. 이전 방식과 비교
|
2026-03-03 12:07:12 +09:00
|
|
|
|
2026-03-03 18:30:56 +09:00
|
|
|
| 항목 | 이전 (버튼 비활성화) | 현재 (클릭 인터셉트) |
|
|
|
|
|
|------|---|---|
|
|
|
|
|
| 버튼 상태 | 빈 필드 있으면 비활성화 | 항상 활성 |
|
|
|
|
|
| 피드백 시점 | 모달 열릴 때부터 | 저장 버튼 클릭 시 |
|
|
|
|
|
| 피드백 방식 | 빨간 테두리 + 에러 문구 | 포커스 이동 + 하이라이트 + 토스트 |
|
|
|
|
|
| 복잡도 | 높음 (MutationObserver, 폴링, CSS 지연) | 낮음 (클릭 이벤트 하나) |
|