ERP-node/docs/ycshin-node/필수입력항목_자동검증_설계.md

8.5 KiB

모달 필수 입력 검증 설계

1. 목표

모든 모달에서 필수 입력값이 빈 상태로 저장 버튼을 클릭하면:

  • 첫 번째 빈 필수 필드로 포커스 이동 + 하이라이트
  • 우측 상단에 토스트 알림 ("○○ 항목을 입력해주세요")
  • 버튼은 항상 활성 상태 (비활성화하지 않음)

2. 전체 구조

┌─────────────────────────────────────────────────────────────────┐
│                DialogContent (모든 모달의 공통 래퍼)               │
│                                                                 │
│   useDialogAutoValidation(contentRef)                           │
│   │                                                             │
│   ├─ 0단계: 모드 확인                                            │
│   │   └─ useTabStore.mode === "user" 일 때만 실행                │
│   │                                                             │
│   ├─ 1단계: 필수 필드 탐지                                       │
│   │   └─ Label 내부 <span> 안에 * 문자 존재 여부                  │
│   │      (라벨 텍스트 직접 매칭 X → span 태그 안의 * 만 감지)      │
│   │                                                             │
│   └─ 2단계: 저장 버튼 클릭 인터셉트                               │
│       │                                                         │
│       ├─ 저장/수정/확인 버튼 클릭 감지                            │
│       │   (data-action-type="save"/"submit"                     │
│       │    또는 data-variant="default")                          │
│       │                                                         │
│       ├─ 빈 필수 필드 있음:                                      │
│       │   ├─ 클릭 이벤트 차단 (stopPropagation + preventDefault) │
│       │   ├─ 첫 번째 빈 필드로 포커스 이동                        │
│       │   ├─ 해당 필드 빨간 테두리 + 하이라이트 애니메이션          │
│       │   └─ 토스트 알림: "{필드명} 항목을 입력해주세요"           │
│       │                                                         │
│       └─ 모든 필수 필드 입력됨:                                   │
│           └─ 클릭 이벤트 통과 (정상 저장 진행)                    │
│                                                                 │
│   제외 조건:                                                     │
│   └─ 필수 필드가 0개인 모달 → 인터셉트 없음                       │
└─────────────────────────────────────────────────────────────────┘

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 (오탐 방지)

지원 필드 타입

V2 컴포넌트 렌더링 요소 빈값 판정
V2Input <input>, <textarea> value.trim() === ""
V2Select <button role="combobox"> querySelector("[data-placeholder]") 존재
V2Date <input> (날짜/시간) value.trim() === ""

4. 저장 버튼 클릭 인터셉트

원리

버튼을 비활성화하지 않고, 클릭 이벤트를 캡처링 단계에서 가로챈다. 빈 필수 필드가 있으면 이벤트를 차단하고, 없으면 통과시킨다.

인터셉트 대상 버튼

조건 예시
data-action-type="save" ButtonPrimary 저장 버튼
data-action-type="submit" ButtonPrimary 제출 버튼
data-variant="default" shadcn Button 기본 (저장/확인/등록)

인터셉트하지 않는 버튼

조건 예시
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() → 드롭다운 열기

하이라이트 애니메이션

빈 필수 필드에 빨간 테두리 + 흔들림 효과를 준다.

@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 속성 제거 (일회성).

토스트 알림

우측 상단에 토스트 메시지를 표시한다.

toast.error(`${fieldLabel} 항목을 입력해주세요`);

6. 동작 흐름

모달 열림
    │
    ▼
DialogContent 마운트
    │
    ▼
useDialogAutoValidation 실행
    │
    ▼
모드 확인 (useTabStore.mode)
    │
    ├─ mode !== "user"? → return
    │
    ▼
필수 필드 탐지 (Label 내 span에서 * 감지)
    │
    ├─ 필수 필드 0개? → return
    │
    ▼
클릭 이벤트 리스너 등록 (캡처링 단계)
    │
    ▼
사용자가 저장 버튼 클릭
    │
    ▼
인터셉트 대상 버튼인가?
    │
    ├─ 아니오 → 클릭 통과
    │
    ▼
빈 필수 필드 검사
    │
    ├─ 모두 입력됨 → 클릭 통과 (정상 저장)
    │
    ├─ 빈 필드 있음:
    │   ├─ e.stopPropagation() + e.preventDefault()
    │   ├─ 첫 번째 빈 필드에 포커스 이동
    │   ├─ 해당 필드에 data-validation-highlight 속성 추가
    │   ├─ 애니메이션 종료 후 속성 제거
    │   └─ toast.error("{필드명} 항목을 입력해주세요")
    │
    ▼
모달 닫힘
    │
    ▼
클린업
    ├─ 이벤트 리스너 제거
    └─ 하이라이트 속성 제거

7. 관련 파일

파일 역할
frontend/lib/hooks/useDialogAutoValidation.ts 검증 훅 본체
frontend/components/ui/dialog.tsx DialogContent에서 훅 호출
frontend/components/ui/button.tsx data-variant 속성 노출
frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx data-action-type 속성 노출
frontend/app/globals.css 하이라이트 애니메이션

8. 적용 범위

현재 (1단계): 사용자 모드만

모달 유형 동작 여부 이유
사용자 모드 모달 (SaveModal 등) O mode === "user" + span * 있음
관리자 모드 모달 (CodeFormModal 등) X mode !== "user" → return
확인/삭제 다이얼로그 (필수 필드 없음) X 필수 필드 0개 → 자동 제외

9. 이전 방식과 비교

항목 이전 (버튼 비활성화) 현재 (클릭 인터셉트)
버튼 상태 빈 필드 있으면 비활성화 항상 활성
피드백 시점 모달 열릴 때부터 저장 버튼 클릭 시
피드백 방식 빨간 테두리 + 에러 문구 포커스 이동 + 하이라이트 + 토스트
복잡도 높음 (MutationObserver, 폴링, CSS 지연) 낮음 (클릭 이벤트 하나)