diff --git a/INDEX.md b/INDEX.md
index 1f0c971..4eaa014 100644
--- a/INDEX.md
+++ b/INDEX.md
@@ -77,7 +77,10 @@ docs/
|------|------|--------------|
| [database-schema.md](specs/database-schema.md) | DB 구조 및 관계도 | DB 변경 전 |
| [board-system-spec.md](specs/board-system-spec.md) | 게시판 시스템 설계 | 게시판 작업 전 |
-| [item-master-integration.md](specs/item-master-integration.md) | 품목관리 통합 스펙 | 품목 관련 작업 전 |
+| [**ITEM-MASTER-INDEX.md**](specs/ITEM-MASTER-INDEX.md) | **품목관리 문서 인덱스 (개발 현황)** | **품목 작업 전 필수** |
+| [item-master-integration.md](specs/item-master-integration.md) | 품목관리 연동 설계 | 품목 연동 구현 시 |
+| [item-master-field-key-validation.md](specs/item-master-field-key-validation.md) | ItemMaster 필드 키 검증 | 품목 필드 작업 전 |
+| [item-master-field-integration.md](specs/item-master-field-integration.md) | ItemMaster 필드 통합 계획 | 품목 필드 통합 시 |
| [docker-setup.md](specs/docker-setup.md) | Docker 환경 구성 | 환경 설정 시 |
| [remote-work-setup.md](specs/remote-work-setup.md) | 원격 개발 설정 | 원격 작업 시 |
@@ -106,12 +109,22 @@ docs/
|------|------|
| [item-master-guide.md](front/item-master-guide.md) | 품목기준관리 페이지-섹션-필드 구조 |
+> 날짜별 API 요청 문서는 `history/2025-11/front-requests/`로 이동됨
+
+### data/ - 데이터 분석
+> 시스템 분석, 데이터 모델링
+
+| 문서 | 설명 |
+|------|------|
+| [analysis/item-db-analysis.md](data/analysis/item-db-analysis.md) | Item DB/API 분석 최종본 |
+
### features/ - 기능별 문서
| 문서 | 설명 |
|------|------|
| [boards/README.md](features/boards/README.md) | 게시판 시스템 구현 |
| [boards/mng-implementation.md](features/boards/mng-implementation.md) | MNG 게시판 구현 상세 |
+| [hr/hr-api-analysis.md](features/hr/hr-api-analysis.md) | HR API 분석 (근태/직원/부서) |
### projects/ - 프로젝트별 문서
@@ -125,7 +138,7 @@ docs/
| 기간 | 문서 |
|------|------|
-| **2025-11** | [item-master-gap-analysis.md](history/2025-11/item-master-gap-analysis.md), [item-master-spec.md](history/2025-11/item-master-spec.md) |
+| **2025-11** | [item-master-gap-analysis.md](history/2025-11/item-master-gap-analysis.md), [item-master-spec.md](history/2025-11/item-master-spec.md), [front-requests/](history/2025-11/front-requests/), [item-master-archived/](history/2025-11/item-master-archived/) |
| **2025-09** | [checkpoint.md](history/2025-09/checkpoint.md), [database-schema.md](history/2025-09/database-schema.md) |
| **Roadmaps** | [december-2025.md](history/roadmaps/december-2025.md) |
@@ -176,6 +189,25 @@ API Flow Tester에서 생성되는 JSON 파일 저장 경로
## 🔄 문서 구조 변경 이력
+- **2025-12-09**: Item Master 문서 정리 및 인덱스 생성
+ - `specs/ITEM-MASTER-INDEX.md` 생성 (개발 현황/필요 항목 정리)
+ - `history/2025-11/item-master-archived/` 생성 (구버전 문서 아카이브)
+ - 중복 문서 정리 (front-requests → history 이동)
+
+- **2025-12-09**: 문서 정리 및 통합
+ - 중복 분석 문서 삭제 (v2, DB_Modeling)
+ - `SAM_Item_DB_API_Analysis_v3_FINAL.md` → `item-db-analysis.md` 리네임
+ - `ITEM_MASTER_FIELD_INTEGRATION_PLAN.md` → `item-master-field-integration.md` 리네임
+ - `HR_API_ANALYSIS.md` → `features/hr/hr-api-analysis.md` 이동
+ - 날짜 접두사 front 문서 → `history/2025-11/front-requests/` 이동
+ - api/docs에서 프로젝트 문서 분리 (swagger, api-flows만 유지)
+
+- **2025-12-09**: api/docs 문서 통합
+ - `api/docs/analysis/` → `docs/data/analysis/` 이동
+ - `api/docs/front/` → `docs/front/` 병합
+ - `api/docs/specs/` → `docs/specs/` 병합
+ - api/docs에는 API 구성/설정 문서만 유지 (swagger, api-flows)
+
- **2025-12-09**: `plans/` 폴더 추가
- 개발 계획 문서용 임시 폴더
- 작업 완료 후 정리 → 삭제 워크플로우
diff --git a/front/item-master-guide.md b/front/item-master-guide.md
index a09effa..42c5def 100644
--- a/front/item-master-guide.md
+++ b/front/item-master-guide.md
@@ -1,70 +1,246 @@
-# 품목기준관리(ItemMaster) 프론트엔드 가이드
+# 품목기준관리(ItemMaster) API 가이드
-> 📌 **품목설정 시스템의 구조, API, 잠금 기능에 대한 프론트엔드 개발 가이드**
+> 품목 입력 화면을 구성하는 **페이지-섹션-필드** 구조 관리 시스템
---
## 1. 개요
-품목기준관리(ItemMaster)는 제품의 입력 화면을 구성하는 **페이지-섹션-필드** 구조를 관리하는 시스템입니다.
-
### 1.1 핵심 개념
-| 엔티티 | 설명 | 예시 |
-|--------|------|------|
-| **Page** | 품목 유형별 화면 (FG, PT, SM, RM, CS) | "완제품 기본정보", "부품 상세" |
-| **Section** | 페이지 내 논리적 영역 | "제품 상세", "BOM 정보" |
-| **Field** | 섹션 내 입력 항목 | "제품명", "규격", "단가" |
-| **BomItem** | BOM 섹션 내 부품 항목 | "부품A x 2개" |
+| 엔티티 | 테이블 | 설명 |
+|--------|--------|------|
+| **Page** | `item_pages` | 품목 유형별 화면 (FG, PT, SM, RM, CS) |
+| **Section** | `item_sections` | 페이지 내 논리적 영역 |
+| **Field** | `item_fields` | 섹션 내 입력 항목 |
+| **BomItem** | `item_bom_items` | BOM 섹션 내 부품 항목 |
+| **CustomTab** | `custom_tabs` | 커스텀 탭 설정 |
+| **UnitOption** | `unit_options` | 단위 옵션 |
-### 1.2 아키텍처 특징
+### 1.2 아키텍처
- **독립 엔티티 구조**: 섹션, 필드, BOM은 독립적으로 존재하며 재사용 가능
- **링크 테이블**: `entity_relationships`로 관계 관리
- **연결 잠금**: 중요한 구조는 잠금으로 보호 가능
+```
+ItemPage (item_type: FG, PT, SM, RM, CS)
+ │
+ │ entity_relationships (is_locked)
+ ▼
+ItemSection (type: default, bom, custom)
+ │
+ ├─ entity_relationships → ItemField
+ └─ entity_relationships → ItemBomItem
+```
+
---
-## 2. 데이터 구조
+## 2. API 엔드포인트
-### 2.1 엔티티 관계도
+### 2.1 초기화
-```
-┌─────────────────────────────────────────────────────────────────┐
-│ ItemPage │
-│ (id, page_name, item_type, is_active) │
-└─────────────┬──────────────────────────────────────────────────┘
- │ entity_relationships (is_locked)
- ▼
-┌─────────────────────────────────────────────────────────────────┐
-│ ItemSection │
-│ (id, title, type, is_template, is_default) │
-└─────────────┬───────────────────────────────────┬──────────────┘
- │ entity_relationships │ entity_relationships
- │ (is_locked) │ (is_locked)
- ▼ ▼
-┌─────────────────────────────┐ ┌─────────────────────────────┐
-│ ItemField │ │ ItemBomItem │
-│ (id, field_name, │ │ (id, item_code, │
-│ field_type, is_required) │ │ item_name, quantity) │
-└─────────────────────────────┘ └─────────────────────────────┘
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/init` | 전체 데이터 로드 |
+
+**응답 구조:**
+```json
+{
+ "pages": [], // 페이지 + 연결된 섹션/필드
+ "sections": [], // 모든 독립 섹션
+ "fields": [], // 모든 독립 필드
+ "customTabs": [], // 커스텀 탭
+ "unitOptions": [] // 단위 옵션
+}
```
-### 2.2 entity_relationships 테이블
+### 2.2 페이지
-모든 엔티티 간 관계는 `entity_relationships` 테이블로 관리됩니다.
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/pages` | 페이지 목록 |
+| POST | `/api/v1/item-master/pages` | 페이지 생성 |
+| PUT | `/api/v1/item-master/pages/{id}` | 페이지 수정 |
+| DELETE | `/api/v1/item-master/pages/{id}` | 페이지 삭제 |
+
+### 2.3 섹션
+
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/sections` | 독립 섹션 목록 |
+| POST | `/api/v1/item-master/sections` | 독립 섹션 생성 |
+| POST | `/api/v1/item-master/pages/{pageId}/sections` | 섹션 생성 + 페이지 연결 |
+| PUT | `/api/v1/item-master/sections/{id}` | 섹션 수정 |
+| DELETE | `/api/v1/item-master/sections/{id}` | 섹션 삭제 |
+| POST | `/api/v1/item-master/sections/{id}/clone` | 섹션 복제 |
+| GET | `/api/v1/item-master/sections/{id}/usage` | 사용처 조회 |
+| PUT | `/api/v1/item-master/pages/{pageId}/sections/reorder` | 섹션 순서 변경 |
+
+### 2.4 필드
+
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/fields` | 독립 필드 목록 |
+| POST | `/api/v1/item-master/fields` | 독립 필드 생성 |
+| POST | `/api/v1/item-master/sections/{sectionId}/fields` | 필드 생성 + 섹션 연결 |
+| PUT | `/api/v1/item-master/fields/{id}` | 필드 수정 |
+| DELETE | `/api/v1/item-master/fields/{id}` | 필드 삭제 |
+| POST | `/api/v1/item-master/fields/{id}/clone` | 필드 복제 |
+| GET | `/api/v1/item-master/fields/{id}/usage` | 사용처 조회 |
+| PUT | `/api/v1/item-master/sections/{sectionId}/fields/reorder` | 필드 순서 변경 |
+
+### 2.5 BOM
+
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/bom-items` | 독립 BOM 목록 |
+| POST | `/api/v1/item-master/bom-items` | 독립 BOM 생성 |
+| POST | `/api/v1/item-master/sections/{sectionId}/bom-items` | BOM 생성 + 섹션 연결 |
+| PUT | `/api/v1/item-master/bom-items/{id}` | BOM 수정 |
+| DELETE | `/api/v1/item-master/bom-items/{id}` | BOM 삭제 |
+
+### 2.6 섹션 템플릿
+
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/section-templates` | 템플릿 목록 |
+| POST | `/api/v1/item-master/section-templates` | 템플릿 생성 |
+| PUT | `/api/v1/item-master/section-templates/{id}` | 템플릿 수정 |
+| DELETE | `/api/v1/item-master/section-templates/{id}` | 템플릿 삭제 |
+
+### 2.7 커스텀 탭
+
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/custom-tabs` | 탭 목록 |
+| POST | `/api/v1/item-master/custom-tabs` | 탭 생성 |
+| PUT | `/api/v1/item-master/custom-tabs/{id}` | 탭 수정 |
+| DELETE | `/api/v1/item-master/custom-tabs/{id}` | 탭 삭제 |
+| PUT | `/api/v1/item-master/custom-tabs/reorder` | 탭 순서 변경 |
+
+### 2.8 단위 옵션
+
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/unit-options` | 단위 목록 |
+| POST | `/api/v1/item-master/unit-options` | 단위 생성 |
+| DELETE | `/api/v1/item-master/unit-options/{id}` | 단위 삭제 |
+
+### 2.9 엔티티 관계 (Link/Unlink)
+
+**페이지-섹션 연결:**
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| POST | `/api/v1/item-master/pages/{pageId}/link-section` | 섹션 연결 |
+| DELETE | `/api/v1/item-master/pages/{pageId}/unlink-section/{sectionId}` | 섹션 연결 해제 |
+
+**페이지-필드 연결:**
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| POST | `/api/v1/item-master/pages/{pageId}/link-field` | 필드 연결 |
+| DELETE | `/api/v1/item-master/pages/{pageId}/unlink-field/{fieldId}` | 필드 연결 해제 |
+
+**섹션-필드 연결:**
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| POST | `/api/v1/item-master/sections/{sectionId}/link-field` | 필드 연결 |
+| DELETE | `/api/v1/item-master/sections/{sectionId}/unlink-field/{fieldId}` | 필드 연결 해제 |
+
+**섹션-BOM 연결:**
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| POST | `/api/v1/item-master/sections/{sectionId}/link-bom` | BOM 연결 |
+| DELETE | `/api/v1/item-master/sections/{sectionId}/unlink-bom/{bomId}` | BOM 연결 해제 |
+
+**관계 조회/정렬:**
+| Method | Endpoint | 설명 |
+|--------|----------|------|
+| GET | `/api/v1/item-master/pages/{pageId}/relationships` | 페이지 관계 조회 |
+| GET | `/api/v1/item-master/pages/{pageId}/structure` | 페이지 구조 조회 |
+| GET | `/api/v1/item-master/sections/{sectionId}/relationships` | 섹션 관계 조회 |
+| POST | `/api/v1/item-master/relationships/reorder` | 관계 순서 변경 |
+
+---
+
+## 3. 데이터 구조
+
+### 3.1 ItemPage
+
+```typescript
+interface ItemPage {
+ id: number;
+ tenant_id: number;
+ group_id: number;
+ page_name: string;
+ item_type: 'FG' | 'PT' | 'SM' | 'RM' | 'CS';
+ source_table: 'products' | 'materials';
+ absolute_path?: string;
+ is_active: boolean;
+ sections: ItemSection[]; // init 응답에 포함
+}
+```
+
+### 3.2 ItemSection
+
+```typescript
+interface ItemSection {
+ id: number;
+ tenant_id: number;
+ group_id: number;
+ title: string;
+ type: string;
+ order_no: number;
+ is_template: boolean;
+ is_default: boolean;
+ is_locked?: boolean; // 연결 잠금 상태
+ description?: string;
+ fields?: ItemField[];
+ bom_items?: ItemBomItem[];
+}
+```
+
+### 3.3 ItemField
+
+```typescript
+interface ItemField {
+ id: number;
+ tenant_id: number;
+ group_id: number;
+ field_name: string;
+ field_key: string; // 저장 시 사용할 키
+ field_type: FieldType;
+ order_no: number;
+ is_required: boolean;
+ is_common: boolean;
+ is_active: boolean;
+ is_locked: boolean;
+ default_value?: string;
+ placeholder?: string;
+ display_condition?: object; // 조건부 표시
+ validation_rules?: object; // 유효성 검사 규칙
+ options?: object; // dropdown 옵션 등
+ properties?: object; // 추가 설정
+ category?: string;
+ description?: string;
+}
+
+type FieldType = 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
+```
+
+### 3.4 EntityRelationship
```typescript
interface EntityRelationship {
id: number;
tenant_id: number;
- group_id: number; // 1: ItemMaster
+ group_id: number;
parent_type: 'page' | 'section';
parent_id: number;
child_type: 'section' | 'field' | 'bom';
child_id: number;
order_no: number;
- is_locked: boolean; // ⭐ 잠금 여부
+ is_locked: boolean;
locked_by?: number;
locked_at?: string;
metadata?: object;
@@ -73,9 +249,9 @@ interface EntityRelationship {
---
-## 3. 잠금(Lock) 기능
+## 4. 잠금(Lock) 기능
-### 3.1 잠금의 의미
+### 4.1 잠금의 의미
**연결이 잠기면:**
- 해당 연결(관계)를 해제할 수 없음
@@ -90,175 +266,40 @@ interface EntityRelationship {
└─ ✅ 섹션에 새 필드 추가 가능
```
-### 3.2 잠금 상태 확인
+### 4.2 잠금 상태 확인
init API 응답에 `is_locked` 필드가 포함됩니다.
-```typescript
-// GET /api/v1/item-master/init 응답
+```json
{
- "pages": [
- {
- "id": 1,
- "page_name": "완제품 기본정보",
- "sections": [
- {
- "id": 10,
- "title": "제품 상세",
- "is_locked": true, // ⭐ 이 연결이 잠김
- "fields": [
- {
- "id": 100,
- "field_name": "제품명",
- "is_locked": false // ⭐ 이 연결은 잠기지 않음
- }
- ]
- }
- ]
- }
- ]
+ "pages": [{
+ "id": 1,
+ "sections": [{
+ "id": 10,
+ "is_locked": true,
+ "fields": [{
+ "id": 100,
+ "is_locked": false
+ }]
+ }]
+ }]
}
```
-### 3.3 프론트엔드 처리 가이드
+### 4.3 잠금 관련 에러
-```typescript
-// 잠금 상태에 따른 UI 처리 예시
-interface SectionProps {
- section: ItemSection;
- isLocked: boolean; // 부모로부터 전달받은 잠금 상태
-}
-
-function SectionItem({ section, isLocked }: SectionProps) {
- return (
-
-
- {section.title}
- {isLocked && } {/* 잠금 아이콘 표시 */}
-
-
- {/* 잠금 시 삭제/분리 버튼 비활성화 */}
-
-
-
-
- {/* 수정은 항상 가능 */}
-
-
- );
-}
-```
-
-### 3.4 잠금 관련 에러 처리
-
-잠금된 항목에 대해 삭제/해제 시도 시 에러가 반환됩니다.
-
-```typescript
-// 에러 응답 예시
+```json
{
"success": false,
"message": "잠금된 연결은 해제할 수 없습니다.",
- "error": "relationship_locked"
-}
-
-// 프론트엔드 에러 처리
-try {
- await deleteSection(sectionId);
-} catch (error) {
- if (error.response?.data?.error === 'entity_protected_by_locked_relationship') {
- toast.error('잠금된 연결로 보호된 항목은 삭제할 수 없습니다.');
- }
+ "error": "entity_protected_by_locked_relationship"
}
```
---
-## 4. API 엔드포인트
-
-### 4.1 초기화 API
-
-| Method | Endpoint | 설명 |
-|--------|----------|------|
-| GET | `/api/v1/item-master/init` | 전체 데이터 로드 (페이지, 섹션, 커스텀탭, 단위옵션) |
-
-**응답 구조:**
-```typescript
-interface InitResponse {
- pages: ItemPage[]; // 페이지 + 연결된 섹션/필드
- sections: ItemSection[]; // 모든 독립 섹션 (재사용 풀)
- customTabs: CustomTab[];
- unitOptions: UnitOption[];
-}
-```
-
-### 4.2 페이지 API
-
-| Method | Endpoint | 설명 |
-|--------|----------|------|
-| GET | `/api/v1/item-master/pages` | 페이지 목록 |
-| POST | `/api/v1/item-master/pages` | 페이지 생성 |
-| PUT | `/api/v1/item-master/pages/{id}` | 페이지 수정 |
-| DELETE | `/api/v1/item-master/pages/{id}` | 페이지 삭제 |
-
-### 4.3 섹션 API
-
-| Method | Endpoint | 설명 |
-|--------|----------|------|
-| GET | `/api/v1/item-master/sections` | 독립 섹션 목록 |
-| POST | `/api/v1/item-master/sections` | 독립 섹션 생성 |
-| POST | `/api/v1/item-master/pages/{pageId}/sections` | 섹션 생성 + 페이지 연결 |
-| PUT | `/api/v1/item-master/sections/{id}` | 섹션 수정 |
-| DELETE | `/api/v1/item-master/sections/{id}` | 섹션 삭제 |
-| POST | `/api/v1/item-master/sections/{id}/clone` | 섹션 복제 |
-| GET | `/api/v1/item-master/sections/{id}/usage` | 사용처 조회 |
-
-### 4.4 필드 API
-
-| Method | Endpoint | 설명 |
-|--------|----------|------|
-| GET | `/api/v1/item-master/fields` | 독립 필드 목록 |
-| POST | `/api/v1/item-master/fields` | 독립 필드 생성 |
-| POST | `/api/v1/item-master/sections/{sectionId}/fields` | 필드 생성 + 섹션 연결 |
-| PUT | `/api/v1/item-master/fields/{id}` | 필드 수정 |
-| DELETE | `/api/v1/item-master/fields/{id}` | 필드 삭제 |
-| POST | `/api/v1/item-master/fields/{id}/clone` | 필드 복제 |
-| GET | `/api/v1/item-master/fields/{id}/usage` | 사용처 조회 |
-
-### 4.5 BOM API
-
-| Method | Endpoint | 설명 |
-|--------|----------|------|
-| GET | `/api/v1/item-master/bom-items` | 독립 BOM 목록 |
-| POST | `/api/v1/item-master/bom-items` | 독립 BOM 생성 |
-| POST | `/api/v1/item-master/sections/{sectionId}/bom-items` | BOM 생성 + 섹션 연결 |
-| PUT | `/api/v1/item-master/bom-items/{id}` | BOM 수정 |
-| DELETE | `/api/v1/item-master/bom-items/{id}` | BOM 삭제 |
-
-### 4.6 순서 변경 API
-
-| Method | Endpoint | 설명 |
-|--------|----------|------|
-| PUT | `/api/v1/item-master/pages/{pageId}/sections/reorder` | 섹션 순서 변경 |
-| PUT | `/api/v1/item-master/sections/{sectionId}/fields/reorder` | 필드 순서 변경 |
-
----
-
## 5. 필드 타입
-### 5.1 지원 필드 타입
-
| field_type | 설명 | 렌더링 컴포넌트 |
|------------|------|----------------|
| `textbox` | 텍스트 입력 | `` |
@@ -268,111 +309,31 @@ interface InitResponse {
| `date` | 날짜 선택 | `` |
| `textarea` | 장문 텍스트 | `` |
-### 5.2 필드 속성
-
-```typescript
-interface ItemField {
- id: number;
- field_name: string;
- field_type: FieldType;
- order_no: number;
- is_required: boolean;
- is_locked?: boolean; // init 응답에 포함
- default_value?: string;
- placeholder?: string;
- display_condition?: object; // 조건부 표시
- validation_rules?: object; // 유효성 검사 규칙
- options?: object; // dropdown 옵션 등
- properties?: object; // 추가 설정
- category?: string;
- description?: string;
- is_common?: boolean;
-}
-```
-
---
-## 6. 상태 관리 권장 패턴
+## 6. 주의사항
-### 6.1 Zustand Store 예시
-
-```typescript
-interface ItemMasterStore {
- // 데이터
- pages: ItemPage[];
- sections: ItemSection[];
- customTabs: CustomTab[];
- unitOptions: UnitOption[];
-
- // 상태
- isLoading: boolean;
- selectedPageId: number | null;
- selectedSectionId: number | null;
-
- // 액션
- fetchInit: () => Promise;
- addSection: (pageId: number, data: CreateSectionDTO) => Promise;
- updateSection: (id: number, data: UpdateSectionDTO) => Promise;
- deleteSection: (id: number) => Promise;
- reorderSections: (pageId: number, items: ReorderItem[]) => Promise;
-
- // 유틸리티
- getSectionsByPage: (pageId: number) => ItemSection[];
- isEntityLocked: (entityType: string, entityId: number) => boolean;
-}
-```
-
-### 6.2 잠금 상태 캐싱
-
-```typescript
-// 잠금 상태를 Map으로 캐싱
-const lockStatusMap = new Map();
-
-function buildLockStatusMap(pages: ItemPage[]) {
- pages.forEach(page => {
- page.sections?.forEach(section => {
- lockStatusMap.set(`section:${section.id}`, section.is_locked);
-
- section.fields?.forEach(field => {
- lockStatusMap.set(`field:${field.id}`, field.is_locked);
- });
-
- section.bom_items?.forEach(bom => {
- lockStatusMap.set(`bom:${bom.id}`, bom.is_locked);
- });
- });
- });
-}
-
-function isLocked(entityType: string, entityId: number): boolean {
- return lockStatusMap.get(`${entityType}:${entityId}`) || false;
-}
-```
-
----
-
-## 7. 주의사항
-
-### 7.1 삭제 시 동작
+### 6.1 삭제 시 동작
- **페이지 삭제**: 연결된 섹션/필드는 삭제되지 않고 관계만 해제
- **섹션 삭제**: 연결된 필드/BOM은 삭제되지 않고 관계만 해제
- **잠금된 연결이 있으면**: 삭제/해제 불가
-### 7.2 복제(Clone) 시 동작
+### 6.2 복제(Clone) 시 동작
- 섹션 복제: 섹션 + 필드 + BOM 모두 복제
- 필드 복제: 필드만 복제
- 복제된 항목은 독립 엔티티로 생성됨
-### 7.3 순서 변경
+### 6.3 순서 변경
- Drag & Drop 후 reorder API 호출 필요
- `items` 배열에 `{id, order_no}` 형태로 전달
---
-## 8. 변경 이력
+## 7. 변경 이력
-| 날짜 | 버전 | 변경 내용 |
-|------|------|----------|
-| 2025-11-27 | 1.0.0 | 잠금(Lock) 기능 추가 |
-| 2025-11-26 | 0.9.0 | 독립 엔티티 아키텍처 적용 |
-| 2025-11-20 | 0.8.0 | entity_relationships 링크 테이블 도입 |
+| 날짜 | 변경 내용 |
+|------|----------|
+| 2025-12-09 | 시스템 기반 문서 전면 재작성 |
+| 2025-11-27 | 잠금(Lock) 기능 추가 |
+| 2025-11-26 | 독립 엔티티 아키텍처 적용 |
+| 2025-11-20 | entity_relationships 링크 테이블 도입 |
diff --git a/plans/hr-api-react-sync-plan.md b/plans/hr-api-react-sync-plan.md
deleted file mode 100644
index 8a2269b..0000000
--- a/plans/hr-api-react-sync-plan.md
+++ /dev/null
@@ -1,669 +0,0 @@
-# HR API - React 동기화 계획
-
-> **작성일**: 2025-12-09
-> **수정일**: 2025-12-09
-> **목적**: API와 React 프론트엔드 간 데이터 타입 동기화
-> **원칙**: **API snake_case 유지** - React에서 camelCase 변환 처리
-
----
-
-## 📋 작업 요약
-
-| 영역 | 작업 | 수정 필요 |
-|------|------|----------|
-| **Employee API** | 기존 snake_case 유지 | ❌ 불필요 |
-| **Attendance API** | 기존 snake_case 유지 | ❌ 불필요 |
-| **Department Tree API** | 기존 snake_case 유지 | ❌ 불필요 |
-| **React 프론트엔드** | 변환 유틸리티 적용 | ✅ 프론트엔드 |
-
----
-
-# Part 1: 백엔드 (API) - 변경 없음
-
-## 설계 원칙
-
-- **API 응답은 snake_case 유지** (Laravel 표준)
-- **json_extra, json_details 구조 그대로 유지**
-- **기존 API 클라이언트 호환성 보장**
-
-## 현재 API 응답 구조
-
-### Employee API 응답
-
-```json
-{
- "id": 1,
- "tenant_id": 1,
- "user_id": 10,
- "department_id": 5,
- "position_key": "DEVELOPER",
- "employment_type_key": "REGULAR",
- "employee_status": "active",
- "profile_photo_path": null,
- "json_extra": {
- "employee_code": "EMP001",
- "resident_number": "******-*******",
- "gender": "male",
- "address": "서울시 강남구",
- "salary": 50000000,
- "hire_date": "2023-01-15",
- "rank": "대리",
- "bank_account": {
- "bank": "국민",
- "account": "123-456-789",
- "holder": "홍길동"
- }
- },
- "user": {
- "id": 10,
- "name": "홍길동",
- "email": "hong@example.com",
- "phone": "010-1234-5678",
- "is_active": true
- },
- "department": {
- "id": 5,
- "name": "개발팀"
- },
- "created_at": "2023-01-15T09:00:00.000000Z",
- "updated_at": "2024-12-09T10:30:00.000000Z"
-}
-```
-
-### Attendance API 응답
-
-```json
-{
- "id": 1,
- "tenant_id": 1,
- "user_id": 10,
- "base_date": "2024-12-09",
- "status": "onTime",
- "json_details": {
- "check_in": "09:00:00",
- "check_out": "18:00:00",
- "work_minutes": 540,
- "overtime_minutes": 60,
- "late_minutes": 0,
- "gps_data": {
- "check_in": { "lat": 37.5665, "lng": 126.9780 }
- }
- },
- "remarks": null,
- "user": {
- "id": 10,
- "name": "홍길동",
- "email": "hong@example.com"
- },
- "created_at": "2024-12-09T09:00:00.000000Z",
- "updated_at": "2024-12-09T18:00:00.000000Z"
-}
-```
-
-### Department Tree API 응답
-
-```json
-[
- {
- "id": 1,
- "tenant_id": 1,
- "parent_id": null,
- "code": "DEV",
- "name": "개발본부",
- "description": "개발 조직",
- "is_active": true,
- "sort_order": 1,
- "children": [
- {
- "id": 2,
- "tenant_id": 1,
- "parent_id": 1,
- "code": "DEV-FE",
- "name": "프론트엔드팀",
- "is_active": true,
- "sort_order": 1,
- "children": []
- }
- ]
- }
-]
-```
-
----
-
-# Part 2: 프론트엔드 (React) 수정사항
-
-## 변환 전략
-
-React 프론트엔드에서 API 응답을 받아 내부 타입으로 변환합니다.
-
-### 변환 유틸리티 위치
-
-```
-react/src/lib/
-├── api/
-│ └── transformers/
-│ ├── employee.ts # Employee 변환
-│ ├── attendance.ts # Attendance 변환
-│ ├── department.ts # Department 변환
-│ └── index.ts # 공통 유틸리티
-```
-
-## 1. 공통 변환 유틸리티
-
-**파일**: `react/src/lib/api/transformers/index.ts`
-
-```typescript
-/**
- * snake_case → camelCase 변환
- */
-export function toCamelCase(str: string): string {
- return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
-}
-
-/**
- * 객체 키를 camelCase로 변환 (재귀)
- */
-export function transformKeys(obj: unknown): T {
- if (obj === null || obj === undefined) {
- return obj as T;
- }
-
- if (Array.isArray(obj)) {
- return obj.map(item => transformKeys(item)) as T;
- }
-
- if (typeof obj === 'object') {
- const result: Record = {};
- for (const [key, value] of Object.entries(obj)) {
- const camelKey = toCamelCase(key);
- result[camelKey] = transformKeys(value);
- }
- return result as T;
- }
-
- return obj as T;
-}
-
-/**
- * ISO 문자열을 Date로 변환
- */
-export function parseDate(dateStr: string | null): Date | null {
- if (!dateStr) return null;
- return new Date(dateStr);
-}
-```
-
-## 2. Employee 변환
-
-**파일**: `react/src/lib/api/transformers/employee.ts`
-
-```typescript
-import { transformKeys } from './index';
-import type { Employee, EmployeeApiResponse } from '@/types/hr';
-
-/**
- * API 응답 → React Employee 타입 변환
- */
-export function transformEmployee(data: EmployeeApiResponse): Employee {
- const base = transformKeys>(data);
- const jsonExtra = data.json_extra ?? {};
-
- return {
- id: String(data.id),
- name: data.user?.name ?? '',
- email: data.user?.email ?? '',
- phone: data.user?.phone ?? null,
- residentNumber: jsonExtra.resident_number ?? null,
- salary: jsonExtra.salary ?? null,
- profileImage: data.profile_photo_path ?? null,
- employeeCode: jsonExtra.employee_code ?? null,
- gender: jsonExtra.gender ?? null,
- address: transformAddress(jsonExtra.address),
- bankAccount: transformBankAccount(jsonExtra.bank_account),
- hireDate: jsonExtra.hire_date ?? null,
- employmentType: mapEmploymentType(data.employment_type_key),
- rank: jsonExtra.rank ?? null,
- status: data.employee_status ?? 'active',
- departmentPositions: buildDepartmentPositions(data),
- userInfo: buildUserInfo(data),
- createdAt: data.created_at ?? null,
- updatedAt: data.updated_at ?? null,
- };
-}
-
-function transformAddress(address: unknown): Employee['address'] {
- if (!address) return null;
-
- if (typeof address === 'string') {
- return {
- zipCode: '',
- address1: address,
- address2: '',
- };
- }
-
- if (typeof address === 'object') {
- const addr = address as Record;
- return {
- zipCode: addr.zip_code ?? addr.zipCode ?? '',
- address1: addr.address1 ?? addr.address_1 ?? '',
- address2: addr.address2 ?? addr.address_2 ?? '',
- };
- }
-
- return null;
-}
-
-function transformBankAccount(bankAccount: unknown): Employee['bankAccount'] {
- if (!bankAccount || typeof bankAccount !== 'object') return null;
-
- const ba = bankAccount as Record;
- return {
- bankName: ba.bank ?? ba.bankName ?? '',
- accountNumber: ba.account ?? ba.accountNumber ?? '',
- accountHolder: ba.holder ?? ba.accountHolder ?? '',
- };
-}
-
-function mapEmploymentType(key: string | null): string | null {
- if (!key) return null;
-
- const map: Record = {
- REGULAR: 'regular',
- CONTRACT: 'contract',
- PARTTIME: 'parttime',
- INTERN: 'intern',
- };
-
- return map[key] ?? key.toLowerCase();
-}
-
-function buildDepartmentPositions(data: EmployeeApiResponse): Employee['departmentPositions'] {
- if (!data.department_id) return [];
-
- return [{
- id: String(data.id),
- departmentId: String(data.department_id),
- departmentName: data.department?.name ?? '',
- positionId: data.position_key ?? '',
- positionName: data.position_key ?? '',
- }];
-}
-
-function buildUserInfo(data: EmployeeApiResponse): Employee['userInfo'] {
- if (!data.user) return null;
-
- return {
- userId: data.user.user_id ?? data.user.email,
- role: 'user', // TODO: 실제 역할 정보
- accountStatus: data.user.is_active ? 'active' : 'inactive',
- };
-}
-
-/**
- * Employee 목록 변환
- */
-export function transformEmployeeList(data: EmployeeApiResponse[]): Employee[] {
- return data.map(transformEmployee);
-}
-```
-
-## 3. Attendance 변환
-
-**파일**: `react/src/lib/api/transformers/attendance.ts`
-
-```typescript
-import type { Attendance, AttendanceApiResponse } from '@/types/hr';
-
-/**
- * API 응답 → React Attendance 타입 변환
- */
-export function transformAttendance(data: AttendanceApiResponse): Attendance {
- const jsonDetails = data.json_details ?? {};
-
- return {
- id: String(data.id),
- employeeId: String(data.user_id),
- employeeName: data.user?.name ?? '',
- department: '', // TODO: user.tenantProfile.department.name
- position: '', // TODO: user.tenantProfile.position_key
- rank: '', // TODO: user.tenantProfile.json_extra.rank
- baseDate: data.base_date,
- checkIn: jsonDetails.check_in ?? null,
- checkOut: jsonDetails.check_out ?? null,
- breakTime: jsonDetails.break_time ?? null,
- overtimeHours: formatOvertimeHours(jsonDetails.overtime_minutes),
- reason: buildReason(data),
- status: data.status,
- createdAt: data.created_at ?? null,
- updatedAt: data.updated_at ?? null,
- };
-}
-
-function formatOvertimeHours(minutes: number | undefined): string | null {
- if (minutes === undefined || minutes === null) return null;
-
- const hours = Math.floor(minutes / 60);
- const mins = minutes % 60;
-
- return mins > 0 ? `${hours}시간 ${mins}분` : `${hours}시간`;
-}
-
-function buildReason(data: AttendanceApiResponse): Attendance['reason'] {
- if (!data.remarks) return null;
-
- const typeMap: Record = {
- vacation: 'vacationRequest',
- businessTrip: 'businessTripRequest',
- fieldWork: 'fieldWorkRequest',
- overtime: 'overtimeRequest',
- };
-
- return {
- type: typeMap[data.status] ?? 'vacationRequest',
- label: data.remarks,
- documentId: null,
- };
-}
-
-/**
- * Attendance 목록 변환
- */
-export function transformAttendanceList(data: AttendanceApiResponse[]): Attendance[] {
- return data.map(transformAttendance);
-}
-```
-
-## 4. Department Tree 변환
-
-**파일**: `react/src/lib/api/transformers/department.ts`
-
-```typescript
-import type { DepartmentNode, DepartmentApiResponse } from '@/types/hr';
-
-/**
- * API 응답 → React Department 타입 변환 (재귀)
- */
-export function transformDepartmentTree(
- data: DepartmentApiResponse[],
- depth: number = 0
-): DepartmentNode[] {
- return data.map(dept => ({
- id: dept.id,
- name: dept.name,
- parentId: dept.parent_id,
- depth: depth,
- children: dept.children
- ? transformDepartmentTree(dept.children, depth + 1)
- : [],
- }));
-}
-```
-
-## 5. API 호출 래퍼
-
-**파일**: `react/src/lib/api/hr.ts`
-
-```typescript
-import { apiClient } from '@/lib/api/client';
-import {
- transformEmployee,
- transformEmployeeList
-} from './transformers/employee';
-import {
- transformAttendance,
- transformAttendanceList
-} from './transformers/attendance';
-import { transformDepartmentTree } from './transformers/department';
-
-// Employee API
-export async function getEmployees(params?: Record) {
- const response = await apiClient.get('/v1/employees', { params });
- return transformEmployeeList(response.data.data);
-}
-
-export async function getEmployee(id: string) {
- const response = await apiClient.get(`/v1/employees/${id}`);
- return transformEmployee(response.data.data);
-}
-
-// Attendance API
-export async function getAttendances(params?: Record) {
- const response = await apiClient.get('/v1/attendances', { params });
- return transformAttendanceList(response.data.data);
-}
-
-export async function getAttendance(id: string) {
- const response = await apiClient.get(`/v1/attendances/${id}`);
- return transformAttendance(response.data.data);
-}
-
-// Department API
-export async function getDepartmentTree(params?: Record) {
- const response = await apiClient.get('/v1/departments/tree', { params });
- return transformDepartmentTree(response.data.data);
-}
-```
-
----
-
-# Part 3: React 타입 정의
-
-**파일**: `react/src/types/hr.ts`
-
-```typescript
-// ============================================================
-// React 내부 타입 (camelCase)
-// ============================================================
-
-export interface Employee {
- id: string;
- name: string;
- email: string;
- phone: string | null;
- residentNumber: string | null;
- salary: number | null;
- profileImage: string | null;
- employeeCode: string | null;
- gender: 'male' | 'female' | null;
- address: {
- zipCode: string;
- address1: string;
- address2: string;
- } | null;
- bankAccount: {
- bankName: string;
- accountNumber: string;
- accountHolder: string;
- } | null;
- hireDate: string | null;
- employmentType: 'regular' | 'contract' | 'parttime' | 'intern' | null;
- rank: string | null;
- status: 'active' | 'leave' | 'resigned';
- departmentPositions: {
- id: string;
- departmentId: string;
- departmentName: string;
- positionId: string;
- positionName: string;
- }[];
- userInfo: {
- userId: string;
- role: string;
- accountStatus: 'active' | 'inactive';
- } | null;
- createdAt: string | null;
- updatedAt: string | null;
-}
-
-export interface Attendance {
- id: string;
- employeeId: string;
- employeeName: string;
- department: string;
- position: string;
- rank: string;
- baseDate: string;
- checkIn: string | null;
- checkOut: string | null;
- breakTime: string | null;
- overtimeHours: string | null;
- reason: {
- type: 'vacationRequest' | 'businessTripRequest' | 'fieldWorkRequest' | 'overtimeRequest';
- label: string;
- documentId: string | null;
- } | null;
- status: string;
- createdAt: string | null;
- updatedAt: string | null;
-}
-
-export interface DepartmentNode {
- id: number;
- name: string;
- parentId: number | null;
- depth: number;
- children: DepartmentNode[];
-}
-
-// ============================================================
-// API 응답 타입 (snake_case)
-// ============================================================
-
-export interface EmployeeApiResponse {
- id: number;
- tenant_id: number;
- user_id: number;
- department_id: number | null;
- position_key: string | null;
- employment_type_key: string | null;
- employee_status: string;
- profile_photo_path: string | null;
- json_extra: Record | null;
- user: {
- id: number;
- user_id?: string;
- name: string;
- email: string;
- phone: string | null;
- is_active: boolean;
- } | null;
- department: {
- id: number;
- name: string;
- } | null;
- created_at: string | null;
- updated_at: string | null;
-}
-
-export interface AttendanceApiResponse {
- id: number;
- tenant_id: number;
- user_id: number;
- base_date: string;
- status: string;
- json_details: Record | null;
- remarks: string | null;
- user: {
- id: number;
- name: string;
- email: string;
- } | null;
- created_at: string | null;
- updated_at: string | null;
-}
-
-export interface DepartmentApiResponse {
- id: number;
- tenant_id: number;
- parent_id: number | null;
- code: string | null;
- name: string;
- description: string | null;
- is_active: boolean;
- sort_order: number;
- children: DepartmentApiResponse[] | null;
-}
-```
-
----
-
-# Part 4: 작업 체크리스트
-
-## 프론트엔드 작업
-
-- [ ] `react/src/lib/api/transformers/index.ts` - 공통 변환 유틸리티
-- [ ] `react/src/lib/api/transformers/employee.ts` - Employee 변환
-- [ ] `react/src/lib/api/transformers/attendance.ts` - Attendance 변환
-- [ ] `react/src/lib/api/transformers/department.ts` - Department 변환
-- [ ] `react/src/lib/api/hr.ts` - API 호출 래퍼
-- [ ] `react/src/types/hr.ts` - 타입 정의 (내부용 + API 응답용)
-- [ ] 기존 API 호출 코드를 래퍼 함수로 교체
-
-## 백엔드 작업
-
-- [x] ~~Resource 클래스 생성~~ → **취소** (기존 응답 유지)
-- [x] ~~Controller 수정~~ → **취소**
-- [x] ~~Service 수정~~ → **취소**
-
----
-
-# Part 5: 장단점 비교
-
-## 현재 접근법 (React 변환)
-
-**장점:**
-- API 하위 호환성 유지 (기존 클라이언트 영향 없음)
-- Laravel 표준 컨벤션 유지 (snake_case)
-- 백엔드 변경 불필요
-
-**단점:**
-- React에서 변환 로직 필요
-- 타입 이중 정의 (API 타입 + 내부 타입)
-
-## 대안 접근법 (API camelCase 변환)
-
-**장점:**
-- React에서 변환 불필요
-- 프론트엔드 코드 단순화
-
-**단점:**
-- 기존 API 클라이언트 호환성 깨짐
-- Laravel 표준과 불일치
-- Resource 클래스 추가 유지보수
-
----
-
-# Part 6: 참고 사항
-
-## 변환 시점
-
-1. **API 호출 직후**: `transformXxx()` 함수로 즉시 변환
-2. **React Query/SWR 사용 시**: fetcher 함수 내에서 변환
-3. **Zustand/Redux 사용 시**: store에 저장 전 변환
-
-## 성능 고려
-
-- 대량 데이터 변환 시 Web Worker 고려
-- 변환 결과 캐싱 (React Query의 staleTime 활용)
-- 필요한 필드만 변환하는 최적화 가능
-
-## 테스트 전략
-
-```typescript
-// 변환 함수 단위 테스트
-describe('transformEmployee', () => {
- it('should transform snake_case to camelCase', () => {
- const apiResponse = { employee_status: 'active' };
- const result = transformEmployee(apiResponse);
- expect(result.status).toBe('active');
- });
-
- it('should handle null json_extra', () => {
- const apiResponse = { json_extra: null };
- const result = transformEmployee(apiResponse);
- expect(result.address).toBeNull();
- });
-});
-```