- BOM 항목 추가/수정/삭제 시 섹션탭 즉시 반영 - 섹션 복제 시 UI 즉시 업데이트 (null vs undefined 이슈 해결) - 항목 수정 기능 추가 (useTemplateManagement) - 실시간 동기화 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1671 lines
50 KiB
Markdown
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 |