# SAM ERP 프로젝트 규칙

SAM 프로젝트(Next.js 프론트엔드) 전용 규칙. 범용 규칙은 `~/.claude/RULES.md` 참조.

---

## 프로젝트 개요

```yaml
sam_project:
  frontend: sam_project/sam-next/sma-next-project/sam-react-prod  # Next.js (현재)
  backend_api: sam_project/sam-api/sam-api                         # PHP Laravel
  design: sam_project/sam-design/sam-design                        # React 디자인 시스템
  hotfix: sam_project/sam-hotfix/sam-hotfix                        # E2E 테스트 결과/핫픽스 관리
  특성: 인증 필수 폐쇄형 ERP 시스템 (SEO 불필요)
```

---

## Git Workflow
**Priority**: 🔴

### 브랜치 구조
| 브랜치 | 역할 | 커밋 상태 |
|--------|------|-----------|
| `develop` | 평소 작업 브랜치 (자유롭게) | 지저분해도 OK |
| `stage` | QA/테스트 환경 | 기능별 squash 정리 |
| `main` | 배포용 (기본 브랜치) | 검증된 것만 |
| `feature/*` | 큰 기능/실험적 작업 시 | 선택적 사용 |

### "git 올려줘" 단축 명령어
`git 올려줘` 입력 시 **develop에 push**:
1. `git status` → 2. `git diff --stat` → 3. `git add -A` → 4. `git commit` (자동 메시지) → 5. `git push origin develop`

- `snapshot.txt`, `.DS_Store` 파일은 항상 제외
- develop에서 자유롭게 커밋 (커밋 메시지 정리 불필요)

### main에 올리기 (기능별 squash merge) — 필수 규칙
사용자가 "main에 올려줘" 또는 특정 기능을 main에 올리라고 지시할 때만 실행.
**절대 자동으로 main에 push하지 않음.**

**🔴 반드시 기능별로 나눠서 올릴 것. 통째로 squash 금지.**

```bash
# 실행 순서
git checkout main
git pull origin main

# 1. develop 커밋 이력 분석 → 기능별 그룹 분류
git log --oneline main..develop

# 2. 기능별로 cherry-pick + squash commit (기능 수만큼 반복)
git cherry-pick --no-commit <기능A커밋1> <기능A커밋2> ...
git commit -m "feat: [기능A 설명]"

git cherry-pick --no-commit <기능B커밋1> <기능B커밋2> ...
git commit -m "feat: [기능B 설명]"

# 3. push 후 develop으로 복귀
git push origin main
git checkout develop
```

**기능 분류 기준**:
- 같은 도메인/모듈 수정은 하나로 묶기 (예: CEO 대시보드 관련 커밋들)
- CI/CD, 문서 등 인프라 변경은 별도 커밋 (예: chore: Jenkinsfile 정비)
- 커밋 메시지 타입: feat(기능), fix(버그), refactor(리팩토링), chore(설정/문서)

**핵심: main에는 기능 단위 커밋만 → 문제 시 `git revert`로 해당 기능만 롤백 가능**

