Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -531,7 +531,7 @@ function detectConflicts(schedules: ScheduleItem[], resourceId: string): Schedul
|
||||
- [x] 레지스트리 등록
|
||||
- [x] 문서화 (README.md)
|
||||
|
||||
#### v2-timeline-scheduler ✅ 구현 완료 (2026-01-30)
|
||||
#### v2-timeline-scheduler ✅ 구현 완료 (2026-01-30, 업데이트: 2026-03-13)
|
||||
|
||||
- [x] 타입 정의 완료
|
||||
- [x] 기본 구조 생성
|
||||
@@ -539,16 +539,20 @@ function detectConflicts(schedules: ScheduleItem[], resourceId: string): Schedul
|
||||
- [x] TimelineGrid (배경)
|
||||
- [x] ResourceColumn (리소스)
|
||||
- [x] ScheduleBar 기본 렌더링
|
||||
- [x] 드래그 이동 (기본)
|
||||
- [x] 리사이즈 (기본)
|
||||
- [x] 드래그 이동 (실제 로직: deltaX → 날짜 계산 → API 저장 → toast)
|
||||
- [x] 리사이즈 (실제 로직: 시작/종료 핸들 → 기간 변경 → API 저장 → toast)
|
||||
- [x] 줌 레벨 전환
|
||||
- [x] 날짜 네비게이션
|
||||
- [ ] 충돌 감지 (향후)
|
||||
- [ ] 가상 스크롤 (향후)
|
||||
- [x] 충돌 감지 (같은 리소스 겹침 → ring-destructive + AlertTriangle)
|
||||
- [x] 마일스톤 표시 (시작일 = 종료일 → 다이아몬드 마커)
|
||||
- [x] 범례 표시 (TimelineLegend: 상태별 색상 + 마일스톤 + 충돌)
|
||||
- [x] 반응형 공통 CSS 적용 (text-[10px] sm:text-sm 패턴)
|
||||
- [x] staticFilters 지원 (커스텀 테이블 필터링)
|
||||
- [x] 가상 스크롤 (@tanstack/react-virtual, 30개 이상 리소스 시 자동 활성화)
|
||||
- [x] 설정 패널 구현
|
||||
- [x] API 연동
|
||||
- [x] 레지스트리 등록
|
||||
- [ ] 테스트 완료
|
||||
- [x] 테스트 완료 (20개 테스트 전체 통과 - 충돌감지 11건 + 날짜계산 9건)
|
||||
- [x] 문서화 (README.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -892,3 +892,79 @@ if (process.env.NODE_ENV === "development") {
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 생산기간(리드타임) 산출 - 현재 상태 및 개선 방안
|
||||
|
||||
> 작성일: 2026-03-16 | 상태: 검토 대기 (스키마 변경 전 상의 필요)
|
||||
|
||||
### 12.1 현재 구현 상태
|
||||
|
||||
**생산일수 계산 로직** (`productionPlanService.ts`):
|
||||
|
||||
```
|
||||
생산일수 = ceil(계획수량 / 일생산능력)
|
||||
종료일 = 납기일 - 안전리드타임
|
||||
시작일 = 종료일 - 생산일수
|
||||
```
|
||||
|
||||
**현재 기본값 (하드코딩):**
|
||||
|
||||
| 항목 | 현재값 | 위치 |
|
||||
|------|--------|------|
|
||||
| 일생산능력 (daily_capacity) | 800 EA/일 | `productionPlanService.ts` 기본값 |
|
||||
| 시간당 능력 (hourly_capacity) | 100 EA/시간 | `productionPlanService.ts` 기본값 |
|
||||
| 안전리드타임 (safety_lead_time) | 1일 | 옵션 기본값 |
|
||||
| 반제품 리드타임 (lead_time) | 1일 | `production_plan_mng` 기본값 |
|
||||
|
||||
**문제점:**
|
||||
- `item_info`에 생산 파라미터 컬럼이 없음
|
||||
- 모든 품목이 동일한 기본값(800EA/일)으로 계산됨
|
||||
- 업체별/품목별 생산능력 차이를 반영 불가
|
||||
|
||||
### 12.2 개선 방향 (상의 후 결정)
|
||||
|
||||
**1단계 (품목 마스터 기반) - 권장:**
|
||||
|
||||
`item_info` 테이블에 컬럼 추가:
|
||||
- `lead_time_days`: 리드타임 (일)
|
||||
- `daily_capacity`: 일생산능력
|
||||
- `min_lot_size`: 최소 생산 단위 (선택)
|
||||
- `setup_time`: 셋업시간 (선택)
|
||||
|
||||
자동 스케줄 생성 시 품목 마스터 조회 → 값 없으면 기본값 사용 (하위 호환)
|
||||
|
||||
**2단계 (설비별 능력) - 고객 요청 시:**
|
||||
|
||||
별도 테이블 `item_equipment_capacity`:
|
||||
- 품목 + 설비 조합별 생산능력 관리
|
||||
- 동일 품목이라도 설비에 따라 능력 다를 때
|
||||
|
||||
**3단계 (공정 라우팅) - 대기업 대응:**
|
||||
|
||||
공정 순서 + 공정별 소요시간 전체 관리
|
||||
- 현재 시점에서는 불필요
|
||||
|
||||
### 12.3 반제품 계획 생성 현황
|
||||
|
||||
**구현 완료 항목:**
|
||||
- API: `POST /production/generate-semi-schedule/preview` (미리보기)
|
||||
- API: `POST /production/generate-semi-schedule` (실제 생성)
|
||||
- BOM 기반 소요량 자동 계산
|
||||
- 타임라인 컴포넌트 내 "반제품 계획 생성" 버튼 (완제품 탭에서만 표시)
|
||||
- 반제품 탭: linkedFilter 제거, staticFilters만 사용 (전체 반제품 표시)
|
||||
|
||||
**반제품 생산기간 계산:**
|
||||
- 반제품 납기일 = 완제품 시작일
|
||||
- 반제품 시작일 = 완제품 시작일 - lead_time (기본 1일)
|
||||
- BOM 소요량 = 완제품 계획수량 x BOM 수량
|
||||
|
||||
**테스트 BOM 데이터:**
|
||||
|
||||
| 완제품 | 반제품 | BOM 수량 |
|
||||
|--------|--------|----------|
|
||||
| ITEM-001 (탑씰 Type A) | SEMI-001 (탑씰 필름 A) | 2 EA/개 |
|
||||
| ITEM-001 (탑씰 Type A) | SEMI-002 (탑씰 접착제) | 0.5 KG/개 |
|
||||
| ITEM-002 (탑씰 Type B) | SEMI-003 (탑씰 필름 B) | 3 EA/개 |
|
||||
| ITEM-002 (탑씰 Type B) | SEMI-004 (탑씰 코팅제) | 0.3 KG/개 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WACE 화면 시스템 - DB 스키마 & 컴포넌트 설정 전체 레퍼런스
|
||||
|
||||
> **최종 업데이트**: 2026-03-13
|
||||
> **최종 업데이트**: 2026-03-16
|
||||
> **용도**: AI 챗봇이 화면 생성 시 참조하는 DB 스키마, 컴포넌트 전체 설정 사전
|
||||
> **관련 문서**: `v2-component-usage-guide.md` (SQL 템플릿, 실행 예시)
|
||||
|
||||
@@ -532,15 +532,20 @@ CREATE TABLE "{테이블명}" (
|
||||
|
||||
---
|
||||
|
||||
### 3.11 v2-timeline-scheduler (간트차트)
|
||||
### 3.11 v2-timeline-scheduler (간트차트/타임라인)
|
||||
|
||||
**용도**: 시간축 기반 일정/계획 시각화. 드래그/리사이즈로 일정 편집.
|
||||
**용도**: 시간축 기반 일정/계획 시각화. 드래그/리사이즈로 일정 편집. 품목별 그룹 뷰, 자동 스케줄 생성, 반제품 계획 연동 지원.
|
||||
|
||||
**기본 설정**:
|
||||
|
||||
| 설정 | 타입 | 기본값 | 설명 |
|
||||
|------|------|--------|------|
|
||||
| selectedTable | string | - | 스케줄 데이터 테이블 |
|
||||
| customTableName | string | - | selectedTable 대신 사용 (useCustomTable=true 시) |
|
||||
| useCustomTable | boolean | `false` | customTableName 사용 여부 |
|
||||
| resourceTable | string | `"equipment_mng"` | 리소스(설비/작업자) 테이블 |
|
||||
| scheduleType | string | `"PRODUCTION"` | 스케줄 유형: `PRODUCTION`/`MAINTENANCE`/`SHIPPING`/`WORK_ASSIGN` |
|
||||
| viewMode | string | - | 뷰 모드: `"itemGrouped"` (품목별 카드 그룹) / 미설정 시 리소스 기반 |
|
||||
| defaultZoomLevel | string | `"day"` | 초기 줌: `day`/`week`/`month` |
|
||||
| editable | boolean | `true` | 편집 가능 |
|
||||
| draggable | boolean | `true` | 드래그 이동 허용 |
|
||||
@@ -548,15 +553,16 @@ CREATE TABLE "{테이블명}" (
|
||||
| rowHeight | number | `50` | 행 높이(px) |
|
||||
| headerHeight | number | `60` | 헤더 높이(px) |
|
||||
| resourceColumnWidth | number | `150` | 리소스 컬럼 너비(px) |
|
||||
| cellWidth.day | number | `60` | 일 단위 셀 너비 |
|
||||
| cellWidth.week | number | `120` | 주 단위 셀 너비 |
|
||||
| cellWidth.month | number | `40` | 월 단위 셀 너비 |
|
||||
| showConflicts | boolean | `true` | 시간 겹침 충돌 표시 |
|
||||
| showProgress | boolean | `true` | 진행률 바 표시 |
|
||||
| showTodayLine | boolean | `true` | 오늘 날짜 표시선 |
|
||||
| showToolbar | boolean | `true` | 상단 툴바 표시 |
|
||||
| showLegend | boolean | `true` | 범례(상태 색상 안내) 표시 |
|
||||
| showNavigation | boolean | `true` | 날짜 네비게이션 버튼 표시 |
|
||||
| showZoomControls | boolean | `true` | 줌 컨트롤 버튼 표시 |
|
||||
| showAddButton | boolean | `true` | 추가 버튼 |
|
||||
| height | number | `500` | 높이(px) |
|
||||
| maxHeight | number | - | 최대 높이(px) |
|
||||
|
||||
**fieldMapping (필수)**:
|
||||
|
||||
@@ -583,10 +589,74 @@ CREATE TABLE "{테이블명}" (
|
||||
| 상태 | 기본 색상 |
|
||||
|------|----------|
|
||||
| planned | `"#3b82f6"` (파랑) |
|
||||
| in_progress | `"#f59e0b"` (주황) |
|
||||
| completed | `"#10b981"` (초록) |
|
||||
| in_progress | `"#10b981"` (초록) |
|
||||
| completed | `"#6b7280"` (회색) |
|
||||
| delayed | `"#ef4444"` (빨강) |
|
||||
| cancelled | `"#6b7280"` (회색) |
|
||||
| cancelled | `"#9ca3af"` (연회색) |
|
||||
|
||||
**staticFilters (정적 필터)** - DB 조회 시 항상 적용되는 WHERE 조건:
|
||||
|
||||
| 설정 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| product_type | string | `"완제품"` 또는 `"반제품"` 등 고정 필터 |
|
||||
| status | string | 상태값 필터 |
|
||||
| (임의 컬럼) | string | 해당 컬럼으로 필터링 |
|
||||
|
||||
```json
|
||||
"staticFilters": {
|
||||
"product_type": "완제품"
|
||||
}
|
||||
```
|
||||
|
||||
**linkedFilter (연결 필터)** - 다른 컴포넌트(주로 테이블)의 선택 이벤트와 연동:
|
||||
|
||||
| 설정 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| sourceField | string | 소스 컴포넌트(좌측 테이블)의 필터 기준 컬럼 |
|
||||
| targetField | string | 타임라인 스케줄 데이터에서 매칭할 컬럼 |
|
||||
| sourceTableName | string | 이벤트 발신 테이블명 (이벤트 필터용) |
|
||||
| sourceComponentId | string | 이벤트 발신 컴포넌트 ID (선택) |
|
||||
| emptyMessage | string | 선택 전 빈 상태 메시지 |
|
||||
| showEmptyWhenNoSelection | boolean | 선택 전 빈 상태 표시 여부 |
|
||||
|
||||
```json
|
||||
"linkedFilter": {
|
||||
"sourceField": "part_code",
|
||||
"targetField": "item_code",
|
||||
"sourceTableName": "sales_order_mng",
|
||||
"emptyMessage": "좌측 수주 목록에서 품목을 선택하세요",
|
||||
"showEmptyWhenNoSelection": true
|
||||
}
|
||||
```
|
||||
|
||||
> **linkedFilter 동작 원리**: v2EventBus의 `TABLE_SELECTION_CHANGE` 이벤트를 구독.
|
||||
> 좌측 테이블에서 행을 선택하면 해당 행의 `sourceField` 값을 수집하여,
|
||||
> 타임라인 데이터 중 `targetField`가 일치하는 스케줄만 클라이언트 측에서 필터링 표시.
|
||||
> `staticFilters`는 서버 측 조회, `linkedFilter`는 클라이언트 측 필터링.
|
||||
|
||||
**viewMode: "itemGrouped" (품목별 그룹 뷰)**:
|
||||
|
||||
리소스(설비) 기반 간트차트 대신, 품목(item_code)별로 카드를 그룹화하여 표시하는 모드.
|
||||
각 카드 안에 해당 품목의 스케줄 바가 미니 타임라인으로 표시됨.
|
||||
|
||||
설정 시 `viewMode: "itemGrouped"`만 추가하면 됨. 툴바에 자동으로:
|
||||
- 날짜 네비게이션 (이전/오늘/다음)
|
||||
- 줌 컨트롤
|
||||
- 새로고침 버튼
|
||||
- (완제품 탭일 때) **완제품 계획 생성** / **반제품 계획 생성** 버튼
|
||||
|
||||
**자동 스케줄 생성 (내장 기능)**:
|
||||
|
||||
`viewMode: "itemGrouped"` + `staticFilters.product_type === "완제품"` 일 때 자동 활성화.
|
||||
|
||||
- **완제품 계획 생성**: linkedFilter로 선택된 수주 품목 기반, 미리보기 다이얼로그 → 확인 후 생성
|
||||
- API: `POST /production/generate-schedule/preview` → `POST /production/generate-schedule`
|
||||
- **반제품 계획 생성**: 현재 타임라인의 완제품 스케줄 기반, BOM 소요량으로 반제품 계획 미리보기 → 확인 후 생성
|
||||
- API: `POST /production/generate-semi-schedule/preview` → `POST /production/generate-semi-schedule`
|
||||
|
||||
> **중요**: 반제품 전용 타임라인에는 `linkedFilter`를 걸지 않는다.
|
||||
> 반제품 item_code가 수주 품목 코드와 다르므로 매칭 불가.
|
||||
> `staticFilters: { product_type: "반제품" }`만 설정하여 전체 반제품 계획을 표시.
|
||||
|
||||
---
|
||||
|
||||
@@ -923,16 +993,32 @@ CREATE TABLE "{테이블명}" (
|
||||
## 4. 패턴 의사결정 트리
|
||||
|
||||
```
|
||||
Q1. 시간축 기반 일정/간트차트? → v2-timeline-scheduler
|
||||
Q2. 다차원 피벗 분석? → v2-pivot-grid
|
||||
Q3. 그룹별 접기/펼치기? → v2-table-grouped
|
||||
Q4. 카드 형태 표시? → v2-card-display
|
||||
Q5. 마스터-디테일?
|
||||
Q1. 좌측 마스터 + 우측 탭(타임라인/테이블) 복합 구성?
|
||||
→ 패턴 F → v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler
|
||||
Q2. 시간축 기반 일정/간트차트?
|
||||
├ 품목별 카드 그룹 뷰? → 패턴 E-2 → v2-timeline-scheduler(viewMode:itemGrouped)
|
||||
└ 리소스(설비) 기반? → 패턴 E → v2-timeline-scheduler
|
||||
Q3. 다차원 피벗 분석? → v2-pivot-grid
|
||||
Q4. 그룹별 접기/펼치기? → v2-table-grouped
|
||||
Q5. 카드 형태 표시? → v2-card-display
|
||||
Q6. 마스터-디테일?
|
||||
├ 우측 멀티 탭? → v2-split-panel-layout + additionalTabs
|
||||
└ 단일 디테일? → v2-split-panel-layout
|
||||
Q6. 단일 테이블? → v2-table-search-widget + v2-table-list
|
||||
Q7. 단일 테이블? → v2-table-search-widget + v2-table-list
|
||||
```
|
||||
|
||||
### 패턴 요약표
|
||||
|
||||
| 패턴 | 대표 화면 | 핵심 컴포넌트 |
|
||||
|------|----------|-------------|
|
||||
| A | 거래처관리 | v2-table-search-widget + v2-table-list |
|
||||
| B | 수주관리 | v2-split-panel-layout |
|
||||
| C | 수주관리(멀티탭) | v2-split-panel-layout + additionalTabs |
|
||||
| D | 재고현황 | v2-table-grouped |
|
||||
| E | 설비 작업일정 | v2-timeline-scheduler (리소스 기반) |
|
||||
| E-2 | 품목별 타임라인 | v2-timeline-scheduler (viewMode: itemGrouped) |
|
||||
| F | 생산계획 | split(custom) + tabs + timeline |
|
||||
|
||||
---
|
||||
|
||||
## 5. 관계(relation) 레퍼런스
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WACE 화면 구현 실행 가이드 (챗봇/AI 에이전트 전용)
|
||||
|
||||
> **최종 업데이트**: 2026-03-13
|
||||
> **최종 업데이트**: 2026-03-16
|
||||
> **용도**: 사용자가 "수주관리 화면 만들어줘"라고 요청하면, 이 문서를 참조하여 SQL을 직접 생성하고 화면을 구현하는 AI 챗봇용 실행 가이드
|
||||
> **핵심**: 이 문서의 SQL 템플릿을 따라 INSERT하면 화면이 자동으로 생성된다
|
||||
|
||||
@@ -533,7 +533,9 @@ DO UPDATE SET layout_data = EXCLUDED.layout_data, updated_at = now();
|
||||
}
|
||||
```
|
||||
|
||||
### 8.5 패턴 E: 타임라인/간트차트
|
||||
### 8.5 패턴 E: 타임라인/간트차트 (리소스 기반)
|
||||
|
||||
**사용 조건**: 설비/작업자 등 리소스 기준으로 스케줄을 시간축에 표시
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -575,6 +577,246 @@ DO UPDATE SET layout_data = EXCLUDED.layout_data, updated_at = now();
|
||||
}
|
||||
```
|
||||
|
||||
### 8.6 패턴 E-2: 타임라인 (품목 그룹 뷰 + 연결 필터)
|
||||
|
||||
**사용 조건**: 좌측 테이블에서 선택한 품목 기반으로 타임라인을 필터링 표시. 품목별 카드 그룹 뷰.
|
||||
|
||||
> 리소스(설비) 기반이 아닌, **품목(item_code)별로 카드 그룹** 형태로 스케줄을 표시한다.
|
||||
> 좌측 테이블에서 행을 선택하면 `linkedFilter`로 해당 품목의 스케줄만 필터링.
|
||||
> `staticFilters`로 완제품/반제품 등 데이터 유형을 고정 필터링.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "timeline_finished",
|
||||
"url": "@/lib/registry/components/v2-timeline-scheduler",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 1920, "height": 800 },
|
||||
"displayOrder": 0,
|
||||
"overrides": {
|
||||
"label": "완제품 생산계획",
|
||||
"selectedTable": "{스케줄_테이블}",
|
||||
"viewMode": "itemGrouped",
|
||||
"fieldMapping": {
|
||||
"id": "id",
|
||||
"resourceId": "item_code",
|
||||
"title": "item_name",
|
||||
"startDate": "start_date",
|
||||
"endDate": "end_date",
|
||||
"status": "status"
|
||||
},
|
||||
"defaultZoomLevel": "day",
|
||||
"staticFilters": {
|
||||
"product_type": "완제품"
|
||||
},
|
||||
"linkedFilter": {
|
||||
"sourceField": "part_code",
|
||||
"targetField": "item_code",
|
||||
"sourceTableName": "{좌측_테이블명}",
|
||||
"emptyMessage": "좌측 목록에서 품목을 선택하세요",
|
||||
"showEmptyWhenNoSelection": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**핵심 설정 설명**:
|
||||
|
||||
| 설정 | 용도 |
|
||||
|------|------|
|
||||
| `viewMode: "itemGrouped"` | 리소스 행이 아닌, 품목별 카드 그룹으로 표시 |
|
||||
| `staticFilters` | DB 조회 시 항상 적용 (서버측 WHERE 조건) |
|
||||
| `linkedFilter` | 다른 컴포넌트 선택 이벤트로 클라이언트 측 필터링 |
|
||||
| `linkedFilter.sourceField` | 소스 테이블에서 가져올 값의 컬럼명 |
|
||||
| `linkedFilter.targetField` | 타임라인 데이터에서 매칭할 컬럼명 |
|
||||
|
||||
> **주의**: `linkedFilter`와 `staticFilters`의 차이
|
||||
> - `staticFilters`: DB SELECT 쿼리의 WHERE 절에 포함 → 서버에서 필터링
|
||||
> - `linkedFilter`: 전체 데이터를 불러온 후, 선택 이벤트에 따라 클라이언트에서 필터링
|
||||
|
||||
### 8.7 패턴 F: 복합 화면 (좌측 테이블 + 우측 탭 내 타임라인)
|
||||
|
||||
**사용 조건**: 생산계획처럼 좌측 마스터 테이블 + 우측에 탭으로 여러 타임라인/테이블을 표시하는 복합 화면.
|
||||
`v2-split-panel-layout`의 `rightPanel.displayMode: "custom"` + `v2-tabs-widget` + `v2-timeline-scheduler` 조합.
|
||||
|
||||
**구조 개요**:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ v2-split-panel-layout │
|
||||
│ ┌──────────┬─────────────────────────────────┐ │
|
||||
│ │ leftPanel │ rightPanel (displayMode:custom)│ │
|
||||
│ │ │ ┌─────────────────────────────┐│ │
|
||||
│ │ v2-table- │ │ v2-tabs-widget ││ │
|
||||
│ │ grouped │ │ ┌───────┬───────┬─────────┐ ││ │
|
||||
│ │ (수주목록) │ │ │완제품 │반제품 │기타 탭 │ ││ │
|
||||
│ │ │ │ └───────┴───────┴─────────┘ ││ │
|
||||
│ │ │ │ ┌─────────────────────────┐ ││ │
|
||||
│ │ │ │ │ v2-timeline-scheduler │ ││ │
|
||||
│ │ │ │ │ (품목별 그룹 뷰) │ ││ │
|
||||
│ │ │ │ └─────────────────────────┘ ││ │
|
||||
│ │ │ └─────────────────────────────┘│ │
|
||||
│ └──────────┴─────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**실제 layout_data 예시** (생산계획 화면 참고):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"components": [
|
||||
{
|
||||
"id": "split_pp",
|
||||
"url": "@/lib/registry/components/v2-split-panel-layout",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 1920, "height": 850 },
|
||||
"displayOrder": 0,
|
||||
"overrides": {
|
||||
"label": "생산계획",
|
||||
"splitRatio": 25,
|
||||
"resizable": true,
|
||||
"autoLoad": true,
|
||||
"syncSelection": true,
|
||||
"leftPanel": {
|
||||
"title": "수주 목록",
|
||||
"displayMode": "custom",
|
||||
"components": [
|
||||
{
|
||||
"id": "grouped_orders",
|
||||
"componentType": "v2-table-grouped",
|
||||
"label": "수주별 품목",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 600, "height": 800 },
|
||||
"componentConfig": {
|
||||
"selectedTable": "sales_order_mng",
|
||||
"groupConfig": {
|
||||
"groupByColumn": "order_number",
|
||||
"groupLabelFormat": "{value}",
|
||||
"defaultExpanded": true,
|
||||
"summary": { "showCount": true }
|
||||
},
|
||||
"columns": [
|
||||
{ "columnName": "part_code", "displayName": "품번", "visible": true, "width": 100 },
|
||||
{ "columnName": "part_name", "displayName": "품명", "visible": true, "width": 120 },
|
||||
{ "columnName": "order_qty", "displayName": "수량", "visible": true, "width": 60 },
|
||||
{ "columnName": "delivery_date", "displayName": "납기일", "visible": true, "width": 90 }
|
||||
],
|
||||
"showCheckbox": true,
|
||||
"checkboxMode": "multi"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"rightPanel": {
|
||||
"title": "생산 계획",
|
||||
"displayMode": "custom",
|
||||
"components": [
|
||||
{
|
||||
"id": "tabs_pp",
|
||||
"componentType": "v2-tabs-widget",
|
||||
"label": "생산계획 탭",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 1400, "height": 800 },
|
||||
"componentConfig": {
|
||||
"tabs": [
|
||||
{
|
||||
"id": "tab_finished",
|
||||
"label": "완제품",
|
||||
"order": 1,
|
||||
"components": [
|
||||
{
|
||||
"id": "timeline_finished",
|
||||
"componentType": "v2-timeline-scheduler",
|
||||
"label": "완제품 타임라인",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 1380, "height": 750 },
|
||||
"componentConfig": {
|
||||
"selectedTable": "production_plan_mng",
|
||||
"viewMode": "itemGrouped",
|
||||
"fieldMapping": {
|
||||
"id": "id",
|
||||
"resourceId": "item_code",
|
||||
"title": "item_name",
|
||||
"startDate": "start_date",
|
||||
"endDate": "end_date",
|
||||
"status": "status"
|
||||
},
|
||||
"defaultZoomLevel": "day",
|
||||
"staticFilters": {
|
||||
"product_type": "완제품"
|
||||
},
|
||||
"linkedFilter": {
|
||||
"sourceField": "part_code",
|
||||
"targetField": "item_code",
|
||||
"sourceTableName": "sales_order_mng",
|
||||
"emptyMessage": "좌측 수주 목록에서 품목을 선택하세요",
|
||||
"showEmptyWhenNoSelection": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tab_semi",
|
||||
"label": "반제품",
|
||||
"order": 2,
|
||||
"components": [
|
||||
{
|
||||
"id": "timeline_semi",
|
||||
"componentType": "v2-timeline-scheduler",
|
||||
"label": "반제품 타임라인",
|
||||
"position": { "x": 0, "y": 0 },
|
||||
"size": { "width": 1380, "height": 750 },
|
||||
"componentConfig": {
|
||||
"selectedTable": "production_plan_mng",
|
||||
"viewMode": "itemGrouped",
|
||||
"fieldMapping": {
|
||||
"id": "id",
|
||||
"resourceId": "item_code",
|
||||
"title": "item_name",
|
||||
"startDate": "start_date",
|
||||
"endDate": "end_date",
|
||||
"status": "status"
|
||||
},
|
||||
"defaultZoomLevel": "day",
|
||||
"staticFilters": {
|
||||
"product_type": "반제품"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"defaultTab": "tab_finished"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"gridSettings": { "columns": 12, "gap": 16, "padding": 16 },
|
||||
"screenResolution": { "width": 1920, "height": 1080 }
|
||||
}
|
||||
```
|
||||
|
||||
**패턴 F 핵심 포인트**:
|
||||
|
||||
| 포인트 | 설명 |
|
||||
|--------|------|
|
||||
| `leftPanel.displayMode: "custom"` | 좌측에 v2-table-grouped 등 자유 배치 |
|
||||
| `rightPanel.displayMode: "custom"` | 우측에 v2-tabs-widget 등 자유 배치 |
|
||||
| `componentConfig` | custom 내부 컴포넌트는 overrides 대신 componentConfig 사용 |
|
||||
| `componentType` | custom 내부에서는 url 대신 componentType 사용 |
|
||||
| 완제품 탭에만 `linkedFilter` | 좌측 테이블과 연동 필터링 |
|
||||
| 반제품 탭에는 `linkedFilter` 없음 | 반제품 item_code가 수주 품목과 다르므로 전체 표시 |
|
||||
| 자동 스케줄 생성 버튼 | `staticFilters.product_type === "완제품"` 일 때 자동 표시 |
|
||||
|
||||
> **displayMode: "custom" 내부 컴포넌트 규칙**:
|
||||
> - `url` 대신 `componentType` 사용 (예: `"v2-timeline-scheduler"`, `"v2-table-grouped"`)
|
||||
> - `overrides` 대신 `componentConfig` 사용
|
||||
> - `position`, `size`는 동일하게 사용
|
||||
|
||||
---
|
||||
|
||||
## 9. Step 7: menu_info INSERT
|
||||
@@ -696,29 +938,47 @@ VALUES
|
||||
사용자가 화면을 요청하면 이 트리로 패턴을 결정한다.
|
||||
|
||||
```
|
||||
Q1. 시간축 기반 일정/간트차트가 필요한가?
|
||||
├─ YES → 패턴 E (타임라인) → v2-timeline-scheduler
|
||||
Q1. 좌측 마스터 + 우측에 탭으로 타임라인/테이블 등 복합 구성이 필요한가?
|
||||
├─ YES → 패턴 F (복합 화면) → v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler
|
||||
└─ NO ↓
|
||||
|
||||
Q2. 다차원 집계/피벗 분석이 필요한가?
|
||||
Q2. 시간축 기반 일정/간트차트가 필요한가?
|
||||
├─ YES → Q2-1. 품목별 카드 그룹 뷰인가?
|
||||
│ ├─ YES → 패턴 E-2 (품목 그룹 타임라인) → v2-timeline-scheduler(viewMode:itemGrouped)
|
||||
│ └─ NO → 패턴 E (리소스 기반 타임라인) → v2-timeline-scheduler
|
||||
└─ NO ↓
|
||||
|
||||
Q3. 다차원 집계/피벗 분석이 필요한가?
|
||||
├─ YES → 피벗 → v2-pivot-grid
|
||||
└─ NO ↓
|
||||
|
||||
Q3. 데이터를 그룹별로 접기/펼치기가 필요한가?
|
||||
Q4. 데이터를 그룹별로 접기/펼치기가 필요한가?
|
||||
├─ YES → 패턴 D (그룹화) → v2-table-grouped
|
||||
└─ NO ↓
|
||||
|
||||
Q4. 이미지+정보를 카드 형태로 표시하는가?
|
||||
Q5. 이미지+정보를 카드 형태로 표시하는가?
|
||||
├─ YES → 카드뷰 → v2-card-display
|
||||
└─ NO ↓
|
||||
|
||||
Q5. 마스터 테이블 선택 시 연관 디테일이 필요한가?
|
||||
├─ YES → Q5-1. 디테일에 여러 탭이 필요한가?
|
||||
Q6. 마스터 테이블 선택 시 연관 디테일이 필요한가?
|
||||
├─ YES → Q6-1. 디테일에 여러 탭이 필요한가?
|
||||
│ ├─ YES → 패턴 C (마스터-디테일+탭) → v2-split-panel-layout + additionalTabs
|
||||
│ └─ NO → 패턴 B (마스터-디테일) → v2-split-panel-layout
|
||||
└─ NO → 패턴 A (기본 마스터) → v2-table-search-widget + v2-table-list
|
||||
```
|
||||
|
||||
### 패턴 선택 빠른 참조
|
||||
|
||||
| 패턴 | 대표 화면 | 핵심 컴포넌트 |
|
||||
|------|----------|-------------|
|
||||
| A | 거래처관리, 코드관리 | v2-table-search-widget + v2-table-list |
|
||||
| B | 수주관리, 발주관리 | v2-split-panel-layout |
|
||||
| C | 수주관리(멀티탭) | v2-split-panel-layout + additionalTabs |
|
||||
| D | 재고현황, 그룹별조회 | v2-table-grouped |
|
||||
| E | 설비 작업일정 | v2-timeline-scheduler (리소스 기반) |
|
||||
| E-2 | 단독 품목별 타임라인 | v2-timeline-scheduler (viewMode: itemGrouped) |
|
||||
| F | 생산계획, 작업지시 | v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler |
|
||||
|
||||
---
|
||||
|
||||
## 13. 화면 간 연결 관계 정의
|
||||
@@ -1119,7 +1379,8 @@ VALUES (
|
||||
| 검색 바 | v2-table-search-widget | `autoSelectFirstTable` |
|
||||
| 좌우 분할 | v2-split-panel-layout | `leftPanel`, `rightPanel`, `relation`, `splitRatio` |
|
||||
| 그룹화 테이블 | v2-table-grouped | `groupConfig.groupByColumn`, `summary` |
|
||||
| 간트차트 | v2-timeline-scheduler | `fieldMapping`, `resourceTable` |
|
||||
| 간트차트 (리소스 기반) | v2-timeline-scheduler | `fieldMapping`, `resourceTable` |
|
||||
| 타임라인 (품목 그룹) | v2-timeline-scheduler | `viewMode:"itemGrouped"`, `staticFilters`, `linkedFilter` |
|
||||
| 피벗 분석 | v2-pivot-grid | `fields(area, summaryType)` |
|
||||
| 카드 뷰 | v2-card-display | `columnMapping`, `cardsPerRow` |
|
||||
| 액션 버튼 | v2-button-primary | `text`, `actionType`, `webTypeConfig.dataflowConfig` |
|
||||
@@ -1144,3 +1405,97 @@ VALUES (
|
||||
| 창고 랙 | v2-rack-structure | `codePattern`, `namePattern`, `maxRows` |
|
||||
| 공정 작업기준 | v2-process-work-standard | `dataSource.itemTable`, `dataSource.routingDetailTable` |
|
||||
| 품목 라우팅 | v2-item-routing | `dataSource.itemTable`, `dataSource.routingDetailTable` |
|
||||
|
||||
---
|
||||
|
||||
## 17. v2-timeline-scheduler 고급 설정 가이드
|
||||
|
||||
### 17.1 viewMode 선택 기준
|
||||
|
||||
| viewMode | 용도 | Y축 |
|
||||
|----------|------|-----|
|
||||
| (미설정) | 설비별 작업일정, 보전계획 | 설비/작업자 행 |
|
||||
| `"itemGrouped"` | 생산계획, 출하계획 | 품목별 카드 그룹 |
|
||||
|
||||
### 17.2 staticFilters vs linkedFilter 비교
|
||||
|
||||
| 구분 | staticFilters | linkedFilter |
|
||||
|------|--------------|-------------|
|
||||
| **적용 시점** | DB SELECT 쿼리 시 | 클라이언트 렌더링 시 |
|
||||
| **위치** | 서버 측 (WHERE 절) | 프론트 측 (JS 필터링) |
|
||||
| **변경 가능** | 고정 (layout에 하드코딩) | 동적 (이벤트 기반) |
|
||||
| **용도** | 완제품/반제품 구분 등 | 좌측 테이블 선택 연동 |
|
||||
|
||||
**조합 예시**:
|
||||
```
|
||||
staticFilters: { product_type: "완제품" } → DB에서 완제품만 조회
|
||||
linkedFilter: { sourceField: "part_code", targetField: "item_code" }
|
||||
→ 완제품 중 좌측에서 선택한 품목만 표시
|
||||
```
|
||||
|
||||
### 17.3 자동 스케줄 생성 (내장 기능)
|
||||
|
||||
`viewMode: "itemGrouped"` + `staticFilters.product_type === "완제품"` 조건 충족 시,
|
||||
타임라인 툴바에 **완제품 계획 생성** / **반제품 계획 생성** 버튼이 자동 표시됨.
|
||||
|
||||
**완제품 계획 생성 플로우**:
|
||||
```
|
||||
1. linkedFilter로 선택된 수주 품목 수집
|
||||
2. POST /production/generate-schedule/preview → 미리보기 다이얼로그
|
||||
3. 사용자 확인 → POST /production/generate-schedule → 실제 생성
|
||||
4. 타임라인 자동 새로고침
|
||||
```
|
||||
|
||||
**반제품 계획 생성 플로우**:
|
||||
```
|
||||
1. 현재 타임라인의 완제품 스케줄 ID 수집
|
||||
2. POST /production/generate-semi-schedule/preview → BOM 기반 소요량 계산
|
||||
3. 미리보기 다이얼로그 (기존 반제품 계획 삭제/유지 정보 포함)
|
||||
4. 사용자 확인 → POST /production/generate-semi-schedule → 실제 생성
|
||||
5. 반제품 탭으로 전환 시 새 데이터 표시
|
||||
```
|
||||
|
||||
### 17.4 반제품 탭 주의사항
|
||||
|
||||
반제품 전용 타임라인에는 `linkedFilter`를 **걸지 않는다**.
|
||||
|
||||
이유: 반제품의 `item_code`(예: `SEMI-001`)와 수주 품목의 `part_code`(예: `ITEM-001`)가
|
||||
서로 다른 값이므로 매칭이 불가능하다. `staticFilters: { product_type: "반제품" }`만 설정.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "timeline_semi",
|
||||
"componentType": "v2-timeline-scheduler",
|
||||
"componentConfig": {
|
||||
"selectedTable": "production_plan_mng",
|
||||
"viewMode": "itemGrouped",
|
||||
"staticFilters": { "product_type": "반제품" },
|
||||
"fieldMapping": { "..." : "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 17.5 이벤트 연동 (v2EventBus)
|
||||
|
||||
타임라인 컴포넌트는 `v2EventBus`를 통해 다른 컴포넌트와 통신한다.
|
||||
|
||||
| 이벤트 | 방향 | 설명 |
|
||||
|--------|------|------|
|
||||
| `TABLE_SELECTION_CHANGE` | 수신 | 좌측 테이블 행 선택 시 linkedFilter 적용 |
|
||||
| `TIMELINE_REFRESH` | 발신/수신 | 타임라인 데이터 새로고침 |
|
||||
|
||||
**연결 필터 이벤트 페이로드**:
|
||||
```typescript
|
||||
{
|
||||
eventType: "TABLE_SELECTION_CHANGE",
|
||||
source: "grouped_orders",
|
||||
tableName: "sales_order_mng",
|
||||
selectedRows: [
|
||||
{ id: "...", part_code: "ITEM-001", ... },
|
||||
{ id: "...", part_code: "ITEM-002", ... }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
타임라인은 `selectedRows`에서 `linkedFilter.sourceField` 값을 추출하여,
|
||||
자신의 데이터 중 `linkedFilter.targetField`가 일치하는 항목만 표시.
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
# 생산계획 화면 (TOPSEAL_PP_MAIN) 테스트 시나리오
|
||||
|
||||
> **화면 URL**: `http://localhost:9771/screens/3985`
|
||||
> **로그인 정보**: `topseal_admin` / `qlalfqjsgh11`
|
||||
> **작성일**: 2026-03-16
|
||||
|
||||
---
|
||||
|
||||
## 사전 조건
|
||||
|
||||
- 백엔드 서버 (포트 8080) 실행 중
|
||||
- 프론트엔드 서버 (포트 9771) 실행 중
|
||||
- `topseal_admin` 계정으로 로그인 완료
|
||||
- 사이드바 > 생산관리 > 생산계획 메뉴 클릭하여 화면 진입
|
||||
|
||||
### 현재 테스트 데이터 현황
|
||||
|
||||
| 구분 | 건수 | 상세 |
|
||||
|------|:----:|------|
|
||||
| 완제품 생산계획 | 7건 | planned(3), in_progress(3), completed(1) |
|
||||
| 반제품 생산계획 | 6건 | planned(2), in_progress(2), completed(1) |
|
||||
| 설비(리소스) | 10개 | CNC밀링#1~#2, 머시닝센터#1, 레이저절단기, 프레스기#1, 용접기#1, 도장설비#1, 조립라인#1, 검사대#1~#2 |
|
||||
| 수주 데이터 | 10건 | sales_order_mng |
|
||||
|
||||
---
|
||||
|
||||
## TC-01. 화면 레이아웃 확인
|
||||
|
||||
### 목적
|
||||
화면이 설계대로 좌/우 분할 패널로 렌더링되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 생산계획 화면 진입
|
||||
2. 좌측 패널에 "수주 데이터" 탭이 보이는지 확인
|
||||
3. 우측 패널에 "완제품" / "반제품" 탭이 보이는지 확인
|
||||
4. 분할 패널 비율이 약 45:55인지 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 좌측: "수주데이터" 탭 + "안전재고 부족분" 탭
|
||||
- [ ] 우측: "완제품" 탭 + "반제품" 탭
|
||||
- [ ] 하단에 버튼들 (새로고침, 자동 스케줄, 병합, 반제품계획, 저장) 표시
|
||||
- [ ] 좌측 하단에 "선택 품목 불러오기" 버튼 표시
|
||||
|
||||
---
|
||||
|
||||
## TC-02. 좌측 패널 - 수주데이터 그룹 테이블
|
||||
|
||||
### 목적
|
||||
v2-table-grouped 컴포넌트의 그룹화 및 접기/펼치기 기능 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. "수주데이터" 탭 선택
|
||||
2. 데이터가 품목코드(part_code) 기준으로 그룹화되었는지 확인
|
||||
3. 그룹 헤더 행에 품명, 품목코드가 표시되는지 확인
|
||||
4. 그룹 헤더 클릭하여 접기/펼치기 토글
|
||||
5. "전체 펼치기" / "전체 접기" 버튼 동작 확인
|
||||
6. 그룹별 합계(수주량, 출고량, 잔량) 표시 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 데이터가 part_code 기준으로 그룹화되어 표시
|
||||
- [ ] 그룹 헤더에 `{품명} ({품목코드})` 형식으로 표시
|
||||
- [ ] 그룹 헤더 클릭 시 하위 행 접기/펼치기 동작
|
||||
- [ ] 전체 펼치기/접기 버튼 정상 동작
|
||||
- [ ] 그룹별 수주량/출고량/잔량 합계 표시
|
||||
|
||||
---
|
||||
|
||||
## TC-03. 좌측 패널 - 체크박스 선택
|
||||
|
||||
### 목적
|
||||
그룹 테이블에서 체크박스 선택이 정상 동작하는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 개별 행 체크박스 선택/해제
|
||||
2. 그룹 헤더 체크박스로 그룹 전체 선택/해제
|
||||
3. 다른 그룹의 행도 동시 선택 가능한지 확인
|
||||
4. 선택된 행이 하이라이트되는지 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 개별 행 체크박스 선택/해제 정상
|
||||
- [ ] 그룹 체크박스로 하위 전체 선택/해제
|
||||
- [ ] 여러 그룹에서 동시 선택 가능
|
||||
- [ ] 선택된 행 시각적 구분 (하이라이트)
|
||||
|
||||
---
|
||||
|
||||
## TC-04. 우측 패널 - 완제품 타임라인 기본 표시
|
||||
|
||||
### 목적
|
||||
v2-timeline-scheduler의 기본 렌더링 및 데이터 표시 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. "완제품" 탭 선택 (기본 선택)
|
||||
2. 타임라인 헤더에 날짜가 표시되는지 확인
|
||||
3. 리소스(설비) 목록이 좌측에 표시되는지 확인
|
||||
4. 스케줄 바가 해당 설비/날짜에 표시되는지 확인
|
||||
5. 스케줄 바에 품명이 표시되는지 확인
|
||||
6. 오늘 날짜 라인(빨간 세로선)이 표시되는지 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 타임라인 헤더에 날짜 표시 (월 그룹 + 일별)
|
||||
- [ ] 좌측 리소스 열에 설비명 표시 (프레스기#1, CNC밀링머신#1 등)
|
||||
- [ ] 7건의 완제품 스케줄 바가 올바른 위치에 표시
|
||||
- [ ] 스케줄 바에 item_name 표시
|
||||
- [ ] 오늘 날짜 (2026-03-16) 위치에 빨간 세로선 표시
|
||||
- [ ] "반제품" 데이터는 보이지 않음 (staticFilters 적용 확인)
|
||||
|
||||
---
|
||||
|
||||
## TC-05. 타임라인 - 상태별 색상 표시
|
||||
|
||||
### 목적
|
||||
스케줄 상태에 따른 색상 구분 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 완제품 탭에서 스케줄 바 색상 확인
|
||||
2. 각 상태별 색상이 다른지 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] `planned` (계획): 파란색 (#3b82f6)
|
||||
- [ ] `in_progress` (진행): 초록색 (#10b981)
|
||||
- [ ] `completed` (완료): 회색 (#6b7280)
|
||||
- [ ] `delayed` (지연): 빨간색 (#ef4444) - 해당 데이터 있으면
|
||||
- [ ] 상태별 색상이 명확히 구분됨
|
||||
|
||||
---
|
||||
|
||||
## TC-06. 타임라인 - 진행률 표시
|
||||
|
||||
### 목적
|
||||
스케줄 바 내부에 진행률이 시각적으로 표시되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 진행률이 있는 스케줄 바 확인
|
||||
2. 바 내부에 진행률 비율만큼 채워진 영역 확인
|
||||
3. 진행률 퍼센트 텍스트 표시 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] `탑씰 Type A` (id:103): 40% 진행률 표시
|
||||
- [ ] `탑씰 Type B` (id:2): 25% 진행률 표시
|
||||
- [ ] `탑씰 Type C` (id:105): 25% 진행률 표시
|
||||
- [ ] `탑씰 Type A` (id:4): 100% 진행률 표시 (완료)
|
||||
- [ ] 바 내부에 진행 영역이 색이 다르게 채워짐
|
||||
|
||||
---
|
||||
|
||||
## TC-07. 타임라인 - 줌 레벨 전환
|
||||
|
||||
### 목적
|
||||
일/주/월 줌 레벨 전환이 정상 동작하는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 툴바에서 "주" (기본) 줌 레벨 확인
|
||||
2. "일" 줌 레벨로 전환 -> 날짜 간격 변화 확인
|
||||
3. "월" 줌 레벨로 전환 -> 날짜 간격 변화 확인
|
||||
4. 다시 "주" 줌 레벨로 복귀
|
||||
|
||||
### 예상 결과
|
||||
- [ ] "일" 모드: 날짜 셀이 넓어지고, 하루 단위로 상세 표시
|
||||
- [ ] "주" 모드: 기본 크기, 주 단위 표시
|
||||
- [ ] "월" 모드: 날짜 셀이 좁아지고, 월 단위로 축소 표시
|
||||
- [ ] 줌 레벨 전환 시 스케줄 바 위치/크기가 자동 조정
|
||||
|
||||
---
|
||||
|
||||
## TC-08. 타임라인 - 날짜 네비게이션
|
||||
|
||||
### 목적
|
||||
이전/다음/오늘 버튼으로 타임라인 이동이 정상 동작하는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 툴바에서 현재 표시 날짜 확인
|
||||
2. "다음" 버튼 클릭 -> 다음 주(또는 기간)로 이동
|
||||
3. "이전" 버튼 클릭 -> 이전 주로 이동
|
||||
4. "오늘" 버튼 클릭 -> 현재 날짜 영역으로 이동
|
||||
5. 2월 초 데이터가 있으므로 충분히 이전으로 이동하여 과거 데이터 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] "다음" 클릭 시 타임라인이 오른쪽(미래)으로 이동
|
||||
- [ ] "이전" 클릭 시 타임라인이 왼쪽(과거)으로 이동
|
||||
- [ ] "오늘" 클릭 시 2026-03-16 부근으로 이동
|
||||
- [ ] 날짜 헤더의 표시 날짜가 변경됨
|
||||
- [ ] 이동 후에도 스케줄 바가 올바른 위치에 표시
|
||||
|
||||
---
|
||||
|
||||
## TC-09. 타임라인 - 드래그 이동
|
||||
|
||||
### 목적
|
||||
스케줄 바를 드래그하여 날짜를 변경하는 기능 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 완제품 탭에서 `planned` 상태의 스케줄 바 선택 (예: 탑씰 Type A, id:106)
|
||||
2. 스케줄 바를 마우스로 클릭하고 좌/우로 드래그
|
||||
3. 드래그 중 바가 마우스를 따라 이동하는지 확인 (시각적 피드백)
|
||||
4. 마우스 놓기 후 결과 확인
|
||||
5. 성공 시 토스트 알림 확인
|
||||
6. DB에 start_date/end_date가 변경되었는지 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 스케줄 바 드래그 시 시각적으로 이동 (opacity 변화)
|
||||
- [ ] 드래그 완료 후 "스케줄이 이동되었습니다" 토스트 표시
|
||||
- [ ] 날짜가 드래그 거리만큼 변경 (시작일/종료일 동일 간격 유지)
|
||||
- [ ] 실패 시 "스케줄 이동 실패" 에러 토스트 표시 후 원래 위치로 복귀
|
||||
|
||||
---
|
||||
|
||||
## TC-10. 타임라인 - 리사이즈 (기간 조정)
|
||||
|
||||
### 목적
|
||||
스케줄 바의 시작/종료 핸들을 드래그하여 기간을 변경하는 기능 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 완제품 탭에서 스케줄 바에 마우스 호버
|
||||
2. 바 좌측/우측에 리사이즈 핸들이 나타나는지 확인
|
||||
3. 우측 핸들을 오른쪽으로 드래그 -> 종료일 연장
|
||||
4. 좌측 핸들을 오른쪽으로 드래그 -> 시작일 변경
|
||||
5. 성공 시 토스트 알림 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 바 호버 시 좌/우측에 리사이즈 핸들(세로 바) 표시
|
||||
- [ ] 우측 핸들 드래그 시 종료일만 변경 (시작일 유지)
|
||||
- [ ] 좌측 핸들 드래그 시 시작일만 변경 (종료일 유지)
|
||||
- [ ] 리사이즈 완료 후 "스케줄 기간이 변경되었습니다" 토스트 표시
|
||||
- [ ] 바 크기가 변경된 기간에 맞게 조정
|
||||
|
||||
---
|
||||
|
||||
## TC-11. 타임라인 - 충돌 감지
|
||||
|
||||
### 목적
|
||||
같은 설비에 시간이 겹치는 스케줄이 있을 때 충돌 표시가 되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 충돌 데이터 확인:
|
||||
- 프레스기#1 (equipment_id=11): id:103 (03/10~03/17), id:4 (01/28~01/30) → 겹치지 않아서 충돌 없음
|
||||
- 조립라인#1 (equipment_id=14): id:5 (02/01~02/02), id:6 (02/01~02/02) → 기간 겹침! (반제품)
|
||||
2. 반제품 탭으로 이동하여 조립라인#1의 충돌 확인
|
||||
3. 또는 드래그로 충돌 상황을 만들어서 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 충돌 스케줄 바에 빨간 외곽선 (`ring-destructive`) 표시
|
||||
- [ ] 충돌 스케줄 바에 경고 아이콘 (AlertTriangle) 표시
|
||||
- [ ] 툴바에 "충돌 N건" 배지 표시 (빨간색)
|
||||
- [ ] 충돌이 없는 경우 배지 미표시
|
||||
|
||||
---
|
||||
|
||||
## TC-12. 타임라인 - 범례 (Legend)
|
||||
|
||||
### 목적
|
||||
하단 범례가 정상 표시되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 타임라인 하단에 범례 영역이 표시되는지 확인
|
||||
2. 상태별 색상 스와치가 표시되는지 확인
|
||||
3. 마일스톤 아이콘이 표시되는지 확인
|
||||
4. 충돌 표시 범례가 표시되는지 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] "계획" (파란색), "진행" (초록색), "완료" (회색), "지연" (빨간색), "취소" (연회색) 표시
|
||||
- [ ] "마일스톤" 다이아몬드 아이콘 표시
|
||||
- [ ] "충돌" 빨간 테두리 아이콘 표시 (showConflicts 설정 시)
|
||||
- [ ] 범례가 타임라인 하단에 깔끔하게 배치
|
||||
|
||||
---
|
||||
|
||||
## TC-13. 반제품 탭 전환
|
||||
|
||||
### 목적
|
||||
반제품 탭으로 전환 시 반제품 데이터만 필터링되어 표시되는지 확인 (staticFilters)
|
||||
|
||||
### 테스트 단계
|
||||
1. 우측 패널에서 "반제품" 탭 클릭
|
||||
2. 표시되는 스케줄이 반제품만인지 확인
|
||||
3. 완제품 데이터가 보이지 않는지 확인
|
||||
4. 다시 "완제품" 탭 클릭하여 전환 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] "반제품" 탭 클릭 시 반제품 스케줄만 표시 (4건)
|
||||
- [ ] 반제품 리소스: 조립라인#1, 용접기#1, 레이저절단기
|
||||
- [ ] 완제품 데이터는 표시되지 않음
|
||||
- [ ] "완제품" 탭 복귀 시 완제품 데이터만 표시
|
||||
|
||||
---
|
||||
|
||||
## TC-14. 버튼 - 새로고침
|
||||
|
||||
### 목적
|
||||
"새로고침" 버튼 클릭 시 데이터가 다시 로드되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 우측 패널 하단의 "새로고침" 버튼 클릭
|
||||
2. 타임라인 데이터가 다시 로드되는지 확인
|
||||
3. 토스트 알림 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 클릭 시 API 호출 (GET /api/production/order-summary)
|
||||
- [ ] 성공 시 "데이터를 새로고침했습니다." 토스트 표시
|
||||
- [ ] 타임라인 데이터 갱신
|
||||
|
||||
---
|
||||
|
||||
## TC-15. 버튼 - 자동 스케줄
|
||||
|
||||
### 목적
|
||||
좌측 테이블에서 수주 데이터를 선택한 후 자동 스케줄 생성이 되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 좌측 패널에서 수주 데이터 행 1개 이상 체크박스 선택
|
||||
2. "자동 스케줄" 버튼 클릭
|
||||
3. 확인 다이얼로그 표시 확인 ("선택한 품목의 자동 스케줄을 생성하시겠습니까?")
|
||||
4. "확인" 클릭
|
||||
5. 결과 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 확인 다이얼로그 표시
|
||||
- [ ] 성공 시 "자동 스케줄이 생성되었습니다." 토스트 표시
|
||||
- [ ] 우측 타임라인에 새로운 스케줄 바 추가
|
||||
- [ ] 실패 시 에러 메시지 표시
|
||||
- [ ] 선택 없이 클릭 시 적절한 안내 메시지
|
||||
|
||||
---
|
||||
|
||||
## TC-16. 버튼 - 선택 품목 불러오기
|
||||
|
||||
### 목적
|
||||
좌측 수주 데이터에서 선택한 품목을 생산계획으로 불러오는 기능 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 좌측 수주데이터 탭에서 품목 선택 (체크박스)
|
||||
2. "선택 품목 불러오기" 버튼 클릭
|
||||
3. 확인 다이얼로그 ("선택한 품목의 생산계획을 생성하시겠습니까?")
|
||||
4. 결과 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 확인 다이얼로그 표시
|
||||
- [ ] 성공 시 "선택 품목이 불러와졌습니다." 토스트 표시
|
||||
- [ ] 타임라인 자동 새로고침
|
||||
|
||||
---
|
||||
|
||||
## TC-17. 버튼 - 저장
|
||||
|
||||
### 목적
|
||||
변경된 생산계획 데이터가 저장되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 타임라인에서 스케줄 바 드래그 또는 리사이즈로 데이터 변경
|
||||
2. "저장" 버튼 클릭
|
||||
3. 저장 결과 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 성공 시 "생산계획이 저장되었습니다." 토스트 표시
|
||||
- [ ] 변경 사항이 DB에 반영
|
||||
|
||||
---
|
||||
|
||||
## TC-18. 반응형 CSS 확인
|
||||
|
||||
### 목적
|
||||
공통 반응형 CSS가 올바르게 적용되었는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 브라우저 창 너비를 640px 이하로 줄이기 (모바일)
|
||||
2. 텍스트 크기, 버튼 크기, 패딩 변화 확인
|
||||
3. 브라우저 창 너비를 1280px 이상으로 늘리기 (데스크톱)
|
||||
4. 원래 크기로 복귀 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 모바일(~640px): 텍스트 `text-[10px]`, 작은 버튼, 좁은 패딩
|
||||
- [ ] 데스크톱(640px~): 텍스트 `text-sm`, 기본 버튼, 넓은 패딩
|
||||
- [ ] 줌 버튼, 네비게이션 버튼, 리소스명, 날짜 헤더 모두 반응형 적용
|
||||
- [ ] 스케줄 바 내부 텍스트도 반응형 (text-[10px] sm:text-xs)
|
||||
- [ ] 범례 텍스트도 반응형
|
||||
|
||||
---
|
||||
|
||||
## TC-19. 마일스톤 표시
|
||||
|
||||
### 목적
|
||||
시작일과 종료일이 같은 스케줄이 마일스톤(다이아몬드)으로 표시되는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. DB에 마일스톤 테스트 데이터 추가:
|
||||
```sql
|
||||
INSERT INTO production_plan_mng (id, item_name, product_type, status, start_date, end_date, equipment_id, progress_rate, company_code)
|
||||
VALUES (200, '마일스톤 테스트', '완제품', 'planned', '2026-03-20', '2026-03-20', 9, '0', 'COMPANY_7');
|
||||
```
|
||||
2. 새로고침 후 해당 날짜에 다이아몬드 마커가 표시되는지 확인
|
||||
3. 호버 시 정보 표시 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 시작일 = 종료일인 스케줄은 바 대신 다이아몬드 마커로 표시
|
||||
- [ ] 다이아몬드가 45도 회전된 정사각형으로 표시
|
||||
- [ ] 호버 시 효과 적용
|
||||
|
||||
---
|
||||
|
||||
## TC-20. 안전재고 부족분 탭
|
||||
|
||||
### 목적
|
||||
좌측 패널의 "안전재고 부족분" 탭이 정상 동작하는지 확인
|
||||
|
||||
### 테스트 단계
|
||||
1. 좌측 패널에서 "안전재고 부족분" 탭 클릭
|
||||
2. inventory_stock 테이블 데이터가 표시되는지 확인
|
||||
3. 빈 데이터인 경우 빈 상태 메시지 확인
|
||||
|
||||
### 예상 결과
|
||||
- [ ] 탭 전환 정상 동작
|
||||
- [ ] 데이터 있으면: 품목코드, 현재고, 안전재고, 창고, 최근입고일 표시
|
||||
- [ ] 데이터 없으면: "안전재고 부족분 데이터가 없습니다" 메시지
|
||||
|
||||
---
|
||||
|
||||
## 알려진 이슈 / 참고 사항
|
||||
|
||||
| 번호 | 내용 | 심각도 |
|
||||
|:----:|------|:------:|
|
||||
| 1 | "1 Issue" 배지가 화면 좌측 하단에 표시됨 (원인 미확인) | 낮음 |
|
||||
| 2 | 생산계획 화면 URL 직접 접근 시 회사정보 화면(138)이 먼저 보일 수 있음 → 사이드바 메뉴를 통해 접근 권장 | 중간 |
|
||||
| 3 | 설비(equipment_info)의 equipment_group이 null → 리소스 그룹핑 미표시 | 낮음 |
|
||||
| 4 | 가상 스크롤은 리소스(설비) 30개 이상일 때 자동 활성화 (현재 10개라 비활성) | 참고 |
|
||||
|
||||
---
|
||||
|
||||
## 테스트 결과 요약
|
||||
|
||||
| TC | 항목 | 결과 | 비고 |
|
||||
|:--:|------|:----:|------|
|
||||
| 01 | 화면 레이아웃 | | |
|
||||
| 02 | 수주데이터 그룹 테이블 | | |
|
||||
| 03 | 체크박스 선택 | | |
|
||||
| 04 | 완제품 타임라인 기본 표시 | | |
|
||||
| 05 | 상태별 색상 | | |
|
||||
| 06 | 진행률 표시 | | |
|
||||
| 07 | 줌 레벨 전환 | | |
|
||||
| 08 | 날짜 네비게이션 | | |
|
||||
| 09 | 드래그 이동 | | |
|
||||
| 10 | 리사이즈 | | |
|
||||
| 11 | 충돌 감지 | | |
|
||||
| 12 | 범례 | | |
|
||||
| 13 | 반제품 탭 전환 | | |
|
||||
| 14 | 새로고침 버튼 | | |
|
||||
| 15 | 자동 스케줄 버튼 | | |
|
||||
| 16 | 선택 품목 불러오기 | | |
|
||||
| 17 | 저장 버튼 | | |
|
||||
| 18 | 반응형 CSS | | |
|
||||
| 19 | 마일스톤 표시 | | |
|
||||
| 20 | 안전재고 부족분 탭 | | |
|
||||
@@ -323,7 +323,7 @@ interface ButtonComponentConfig {
|
||||
|
||||
| 파일 | 내용 |
|
||||
|------|------|
|
||||
| `frontend/lib/button-icon-map.ts` | 버튼 액션별 추천 아이콘 매핑 + 아이콘 동적 렌더링 유틸 |
|
||||
| `frontend/lib/button-icon-map.tsx` | 버튼 액션별 추천 아이콘 매핑 + 아이콘 동적 렌더링 유틸 |
|
||||
|
||||
---
|
||||
|
||||
@@ -338,3 +338,52 @@ interface ButtonComponentConfig {
|
||||
- 외부 SVG 붙여넣기도 지원 → 관리자가 회사 로고 등 자체 아이콘을 등록 가능
|
||||
- lucide 커스텀 아이콘은 `componentConfig.customIcons`에, SVG 아이콘은 `componentConfig.customSvgIcons`에 저장
|
||||
- lucide 아이콘 렌더링: 아이콘 이름 → 컴포넌트 매핑, SVG 아이콘 렌더링: `dangerouslySetInnerHTML` + DOMPurify 정화
|
||||
- **동적 아이콘 로딩**: `iconMap`에 명시적으로 import되지 않은 lucide 아이콘도 `getLucideIcon()` 호출 시 `lucide-react`의 전체 아이콘(`icons`)에서 자동 조회 후 캐싱 → 화면 관리에서 선택한 모든 lucide 아이콘이 실제 화면에서도 렌더링됨
|
||||
- **커스텀 아이콘 전역 관리 (미구현)**: 커스텀 아이콘을 버튼별(`componentConfig`)이 아닌 시스템 전역(`custom_icon_registry` 테이블)으로 관리하여, 한번 추가한 커스텀 아이콘이 모든 화면의 모든 버튼에서 사용 가능하도록 확장 예정
|
||||
|
||||
---
|
||||
|
||||
## [미구현] 커스텀 아이콘 전역 관리
|
||||
|
||||
### 현재 문제
|
||||
|
||||
- 커스텀 아이콘이 `componentConfig.customIcons`에 저장 → **해당 버튼에서만** 보임
|
||||
- 저장1 버튼에 추가한 커스텀 아이콘이 저장2 버튼, 다른 화면에서는 안 보임
|
||||
- 같은 아이콘을 쓰려면 매번 검색해서 다시 추가해야 함
|
||||
|
||||
### 변경 후 동작
|
||||
|
||||
- 커스텀 아이콘을 **회사(company_code) 단위 전역**으로 관리
|
||||
- 어떤 화면의 어떤 버튼에서든 커스텀 아이콘 추가 → 모든 화면의 모든 버튼에서 커스텀란에 표시
|
||||
- 버튼 액션 종류와 무관하게 모든 커스텀 아이콘이 노출
|
||||
|
||||
### DB 테이블 (신규)
|
||||
|
||||
```sql
|
||||
CREATE TABLE custom_icon_registry (
|
||||
id VARCHAR(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
company_code VARCHAR(500) NOT NULL,
|
||||
icon_name VARCHAR(500) NOT NULL,
|
||||
icon_type VARCHAR(500) DEFAULT 'lucide', -- 'lucide' | 'svg'
|
||||
svg_data TEXT, -- SVG일 경우 원본 데이터
|
||||
created_date TIMESTAMP DEFAULT now(),
|
||||
updated_date TIMESTAMP DEFAULT now(),
|
||||
writer VARCHAR(500)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_custom_icon_registry_company ON custom_icon_registry(company_code);
|
||||
```
|
||||
|
||||
### 백엔드 API (신규)
|
||||
|
||||
| 메서드 | 경로 | 설명 |
|
||||
|--------|------|------|
|
||||
| GET | `/api/custom-icons` | 커스텀 아이콘 목록 조회 (company_code 필터) |
|
||||
| POST | `/api/custom-icons` | 커스텀 아이콘 추가 |
|
||||
| DELETE | `/api/custom-icons/:id` | 커스텀 아이콘 삭제 |
|
||||
|
||||
### 프론트엔드 변경
|
||||
|
||||
- `ButtonConfigPanel` — 커스텀 아이콘 조회/추가/삭제를 API 호출로 변경
|
||||
- 기존 `componentConfig.customIcons` 데이터는 하위 호환으로 병합 표시 (점진적 마이그레이션)
|
||||
- `componentConfig.customSvgIcons`도 동일하게 전역 테이블로 이관
|
||||
|
||||
@@ -145,8 +145,24 @@
|
||||
|
||||
- **결정**: lucide-react에서 export되는 전체 아이콘 이름 목록을 검색 가능
|
||||
- **근거**: 관리자가 "어떤 아이콘이 있는지" 모르므로 검색 기능이 필수
|
||||
- **구현**: lucide 아이콘 이름 배열을 상수로 관리하고, CommandInput으로 필터링
|
||||
- **주의**: 전체 아이콘 컴포넌트를 import하지 않고, 이름 배열만 관리 → 선택 시에만 해당 아이콘을 매핑에 추가
|
||||
- **구현**: `lucide-react`의 `icons` 객체에서 `Object.keys()`로 전체 이름 목록을 가져오고, CommandInput으로 필터링
|
||||
- **주의**: `allLucideIcons`는 `button-icon-map.tsx`에서 re-export하여 import를 중앙화
|
||||
|
||||
### 18. 커스텀 아이콘 전역 관리 (미구현)
|
||||
|
||||
- **결정**: 커스텀 아이콘을 버튼별(`componentConfig`) → 시스템 전역(`custom_icon_registry` 테이블)으로 변경
|
||||
- **근거**: 현재는 버튼 A에서 추가한 커스텀 아이콘이 버튼 B, 다른 화면에서 안 보여 매번 재등록 필요. 아이콘은 시각적 자원이므로 액션이나 화면에 종속될 이유가 없음
|
||||
- **범위 검토**: 버튼별 < 화면 단위 < **시스템 전역(채택)** — 같은 아이콘을 여러 화면에서 재사용하는 ERP 특성에 시스템 전역이 가장 적합
|
||||
- **저장**: `custom_icon_registry` 테이블 (company_code 멀티테넌시), lucide 이름 또는 SVG 데이터 저장
|
||||
- **하위 호환**: 기존 `componentConfig.customIcons` 데이터는 병합 표시 후 점진적 마이그레이션
|
||||
|
||||
### 19. 동적 아이콘 로딩 (getLucideIcon fallback)
|
||||
|
||||
- **결정**: `getLucideIcon(name)`이 `iconMap`에 없는 아이콘을 `lucide-react`의 `icons` 전체 객체에서 동적으로 조회 후 캐싱
|
||||
- **근거**: 화면 관리에서 커스텀 lucide 아이콘을 선택하면 `componentConfig.customIcons`에 이름만 저장됨. 디자이너 세션에서는 `addToIconMap()`으로 런타임에 등록되지만, 실제 화면(뷰어) 로드 시에는 `iconMap`에 해당 아이콘이 없어 렌더링 실패. `icons` fallback을 추가하면 **어떤 lucide 아이콘이든 이름만으로 자동 렌더링**
|
||||
- **구현**: `button-icon-map.tsx`에 `import { icons as allLucideIcons } from "lucide-react"` 추가, `getLucideIcon()`에서 `iconMap` miss 시 `allLucideIcons[name]` 조회 후 `iconMap`에 캐싱
|
||||
- **번들 영향**: `icons` 전체 객체 import로 번들 크기 증가 (~100-200KB). ERP 애플리케이션 특성상 수용 가능한 수준이며, 관리자가 선택한 모든 아이콘이 실제 화면에서 동작하는 것이 더 중요
|
||||
- **대안 검토**: 뷰어 로드 시 `customIcons`를 순회하여 개별 등록 → 기각 (모든 뷰어 컴포넌트에 로직 추가 필요, 누락 위험)
|
||||
|
||||
---
|
||||
|
||||
@@ -159,7 +175,7 @@
|
||||
| 뷰어 렌더링 (수정) | `frontend/components/screen/InteractiveScreenViewer.tsx` | 버튼 렌더링 분기 (2041~2059행) |
|
||||
| 위젯 (수정) | `frontend/components/screen/widgets/types/ButtonWidget.tsx` | 위젯 기반 버튼 렌더링 (67~86행) |
|
||||
| 최적화 버튼 (수정) | `frontend/components/screen/OptimizedButtonComponent.tsx` | 최적화된 버튼 렌더링 (643~674행) |
|
||||
| 아이콘 매핑 (신규) | `frontend/lib/button-icon-map.ts` | 액션별 추천 아이콘 + 동적 렌더링 유틸 |
|
||||
| 아이콘 매핑 (신규) | `frontend/lib/button-icon-map.tsx` | 액션별 추천 아이콘 + 동적 렌더링 유틸 + allLucideIcons fallback |
|
||||
| 타입 정의 (참고) | `frontend/types/screen.ts` | ComponentData, componentConfig 타입 |
|
||||
|
||||
---
|
||||
@@ -169,17 +185,21 @@
|
||||
### lucide-react 아이콘 동적 렌더링
|
||||
|
||||
```typescript
|
||||
// button-icon-map.ts
|
||||
import { Check, Save, Trash2, Pencil, ... } from "lucide-react";
|
||||
// button-icon-map.tsx
|
||||
import { Check, Save, ..., icons as allLucideIcons, type LucideIcon } from "lucide-react";
|
||||
|
||||
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Check, Save, Trash2, Pencil, ...
|
||||
};
|
||||
// 추천 아이콘은 명시적 import, 나머지는 동적 조회
|
||||
const iconMap: Record<string, LucideIcon> = { Check, Save, ... };
|
||||
|
||||
export function renderButtonIcon(name: string, size: string | number) {
|
||||
const IconComponent = iconMap[name];
|
||||
if (!IconComponent) return null;
|
||||
return <IconComponent style={getIconSizeStyle(size)} />;
|
||||
export function getLucideIcon(name: string): LucideIcon | undefined {
|
||||
if (iconMap[name]) return iconMap[name];
|
||||
// iconMap에 없으면 lucide-react 전체에서 동적 조회 후 캐싱
|
||||
const found = allLucideIcons[name as keyof typeof allLucideIcons];
|
||||
if (found) {
|
||||
iconMap[name] = found;
|
||||
return found;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -125,12 +125,30 @@
|
||||
- [x] 커스텀 아이콘 삭제 시 디폴트 아이콘으로 복귀 → 아이콘 모드 유지 확인
|
||||
- [x] deprecated 액션에서 디폴트 폴백 아이콘(SquareMousePointer) 표시 확인
|
||||
|
||||
### 6단계: 정리
|
||||
### 6단계: 동적 아이콘 로딩 (뷰어 렌더링 누락 수정)
|
||||
|
||||
- [x] TypeScript 컴파일 에러 없음 확인 (우리 파일 6개 모두 0 에러)
|
||||
- [x] `button-icon-map.tsx`에 `icons as allLucideIcons` import 추가
|
||||
- [x] `getLucideIcon()` — `iconMap` miss 시 `allLucideIcons` fallback 조회 + 캐싱
|
||||
- [x] `allLucideIcons`를 `button-icon-map.tsx`에서 re-export (import 중앙화)
|
||||
- [x] `ButtonConfigPanel.tsx` — `lucide-react` 직접 import 제거, `button-icon-map`에서 import로 통합
|
||||
- [x] 화면 관리에서 선택한 커스텀 lucide 아이콘이 실제 화면(뷰어)에서도 렌더링됨 확인
|
||||
|
||||
### 7단계: 정리
|
||||
|
||||
- [x] TypeScript 컴파일 에러 없음 확인
|
||||
- [x] 불필요한 import 없음 확인
|
||||
- [x] 문서 3개 최신화 (동적 로딩 반영)
|
||||
- [x] 이 체크리스트 완료 표시 업데이트
|
||||
|
||||
### 8단계: 커스텀 아이콘 전역 관리 (미구현)
|
||||
|
||||
- [ ] `custom_icon_registry` 테이블 마이그레이션 SQL 작성 및 실행 (개발섭 + 본섭)
|
||||
- [ ] 백엔드 API 구현 (GET/POST/DELETE `/api/custom-icons`)
|
||||
- [ ] 프론트엔드 API 클라이언트 함수 추가 (`lib/api/`)
|
||||
- [ ] `ButtonConfigPanel` — 커스텀 아이콘 조회/추가/삭제를 전역 API로 변경
|
||||
- [ ] 기존 `componentConfig.customIcons` 하위 호환 병합 처리
|
||||
- [ ] 검증: 화면 A에서 추가한 커스텀 아이콘이 화면 B에서도 보이는지 확인
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
@@ -156,3 +174,6 @@
|
||||
| 2026-03-04 | 텍스트 위치 4방향 설정 추가 (왼쪽/오른쪽/위쪽/아래쪽) |
|
||||
| 2026-03-04 | 버튼 테두리 이중 적용 수정 — position wrapper에서 border strip, border shorthand 제거 |
|
||||
| 2026-03-04 | 프리셋 라벨 한글화 (작게/보통/크게/매우 크게), 라벨 "아이콘 크기 비율"로 변경 |
|
||||
| 2026-03-13 | 동적 아이콘 로딩 — `getLucideIcon()` fallback으로 `allLucideIcons` 조회+캐싱, import 중앙화 |
|
||||
| 2026-03-13 | 문서 3개 최신화 (계획서 설계 원칙, 맥락노트 결정사항 #18, 체크리스트 6-7단계) |
|
||||
| 2026-03-13 | 커스텀 아이콘 전역 관리 계획 추가 (8단계, 미구현) — DB 테이블 + API + 프론트 변경 예정 |
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# BTN - 버튼 UI 스타일 기준정보
|
||||
|
||||
## 1. 스타일 기준
|
||||
|
||||
### 공통 스타일
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 높이 | 40px |
|
||||
| 표시모드 | 아이콘 + 텍스트 (icon-text) |
|
||||
| 아이콘 | 액션별 첫 번째 기본 아이콘 (자동 선택) |
|
||||
| 아이콘 크기 비율 | 보통 |
|
||||
| 아이콘-텍스트 간격 | 6px |
|
||||
| 텍스트 위치 | 오른쪽 (아이콘 왼쪽, 텍스트 오른쪽) |
|
||||
| 테두리 모서리 | 8px |
|
||||
| 테두리 색상/두께 | 없음 (투명, borderWidth: 0) |
|
||||
| 텍스트 색상 | #FFFFFF (흰색) |
|
||||
| 텍스트 크기 | 12px |
|
||||
| 텍스트 굵기 | normal (보통) |
|
||||
| 텍스트 정렬 | 왼쪽 |
|
||||
|
||||
### 배경색 (액션별)
|
||||
|
||||
| 액션 타입 | 배경색 | 비고 |
|
||||
|---|---|---|
|
||||
| `delete` | `#F04544` | 빨간색 |
|
||||
| `excel_download`, `excel_upload`, `multi_table_excel_upload` | `#212121` | 검정색 |
|
||||
| 그 외 모든 액션 | `#3B83F6` | 파란색 (기본값) |
|
||||
|
||||
배경색은 디자이너에서 액션을 변경하면 자동으로 바뀐다.
|
||||
|
||||
### 너비 (텍스트 글자수별)
|
||||
|
||||
| 글자수 | 너비 |
|
||||
|---|---|
|
||||
| 6글자 이하 | 140px |
|
||||
| 7글자 이상 | 160px |
|
||||
|
||||
### 액션별 기본 아이콘
|
||||
|
||||
디자이너에서 표시모드를 "아이콘" 또는 "아이콘+텍스트"로 변경하면 액션에 맞는 첫 번째 아이콘이 자동 선택된다.
|
||||
|
||||
소스: `frontend/lib/button-icon-map.tsx` > `actionIconMap`
|
||||
|
||||
| action.type | 기본 아이콘 |
|
||||
|---|---|
|
||||
| `save` | Check |
|
||||
| `delete` | Trash2 |
|
||||
| `edit` | Pencil |
|
||||
| `navigate` | ArrowRight |
|
||||
| `modal` | Maximize2 |
|
||||
| `transferData` | SendHorizontal |
|
||||
| `excel_download` | Download |
|
||||
| `excel_upload` | Upload |
|
||||
| `quickInsert` | Zap |
|
||||
| `control` | Settings |
|
||||
| `barcode_scan` | ScanLine |
|
||||
| `operation_control` | Truck |
|
||||
| `event` | Send |
|
||||
| `copy` | Copy |
|
||||
| (그 외/없음) | SquareMousePointer |
|
||||
|
||||
---
|
||||
|
||||
## 2. 코드 반영 현황
|
||||
|
||||
### 컴포넌트 기본값 (신규 버튼 생성 시 적용)
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `frontend/lib/registry/components/v2-button-primary/index.ts` | defaultConfig, defaultSize (140x40) |
|
||||
| `frontend/lib/registry/components/v2-button-primary/config.ts` | ButtonPrimaryDefaultConfig |
|
||||
|
||||
### 액션 변경 시 배경색 자동 변경
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `frontend/components/screen/config-panels/ButtonConfigPanel.tsx` | 액션 변경 시 배경색/텍스트색 자동 설정 |
|
||||
|
||||
### 렌더링 배경색 우선순위
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx` | 배경색 결정 우선순위 개선 |
|
||||
|
||||
배경색 결정 순서:
|
||||
1. `webTypeConfig.backgroundColor`
|
||||
2. `componentConfig.backgroundColor`
|
||||
3. `component.style.backgroundColor`
|
||||
4. `componentConfig.style.backgroundColor`
|
||||
5. `component.style.labelColor` (레거시 호환)
|
||||
6. 액션별 기본 배경색 (`#F04544` / `#212121` / `#3B83F6`)
|
||||
|
||||
### 미반영 (추후 작업)
|
||||
|
||||
- split-panel 내부 버튼의 코드 기본값 (split-panel 컴포넌트가 자체 생성하는 버튼)
|
||||
|
||||
---
|
||||
|
||||
## 3. DB 데이터 매핑 (layout_data JSON)
|
||||
|
||||
버튼은 `layout_data.components[]` 배열 안에 `url`이 `v2-button-primary`인 컴포넌트로 저장된다.
|
||||
|
||||
| 항목 | JSON 위치 | 값 |
|
||||
|---|---|---|
|
||||
| 높이 | `size.height` | `40` |
|
||||
| 너비 | `size.width` | `140` 또는 `160` |
|
||||
| 표시모드 | `overrides.displayMode` | `"icon-text"` |
|
||||
| 아이콘 이름 | `overrides.icon.name` | 액션별 영문 이름 |
|
||||
| 아이콘 타입 | `overrides.icon.type` | `"lucide"` |
|
||||
| 아이콘 크기 | `overrides.icon.size` | `"보통"` |
|
||||
| 텍스트 위치 | `overrides.iconTextPosition` | `"right"` |
|
||||
| 아이콘-텍스트 간격 | `overrides.iconGap` | `6` |
|
||||
| 테두리 모서리 | `overrides.style.borderRadius` | `"8px"` |
|
||||
| 텍스트 색상 | `overrides.style.labelColor` | `"#FFFFFF"` |
|
||||
| 텍스트 크기 | `overrides.style.fontSize` | `"12px"` |
|
||||
| 텍스트 굵기 | `overrides.style.fontWeight` | `"normal"` |
|
||||
| 텍스트 정렬 | `overrides.style.labelTextAlign` | `"left"` |
|
||||
| 배경색 | `overrides.style.backgroundColor` | 액션별 색상 |
|
||||
|
||||
버튼이 위치하는 구조별 경로:
|
||||
- 일반 버튼: `layout_data.components[]`
|
||||
- 탭 위젯 내부: `layout_data.components[].overrides.tabs[].components[]`
|
||||
- split-panel 내부: `layout_data.components[].overrides.rightPanel.components[]`
|
||||
|
||||
---
|
||||
|
||||
## 4. 탑씰(COMPANY_7) 일괄 변경 작업 기록
|
||||
|
||||
### 대상
|
||||
- **회사**: 탑씰 (company_code = 'COMPANY_7')
|
||||
- **테이블**: screen_layouts_v2 (배포서버)
|
||||
- **스크립트**: `backend-node/scripts/btn-bulk-update-company7.ts`
|
||||
- **백업 테이블**: `screen_layouts_v2_backup_company7`
|
||||
|
||||
### 작업 이력
|
||||
|
||||
| 날짜 | 작업 내용 | 비고 |
|
||||
|---|---|---|
|
||||
| 2026-03-13 | 백업 테이블 생성 | |
|
||||
| 2026-03-13 | 전체 버튼 공통 스타일 일괄 적용 | 높이, 아이콘, 텍스트 스타일, 배경색, 모서리 |
|
||||
| 2026-03-13 | 탭 위젯 내부 버튼 스타일 보정 | componentConfig + root style 양쪽 적용 |
|
||||
| 2026-03-13 | fontWeight "400" → "normal" 보정 | |
|
||||
| 2026-03-13 | overrides.style.width 제거 | size.width와 충돌 방지 |
|
||||
| 2026-03-13 | save 액션 55개에 "저장" 텍스트 명시 | |
|
||||
| 2026-03-13 | "엑셀다운로드" → "Excel" 텍스트 통일 | |
|
||||
| 2026-03-13 | Excel 버튼 배경색 #212121 통일 | |
|
||||
| 2026-03-13 | 전체 버튼 너비 140px 통일 | |
|
||||
| 2026-03-13 | 7글자 이상 버튼 너비 160px 재조정 | |
|
||||
| 2026-03-13 | split-panel 내부 버튼 스타일 적용 | BOM관리 등 7개 버튼 |
|
||||
|
||||
### 스킵 항목
|
||||
- `transferData` 액션의 텍스트 없는 버튼 1개 (screen=5976)
|
||||
|
||||
### 알려진 이슈
|
||||
- **반응형 너비 불일치**: 디자이너에서 설정한 `size.width`가 실제 화면(`ResponsiveGridRenderer`)에서 반영되지 않을 수 있음. 버튼 wrapper에 `width` 속성이 누락되어 flex shrink-to-fit 동작으로 너비가 줄어드는 현상. 세로(height)는 정상 반영됨.
|
||||
|
||||
### 원복 (필요 시)
|
||||
|
||||
```sql
|
||||
UPDATE screen_layouts_v2 AS target
|
||||
SET layout_data = backup.layout_data
|
||||
FROM screen_layouts_v2_backup_company7 AS backup
|
||||
WHERE target.layout_id = backup.layout_id;
|
||||
```
|
||||
|
||||
### 백업 테이블 정리
|
||||
|
||||
```sql
|
||||
DROP TABLE screen_layouts_v2_backup_company7;
|
||||
```
|
||||
@@ -0,0 +1,420 @@
|
||||
# [계획서] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성
|
||||
|
||||
> 관련 문서: [맥락노트](./MPN[맥락]-품번-수동접두어채번.md) | [체크리스트](./MPN[체크]-품번-수동접두어채번.md)
|
||||
|
||||
## 개요
|
||||
|
||||
기준정보 - 품목 정보 등록 모달에서 품번(`item_number`) 채번의 세 가지 문제를 해결합니다.
|
||||
|
||||
1. **BULK1 덮어쓰기 문제**: 사용자가 "ㅁㅁㅁ"을 입력해도 수동 값 추출이 실패하여 DB 숨은 값 `manualConfig.value = "BULK1"`로 덮어씌워짐
|
||||
2. **순번 공유 문제**: `buildPrefixKey`가 수동 파트를 건너뛰어 모든 접두어가 같은 시퀀스 카운터를 공유함
|
||||
3. **연속 구분자(--) 문제**: 카테고리가 비었을 때 `joinPartsWithSeparators`가 빈 파트에도 구분자를 붙여 `--` 발생 + 템플릿 불일치로 수동 값 추출 실패 → `userInputCode` 전체(구분자 포함)가 수동 값이 됨
|
||||
|
||||
---
|
||||
|
||||
## 현재 동작
|
||||
|
||||
### 채번 규칙 구성 (옵션설정 > 코드설정)
|
||||
|
||||
```
|
||||
규칙1(카테고리/재질, 자동) → "-" → 규칙2(문자, 직접입력) → "-" → 규칙3(순번, 자동, 3자리, 시작=5)
|
||||
```
|
||||
|
||||
### 실제 저장 흐름 (사용자가 "ㅁㅁㅁ" 입력 시)
|
||||
|
||||
1. 모달 열림 → `_numberingRuleId` 설정됨 (TextInputComponent L117-128)
|
||||
2. 사용자가 "ㅁㅁㅁ" 입력 → `formData.item_number = "ㅁㅁㅁ"`
|
||||
3. 저장 클릭 → `buttonActions.ts`가 `_numberingRuleId` 확인 → `allocateCode(ruleId, "ㅁㅁㅁ", formData)` 호출
|
||||
4. 백엔드: 템플릿 기반 수동 값 추출 시도 → **실패** (입력 "ㅁㅁㅁ"이 템플릿 "CATEGORY-____-XXX"와 불일치)
|
||||
5. 폴백: `manualConfig.value = "BULK1"` 사용 → **사용자 입력 "ㅁㅁㅁ" 완전 무시됨**
|
||||
6. `buildPrefixKey`가 수동 파트를 건너뜀 → prefix_key에 접두어 미포함 → 공유 카운터 사용
|
||||
7. 결과: **-BULK1-015** (사용자가 뭘 입력하든 항상 BULK1, 항상 공유 카운터)
|
||||
|
||||
### 문제 1: 순번 공유 (buildPrefixKey)
|
||||
|
||||
**위치**: `numberingRuleService.ts` L85-88
|
||||
|
||||
```typescript
|
||||
if (part.generationMethod === "manual") {
|
||||
// 수동 입력 파트는 prefix에서 제외 (값이 매번 달라질 수 있으므로)
|
||||
continue; // ← 접두어별 순번 분리를 막는 원인
|
||||
}
|
||||
```
|
||||
|
||||
이 `continue` 때문에 수동 입력값이 prefix_key에 포함되지 않습니다.
|
||||
"ㅁㅁㅁ", "ㅇㅇㅇ", "BULK1" 전부 **같은 시퀀스 카운터를 공유**합니다.
|
||||
|
||||
### 문제 2: BULK1 덮어쓰기 (추출 실패 + manualConfig.value 폴백)
|
||||
|
||||
**발생 흐름**:
|
||||
|
||||
1. 사용자가 "ㅁㅁㅁ" 입력 → `userInputCode = "ㅁㅁㅁ"` 으로 `allocateCode` 호출
|
||||
2. `allocateCode` 내부에서 **prefix_key를 먼저 빌드** (L1306) → 수동 값 추출은 그 이후 (L1332-1442)
|
||||
3. 템플릿 기반 수동 값 추출 시도 (L1411-1436):
|
||||
```
|
||||
템플릿: "카테고리값-____-XXX" (카테고리값-수동입력위치-순번)
|
||||
사용자 입력: "ㅁㅁㅁ"
|
||||
```
|
||||
4. "ㅁㅁㅁ"은 "카테고리값-"으로 시작하지 않음 → `startsWith` 불일치 → **추출 실패** → `extractedManualValues = []`
|
||||
5. 코드 조합 단계 (L1448-1454)에서 폴백 체인 동작:
|
||||
```typescript
|
||||
const manualValue =
|
||||
extractedManualValues[0] || // undefined (추출 실패)
|
||||
part.manualConfig?.value || // "BULK1" (DB 숨은 값) ← 여기서 덮어씌워짐
|
||||
"";
|
||||
```
|
||||
6. 결과: `-BULK1-015` (사용자 입력 "ㅁㅁㅁ"이 완전히 무시됨)
|
||||
|
||||
**DB 숨은 값 원인**:
|
||||
- DB `numbering_rule_parts.manual_config` 컬럼에 `{"value": "BULK1", "placeholder": "..."}` 저장됨
|
||||
- `ManualConfigPanel.tsx`에는 `placeholder` 입력란만 있고 **`value` 입력란이 없음**
|
||||
- 플레이스홀더 수정 시 `{ ...config, placeholder: ... }` 스프레드로 기존 `value: "BULK1"`이 계속 보존됨
|
||||
|
||||
### 문제 3: 연속 구분자(--) 문제
|
||||
|
||||
**발생 흐름**:
|
||||
|
||||
1. 카테고리 미선택 → 카테고리 파트 값 = `""` (빈 문자열)
|
||||
2. `joinPartsWithSeparators`가 빈 파트에도 구분자 `-`를 추가 → 연속 빈 파트 시 `--` 발생
|
||||
3. 사용자 입력 필드에 `-제발-015` 형태로 표시 (선행 `-`)
|
||||
4. `extractManualValuesFromInput`에서 템플릿이 `CATEGORY-____-XXX`로 생성됨 (실제 값 `""` 대신 플레이스홀더 `"CATEGORY"` 사용)
|
||||
5. 입력 `-제발-015`이 `CATEGORY-`로 시작하지 않음 → 추출 실패
|
||||
6. 폴백: `userInputCode` 전체 `-제발-015`가 수동 값이 됨
|
||||
7. 코드 조합: `""` + `-` + `-제발-015` + `-` + `003` = `--제발-015-003`
|
||||
|
||||
### 정상 동작 확인된 부분
|
||||
|
||||
| 항목 | 상태 | 근거 |
|
||||
|------|------|------|
|
||||
| `_numberingRuleId` 유지 | 정상 | 사용자 입력해도 allocateCode가 호출됨 |
|
||||
| 시퀀스 증가 | 정상 | 순번이 증가하고 있음 (015 등) |
|
||||
| 코드 조합 | 정상 | 구분자, 파트 순서 등 올바르게 결합됨 |
|
||||
|
||||
### 비정상 확인된 부분
|
||||
|
||||
| 항목 | 상태 | 근거 |
|
||||
|------|------|------|
|
||||
| 수동 값 추출 | **실패** | 사용자 입력 "ㅁㅁㅁ"이 템플릿과 불일치 → 추출 실패 → BULK1 폴백 |
|
||||
| prefix_key 분리 | **실패** | `buildPrefixKey`가 수동 파트 skip → 모든 접두어가 같은 시퀀스 공유 |
|
||||
| 연속 구분자 | **실패** | 빈 파트에 구분자 추가 + 템플릿 플레이스홀더 불일치 → `--` 발생 |
|
||||
|
||||
---
|
||||
|
||||
## 변경 후 동작
|
||||
|
||||
### prefix_key에 수동 파트 값 포함
|
||||
|
||||
```
|
||||
현재: prefix_key = 카테고리값만 (수동 파트 무시)
|
||||
변경: prefix_key = 카테고리값 + "|" + 수동입력값
|
||||
```
|
||||
|
||||
### allocateCode 실행 순서 변경
|
||||
|
||||
```
|
||||
현재: buildPrefixKey → 시퀀스 할당 → 수동 값 추출 → 코드 조합
|
||||
변경: 수동 값 추출 → buildPrefixKey(수동 값 포함) → 시퀀스 할당 → 코드 조합
|
||||
```
|
||||
|
||||
### 순번 동작
|
||||
|
||||
```
|
||||
"ㅁㅁㅁ" 첫 등록 → prefix_key="카테고리|ㅁㅁㅁ", sequence=1 → -ㅁㅁㅁ-001
|
||||
"ㅁㅁㅁ" 두번째 → prefix_key="카테고리|ㅁㅁㅁ", sequence=2 → -ㅁㅁㅁ-002
|
||||
"ㅇㅇㅇ" 첫 등록 → prefix_key="카테고리|ㅇㅇㅇ", sequence=1 → -ㅇㅇㅇ-001
|
||||
"ㅁㅁㅁ" 세번째 → prefix_key="카테고리|ㅁㅁㅁ", sequence=3 → -ㅁㅁㅁ-003
|
||||
```
|
||||
|
||||
### BULK1 폴백 제거 (코드 + DB 이중 조치)
|
||||
|
||||
```
|
||||
코드: 폴백 체인에서 manualConfig.value 제거 → extractedManualValues만 사용
|
||||
DB: manual_config에서 "value": "BULK1" 키 제거 → 유령 기본값 정리
|
||||
```
|
||||
|
||||
### 연속 구분자 방지 + 템플릿 정합성 복원
|
||||
|
||||
```
|
||||
joinPartsWithSeparators: 빈 파트 뒤에 이미 구분자가 있으면 중복 추가하지 않음
|
||||
extractManualValuesFromInput: 카테고리/참조 빈 값 시 "" 반환 (플레이스홀더 "CATEGORY"/"REF" 대신)
|
||||
→ 템플릿이 실제 코드 구조와 일치 → 추출 성공 → -- 방지
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 시각적 예시
|
||||
|
||||
| 사용자 입력 | 현재 동작 | 원인 | 변경 후 동작 |
|
||||
|------------|----------|------|-------------|
|
||||
| `ㅁㅁㅁ` (첫번째) | `-BULK1-015` | 추출 실패 → BULK1 폴백 + 공유 카운터 | `카테고리값-ㅁㅁㅁ-001` |
|
||||
| `ㅁㅁㅁ` (두번째) | `-BULK1-016` | 동일 | `카테고리값-ㅁㅁㅁ-002` |
|
||||
| `ㅇㅇㅇ` (첫번째) | `-BULK1-017` | 동일 | `카테고리값-ㅇㅇㅇ-001` |
|
||||
| (입력 안 함) | `-BULK1-018` | manualConfig.value 폴백 | 에러 반환 (수동 파트 필수 입력) |
|
||||
| 카테고리 비었을 때 | `--제발-015-003` | 빈 파트 구분자 중복 + 템플릿 불일치 | `-제발-001` |
|
||||
|
||||
---
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 사용자
|
||||
participant BA as buttonActions.ts
|
||||
participant API as allocateNumberingCode API
|
||||
participant NRS as numberingRuleService
|
||||
participant DB as numbering_rule_sequences
|
||||
|
||||
User->>BA: 저장 클릭 (item_number = "ㅁㅁㅁ")
|
||||
BA->>API: allocateCode(ruleId, "ㅁㅁㅁ", formData)
|
||||
API->>NRS: allocateCode()
|
||||
|
||||
Note over NRS: 1단계: 수동 값 추출 (buildPrefixKey 전에 수행)
|
||||
NRS->>NRS: extractManualValuesFromInput("ㅁㅁㅁ")
|
||||
Note over NRS: 템플릿 파싱 실패 → 폴백: userInputCode 전체 사용
|
||||
NRS->>NRS: extractedManualValues = ["ㅁㅁㅁ"]
|
||||
|
||||
Note over NRS: 2단계: prefix_key 빌드 (수동 값 포함)
|
||||
NRS->>NRS: buildPrefixKey(rule, formData, ["ㅁㅁㅁ"])
|
||||
Note over NRS: prefix_key = "카테고리값|ㅁㅁㅁ"
|
||||
|
||||
Note over NRS: 3단계: 시퀀스 할당
|
||||
NRS->>DB: UPSERT sequences (prefix_key="카테고리값|ㅁㅁㅁ")
|
||||
DB-->>NRS: current_sequence = 1
|
||||
|
||||
Note over NRS: 4단계: 코드 조합
|
||||
NRS->>NRS: 카테고리값 + "-" + "ㅁㅁㅁ" + "-" + "001"
|
||||
NRS-->>API: "카테고리값-ㅁㅁㅁ-001"
|
||||
API-->>BA: generatedCode
|
||||
BA->>BA: formData.item_number = "카테고리값-ㅁㅁㅁ-001"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경 대상 파일
|
||||
|
||||
| 파일 | 변경 내용 | 규모 |
|
||||
|------|----------|------|
|
||||
| `backend-node/src/services/numberingRuleService.ts` | `buildPrefixKey`에 `manualValues` 파라미터 추가, `allocateCode`에서 수동 값 추출 순서 변경 + 폴백 체인 정리, `extractManualValuesFromInput` 헬퍼 분리, `joinPartsWithSeparators` 연속 구분자 방지, 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경, `previewCode`에 `manualInputValue` 파라미터 추가 + `startFrom` 적용 | ~80줄 |
|
||||
| `backend-node/src/controllers/numberingRuleController.ts` | preview 엔드포인트에 `manualInputValue` body 파라미터 수신 추가 | ~2줄 |
|
||||
| `frontend/lib/api/numberingRule.ts` | `previewNumberingCode`에 `manualInputValue` 파라미터 추가 | ~3줄 |
|
||||
| `frontend/components/v2/V2Input.tsx` | 수동 입력값 변경 시 디바운스(300ms) preview API 호출 + suffix(순번) 실시간 갱신 | ~35줄 |
|
||||
| `db/migrations/1053_remove_bulk1_manual_config_value.sql` | `numbering_rule_parts.manual_config`에서 `value: "BULK1"` 제거 | SQL 1건 |
|
||||
|
||||
### buildPrefixKey 호출부 영향 분석
|
||||
|
||||
| 호출부 | 위치 | `manualValues` 전달 | 영향 |
|
||||
|--------|------|---------------------|------|
|
||||
| `previewCode` | L1091 | `manualInputValue` 전달 시 포함 | 접두어별 정확한 순번 조회 |
|
||||
| `allocateCode` | L1332 | 전달 | prefix_key에 수동 값 포함됨 |
|
||||
|
||||
### 멀티테넌시 체크
|
||||
|
||||
| 항목 | 상태 | 근거 |
|
||||
|------|------|------|
|
||||
| `buildPrefixKey` | 영향 없음 | 시그니처만 확장, company_code 관련 변경 없음 |
|
||||
| `allocateCode` | 이미 준수 | L1302에서 `companyCode`로 규칙 조회, L1313에서 시퀀스 할당 시 `companyCode` 전달 |
|
||||
| `joinPartsWithSeparators` | 영향 없음 | 순수 문자열 조합 함수, company_code 무관 |
|
||||
| DB 마이그레이션 | 해당 없음 | JSONB 내부 값 정리, company_code 무관 |
|
||||
|
||||
---
|
||||
|
||||
## 코드 설계
|
||||
|
||||
### 1. `joinPartsWithSeparators` 수정 - 연속 구분자 방지
|
||||
|
||||
**위치**: L36-48
|
||||
**변경**: 빈 파트 뒤에 이미 구분자가 있으면 중복 추가하지 않음
|
||||
|
||||
```typescript
|
||||
function joinPartsWithSeparators(partValues: string[], sortedParts: any[], globalSeparator: string): string {
|
||||
let result = "";
|
||||
partValues.forEach((val, idx) => {
|
||||
result += val;
|
||||
if (idx < partValues.length - 1) {
|
||||
const sep = sortedParts[idx].separatorAfter ?? sortedParts[idx].autoConfig?.separatorAfter ?? globalSeparator;
|
||||
if (val || !result.endsWith(sep)) {
|
||||
result += sep;
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `buildPrefixKey` 수정 - 수동 파트 값을 prefix에 포함
|
||||
|
||||
**위치**: L75-88
|
||||
**변경**: 세 번째 파라미터 `manualValues` 추가. 전달되면 prefix_key에 포함.
|
||||
|
||||
```typescript
|
||||
private async buildPrefixKey(
|
||||
rule: NumberingRuleConfig,
|
||||
formData?: Record<string, any>,
|
||||
manualValues?: string[]
|
||||
): Promise<string> {
|
||||
const sortedParts = [...rule.parts].sort((a: any, b: any) => a.order - b.order);
|
||||
const prefixParts: string[] = [];
|
||||
let manualIndex = 0;
|
||||
|
||||
for (const part of sortedParts) {
|
||||
if (part.partType === "sequence") continue;
|
||||
|
||||
if (part.generationMethod === "manual") {
|
||||
const manualValue = manualValues?.[manualIndex] || "";
|
||||
manualIndex++;
|
||||
if (manualValue) {
|
||||
prefixParts.push(manualValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ... 나머지 기존 로직 (text, date, category, reference 등) 그대로 유지 ...
|
||||
}
|
||||
|
||||
return prefixParts.join("|");
|
||||
}
|
||||
```
|
||||
|
||||
**하위 호환성**: `manualValues`는 optional. `previewCode`(L1091)는 전달하지 않으므로 동작 변화 없음.
|
||||
|
||||
### 3. `allocateCode` 수정 - 수동 값 추출 순서 변경 + 폴백 정리
|
||||
|
||||
**위치**: L1290-1584
|
||||
**핵심 변경 2가지**:
|
||||
|
||||
(A) 기존에는 `buildPrefixKey`(L1306) → 수동 값 추출(L1332) 순서였으나, **수동 값 추출 → `buildPrefixKey`** 순서로 변경.
|
||||
|
||||
(B) 코드 조합 단계(L1448-1454)에서 `manualConfig.value` 폴백 제거.
|
||||
|
||||
```typescript
|
||||
async allocateCode(ruleId, companyCode, formData?, userInputCode?) {
|
||||
// ... 규칙 조회 ...
|
||||
|
||||
// 1단계: 수동 파트 값 추출 (buildPrefixKey 호출 전에 수행)
|
||||
const manualParts = rule.parts.filter(p => p.generationMethod === "manual");
|
||||
let extractedManualValues: string[] = [];
|
||||
|
||||
if (manualParts.length > 0 && userInputCode) {
|
||||
extractedManualValues = await this.extractManualValuesFromInput(
|
||||
rule, userInputCode, formData
|
||||
);
|
||||
|
||||
// 폴백: 추출 실패 시 userInputCode 전체를 수동 값으로 사용
|
||||
if (extractedManualValues.length === 0 && manualParts.length === 1) {
|
||||
extractedManualValues = [userInputCode];
|
||||
}
|
||||
}
|
||||
|
||||
// 2단계: 수동 값을 포함하여 prefix_key 빌드
|
||||
const prefixKey = await this.buildPrefixKey(rule, formData, extractedManualValues);
|
||||
|
||||
// 3단계: 시퀀스 할당 (기존 로직 그대로)
|
||||
|
||||
// 4단계: 코드 조합 (manualConfig.value 폴백 제거)
|
||||
// 기존: extractedManualValues[i] || part.manualConfig?.value || ""
|
||||
// 변경: extractedManualValues[i] || ""
|
||||
}
|
||||
```
|
||||
|
||||
### 4. `extractManualValuesFromInput` 헬퍼 분리 + 템플릿 정합성 복원
|
||||
|
||||
기존 `allocateCode` 내부의 수동 값 추출 로직(L1332-1442)을 별도 private 메서드로 추출.
|
||||
로직 자체는 변경 없음, 위치만 이동.
|
||||
카테고리/참조 파트의 빈 값 처리를 실제 코드 생성과 일치시킴.
|
||||
|
||||
```typescript
|
||||
private async extractManualValuesFromInput(
|
||||
rule: NumberingRuleConfig,
|
||||
userInputCode: string,
|
||||
formData?: Record<string, any>
|
||||
): Promise<string[]> {
|
||||
// 기존 L1332-1442의 로직을 그대로 이동
|
||||
// 변경: 카테고리/참조 빈 값 시 "CATEGORY"/"REF" 대신 "" 반환
|
||||
// → 템플릿이 실제 코드 구조와 일치 → 추출 성공률 향상
|
||||
}
|
||||
```
|
||||
|
||||
### 5. DB 마이그레이션 - BULK1 유령 기본값 제거
|
||||
|
||||
**파일**: `db/migrations/1053_remove_bulk1_manual_config_value.sql`
|
||||
|
||||
`numbering_rule_parts.manual_config` 컬럼에서 `value` 키를 제거합니다.
|
||||
|
||||
```sql
|
||||
-- manual_config에서 "value" 키 제거 (BULK1 유령 기본값 정리)
|
||||
UPDATE numbering_rule_parts
|
||||
SET manual_config = manual_config - 'value'
|
||||
WHERE generation_method = 'manual'
|
||||
AND manual_config ? 'value'
|
||||
AND manual_config->>'value' = 'BULK1';
|
||||
```
|
||||
|
||||
> PostgreSQL JSONB 연산자 `-`를 사용하여 특정 키만 제거.
|
||||
> `manual_config`의 나머지 필드(`placeholder` 등)는 유지됨.
|
||||
> "BULK1" 값을 가진 레코드만 대상으로 하여 안전성 확보.
|
||||
|
||||
---
|
||||
|
||||
## 설계 원칙
|
||||
|
||||
- **변경 범위 최소화**: `numberingRuleService.ts` 코드 변경 + DB 마이그레이션 1건
|
||||
- **이중 조치**: 코드에서 `manualConfig.value` 폴백 제거 + DB에서 유령 값 정리
|
||||
- `buildPrefixKey`의 `manualValues`는 optional → 기존 호출부(`previewCode` 등)에 영향 없음
|
||||
- `allocateCode` 내부 로직 순서만 변경 (추출 → prefix_key 빌드), 새 로직 추가 아님
|
||||
- 수동 값 추출 로직은 기존 코드를 헬퍼로 분리할 뿐, 로직 자체는 변경 없음
|
||||
- DB 마이그레이션은 "BULK1" 값만 정확히 타겟팅하여 부작용 방지
|
||||
- `TextInputComponent.tsx` 변경 불필요 (현재 동작이 올바름)
|
||||
- 프론트엔드 변경 없음 → 프론트엔드 테스트 불필요
|
||||
- `joinPartsWithSeparators`는 연속 구분자만 방지, 기존 구분자 구조 유지
|
||||
- 템플릿 카테고리/참조 빈 값을 실제 코드와 일치시켜 추출 성공률 향상
|
||||
|
||||
---
|
||||
|
||||
## 실시간 순번 미리보기 (추가 기능)
|
||||
|
||||
### 배경
|
||||
|
||||
품목 등록 모달에서 수동 입력 세그먼트 우측에 표시되는 순번(suffix)이 입력값과 무관하게 고정되어 있었음. 사용자가 "ㅇㅇ"을 입력하면 해당 접두어로 이미 몇 개가 등록되었는지에 따라 순번이 달라져야 함.
|
||||
|
||||
### 목표 동작
|
||||
|
||||
```
|
||||
모달 열림 : -[입력하시오]-005 (startFrom=5 기반 기본 순번)
|
||||
"ㅇㅇ" 입력 : -[ㅇㅇ]-005 (기존 "ㅇㅇ" 등록 0건)
|
||||
저장 후 재입력 "ㅇㅇ": -[ㅇㅇ]-006 (기존 "ㅇㅇ" 등록 1건)
|
||||
```
|
||||
|
||||
### 아키텍처
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 사용자
|
||||
participant V2 as V2Input
|
||||
participant API as previewNumberingCode
|
||||
participant BE as numberingRuleService.previewCode
|
||||
participant DB as numbering_rule_sequences
|
||||
|
||||
User->>V2: 수동 입력 "ㅇㅇ"
|
||||
Note over V2: 디바운스 300ms
|
||||
V2->>API: preview(ruleId, formData, "ㅇㅇ")
|
||||
API->>BE: previewCode(ruleId, companyCode, formData, "ㅇㅇ")
|
||||
BE->>BE: buildPrefixKey(rule, formData, ["ㅇㅇ"])
|
||||
Note over BE: prefix_key = "카테고리|ㅇㅇ"
|
||||
BE->>DB: getSequenceForPrefix(prefix_key)
|
||||
DB-->>BE: currentSeq = 0
|
||||
Note over BE: nextSequence = 0 + startFrom(5) = 5
|
||||
BE-->>API: "-____-005"
|
||||
API-->>V2: generatedCode
|
||||
V2->>V2: suffix = "-005" 갱신
|
||||
Note over V2: 화면 표시: -[ㅇㅇ]-005
|
||||
```
|
||||
|
||||
### 변경 내용
|
||||
|
||||
1. **백엔드 컨트롤러**: preview 엔드포인트가 `req.body.manualInputValue` 수신
|
||||
2. **백엔드 서비스**: `previewCode`가 `manualInputValue`를 받아 `buildPrefixKey`에 전달 → 접두어별 정확한 시퀀스 조회
|
||||
3. **백엔드 서비스**: 수동 파트가 있는데 `manualInputValue`가 없는 초기 상태 → 레거시 공용 시퀀스 조회 건너뜀, `currentSeq = 0` 사용 → `startFrom` 기본값 표시
|
||||
4. **프론트엔드 API**: `previewNumberingCode`에 `manualInputValue` 파라미터 추가
|
||||
5. **V2Input**: `manualInputValue` 변경 시 디바운스(300ms) preview API 재호출 → `numberingTemplateRef` 갱신 → suffix 실시간 업데이트
|
||||
6. **V2Input**: 카테고리 변경 시 초기 useEffect에서도 현재 `manualInputValue`를 preview에 전달 → 카테고리 변경/삭제 시 순번 즉시 반영
|
||||
7. **코드 정리**: 카테고리 해석 로직 3곳 중복 → `resolveCategoryFormat` 헬퍼로 통합 (약 100줄 감소)
|
||||
@@ -0,0 +1,161 @@
|
||||
# [맥락노트] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성
|
||||
|
||||
> 관련 문서: [계획서](./MPN[계획]-품번-수동접두어채번.md) | [체크리스트](./MPN[체크]-품번-수동접두어채번.md)
|
||||
|
||||
---
|
||||
|
||||
## 왜 이 작업을 하는가
|
||||
|
||||
- 기준정보 - 품목정보 등록 모달에서 품번 인풋에 사용자가 값을 입력해도 무시되고 "BULK1"로 저장됨
|
||||
- 서로 다른 접두어("ㅁㅁㅁ", "ㅇㅇㅇ")를 입력해도 전부 같은 시퀀스 카운터를 공유함
|
||||
- 카테고리 미선택 시 `--제발-015-003` 처럼 연속 구분자가 발생함
|
||||
- 사용자 입력이 반영되고, 접두어별로 독립된 순번이 부여되어야 함
|
||||
|
||||
---
|
||||
|
||||
## 핵심 결정 사항과 근거
|
||||
|
||||
### 1. 수동 값 추출을 buildPrefixKey 전으로 이동
|
||||
|
||||
- **결정**: `allocateCode` 내부에서 수동 값 추출 → buildPrefixKey 순서로 변경
|
||||
- **근거**: 기존에는 buildPrefixKey(L1306)가 먼저 실행된 후 수동 값 추출(L1332)이 진행됨. 수동 값이 prefix_key에 포함되려면 추출이 먼저 되어야 함
|
||||
- **대안 검토**: buildPrefixKey 내부에서 직접 추출 → 기각 (역할 분리 위반, previewCode 호출에도 영향)
|
||||
|
||||
### 2. buildPrefixKey에 수동 파트 값 포함
|
||||
|
||||
- **결정**: `manualValues` optional 파라미터 추가, 전달되면 prefix_key에 포함
|
||||
- **근거**: 기존 `continue`(L85-87)로 수동 파트가 prefix_key에서 제외되어 모든 접두어가 같은 시퀀스를 공유함
|
||||
- **하위호환**: optional 파라미터이므로 `previewCode`(L1091) 등 기존 호출부는 영향 없음
|
||||
|
||||
### 3. 템플릿 파싱 실패 시 userInputCode 전체를 수동 값으로 사용
|
||||
|
||||
- **결정**: 수동 파트가 1개이고 템플릿 기반 추출이 실패하면 `userInputCode` 전체를 수동 값으로 사용
|
||||
- **근거**: 사용자가 "ㅁㅁㅁ"처럼 접두어 부분만 입력하면 템플릿 "카테고리값-____-XXX"와 불일치. `startsWith` 조건 실패로 추출이 안 됨. 이 경우 입력 전체가 수동 값임
|
||||
- **제한**: 수동 파트가 2개 이상이면 이 폴백 불가 (어디서 분리할지 알 수 없음)
|
||||
|
||||
### 4. 코드 조합에서 manualConfig.value 폴백 제거
|
||||
|
||||
- **결정**: `extractedManualValues[i] || part.manualConfig?.value || ""` → `extractedManualValues[i] || ""`
|
||||
- **근거**: `manualConfig.value`는 UI에서 입력/편집할 수 없는 유령 필드. `ManualConfigPanel.tsx`에 `value` 입력란이 없어 DB에 한번 저장되면 스프레드 연산자로 계속 보존됨
|
||||
- **이중 조치**: 코드에서 폴백 제거 + DB 마이그레이션으로 기존 "BULK1" 값 정리
|
||||
|
||||
### 5. DB 마이그레이션은 BULK1만 타겟팅
|
||||
|
||||
- **결정**: `manual_config->>'value' = 'BULK1'` 조건으로 한정
|
||||
- **근거**: 다른 value가 의도적으로 설정된 경우가 있을 수 있음. 확인된 문제("BULK1")만 정리하여 부작용 방지
|
||||
- **대안 검토**: 전체 `manual_config.value` 키 제거 → 보류 (운영 판단 필요)
|
||||
|
||||
### 6. extractManualValuesFromInput 헬퍼 분리
|
||||
|
||||
- **결정**: 기존 `allocateCode` 내부의 수동 값 추출 로직(L1332-1442)을 별도 private 메서드로 추출
|
||||
- **근거**: 추출 로직이 약 110줄로 `allocateCode`가 과도하게 비대함. 헬퍼로 분리하면 순서 변경도 자연스러움
|
||||
- **원칙**: 로직 자체는 변경 없음, 위치만 이동 (구조적 변경과 행위적 변경 분리)
|
||||
|
||||
### 7. 프론트엔드 변경 불필요
|
||||
|
||||
- **결정**: 프론트엔드 코드 수정 없음
|
||||
- **근거**: `_numberingRuleId`가 사용자 입력 시에도 유지되고 있음 확인. `buttonActions.ts`가 정상적으로 `allocateCode`를 호출함. 문제는 백엔드 로직에만 있음
|
||||
|
||||
### 8. joinPartsWithSeparators 연속 구분자 방지
|
||||
|
||||
- **결정**: 빈 파트 뒤에 이미 같은 구분자가 있으면 중복 추가하지 않음
|
||||
- **근거**: 카테고리가 비면 파트 값 `""` + 구분자 `-`가 반복되어 `--` 발생. 구분자 구조(`-ㅁㅁㅁ-001`)는 유지하되 연속(`--`)만 방지
|
||||
- **조건**: `if (val || !result.endsWith(sep))` — 값이 있으면 항상 추가, 값이 없으면 이미 같은 구분자로 끝나면 스킵
|
||||
|
||||
### 9. 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경
|
||||
|
||||
- **결정**: `extractManualValuesFromInput` 내부의 카테고리/참조 빈 값 반환을 `"CATEGORY"`/`"REF"` → `""`로 변경
|
||||
- **근거**: 실제 코드 생성에서 빈 카테고리는 `""`인데 템플릿에서 `"CATEGORY"`를 쓰면 구조 불일치로 추출 실패. 로그로 확인: `userInputCode=-제발-015, previewTemplate=CATEGORY-____-XXX, extractedManualValues=[]`
|
||||
- **카테고리 있을 때**: `catMapping2?.format` 반환은 수정 전후 동일하여 영향 없음
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일 위치
|
||||
|
||||
| 구분 | 파일 경로 | 설명 |
|
||||
|------|----------|------|
|
||||
| 수정 대상 | `backend-node/src/services/numberingRuleService.ts` | joinPartsWithSeparators(L36), buildPrefixKey(L75), extractManualValuesFromInput(신규), allocateCode(L1296) |
|
||||
| 신규 생성 | `db/migrations/1053_remove_bulk1_manual_config_value.sql` | BULK1 유령 값 정리 마이그레이션 |
|
||||
| 변경 없음 | `frontend/components/screen/widgets/TextInputComponent.tsx` | _numberingRuleId 유지 확인 완료 |
|
||||
| 변경 없음 | `frontend/lib/registry/components/numbering-rule/config.ts` | 채번 설정 레지스트리 |
|
||||
| 변경 없음 | `frontend/components/screen/config-panels/NumberConfigPanel.tsx` | 채번 규칙 설정 패널 |
|
||||
| 참고 | `backend-node/src/controllers/numberingRuleController.ts` | allocateNumberingCode 컨트롤러 |
|
||||
|
||||
---
|
||||
|
||||
## 기술 참고
|
||||
|
||||
### allocateCode 실행 순서 (변경 전 → 후)
|
||||
|
||||
```
|
||||
변경 전: buildPrefixKey(L1306) → 시퀀스 할당 → 수동 값 추출(L1332) → 코드 조합
|
||||
변경 후: 수동 값 추출 → buildPrefixKey(수동 값 포함) → 시퀀스 할당 → 코드 조합
|
||||
```
|
||||
|
||||
### prefix_key 구성 (변경 전 → 후)
|
||||
|
||||
```
|
||||
변경 전: "카테고리값" (수동 파트 무시, 모든 접두어가 같은 키)
|
||||
변경 후: "카테고리값|ㅁㅁㅁ" (수동 파트 포함, 접두어별 독립 키)
|
||||
```
|
||||
|
||||
### 폴백 체인 (변경 전 → 후)
|
||||
|
||||
```
|
||||
변경 전: extractedManualValues[i] || manualConfig.value || ""
|
||||
변경 후: extractedManualValues[i] || ""
|
||||
```
|
||||
|
||||
### joinPartsWithSeparators 연속 구분자 방지 (변경 전 → 후)
|
||||
|
||||
```
|
||||
변경 전: "" + "-" + "" + "-" + "ㅁㅁㅁ" → "--ㅁㅁㅁ"
|
||||
변경 후: "" + "-" (이미 "-"로 끝남, 스킵) + "ㅁㅁㅁ" → "-ㅁㅁㅁ"
|
||||
```
|
||||
|
||||
### 템플릿 정합성 (변경 전 → 후)
|
||||
|
||||
```
|
||||
변경 전: 카테고리 비었을 때 템플릿 = "CATEGORY-____-XXX" / 입력 = "-제발-015" → 불일치 → 추출 실패
|
||||
변경 후: 카테고리 비었을 때 템플릿 = "-____-XXX" / 입력 = "-제발-015" → 일치 → 추출 성공
|
||||
```
|
||||
|
||||
### 10. 실시간 순번 미리보기 구현 방식
|
||||
|
||||
- **결정**: V2Input에서 `manualInputValue` 변경 시 디바운스(300ms)로 preview API를 재호출하여 suffix(순번)를 갱신
|
||||
- **근거**: 기존 preview API는 `manualInputValue` 없이 호출되어 모든 접두어가 같은 기본 순번을 표시함. 접두어별 정확한 순번을 보여주려면 preview 시점에도 수동 값을 전달하여 해당 prefix_key의 시퀀스를 조회해야 함
|
||||
- **대안 검토**: 프론트엔드에서 카운트 API를 별도 호출 → 기각 (기존 `previewCode` 흐름 재사용이 프로젝트 관행에 부합)
|
||||
- **디바운스 300ms**: 사용자 타이핑 중 과도한 API 호출 방지. 프로젝트 기존 패턴(검색 디바운스 등)과 동일
|
||||
|
||||
### 11. previewCode에 manualInputValue 전달
|
||||
|
||||
- **결정**: `previewCode` 시그니처에 `manualInputValue?: string` 추가, `buildPrefixKey`에 `[manualInputValue]`로 전달
|
||||
- **근거**: `buildPrefixKey`가 이미 `manualValues` optional 파라미터를 지원하므로 자연스럽게 확장 가능. 순번 조회 시 접두어별 독립 시퀀스를 정확히 반영함
|
||||
- **하위호환**: optional 파라미터이므로 기존 호출(`formData`만 전달)에 영향 없음
|
||||
|
||||
### 12. 초기 상태에서 레거시 시퀀스 조회 방지
|
||||
|
||||
- **결정**: `previewCode`에서 수동 파트가 있는데 `manualInputValue`가 없으면 시퀀스 조회를 건너뛰고 `currentSeq = 0` 사용
|
||||
- **근거**: 수정 전에는 모든 할당이 수동 파트 없는 공용 prefix_key를 사용했으므로 레거시 시퀀스가 누적되어 있음(예: 16). 모달 초기 상태에서 이 공용 키를 조회하면 `-016`이 표시됨. 아직 어떤 접두어인지 모르는 상태이므로 `startFrom` 기본값을 보여주는 것이 정확함
|
||||
- **`currentSeq = 0` + `startFrom`**: `nextSequence = 0 + startFrom(5) = 5` → `-005` 표시. 사용자가 입력하면 디바운스 preview가 해당 접두어의 실제 시퀀스를 조회
|
||||
|
||||
### 13. 카테고리 변경 시 수동 입력값 포함하여 순번 재조회
|
||||
|
||||
- **결정**: 초기 useEffect(카테고리 변경 트리거)에서 `previewNumberingCode` 호출 시 현재 `manualInputValue`도 함께 전달
|
||||
- **근거**: 카테고리를 바꾸거나 삭제하면 prefix_key가 달라지므로 순번도 달라져야 함. 기존에는 입력값 변경과 카테고리 변경이 별도 트리거여서 카테고리 변경 시 수동 값이 누락됨
|
||||
- **빈 입력값 처리**: `manualInputValue || undefined`로 처리하여 빈 문자열일 때는 기존처럼 `skipSequenceLookup` 작동
|
||||
|
||||
### 14. 카테고리 해석 로직 resolveCategoryFormat 헬퍼 통합
|
||||
|
||||
- **결정**: `previewCode`, `allocateCode`, `extractManualValuesFromInput` 3곳에 복붙된 카테고리 매핑 해석 로직을 `resolveCategoryFormat` private 메서드로 추출
|
||||
- **근거**: 동일 로직 약 50줄이 3곳에 복사되어 있었음 (변수명만 pool2/ct2/cc2 등으로 다름). 한 곳을 수정하면 나머지도 동일하게 수정해야 하는 유지보수 위험
|
||||
- **원칙**: 구조적 변경만 수행 (로직 변경 없음)
|
||||
|
||||
### BULK1이 DB에 남아있는 이유
|
||||
|
||||
```
|
||||
ManualConfigPanel.tsx: placeholder 입력란만 존재 (value 입력란 없음)
|
||||
플레이스홀더 수정 시: { ...existingConfig, placeholder: newValue }
|
||||
→ 기존 config에 value: "BULK1"이 있으면 스프레드로 계속 보존됨
|
||||
→ UI에서 제거 불가능한 유령 값
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
# [체크리스트] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성
|
||||
|
||||
> 관련 문서: [계획서](./MPN[계획]-품번-수동접두어채번.md) | [맥락노트](./MPN[맥락]-품번-수동접두어채번.md)
|
||||
|
||||
---
|
||||
|
||||
## 공정 상태
|
||||
|
||||
- 전체 진행률: **100%** (전체 완료)
|
||||
- 현재 단계: 완료
|
||||
|
||||
---
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### 1단계: 구조적 변경 (행위 변경 없음)
|
||||
|
||||
- [x] `numberingRuleService.ts`에서 수동 값 추출 로직을 `extractManualValuesFromInput` private 메서드로 분리
|
||||
- [x] 기존 `allocateCode` 내부에서 분리한 메서드 호출로 교체
|
||||
- [x] 기존 동작과 동일한지 확인 (구조적 변경만, 행위 변경 없음)
|
||||
|
||||
### 2단계: buildPrefixKey 수정
|
||||
|
||||
- [x] `buildPrefixKey` 시그니처에 `manualValues?: string[]` 파라미터 추가
|
||||
- [x] 수동 파트 처리 로직 변경: `continue` → `manualValues`에서 값 꺼내 `prefixParts`에 추가
|
||||
- [x] `previewCode` 호출부에 영향 없음 확인 (optional 파라미터)
|
||||
|
||||
### 3단계: allocateCode 순서 변경 + 폴백 정리
|
||||
|
||||
- [x] 수동 값 추출 로직을 `buildPrefixKey` 호출 전으로 이동
|
||||
- [x] 수동 파트 1개 + 추출 실패 시 `userInputCode` 전체를 수동 값으로 사용하는 폴백 추가
|
||||
- [x] `buildPrefixKey` 호출 시 `extractedManualValues`를 세 번째 인자로 전달
|
||||
- [x] 코드 조합 단계에서 `part.manualConfig?.value` 폴백 제거
|
||||
|
||||
### 4단계: DB 마이그레이션
|
||||
|
||||
- [x] `db/migrations/1053_remove_bulk1_manual_config_value.sql` 작성
|
||||
- [x] `manual_config->>'value' = 'BULK1'` 조건으로 JSONB에서 `value` 키 제거
|
||||
- [x] 마이그레이션 실행 (9건 정리 완료)
|
||||
|
||||
### 5단계: 연속 구분자(--) 방지
|
||||
|
||||
- [x] `joinPartsWithSeparators`에서 빈 파트 뒤 연속 구분자 방지 로직 추가
|
||||
- [x] `extractManualValuesFromInput`에서 카테고리/참조 빈 값 시 `""` 반환 (템플릿 정합성)
|
||||
|
||||
### 6단계: 검증
|
||||
|
||||
- [x] 카테고리 선택 + 수동입력 "ㅁㅁㅁ" → 카테고리값-ㅁㅁㅁ-001 생성 확인
|
||||
- [x] 카테고리 미선택 + 수동입력 "ㅁㅁㅁ" → -ㅁㅁㅁ-001 생성 확인 (-- 아님)
|
||||
- [x] 같은 접두어 "ㅁㅁㅁ" 재등록 → -ㅁㅁㅁ-002 순번 증가 확인
|
||||
- [x] 다른 접두어 "ㅇㅇㅇ" 등록 → -ㅇㅇㅇ-001 독립 시퀀스 확인
|
||||
- [x] 수동 파트 없는 채번 규칙 동작 영향 없음 확인
|
||||
- [x] previewCode (미리보기) 동작 영향 없음 확인
|
||||
- [x] BULK1이 더 이상 생성되지 않음 확인
|
||||
|
||||
### 7단계: 실시간 순번 미리보기
|
||||
|
||||
- [x] 백엔드 컨트롤러: preview 엔드포인트에 `manualInputValue` body 파라미터 수신 추가
|
||||
- [x] 백엔드 서비스: `previewCode`에 `manualInputValue` 파라미터 추가, `buildPrefixKey`에 전달
|
||||
- [x] 프론트엔드 API: `previewNumberingCode`에 `manualInputValue` 파라미터 추가
|
||||
- [x] V2Input: `manualInputValue` 변경 시 디바운스(300ms) preview API 호출 + suffix 갱신
|
||||
- [x] 백엔드 서비스: 초기 상태(수동 입력 없음) 시 레거시 공용 시퀀스 조회 건너뜀 → startFrom 기본값 표시
|
||||
- [x] V2Input: 카테고리 변경 시 초기 useEffect에서도 `manualInputValue` 전달 → 순번 즉시 반영
|
||||
- [x] 린트 에러 없음 확인
|
||||
|
||||
### 8단계: 코드 정리
|
||||
|
||||
- [x] 카테고리 해석 로직 3곳 중복 → `resolveCategoryFormat` 헬퍼 추출 (약 100줄 감소)
|
||||
- [x] 임시 변수명 정리 (pool2/ct2/cc2 등 복붙 흔적 제거)
|
||||
- [x] 린트 에러 없음 확인
|
||||
|
||||
### 9단계: 정리
|
||||
|
||||
- [x] 계획서/맥락노트/체크리스트 최신화
|
||||
|
||||
---
|
||||
|
||||
## 알려진 이슈 (보류)
|
||||
|
||||
| 이슈 | 설명 | 상태 |
|
||||
|------|------|------|
|
||||
| 저장 실패 시 순번 갭 | allocateCode와 saveFormData가 별도 트랜잭션이라 저장 실패해도 순번 소비됨 | 보류 |
|
||||
| 유령 데이터 | 중복 품명으로 간헐적 저장 성공 + 리스트 미노출 | 보류 |
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 내용 |
|
||||
|------|------|
|
||||
| 2026-03-11 | 계획서, 맥락노트, 체크리스트 작성 완료 |
|
||||
| 2026-03-11 | 1-4단계 구현 완료 |
|
||||
| 2026-03-11 | 5단계 추가 구현 (연속 구분자 방지 + 템플릿 정합성 복원) |
|
||||
| 2026-03-11 | 계맥체 최신화 완료. 문제 4-5 보류 |
|
||||
| 2026-03-12 | 7단계 실시간 순번 미리보기 구현 완료 (백엔드/프론트엔드 4파일) |
|
||||
| 2026-03-12 | 계맥체 최신화 완료 |
|
||||
| 2026-03-12 | 초기 상태 레거시 시퀀스 조회 방지 수정 + 계맥체 반영 |
|
||||
| 2026-03-12 | 카테고리 변경 시 수동 입력값 포함 순번 재조회 수정 |
|
||||
| 2026-03-12 | resolveCategoryFormat 헬퍼 추출 코드 정리 + 계맥체 최신화 |
|
||||
| 2026-03-12 | 6단계 검증 완료. 전체 완료 |
|
||||
Reference in New Issue
Block a user