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

8.5 KiB

모달 자동 검증 설계

1. 목표

모든 모달에서 필수 입력값이 있는 경우:

  • 빈 필수 필드 아래에 경고 문구 표시
  • 모든 필수 필드가 입력되기 전까지 저장/등록 버튼 비활성화

2. 전체 구조

┌─────────────────────────────────────────────────────────────────┐
│                DialogContent (모든 모달의 공통 래퍼)               │
│                                                                 │
│   useDialogAutoValidation(contentRef)                           │
│   │                                                             │
│   ├─ 0단계: 모드 확인                                            │
│   │   └─ useTabStore.mode === "user" 일 때만 실행                │
│   │      (관리자 모드에서는 return → 나중에 필요 시 확장)           │
│   │                                                             │
│   ├─ 1단계: 필수 필드 탐지                                       │
│   │   └─ Label 내부 <span> 안에 * 문자 존재 여부                  │
│   │      (라벨 텍스트 직접 매칭 X → span 태그 안의 * 만 감지)      │
│   │                                                             │
│   ├─ 2단계: 시각적 피드백                                        │
│   │   ├─ 빈 필수 필드 → 빨간 테두리 (border-destructive)         │
│   │   └─ 필드 아래 에러 메시지 주입 ("필수 입력 항목입니다")        │
│   │                                                             │
│   └─ 3단계: 버튼 비활성화                                        │
│       │                                                         │
│       ├─ 기본: 모달 내 모든 <button> 비활성화                     │
│       │                                                         │
│       ├─ 제외 (항상 활성): 아래 조건에 해당하는 버튼               │
│       │   ├─ data-variant: outline, ghost, destructive, secondary│
│       │   │   (취소, 닫기, 삭제 등 shadcn Button)                │
│       │   ├─ role: combobox, tab, switch, radio, checkbox        │
│       │   │   (폼 컨트롤, 탭 등 UI 요소)                         │
│       │   ├─ data-slot="select-trigger" (Radix Select 트리거)    │
│       │   ├─ data-dialog-close (모달 닫기 X 버튼)                │
│       │   └─ data-action-type != save/submit                     │
│       │       (ButtonPrimary 비저장 액션)                         │
│       │                                                         │
│       ├─ 빈 필수 필드 있음 → 나머지 버튼 반투명 + 클릭 차단       │
│       └─ 모든 필수 필드 입력됨 → 전체 버튼 활성화                 │
│                                                                 │
│   제외 조건:                                                     │
│   └─ 필수 필드가 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 (오탐 방지)

코드

const hasRequiredMark = Array.from(label.querySelectorAll("span"))
  .some(span => span.textContent?.trim() === "*");
if (!hasRequiredMark) return;

4. 버튼 비활성화: 기본 비활성 + 허용 목록 방식

원리

모달 내 모든 <button> 요소를 기본 비활성화하고, 안전한 버튼(취소, 닫기, 삭제, 폼 컨트롤 등)만 허용 목록으로 제외한다. 저장 버튼의 구현 방식(shadcn Button / 네이티브 button)에 관계없이 동작한다.

허용 목록 (항상 활성 유지)

셀렉터 용도
data-variant="outline" 취소 버튼
data-variant="ghost" 닫기 버튼
data-variant="destructive" 삭제 버튼
data-variant="secondary" 보조 액션
role="combobox" V2Select 드롭다운 트리거
role="tab/switch/radio/checkbox" 폼 컨트롤, 탭 등 UI 요소
data-slot="select-trigger" Radix Select 트리거
data-dialog-close 모달 닫기 X 버튼
data-action-type != save/submit ButtonPrimary 비저장 액션

비활성화 대상 (허용 목록에 해당하지 않는 모든 버튼)

예시 이유
<Button>저장</Button> (data-variant="default") shadcn 기본 버튼
<button data-action-type="save"> ButtonPrimary 저장 액션
<button data-action-type="submit"> ButtonPrimary 제출 액션
기타 variant/role 없는 <button> 알 수 없는 버튼은 안전하게 차단

5. 동작 흐름

모달 열림
    │
    ▼
DialogContent 마운트
    │
    ▼
useDialogAutoValidation 실행
    │
    ▼
모드 확인 (useTabStore.mode)
    │
    ├─ mode !== "user"? → return (관리자 모드에서는 비활성)
    │
    ▼
필수 필드 탐지 (Label 내 span에서 * 감지)
    │
    ├─ 필수 필드 0개? → return (비활성)
    │
    ▼
초기 검증 실행 (50ms 후)
    │
    ├─ 빈 필수 필드 발견
    │   ├─ 해당 input에 data-validation-empty 속성 추가 (CSS 지연 테두리)
    │   ├─ input 아래에 "필수 입력 항목입니다" 에러 메시지 주입
    │   └─ 허용 목록 외 모든 버튼 비활성화
    │
    ├─ 모든 필수 필드 입력됨
    │   ├─ 에러 메시지 제거
    │   ├─ data-validation-empty 속성 제거
    │   └─ 전체 버튼 활성화
    │
    ▼
이벤트 리스너 등록
    ├─ input 이벤트 → 재검증
    ├─ change 이벤트 → 재검증
    ├─ click 캡처링 → 비활성 버튼 클릭 차단
    └─ MutationObserver → DOM 변경 시 재검증

모달 닫힘
    │
    ▼
클린업
    ├─ 이벤트 리스너 제거
    ├─ MutationObserver 해제
    ├─ 폴링 타이머 해제
    ├─ 주입된 에러 메시지 제거
    ├─ 버튼 비활성화 상태 복원
    └─ data-validation-empty 속성 제거

6. 관련 파일

파일 역할
frontend/lib/hooks/useDialogAutoValidation.ts 자동 검증 훅 본체
frontend/components/ui/button.tsx data-variant 속성 노출
frontend/components/ui/dialog.tsx DialogContent에서 훅 호출, data-dialog-close 속성
frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx data-action-type 속성 노출
frontend/app/globals.css CSS 애니메이션 지연 (깜빡임 방지)

7. 적용 범위

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

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

나중에 (2단계): 관리자 모드 확장 시

// 1단계 (현재)
if (mode !== "user") return;

// 2단계 (확장)
if (!["user", "admin"].includes(mode)) return;