# 코딩 컨벤션 및 필수 규칙

---

## Client Component 필수

모든 페이지는 `'use client'` 선언 필수. Server Component 사용 금지.

```tsx
// ✅ 올바른 패턴
'use client';
export default function Page() { ... }

// ❌ 금지
export default async function Page() { ... }
```

**이유**: 폐쇄형 ERP (SEO 불필요), Server Component에서 쿠키 수정(토큰 갱신) 불가

## 데이터 로딩 패턴

```tsx
'use client';
import { useEffect, useState } from 'react';
import { getData } from '@/components/.../actions';

export default function Page() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    getData()
      .then(result => {
        if (result.success) setData(result.data);
      })
      .finally(() => setIsLoading(false));
  }, []);

  if (isLoading) return <div>로딩 중...</div>;
  return <Component data={data} />;
}
```

---

## buildApiUrl 필수 사용

```tsx
// ✅ 필수
import { buildApiUrl } from '@/lib/api/query-params';
const url = buildApiUrl('/api/v1/items', { search, page });

// ❌ 금지
const params = new URLSearchParams();
params.set('search', value);
const url = `${API_URL}/api/v1/items?${params.toString()}`;
```

---

## 컴포넌트 재사용 우선

새 컴포넌트 작성 전 확인 순서:
1. `src/components/organisms/index.ts` export 목록
2. `src/components/molecules/` 내 공통 컴포넌트
3. `src/components/ui/` 내 UI 컴포넌트
4. dev/component-registry 페이지 검색
5. 동일 도메인 기존 컴포넌트

---

## FormField 사용 (신규 폼)

```tsx
// ✅ 신규 폼 - FormField 사용
<FormField label="회사명" value={v} onChange={handleChange} />

// ❌ 신규 폼에서 수동 조합 금지
<div className="space-y-2">
  <Label>회사명</Label>
  <Input value={v} onChange={handleChange} />
</div>
```

**기존 폼**: 건드리지 않음 (정상 작동 중이면 마이그레이션 불필요)

---

## Zod 스키마 검증 (신규 폼)

```tsx
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

// 1. 스키마 정의
const formSchema = z.object({
  itemName: z.string().min(1, '품목명을 입력하세요'),
  quantity: z.number().min(1, '1 이상 입력하세요'),
  status: z.enum(['active', 'inactive']),
  memo: z.string().optional(),
});

// 2. 타입 추출 (별도 interface 정의 불필요)
type FormData = z.infer<typeof formSchema>;

// 3. useForm에 연결
const form = useForm<FormData>({
  resolver: zodResolver(formSchema),
  defaultValues: { itemName: '', quantity: 1, status: 'active' },
});
```

**규칙**:
- 에러 메시지 한글 작성
- 스키마 위치: 컴포넌트 파일 상단 또는 `schema.ts`
- `z.infer` 사용, 별도 `interface` 중복 정의 금지

---

## 팝업 정책

```
❌ 금지: alert(), confirm(), prompt()
✅ 사용: Radix UI Dialog/AlertDialog, toast (sonner)
```

---

## 검색 모달 표준

```
❌ 금지: Dialog + Input + 리스트 직접 조합
✅ 사용: SearchableSelectionModal<T>
```

---

## 리스트 페이지 필수 항목

`IntegratedListTemplateV2` 사용 시:
- [ ] `useColumnSettings` + `ColumnSettingsPopover` 적용
- [ ] `renderMobileCard` (모바일 카드) 구현
- [ ] `selectedItems: Set<string>` (체크박스) 구현
- [ ] `tableHeaderActions` (테이블 내 필터) 필요 시 구현

---

## 테이블 rowSpan/colSpan (문서/보고서)

**반드시 구조 분석 → 코딩 순서**:

1. **플랫 인덱스 맵**: 실제 렌더링 행 수 기준으로 인덱스 산정
2. **병합 범위 표기**: span은 그룹 첫 행에만
3. **Coverage Map 패턴**:

```typescript
function buildCoverageMap(items, spanKey) {
  const map = {};
  const covered = new Set();
  items.forEach((item, idx) => {
    const span = item[spanKey];
    if (span && span > 1) {
      map[idx] = span;
      for (let i = idx + 1; i < idx + span; i++) covered.add(i);
    }
  });
  return { map, covered };
}
// map에 있으면 → <td rowSpan={span}>
// covered에 있으면 → skip (렌더링 안 함)
// 둘 다 아니면 → 일반 <td>
```

---

## Git 규칙

- **develop**: 평소 작업 (자유롭게 커밋)
- **main**: 기능별 squash merge만 (직접 push 금지)
- **커밋 메시지**: `[타입]: 작업내용` (feat, fix, chore, refactor 등)
- **`snapshot.txt`, `.DS_Store`**: 항상 제외

---

## 빌드 정책

- 개발자가 직접 빌드 확인
- TypeScript strict 모드 사용
- ESLint: 빌드 시 무시 (CI에서 별도 처리)

---

## 신규 페이지 생성 체크리스트

- [ ] `'use client'` 선언
- [ ] `?mode=new/edit` 쿼리파라미터 패턴 사용 (`/new`, `/edit` 경로 금지)
- [ ] Server Action에서 `buildApiUrl()` 사용
- [ ] 기존 컴포넌트 재사용 확인 (organisms, molecules 검색)
- [ ] 리스트 페이지: `IntegratedListTemplateV2` 사용 검토
- [ ] 폼 페이지: FormField, Zod 스키마 사용 (신규)
- [ ] 검색 모달: `SearchableSelectionModal` 사용
- [ ] 하단 sticky 액션 바 구현
- [ ] 모바일 반응형 대응
- [ ] 타입 체크 (`npx tsc --noEmit`)