### feature 브랜치 사용 기준
| 상황 | 방법 |
|------|------|
| 일반 작업 | develop에서 바로 |
| 1주일+ 걸리는 큰 기능 | feature/* 따서 작업 |
| 실험적 시도 | feature/* 따서 작업 |
| 백엔드와 동시 수정 건 | 각자 feature/* 권장 |

### 금지 사항
- ❌ main에 직접 커밋/push
- ❌ `git push --force` (main/develop)
- ❌ 사용자 지시 없이 main에 merge

---

## Client Component 사용 원칙
**Priority**: 🔴

### 배경
- 폐쇄형 사이트 → SEO 불필요, 오히려 노출되면 안 됨
- Server Component에서는 쿠키 수정(토큰 갱신) 불가

### 규칙
- **Server Component 사용 금지**: `export default async function Page()` 패턴 금지
- **Client Component 사용**: 모든 페이지는 `'use client'` 선언 필수
- **데이터 로딩**: useEffect에서 Server Action 호출

```typescript
// ✅ 올바른 패턴
'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 => setData(result.data))
      .finally(() => setIsLoading(false));
  }, []);

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

```typescript
// ❌ 잘못된 패턴
export default async function Page() {
  const result = await getData();
  return <Component initialData={result.data} />;
}
```

---

## HttpOnly Cookie API Communication
**Priority**: 🔴

- HttpOnly 쿠키는 JavaScript로 읽을 수 없음
- **모든 인증 API 호출은 Next.js API route 프록시 필수**

```typescript
// ✅ Next.js API Proxy
// /src/app/api/proxy/[...path]/route.ts
export async function GET(request: NextRequest, { params }: { params: { path: string[] } }) {
  const token = request.cookies.get('access_token')?.value;
  const response = await fetch(`${BACKEND_URL}/${params.path.join('/')}`, {
    headers: { 'Authorization': `Bearer ${token}` },
  });
  return response;
}

// 프론트엔드에서는 프록시 호출
const response = await fetch('/api/proxy/item-master/init');
```

---

## 기획서/스크린샷 기반 UI 구현 프로세스
**Priority**: 🔴

### 기획서 Description 영역 처리
기획서 스크린샷의 Description 영역(보통 오른쪽 검은 배경)은 **설명용**이며 UI에 구현하지 않음.
빨간 원 번호, 설명 텍스트, 메타 정보 → 절대 UI에 추가 금지.

### 필수 5단계 프로세스

**1단계: Description 정독 및 요소 추출**
- 각 번호(①②③...) 항목별 정확히 파악
- 필터 조건, 테이블 헤더, 버튼/액션, 특수 기능 추출

**2단계: 구성 계획 작성 및 사용자 확인**
🔴 구현 전 반드시 계획 제시 후 사용자 확인 필수. 확인 없이 구현 진행 절대 금지.

```markdown
## [페이지명] 구성 계획
### 필터 조건
| 필터명 | 타입 | 옵션 | 기본값 |
### 테이블 컬럼
| 순서 | 컬럼명 | 설명 |
### 특수 기능
- [기능1]: [설명]
```

**3단계: 기존 패턴 검색**
```
1순위: 동일 기능 컴포넌트 (예: "*Dashboard*.tsx")
2순위: 유사 도메인 컴포넌트
3순위: 공통 UI 컴포넌트 (src/components/ui/)
```

**4단계: 구현** - 기획서 요소만, 임의 추가 절대 금지

**5단계: 검증 체크리스트**
```markdown
| 기획서 요소 | 구현 여부 | 비고 |
```

---

## Component Pattern Reuse
**Priority**: 🔴

- 새 컴포넌트 만들기 전 프로젝트 내 유사 컴포넌트 검색 필수
- 스크린샷만으로 추측 금지, 프로젝트 표준 우선

| 요소 | 확인 사항 |
|------|----------|
| 모달/다이얼로그 | 너비, 배경색, 헤더 구조, 버튼 배치 |
| 문서/프린트 | 용지 스타일, 헤더/푸터, 결재라인 |
| 폼 | 레이아웃, 필드 배치, 버튼 위치 |
| 테이블/리스트 | 컬럼 구조, 체크박스, 페이지네이션 |

### 컴포넌트 레지스트리 활용 (dev/component-registry)
실시간 스캔 기반 컴포넌트 목록 + 관계도 페이지가 존재함. 새로고침 시 최신 상태 반영.

**새 컴포넌트 생성 전 필수 확인**:
1. **목록 뷰**: 동일/유사 컴포넌트가 이미 있는지 검색
2. **관계도 뷰**: 유사 컴포넌트의 구성요소(imports)를 확인하여 동일한 공통 컴포넌트 조합 패턴 따르기

**기존 컴포넌트 수정 시 필수 확인**:
- 관계도의 **사용처(usedBy)** 확인 → 수정 시 영향받는 범위 파악
- usedBy가 많은 공통 컴포넌트일수록 수정 시 주의

---

## Common Table Standards
**Priority**: 🔴

### 필수 컬럼 구조
- **체크박스** → **번호(1부터)** → **데이터 컬럼** → **작업 컬럼**
- 작업 버튼: 체크박스 선택 시만 표시
- 번호: `globalIndex` 사용 또는 `(currentPage - 1) * pageSize + index + 1`

---

## Document Table Merging (rowSpan/colSpan)
**Priority**: 🔴

### 핵심: 구조 분석 → 코딩 (절대 순서 바꾸지 않음)

**1단계: 플랫 인덱스 맵** - 논리적 No가 아닌 실제 렌더링 행 수 기준
```
flatIdx 0: No.1 겉모양
flatIdx 1: No.2 치수-두께  ← No.2 시작 (methodSpan: 3)
flatIdx 2: No.2 치수-너비
flatIdx 3: No.2 치수-길이  ← No.2 끝
```

**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>
```

---

## Page Layout Standards
**Priority**: 🟡

- **AuthenticatedLayout**: `<main>`에 패딩 없음
- **PageLayout**: `p-3 md:p-6` 패딩 담당
- **page.tsx**: 패딩 wrapper 금지 (이중 패딩 방지)

---

## 페이지 모드 라우팅 패턴 (mode=new/edit/view)
**Priority**: 🔴

### 라우팅 규칙
- **별도 `/new` 경로 금지** → `?mode=new` 쿼리파라미터 사용
- **별도 `/edit` 경로 금지** → `?mode=edit` 쿼리파라미터 사용
- 목록과 등록/수정을 **같은 page.tsx에서 분기**

```typescript
// ✅ 올바른 패턴: page.tsx에서 mode 분기
export default function SomePage() {
  const searchParams = useSearchParams();
  const mode = searchParams.get('mode');

  if (mode === 'new') return <SomeForm />;
  return <SomeList />;
}

// ✅ 상세+수정: [id] 경로에서 mode 분기
export default function SomeDetailPage() {
  const params = useParams();
  const searchParams = useSearchParams();
  const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';

  return <SomeDetail id={params.id} mode={mode} />;
}
```

```typescript
// ❌ 금지 패턴
router.push('/some-page/new')     // → router.push('/some-page?mode=new')
router.push('/some-page/123/edit') // → router.push('/some-page/123?mode=edit')
```

### 등록/수정/상세 페이지 헤더
| 위치 | 요소 |
|------|------|
| 상단 좌측 | 페이지 제목 (`<h1>`) |
| 상단 우측 | `← 목록으로` 링크 (`Button variant="link"`) |

```typescript
// ✅ 표준 헤더
<div className="flex items-center justify-between">
  <h1 className="text-xl font-bold">페이지 제목</h1>
  <Button variant="link" className="text-muted-foreground"
    onClick={() => router.push(listPath)}>
    ← 목록으로
  </Button>
</div>
```

### 하단 Sticky 액션 바 (필수)
폼 콘텐츠 아래에 **sticky bottom bar**로 버튼 배치. 취소는 좌측, 주요 액션은 우측.

| 모드 | 좌측 | 우측 |
|------|------|------|
| 등록 (new) | `X 취소` | `💾 저장` |
| 상세 (view) | `X 취소` (목록으로) | `✏️ 수정` |
| 수정 (edit) | `X 취소` | `💾 저장` |

```typescript
// ✅ 표준 하단 Sticky 액션 바
<div className="sticky bottom-0 bg-white border-t shadow-sm">
  <div className="px-3 py-3 md:px-6 md:py-4 flex items-center justify-between">
    <Button variant="outline" onClick={() => router.push(listPath)}>
      <X className="h-4 w-4 mr-1" />
      취소
    </Button>
    <Button onClick={handleSubmit} disabled={isSubmitting}>
      {isSubmitting ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Save className="h-4 w-4 mr-1" />}
      {isNewMode ? '저장' : '저장'}
    </Button>
  </div>
</div>
```

**규칙:**
- Card 내부에 버튼 넣지 않음 → sticky 하단 바 사용
- 아이콘 포함: 취소(`X`), 저장(`Save`), 수정(`Pencil`)
- 상세(view) 모드에서 "취소"는 목록으로 이동, "수정"은 `?mode=edit` 전환

---

## Design Popup Policy
**Priority**: 🟡

- `alert()`, `confirm()`, `prompt()` 사용 금지
- Radix UI Dialog/AlertDialog 또는 `toast from 'sonner'` 사용

---

## Radix UI Select Controlled Mode Bug
**Priority**: 🟡

빈 값('')으로 마운트 후 value 변경이 반영 안 되는 버그:
```tsx
// ✅ key prop으로 강제 리마운트
<Select key={`${fieldKey}-${stringValue}`} value={stringValue} onValueChange={onChange}>
```

---

## Build Policy
**Priority**: 🔴

- Claude가 직접 `npm run build` 실행 금지
- 빌드 필요 시 사용자에게 "빌드 확인해주세요" 요청

---

## React → Next.js Migration Rules
**Priority**: 🔴

### localStorage Access
```typescript
// ✅ Next.js Pattern
const [data, setData] = useState(() => {
  if (typeof window === 'undefined') return defaultValue;
  const saved = localStorage.getItem('key');
  return saved ? JSON.parse(saved) : defaultValue;
});
```

### App Router Rules
- Client Components: 'use client' for interactivity, state, browser APIs
- Dynamic Import: `next/dynamic` with `ssr: false` for client-only components

---

## Large File Migration Workflow
**Priority**: 🟡

**섹션당 6단계**: 구조 파악 → 기능 구현 → 기능 검증 → 스타일 파악 → 스타일 구현 → 스타일 검증

분할 전략: <1000줄 전체 | 1000-3000줄 3-4섹션 | >3000줄 1000줄 단위

---

## Backend API Policy
**Priority**: 🟡

- **신규 API 생성 금지**: 새로운 엔드포인트/컨트롤러 생성은 직접 하지 않음 → 요청 문서로 정리
- **기존 API 수정/추가 가능**: 이미 존재하는 API의 수정, 필드 추가, 로직 변경은 직접 수행 가능
- 백엔드 경로: `sam_project/sam-api/sam-api` (PHP Laravel)
- 수정 시 기존 코드 패턴(Service-First, 기존 응답 구조) 준수
- 신규 API가 필요한 경우 요청 문서로 정리:
```markdown
## 백엔드 API 신규 요청
### 엔드포인트: [HTTP METHOD /api/v1/path]
### 목적: [설명]
### 요청/응답 구조: [내용]
```

---

## Test URL Documentation Rules
**Priority**: 🟡

- 메인 페이지만 등록, 세부 페이지(상세/수정/등록) 제외
- 간결한 목록 유지

---

## Zod 스키마 검증 (신규 코드 적용)
**Priority**: 🟡

### 적용 범위
- **신규 폼**: Zod 스키마 필수 적용
- **기존 폼**: 건드리지 않음 (정상 작동 중이면 마이그레이션 불필요)
- **API 응답**: 신규 서버 액션에서 선택적 적용

### 신규 폼 작성 패턴
```typescript
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에 zodResolver 연결
const form = useForm<FormData>({
  resolver: zodResolver(formSchema),
  defaultValues: { itemName: '', quantity: 1, status: 'active' },
});
```

### 규칙
- **스키마 위치**: 컴포넌트 파일 상단 또는 같은 디렉토리의 `schema.ts`
- **타입 추출**: `z.infer<typeof schema>` 사용, 별도 `interface` 중복 정의 금지
- **에러 메시지**: 한글로 작성 (사용자에게 직접 표시됨)
- **`as` 캐스트 지양**: Zod 스키마로 타입이 보장되므로 `as` 캐스트 불필요

### 사용하지 않는 경우
- 기존 `rules={{ required: true }}` 패턴으로 작동 중인 폼
- 단순 필드 1~2개짜리 인라인 폼 (오버엔지니어링)

---

## Server Action 공통 유틸리티
**Priority**: 🔴

### 규칙:
- `buildApiUrl()` 사용 필수 (직접 `new URLSearchParams` 또는 `${API_URL}` 조립 금지)
- 페이지네이션 조회 → `executePaginatedAction()` 사용
- 단건/목록 조회 → `executeServerAction()` 유지
- `toPaginationMeta()` 직접 사용도 허용
- **`'use server'` 파일에서 타입 re-export 금지** — `export type { X } from '...'` 사용 불가 (Next.js Turbopack 제한: async 함수만 export 허용). 인라인 `export interface` / `export type X = ...`는 허용. 컴포넌트에서 타입이 필요하면 원본 파일에서 직접 import할 것

### 현황:
- **전체 43개 actions.ts 마이그레이션 완료** (2026-02-12)
- `new URLSearchParams` 사용 0건 (actions.ts 기준)
- 모든 URL 빌딩은 `buildApiUrl(path, params)` 사용

```typescript
// ✅ 필수 패턴
import { buildApiUrl } from '@/lib/api/query-params';

// 쿼리 파라미터 있는 경우
url: buildApiUrl('/api/v1/items', {
  search: params.search,
  status: params.status !== 'all' ? params.status : undefined,
  page: params.page,
}),

// 동적 경로 + 파라미터
url: buildApiUrl(`/api/v1/items/${id}`, { with_details: true }),

// 파라미터 없는 단순 경로
url: buildApiUrl('/api/v1/items'),
```

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

---

## Common Component Usage Rules
**Priority**: 🔴

신규 페이지/모달 작업 시 **반드시** 공통 패턴 가이드를 먼저 읽고 기존 구조를 따를 것.

**트리거 → 가이드 읽기:**
| 작업 유형 | 읽을 파일 |
|-----------|----------|
| 검색 모달/선택 팝업 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "검색 모달" 섹션 |
| 리스트/목록 페이지 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "리스트 페이지" 섹션 |
| **IntegratedListTemplateV2 적용/리팩토링** | `claudedocs/guides/[GUIDE] common-page-patterns.md` → **"IntegratedListTemplateV2 표준 적용"** 섹션 |
| 상세/수정/등록 페이지 | `claudedocs/guides/[GUIDE] common-page-patterns.md` → "상세/폼 페이지" 섹션 |
| 새 organisms 필요 | `src/components/organisms/index.ts` 먼저 확인 → 없으면 생성 |

**핵심 원칙:**
- 새 파일 만들기 전 `organisms/`, `molecules/` export 목록 확인
- 검색+선택 모달 → `SearchableSelectionModal<T>` 사용 (직접 Dialog 조합 금지)
- 리스트 페이지 → `UniversalListPage` 또는 organisms 조합
- **IntegratedListTemplateV2 사용 시 → 컬럼 설정(`useColumnSettings` + `ColumnSettingsPopover`), 모바일 카드(`renderMobileCard`), 체크박스(`Set<string>`), 테이블 내 필터(`tableHeaderActions`) 필수 적용**
- 상세/폼 → Card + 기존 패턴 따르기

### 🔴 리스트 페이지 공통 기능은 공통 컴포넌트에서 해결
리스트 페이지 전체에 적용해야 하는 기능은 **개별 페이지 수정 금지**. 반드시 공통 레이어에서 처리.

| 기능 | 수정 위치 | 개별 페이지 수정 |
|------|----------|----------------|
| 검색 상태 보존 | `UniversalListPage` (내장 `useListSearchState`) | ❌ 금지 |
| 검색 X(클리어) 버튼 | `SearchFilter` + `IntegratedListTemplateV2` | ❌ 금지 |
| 검색 디바운스 | `UniversalListPage` 내부 300ms debounce | ❌ 금지 |
| 체크박스 선택 | `IntegratedListTemplateV2` | ❌ 금지 |
| 페이지네이션 | `IntegratedListTemplateV2` | ❌ 금지 |
| 모바일 카드/인피니티 | `IntegratedListTemplateV2` | ❌ 금지 |
| 컬럼 설정 | `useColumnSettings` + `ColumnSettingsPopover` | ❌ 금지 |

**원칙: "26개 페이지에 하나씩 적용" → 잘못된 접근. "공통 1곳 수정 → 전체 자동 적용" → 올바른 접근.**

### 🔴 목록 페이지 검색 — UniversalListPage + clientSideFiltering 필수
목록 페이지의 검색은 **반드시 `UniversalListPage` + `clientSideFiltering: true` + `searchFilter`** 패턴을 사용한다.
`IntegratedListTemplateV2`에 직접 `onSearchChange`를 연결하면 키입력마다 화면이 깜빡이며 한글 조합이 불가능해진다.

```typescript
// ✅ 올바른 패턴 — UniversalListPage + clientSideFiltering (품목관리, 수주관리와 동일)
const config: UniversalListConfig<MyItem> = {
  clientSideFiltering: true,
  searchFilter: (item, searchValue) => {
    const q = searchValue.toLowerCase();
    return item.name.toLowerCase().includes(q) || item.code.toLowerCase().includes(q);
  },
  searchPlaceholder: '이름, 코드 검색...',
};
// 데이터를 한 번 API로 로드 → 검색은 메모리에서 즉시 필터링 → 깜빡임 없음
```

```typescript
// ❌ 금지 패턴 — IntegratedListTemplateV2에 onSearchChange 직접 연결
<IntegratedListTemplateV2
  searchValue={searchTerm}
  onSearchChange={(q) => { setSearchTerm(q); }} // 키입력마다 state 변경 → re-render → 화면 깜빡임
/>
```

### 🔴 금액 입력 컴포넌트 선택 규칙
| 용도 | 컴포넌트 | 특징 |
|------|---------|------|
| **금액 입력** (경조사비, 비용 등) | `CurrencyInput` | 입력 중 실시간 천단위 콤마, ₩ 표시 |
| **수량 입력** (재고, BOM 등) | `NumberInput` | 소수점 허용, 포커스 해제 시 콤마(`useComma`) |
| **테이블 내 금액** (단가 등) | `NumberInput useComma` | 간결한 인라인용 |

`NumberInput useComma`는 포커스 해제 시에만 콤마 표시. 금액 전용 입력에는 반드시 `CurrencyInput` 사용.

### 🔴 날짜 범위 입력 — DateRangeSelector 필수
시작일~종료일 날짜 범위가 필요하면 **반드시 `DateRangeSelector`** 사용. `DatePicker` 2개를 직접 조합하지 않는다.

```typescript
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';

// ✅ 올바른 패턴 — DateRangeSelector 사용
<DateRangeSelector
  startDate={startDate}
  endDate={endDate}
  onStartDateChange={setStartDate}
  onEndDateChange={setEndDate}
  hidePresets           // 프리셋 버튼 불필요 시
/>
```

```typescript
// ❌ 금지 패턴 — DatePicker 2개 직접 조합
<DatePicker value={startDate} onChange={setStartDate} />
<span>~</span>
<DatePicker value={endDate} onChange={setEndDate} />
```

| Props | 설명 |
|-------|------|
| `hidePresets` | 프리셋 버튼(당월, 전월 등) 숨김 |
| `presets` | 표시할 프리셋 선택 (`['thisMonth', 'lastMonth']` 등) |
| `variant` | `'combined'`(기본, 합친 형태) / `'split'`(DatePicker 2개) |
| `presetsPosition` | `'inline'`(기본) / `'below'`(별도 줄) |
| `extraActions` | 우측 추가 버튼 (엑셀, 조회 등) |

---

## FormField 사용 규칙 (신규 폼 필수)
**Priority**: 🟡

### 적용 범위
- **신규 폼**: `Label + Input` 수동 조합 대신 `FormField` molecule 필수 사용
- **기존 폼**: 건드리지 않음 (해당 파일 수정 시에만 선택적 전환)

### 사용 패턴
```typescript
import { FormField } from '@/components/molecules/FormField';

// ✅ 올바른 패턴 - FormField 사용
<FormField
  label="회사명"
  value={formData.companyName}
  onChange={(value) => handleChange('companyName', value)}
  placeholder="회사명"
  disabled={!isEditMode}
/>

// ❌ 잘못된 패턴 - Label + Input 수동 조합
<div className="space-y-2">
  <Label>회사명</Label>
  <Input
    value={formData.companyName}
    onChange={(e) => handleChange('companyName', e.target.value)}
    placeholder="회사명"
    disabled={!isEditMode}
  />
</div>
```

### FormField 지원 타입
| type | 설명 | 대체 컴포넌트 |
|------|------|---------------|
| `text` (기본값) | 일반 텍스트 입력 | Label + Input |
| `number` | 숫자 입력 | Label + Input[type=number] |
| `email` | 이메일 입력 | Label + Input[type=email] |
| `tel` | 전화번호 (자동 포맷) | Label + PhoneInput |
| `businessNumber` | 사업자등록번호 (자동 포맷) | Label + BusinessNumberInput |
| `textarea` | 여러 줄 텍스트 | Label + Textarea |

### FormField로 대체하지 않는 경우
- **특수 컴포넌트 필드**: Select, DatePicker, ImageUpload, FileInput, AccountNumberInput 등
- **복합 레이아웃 필드**: 주소 검색(버튼+입력), 다중 입력 조합
- **커스텀 인터랙션**: 편집/읽기 모드가 다른 컴포넌트(예: 결제일 Select↔Input 전환)

---

## Module Separation Architecture
**Priority**: 🔴

### 개요
멀티테넌트 모듈 분리 아키텍처. 테넌트별로 필요한 모듈만 활성화하여 불필요한 기능 숨김.
`tenant.options.industry` 미설정 시 **모든 모듈 활성화** (기존 동작 100% 유지).

### 핵심 패턴: moduleAware 안전 장치
```typescript
const { isEnabled, tenantIndustry } = useModules();
const moduleAware = !!tenantIndustry; // industry 미설정 → false → 전부 허용

if (!moduleAware) return allData; // 기존과 동일
return filteredData; // 모듈 기반 필터링
```

### 모듈 구조
```
src/modules/
├── types.ts          # ModuleId, TenantIndustry, MODULE_REGISTRY
├── config.ts         # INDUSTRY_MODULES (업종별 활성 모듈 매핑)
├── ModuleGuard.tsx   # 라우트 기반 접근 제어 (layout.tsx에서 사용)
└── ModuleProvider.tsx # React Context (tenant API → enabledModules 계산)

src/hooks/useModules.ts  # { isEnabled, tenantIndustry, enabledModules, ... }
```

### 모듈 ID 목록
| ModuleId | 설명 | 테넌트 |
|----------|------|--------|
| `production` | 생산관리 | 경동 |
| `quality` | 품질관리 | 경동 |
| `construction` | 시공관리 | 주일 |
| `vehicle-management` | 차량관리 | 선택적 |

### 라우트 가드 vs 명시적 가드
- **라우트 가드 (ModuleGuard)**: `/production/*`, `/quality/*` 등 전용 라우트
- **명시적 가드 (useModules)**: 공통 라우트 내 모듈 의존 페이지 (예: `/sales/*/production-orders`)

```typescript
// 공통 라우트 내 모듈 의존 페이지 — 명시적 가드 필수
if (tenantIndustry && !isEnabled('production')) {
  return <div>생산관리 모듈이 활성화되어 있지 않습니다.</div>;
}
```

### 크로스 모듈 임포트 규칙
- **Common → Tenant 직접 import 금지** (검증 스크립트: `scripts/verify-module-separation.sh`)
- **허용 예외**: `// MODULE_SEPARATION_OK` 주석 + `src/lib/api/` 공유 래퍼
- **Tenant → Common import**: 자유
- **Tenant → Tenant import**: 금지 (dynamic import만 허용)

### MODULE.md 경계 마커
각 테넌트 모듈 디렉토리에 `MODULE.md` 파일로 모듈 경계 문서화:
- `src/components/production/MODULE.md`
- `src/components/quality/MODULE.md`
- `src/components/business/construction/MODULE.md`
- `src/components/vehicle-management/MODULE.md`

### Path Aliases
```json
// tsconfig.json
"@modules/*": ["./src/modules/*"]
```

---

## User Environment
**Priority**: 🟢

- 스크린샷: 항상 바탕화면 `/Users/byeongcheolryu/Desktop/`
- 파일명 패턴: `스크린샷 YYYY-MM-DD 오전/오후 HH.MM.SS.png`
