diff --git a/sam-docs/frontend/v1/00-overview.md b/sam-docs/frontend/v1/00-overview.md
index 1b6c66cf..f6cdc73e 100644
--- a/sam-docs/frontend/v1/00-overview.md
+++ b/sam-docs/frontend/v1/00-overview.md
@@ -1,7 +1,7 @@
# SAM ERP 프론트엔드 개발 가이드
> **대상**: SAM ERP 프론트엔드 신규/기존 개발자
-> **최종 업데이트**: 2026-03-13
+> **최종 업데이트**: 2026-03-20
---
@@ -19,6 +19,12 @@
| [07-hooks.md](./07-hooks.md) | 공통 Hooks |
| [08-utilities.md](./08-utilities.md) | 유틸리티 함수 (포맷터, URL 빌더, 인쇄 등) |
| [09-coding-conventions.md](./09-coding-conventions.md) | 코딩 컨벤션 및 필수 규칙 |
+| [10-module-separation.md](./10-module-separation.md) | 멀티테넌트 모듈 분리 아키텍처 |
+| [11-page-patterns.md](./11-page-patterns.md) | 페이지 패턴 (리스트, 상세/폼, 검색 모달) |
+| [12-form-validation.md](./12-form-validation.md) | 폼 검증 (Zod 스키마, 트러블슈팅) |
+| [13-mobile-patterns.md](./13-mobile-patterns.md) | 모바일 반응형 패턴 |
+| [14-error-handling.md](./14-error-handling.md) | 에러 핸들링 (ApiError, Next.js 에러 파일) |
+| [15-deployment.md](./15-deployment.md) | 배포 가이드 (Vercel) |
---
diff --git a/sam-docs/frontend/v1/10-module-separation.md b/sam-docs/frontend/v1/10-module-separation.md
new file mode 100644
index 00000000..53ce5c23
--- /dev/null
+++ b/sam-docs/frontend/v1/10-module-separation.md
@@ -0,0 +1,142 @@
+# 10. 모듈 분리 아키텍처
+
+> 대상: 프론트엔드 개발자
+> 최종 업데이트: 2026-03-20
+
+---
+
+## 목차
+
+| 번호 | 항목 |
+|------|------|
+| 10.1 | [개요](#101-개요) |
+| 10.2 | [모듈 구조](#102-모듈-구조) |
+| 10.3 | [핵심 패턴: moduleAware](#103-핵심-패턴-moduleaware) |
+| 10.4 | [라우트 가드](#104-라우트-가드) |
+| 10.5 | [크로스 모듈 임포트 규칙](#105-크로스-모듈-임포트-규칙) |
+| 10.6 | [적용 영역](#106-적용-영역) |
+
+---
+
+## 10.1 개요
+
+SAM ERP는 멀티테넌트 시스템으로, 테넌트별 업종(`tenant.options.industry`)에 따라 필요한 모듈만 활성화합니다.
+
+**핵심 원칙**: `industry` 미설정 시 모든 모듈 활성화 (기존 동작 100% 유지)
+
+| 테넌트 | industry | 활성 모듈 |
+|--------|----------|----------|
+| 경동 | `manufacturing` | production, quality |
+| 주일 | `construction` | construction |
+| 미설정 | - | 전체 (기존 호환) |
+
+---
+
+## 10.2 모듈 구조
+
+### 파일 구조
+
+```
+src/modules/
+ types.ts # ModuleId, TenantIndustry, MODULE_REGISTRY
+ tenant-config.ts # INDUSTRY_MODULE_MAP (업종별 활성 모듈 매핑)
+ index.ts # getModuleForRoute, getEnabledDashboardSections
+
+src/hooks/useModules.ts # React Hook
+```
+
+### ModuleId 목록
+
+| ModuleId | 설명 | 라우트 접두사 |
+|----------|------|-------------|
+| `common` | 공통 ERP | - |
+| `production` | 생산관리 | `/production` |
+| `quality` | 품질관리 | `/quality` |
+| `construction` | 시공관리 | `/business/construction` |
+| `vehicle-management` | 차량관리 | `/vehicle-management` |
+
+### useModules Hook
+
+```typescript
+const { isEnabled, tenantIndustry, enabledModules } = useModules();
+
+// 특정 모듈 활성화 여부 확인
+if (isEnabled('production')) {
+ // 생산관리 관련 UI 표시
+}
+```
+
+---
+
+## 10.3 핵심 패턴: moduleAware
+
+```typescript
+const { isEnabled, tenantIndustry } = useModules();
+const moduleAware = !!tenantIndustry; // industry 미설정 -> false -> 전부 허용
+
+if (!moduleAware) return allData; // 기존과 동일
+return filteredData; // 모듈 기반 필터링
+```
+
+- `moduleAware = false` -> 필터링 없음, 모든 기능 노출
+- `moduleAware = true` -> 테넌트 업종에 맞는 모듈만 노출
+
+---
+
+## 10.4 라우트 가드
+
+### 라우트 기반 가드 (ModuleGuard)
+
+`/production/*`, `/quality/*` 등 전용 라우트는 layout.tsx에서 자동 차단:
+
+```typescript
+// src/app/[locale]/(protected)/production/layout.tsx
+
+ {children}
+
+```
+
+### 명시적 가드
+
+공통 라우트 내 모듈 의존 페이지는 직접 체크:
+
+```typescript
+// /sales/*/production-orders 같은 공통 라우트 내 모듈 의존 페이지
+if (tenantIndustry && !isEnabled('production')) {
+ return
생산관리 모듈이 활성화되어 있지 않습니다.
;
+}
+```
+
+---
+
+## 10.5 크로스 모듈 임포트 규칙
+
+| 방향 | 허용 | 비고 |
+|------|------|------|
+| Common -> Tenant | 직접 import 금지 | `// MODULE_SEPARATION_OK` 주석 + 공유 래퍼만 허용 |
+| Tenant -> Common | 자유 | - |
+| Tenant -> Tenant | 금지 | dynamic import만 허용 |
+
+검증 스크립트: `scripts/verify-module-separation.sh`
+
+### 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
+```
+
+---
+
+## 10.6 적용 영역
+
+| 영역 | 적용 방식 |
+|------|----------|
+| CEO 대시보드 | `moduleAware` 패턴으로 섹션 필터링 |
+| 사이드바 메뉴 | `isEnabled()` 체크로 메뉴 숨김 |
+| 라우트 접근 | ModuleGuard 또는 명시적 가드 |
+| API 호출 | 비활성 모듈 관련 API 호출 차단 |
diff --git a/sam-docs/frontend/v1/11-page-patterns.md b/sam-docs/frontend/v1/11-page-patterns.md
new file mode 100644
index 00000000..859ccdc2
--- /dev/null
+++ b/sam-docs/frontend/v1/11-page-patterns.md
@@ -0,0 +1,202 @@
+# 11. 페이지 패턴 가이드
+
+> 대상: 프론트엔드 개발자
+> 최종 업데이트: 2026-03-20
+
+---
+
+## 목차
+
+| 번호 | 항목 |
+|------|------|
+| 11.1 | [리스트 페이지](#111-리스트-페이지) |
+| 11.2 | [상세/폼 페이지](#112-상세폼-페이지) |
+| 11.3 | [검색 모달](#113-검색-모달) |
+| 11.4 | [공통 기능은 공통 컴포넌트에서](#114-공통-기능은-공통-컴포넌트에서) |
+
+---
+
+## 11.1 리스트 페이지
+
+### UniversalListPage (권장)
+
+대부분의 리스트 페이지는 `UniversalListPage`로 구성합니다.
+
+```typescript
+const config: UniversalListConfig = {
+ // 검색 (클라이언트 사이드 필터링)
+ clientSideFiltering: true,
+ searchFilter: (item, searchValue) => {
+ const q = searchValue.toLowerCase();
+ return item.name.toLowerCase().includes(q) || item.code.toLowerCase().includes(q);
+ },
+ searchPlaceholder: '이름, 코드 검색...',
+
+ // 컬럼 정의
+ columns: [...],
+
+ // 모바일 카드
+ renderMobileCard: (item) => ,
+};
+```
+
+### IntegratedListTemplateV2 필수 적용 항목
+
+IntegratedListTemplateV2 사용 시 다음 10가지를 반드시 확인:
+
+| # | 항목 | 필수 |
+|---|------|------|
+| 1 | `useColumnSettings` + `ColumnSettingsPopover` | O |
+| 2 | `searchValue` + `onSearchChange` (or clientSideFiltering) | O |
+| 3 | 체크박스 `Set` 패턴 | O |
+| 4 | 페이지네이션 | O |
+| 5 | `renderMobileCard` | O |
+| 6 | 테이블 행 클릭 -> 상세 이동 | O |
+| 7 | 헤더 레이아웃 (StatCards, 필터) | O |
+| 8 | `tableHeaderActions` (테이블 내 필터) | 선택 |
+| 9 | `filterConfig` (필터 설정) | 선택 |
+| 10 | 탭 구성 | 선택 |
+
+### 검색 패턴 (필수)
+
+```typescript
+// ✅ 올바른 패턴 -- UniversalListPage + clientSideFiltering
+const config: UniversalListConfig = {
+ clientSideFiltering: true,
+ searchFilter: (item, searchValue) => {
+ const q = searchValue.toLowerCase();
+ return item.name.toLowerCase().includes(q);
+ },
+};
+// 데이터를 한 번 API로 로드 -> 검색은 메모리에서 즉시 필터링 -> 깜빡임 없음
+```
+
+```typescript
+// ❌ 금지 패턴 -- IntegratedListTemplateV2에 onSearchChange 직접 연결
+ { setSearchTerm(q); }}
+/>
+// 키입력마다 state 변경 -> re-render -> 화면 깜빡임, 한글 조합 불가
+```
+
+---
+
+## 11.2 상세/폼 페이지
+
+### 라우팅 규칙
+
+- 별도 `/new` 경로 금지 -> `?mode=new` 쿼리파라미터 사용
+- 별도 `/edit` 경로 금지 -> `?mode=edit` 쿼리파라미터 사용
+
+```typescript
+// ✅ 올바른 패턴
+router.push('/some-page?mode=new');
+router.push('/some-page/123?mode=edit');
+
+// ❌ 금지 패턴
+router.push('/some-page/new');
+router.push('/some-page/123/edit');
+```
+
+### 페이지 구조
+
+```typescript
+export default function SomePage() {
+ const searchParams = useSearchParams();
+ const mode = searchParams.get('mode');
+
+ if (mode === 'new') return ;
+ return ;
+}
+```
+
+### 헤더 표준
+
+| 위치 | 요소 |
+|------|------|
+| 상단 좌측 | 페이지 제목 (``) |
+| 상단 우측 | `<- 목록으로` 링크 |
+
+### 하단 Sticky 액션 바 (필수)
+
+```typescript
+
+
+
+
+
+
+```
+
+| 모드 | 좌측 | 우측 |
+|------|------|------|
+| 등록 (new) | 취소 | 저장 |
+| 상세 (view) | 취소 (목록으로) | 수정 |
+| 수정 (edit) | 취소 | 저장 |
+
+### 폼 레이아웃
+
+- Card 내부에 버튼 넣지 않음 -> sticky 하단 바 사용
+- 아이콘 포함: 취소(`X`), 저장(`Save`), 수정(`Pencil`)
+
+---
+
+## 11.3 검색 모달
+
+### SearchableSelectionModal (필수)
+
+검색+선택 기능이 필요한 모달은 반드시 `SearchableSelectionModal`을 사용합니다.
+
+```typescript
+// ✅ 올바른 패턴
+
+ open={open}
+ onOpenChange={setOpen}
+ title="거래처 선택"
+ fetchData={async () => {
+ const result = await getVendors();
+ return result.success ? result.data : [];
+ }}
+ columns={[
+ { key: 'vendorName', label: '거래처명' },
+ { key: 'businessNumber', label: '사업자번호' },
+ ]}
+ onSelect={(vendor) => {
+ setSelectedVendor(vendor);
+ }}
+ searchFilter={(item, query) =>
+ item.vendorName.includes(query) || item.businessNumber?.includes(query)
+ }
+/>
+```
+
+```typescript
+// ❌ 금지 패턴 -- Dialog + Table 직접 조합
+
+```
+
+---
+
+## 11.4 공통 기능은 공통 컴포넌트에서
+
+리스트 페이지 전체에 적용해야 하는 기능은 개별 페이지 수정 금지. 반드시 공통 레이어에서 처리.
+
+| 기능 | 수정 위치 | 개별 페이지 수정 |
+|------|----------|----------------|
+| 검색 상태 보존 | `UniversalListPage` | 금지 |
+| 검색 X(클리어) 버튼 | `SearchFilter` + `IntegratedListTemplateV2` | 금지 |
+| 검색 디바운스 | `UniversalListPage` 내부 300ms | 금지 |
+| 체크박스 선택 | `IntegratedListTemplateV2` | 금지 |
+| 페이지네이션 | `IntegratedListTemplateV2` | 금지 |
+| 모바일 카드/인피니티 | `IntegratedListTemplateV2` | 금지 |
+| 컬럼 설정 | `useColumnSettings` + `ColumnSettingsPopover` | 금지 |
+
+**원칙**: "26개 페이지에 하나씩 적용" -> 잘못된 접근. "공통 1곳 수정 -> 전체 자동 적용" -> 올바른 접근.
diff --git a/sam-docs/frontend/v1/12-form-validation.md b/sam-docs/frontend/v1/12-form-validation.md
new file mode 100644
index 00000000..f2ec76ee
--- /dev/null
+++ b/sam-docs/frontend/v1/12-form-validation.md
@@ -0,0 +1,153 @@
+# 12. 폼 검증 (Zod)
+
+> 대상: 프론트엔드 개발자
+> 최종 업데이트: 2026-03-20
+
+---
+
+## 목차
+
+| 번호 | 항목 |
+|------|------|
+| 12.1 | [적용 범위](#121-적용-범위) |
+| 12.2 | [기본 패턴](#122-기본-패턴) |
+| 12.3 | [트러블슈팅](#123-트러블슈팅) |
+| 12.4 | [체크리스트](#124-체크리스트) |
+
+---
+
+## 12.1 적용 범위
+
+| 대상 | Zod 적용 |
+|------|---------|
+| 신규 폼 | 필수 |
+| 기존 폼 (정상 작동 중) | 건드리지 않음 |
+| 단순 필드 1-2개 인라인 폼 | 불필요 (오버엔지니어링) |
+| 신규 서버 액션 API 응답 | 선택적 |
+
+---
+
+## 12.2 기본 패턴
+
+### 스키마 정의 + 타입 추출
+
+```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;
+
+// 3. useForm에 zodResolver 연결
+const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: { itemName: '', quantity: 1, status: 'active' },
+});
+```
+
+### 규칙
+
+- **스키마 위치**: 컴포넌트 파일 상단 또는 같은 디렉토리의 `schema.ts`
+- **타입 추출**: `z.infer` 사용, 별도 `interface` 중복 정의 금지
+- **에러 메시지**: 한글로 작성 (사용자에게 직접 표시됨)
+- **`as` 캐스트 지양**: Zod 스키마로 타입이 보장되므로 불필요
+
+---
+
+## 12.3 트러블슈팅
+
+### 문제 1: 에러 메시지가 영어로 나옴
+
+```typescript
+// ❌ 기본 에러 메시지 (영어)
+const schema = z.object({
+ name: z.string().min(1),
+});
+// -> "String must contain at least 1 character(s)"
+
+// ✅ 한글 메시지 명시
+const schema = z.object({
+ name: z.string().min(1, '이름을 입력하세요'),
+});
+```
+
+### 문제 2: 불필요한 필드까지 검증됨
+
+수정 폼에서 등록 전용 필드가 검증되는 경우:
+
+```typescript
+// ✅ .omit()으로 불필요 필드 제외
+const editSchema = createSchema.omit({ password: true });
+```
+
+### 문제 3: 조건부 필수 필드
+
+특정 값 선택 시에만 다른 필드가 필수인 경우:
+
+```typescript
+// ✅ .superRefine() 사용
+const schema = z.object({
+ type: z.enum(['individual', 'company']),
+ companyName: z.string().optional(),
+}).superRefine((data, ctx) => {
+ if (data.type === 'company' && !data.companyName) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: '법인명을 입력하세요',
+ path: ['companyName'],
+ });
+ }
+});
+```
+
+### 문제 4: .omit() 후 refinement 소실
+
+`.omit()`은 `.refine()`, `.superRefine()` 등 체이닝된 검증을 제거합니다.
+
+```typescript
+// ❌ refinement가 사라짐
+const base = z.object({ a: z.string(), b: z.string() })
+ .refine((d) => d.a !== d.b, { message: 'a와 b는 달라야 합니다' });
+const partial = base.omit({ b: true }); // refine 소실!
+
+// ✅ .omit() 후 refinement 재적용
+const partial = base.omit({ b: true });
+const withRefine = partial.refine(...); // 필요 시 다시 추가
+```
+
+### 문제 5: 폼 필드명과 스키마 키 불일치
+
+```typescript
+// ❌ 폼에서는 camelCase, 스키마에서는 snake_case
+const schema = z.object({ item_name: z.string() });
+// useForm에서 register('itemName') -> 검증 안 됨
+
+// ✅ 동일한 키 사용
+const schema = z.object({ itemName: z.string() });
+```
+
+---
+
+## 12.4 체크리스트
+
+신규 폼 작성 시 확인:
+
+| # | 항목 |
+|---|------|
+| 1 | 스키마 정의 완료 (모든 필드 포함) |
+| 2 | `z.infer`로 타입 추출 (별도 interface 없음) |
+| 3 | `zodResolver` 연결 |
+| 4 | 에러 메시지 한글 작성 |
+| 5 | 조건부 필수 -> `.superRefine()` 사용 |
+| 6 | 수정 폼 -> `.omit()` 적용 시 refinement 재확인 |
+| 7 | 스키마 키와 폼 필드명 일치 확인 |
+| 8 | `as` 캐스트 제거 |
diff --git a/sam-docs/frontend/v1/13-mobile-patterns.md b/sam-docs/frontend/v1/13-mobile-patterns.md
new file mode 100644
index 00000000..629195f9
--- /dev/null
+++ b/sam-docs/frontend/v1/13-mobile-patterns.md
@@ -0,0 +1,168 @@
+# 13. 모바일 반응형 패턴
+
+> 대상: 프론트엔드 개발자
+> 최종 업데이트: 2026-03-20
+
+---
+
+## 목차
+
+| 번호 | 항목 |
+|------|------|
+| 13.1 | [대상 디바이스](#131-대상-디바이스) |
+| 13.2 | [그리드 레이아웃](#132-그리드-레이아웃) |
+| 13.3 | [테이블 대응](#133-테이블-대응) |
+| 13.4 | [모달/다이얼로그](#134-모달다이얼로그) |
+| 13.5 | [버튼/액션](#135-버튼액션) |
+| 13.6 | [텍스트 처리](#136-텍스트-처리) |
+| 13.7 | [체크리스트](#137-체크리스트) |
+
+---
+
+## 13.1 대상 디바이스
+
+| 디바이스 | 너비 | Tailwind 접두사 |
+|---------|------|----------------|
+| Galaxy Z Fold 5 (접힌) | 344px | 기본 (접두사 없음) |
+| 일반 모바일 | 375-430px | 기본 |
+| 커스텀 브레이크포인트 | 480px | `xs:` |
+| 태블릿 | 768px | `md:` |
+| 데스크탑 | 1024px+ | `lg:` |
+
+커스텀 브레이크포인트 `xs` (480px)는 `tailwind.config.ts`에 정의:
+
+```typescript
+// tailwind.config.ts
+theme: {
+ screens: {
+ xs: '480px',
+ // sm, md, lg, xl 기본값 유지
+ }
+}
+```
+
+---
+
+## 13.2 그리드 레이아웃
+
+**모바일 퍼스트**: 항상 `grid-cols-1`에서 시작, 위로 확장
+
+```typescript
+// ✅ 올바른 패턴
+
+
+// ❌ 데스크탑 퍼스트
+
+```
+
+### 폼 레이아웃 예시
+
+```typescript
+// 기본 정보 폼 (4컬럼 -> 모바일 1컬럼)
+
+
견적번호
+
접수일
+
수주처
+
현장명
+
+```
+
+---
+
+## 13.3 테이블 대응
+
+### 방법 1: IntegratedListTemplateV2 모바일 카드 (권장)
+
+```typescript
+
(
+
+ )}
+/>
+```
+
+### 방법 2: 가로 스크롤
+
+```typescript
+
+```
+
+### 방법 3: 컬럼 숨김
+
+```typescript
+비고
+{item.memo}
+```
+
+---
+
+## 13.4 모달/다이얼로그
+
+```typescript
+// ✅ 모바일에서 화면 벗어나지 않게
+
+```
+
+모바일에서 모달 내 폼:
+- 필드를 1컬럼으로 쌓기
+- 하단 버튼은 `flex-col` 또는 `flex-wrap`
+
+---
+
+## 13.5 버튼/액션
+
+```typescript
+// ✅ 모바일: 아이콘만, 태블릿+: 아이콘+텍스트
+
+```
+
+버튼 그룹 래핑:
+
+```typescript
+
+
+
+
+```
+
+---
+
+## 13.6 텍스트 처리
+
+| 상황 | 클래스 |
+|------|--------|
+| 한 줄 말줄임 | `truncate` |
+| 여러 줄 말줄임 | `line-clamp-2` |
+| 줄바꿈 방지 (한글) | `break-keep` |
+| 긴 단어 강제 줄바꿈 | `break-all` |
+| 반응형 폰트 | `text-sm md:text-base` |
+
+---
+
+## 13.7 체크리스트
+
+새 페이지/컴포넌트 작성 시:
+
+| # | 항목 |
+|---|------|
+| 1 | 그리드: `grid-cols-1`에서 시작하는가? |
+| 2 | 테이블: 모바일 카드 또는 가로 스크롤 적용했는가? |
+| 3 | 모달: `max-w-[calc(100vw-2rem)]` 적용했는가? |
+| 4 | 버튼: 모바일 아이콘만/데스크탑 아이콘+텍스트 패턴인가? |
+| 5 | 긴 텍스트: `truncate` 또는 `line-clamp` 처리했는가? |
+| 6 | Galaxy Fold (344px) 너비에서 깨지지 않는가? |
+| 7 | 줌 방지: input `font-size` 16px 이상인가? |
diff --git a/sam-docs/frontend/v1/14-error-handling.md b/sam-docs/frontend/v1/14-error-handling.md
new file mode 100644
index 00000000..2a553c3a
--- /dev/null
+++ b/sam-docs/frontend/v1/14-error-handling.md
@@ -0,0 +1,192 @@
+# 14. 에러 핸들링
+
+> 대상: 프론트엔드 개발자
+> 최종 업데이트: 2026-03-20
+
+---
+
+## 목차
+
+| 번호 | 항목 |
+|------|------|
+| 14.1 | [API 에러 핸들러](#141-api-에러-핸들러) |
+| 14.2 | [에러 클래스](#142-에러-클래스) |
+| 14.3 | [컴포넌트에서 에러 처리](#143-컴포넌트에서-에러-처리) |
+| 14.4 | [Next.js 에러 파일](#144-nextjs-에러-파일) |
+| 14.5 | [에러 표시 패턴](#145-에러-표시-패턴) |
+
+---
+
+## 14.1 API 에러 핸들러
+
+`src/lib/api/error-handler.ts`에서 HTTP 상태별 처리:
+
+| 상태코드 | 처리 | 동작 |
+|---------|------|------|
+| 401 | 인증 만료 | `/login`으로 리다이렉트 |
+| 403 | 권한 없음 | ApiError throw |
+| 400 + `duplicate_id` | 품목코드 중복 | DuplicateCodeError throw |
+| 422 | Validation 에러 | 상세 필드 에러 포함 ApiError throw |
+| 기타 | 일반 에러 | ApiError throw |
+
+### 422 Validation 에러 폴백
+
+API 응답 포맷이 2가지 존재하므로 폴백 처리:
+
+```typescript
+// data.errors (Laravel 기본) || data.error.details (커스텀 포맷)
+const validationErrors = data.errors || data.error?.details;
+```
+
+이 폴백은 다음 3곳에 동일하게 적용:
+- `error-handler.ts` (Server Action 경로)
+- `client.ts` (클라이언트 API 경로)
+- `index.ts` (직접 fetch 경로)
+
+---
+
+## 14.2 에러 클래스
+
+### ApiError
+
+```typescript
+class ApiError extends Error {
+ constructor(
+ public status: number,
+ public message: string,
+ public errors?: Record // 필드별 에러 메시지
+ ) {}
+}
+```
+
+### DuplicateCodeError
+
+```typescript
+class DuplicateCodeError extends ApiError {
+ constructor(
+ public message: string,
+ public duplicateId: number,
+ public duplicateCode: string
+ ) {}
+}
+```
+
+### getErrorMessage 유틸리티
+
+```typescript
+import { getErrorMessage } from '@/lib/api/error-handler';
+
+// 에러 객체에서 사용자 친화적 메시지 추출
+const message = getErrorMessage(error);
+// ApiError: "[422] 입력값을 확인해주세요."
+// Error: error.message
+// unknown: "알 수 없는 오류가 발생했습니다"
+```
+
+---
+
+## 14.3 컴포넌트에서 에러 처리
+
+### 기본 패턴 (Server Action 호출)
+
+```typescript
+try {
+ const result = await saveItem(formData);
+ if (!result.success) {
+ toast.error(result.error || '저장 실패');
+ return;
+ }
+ toast.success('저장 완료');
+} catch (error) {
+ if (isNextRedirectError(error)) throw error; // redirect는 재throw
+ toast.error(getErrorMessage(error));
+}
+```
+
+### Validation 에러 상세 표시
+
+```typescript
+if (error instanceof ApiError && error.errors) {
+ const firstKey = Object.keys(error.errors)[0];
+ const firstError = error.errors[firstKey];
+ toast.error(`${error.message}\n${firstKey}: ${firstError[0]}`);
+}
+```
+
+### redirect 에러 구분
+
+Next.js의 `redirect()`는 내부적으로 에러를 throw하므로 catch에서 구분 필요:
+
+```typescript
+import { isNextRedirectError } from '@/lib/utils/redirect-error';
+
+catch (error) {
+ if (isNextRedirectError(error)) throw error; // 반드시 재throw
+ // 실제 에러만 처리
+}
+```
+
+---
+
+## 14.4 Next.js 에러 파일
+
+| 파일 | 용도 | 'use client' | 위치 우선순위 |
+|------|------|-------------|-------------|
+| `error.tsx` | 런타임 에러 경계 | 필수 | 특정 라우트 > 그룹 > locale > root |
+| `not-found.tsx` | 404 페이지 | 불필요 | 특정 라우트 > locale > root |
+| `global-error.tsx` | 루트 레이아웃 에러 | 필수 | root만 |
+| `loading.tsx` | 로딩 상태 (Suspense) | 불필요 | 특정 라우트 > 그룹 |
+
+### error.tsx 필수 구조
+
+```typescript
+'use client';
+
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ return (
+
+
오류가 발생했습니다
+
{error.message}
+
+
+ );
+}
+```
+
+### global-error.tsx 필수 구조
+
+```typescript
+'use client';
+
+// 반드시 , 포함
+export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
+ return (
+
+
+ 오류가 발생했습니다
+
+
+
+ );
+}
+```
+
+---
+
+## 14.5 에러 표시 패턴
+
+| 상황 | 방법 |
+|------|------|
+| API 호출 실패 | `toast.error(getErrorMessage(error))` |
+| 폼 검증 실패 | Zod + react-hook-form 자동 표시 |
+| 필드별 서버 에러 | `error.errors` 파싱 후 toast |
+| 치명적 에러 | `error.tsx` 에러 바운더리 |
+| 네트워크 에러 | toast + 재시도 안내 |
+
+**금지**: `alert()`, `confirm()`, `prompt()` 사용 금지 -> `toast` (sonner) 또는 `Dialog` 사용
diff --git a/sam-docs/frontend/v1/15-deployment.md b/sam-docs/frontend/v1/15-deployment.md
new file mode 100644
index 00000000..2d5ce0c0
--- /dev/null
+++ b/sam-docs/frontend/v1/15-deployment.md
@@ -0,0 +1,172 @@
+# 15. 배포 가이드 (Vercel)
+
+> 대상: 프론트엔드 개발자, DevOps
+> 최종 업데이트: 2026-03-20
+
+---
+
+## 목차
+
+| 번호 | 항목 |
+|------|------|
+| 15.1 | [배포 환경](#151-배포-환경) |
+| 15.2 | [환경 변수](#152-환경-변수) |
+| 15.3 | [빌드 설정](#153-빌드-설정) |
+| 15.4 | [Puppeteer 설정](#154-puppeteer-설정) |
+| 15.5 | [배포 체크리스트](#155-배포-체크리스트) |
+| 15.6 | [비용 관리](#156-비용-관리) |
+
+---
+
+## 15.1 배포 환경
+
+| 환경 | 브랜치 | URL | 용도 |
+|------|--------|-----|------|
+| Development | `develop` | localhost:3000 | 로컬 개발 |
+| Stage | `stage` | stage.xxx.com | QA/테스트 |
+| Production | `main` | app.xxx.com | 실서비스 |
+
+환경 구분은 `NEXT_PUBLIC_APP_ENV` 환경변수로 판별:
+
+```typescript
+const env = process.env.NEXT_PUBLIC_APP_ENV; // 'local' | 'development' | 'staging' | 'production'
+```
+
+---
+
+## 15.2 환경 변수
+
+### 필수 환경 변수
+
+| 변수 | 설명 | 예시 |
+|------|------|------|
+| `NEXT_PUBLIC_API_URL` | 백엔드 API 주소 | `https://api.xxx.com` |
+| `NEXT_PUBLIC_APP_ENV` | 실행 환경 | `production` |
+| `JWT_SECRET` | JWT 토큰 시크릿 | (보안) |
+
+### Vercel 설정 방법
+
+1. Vercel Dashboard -> Settings -> Environment Variables
+2. 환경별(Production/Preview/Development) 분리 설정
+3. 민감 정보(JWT_SECRET 등)는 Encrypted로 저장
+
+---
+
+## 15.3 빌드 설정
+
+### vercel.json
+
+```json
+{
+ "functions": {
+ "app/**/*.ts": {
+ "memory": 1024,
+ "maxDuration": 30
+ }
+ },
+ "regions": ["icn1"]
+}
+```
+
+| 설정 | 값 | 설명 |
+|------|-----|------|
+| memory | 1024MB | PDF 생성 등 무거운 작업 대응 |
+| maxDuration | 30초 | 함수 실행 타임아웃 |
+| regions | icn1 | 서울 리전 (한국 서비스) |
+
+### TypeScript strict mode
+
+Vercel 빌드 시 `tsc --noEmit`이 실행되므로 타입 에러가 있으면 배포 실패:
+
+```bash
+# 배포 전 로컬에서 확인
+npx tsc --noEmit
+```
+
+---
+
+## 15.4 Puppeteer 설정
+
+PDF 생성(견적서, 거래명세서 등)을 위한 Puppeteer 환경별 분기:
+
+```typescript
+// 로컬: 시스템 Chrome 사용
+// Vercel: puppeteer-core + @sparticuz/chromium (경량)
+
+const browser = await (isVercel
+ ? puppeteerCore.launch({
+ args: chromium.args,
+ executablePath: await chromium.executablePath(),
+ headless: chromium.headless,
+ })
+ : puppeteer.launch({ headless: true })
+);
+```
+
+### 패키지 구성
+
+```json
+{
+ "dependencies": {
+ "puppeteer-core": "^x.x.x",
+ "@sparticuz/chromium": "^x.x.x"
+ },
+ "devDependencies": {
+ "puppeteer": "^x.x.x"
+ }
+}
+```
+
+- `puppeteer`: 로컬 개발용 (devDependencies)
+- `puppeteer-core` + `@sparticuz/chromium`: Vercel 배포용 (dependencies)
+
+---
+
+## 15.5 배포 체크리스트
+
+### 배포 전
+
+| # | 항목 |
+|---|------|
+| 1 | `npx tsc --noEmit` 에러 없음 |
+| 2 | `npm run build` 성공 |
+| 3 | 환경 변수 Vercel에 설정 완료 |
+| 4 | 백엔드 CORS에 Vercel 도메인 추가 |
+| 5 | API URL이 올바른 환경을 가리키는지 확인 |
+
+### 배포 후
+
+| # | 항목 |
+|---|------|
+| 1 | 로그인 정상 동작 (쿠키/토큰) |
+| 2 | API 호출 정상 (CORS 에러 없음) |
+| 3 | PDF 생성 정상 (Puppeteer) |
+| 4 | 모바일 접속 확인 |
+
+### 백엔드 CORS 설정
+
+```php
+// Laravel: config/cors.php
+'allowed_origins' => [
+ 'https://your-app.vercel.app',
+ 'https://your-custom-domain.com',
+],
+```
+
+---
+
+## 15.6 비용 관리
+
+### 예상 비용 (50개 회사 기준)
+
+| 항목 | 월 비용 |
+|------|--------|
+| Serverless Functions | ~$15-20 |
+| Bandwidth | ~$5-8 |
+| 합계 | ~$22-28 |
+
+### 비용 절감 전략
+
+- **Edge Middleware**: 인증 체크 등 가벼운 로직을 Edge에서 처리 (Functions 비용 95% 절감 가능)
+- **정적 페이지 최적화**: ISR(Incremental Static Regeneration) 적용 가능한 페이지 식별
+- **이미지 최적화**: `next/image` 활용으로 Bandwidth 절감