# 폼 유효성 검사 가이드

전체 시스템에서 필수 필드 유효성 검사 및 에러 표시를 일관되게 적용하기 위한 가이드입니다.

## 📋 목차

1. [개요](#개요)
2. [컴포넌트](#컴포넌트)
3. [유틸리티](#유틸리티)
4. [사용 방법](#사용-방법)
5. [실제 예시](#실제-예시)

---

## 개요

### 주요 기능

- ✅ **필수 필드 표시**: 라벨에 빨간색 별표(*) 자동 표시
- ✅ **실시간 에러 표시**: 필드별 에러 메시지를 빨간색으로 표시
- ✅ **다양한 검증 규칙**: required, min, max, email, phone, pattern 등
- ✅ **시각적 피드백**: 에러가 있는 필드는 빨간 테두리로 강조
- ✅ **섹션별 에러 카운트**: FormSection에 에러 개수 표시

---

## 컴포넌트

### 1. FormField (molecules)

향상된 FormField 컴포넌트는 다양한 입력 타입을 지원하며, 에러 표시 기능이 내장되어 있습니다.

**지원 타입:**
- text, number, email, password, tel, date, datetime-local
- textarea
- select

**Props:**
```typescript
interface FormFieldProps {
  label: string;                    // 필드 라벨
  value: string | number;           // 현재 값
  onChange: (value: string) => void; // 값 변경 핸들러
  type?: string;                     // 입력 타입
  required?: boolean;                // 필수 여부 (빨간 별표 표시)
  error?: string;                    // 에러 메시지
  placeholder?: string;              // 플레이스홀더
  disabled?: boolean;                // 비활성화 여부
  options?: { value: string; label: string }[]; // select 옵션
  min?: number;                      // 최소값
  max?: number;                      // 최대값
  step?: number;                     // 증감 단위
  rows?: number;                     // textarea 행 수
  className?: string;                // 추가 CSS 클래스
  helperText?: string;               // 도움말 텍스트
}
```

### 2. FormSection (organisms)

폼 섹션에 에러 카운트를 표시할 수 있습니다.

**Props:**
```typescript
interface FormSectionProps {
  title: string;           // 섹션 제목
  children: ReactNode;     // 자식 요소
  collapsible?: boolean;   // 접기/펼치기 가능 여부
  defaultOpen?: boolean;   // 기본 열림 상태
  hasError?: boolean;      // 에러 여부 (빨간 테두리)
  errorCount?: number;     // 에러 개수
}
```

---

## 유틸리티

### validation.ts

`/components/utils/validation.ts`에 유효성 검사 유틸리티 함수들이 제공됩니다.

#### ValidationRule 인터페이스

```typescript
interface ValidationRule {
  required?: boolean;         // 필수 입력
  min?: number;              // 최소값
  max?: number;              // 최대값
  minLength?: number;        // 최소 길이
  maxLength?: number;        // 최대 길이
  pattern?: RegExp;          // 정규식 패턴
  email?: boolean;           // 이메일 형식
  phone?: boolean;           // 전화번호 형식
  number?: boolean;          // 숫자 형식
  custom?: (value: any) => boolean; // 커스텀 검증
  message?: string;          // 커스텀 에러 메시지
}
```

#### 주요 함수

**1. validateField** - 단일 필드 검증
```typescript
validateField(value: any, rules: ValidationRule): string | null
```

**2. validateForm** - 전체 폼 검증
```typescript
validateForm(
  data: Record<string, any>, 
  rules: Record<string, ValidationRule>
): Record<string, string>
```

**3. hasErrors** - 에러 존재 여부 확인
```typescript
hasErrors(errors: Record<string, string>): boolean
```

**4. getFieldError** - 특정 필드 에러 가져오기
```typescript
getFieldError(errors: Record<string, string>, field: string): string | undefined
```

---

## 사용 방법

### 기본 사용법

```typescript
import { useState } from "react";
import { FormField } from "./components/molecules/FormField";
import { FormSection } from "./components/organisms/FormSection";
import { validateForm, hasErrors } from "./components/utils/validation";
import { toast } from "sonner@2.0.3";

function MyForm() {
  // 1. 폼 데이터 state
  const [formData, setFormData] = useState({
    customerName: "",
    email: "",
    phone: "",
    quantity: "",
  });

  // 2. 에러 state
  const [errors, setErrors] = useState<Record<string, string>>({});

  // 3. 유효성 검사 규칙 정의
  const validationRules = {
    customerName: {
      required: true,
      minLength: 2,
      message: "고객명은 2자 이상 입력해주세요"
    },
    email: {
      required: true,
      email: true,
    },
    phone: {
      required: true,
      phone: true,
    },
    quantity: {
      required: true,
      number: true,
      min: 1,
    }
  };

  // 4. 필드 변경 핸들러
  const handleFieldChange = (field: string, value: string) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    // 필드 변경 시 해당 필드 에러 제거
    if (errors[field]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  };

  // 5. 저장 핸들러
  const handleSave = () => {
    // 폼 전체 유효성 검사
    const validationErrors = validateForm(formData, validationRules);
    
    if (hasErrors(validationErrors)) {
      setErrors(validationErrors);
      toast.error("입력 내용을 확인해주세요.");
      return;
    }

    // 유효성 검사 통과 시 저장 로직
    console.log("저장:", formData);
    toast.success("저장되었습니다.");
  };

  // 6. 렌더링
  return (
    <FormSection 
      title="기본 정보"
      hasError={hasErrors(errors)}
      errorCount={Object.keys(errors).length}
    >
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <FormField
          label="고객명"
          value={formData.customerName}
          onChange={(value) => handleFieldChange("customerName", value)}
          required
          error={errors.customerName}
          placeholder="고객명을 입력하세요"
        />

        <FormField
          label="이메일"
          type="email"
          value={formData.email}
          onChange={(value) => handleFieldChange("email", value)}
          required
          error={errors.email}
          placeholder="example@company.com"
        />

        <FormField
          label="전화번호"
          type="tel"
          value={formData.phone}
          onChange={(value) => handleFieldChange("phone", value)}
          required
          error={errors.phone}
          placeholder="010-1234-5678"
        />

        <FormField
          label="수량"
          type="number"
          value={formData.quantity}
          onChange={(value) => handleFieldChange("quantity", value)}
          required
          error={errors.quantity}
          min={1}
          placeholder="수량을 입력하세요"
        />
      </div>

      <div className="flex justify-end gap-2 mt-6">
        <Button variant="outline" onClick={() => setFormData({
          customerName: "",
          email: "",
          phone: "",
          quantity: "",
        })}>
          취소
        </Button>
        <Button onClick={handleSave}>저장</Button>
      </div>
    </FormSection>
  );
}
```

---

## 실제 예시

### Select (드롭다운) 사용

```typescript
<FormField
  label="품목 유형"
  type="select"
  value={formData.itemType}
  onChange={(value) => handleFieldChange("itemType", value)}
  required
  error={errors.itemType}
  options={[
    { value: "FG", label: "제품" },
    { value: "RM", label: "원자재" },
    { value: "SM", label: "부자재" },
    { value: "CS", label: "소모품" },
  ]}
/>
```

### Textarea 사용

```typescript
<FormField
  label="비고"
  type="textarea"
  value={formData.note}
  onChange={(value) => handleFieldChange("note", value)}
  error={errors.note}
  placeholder="비고를 입력하세요"
  rows={5}
/>
```

### 커스텀 검증

```typescript
const validationRules = {
  deliveryDate: {
    required: true,
    custom: (value) => {
      const selected = new Date(value);
      const today = new Date();
      return selected >= today;
    },
    message: "납기일은 오늘 이후여야 합니다"
  }
};
```

### 조건부 필수 필드

```typescript
const validationRules = {
  specification: {
    required: formData.itemType !== "FG", // 제품이 아닐 때만 필수
    message: "규격을 입력해주세요"
  }
};
```

---

## 에러 표시 스타일

### 필드 에러 표시
- 필드 라벨: 빨간색으로 변경
- 입력 필드: 빨간색 테두리
- 에러 메시지: 필드 하단에 빨간색 텍스트로 표시
- 아이콘: 작은 빨간 점과 함께 표시

### 섹션 에러 표시
- 카드 테두리: 빨간색
- 섹션 제목: 빨간색
- 에러 카운트: "⚠️ N개 오류" 형태로 표시

---

## 적용 대상 페이지

모든 입력 폼이 있는 페이지에 적용 권장:

### 판매관리
- ✅ 견적작성
- ✅ 수주등록
- ✅ 거래처 등록

### 구매관리
- ✅ 발주 등록
- ✅ 입고 등록
- ✅ 공급업체 등록

### 생산관리
- ✅ 품목 등록
- ✅ 작업지시서 작성
- ✅ BOM 등록

### 품질관리
- ✅ 검사 기준 등록
- ✅ 검사 결과 등록

### 기준정보
- ✅ 고객 등록
- ✅ 제품 등록
- ✅ 자재 등록

### 시스템관리
- ✅ 사용자 등록
- ✅ 부서 등록

---

## 마이그레이션 체크리스트

기존 컴포넌트에 유효성 검사를 적용할 때:

1. [ ] `validation.ts` import 추가
2. [ ] `errors` state 추가
3. [ ] 유효성 검사 규칙 정의
4. [ ] Input을 FormField로 교체 (또는 error prop 추가)
5. [ ] 저장 핸들러에 `validateForm` 호출 추가
6. [ ] FormSection에 에러 카운트 추가
7. [ ] 테스트: 필수 필드 빈 값으로 저장 시도
8. [ ] 테스트: 잘못된 형식 입력 시 에러 확인

---

## 주의사항

1. **기존 toast.error와 병행 사용**: 필드별 에러와 전체 에러 메시지를 함께 제공
2. **에러 제거 타이밍**: 사용자가 필드를 수정하면 즉시 해당 필드 에러 제거
3. **섹션별 에러**: 여러 섹션이 있는 경우 각 섹션의 에러를 별도로 계산
4. **접근성**: required 속성으로 스크린 리더 지원

---

## 문의

유효성 검사 관련 문의사항이나 개선 제안은 개발팀으로 연락 주세요.
