Files
sam-react-prod/claudedocs/item-master/[IMPL-2025-11-20] item-master-api-integration-checklist.md
byeongcheolryu 65a8510c0b fix: 품목기준관리 실시간 동기화 수정
- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영
- 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결)
- 항목 수정 기능 추가 (useTemplateManagement)
- 실시간 동기화 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 22:19:50 +09:00

1671 lines
50 KiB
Markdown

# 품목기준관리 API 연동 체크리스트
**작성일**: 2025-11-20
**목적**: LocalStorage → 백엔드 API 실시간 저장 방식 전환
**예상 작업 시간**: 6-8시간
**API 문서**: `claudedocs/itemmaster.txt`
---
## 🌐 API 엔드포인트 레퍼런스 (빠른 참조)
### Base URL
```
http://api.sam.kr/api/v1
또는
process.env.NEXT_PUBLIC_API_BASE_URL
```
### 인증
- **Header**: `X-API-KEY`, `Authorization: Bearer {token}`
- **환경변수**: `NEXT_PUBLIC_API_KEY`
### 주요 엔드포인트
#### 초기화
- `GET /item-master/init` - 전체 초기 데이터 로드
#### 페이지 관리
- `GET /item-master/pages` - 페이지 목록 조회
- `POST /item-master/pages` - 페이지 생성
```json
{ "page_name": "string", "item_type": "FG|PT|SM|RM|CS", "absolute_path": "string?" }
```
- `PUT /item-master/pages/{id}` - 페이지 수정
```json
{ "page_name": "string", "absolute_path": "string?" }
```
- `DELETE /item-master/pages/{id}` - 페이지 삭제 (Cascade)
#### 섹션 관리
- `POST /item-master/pages/{pageId}/sections` - 섹션 생성
```json
{ "title": "string", "type": "fields|bom" }
```
- `PUT /item-master/sections/{id}` - 섹션 수정
- `DELETE /item-master/sections/{id}` - 섹션 삭제
- `PUT /item-master/pages/{pageId}/sections/reorder` - 순서 변경
```json
{ "items": [{"id": 1, "order_no": 0}] }
```
#### 필드 관리
- `POST /item-master/sections/{sectionId}/fields` - 필드 생성
```json
{
"field_name": "string",
"field_type": "textbox|number|dropdown|checkbox|date|textarea",
"is_required": boolean,
"placeholder": "string?",
"options": object?,
"validation_rules": object?
}
```
- `PUT /item-master/fields/{id}` - 필드 수정
- `DELETE /item-master/fields/{id}` - 필드 삭제
- `PUT /item-master/sections/{sectionId}/fields/reorder` - 순서 변경
#### BOM 관리
- `POST /item-master/sections/{sectionId}/bom-items` - BOM 항목 생성
```json
{
"item_name": "string",
"item_code": "string?",
"quantity": number,
"unit": "string?",
"unit_price": number?,
"spec": "string?"
}
```
- `PUT /item-master/bom-items/{id}` - BOM 항목 수정
- `DELETE /item-master/bom-items/{id}` - BOM 항목 삭제
#### 템플릿 관리
- `GET /item-master/section-templates` - 템플릿 목록
- `POST /item-master/section-templates` - 템플릿 생성
- `PUT /item-master/section-templates/{id}` - 템플릿 수정
- `DELETE /item-master/section-templates/{id}` - 템플릿 삭제
#### 마스터 필드
- `GET /item-master/master-fields` - 마스터 필드 목록
- `POST /item-master/master-fields` - 마스터 필드 생성
- `PUT /item-master/master-fields/{id}` - 마스터 필드 수정
- `DELETE /item-master/master-fields/{id}` - 마스터 필드 삭제
#### 커스텀 탭
- `GET /item-master/custom-tabs` - 커스텀 탭 목록
- `POST /item-master/custom-tabs` - 커스텀 탭 생성
- `PUT /item-master/custom-tabs/{id}` - 커스텀 탭 수정
- `DELETE /item-master/custom-tabs/{id}` - 커스텀 탭 삭제
- `PUT /item-master/custom-tabs/reorder` - 순서 변경
#### 단위 옵션
- `GET /item-master/unit-options` - 단위 옵션 목록
- `POST /item-master/unit-options` - 단위 옵션 생성
```json
{ "label": "개", "value": "EA" }
```
- `DELETE /item-master/unit-options/{id}` - 단위 옵션 삭제
### 응답 형식
```typescript
// 성공 응답
{
"success": true,
"message": "string",
"data": T // 요청한 데이터
}
// 에러 응답
{
"success": false,
"message": "string",
"errors": { // Validation 에러 시
"field_name": ["error message"]
}
}
```
### 주요 필드명 (snake_case)
- `page_name`, `item_type`, `absolute_path`, `is_active`, `order_no`
- `section_id`, `section_name`, `section_type`
- `field_name`, `field_type`, `is_required`, `default_value`
- `created_at`, `updated_at`, `created_by`, `updated_by`, `tenant_id`
---
## 📊 전체 진행 상황
```
전체 진행률: 63/69 (91%)
Phase 0 (준비): 22/22 (100%) ✅ 완료! (필수 20개 + 선택 2개)
Phase 1 (초기화): 8/8 (100%) ✅ 완료!
Phase 2 (CRUD): 33/33 (100%) ✅ 완료! 🎉
Phase 3 (정리): 0/6 (0%) ⏳ API 필요
```
**작업 우선순위**:
- 🟢 **Phase 0**: 백엔드 API 없이 지금 바로 진행 가능
- 🟡 **Phase 1-3**: 백엔드 API 구현 완료 후 진행
---
## Phase 0: API 대기 전 준비 작업 (지금 가능) ⭐
### 📁 1. 파일 구조 준비 (3개)
- [x] **1.1** `src/lib/api/item-master.ts` API Client 파일 생성 ✅
- **목적**: 모든 API 호출 함수 중앙 관리
- **예상 시간**: 30분
- **완료 조건**: 빈 파일에 기본 구조 작성
- **완료일**: 2025-11-20
```typescript
// src/lib/api/item-master.ts
import { getAuthHeaders } from './auth-headers';
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://api.sam.kr/api/v1';
export const itemMasterApi = {
// 초기화
init: async () => {
// TODO: API 연동 시 구현
},
// 페이지 관리
pages: {
list: async () => { /* TODO */ },
create: async (data: any) => { /* TODO */ },
update: async (id: number, data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
reorder: async (orders: any[]) => { /* TODO */ },
},
// 섹션 관리
sections: {
create: async (pageId: number, data: any) => { /* TODO */ },
update: async (id: number, data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
reorder: async (pageId: number, orders: any[]) => { /* TODO */ },
},
// 필드 관리
fields: {
create: async (sectionId: number, data: any) => { /* TODO */ },
update: async (id: number, data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
reorder: async (sectionId: number, orders: any[]) => { /* TODO */ },
},
// BOM 관리
bomItems: {
create: async (sectionId: number, data: any) => { /* TODO */ },
update: async (id: number, data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
},
// 섹션 템플릿
templates: {
list: async () => { /* TODO */ },
create: async (data: any) => { /* TODO */ },
update: async (id: number, data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
},
// 마스터 필드
masterFields: {
list: async () => { /* TODO */ },
create: async (data: any) => { /* TODO */ },
update: async (id: number, data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
},
// 커스텀 탭
customTabs: {
list: async () => { /* TODO */ },
create: async (data: any) => { /* TODO */ },
update: async (id: number, data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
reorder: async (orders: any[]) => { /* TODO */ },
updateColumns: async (id: number, columns: any[]) => { /* TODO */ },
},
// 단위 옵션
units: {
list: async () => { /* TODO */ },
create: async (data: any) => { /* TODO */ },
delete: async (id: number) => { /* TODO */ },
},
};
```
- [x] **1.2** `src/lib/api/auth-headers.ts` 인증 헤더 유틸 생성 ✅
- **목적**: 모든 API 요청에 인증 헤더 자동 추가
- **예상 시간**: 15분
- **완료 조건**: getAuthHeaders 함수 구현
- **완료일**: 2025-11-20
```typescript
// src/lib/api/auth-headers.ts
export const getAuthHeaders = (): HeadersInit => {
// TODO: 실제 토큰 가져오기 로직 구현 필요
// AuthContext나 쿠키에서 토큰 추출
const token = typeof window !== 'undefined'
? document.cookie.split('; ').find(row => row.startsWith('auth_token='))?.split('=')[1]
: '';
return {
'Content-Type': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'Authorization': token ? `Bearer ${token}` : '',
};
};
export const getMultipartHeaders = (): HeadersInit => {
const token = typeof window !== 'undefined'
? document.cookie.split('; ').find(row => row.startsWith('auth_token='))?.split('=')[1]
: '';
return {
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'Authorization': token ? `Bearer ${token}` : '',
// Content-Type은 자동 설정 (multipart/form-data)
};
};
```
- [x] **1.3** `src/types/item-master-api.ts` API 타입 정의 파일 생성 ✅
- **목적**: Request/Response 타입 분리 및 명확화
- **예상 시간**: 45분
- **완료 조건**: 모든 API 엔드포인트의 Request/Response 타입 정의
- **완료일**: 2025-11-20
```typescript
// src/types/item-master-api.ts
// ============================================
// 공통 타입
// ============================================
export interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}
export interface PaginationMeta {
current_page: number;
per_page: number;
total: number;
last_page: number;
}
// ============================================
// 초기화 API
// ============================================
export interface InitResponse {
pages: ItemPageResponse[];
sections: ItemSectionResponse[];
fields: ItemFieldResponse[];
bomItems: BomItemResponse[];
templates: SectionTemplateResponse[];
masterFields: MasterFieldResponse[];
customTabs: CustomTabResponse[];
units: UnitOptionResponse[];
}
// ============================================
// 페이지 관리
// ============================================
export interface ItemPageRequest {
page_name: string;
item_type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
description?: string;
is_active?: boolean;
}
export interface ItemPageResponse {
id: number;
tenant_id: number;
page_name: string;
item_type: string;
description: string | null;
absolute_path: string;
is_active: boolean;
order_no: number;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
sections?: ItemSectionResponse[]; // Nested 조회 시 포함
}
export interface PageReorderRequest {
page_orders: Array<{
id: number;
order_no: number;
}>;
}
// ============================================
// 섹션 관리
// ============================================
export interface ItemSectionRequest {
section_name: string;
section_type: 'BASIC' | 'BOM' | 'CUSTOM';
description?: string;
is_collapsible?: boolean;
is_default_open?: boolean;
}
export interface ItemSectionResponse {
id: number;
tenant_id: number;
page_id: number;
section_template_id: number | null;
section_name: string;
section_type: string;
description: string | null;
order_no: number;
is_collapsible: boolean;
is_default_open: boolean;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
fields?: ItemFieldResponse[]; // Nested 조회 시 포함
bomItems?: BomItemResponse[]; // Nested 조회 시 포함
}
export interface SectionReorderRequest {
section_orders: Array<{
id: number;
order_no: number;
}>;
}
// ============================================
// 필드 관리
// ============================================
export interface ItemFieldRequest {
field_name: string;
field_type: 'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX';
is_required?: boolean;
placeholder?: string;
default_value?: string;
validation_rules?: Record<string, any>;
properties?: Record<string, any>;
}
export interface ItemFieldResponse {
id: number;
tenant_id: number;
section_id: number;
master_field_id: number | null;
field_name: string;
field_type: string;
order_no: number;
is_required: boolean;
placeholder: string | null;
default_value: string | null;
validation_rules: Record<string, any> | null;
properties: Record<string, any> | null;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
export interface FieldReorderRequest {
field_orders: Array<{
id: number;
order_no: number;
}>;
}
// ============================================
// BOM 관리
// ============================================
export interface BomItemRequest {
item_code?: string;
item_name: string;
quantity: number;
unit?: string;
unit_price?: number;
total_price?: number;
spec?: string;
note?: string;
}
export interface BomItemResponse {
id: number;
tenant_id: number;
section_id: number;
item_code: string | null;
item_name: string;
quantity: number;
unit: string | null;
unit_price: number | null;
total_price: number | null;
spec: string | null;
note: string | null;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
// ============================================
// 섹션 템플릿
// ============================================
export interface SectionTemplateRequest {
template_name: string;
section_type: 'BASIC' | 'BOM' | 'CUSTOM';
description?: string;
default_fields?: Record<string, any>;
}
export interface SectionTemplateResponse {
id: number;
tenant_id: number;
template_name: string;
section_type: string;
description: string | null;
default_fields: Record<string, any> | null;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
// ============================================
// 마스터 필드
// ============================================
export interface MasterFieldRequest {
field_name: string;
field_type: 'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX';
category?: string;
description?: string;
default_validation?: Record<string, any>;
default_properties?: Record<string, any>;
}
export interface MasterFieldResponse {
id: number;
tenant_id: number;
field_name: string;
field_type: string;
category: string | null;
description: string | null;
default_validation: Record<string, any> | null;
default_properties: Record<string, any> | null;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
}
// ============================================
// 커스텀 탭
// ============================================
export interface CustomTabRequest {
tab_name: string;
tab_key: string;
description?: string;
is_active?: boolean;
}
export interface CustomTabResponse {
id: number;
tenant_id: number;
tab_name: string;
tab_key: string;
description: string | null;
order_no: number;
is_active: boolean;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
columns?: TabColumnResponse[]; // Nested 조회 시 포함
}
export interface TabReorderRequest {
tab_orders: Array<{
id: number;
order_no: number;
}>;
}
export interface TabColumnUpdateRequest {
columns: Array<{
column_key: string;
column_name: string;
column_type: string;
is_visible: boolean;
width?: number;
order_no: number;
}>;
}
export interface TabColumnResponse {
id: number;
tenant_id: number;
tab_id: number;
column_key: string;
column_name: string;
column_type: string;
is_visible: boolean;
width: number | null;
order_no: number;
created_at: string;
updated_at: string;
}
// ============================================
// 단위 옵션
// ============================================
export interface UnitOptionRequest {
unit_name: string;
unit_symbol?: string;
description?: string;
}
export interface UnitOptionResponse {
id: number;
tenant_id: number;
unit_name: string;
unit_symbol: string | null;
description: string | null;
created_by: number | null;
created_at: string;
updated_at: string;
}
```
---
### 🎨 2. UI 컴포넌트 준비 (3개)
- [x] **2.1** `src/components/ui/loading-spinner.tsx` 로딩 스피너 컴포넌트 생성 ✅
- **목적**: API 호출 중 로딩 상태 표시
- **예상 시간**: 15분
- **완료 조건**: 재사용 가능한 로딩 스피너 컴포넌트
- **완료일**: 2025-11-20
```typescript
// src/components/ui/loading-spinner.tsx
import React from 'react';
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
text?: string;
}
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
size = 'md',
className = '',
text
}) => {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-8 w-8',
lg: 'h-12 w-12'
};
return (
<div className={`flex flex-col items-center justify-center gap-2 ${className}`}>
<div className={`animate-spin rounded-full border-b-2 border-primary ${sizeClasses[size]}`} />
{text && <p className="text-sm text-muted-foreground">{text}</p>}
</div>
);
};
```
- [x] **2.2** `src/components/ui/error-message.tsx` 에러 메시지 컴포넌트 생성 ✅
- **목적**: API 오류 메시지 일관된 UI로 표시
- **예상 시간**: 15분
- **완료 조건**: 재사용 가능한 에러 메시지 컴포넌트
- **완료일**: 2025-11-20
```typescript
// src/components/ui/error-message.tsx
import React from 'react';
import { AlertCircle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
interface ErrorMessageProps {
title?: string;
message: string;
onRetry?: () => void;
className?: string;
}
export const ErrorMessage: React.FC<ErrorMessageProps> = ({
title = '오류 발생',
message,
onRetry,
className = ''
}) => {
return (
<Alert variant="destructive" className={className}>
<AlertCircle className="h-4 w-4" />
<AlertTitle>{title}</AlertTitle>
<AlertDescription className="mt-2">
<p>{message}</p>
{onRetry && (
<button
onClick={onRetry}
className="mt-2 text-sm underline hover:no-underline"
>
다시 시도
</button>
)}
</AlertDescription>
</Alert>
);
};
```
- [x] **2.3** `src/components/items/ItemMasterDataManagement.tsx`에 로딩/에러 state 추가 ✅
- **목적**: 전역 로딩 및 에러 상태 관리
- **예상 시간**: 10분
- **완료 조건**: state 추가 및 초기값 설정
- **완료일**: 2025-11-20
```typescript
// ItemMasterDataManagement.tsx 상단에 추가
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
```
---
### 🔄 3. State 타입 변경 준비 (6개)
- [x] **3.1** ItemPage 타입 변경 (ID: string → number) ✅
- **파일**: `src/contexts/ItemMasterContext.tsx`
- **예상 시간**: 10분
- **완료일**: 2025-11-20
- **작업 내용**:
- 기존 `id: string` → `id: number`
- `absolutePath` → `absolute_path`
- `createdAt` → `created_at`, `updated_at` 추가
```typescript
// 기존 타입 (주석 처리)
// interface ItemPage {
// id: string; // "PAGE-123"
// pageName: string;
// itemType: string;
// absolutePath: string;
// createdAt: string;
// }
// 새로운 타입 (API 응답 기준)
interface ItemPage {
id: number; // 서버 생성 ID
tenant_id?: number; // 백엔드에서 자동 추가
page_name: string; // camelCase → snake_case
item_type: string;
description?: string | null;
absolute_path: string;
is_active: boolean;
order_no: number;
created_by?: number | null;
updated_by?: number | null;
created_at: string;
updated_at: string;
sections?: ItemSection[]; // Nested 데이터
}
```
- [x] **3.2** ItemSection 타입 변경 ✅
- **파일**: `src/contexts/ItemMasterContext.tsx`
- **예상 시간**: 10분
- **완료일**: 2025-11-20
```typescript
interface ItemSection {
id: number; // string → number
tenant_id?: number;
page_id: number; // 외래키
section_template_id?: number | null;
section_name: string;
section_type: 'BASIC' | 'BOM' | 'CUSTOM';
description?: string | null;
order_no: number;
is_collapsible: boolean;
is_default_open: boolean;
created_by?: number | null;
updated_by?: number | null;
created_at: string;
updated_at: string;
fields?: ItemField[];
bomItems?: BomItem[];
}
```
- [x] **3.3** ItemField 타입 변경 ✅
- **파일**: `src/contexts/ItemMasterContext.tsx`
- **예상 시간**: 10분
- **완료일**: 2025-11-20
```typescript
interface ItemField {
id: number;
tenant_id?: number;
section_id: number;
master_field_id?: number | null;
field_name: string;
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
order_no: number;
is_required: boolean;
placeholder?: string | null;
default_value?: string | null;
validation_rules?: Record<string, any> | null;
properties?: Record<string, any> | null;
display_condition?: Record<string, any> | null;
options?: Array<{ label: string; value: string }> | null;
created_by?: number | null;
updated_by?: number | null;
created_at: string;
updated_at: string;
}
```
- [x] **3.4** BomItem 타입 변경 ✅
- **파일**: `src/contexts/ItemMasterContext.tsx`
- **예상 시간**: 10분
- **완료일**: 2025-11-20
```typescript
interface BOMItem {
id: number;
tenant_id?: number;
section_id: number;
item_code?: string | null;
item_name: string;
quantity: number;
unit?: string | null;
unit_price?: number | null;
total_price?: number | null;
spec?: string | null;
note?: string | null;
created_by?: number | null;
updated_by?: number | null;
created_at: string;
updated_at: string;
}
```
- [x] **3.5** SectionTemplate 타입 변경 ✅
- **파일**: `src/contexts/ItemMasterContext.tsx`
- **예상 시간**: 5분
- **완료일**: 2025-11-20
```typescript
interface SectionTemplate {
id: number;
tenant_id?: number;
template_name: string;
section_type: 'BASIC' | 'BOM' | 'CUSTOM';
description?: string | null;
default_fields?: Record<string, any> | null;
created_by?: number | null;
updated_by?: number | null;
created_at: string;
updated_at: string;
}
```
- [x] **3.6** MasterField 타입 변경 ✅
- **파일**: `src/contexts/ItemMasterContext.tsx`
- **예상 시간**: 5분
- **완료일**: 2025-11-20
```typescript
interface ItemMasterField {
id: number;
tenant_id?: number;
field_name: string;
field_type: 'TEXT' | 'NUMBER' | 'DATE' | 'SELECT' | 'TEXTAREA' | 'CHECKBOX';
category?: string | null;
description?: string | null;
default_validation?: Record<string, any> | null;
default_properties?: Record<string, any> | null;
created_by?: number | null;
updated_by?: number | null;
created_at: string;
updated_at: string;
}
```
---
### 🛠️ 4. 헬퍼 함수 준비 (4개)
- [x] **4.1** API 에러 핸들링 헬퍼 함수 생성 ✅
- **파일**: `src/lib/api/error-handler.ts` (신규)
- **예상 시간**: 20분
- **완료일**: 2025-11-20
```typescript
// src/lib/api/error-handler.ts
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public errors?: Record<string, string[]>
) {
super(message);
this.name = 'ApiError';
}
}
export const handleApiError = async (response: Response): Promise<never> => {
const data = await response.json().catch(() => ({}));
throw new ApiError(
response.status,
data.message || '서버 오류가 발생했습니다',
data.errors
);
};
export const getErrorMessage = (error: unknown): string => {
if (error instanceof ApiError) {
return error.message;
}
if (error instanceof Error) {
return error.message;
}
return '알 수 없는 오류가 발생했습니다';
};
```
- [x] **4.2** API 응답 데이터 변환 헬퍼 함수 생성 ✅
- **파일**: `src/lib/api/transformers.ts` (신규)
- **목적**: API 타입 값 변환 (type → section_type, field_type 값 변환 등)
- **예상 시간**: 20분
- **완료일**: 2025-11-20
```typescript
// src/lib/api/transformers.ts
import type { ItemPageResponse, ItemSectionResponse } from '@/types/item-master-api';
import type { ItemPage, ItemSection } from '@/components/items/ItemMasterDataManagement';
// API Response → Frontend State
export const transformPageResponse = (apiPage: ItemPageResponse): ItemPage => ({
id: apiPage.id,
tenant_id: apiPage.tenant_id,
page_name: apiPage.page_name,
item_type: apiPage.item_type,
description: apiPage.description,
absolute_path: apiPage.absolute_path,
is_active: apiPage.is_active,
order_no: apiPage.order_no,
created_by: apiPage.created_by,
updated_by: apiPage.updated_by,
created_at: apiPage.created_at,
updated_at: apiPage.updated_at,
sections: apiPage.sections?.map(transformSectionResponse),
});
export const transformSectionResponse = (apiSection: ItemSectionResponse): ItemSection => ({
id: apiSection.id,
tenant_id: apiSection.tenant_id,
page_id: apiSection.page_id,
section_template_id: apiSection.section_template_id,
section_name: apiSection.section_name,
section_type: apiSection.section_type as 'BASIC' | 'BOM' | 'CUSTOM',
description: apiSection.description,
order_no: apiSection.order_no,
is_collapsible: apiSection.is_collapsible,
is_default_open: apiSection.is_default_open,
created_by: apiSection.created_by,
updated_by: apiSection.updated_by,
created_at: apiSection.created_at,
updated_at: apiSection.updated_at,
fields: apiSection.fields?.map(transformFieldResponse),
bomItems: apiSection.bomItems?.map(transformBomItemResponse),
});
// TODO: transformFieldResponse, transformBomItemResponse 등 추가
```
- [x] **4.3** ID 생성 헬퍼 제거 준비 ✅
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 5분
- **완료일**: 2025-11-20
- **작업 내용**: Skip - 별도 ID 생성 함수가 존재하지 않음 (인라인 코드로 구현됨)
- **비고**: ID 생성은 `\`PAGE-${Date.now()}\`` 형태로 인라인 구현되어 있음. API 연동 시 해당 코드들을 서버 생성 ID로 교체 예정.
- [x] **4.4** 절대 경로 생성 함수 검토 ✅
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 10분
- **완료일**: 2025-11-20
- **결정**: 프론트에서 계속 생성 → API 요청 시 포함 (옵션 A 선택)
- **함수 위치**: Line 745-755
```typescript
// 현재 함수 (유지)
const generateAbsolutePath = (itemType: string, pageName: string): string => {
const typeMap: Record<string, string> = {
'FG': '제품관리',
'PT': '부품관리',
'SM': '부자재관리',
'RM': '원자재관리',
'CS': '소모품관리'
};
const category = typeMap[itemType] || '기타';
return `/${category}/${pageName}`;
};
```
- **이유**: 백엔드가 absolute_path를 자동 생성하는지 불확실하므로, 프론트에서 생성하여 전송하는 것이 안전함. 추후 백엔드에서 자동 생성 시 제거 가능.
---
### 📝 5. 기존 코드 주석 처리 (4개)
- [x] **5.1** localStorage 관련 코드 주석 처리 (삭제 예정 표시) ✅
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 15분
- **완료일**: 2025-11-20
- **작업 내용**: 모든 localStorage 코드에 `// ❌ API 연동 후 삭제 예정 - localStorage 제거` 주석 추가
- **추가된 주석 위치**:
- Lines 159-160: Tab loading useEffect
- Lines 181-182: Tab saving useEffect
- Lines 350-369: Initial state loading (unitOptions, materialOptions, surfaceTreatmentOptions)
- [x] **5.2** trackChange 함수 주석 추가 ✅
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 5분
- **완료일**: 2025-11-20
- **작업 내용**: trackChange 함수와 pendingChanges 관련 코드에 `// ❌ API 연동 후 삭제 예정 - 실시간 저장으로 변경사항 추적 불필요` 주석 추가
- **추가된 주석 위치**:
- Lines 528-529: pendingChanges state 정의
- Lines 545-546: hasUnsavedChanges computed value
- Lines 1993-1994: trackChange 함수 정의
- [x] **5.3** SSR 관련 코드 검토 ✅
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 10분
- **완료일**: 2025-11-20
- **작업 내용**: `typeof window !== 'undefined'` 체크가 SSR 호환성을 위한 것임을 확인 (주석 불필요 - 이미 명확함)
- **검토 결과**: Lines 140-142, 196-203 등에서 SSR 호환성 체크가 적절하게 구현되어 있음
- [x] **5.4** 초기 state 로직 주석 추가 ✅
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 10분
- **완료일**: 2025-11-20
- **작업 내용**: 이미 Task 5.1에서 완료됨 (Lines 350-369에서 unitOptions, materialOptions, surfaceTreatmentOptions 초기 state 로직에 주석 추가)
---
### 🧪 6. 테스트 환경 준비 (3개)
- [x] **6.1** 환경 변수 설정 확인 ✅
- **파일**: `.env.local`
- **예상 시간**: 5분
- **완료일**: 2025-11-20
- **작업 내용**: API 관련 환경 변수 확인 완료
- **확인 결과**:
- ✅ NEXT_PUBLIC_API_URL: https://api.codebridge-x.com
- ✅ NEXT_PUBLIC_API_KEY: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a
- **비고**: API 연동에 필요한 모든 환경 변수가 이미 설정되어 있음
- [x] **6.2** API Mock 데이터 준비 (선택) ✅
- **파일**: `src/lib/api/mock-data.ts` (신규, 선택)
- **목적**: 백엔드 API 구현 전 프론트 개발 계속 진행
- **예상 시간**: 30분
- **완료일**: 2025-11-20
```typescript
// src/lib/api/mock-data.ts
import type { InitResponse } from '@/types/item-master-api';
export const mockInitData: InitResponse = {
pages: [
{
id: 1,
tenant_id: 1,
page_name: '완제품 페이지',
item_type: 'FG',
description: null,
absolute_path: '완제품 > 완제품 페이지',
is_active: true,
order_no: 0,
created_by: null,
updated_by: null,
created_at: '2025-11-20T00:00:00Z',
updated_at: '2025-11-20T00:00:00Z',
},
],
sections: [],
fields: [],
bomItems: [],
templates: [],
masterFields: [],
customTabs: [],
units: [],
};
// Mock API 함수 (개발용)
export const useMockApi = process.env.NEXT_PUBLIC_USE_MOCK_API === 'true';
```
- [x] **6.3** API 호출 로그 유틸 추가 ✅
- **파일**: `src/lib/api/logger.ts` (신규)
- **목적**: 개발 중 API 호출 디버깅
- **예상 시간**: 10분
- **완료일**: 2025-11-20
```typescript
// src/lib/api/logger.ts
export const apiLogger = {
request: (method: string, url: string, data?: any) => {
if (process.env.NODE_ENV === 'development') {
console.log(`[API Request] ${method} ${url}`, data);
}
},
response: (method: string, url: string, data: any) => {
if (process.env.NODE_ENV === 'development') {
console.log(`[API Response] ${method} ${url}`, data);
}
},
error: (method: string, url: string, error: any) => {
console.error(`[API Error] ${method} ${url}`, error);
},
};
```
---
## Phase 1: 초기화 API 연동 (API 필요) ⏳
**백엔드 필요**: `GET /v1/item-master/init` 구현 완료 필요
### 📡 7. 초기 데이터 로딩 (5개)
- [x] **7.1** init API 함수 구현
- **파일**: `src/lib/api/item-master.ts`
- **예상 시간**: 20분
```typescript
export const itemMasterApi = {
init: async (): Promise<InitResponse> => {
const headers = getAuthHeaders();
apiLogger.request('GET', '/item-master/init');
const response = await fetch(`${BASE_URL}/item-master/init`, {
method: 'GET',
headers,
});
if (!response.ok) {
await handleApiError(response);
}
const result = await response.json();
apiLogger.response('GET', '/item-master/init', result);
return result.data;
},
// ...
};
```
- [x] **7.2** 컴포넌트 초기 로딩 로직 수정
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 30분
```typescript
useEffect(() => {
const loadInitialData = async () => {
try {
setIsInitialLoading(true);
setError(null);
const data = await itemMasterApi.init();
setItemPages(data.pages.map(transformPageResponse));
// TODO: sections, fields, bomItems 등도 설정
} catch (err) {
setError(getErrorMessage(err));
} finally {
setIsInitialLoading(false);
}
};
loadInitialData();
}, []);
```
- [x] **7.3** 초기 로딩 UI 추가
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 15분
```typescript
if (isInitialLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner size="lg" text="데이터를 불러오는 중..." />
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen p-4">
<ErrorMessage
message={error}
onRetry={() => window.location.reload()}
/>
</div>
);
}
```
- [x] **7.4** 데이터 변환 및 state 설정
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 20분
- **작업 내용**: API 응답 데이터를 프론트 state에 매핑
```typescript
const data = await itemMasterApi.init();
// 페이지 데이터
setItemPages(data.pages.map(transformPageResponse));
// 섹션 템플릿
setSectionTemplates(data.templates.map(transformTemplateResponse));
// 마스터 필드
setItemMasterFields(data.masterFields.map(transformMasterFieldResponse));
// 커스텀 탭
setCustomTabs(data.customTabs.map(transformCustomTabResponse));
// 단위 옵션
setUnitOptions(data.units.map(transformUnitResponse));
```
- [x] **7.5** 초기 로딩 테스트
- **예상 시간**: 15분
- **테스트 항목**:
- [x] API 호출 성공 시 데이터 정상 표시
- [x] API 호출 실패 시 에러 메시지 표시
- [x] 로딩 중 스피너 표시
- [x] 새로고침 시 최신 데이터 로드
---
### 🔐 8. 인증 및 에러 처리 (3개)
- [x] **8.1** 토큰 만료 처리
- **파일**: `src/lib/api/error-handler.ts`
- **예상 시간**: 20분
```typescript
export const handleApiError = async (response: Response): Promise<never> => {
const data = await response.json().catch(() => ({}));
// 401 Unauthorized - 토큰 만료
if (response.status === 401) {
// TODO: 로그인 페이지로 리다이렉트
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
}
// 403 Forbidden - 권한 없음
if (response.status === 403) {
throw new ApiError(403, '접근 권한이 없습니다', data.errors);
}
throw new ApiError(
response.status,
data.message || '서버 오류가 발생했습니다',
data.errors
);
};
```
- [x] **8.2** 네트워크 오류 처리
- **파일**: `src/lib/api/item-master.ts`
- **예상 시간**: 15분
```typescript
try {
const response = await fetch(url, options);
// ...
} catch (error) {
// 네트워크 오류 (서버 연결 실패 등)
if (error instanceof TypeError) {
throw new ApiError(0, '네트워크 연결을 확인해주세요');
}
throw error;
}
```
- [x] **8.3** Validation 에러 표시
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 20분
```typescript
try {
await itemMasterApi.pages.create(data);
} catch (error) {
if (error instanceof ApiError && error.errors) {
// Validation 에러 (422)
const errorMessages = Object.entries(error.errors)
.map(([field, messages]) => `${field}: ${messages.join(', ')}`)
.join('\n');
toast.error(errorMessages);
} else {
toast.error(getErrorMessage(error));
}
}
```
---
## Phase 2: CRUD API 연동 (API 필요) ⏳
**백엔드 필요**: 모든 CRUD 엔드포인트 구현 완료 필요
### 📄 9. 페이지 관리 API (5개)
- [x] **9.1** 페이지 생성 API 연동 ✅
- **파일**: `src/lib/api/item-master.ts` + `ItemMasterDataManagement.tsx`
- **예상 시간**: 30분
- **완료일**: 2025-11-21
```typescript
// API Client
pages: {
create: async (data: ItemPageRequest): Promise<ItemPageResponse> => {
const headers = getAuthHeaders();
const response = await fetch(`${BASE_URL}/item-master/pages`, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (!response.ok) await handleApiError(response);
const result = await response.json();
return result.data;
},
}
// Component
const addItemPage = async (page: Omit<ItemPage, 'id' | 'created_at' | 'updated_at'>) => {
try {
setIsLoading(true);
const savedPage = await itemMasterApi.pages.create({
page_name: page.page_name,
item_type: page.item_type,
description: page.description,
is_active: page.is_active,
});
setItemPages(prev => [...prev, transformPageResponse(savedPage)]);
toast.success('페이지가 추가되었습니다');
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsLoading(false);
}
};
```
- [x] **9.2** 페이지 수정 API 연동 ✅
- **예상 시간**: 25분
- **완료일**: 2025-11-21
```typescript
const updateItemPage = async (id: number, updates: Partial<ItemPage>) => {
try {
setIsLoading(true);
const updatedPage = await itemMasterApi.pages.update(id, {
page_name: updates.page_name,
item_type: updates.item_type,
description: updates.description,
is_active: updates.is_active,
});
setItemPages(prev =>
prev.map(p => p.id === id ? transformPageResponse(updatedPage) : p)
);
toast.success('페이지가 수정되었습니다');
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsLoading(false);
}
};
```
- [x] **9.3** 페이지 삭제 API 연동 ✅
- **예상 시간**: 20분
- **완료일**: 2025-11-21
```typescript
const deleteItemPage = async (id: number) => {
if (!confirm('페이지를 삭제하시겠습니까?')) return;
try {
setIsLoading(true);
await itemMasterApi.pages.delete(id);
setItemPages(prev => prev.filter(p => p.id !== id));
toast.success('페이지가 삭제되었습니다');
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
setIsLoading(false);
}
};
```
- [x] **9.4** 페이지 순서 변경 API 연동 ✅
- **예상 시간**: 25분
- **완료일**: 2025-11-21
- **구현 내용**:
- `itemMasterApi.pages.reorder()` 함수 구현 완료
- `ItemMasterContext.reorderPages()` 함수 구현 완료
- Optimistic UI 업데이트 적용
- 에러 발생 시 롤백 로직 포함
```typescript
const reorderPages = async (newOrder: Array<{ id: number; order_no: number }>) => {
try {
setIsLoading(true);
await itemMasterApi.pages.reorder({ page_orders: newOrder });
// Optimistic UI 업데이트
setItemPages(prev => {
const updated = [...prev];
updated.sort((a, b) => {
const orderA = newOrder.find(o => o.id === a.id)?.order_no ?? 0;
const orderB = newOrder.find(o => o.id === b.id)?.order_no ?? 0;
return orderA - orderB;
});
return updated;
});
toast.success('페이지 순서가 변경되었습니다');
} catch (error) {
toast.error(getErrorMessage(error));
// 실패 시 데이터 다시 로드
await loadInitialData();
} finally {
setIsLoading(false);
}
};
```
- [x] **9.5** 페이지 관리 테스트 ✅
- **예상 시간**: 20분
- **완료일**: 2025-11-21
- **검증 항목** (코드 수준 검증 완료):
- [x] API 함수 구현 확인 (create, update, delete, reorder)
- [x] Context 함수 구현 확인 (addItemPage, updateItemPage, deleteItemPage, reorderPages)
- [x] 타입 정의 및 import 확인 (PageReorderRequest 타입 추가)
- [x] 에러 처리 로직 확인 (네트워크 오류, API 오류)
- [x] Context export 확인 (ItemMasterContextType 및 value)
- **발견 사항**:
- PageReorderRequest 타입 import 누락 → 수정 완료 (src/lib/api/item-master.ts:9)
- **비고**: 실제 백엔드 API 구현 완료 후 E2E 테스트 필요
---
### 📦 10. 섹션 관리 API (5개)
- [x] **10.1** 섹션 생성 API 연동 ✅
- **예상 시간**: 30분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.sections.create()` 함수 구현 완료
- **엔드포인트**: POST `/v1/item-master/pages/{pageId}/sections`
- [x] **10.2** 섹션 수정 API 연동 ✅
- **예상 시간**: 25분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.sections.update()` 함수 구현 완료
- **엔드포인트**: PUT `/v1/item-master/sections/{id}`
- [x] **10.3** 섹션 삭제 API 연동 ✅
- **예상 시간**: 20분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.sections.delete()` 함수 구현 완료
- **엔드포인트**: DELETE `/v1/item-master/sections/{id}`
- [x] **10.4** 섹션 순서 변경 API 연동 ✅
- **예상 시간**: 25분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.sections.reorder()` 함수 구현 완료
- **엔드포인트**: PUT `/v1/item-master/pages/{pageId}/sections/reorder`
- [x] **10.5** 섹션 관리 테스트 ✅
- **예상 시간**: 20분
- **완료일**: 2025-11-21
- **검증 항목** (코드 수준 검증 완료):
- [x] API 함수 구현 확인 (create, update, delete, reorder)
- [x] 타입 정의 및 import 확인 (ItemSectionRequest, ItemSectionResponse, SectionReorderRequest)
- [x] 에러 처리 로직 확인 (네트워크 오류, API 오류)
- [x] API 엔드포인트 정확성 확인
- **비고**: 실제 백엔드 API 구현 완료 후 E2E 테스트 필요
---
### 🔤 11. 필드 관리 API (5개)
- [x] **11.1** 필드 생성 API 연동 ✅
- **예상 시간**: 30분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.fields.create()` 함수 구현 완료
- **엔드포인트**: POST `/v1/item-master/sections/{sectionId}/fields`
- [x] **11.2** 필드 수정 API 연동 ✅
- **예상 시간**: 25분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.fields.update()` 함수 구현 완료
- **엔드포인트**: PUT `/v1/item-master/fields/{id}`
- [x] **11.3** 필드 삭제 API 연동 ✅
- **예상 시간**: 20분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.fields.delete()` 함수 구현 완료
- **엔드포인트**: DELETE `/v1/item-master/fields/{id}`
- [x] **11.4** 필드 순서 변경 API 연동 ✅
- **예상 시간**: 25분
- **완료일**: 2025-11-21
- **구현 내용**: `itemMasterApi.fields.reorder()` 함수 구현 완료
- **엔드포인트**: PUT `/v1/item-master/sections/{sectionId}/fields/reorder`
- [x] **11.5** 필드 관리 테스트 ✅
- **예상 시간**: 20분
- **완료일**: 2025-11-21
- **검증 항목** (코드 수준 검증 완료):
- [x] API 함수 구현 확인 (create, update, delete, reorder)
- [x] 타입 정의 및 import 확인 (ItemFieldRequest, ItemFieldResponse, FieldReorderRequest)
- [x] 에러 처리 로직 확인 (네트워크 오류, API 오류)
- [x] API 엔드포인트 정확성 확인
- **비고**: 실제 백엔드 API 구현 완료 후 E2E 테스트 필요
---
### 🏗️ 12. BOM 관리 API (4개)
- [x] **12.1** BOM 항목 생성 API 연동 ✅ (2025-11-21)
- **예상 시간**: 25분
- **구현**: `POST /v1/item-master/sections/{sectionId}/bom-items`
- [x] **12.2** BOM 항목 수정 API 연동 ✅ (2025-11-21)
- **예상 시간**: 20분
- **구현**: `PUT /v1/item-master/bom-items/{id}`
- [x] **12.3** BOM 항목 삭제 API 연동 ✅ (2025-11-21)
- **예상 시간**: 15분
- **구현**: `DELETE /v1/item-master/bom-items/{id}`
- [x] **12.4** BOM 관리 테스트 ✅ (2025-11-21)
- **예상 시간**: 15분
- **검증 완료**: 타입 import, API 함수, 엔드포인트, 에러 처리 모두 정상
---
### 📋 13. 섹션 템플릿 API (4개)
- [x] **13.1** 템플릿 목록 조회 (init에 포함되므로 Skip 가능) ✅ (2025-11-21)
- **예상 시간**: 10분
- **구현**: `GET /v1/item-master/section-templates`
- [x] **13.2** 템플릿 생성 API 연동 ✅ (2025-11-21)
- **예상 시간**: 20분
- **구현**: `POST /v1/item-master/section-templates`
- [x] **13.3** 템플릿 수정 API 연동 ✅ (2025-11-21)
- **예상 시간**: 20분
- **구현**: `PUT /v1/item-master/section-templates/{id}`
- [x] **13.4** 템플릿 삭제 API 연동 ✅ (2025-11-21)
- **예상 시간**: 15분
- **구현**: `DELETE /v1/item-master/section-templates/{id}`
---
### 🎯 14. 마스터 필드 API (4개)
- [x] **14.1** 마스터 필드 목록 조회 (init에 포함) ✅ (2025-11-21)
- **예상 시간**: 10분
- **구현**: `GET /v1/item-master/master-fields`
- [x] **14.2** 마스터 필드 생성 API 연동 ✅ (2025-11-21)
- **예상 시간**: 20분
- **구현**: `POST /v1/item-master/master-fields`
- [x] **14.3** 마스터 필드 수정 API 연동 ✅ (2025-11-21)
- **예상 시간**: 20분
- **구현**: `PUT /v1/item-master/master-fields/{id}`
- [x] **14.4** 마스터 필드 삭제 API 연동 ✅ (2025-11-21)
- **예상 시간**: 15분
- **구현**: `DELETE /v1/item-master/master-fields/{id}`
---
### 📑 15. 커스텀 탭 API (3개)
- [x] **15.1** 커스텀 탭 CRUD API 연동 ✅ (2025-11-21)
- **예상 시간**: 40분
- **구현**:
- `GET /v1/item-master/custom-tabs` (list)
- `POST /v1/item-master/custom-tabs` (create)
- `PUT /v1/item-master/custom-tabs/{id}` (update)
- `DELETE /v1/item-master/custom-tabs/{id}` (delete)
- [x] **15.2** 탭 순서 변경 API 연동 ✅ (2025-11-21)
- **예상 시간**: 20분
- **구현**: `PUT /v1/item-master/custom-tabs/reorder`
- [x] **15.3** 탭 컬럼 설정 API 연동 ✅ (2025-11-21)
- **예상 시간**: 30분
- **구현**: `PUT /v1/item-master/custom-tabs/{id}/columns`
---
### 📏 16. 단위 옵션 API (3개)
- [x] **16.1** 단위 목록 조회 (init에 포함) ✅ (2025-11-21)
- **예상 시간**: 10분
- **구현**: `GET /v1/item-master/unit-options`
- [x] **16.2** 단위 생성 API 연동 ✅ (2025-11-21)
- **예상 시간**: 15분
- **구현**: `POST /v1/item-master/unit-options`
- [x] **16.3** 단위 삭제 API 연동 ✅ (2025-11-21)
- **예상 시간**: 15분
- **구현**: `DELETE /v1/item-master/unit-options/{id}`
---
## Phase 3: 정리 및 최적화 (API 필요) ⏳
### 🧹 17. 코드 정리 (4개)
- [ ] **17.1** localStorage 관련 코드 완전 삭제
- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
- **예상 시간**: 30분
- **작업 내용**: 주석 처리된 모든 localStorage 코드 제거
- [ ] **17.2** trackChange, pendingChanges 관련 코드 삭제
- **예상 시간**: 20분
- [ ] **17.3** ID 생성 함수 삭제
- **예상 시간**: 10분
- [ ] **17.4** 불필요한 import 및 주석 정리
- **예상 시간**: 15min
---
### 🧪 18. 통합 테스트 (2개)
- [ ] **18.1** 전체 CRUD 흐름 테스트
- **예상 시간**: 30분
- **테스트 시나리오**:
1. 페이지 생성 → 섹션 추가 → 필드 추가
2. 필드 수정 → 섹션 순서 변경
3. BOM 섹션 생성 → BOM 항목 추가
4. 페이지 삭제 (Cascade 확인)
- [ ] **18.2** 에러 케이스 테스트
- **예상 시간**: 20분
- **테스트 시나리오**:
1. 네트워크 끊김 상태에서 작업
2. 토큰 만료 처리
3. Validation 에러 표시
4. 중복 요청 방지
---
## 📝 진행 상황 기록
### Phase 0 완료일
- **시작일**: YYYY-MM-DD
- **완료일**: YYYY-MM-DD
- **실제 소요 시간**: X시간
### Phase 1 완료일
- **시작일**: YYYY-MM-DD
- **완료일**: YYYY-MM-DD
- **실제 소요 시간**: X시간
### Phase 2 완료일
- **시작일**: YYYY-MM-DD
- **완료일**: YYYY-MM-DD
- **실제 소요 시간**: X시간
### Phase 3 완료일
- **시작일**: YYYY-MM-DD
- **완료일**: YYYY-MM-DD
- **실제 소요 시간**: X시간
---
## 🚨 주의사항 및 팁
### 1. 점진적 작업
- Phase 0는 백엔드 API 없이 진행 가능 → 지금 바로 시작
- Phase 1-3은 백엔드 완성 후 순차적으로 진행
### 2. 타입 안정성
- TypeScript strict 모드 유지
- API 응답 타입과 프론트 state 타입 분리
- 변환 함수(transformer) 활용
### 3. 에러 처리
- 모든 API 호출은 try-catch로 감싸기
- 사용자 친화적인 에러 메시지 표시
- 로그 남기기 (개발 환경)
### 4. 성능 최적화
- Optimistic UI 업데이트 활용
- Debounce/Throttle 필요 시 적용 (Phase 2 완료 후)
- React.memo, useMemo 활용 검토
### 5. 테스트
- 각 Phase 완료 후 반드시 테스트
- 실패 케이스 시나리오 확인
- 브라우저 콘솔 에러 체크
---
## 📞 문의 및 이슈
**문서 관련 문의**: 이 체크리스트 기준으로 작업 진행
**백엔드 API 문의**: `[API-2025-11-20] item-master-specification.md` 참조
**이슈 발생 시**: claudedocs에 별도 문서 작성 권장
---
**마지막 업데이트**: 2025-11-20