---
globs: src/components/**/*.tsx
---

# 컴포넌트 코딩 규칙

## 폼 컴포넌트 (ui/)
- `forwardRef` 패턴 사용 (Button, Input, Select)
- disabled 스타일: `disabled:opacity-50 disabled:cursor-not-allowed`
- focus 스타일: `focus:outline-none focus:ring-1 focus:ring-primary/50`
- `cn()` 유틸리티로 Tailwind 클래스 병합

## CodeSelect props
```tsx
<CodeSelect
  codeId="CODE_GROUP_ID"     // 필수: 코드 그룹 ID
  value={value}
  onChange={(e) => setValue(e.target.value)}
  placeholder="선택"          // 기본값
/>
```
- API: `POST /api/common/code-list` (body: `{ codeId }`)
- 응답 필드: `CODE_ID`, `CODE_NAME`

## FileUpload props
```tsx
<FileUpload
  targetObjId="대상ID"       // 필수
  docType="문서타입코드"      // 필수
  docTypeName="문서타입명"    // 필수
  onUploadComplete={() => fetchFiles()}
/>
```
- formData 키: `"file"`, `"targetObjId"`, `"docType"`, `"docTypeName"`

## DataGrid GridColumn 인터페이스
```typescript
{
  title: string;
  field: string;              // RESULTLIST의 대문자 필드명
  width?: number | string;
  hozAlign?: "left" | "center" | "right";
  formatter?: "money" | ((cell: unknown, row: Record<string, unknown>) => string);
  cellClick?: (row: Record<string, unknown>) => void;
  visible?: boolean;
}
```
- formatter `"money"` → `numberWithCommas()` 자동 적용
- 데이터 소스: `data` prop (배열) 또는 `dataUrl` (POST)
- 빈 데이터 메시지: "데이터가 없습니다."

## SearchForm 구조
```tsx
<SearchForm onSearch={fetchData}>
  <SearchField label="검색어">
    <Input name="keyword" />
  </SearchField>
  <SearchField label="분류">
    <CodeSelect codeId="CATEGORY" name="category" />
  </SearchField>
</SearchForm>
```
