- ItemMasterDataManagement 컴포넌트 구조화 (tabs, dialogs, components 분리) - HierarchyTab 타입 에러 수정 (BOMItem section_id, updated_at 추가) - API 클라이언트 구현 (item-master.ts, 13개 엔드포인트) - ItemMasterContext 구현 (상태 관리 및 데이터 흐름) - 백엔드 요구사항 문서 작성 (CORS 설정, API 스펙 등) - SSR 호환성 수정 (navigator API typeof window 체크) - 미사용 변수 ESLint 에러 해결 - Context 리팩토링 (AuthContext, RootProvider 추가) - API 유틸리티 추가 (error-handler, logger, transformers) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
958 lines
26 KiB
Markdown
958 lines
26 KiB
Markdown
# 품목기준관리 API 설계 문서
|
|
|
|
**작성일**: 2025-11-18
|
|
**목적**: 품목기준관리 페이지의 설정 데이터를 서버와 동기화하기 위한 API 구조 설계
|
|
|
|
---
|
|
|
|
## 📋 목차
|
|
|
|
1. [개요](#개요)
|
|
2. [데이터 구조 분석](#데이터-구조-분석)
|
|
3. [API 엔드포인트 설계](#api-엔드포인트-설계)
|
|
4. [데이터 모델](#데이터-모델)
|
|
5. [저장/불러오기 시나리오](#저장불러오기-시나리오)
|
|
6. [버전 관리 전략](#버전-관리-전략)
|
|
7. [에러 처리](#에러-처리)
|
|
|
|
---
|
|
|
|
## 개요
|
|
|
|
### 테넌트 정보 구조
|
|
|
|
본 시스템은 로그인 시 받는 실제 테넌트 정보 구조를 기반으로 설계되었습니다.
|
|
|
|
```typescript
|
|
// 로그인 성공 시 받는 실제 사용자 정보
|
|
{
|
|
userId: "TestUser3",
|
|
name: "드미트리",
|
|
tenant: {
|
|
id: 282, // ✅ 테넌트 고유 ID (number 타입)
|
|
company_name: "(주)테크컴퍼니", // 테넌트 회사명
|
|
business_num: "123-45-67890", // 사업자 번호
|
|
tenant_st_code: "trial" // 테넌트 상태 코드
|
|
}
|
|
}
|
|
```
|
|
|
|
**중요**: API 엔드포인트의 `{tenantId}`는 위 구조의 `tenant.id` 값(number 타입, 예: 282)을 의미합니다.
|
|
|
|
### 시스템 흐름
|
|
|
|
```
|
|
┌──────────────────────────────┐
|
|
│ 로그인 (Login) │
|
|
│ tenant.id: 282 (number) │
|
|
└────────┬─────────────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────┐
|
|
│ 테넌트 (Tenant) │
|
|
│ 고유 필드 구성 │
|
|
│ tenant.id 기반 격리 │
|
|
└────────┬─────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────┐
|
|
│ 품목기준관리 페이지 │
|
|
│ (Item Master Config Page) │
|
|
│ │
|
|
│ - 페이지 구조 설정 │
|
|
│ - 섹션 구성 │
|
|
│ - 필드 정의 │
|
|
│ - 마스터 데이터 관리 │
|
|
│ - 버전 관리 │
|
|
└────────┬───────────────────────┘
|
|
│ Save (with tenant.id)
|
|
▼
|
|
┌────────────────────────────────┐
|
|
│ API Server │
|
|
│ (Backend) │
|
|
│ │
|
|
│ - 테넌트별 데이터 저장 │
|
|
│ - tenant.id 검증 │
|
|
│ - 버전 관리 │
|
|
│ - 유효성 검증 │
|
|
└────────┬───────────────────────┘
|
|
│ Load (filtered by tenant.id)
|
|
▼
|
|
┌────────────────────────────────┐
|
|
│ 품목관리 페이지 │
|
|
│ (Item Management Page) │
|
|
│ │
|
|
│ - 설정 기반 동적 폼 생성 │
|
|
│ - 실제 품목 데이터 입력 │
|
|
└────────────────────────────────┘
|
|
```
|
|
|
|
### 핵심 요구사항
|
|
|
|
1. **테넌트 격리**: 각 테넌트별로 독립적인 설정 (`tenant.id` 기반 완전 격리)
|
|
2. **계층 구조**: Page → Section → Field 3단계 계층
|
|
3. **버전 관리**: 설정 변경 이력 추적
|
|
4. **재사용성**: 템플릿 기반 섹션/필드 재사용
|
|
5. **동적 생성**: 설정 기반 품목관리 페이지 동적 렌더링
|
|
6. **서버 검증**: JWT의 tenant.id와 API 요청의 tenantId 일치 검증
|
|
|
|
---
|
|
|
|
## 데이터 구조 분석
|
|
|
|
### 1. 계층 구조 (Hierarchical Structure)
|
|
|
|
```
|
|
ItemMasterConfig (전체 설정)
|
|
│
|
|
├─ ItemPage[] (페이지 배열)
|
|
│ ├─ id
|
|
│ ├─ pageName
|
|
│ ├─ itemType (FG/PT/SM/RM/CS)
|
|
│ └─ sections[]
|
|
│ │
|
|
│ ├─ ItemSection (섹션)
|
|
│ │ ├─ id
|
|
│ │ ├─ title
|
|
│ │ ├─ type ('fields' | 'bom')
|
|
│ │ ├─ order
|
|
│ │ └─ fields[]
|
|
│ │ │
|
|
│ │ └─ ItemField (필드)
|
|
│ │ ├─ id
|
|
│ │ ├─ name
|
|
│ │ ├─ fieldKey
|
|
│ │ ├─ property (ItemFieldProperty)
|
|
│ │ └─ displayCondition
|
|
│
|
|
├─ SectionTemplate[] (재사용 섹션 템플릿)
|
|
│
|
|
├─ ItemMasterField[] (재사용 필드 템플릿)
|
|
│
|
|
└─ MasterData (마스터 데이터들)
|
|
├─ SpecificationMaster[]
|
|
├─ MaterialItemName[]
|
|
├─ ItemCategory[]
|
|
├─ ItemUnit[]
|
|
├─ ItemMaterial[]
|
|
├─ SurfaceTreatment[]
|
|
├─ PartTypeOption[]
|
|
├─ PartUsageOption[]
|
|
└─ GuideRailOption[]
|
|
```
|
|
|
|
### 2. 저장해야 할 데이터 범위
|
|
|
|
#### ✅ 저장 필수 데이터
|
|
1. **페이지 구조** (`itemPages`)
|
|
2. **섹션 템플릿** (`sectionTemplates`)
|
|
3. **항목 마스터** (`itemMasterFields`)
|
|
4. **마스터 데이터** (9가지):
|
|
- 규격 마스터 (`specificationMasters`)
|
|
- 품목명 마스터 (`materialItemNames`)
|
|
- 품목 분류 (`itemCategories`)
|
|
- 단위 (`itemUnits`)
|
|
- 재질 (`itemMaterials`)
|
|
- 표면처리 (`surfaceTreatments`)
|
|
- 부품 유형 옵션 (`partTypeOptions`)
|
|
- 부품 용도 옵션 (`partUsageOptions`)
|
|
- 가이드레일 옵션 (`guideRailOptions`)
|
|
|
|
#### ❌ 저장 불필요 데이터
|
|
- **실제 품목 데이터** (`itemMasters`) - 별도 API로 관리
|
|
|
|
---
|
|
|
|
## API 엔드포인트 설계
|
|
|
|
### Base URL
|
|
```
|
|
/api/tenants/{tenantId}/item-master-config
|
|
```
|
|
|
|
**참고**: `{tenantId}`는 로그인 응답의 `tenant.id` 값(number 타입)입니다. 예: `/api/tenants/282/item-master-config`
|
|
|
|
### 서버 검증 (Server-side Validation)
|
|
|
|
모든 API 요청에서 다음 검증을 수행해야 합니다:
|
|
|
|
```typescript
|
|
// Middleware 예시
|
|
async function validateTenantAccess(req, res, next) {
|
|
// 1. JWT에서 사용자의 tenant.id 추출
|
|
const userTenantId = req.user.tenant.id; // number (예: 282)
|
|
|
|
// 2. URL 파라미터의 tenantId 추출 및 타입 변환
|
|
const requestedTenantId = parseInt(req.params.tenantId, 10);
|
|
|
|
// 3. 일치 검증
|
|
if (userTenantId !== requestedTenantId) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: {
|
|
code: "FORBIDDEN",
|
|
message: "접근 권한이 없습니다.",
|
|
details: {
|
|
userTenantId,
|
|
requestedTenantId,
|
|
reason: "테넌트 ID 불일치"
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
next();
|
|
}
|
|
```
|
|
|
|
### 1. 전체 설정 조회 (GET)
|
|
|
|
#### 엔드포인트
|
|
```
|
|
GET /api/tenants/{tenantId}/item-master-config
|
|
```
|
|
|
|
**예시**: `GET /api/tenants/282/item-master-config`
|
|
|
|
#### Query Parameters
|
|
| Parameter | Type | Required | Description |
|
|
|-----------|------|----------|-------------|
|
|
| version | string | No | 버전 (기본값: latest) |
|
|
| includeInactive | boolean | No | 비활성 항목 포함 여부 (기본값: false) |
|
|
|
|
#### Response
|
|
```typescript
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"tenantId": 282, // ✅ number 타입
|
|
"version": "1.0",
|
|
"lastUpdated": "2025-11-18T10:30:00Z",
|
|
"updatedBy": "TestUser3",
|
|
"config": {
|
|
// 페이지 구조
|
|
"pages": ItemPage[],
|
|
|
|
// 재사용 템플릿
|
|
"sectionTemplates": SectionTemplate[],
|
|
"itemMasterFields": ItemMasterField[],
|
|
|
|
// 마스터 데이터
|
|
"masters": {
|
|
"specifications": SpecificationMaster[],
|
|
"materialNames": MaterialItemName[],
|
|
"categories": ItemCategory[],
|
|
"units": ItemUnit[],
|
|
"materials": ItemMaterial[],
|
|
"surfaceTreatments": SurfaceTreatment[],
|
|
"partTypes": PartTypeOption[],
|
|
"partUsages": PartUsageOption[],
|
|
"guideRailOptions": GuideRailOption[]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. 전체 설정 저장 (POST/PUT)
|
|
|
|
#### 엔드포인트
|
|
```
|
|
POST /api/tenants/{tenantId}/item-master-config
|
|
PUT /api/tenants/{tenantId}/item-master-config/{version}
|
|
```
|
|
|
|
#### Request Body
|
|
```typescript
|
|
{
|
|
"version": "1.0", // 버전 명시 (PUT의 경우 URL의 version과 일치해야 함)
|
|
"comment": "초기 설정 저장", // 변경 사유 (선택)
|
|
"config": {
|
|
"pages": ItemPage[],
|
|
"sectionTemplates": SectionTemplate[],
|
|
"itemMasterFields": ItemMasterField[],
|
|
"masters": {
|
|
"specifications": SpecificationMaster[],
|
|
"materialNames": MaterialItemName[],
|
|
"categories": ItemCategory[],
|
|
"units": ItemUnit[],
|
|
"materials": ItemMaterial[],
|
|
"surfaceTreatments": SurfaceTreatment[],
|
|
"partTypes": PartTypeOption[],
|
|
"partUsages": PartUsageOption[],
|
|
"guideRailOptions": GuideRailOption[]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Response
|
|
```typescript
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"tenantId": 282, // ✅ number 타입
|
|
"version": "1.0",
|
|
"savedAt": "2025-11-18T10:30:00Z",
|
|
"savedBy": "TestUser3"
|
|
},
|
|
"message": "설정이 성공적으로 저장되었습니다."
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. 특정 페이지 조회 (GET)
|
|
|
|
#### 엔드포인트
|
|
```
|
|
GET /api/tenants/{tenantId}/item-master-config/pages/{pageId}
|
|
```
|
|
|
|
**예시**: `GET /api/tenants/282/item-master-config/pages/PAGE-001`
|
|
|
|
#### Response
|
|
```typescript
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"page": ItemPage,
|
|
"metadata": {
|
|
"tenantId": 282, // ✅ number 타입
|
|
"version": "1.0",
|
|
"lastUpdated": "2025-11-18T10:30:00Z"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4. 특정 페이지 업데이트 (PUT)
|
|
|
|
#### 엔드포인트
|
|
```
|
|
PUT /api/tenants/{tenantId}/item-master-config/pages/{pageId}
|
|
```
|
|
|
|
#### Request Body
|
|
```typescript
|
|
{
|
|
"page": ItemPage,
|
|
"comment": "페이지 구조 변경"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5. 페이지 추가 (POST)
|
|
|
|
#### 엔드포인트
|
|
```
|
|
POST /api/tenants/{tenantId}/item-master-config/pages
|
|
```
|
|
|
|
#### Request Body
|
|
```typescript
|
|
{
|
|
"page": {
|
|
"id": "PAGE-001",
|
|
"pageName": "제품 등록",
|
|
"itemType": "FG",
|
|
"sections": [],
|
|
"isActive": true,
|
|
"createdAt": "2025-11-18T10:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6. 섹션 템플릿 관리
|
|
|
|
#### 엔드포인트
|
|
```
|
|
GET /api/tenants/{tenantId}/item-master-config/section-templates
|
|
POST /api/tenants/{tenantId}/item-master-config/section-templates
|
|
PUT /api/tenants/{tenantId}/item-master-config/section-templates/{templateId}
|
|
DELETE /api/tenants/{tenantId}/item-master-config/section-templates/{templateId}
|
|
```
|
|
|
|
---
|
|
|
|
### 7. 항목 마스터 관리
|
|
|
|
#### 엔드포인트
|
|
```
|
|
GET /api/tenants/{tenantId}/item-master-config/item-master-fields
|
|
POST /api/tenants/{tenantId}/item-master-config/item-master-fields
|
|
PUT /api/tenants/{tenantId}/item-master-config/item-master-fields/{fieldId}
|
|
DELETE /api/tenants/{tenantId}/item-master-config/item-master-fields/{fieldId}
|
|
```
|
|
|
|
---
|
|
|
|
### 8. 마스터 데이터 관리
|
|
|
|
각 마스터 데이터별 CRUD API
|
|
|
|
```
|
|
# 규격 마스터
|
|
GET /api/tenants/{tenantId}/item-master-config/masters/specifications
|
|
POST /api/tenants/{tenantId}/item-master-config/masters/specifications
|
|
PUT /api/tenants/{tenantId}/item-master-config/masters/specifications/{id}
|
|
DELETE /api/tenants/{tenantId}/item-master-config/masters/specifications/{id}
|
|
|
|
# 품목명 마스터
|
|
GET /api/tenants/{tenantId}/item-master-config/masters/material-names
|
|
POST /api/tenants/{tenantId}/item-master-config/masters/material-names
|
|
PUT /api/tenants/{tenantId}/item-master-config/masters/material-names/{id}
|
|
DELETE /api/tenants/{tenantId}/item-master-config/masters/material-names/{id}
|
|
|
|
# ... (나머지 마스터 데이터도 동일 패턴)
|
|
```
|
|
|
|
---
|
|
|
|
## 데이터 모델
|
|
|
|
### 1. ItemMasterConfig (전체 설정)
|
|
|
|
```typescript
|
|
interface ItemMasterConfig {
|
|
tenantId: number; // ✅ 테넌트 ID (number 타입, 예: 282)
|
|
version: string; // 버전 (1.0, 1.1, 2.0...)
|
|
lastUpdated: string; // 마지막 업데이트 시간 (ISO 8601)
|
|
updatedBy: string; // 업데이트한 사용자 ID
|
|
comment?: string; // 변경 사유
|
|
config: {
|
|
pages: ItemPage[];
|
|
sectionTemplates: SectionTemplate[];
|
|
itemMasterFields: ItemMasterField[];
|
|
masters: {
|
|
specifications: SpecificationMaster[];
|
|
materialNames: MaterialItemName[];
|
|
categories: ItemCategory[];
|
|
units: ItemUnit[];
|
|
materials: ItemMaterial[];
|
|
surfaceTreatments: SurfaceTreatment[];
|
|
partTypes: PartTypeOption[];
|
|
partUsages: PartUsageOption[];
|
|
guideRailOptions: GuideRailOption[];
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
### 2. API Response 공통 형식
|
|
|
|
#### 성공 응답
|
|
```typescript
|
|
interface ApiSuccessResponse<T> {
|
|
success: true;
|
|
data: T;
|
|
message?: string;
|
|
metadata?: {
|
|
total?: number;
|
|
page?: number;
|
|
pageSize?: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
#### 에러 응답
|
|
```typescript
|
|
interface ApiErrorResponse {
|
|
success: false;
|
|
error: {
|
|
code: string; // 에러 코드 (VALIDATION_ERROR, NOT_FOUND 등)
|
|
message: string; // 사용자용 에러 메시지
|
|
details?: any; // 상세 에러 정보
|
|
timestamp: string; // 에러 발생 시간
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 저장/불러오기 시나리오
|
|
|
|
### 시나리오 1: 초기 설정 저장
|
|
|
|
**상황**: 품목기준관리 페이지에서 처음으로 설정을 저장
|
|
|
|
```typescript
|
|
// 1. 사용자가 Save 버튼 클릭
|
|
// 2. Frontend에서 전체 설정 데이터 준비
|
|
const configData = {
|
|
version: "1.0",
|
|
comment: "초기 설정",
|
|
config: {
|
|
pages: itemPages,
|
|
sectionTemplates: sectionTemplates,
|
|
itemMasterFields: itemMasterFields,
|
|
masters: {
|
|
specifications: specificationMasters,
|
|
materialNames: materialItemNames,
|
|
// ... 나머지 마스터 데이터
|
|
}
|
|
}
|
|
};
|
|
|
|
// 3. API 호출
|
|
const response = await fetch(`/api/tenants/${tenantId}/item-master-config`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(configData)
|
|
});
|
|
|
|
// 4. 성공 시 localStorage 업데이트
|
|
if (response.ok) {
|
|
localStorage.setItem('mes-itemMasterConfig-version', '1.0');
|
|
localStorage.setItem('mes-itemMasterConfig-lastSync', new Date().toISOString());
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 시나리오 2: 설정 불러오기 (페이지 로드)
|
|
|
|
**상황**: 품목기준관리 페이지 접속 시
|
|
|
|
```typescript
|
|
// 1. 컴포넌트 마운트 시 useEffect
|
|
useEffect(() => {
|
|
const loadConfig = async () => {
|
|
try {
|
|
// 2. 서버에서 최신 설정 조회
|
|
const response = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config?version=latest`
|
|
);
|
|
|
|
const { data } = await response.json();
|
|
|
|
// 3. Context 상태 업데이트
|
|
setItemPages(data.config.pages);
|
|
setSectionTemplates(data.config.sectionTemplates);
|
|
setItemMasterFields(data.config.itemMasterFields);
|
|
setSpecificationMasters(data.config.masters.specifications);
|
|
// ... 나머지 데이터 설정
|
|
|
|
// 4. localStorage에 캐시
|
|
localStorage.setItem('mes-itemMasterConfig', JSON.stringify(data));
|
|
localStorage.setItem('mes-itemMasterConfig-version', data.version);
|
|
localStorage.setItem('mes-itemMasterConfig-lastSync', new Date().toISOString());
|
|
|
|
} catch (error) {
|
|
// 5. 에러 시 localStorage 폴백
|
|
const cachedConfig = localStorage.getItem('mes-itemMasterConfig');
|
|
if (cachedConfig) {
|
|
const data = JSON.parse(cachedConfig);
|
|
// ... 캐시된 데이터로 설정
|
|
}
|
|
}
|
|
};
|
|
|
|
loadConfig();
|
|
}, [tenantId]);
|
|
```
|
|
|
|
---
|
|
|
|
### 시나리오 3: 특정 항목만 업데이트
|
|
|
|
**상황**: 규격 마스터 1개만 추가
|
|
|
|
```typescript
|
|
// 1. 새 규격 마스터 추가
|
|
const newSpec = {
|
|
id: "SPEC-NEW-001",
|
|
specificationCode: "2.0T x 1219 x 2438",
|
|
itemType: "RM",
|
|
// ... 나머지 필드
|
|
};
|
|
|
|
// 2. 부분 업데이트 API 호출
|
|
const response = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config/masters/specifications`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(newSpec)
|
|
}
|
|
);
|
|
|
|
// 3. Context 상태 업데이트
|
|
if (response.ok) {
|
|
addSpecificationMaster(newSpec);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 시나리오 4: 버전 업그레이드
|
|
|
|
**상황**: 기존 설정을 기반으로 새 버전 생성
|
|
|
|
```typescript
|
|
// 1. 현재 버전 조회
|
|
const currentConfig = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config?version=1.0`
|
|
).then(res => res.json());
|
|
|
|
// 2. 수정사항 반영
|
|
const updatedConfig = {
|
|
...currentConfig.data.config,
|
|
pages: [...currentConfig.data.config.pages, newPage]
|
|
};
|
|
|
|
// 3. 새 버전으로 저장
|
|
const response = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
version: "1.1",
|
|
comment: "신규 페이지 추가",
|
|
config: updatedConfig
|
|
})
|
|
}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 버전 관리 전략
|
|
|
|
### 1. 버전 네이밍 규칙
|
|
|
|
```
|
|
{MAJOR}.{MINOR}
|
|
|
|
MAJOR: 구조적 변경 (페이지 추가/삭제, 필드 타입 변경)
|
|
MINOR: 데이터 추가 (마스터 데이터 추가, 섹션 추가)
|
|
|
|
예시:
|
|
1.0 - 초기 버전
|
|
1.1 - 마스터 데이터 추가
|
|
1.2 - 섹션 추가
|
|
2.0 - 페이지 구조 변경
|
|
```
|
|
|
|
### 2. 버전 관리 테이블 구조
|
|
|
|
```sql
|
|
CREATE TABLE item_master_config_versions (
|
|
id VARCHAR(50) PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL, -- ✅ number 타입 (tenant.id와 일치)
|
|
version VARCHAR(10) NOT NULL,
|
|
config JSON NOT NULL,
|
|
comment TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
created_by VARCHAR(50),
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
UNIQUE KEY unique_tenant_version (tenant_id, version),
|
|
INDEX idx_tenant_active (tenant_id, is_active),
|
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
|
);
|
|
```
|
|
|
|
**참고**: `tenant_id`는 BIGINT 타입으로 정의하여 로그인 응답의 `tenant.id`(number) 값과 정확히 일치하도록 합니다.
|
|
|
|
### 3. 버전 조회 전략
|
|
|
|
```typescript
|
|
// Latest 버전 조회
|
|
GET /api/tenants/{tenantId}/item-master-config?version=latest
|
|
|
|
// 특정 버전 조회
|
|
GET /api/tenants/{tenantId}/item-master-config?version=1.0
|
|
|
|
// 버전 목록 조회
|
|
GET /api/tenants/{tenantId}/item-master-config/versions
|
|
// Response:
|
|
{
|
|
"versions": [
|
|
{ "version": "1.0", "createdAt": "2025-11-01", "comment": "초기 버전" },
|
|
{ "version": "1.1", "createdAt": "2025-11-10", "comment": "마스터 데이터 추가" },
|
|
{ "version": "2.0", "createdAt": "2025-11-18", "comment": "페이지 구조 변경" }
|
|
],
|
|
"current": "2.0"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 에러 처리
|
|
|
|
### 1. 에러 코드 정의
|
|
|
|
| Code | HTTP Status | Description |
|
|
|------|-------------|-------------|
|
|
| `VALIDATION_ERROR` | 400 | 데이터 유효성 검증 실패 |
|
|
| `UNAUTHORIZED` | 401 | 인증 실패 |
|
|
| `FORBIDDEN` | 403 | 권한 없음 (테넌트 접근 권한 없음) |
|
|
| `NOT_FOUND` | 404 | 설정 또는 버전을 찾을 수 없음 |
|
|
| `CONFLICT` | 409 | 버전 충돌 (이미 존재하는 버전) |
|
|
| `VERSION_MISMATCH` | 409 | 버전 불일치 (동시 수정 충돌) |
|
|
| `SERVER_ERROR` | 500 | 서버 내부 오류 |
|
|
|
|
### 2. 에러 응답 예시
|
|
|
|
#### Validation Error
|
|
```typescript
|
|
{
|
|
"success": false,
|
|
"error": {
|
|
"code": "VALIDATION_ERROR",
|
|
"message": "입력 데이터가 올바르지 않습니다.",
|
|
"details": {
|
|
"field": "config.pages[0].sections[0].fields[0].property.inputType",
|
|
"message": "inputType은 필수입니다.",
|
|
"value": null
|
|
},
|
|
"timestamp": "2025-11-18T10:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Version Conflict
|
|
```typescript
|
|
{
|
|
"success": false,
|
|
"error": {
|
|
"code": "CONFLICT",
|
|
"message": "버전 1.0이 이미 존재합니다.",
|
|
"details": {
|
|
"existingVersion": "1.0",
|
|
"suggestedVersion": "1.1"
|
|
},
|
|
"timestamp": "2025-11-18T10:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 프론트엔드 구현 가이드
|
|
|
|
### 1. API 클라이언트 생성
|
|
|
|
```typescript
|
|
// src/lib/api/itemMasterConfigApi.ts
|
|
|
|
export const ItemMasterConfigAPI = {
|
|
// 전체 설정 조회
|
|
async getConfig(tenantId: number, version = 'latest') { // ✅ number 타입
|
|
const response = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config?version=${version}`
|
|
);
|
|
if (!response.ok) throw new Error('설정 조회 실패');
|
|
return response.json();
|
|
},
|
|
|
|
// 전체 설정 저장
|
|
async saveConfig(tenantId: number, config: ItemMasterConfig) { // ✅ number 타입
|
|
const response = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config)
|
|
}
|
|
);
|
|
if (!response.ok) throw new Error('설정 저장 실패');
|
|
return response.json();
|
|
},
|
|
|
|
// 페이지 조회
|
|
async getPage(tenantId: number, pageId: string) { // ✅ number 타입
|
|
const response = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config/pages/${pageId}`
|
|
);
|
|
if (!response.ok) throw new Error('페이지 조회 실패');
|
|
return response.json();
|
|
},
|
|
|
|
// 규격 마스터 추가
|
|
async addSpecification(tenantId: number, spec: SpecificationMaster) { // ✅ number 타입
|
|
const response = await fetch(
|
|
`/api/tenants/${tenantId}/item-master-config/masters/specifications`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(spec)
|
|
}
|
|
);
|
|
if (!response.ok) throw new Error('규격 추가 실패');
|
|
return response.json();
|
|
}
|
|
};
|
|
```
|
|
|
|
**사용 예시**:
|
|
```typescript
|
|
// AuthContext에서 tenant.id를 추출하여 사용
|
|
const { user } = useAuth();
|
|
const tenantId = user.tenant.id; // number 타입 (예: 282)
|
|
|
|
// API 호출
|
|
const config = await ItemMasterConfigAPI.getConfig(tenantId);
|
|
```
|
|
|
|
### 2. Context 통합
|
|
|
|
```typescript
|
|
// ItemMasterContext.tsx
|
|
|
|
// 서버 동기화 함수 추가
|
|
const syncWithServer = async () => {
|
|
try {
|
|
const { data } = await ItemMasterConfigAPI.getConfig(tenantId);
|
|
|
|
// 모든 상태 업데이트
|
|
setItemPages(data.config.pages);
|
|
setSectionTemplates(data.config.sectionTemplates);
|
|
// ... 나머지 데이터
|
|
|
|
// localStorage 캐시
|
|
localStorage.setItem('mes-itemMasterConfig', JSON.stringify(data));
|
|
localStorage.setItem('mes-itemMasterConfig-lastSync', new Date().toISOString());
|
|
|
|
} catch (error) {
|
|
console.error('서버 동기화 실패:', error);
|
|
// localStorage 폴백
|
|
}
|
|
};
|
|
|
|
// 저장 함수 추가
|
|
const saveToServer = async () => {
|
|
try {
|
|
const configData = {
|
|
version: currentVersion,
|
|
comment: saveComment,
|
|
config: {
|
|
pages: itemPages,
|
|
sectionTemplates: sectionTemplates,
|
|
itemMasterFields: itemMasterFields,
|
|
masters: {
|
|
specifications: specificationMasters,
|
|
materialNames: materialItemNames,
|
|
// ... 나머지
|
|
}
|
|
}
|
|
};
|
|
|
|
await ItemMasterConfigAPI.saveConfig(tenantId, configData);
|
|
|
|
} catch (error) {
|
|
console.error('저장 실패:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 다음 단계
|
|
|
|
### Phase 1: API 모킹 (현재)
|
|
1. ✅ API 구조 설계 완료
|
|
2. ⏳ Mock API 서버 구현 (MSW 또는 json-server)
|
|
3. ⏳ 프론트엔드 API 클라이언트 구현
|
|
4. ⏳ Context와 API 통합
|
|
|
|
### Phase 2: 백엔드 구현
|
|
1. ⏳ 데이터베이스 스키마 설계
|
|
2. ⏳ API 엔드포인트 구현
|
|
3. ⏳ 인증/권한 처리
|
|
4. ⏳ 버전 관리 로직 구현
|
|
|
|
### Phase 3: 품목관리 페이지 동적 생성
|
|
1. ⏳ 설정 기반 폼 렌더러 구현
|
|
2. ⏳ 조건부 표시 로직 구현
|
|
3. ⏳ 유효성 검증 구현
|
|
4. ⏳ 실제 품목 데이터 저장 API 연동
|
|
|
|
---
|
|
|
|
## 부록
|
|
|
|
### A. localStorage 키 규칙
|
|
|
|
**❌ 기존 (tenant.id 없음 - 데이터 오염 위험)**:
|
|
```typescript
|
|
// 테넌트 ID가 없어서 테넌트 전환 시 데이터 오염 발생!
|
|
'mes-itemMasterConfig'
|
|
'mes-specificationMasters'
|
|
```
|
|
|
|
**✅ 권장 (tenant.id 포함 - 완전한 격리)**:
|
|
```typescript
|
|
// 설정 데이터 (tenant.id 포함)
|
|
`mes-${tenantId}-itemMasterConfig` // 예: 'mes-282-itemMasterConfig'
|
|
`mes-${tenantId}-itemMasterConfig-version`
|
|
`mes-${tenantId}-itemMasterConfig-lastSync`
|
|
|
|
// 개별 마스터 데이터 (tenant.id + 버전 포함)
|
|
`mes-${tenantId}-specificationMasters` // 예: 'mes-282-specificationMasters'
|
|
`mes-${tenantId}-specificationMasters-version`
|
|
`mes-${tenantId}-materialItemNames`
|
|
`mes-${tenantId}-materialItemNames-version`
|
|
`mes-${tenantId}-itemCategories`
|
|
`mes-${tenantId}-itemUnits`
|
|
`mes-${tenantId}-itemMaterials`
|
|
`mes-${tenantId}-surfaceTreatments`
|
|
`mes-${tenantId}-partTypeOptions`
|
|
`mes-${tenantId}-partUsageOptions`
|
|
`mes-${tenantId}-guideRailOptions`
|
|
```
|
|
|
|
**구현 예시**:
|
|
```typescript
|
|
// TenantAwareCache 클래스 사용 (권장)
|
|
// 자세한 구현은 [REF-2025-11-19] multi-tenancy-implementation.md 참조
|
|
const cache = new TenantAwareCache(user.tenant.id);
|
|
cache.set('itemMasterConfig', configData);
|
|
|
|
// 또는 직접 구현
|
|
const key = `mes-${user.tenant.id}-itemMasterConfig`; // 'mes-282-itemMasterConfig'
|
|
localStorage.setItem(key, JSON.stringify(configData));
|
|
```
|
|
|
|
**테넌트 전환 시 캐시 삭제**:
|
|
```typescript
|
|
// 로그아웃 또는 테넌트 전환 시
|
|
function clearTenantCache(tenantId: number) {
|
|
const keys = Object.keys(localStorage);
|
|
const prefix = `mes-${tenantId}-`;
|
|
keys.forEach(key => {
|
|
if (key.startsWith(prefix)) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
### B. 타입 정의 파일 위치
|
|
|
|
```
|
|
src/
|
|
├─ types/
|
|
│ ├─ itemMaster.ts # 품목 관련 타입
|
|
│ ├─ itemMasterConfig.ts # 설정 관련 타입
|
|
│ └─ api.ts # API 응답 타입
|
|
├─ lib/
|
|
│ └─ api/
|
|
│ └─ itemMasterConfigApi.ts # API 클라이언트
|
|
└─ contexts/
|
|
└─ ItemMasterContext.tsx # Context (기존)
|
|
```
|
|
|
|
---
|
|
|
|
**문서 버전**: 1.0
|
|
**마지막 업데이트**: 2025-11-18 |