Compare commits
2 Commits
4ab2761c82
...
17e212118c
| Author | SHA1 | Date |
|---|---|---|
|
|
17e212118c | |
|
|
152558d593 |
|
|
@ -182,12 +182,43 @@ interface ColumnConfig {
|
|||
|
||||
#### 주요 특징
|
||||
|
||||
- 좌측: 마스터 목록 (리스트/테이블 모드)
|
||||
- 좌측: 마스터 목록 (리스트/테이블/커스텀 모드)
|
||||
- 우측: 디테일 정보 (연관 데이터)
|
||||
- 좌우 비율 조절 가능 (드래그 리사이즈)
|
||||
- 다중 탭 지원 (우측 패널)
|
||||
- N:M 관계 데이터 지원
|
||||
- 중복 제거 기능
|
||||
- **커스텀 모드 (NEW)**: 패널 내부에 자유롭게 컴포넌트 배치
|
||||
|
||||
#### 커스텀 모드 (displayMode: "custom")
|
||||
|
||||
패널 내부에 다양한 컴포넌트를 자유롭게 배치할 수 있는 모드입니다. **v2-tabs-widget과 동일한 구조**로 구현되어 있습니다.
|
||||
|
||||
**특징:**
|
||||
- 패널 내부에 버튼, 테이블, 입력 필드 등 모든 V2 컴포넌트 배치 가능
|
||||
- 드래그앤드롭으로 컴포넌트 이동
|
||||
- 리사이즈 핸들로 크기 조절
|
||||
- **컴포넌트 클릭 시 좌측 설정 패널에서 해당 컴포넌트 속성 편집**
|
||||
- 디자인 모드에서 실제 컴포넌트가 렌더링되어 미리보기 가능
|
||||
|
||||
**사용 방법:**
|
||||
1. 좌측/우측 패널의 `displayMode`를 `"custom"`으로 설정
|
||||
2. 컴포넌트 팔레트에서 원하는 컴포넌트를 패널로 드래그
|
||||
3. 배치된 컴포넌트 클릭 → 좌측 패널에서 속성 편집
|
||||
4. 드래그 핸들(상단)로 이동, 모서리 핸들로 크기 조절
|
||||
|
||||
**컴포넌트 구조:**
|
||||
```typescript
|
||||
interface PanelInlineComponent {
|
||||
id: string;
|
||||
componentType: string;
|
||||
label?: string;
|
||||
position?: { x: number; y: number };
|
||||
size?: { width: number; height: number };
|
||||
componentConfig?: Record<string, any>;
|
||||
style?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 데이터 흐름
|
||||
|
||||
|
|
@ -220,7 +251,7 @@ interface SplitPanelLayoutConfig {
|
|||
tableName?: string;
|
||||
useCustomTable?: boolean;
|
||||
customTableName?: string;
|
||||
displayMode?: "list" | "table";
|
||||
displayMode?: "list" | "table" | "custom"; // 🆕 커스텀 모드 추가
|
||||
showSearch?: boolean;
|
||||
showAdd?: boolean;
|
||||
showEdit?: boolean;
|
||||
|
|
@ -228,15 +259,21 @@ interface SplitPanelLayoutConfig {
|
|||
columns?: ColumnConfig[];
|
||||
tableConfig?: TableDisplayConfig;
|
||||
dataFilter?: DataFilterConfig;
|
||||
|
||||
// 🆕 커스텀 모드 전용: 내부 컴포넌트 배열
|
||||
components?: PanelInlineComponent[];
|
||||
};
|
||||
|
||||
// 우측 패널
|
||||
rightPanel: {
|
||||
title: string;
|
||||
tableName?: string;
|
||||
displayMode?: "list" | "table";
|
||||
displayMode?: "list" | "table" | "custom"; // 🆕 커스텀 모드 추가
|
||||
columns?: ColumnConfig[];
|
||||
|
||||
// 🆕 커스텀 모드 전용: 내부 컴포넌트 배열
|
||||
components?: PanelInlineComponent[];
|
||||
|
||||
// 관계 설정
|
||||
relation?: {
|
||||
type?: "join" | "detail";
|
||||
|
|
|
|||
|
|
@ -959,6 +959,29 @@ receiveData(data, config): Promise<void>
|
|||
**구독 이벤트**:
|
||||
- `refreshTable` - 내부 테이블 갱신
|
||||
|
||||
**커스텀 모드 (displayMode: "custom")**:
|
||||
- 패널 내부에 자유롭게 컴포넌트 배치 가능 (v2-tabs-widget과 동일 구조)
|
||||
- 컴포넌트 클릭 시 좌측 설정 패널에서 속성 편집
|
||||
- 드래그앤드롭으로 컴포넌트 이동, 리사이즈 핸들로 크기 조절
|
||||
- 디자인 모드에서 실제 컴포넌트 렌더링 (미리보기)
|
||||
|
||||
```typescript
|
||||
// 커스텀 모드 설정 예시
|
||||
leftPanel: {
|
||||
displayMode: "custom",
|
||||
components: [
|
||||
{
|
||||
id: "btn-1",
|
||||
componentType: "v2-button-primary",
|
||||
label: "저장",
|
||||
position: { x: 10, y: 10 },
|
||||
size: { width: 100, height: 40 },
|
||||
componentConfig: { buttonAction: "save" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 연동 가능한 조합
|
||||
|
|
|
|||
|
|
@ -0,0 +1,582 @@
|
|||
# 다음 구현 필요 컴포넌트 개발 계획
|
||||
|
||||
> **작성일**: 2026-01-30
|
||||
> **상태**: 계획 수립 완료
|
||||
> **우선순위**: v2-table-grouped (1순위) → v2-timeline-scheduler (2순위)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
생산계획관리 화면의 정식 버전 구현을 위해 필요한 2개의 신규 컴포넌트 개발 계획입니다.
|
||||
|
||||
| 컴포넌트 | 용도 | 난이도 | 예상 작업량 |
|
||||
|----------|------|:------:|:----------:|
|
||||
| `v2-table-grouped` | 그룹화 테이블 (접기/펼치기) | 중 | 2-3일 |
|
||||
| `v2-timeline-scheduler` | 타임라인/간트차트 스케줄러 | 상 | 5-7일 |
|
||||
|
||||
---
|
||||
|
||||
## 1. v2-table-grouped (그룹화 테이블)
|
||||
|
||||
### 1.1 컴포넌트 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| **컴포넌트 ID** | `v2-table-grouped` |
|
||||
| **카테고리** | DISPLAY |
|
||||
| **용도** | 데이터를 특정 컬럼 기준으로 그룹화하여 접기/펼치기 기능 제공 |
|
||||
| **기반 컴포넌트** | `v2-table-list` 확장 |
|
||||
| **참고 UI** | Excel 그룹화, VS Code 파일 그룹화 |
|
||||
|
||||
### 1.2 핵심 기능
|
||||
|
||||
| 기능 | 설명 | 우선순위 |
|
||||
|------|------|:--------:|
|
||||
| 그룹화 | 지정된 컬럼 기준으로 데이터 그룹핑 | 필수 |
|
||||
| 접기/펼치기 | 그룹 행 클릭 시 하위 항목 토글 | 필수 |
|
||||
| 그룹 요약 | 그룹별 합계/개수 표시 | 필수 |
|
||||
| 다중 그룹 | 여러 컬럼 기준 중첩 그룹화 | 선택 |
|
||||
| 그룹 선택 | 그룹 체크박스로 하위 전체 선택 | 필수 |
|
||||
| 전체 펼치기/접기 | 모든 그룹 일괄 토글 | 필수 |
|
||||
|
||||
### 1.3 UI 목업
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ [전체 펼치기] [전체 접기] [3개 그룹] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ▼ □ 품목A (P001) 수량: 150 3건 │
|
||||
│ ├─ □ 2026-01-15 생산계획001 50개 설비A │
|
||||
│ ├─ □ 2026-01-16 생산계획002 50개 설비B │
|
||||
│ └─ □ 2026-01-17 생산계획003 50개 설비A │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ► □ 품목B (P002) 수량: 200 2건 │ ← 접힌 상태
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ▼ □ 품목C (P003) 수량: 100 1건 │
|
||||
│ └─ □ 2026-01-18 생산계획004 100개 설비C │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.4 타입 정의 (types.ts)
|
||||
|
||||
```typescript
|
||||
import { ColumnConfig } from "../v2-table-list/types";
|
||||
|
||||
/**
|
||||
* 그룹화 설정
|
||||
*/
|
||||
export interface GroupConfig {
|
||||
/** 그룹화 기준 컬럼 */
|
||||
groupByColumn: string;
|
||||
|
||||
/** 그룹 표시 형식 (예: "{item_name} ({item_code})") */
|
||||
groupLabelFormat?: string;
|
||||
|
||||
/** 그룹 요약 설정 */
|
||||
summary?: {
|
||||
/** 합계 컬럼 */
|
||||
sumColumns?: string[];
|
||||
/** 개수 표시 여부 */
|
||||
showCount?: boolean;
|
||||
};
|
||||
|
||||
/** 초기 펼침 상태 */
|
||||
defaultExpanded?: boolean;
|
||||
|
||||
/** 중첩 그룹 (다중 그룹화) */
|
||||
nestedGroup?: GroupConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹화 테이블 설정
|
||||
*/
|
||||
export interface TableGroupedConfig {
|
||||
/** 테이블명 */
|
||||
selectedTable?: string;
|
||||
|
||||
/** 커스텀 테이블 사용 */
|
||||
useCustomTable?: boolean;
|
||||
customTableName?: string;
|
||||
|
||||
/** 그룹화 설정 */
|
||||
groupConfig: GroupConfig;
|
||||
|
||||
/** 컬럼 설정 (v2-table-list와 동일) */
|
||||
columns?: ColumnConfig[];
|
||||
|
||||
/** 체크박스 표시 */
|
||||
showCheckbox?: boolean;
|
||||
|
||||
/** 체크박스 모드 */
|
||||
checkboxMode?: "single" | "multi";
|
||||
|
||||
/** 페이지네이션 (그룹 단위) */
|
||||
pagination?: {
|
||||
enabled: boolean;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
/** 정렬 설정 */
|
||||
defaultSort?: {
|
||||
column: string;
|
||||
direction: "asc" | "desc";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 상태
|
||||
*/
|
||||
export interface GroupState {
|
||||
/** 그룹 키 (groupByColumn 값) */
|
||||
groupKey: string;
|
||||
|
||||
/** 펼침 여부 */
|
||||
expanded: boolean;
|
||||
|
||||
/** 그룹 내 데이터 */
|
||||
items: any[];
|
||||
|
||||
/** 그룹 요약 데이터 */
|
||||
summary?: Record<string, number>;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 파일 구조
|
||||
|
||||
```
|
||||
frontend/lib/registry/components/v2-table-grouped/
|
||||
├── index.ts # Definition (V2TableGroupedDefinition)
|
||||
├── types.ts # 타입 정의
|
||||
├── config.ts # 기본 설정값
|
||||
├── TableGroupedComponent.tsx # 메인 컴포넌트
|
||||
├── TableGroupedConfigPanel.tsx # 설정 패널
|
||||
├── TableGroupedRenderer.tsx # 레지스트리 등록
|
||||
├── components/
|
||||
│ ├── GroupHeader.tsx # 그룹 헤더 (펼치기/접기)
|
||||
│ ├── GroupSummary.tsx # 그룹 요약
|
||||
│ └── GroupCheckbox.tsx # 그룹 체크박스
|
||||
├── hooks/
|
||||
│ └── useGroupedData.ts # 그룹화 로직 훅
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 1.6 구현 단계
|
||||
|
||||
| 단계 | 작업 내용 | 예상 시간 |
|
||||
|:----:|----------|:---------:|
|
||||
| 1 | 타입 정의 및 기본 구조 생성 | 2시간 |
|
||||
| 2 | `useGroupedData` 훅 구현 (데이터 그룹화 로직) | 4시간 |
|
||||
| 3 | `GroupHeader` 컴포넌트 (펼치기/접기 UI) | 2시간 |
|
||||
| 4 | `TableGroupedComponent` 메인 구현 | 6시간 |
|
||||
| 5 | 그룹 체크박스 연동 | 2시간 |
|
||||
| 6 | 그룹 요약 (합계/개수) | 2시간 |
|
||||
| 7 | `TableGroupedConfigPanel` 설정 패널 | 4시간 |
|
||||
| 8 | 테스트 및 문서화 | 2시간 |
|
||||
|
||||
**총 예상: 24시간 (약 3일)**
|
||||
|
||||
### 1.7 v2-table-list와의 차이점
|
||||
|
||||
| 항목 | v2-table-list | v2-table-grouped |
|
||||
|------|---------------|------------------|
|
||||
| 데이터 구조 | 플랫 리스트 | 계층 구조 (그룹 > 아이템) |
|
||||
| 렌더링 | 행 단위 | 그룹 헤더 + 상세 행 |
|
||||
| 선택 | 개별 행 | 그룹 단위 / 개별 단위 |
|
||||
| 요약 | 전체 합계 (선택) | 그룹별 요약 |
|
||||
| 페이지네이션 | 행 단위 | 그룹 단위 |
|
||||
|
||||
---
|
||||
|
||||
## 2. v2-timeline-scheduler (타임라인 스케줄러)
|
||||
|
||||
### 2.1 컴포넌트 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| **컴포넌트 ID** | `v2-timeline-scheduler` |
|
||||
| **카테고리** | DISPLAY |
|
||||
| **용도** | 간트차트 형태의 일정/계획 시각화 및 편집 |
|
||||
| **참고 UI** | MS Project, Jira Timeline, dhtmlxGantt |
|
||||
| **외부 라이브러리** | 고려 중: `@tanstack/react-virtual` (가상 스크롤) |
|
||||
|
||||
### 2.2 핵심 기능
|
||||
|
||||
| 기능 | 설명 | 우선순위 |
|
||||
|------|------|:--------:|
|
||||
| 타임라인 그리드 | 날짜 기준 그리드 표시 (일/주/월) | 필수 |
|
||||
| 스케줄 바 | 시작~종료 기간 바 렌더링 | 필수 |
|
||||
| 리소스 행 | 설비/작업자별 행 구분 | 필수 |
|
||||
| 드래그 이동 | 스케줄 바 드래그로 날짜 변경 | 필수 |
|
||||
| 리사이즈 | 바 양쪽 핸들로 기간 조정 | 필수 |
|
||||
| 줌 레벨 | 일/주/월 단위 전환 | 필수 |
|
||||
| 충돌 표시 | 같은 리소스 시간 겹침 경고 | 선택 |
|
||||
| 진행률 표시 | 바 내부 진행률 표시 | 선택 |
|
||||
| 마일스톤 | 단일 일정 마커 | 선택 |
|
||||
|
||||
### 2.3 UI 목업
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ [◀ 이전] [오늘] [다음 ▶] 2026년 1월 [일] [주] [월] [+ 추가] │
|
||||
├────────────┬─────────────────────────────────────────────────────────────────┤
|
||||
│ │ 15(수) │ 16(목) │ 17(금) │ 18(토) │ 19(일) │ 20(월) │ 21(화) │
|
||||
├────────────┼─────────────────────────────────────────────────────────────────┤
|
||||
│ 설비A │ ████████████████ │
|
||||
│ │ [생산계획001] │ │
|
||||
├────────────┼─────────────────────────────────────────────────────────────────┤
|
||||
│ 설비B │ █████████████████████████ │
|
||||
│ │ [생산계획002 ] │
|
||||
├────────────┼─────────────────────────────────────────────────────────────────┤
|
||||
│ 설비C │ ████████████████ │
|
||||
│ │ [생산계획003] │
|
||||
└────────────┴─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
범례: ██ 진행중 ██ 완료 ██ 지연 ◆ 마일스톤
|
||||
```
|
||||
|
||||
### 2.4 타입 정의 (types.ts)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 줌 레벨
|
||||
*/
|
||||
export type ZoomLevel = "day" | "week" | "month";
|
||||
|
||||
/**
|
||||
* 스케줄 상태
|
||||
*/
|
||||
export type ScheduleStatus = "planned" | "in_progress" | "completed" | "delayed" | "cancelled";
|
||||
|
||||
/**
|
||||
* 스케줄 항목
|
||||
*/
|
||||
export interface ScheduleItem {
|
||||
/** 고유 ID */
|
||||
id: string;
|
||||
|
||||
/** 리소스 ID (설비/작업자) */
|
||||
resourceId: string;
|
||||
|
||||
/** 표시 제목 */
|
||||
title: string;
|
||||
|
||||
/** 시작 일시 */
|
||||
startDate: string; // ISO 8601 format
|
||||
|
||||
/** 종료 일시 */
|
||||
endDate: string;
|
||||
|
||||
/** 상태 */
|
||||
status: ScheduleStatus;
|
||||
|
||||
/** 진행률 (0-100) */
|
||||
progress?: number;
|
||||
|
||||
/** 색상 (CSS color) */
|
||||
color?: string;
|
||||
|
||||
/** 추가 데이터 */
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스 (행)
|
||||
*/
|
||||
export interface Resource {
|
||||
/** 리소스 ID */
|
||||
id: string;
|
||||
|
||||
/** 표시명 */
|
||||
name: string;
|
||||
|
||||
/** 그룹 (선택) */
|
||||
group?: string;
|
||||
|
||||
/** 아이콘 (선택) */
|
||||
icon?: string;
|
||||
|
||||
/** 용량 (선택, 충돌 계산용) */
|
||||
capacity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 타임라인 설정
|
||||
*/
|
||||
export interface TimelineSchedulerConfig {
|
||||
/** 테이블명 (스케줄 데이터) */
|
||||
selectedTable?: string;
|
||||
|
||||
/** 리소스 테이블명 */
|
||||
resourceTable?: string;
|
||||
|
||||
/** 필드 매핑 */
|
||||
fieldMapping: {
|
||||
id: string;
|
||||
resourceId: string;
|
||||
title: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status?: string;
|
||||
progress?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
/** 리소스 필드 매핑 */
|
||||
resourceFieldMapping?: {
|
||||
id: string;
|
||||
name: string;
|
||||
group?: string;
|
||||
};
|
||||
|
||||
/** 초기 줌 레벨 */
|
||||
defaultZoomLevel?: ZoomLevel;
|
||||
|
||||
/** 초기 표시 날짜 */
|
||||
initialDate?: string;
|
||||
|
||||
/** 편집 가능 여부 */
|
||||
editable?: boolean;
|
||||
|
||||
/** 드래그 이동 허용 */
|
||||
allowDrag?: boolean;
|
||||
|
||||
/** 리사이즈 허용 */
|
||||
allowResize?: boolean;
|
||||
|
||||
/** 충돌 체크 */
|
||||
checkConflicts?: boolean;
|
||||
|
||||
/** 상태별 색상 */
|
||||
statusColors?: Record<ScheduleStatus, string>;
|
||||
|
||||
/** 리소스 컬럼 너비 */
|
||||
resourceColumnWidth?: number;
|
||||
|
||||
/** 행 높이 */
|
||||
rowHeight?: number;
|
||||
|
||||
/** 셀 너비 (줌 레벨별) */
|
||||
cellWidth?: {
|
||||
day: number;
|
||||
week: number;
|
||||
month: number;
|
||||
};
|
||||
|
||||
/** 툴바 표시 */
|
||||
showToolbar?: boolean;
|
||||
|
||||
/** 범례 표시 */
|
||||
showLegend?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 핸들러
|
||||
*/
|
||||
export interface TimelineEvents {
|
||||
/** 스케줄 클릭 */
|
||||
onScheduleClick?: (schedule: ScheduleItem) => void;
|
||||
|
||||
/** 스케줄 더블클릭 */
|
||||
onScheduleDoubleClick?: (schedule: ScheduleItem) => void;
|
||||
|
||||
/** 드래그 완료 */
|
||||
onScheduleDrag?: (schedule: ScheduleItem, newStart: Date, newEnd: Date) => void;
|
||||
|
||||
/** 리사이즈 완료 */
|
||||
onScheduleResize?: (schedule: ScheduleItem, newStart: Date, newEnd: Date) => void;
|
||||
|
||||
/** 빈 영역 클릭 (새 스케줄 추가용) */
|
||||
onEmptyClick?: (resourceId: string, date: Date) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 파일 구조
|
||||
|
||||
```
|
||||
frontend/lib/registry/components/v2-timeline-scheduler/
|
||||
├── index.ts # Definition (V2TimelineSchedulerDefinition)
|
||||
├── types.ts # 타입 정의
|
||||
├── config.ts # 기본 설정값
|
||||
├── TimelineSchedulerComponent.tsx # 메인 컴포넌트
|
||||
├── TimelineSchedulerConfigPanel.tsx # 설정 패널
|
||||
├── TimelineSchedulerRenderer.tsx # 레지스트리 등록
|
||||
├── components/
|
||||
│ ├── TimelineHeader.tsx # 날짜 헤더
|
||||
│ ├── TimelineGrid.tsx # 그리드 배경
|
||||
│ ├── ResourceColumn.tsx # 리소스 컬럼 (좌측)
|
||||
│ ├── ScheduleBar.tsx # 스케줄 바 (드래그/리사이즈)
|
||||
│ ├── TimelineToolbar.tsx # 툴바 (줌, 네비게이션)
|
||||
│ ├── TimelineLegend.tsx # 범례
|
||||
│ └── ConflictIndicator.tsx # 충돌 표시
|
||||
├── hooks/
|
||||
│ ├── useTimelineState.ts # 타임라인 상태 관리
|
||||
│ ├── useScheduleDrag.ts # 드래그 로직
|
||||
│ ├── useScheduleResize.ts # 리사이즈 로직
|
||||
│ └── useDateCalculation.ts # 날짜/위치 계산
|
||||
├── utils/
|
||||
│ ├── dateUtils.ts # 날짜 유틸리티
|
||||
│ └── conflictDetection.ts # 충돌 감지
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 2.6 구현 단계
|
||||
|
||||
| 단계 | 작업 내용 | 예상 시간 |
|
||||
|:----:|----------|:---------:|
|
||||
| 1 | 타입 정의 및 기본 구조 생성 | 3시간 |
|
||||
| 2 | `TimelineHeader` (날짜 헤더, 줌 레벨) | 4시간 |
|
||||
| 3 | `TimelineGrid` (그리드 배경) | 3시간 |
|
||||
| 4 | `ResourceColumn` (리소스 목록) | 2시간 |
|
||||
| 5 | `ScheduleBar` 기본 렌더링 | 4시간 |
|
||||
| 6 | 드래그 이동 구현 | 6시간 |
|
||||
| 7 | 리사이즈 구현 | 4시간 |
|
||||
| 8 | 줌 레벨 전환 (일/주/월) | 3시간 |
|
||||
| 9 | 날짜 네비게이션 | 2시간 |
|
||||
| 10 | 충돌 감지 및 표시 | 4시간 |
|
||||
| 11 | 가상 스크롤 (대용량 데이터) | 4시간 |
|
||||
| 12 | `TimelineSchedulerConfigPanel` | 4시간 |
|
||||
| 13 | API 연동 (저장/로드) | 4시간 |
|
||||
| 14 | 테스트 및 문서화 | 3시간 |
|
||||
|
||||
**총 예상: 50시간 (약 6-7일)**
|
||||
|
||||
### 2.7 핵심 알고리즘
|
||||
|
||||
#### 날짜 → 픽셀 위치 변환
|
||||
|
||||
```typescript
|
||||
function dateToPosition(date: Date, viewStart: Date, cellWidth: number, zoomLevel: ZoomLevel): number {
|
||||
const diffMs = date.getTime() - viewStart.getTime();
|
||||
|
||||
switch (zoomLevel) {
|
||||
case "day":
|
||||
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
||||
return diffDays * cellWidth;
|
||||
case "week":
|
||||
const diffWeeks = diffMs / (1000 * 60 * 60 * 24 * 7);
|
||||
return diffWeeks * cellWidth;
|
||||
case "month":
|
||||
// 월 단위는 일수가 다르므로 별도 계산
|
||||
return calculateMonthPosition(date, viewStart, cellWidth);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 충돌 감지
|
||||
|
||||
```typescript
|
||||
function detectConflicts(schedules: ScheduleItem[], resourceId: string): ScheduleItem[][] {
|
||||
const resourceSchedules = schedules
|
||||
.filter(s => s.resourceId === resourceId)
|
||||
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
|
||||
|
||||
const conflicts: ScheduleItem[][] = [];
|
||||
|
||||
for (let i = 0; i < resourceSchedules.length; i++) {
|
||||
const current = resourceSchedules[i];
|
||||
const overlapping = resourceSchedules.filter(s =>
|
||||
s.id !== current.id &&
|
||||
new Date(s.startDate) < new Date(current.endDate) &&
|
||||
new Date(s.endDate) > new Date(current.startDate)
|
||||
);
|
||||
|
||||
if (overlapping.length > 0) {
|
||||
conflicts.push([current, ...overlapping]);
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 우선순위 및 일정
|
||||
|
||||
### 3.1 권장 순서
|
||||
|
||||
```
|
||||
1단계: v2-table-grouped (2-3일)
|
||||
↓
|
||||
2단계: v2-timeline-scheduler (5-7일)
|
||||
↓
|
||||
3단계: 생산계획관리 정식 버전 화면 구성 (1-2일)
|
||||
```
|
||||
|
||||
### 3.2 이유
|
||||
|
||||
1. **v2-table-grouped 먼저**:
|
||||
- `v2-table-list` 기반 확장으로 난이도 낮음
|
||||
- 생산계획 외 다른 화면(BOM, 수주 등)에서도 활용 가능
|
||||
- 타임라인 개발 중에도 테스트용으로 사용 가능
|
||||
|
||||
2. **v2-timeline-scheduler 나중**:
|
||||
- 복잡도가 높아 집중 개발 필요
|
||||
- 드래그/리사이즈 등 인터랙션 테스트 필요
|
||||
- 생산계획관리 전용 컴포넌트
|
||||
|
||||
### 3.3 체크리스트
|
||||
|
||||
#### v2-table-grouped
|
||||
|
||||
- [ ] 타입 정의 완료
|
||||
- [ ] 기본 구조 생성
|
||||
- [ ] useGroupedData 훅 구현
|
||||
- [ ] GroupHeader 컴포넌트
|
||||
- [ ] 메인 컴포넌트 구현
|
||||
- [ ] 그룹 체크박스 연동
|
||||
- [ ] 그룹 요약 (합계/개수)
|
||||
- [ ] 설정 패널 구현
|
||||
- [ ] 레지스트리 등록
|
||||
- [ ] 테스트 완료
|
||||
- [ ] 문서화
|
||||
|
||||
#### v2-timeline-scheduler
|
||||
|
||||
- [ ] 타입 정의 완료
|
||||
- [ ] 기본 구조 생성
|
||||
- [ ] TimelineHeader (날짜)
|
||||
- [ ] TimelineGrid (배경)
|
||||
- [ ] ResourceColumn (리소스)
|
||||
- [ ] ScheduleBar 기본 렌더링
|
||||
- [ ] 드래그 이동
|
||||
- [ ] 리사이즈
|
||||
- [ ] 줌 레벨 전환
|
||||
- [ ] 날짜 네비게이션
|
||||
- [ ] 충돌 감지
|
||||
- [ ] 가상 스크롤
|
||||
- [ ] 설정 패널 구현
|
||||
- [ ] API 연동
|
||||
- [ ] 레지스트리 등록
|
||||
- [ ] 테스트 완료
|
||||
- [ ] 문서화
|
||||
|
||||
---
|
||||
|
||||
## 4. 참고 자료
|
||||
|
||||
### 기존 V2 컴포넌트 참고
|
||||
|
||||
- `v2-table-list`: 테이블 렌더링, 체크박스, 페이지네이션
|
||||
- `v2-pivot-grid`: 복잡한 그리드 렌더링, 가상 스크롤
|
||||
- `v2-split-panel-layout`: 커스텀 모드 컴포넌트 배치
|
||||
|
||||
### 외부 라이브러리 검토
|
||||
|
||||
| 라이브러리 | 용도 | 고려 사항 |
|
||||
|----------|------|----------|
|
||||
| `@tanstack/react-virtual` | 가상 스크롤 | 이미 사용 중, 확장 용이 |
|
||||
| `date-fns` | 날짜 계산 | 이미 사용 중 |
|
||||
| `react-dnd` | 드래그앤드롭 | 검토 필요, 현재 네이티브 구현 |
|
||||
|
||||
### 관련 문서
|
||||
|
||||
- [생산계획관리 화면 설계](../03_production/production-plan.md)
|
||||
- [V2 컴포넌트 분석 가이드](../../V2_컴포넌트_분석_가이드.md)
|
||||
- [컴포넌트 개발 가이드](../../../frontend/docs/component-development-guide.md)
|
||||
|
||||
---
|
||||
|
||||
**작성자**: Claude AI
|
||||
**최종 수정**: 2026-01-30
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
|
||||
| ID | 이름 | 용도 | 주요 옵션 |
|
||||
|----|------|------|----------|
|
||||
| `v2-split-panel-layout` | 분할 패널 | 마스터-디테일 좌우 분할 | splitRatio, resizable, relation |
|
||||
| `v2-split-panel-layout` | 분할 패널 | 마스터-디테일 좌우 분할 | splitRatio, resizable, relation, **displayMode: custom** |
|
||||
| `v2-tabs-widget` | 탭 위젯 | 탭 전환, 탭 내 컴포넌트 | tabs, activeTabId |
|
||||
| `v2-section-card` | 섹션 카드 | 제목+테두리 그룹화 | title, collapsible, padding |
|
||||
| `v2-section-paper` | 섹션 페이퍼 | 배경색 그룹화 | backgroundColor, padding, shadow |
|
||||
|
|
@ -284,6 +284,45 @@
|
|||
}
|
||||
```
|
||||
|
||||
#### v2-split-panel-layout 커스텀 모드 (NEW)
|
||||
|
||||
패널 내부에 자유롭게 컴포넌트를 배치할 수 있습니다. (v2-tabs-widget과 동일 구조)
|
||||
|
||||
```typescript
|
||||
{
|
||||
leftPanel: {
|
||||
displayMode: "custom", // 커스텀 모드 활성화
|
||||
components: [ // 내부 컴포넌트 배열
|
||||
{
|
||||
id: "btn-save",
|
||||
componentType: "v2-button-primary",
|
||||
label: "저장",
|
||||
position: { x: 10, y: 10 },
|
||||
size: { width: 100, height: 40 },
|
||||
componentConfig: { buttonAction: "save" }
|
||||
},
|
||||
{
|
||||
id: "tbl-list",
|
||||
componentType: "v2-table-list",
|
||||
label: "목록",
|
||||
position: { x: 10, y: 60 },
|
||||
size: { width: 400, height: 300 },
|
||||
componentConfig: { selectedTable: "테이블명" }
|
||||
}
|
||||
]
|
||||
},
|
||||
rightPanel: {
|
||||
displayMode: "table" // 기존 모드 유지
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**디자인 모드 기능**:
|
||||
- 컴포넌트 클릭 → 좌측 설정 패널에서 속성 편집
|
||||
- 드래그 핸들(상단)로 이동
|
||||
- 리사이즈 핸들(모서리)로 크기 조절
|
||||
- 실제 컴포넌트 미리보기 렌더링
|
||||
|
||||
#### v2-card-display 필수 설정
|
||||
|
||||
```typescript
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
# 생산계획관리 (Production Plan Management)
|
||||
|
||||
> ⚠️ **중요 안내**: 이 화면은 **복합형 레이아웃** (좌우 분할 패널 + 타임라인 스케줄러)으로, 현재 V2 컴포넌트만으로는 완전한 구현이 불가능합니다. 아래 문서는 화면 분석 및 향후 구현 계획을 위한 참조용입니다.
|
||||
> **Screen ID**: /screens/{TBD}
|
||||
> **메뉴 경로**: 생산관리 > 생산계획관리
|
||||
|
||||
> ⚠️ **구현 상태 안내**
|
||||
> - **간소화 버전 (패턴 A)**: ✅ 즉시 구현 가능
|
||||
> - **정식 버전 (복합 레이아웃)**: ⚠️ 컴포넌트 개발 후 구현 가능
|
||||
> - ~~`v2-split-panel-layout` 커스텀 모드 확장~~ ✅ **구현 완료**
|
||||
> - `v2-table-grouped`, `v2-timeline-scheduler` 신규 개발 (2개)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -10,16 +17,61 @@
|
|||
|------|------|
|
||||
| **화면명** | 생산계획관리 |
|
||||
| **영문명** | Production Plan Management |
|
||||
| **화면 유형** | 복합형 (좌우 분할 패널 + 타임라인 스케줄러) |
|
||||
| **Screen ID** | /screens/{TBD} |
|
||||
| **메뉴 경로** | 생산관리 > 생산계획관리 |
|
||||
| **화면 유형 (전체)** | 복합형 (좌우 분할 패널 + 타임라인 스케줄러) - ⚠️ 컴포넌트 확장 후 구현 |
|
||||
| **화면 유형 (간소화)** | **패턴 A** (기본 마스터 화면) - ✅ 즉시 구현 가능 |
|
||||
| **메인 테이블** | `production_plan_mng` |
|
||||
| **관련 테이블** | `sales_order_mng`, `item_info`, `equipment_info`, `bom_info` |
|
||||
| **주요 기능** | 수주 기반 생산계획 수립, 타임라인 스케줄러, 자동 스케줄 생성, 반제품 계획 연동 |
|
||||
| **필요 작업** | ~~1. `v2-split-panel-layout` 확장~~ ✅ 완료 / 2. `v2-table-grouped` 개발 / 3. `v2-timeline-scheduler` 개발 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 구조 분석
|
||||
## 2. 테이블 선택 및 화면 구조
|
||||
|
||||
### 2.1 레이아웃 구조
|
||||
### 2.1 사용 테이블
|
||||
|
||||
| 테이블명 | 용도 | 비고 |
|
||||
|----------|------|------|
|
||||
| `production_plan_mng` | 생산계획 마스터 | 주 테이블 |
|
||||
| `sales_order_mng` | 수주 정보 | 참조 (수주 기반 계획) |
|
||||
| `item_info` | 품목 정보 | 참조 (품목코드, 품명) |
|
||||
| `equipment_info` | 설비 정보 | 참조 (설비 할당) |
|
||||
| `bom_info` | BOM 정보 | 참조 (반제품 계획 생성) |
|
||||
|
||||
### 2.2 테이블 관계
|
||||
|
||||
```
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ sales_order_mng │ │ production_plan_mng │
|
||||
├──────────────────────┤ ├──────────────────────┤
|
||||
│ order_no (PK) │──N:1──│ order_no (FK) │
|
||||
│ item_code │ │ id (PK) │
|
||||
│ ... │ │ plan_no │
|
||||
└──────────────────────┘ │ item_code │
|
||||
│ equipment_id (FK) │
|
||||
┌──────────────────────┐ │ parent_plan_id (FK) │
|
||||
│ item_info │ │ ... │
|
||||
├──────────────────────┤ └──────────────────────┘
|
||||
│ item_code (PK) │──1:N── │
|
||||
│ item_name │ │
|
||||
│ ... │ │
|
||||
└──────────────────────┘ │
|
||||
│
|
||||
┌──────────────────────┐ │
|
||||
│ equipment_info │ │
|
||||
├──────────────────────┤ │
|
||||
│ id (PK) │──1:N──────────┘
|
||||
│ equipment_code │
|
||||
│ equipment_name │
|
||||
│ ... │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 화면 구조 분석 (전체 버전)
|
||||
|
||||
#### 2.3.1 레이아웃 구조
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
|
|
@ -44,7 +96,7 @@
|
|||
└────────────────────────────────┴──┴──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 탭 구조
|
||||
#### 2.3.2 탭 구조
|
||||
|
||||
**왼쪽 패널 탭**:
|
||||
1. **수주데이터**: 수주 목록 (품목별 그룹핑)
|
||||
|
|
@ -117,54 +169,119 @@
|
|||
| 기본 버튼 (엑셀, 사용자옵션) | ✅ | `v2-button-primary` | |
|
||||
| 단일 테이블 목록 | ✅ | `v2-table-list` | |
|
||||
| 기본 모달 폼 | ✅ | 모달 화면 | |
|
||||
| 좌우 분할 패널 (기본) | ⚠️ | `v2-split-panel-layout` | 테이블/리스트만 표시 가능 |
|
||||
| 좌우 분할 패널 (커스텀 모드) | ✅ | `v2-split-panel-layout` | **displayMode: "custom" 지원** |
|
||||
| 탭 컴포넌트 (기본) | ⚠️ | `v2-tabs-widget` | 디자인 모드에서 컴포넌트 배치 |
|
||||
|
||||
### 4.2 현재 V2 컴포넌트의 제한 사항
|
||||
### 4.2 현재 V2 컴포넌트의 제한 사항 및 확장 방안
|
||||
|
||||
#### `v2-split-panel-layout` 제한
|
||||
#### `v2-split-panel-layout` 현재 기능
|
||||
|
||||
**현재 기능**:
|
||||
- 좌우 분할 패널 (리사이즈 가능)
|
||||
- 각 패널에 **테이블** 또는 **리스트** 표시 (`displayMode: "list" | "table"`)
|
||||
- leftPanel ↔ rightPanel 관계 설정 (relation)
|
||||
- 우측 패널에 추가 탭 (additionalTabs)
|
||||
| 기능 | 지원 | 설명 |
|
||||
|------|:----:|------|
|
||||
| 좌우 분할 패널 | ✅ | 리사이즈 가능 |
|
||||
| 테이블/리스트 표시 | ✅ | `displayMode: "list" \| "table"` |
|
||||
| 마스터-디테일 관계 | ✅ | `relation` 설정 |
|
||||
| 우측 추가 탭 (`additionalTabs`) | ⚠️ | **타입만 정의, 렌더링 미구현** |
|
||||
| **패널 내 임의 컴포넌트 배치** | ✅ | **`displayMode: "custom"` - 구현 완료!** |
|
||||
|
||||
**제한 사항**:
|
||||
- ❌ 패널 안에 **임의의 컴포넌트** (타임라인 등)를 배치할 수 없음
|
||||
- ❌ **그룹화된 테이블** (접기/펼치기) 미지원
|
||||
- ❌ 복잡한 커스텀 UI 배치 불가
|
||||
#### ✅ 커스텀 모드 (구현 완료)
|
||||
|
||||
#### `v2-tabs-widget` 제한
|
||||
`v2-tabs-widget`과 동일한 구조로 패널 안에 컴포넌트를 자유롭게 배치:
|
||||
|
||||
**현재 기능**:
|
||||
- 탭별 컴포넌트 자유 배치
|
||||
- 디자인 모드에서 드래그&드롭
|
||||
```typescript
|
||||
leftPanel: {
|
||||
displayMode: "list" | "table" | "custom", // ✅ 커스텀 모드 지원
|
||||
|
||||
// 기존 모드 (list, table)
|
||||
tableName?: string,
|
||||
columns?: [...],
|
||||
|
||||
// ✅ 커스텀 모드 - 패널 안에 자유롭게 컴포넌트 배치
|
||||
components?: PanelInlineComponent[],
|
||||
}
|
||||
|
||||
**제한 사항**:
|
||||
- ❌ 탭 내에 **다른 V2 컴포넌트**를 완전히 자유롭게 배치하기 어려움
|
||||
- ❌ 런타임에서 복잡한 컴포넌트 렌더링 제한
|
||||
// PanelInlineComponent 구조
|
||||
interface PanelInlineComponent {
|
||||
id: string;
|
||||
componentType: string;
|
||||
label?: string;
|
||||
position?: { x: number; y: number };
|
||||
size?: { width: number; height: number };
|
||||
componentConfig?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
**커스텀 모드 기능**:
|
||||
- ✅ 드래그앤드롭으로 컴포넌트 이동
|
||||
- ✅ 리사이즈 핸들로 크기 조절
|
||||
- ✅ 컴포넌트 클릭 시 좌측 설정 패널에서 속성 편집
|
||||
- ✅ 디자인 모드에서 실제 컴포넌트 미리보기 렌더링
|
||||
- ✅ 탭, 테이블, 버튼 등 모든 V2 컴포넌트 배치 가능
|
||||
|
||||
#### `v2-tabs-widget` 현재 기능
|
||||
|
||||
| 기능 | 지원 | 설명 |
|
||||
|------|:----:|------|
|
||||
| 탭별 컴포넌트 배치 | ✅ | `components[]` |
|
||||
| 디자인 모드 드래그&드롭 | ✅ | 위치/크기 조정 |
|
||||
| 런타임 렌더링 | ✅ | `DynamicComponentRenderer` 사용 |
|
||||
|
||||
**`v2-tabs-widget` 구조** (참고용):
|
||||
```typescript
|
||||
interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
components?: TabInlineComponent[]; // 탭 안에 배치된 컴포넌트들
|
||||
}
|
||||
|
||||
interface TabInlineComponent {
|
||||
id: string;
|
||||
componentType: string; // "v2-table-list", "v2-timeline-scheduler" 등
|
||||
position?: { x: number; y: number };
|
||||
size?: { width: number; height: number };
|
||||
componentConfig?: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 생산계획관리에 필요한 기능 vs 현재 지원
|
||||
|
||||
| 필요 기능 | 현재 지원 | 설명 |
|
||||
|----------|:---------:|------|
|
||||
| 좌우 분할 패널 | ⚠️ 부분 | `v2-split-panel-layout` - 테이블만 가능 |
|
||||
| 왼쪽 패널 탭 (수주/안전재고) | ❌ | 분할 패널 내 탭 조합 미지원 |
|
||||
| **그룹화된 테이블** (품목별 접기/펼치기) | ❌ | 신규 개발 필요 |
|
||||
| 오른쪽 패널 탭 (완제품/반제품) | ❌ | 분할 패널 내 탭 조합 미지원 |
|
||||
| **타임라인 스케줄러** (간트차트) | ❌ | 신규 개발 필요 |
|
||||
| 드래그&드롭 스케줄 이동 | ❌ | 신규 개발 필요 |
|
||||
| 복잡한 상세 모달 (분할, 설비할당) | ❌ | 커스텀 모달 개발 필요 |
|
||||
| 필요 기능 | 현재 지원 | 해결 방안 |
|
||||
|----------|:---------:|----------|
|
||||
| 좌우 분할 패널 | ✅ | `v2-split-panel-layout` |
|
||||
| 왼쪽 패널 탭 (수주/안전재고) | ✅ | **커스텀 모드 + `v2-tabs-widget` 조합 (구현 완료)** |
|
||||
| 오른쪽 패널 탭 (완제품/반제품) | ✅ | **커스텀 모드 + `v2-tabs-widget` 조합 (구현 완료)** |
|
||||
| **그룹화된 테이블** (품목별 접기/펼치기) | ❌ | **신규 개발 필요** |
|
||||
| **타임라인 스케줄러** (간트차트) | ❌ | **신규 개발 필요** |
|
||||
| 드래그&드롭 스케줄 이동 | ❌ | `v2-timeline-scheduler`에 포함 |
|
||||
| 복잡한 상세 모달 | ✅ | 기존 모달 화면으로 충분 |
|
||||
|
||||
### 4.4 향후 개발 필요 컴포넌트
|
||||
### 4.4 향후 개발 필요 사항
|
||||
|
||||
#### 기존 컴포넌트 확장 (1개)
|
||||
|
||||
| 컴포넌트 | 작업 내용 | 상태 |
|
||||
|----------|----------|:------:|
|
||||
| `v2-split-panel-layout` | `displayMode: "custom"` 추가 + `components[]` 지원 | ✅ **완료** |
|
||||
|
||||
#### 신규 컴포넌트 개발 (2개) - 대체 불가
|
||||
|
||||
| 컴포넌트 | 필요 이유 | 난이도 |
|
||||
|----------|----------|:------:|
|
||||
| **`v2-table-grouped`** | 품목별 접기/펼치기 그룹화 테이블 | 중 |
|
||||
| **`v2-timeline-scheduler`** | 간트차트 형태의 타임라인 스케줄러 | 상 |
|
||||
|
||||
#### 불필요한 컴포넌트 (이전 분석에서 제외)
|
||||
|
||||
| 컴포넌트 | 제외 이유 |
|
||||
|----------|----------|
|
||||
| ~~`v2-split-panel-enhanced`~~ | 커스텀 모드로 대체 가능 |
|
||||
| ~~`v2-modal-complex`~~ | 기존 모달 화면으로 충분 |
|
||||
|
||||
```
|
||||
필요 컴포넌트 목록:
|
||||
1. v2-timeline-scheduler - 타임라인/간트차트 스케줄러 (핵심!)
|
||||
2. v2-table-grouped - 그룹화된 테이블 (접기/펼치기)
|
||||
3. v2-split-panel-enhanced - 패널 내 임의 컴포넌트 배치 가능한 확장판
|
||||
4. v2-modal-complex - 복잡한 모달 (분할, 다단계, 설비할당)
|
||||
최종 필요 작업:
|
||||
1. ✅ v2-split-panel-layout 확장 - displayMode: "custom" 추가 [완료]
|
||||
2. v2-table-grouped 신규 개발 - 그룹화 테이블 (접기/펼치기)
|
||||
3. v2-timeline-scheduler 신규 개발 - 타임라인/간트차트 (핵심!)
|
||||
```
|
||||
|
||||
### 4.5 현재 구현 가능한 최대 범위
|
||||
|
|
@ -189,6 +306,63 @@
|
|||
- 자동 스케줄 생성
|
||||
- 드래그로 스케줄 이동
|
||||
|
||||
### 4.6 정식 버전 구현 구조 (확장 후)
|
||||
|
||||
`v2-split-panel-layout`에 커스텀 모드를 추가한 후의 **정식 버전 구현 구조**:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ v2-split-panel-layout (양쪽 패널 displayMode: "custom") │
|
||||
├────────────────────────────────┬─────────────────────────────────────┤
|
||||
│ leftPanel.components: [ │ rightPanel.components: [ │
|
||||
│ v2-tabs-widget { │ v2-tabs-widget { │
|
||||
│ tabs: [ │ tabs: [ │
|
||||
│ { │ { │
|
||||
│ label: "수주데이터", │ label: "완제품 생산계획", │
|
||||
│ components: [ │ components: [ │
|
||||
│ v2-table-grouped │ v2-timeline-scheduler │
|
||||
│ ] │ ] │
|
||||
│ }, │ }, │
|
||||
│ { │ { │
|
||||
│ label: "안전재고부족", │ label: "반제품 생산계획", │
|
||||
│ components: [ │ components: [ │
|
||||
│ v2-table-grouped │ v2-timeline-scheduler │
|
||||
│ ] │ ] │
|
||||
│ } │ } │
|
||||
│ ] │ ] │
|
||||
│ } │ } │
|
||||
│ ] │ ] │
|
||||
└────────────────────────────────┴─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**컴포넌트 중첩 구조**:
|
||||
```
|
||||
v2-split-panel-layout (displayMode: "custom")
|
||||
├── leftPanel.components[]
|
||||
│ └── v2-tabs-widget
|
||||
│ ├── tabs[0]: "수주데이터"
|
||||
│ │ └── components[]
|
||||
│ │ └── v2-table-grouped (품목별 그룹화)
|
||||
│ └── tabs[1]: "안전재고부족"
|
||||
│ └── components[]
|
||||
│ └── v2-table-grouped
|
||||
│
|
||||
└── rightPanel.components[]
|
||||
└── v2-tabs-widget
|
||||
├── tabs[0]: "완제품 생산계획"
|
||||
│ └── components[]
|
||||
│ └── v2-timeline-scheduler (간트차트)
|
||||
└── tabs[1]: "반제품 생산계획"
|
||||
└── components[]
|
||||
└── v2-timeline-scheduler
|
||||
```
|
||||
|
||||
**장점**:
|
||||
- 기존 `v2-tabs-widget` 로직 100% 재사용
|
||||
- 패널 안에 어떤 컴포넌트든 배치 가능
|
||||
- 별도 `v2-split-panel-enhanced` 개발 불필요
|
||||
- 확장성 뛰어남 (향후 다른 복합 화면에도 적용 가능)
|
||||
|
||||
---
|
||||
|
||||
## 5. 단순화된 구현 방안 (임시)
|
||||
|
|
@ -211,42 +385,107 @@
|
|||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 간소화 버전 테이블 컬럼
|
||||
### 5.2 컴포넌트 배치도 (간소화 버전)
|
||||
|
||||
| 순서 | 컬럼명 | 표시명 | 정렬 | 형식 |
|
||||
|:----:|--------|--------|:----:|------|
|
||||
| 1 | plan_no | 계획번호 | left | text |
|
||||
| 2 | plan_date | 계획일자 | center | date |
|
||||
| 3 | item_code | 품목코드 | left | text |
|
||||
| 4 | item_name | 품명 | left | text |
|
||||
| 5 | plan_qty | 계획수량 | right | number |
|
||||
| 6 | start_date | 시작일 | center | date |
|
||||
| 7 | end_date | 종료일 | center | date |
|
||||
| 8 | due_date | 납기일 | center | date |
|
||||
| 9 | equipment_name | 설비 | left | text |
|
||||
| 10 | status | 상태 | center | badge |
|
||||
| 11 | manager_name | 담당자 | left | text |
|
||||
| 12 | product_type | 제품유형 | center | text |
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ v2-table-search-widget (comp_search) │
|
||||
│ [품목코드] [품명] [계획기간: 시작~종료] [상태] [조회] [엑셀다운로드] │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 생산계획 목록 [신규등록] [수정] [삭제] [작업지시 생성] │
|
||||
│ ─────────────────────────────────────────────────────────────────────────── │
|
||||
│ v2-table-list (comp_table) │
|
||||
│ □ | 계획번호 | 계획일자 | 품목코드 | 품명 | 계획수량 | 시작일 | 종료일 | ... │
|
||||
│ □ | PP-001 | 2026-01 | ITEM001 | 제품A| 1,000 | 01-30 | 02-05 | ... │
|
||||
│ □ | PP-002 | 2026-01 | ITEM002 | 제품B| 500 | 02-01 | 02-10 | ... │
|
||||
│ │
|
||||
│ [1] [2] [3] ... [10] 페이지당: [20] ▼ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.3 간소화 버전 모달 필드
|
||||
### 5.3 컴포넌트 목록 (간소화 버전)
|
||||
|
||||
| 필드명 | 라벨 | 타입 | 필수 |
|
||||
|--------|------|------|:----:|
|
||||
| plan_no | 계획번호 | text | ✅ |
|
||||
| plan_date | 계획일자 | date | ✅ |
|
||||
| item_code | 품목코드 | select (품목 검색) | ✅ |
|
||||
| item_name | 품명 | text (자동) | |
|
||||
| plan_qty | 계획수량 | number | ✅ |
|
||||
| start_date | 시작일 | date | ✅ |
|
||||
| end_date | 종료일 | date | ✅ |
|
||||
| due_date | 납기일 | date | |
|
||||
| equipment_id | 설비 | select | |
|
||||
| status | 상태 | select | ✅ |
|
||||
| priority | 우선순위 | select | |
|
||||
| work_shift | 작업조 | select | |
|
||||
| manager_name | 담당자 | text | |
|
||||
| remarks | 비고 | textarea | |
|
||||
| product_type | 제품유형 | select | |
|
||||
| 컴포넌트 ID | 컴포넌트 타입 | 역할 |
|
||||
|-------------|---------------|------|
|
||||
| `comp_search` | v2-table-search-widget | 검색 필터 |
|
||||
| `comp_table` | v2-table-list | 생산계획 목록 |
|
||||
| `comp_btn_register` | v2-button-primary | 신규 등록 |
|
||||
| `comp_btn_edit` | v2-button-primary | 수정 |
|
||||
| `comp_btn_delete` | v2-button-primary | 삭제 |
|
||||
| `comp_btn_work_order` | v2-button-primary | 작업지시 생성 |
|
||||
|
||||
### 5.4 간소화 버전 테이블 컬럼
|
||||
|
||||
| 순서 | 컬럼명 | 표시명 | 정렬 | 형식 | 너비 |
|
||||
|:----:|--------|--------|:----:|------|:----:|
|
||||
| 1 | plan_no | 계획번호 | left | text | 120 |
|
||||
| 2 | plan_date | 계획일자 | center | date | 100 |
|
||||
| 3 | item_code | 품목코드 | left | text | 100 |
|
||||
| 4 | item_name | 품명 | left | text | 150 |
|
||||
| 5 | plan_qty | 계획수량 | right | number | 100 |
|
||||
| 6 | start_date | 시작일 | center | date | 100 |
|
||||
| 7 | end_date | 종료일 | center | date | 100 |
|
||||
| 8 | due_date | 납기일 | center | date | 100 |
|
||||
| 9 | equipment_name | 설비 | left | text | 120 |
|
||||
| 10 | status | 상태 | center | badge | 80 |
|
||||
| 11 | manager_name | 담당자 | left | text | 100 |
|
||||
| 12 | product_type | 제품유형 | center | text | 80 |
|
||||
|
||||
### 5.5 간소화 버전 모달 필드
|
||||
|
||||
| 필드명 | 라벨 | 타입 | 필수 | 비고 |
|
||||
|--------|------|------|:----:|------|
|
||||
| plan_no | 계획번호 | text (readonly) | ✅ | 자동채번: PP-YYYYMMDD-0001 |
|
||||
| plan_date | 계획일자 | date | ✅ | 기본값: 오늘 |
|
||||
| item_code | 품목코드 | select (품목 검색) | ✅ | 엔티티: item_info |
|
||||
| item_name | 품명 | text (readonly) | | 자동 입력 |
|
||||
| plan_qty | 계획수량 | number | ✅ | min: 1 |
|
||||
| start_date | 시작일 | date | ✅ | |
|
||||
| end_date | 종료일 | date | ✅ | start_date 이후 |
|
||||
| due_date | 납기일 | date | | |
|
||||
| equipment_id | 설비 | select | | 엔티티: equipment_info |
|
||||
| equipment_name | 설비명 | text (readonly) | | 자동 입력 |
|
||||
| status | 상태 | select | ✅ | 공통코드: PROD_PLAN_STATUS |
|
||||
| priority | 우선순위 | select | | 공통코드: PRIORITY |
|
||||
| work_shift | 작업조 | select | | 공통코드: WORK_SHIFT |
|
||||
| manager_name | 담당자 | text | | |
|
||||
| product_type | 제품유형 | select | | 공통코드: PRODUCT_TYPE |
|
||||
| remarks | 비고 | textarea | | |
|
||||
|
||||
### 5.6 상태 배지 및 공통코드
|
||||
|
||||
#### 상태 (PROD_PLAN_STATUS)
|
||||
|
||||
| 코드 | 표시명 | 배경색 | 글자색 |
|
||||
|------|--------|--------|--------|
|
||||
| 계획 | 계획 | #dbeafe | #1e40af |
|
||||
| 지시 | 지시 | #fef3c7 | #92400e |
|
||||
| 진행 | 진행 | #d1fae5 | #065f46 |
|
||||
| 완료 | 완료 | #f3f4f6 | #4b5563 |
|
||||
|
||||
#### 우선순위 (PRIORITY)
|
||||
|
||||
| 코드 | 표시명 |
|
||||
|------|--------|
|
||||
| 긴급 | 긴급 |
|
||||
| 높음 | 높음 |
|
||||
| 보통 | 보통 |
|
||||
| 낮음 | 낮음 |
|
||||
|
||||
#### 작업조 (WORK_SHIFT)
|
||||
|
||||
| 코드 | 표시명 |
|
||||
|------|--------|
|
||||
| 주간 | 주간 |
|
||||
| 야간 | 야간 |
|
||||
| 주야 | 주야 |
|
||||
|
||||
#### 제품유형 (PRODUCT_TYPE)
|
||||
|
||||
| 코드 | 표시명 |
|
||||
|------|--------|
|
||||
| 완제품 | 완제품 |
|
||||
| 반제품 | 반제품 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -395,18 +634,34 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
- 기본 모달 폼
|
||||
- 상태 배지
|
||||
|
||||
### Phase 2: 컴포넌트 개발 후
|
||||
### Phase 2: 컴포넌트 확장 및 개발
|
||||
|
||||
1. `v2-tabs` 컴포넌트 개발
|
||||
2. `v2-split-panel` 컴포넌트 개발
|
||||
3. `v2-table-grouped` 컴포넌트 개발
|
||||
1. ~~**`v2-split-panel-layout` 확장**~~ ✅ **완료**
|
||||
- ✅ `displayMode: "custom"` 추가
|
||||
- ✅ `components[]` 필드 지원 (탭 컴포넌트와 동일한 구조)
|
||||
- ✅ 디자인 모드에서 컴포넌트 드래그&드롭 지원
|
||||
- ✅ 컴포넌트 클릭 시 좌측 설정 패널에서 속성 편집
|
||||
|
||||
2. **`v2-table-grouped` 신규 개발**
|
||||
- 품목별 그룹핑 (접기/펼치기)
|
||||
- 그룹 행과 상세 행 구분
|
||||
- 체크박스 선택 (그룹 단위 / 개별 단위)
|
||||
|
||||
### Phase 3: 타임라인 스케줄러
|
||||
|
||||
1. `v2-timeline-scheduler` 컴포넌트 개발
|
||||
2. 드래그&드롭 기능
|
||||
3. 자동 스케줄 생성 로직
|
||||
4. 반제품 연동
|
||||
1. **`v2-timeline-scheduler` 신규 개발**
|
||||
- 간트차트 형태 타임라인
|
||||
- 날짜 그리드 (일/주/월 단위)
|
||||
- 생산계획 바 렌더링
|
||||
|
||||
2. **드래그&드롭 기능**
|
||||
- 계획 바 드래그로 날짜 이동
|
||||
- 계획 바 크기 조정 (기간 변경)
|
||||
|
||||
3. **비즈니스 로직**
|
||||
- 자동 스케줄 생성 API
|
||||
- BOM 기반 반제품 계획 연동
|
||||
- 설비별 용량 체크
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -488,7 +743,25 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
"position": { "x": 0, "y": 20, "z": 1 },
|
||||
"overrides": {
|
||||
"type": "v2-table-search-widget",
|
||||
"label": "검색 필터"
|
||||
"label": "검색 필터",
|
||||
"searchFields": [
|
||||
{ "field": "item_code", "label": "품목코드", "type": "text" },
|
||||
{ "field": "item_name", "label": "품명", "type": "text" },
|
||||
{ "field": "plan_date", "label": "계획기간", "type": "daterange" },
|
||||
{
|
||||
"field": "status",
|
||||
"label": "상태",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{ "value": "", "label": "전체" },
|
||||
{ "value": "계획", "label": "계획" },
|
||||
{ "value": "지시", "label": "지시" },
|
||||
{ "value": "진행", "label": "진행" },
|
||||
{ "value": "완료", "label": "완료" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"targetTableId": "comp_table"
|
||||
},
|
||||
"displayOrder": 0
|
||||
},
|
||||
|
|
@ -501,23 +774,39 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
"type": "v2-table-list",
|
||||
"label": "생산계획 목록",
|
||||
"columns": [
|
||||
{ "columnName": "plan_no", "displayName": "계획번호", "order": 0, "visible": true, "sortable": true, "format": "text", "align": "left" },
|
||||
{ "columnName": "plan_date", "displayName": "계획일자", "order": 1, "visible": true, "sortable": true, "format": "date", "align": "center" },
|
||||
{ "columnName": "item_code", "displayName": "품목코드", "order": 2, "visible": true, "sortable": true, "format": "text", "align": "left" },
|
||||
{ "columnName": "item_name", "displayName": "품명", "order": 3, "visible": true, "sortable": true, "format": "text", "align": "left" },
|
||||
{ "columnName": "plan_qty", "displayName": "계획수량", "order": 4, "visible": true, "sortable": true, "format": "number", "align": "right" },
|
||||
{ "columnName": "start_date", "displayName": "시작일", "order": 5, "visible": true, "sortable": true, "format": "date", "align": "center" },
|
||||
{ "columnName": "end_date", "displayName": "종료일", "order": 6, "visible": true, "sortable": true, "format": "date", "align": "center" },
|
||||
{ "columnName": "due_date", "displayName": "납기일", "order": 7, "visible": true, "sortable": true, "format": "date", "align": "center" },
|
||||
{ "columnName": "equipment_name", "displayName": "설비", "order": 8, "visible": true, "sortable": true, "format": "text", "align": "left" },
|
||||
{ "columnName": "status", "displayName": "상태", "order": 9, "visible": true, "sortable": true, "format": "text", "align": "center" },
|
||||
{ "columnName": "product_type", "displayName": "제품유형", "order": 10, "visible": true, "sortable": true, "format": "text", "align": "center" },
|
||||
{ "columnName": "manager_name", "displayName": "담당자", "order": 11, "visible": true, "sortable": true, "format": "text", "align": "left" }
|
||||
{ "columnName": "plan_no", "displayName": "계획번호", "order": 0, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 120 },
|
||||
{ "columnName": "plan_date", "displayName": "계획일자", "order": 1, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 },
|
||||
{ "columnName": "item_code", "displayName": "품목코드", "order": 2, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 100 },
|
||||
{ "columnName": "item_name", "displayName": "품명", "order": 3, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 150 },
|
||||
{ "columnName": "plan_qty", "displayName": "계획수량", "order": 4, "visible": true, "sortable": true, "format": "number", "align": "right", "width": 100 },
|
||||
{ "columnName": "start_date", "displayName": "시작일", "order": 5, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 },
|
||||
{ "columnName": "end_date", "displayName": "종료일", "order": 6, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 },
|
||||
{ "columnName": "due_date", "displayName": "납기일", "order": 7, "visible": true, "sortable": true, "format": "date", "align": "center", "width": 100 },
|
||||
{ "columnName": "equipment_name", "displayName": "설비", "order": 8, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 120 },
|
||||
{
|
||||
"columnName": "status",
|
||||
"displayName": "상태",
|
||||
"order": 9,
|
||||
"visible": true,
|
||||
"sortable": true,
|
||||
"format": "badge",
|
||||
"align": "center",
|
||||
"width": 80,
|
||||
"badgeConfig": {
|
||||
"계획": { "background": "#dbeafe", "color": "#1e40af" },
|
||||
"지시": { "background": "#fef3c7", "color": "#92400e" },
|
||||
"진행": { "background": "#d1fae5", "color": "#065f46" },
|
||||
"완료": { "background": "#f3f4f6", "color": "#4b5563" }
|
||||
}
|
||||
},
|
||||
{ "columnName": "product_type", "displayName": "제품유형", "order": 10, "visible": true, "sortable": true, "format": "text", "align": "center", "width": 80 },
|
||||
{ "columnName": "manager_name", "displayName": "담당자", "order": 11, "visible": true, "sortable": true, "format": "text", "align": "left", "width": 100 }
|
||||
],
|
||||
"selectedTable": "production_plan_mng",
|
||||
"pagination": { "enabled": true, "pageSize": 20, "pageSizeOptions": [10, 20, 50, 100] },
|
||||
"checkbox": { "enabled": true, "multiple": true },
|
||||
"horizontalScroll": { "enabled": true, "minColumnWidth": 80, "maxColumnWidth": 200 }
|
||||
"horizontalScroll": { "enabled": true, "minColumnWidth": 80, "maxColumnWidth": 200 },
|
||||
"defaultSort": { "field": "plan_date", "order": "desc" }
|
||||
},
|
||||
"displayOrder": 1
|
||||
},
|
||||
|
|
@ -525,7 +814,7 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
"id": "comp_btn_register",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 100, "height": 40 },
|
||||
"position": { "x": 1580, "y": 70, "z": 1 },
|
||||
"position": { "x": 1480, "y": 70, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "신규 등록",
|
||||
"type": "v2-button-primary",
|
||||
|
|
@ -538,12 +827,13 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
"id": "comp_btn_edit",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 80, "height": 40 },
|
||||
"position": { "x": 1690, "y": 70, "z": 1 },
|
||||
"position": { "x": 1590, "y": 70, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "수정",
|
||||
"type": "v2-button-primary",
|
||||
"action": { "type": "edit", "modalTitle": "생산계획 수정", "targetScreenId": null },
|
||||
"variant": "secondary"
|
||||
"variant": "secondary",
|
||||
"requireSelection": true
|
||||
},
|
||||
"displayOrder": 3
|
||||
},
|
||||
|
|
@ -551,14 +841,34 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
"id": "comp_btn_delete",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 80, "height": 40 },
|
||||
"position": { "x": 1780, "y": 70, "z": 1 },
|
||||
"position": { "x": 1680, "y": 70, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "삭제",
|
||||
"type": "v2-button-primary",
|
||||
"action": { "type": "delete" },
|
||||
"variant": "danger"
|
||||
"variant": "danger",
|
||||
"requireSelection": true,
|
||||
"enableDataflowControl": true,
|
||||
"dataflowDiagramId": null
|
||||
},
|
||||
"displayOrder": 4
|
||||
},
|
||||
{
|
||||
"id": "comp_btn_work_order",
|
||||
"url": "@/lib/registry/components/v2-button-primary",
|
||||
"size": { "width": 120, "height": 40 },
|
||||
"position": { "x": 1770, "y": 70, "z": 1 },
|
||||
"overrides": {
|
||||
"text": "작업지시 생성",
|
||||
"type": "v2-button-primary",
|
||||
"action": { "type": "custom" },
|
||||
"variant": "primary",
|
||||
"requireSelection": true,
|
||||
"enableDataflowControl": true,
|
||||
"dataflowDiagramId": null,
|
||||
"confirmMessage": "선택한 계획에 대해 작업지시를 생성하시겠습니까?"
|
||||
},
|
||||
"displayOrder": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -566,9 +876,312 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
|
||||
---
|
||||
|
||||
## 10. 구현 체크리스트
|
||||
## 10. 비즈니스 로직 요청서 (간소화 버전)
|
||||
|
||||
### 10.1 간소화 버전 (현재 구현 가능)
|
||||
> **중요**: 이 섹션은 표준 양식에 따라 작성되었습니다.
|
||||
|
||||
```
|
||||
=== 비즈니스 로직 요청서 ===
|
||||
|
||||
【화면 정보】
|
||||
- 화면명: 생산계획관리 (간소화 버전)
|
||||
- 회사코드: {COMPANY_CODE}
|
||||
- 메뉴ID: {TBD}
|
||||
|
||||
【테이블 정보】
|
||||
- 메인 테이블: production_plan_mng
|
||||
- 디테일 테이블: 없음 (간소화 버전)
|
||||
- 관계 FK: 없음
|
||||
|
||||
【버튼 목록】
|
||||
버튼1:
|
||||
- 버튼명: 신규 등록
|
||||
- 동작 유형: 모달 열기 (insert)
|
||||
- 조건: 없음
|
||||
- 대상 테이블: production_plan_mng
|
||||
- 추가 동작:
|
||||
1. plan_no 자동채번 (PP-YYYYMMDD-0001)
|
||||
2. plan_date 기본값 = 오늘
|
||||
3. status 기본값 = '계획'
|
||||
|
||||
버튼2:
|
||||
- 버튼명: 수정
|
||||
- 동작 유형: 모달 열기 (update)
|
||||
- 조건: 행 선택 필수
|
||||
- 대상 테이블: production_plan_mng
|
||||
- 추가 동작: 없음
|
||||
|
||||
버튼3:
|
||||
- 버튼명: 삭제
|
||||
- 동작 유형: 삭제 (delete)
|
||||
- 조건:
|
||||
1. 행 선택 필수
|
||||
2. status != '진행' AND status != '완료'
|
||||
- 대상 테이블: production_plan_mng
|
||||
- 추가 동작: 삭제 확인 다이얼로그
|
||||
|
||||
버튼4:
|
||||
- 버튼명: 작업지시 생성
|
||||
- 동작 유형: 수정 (update)
|
||||
- 조건:
|
||||
1. 행 선택 필수 (다중 선택 가능)
|
||||
2. status = '계획'
|
||||
- 대상 테이블: production_plan_mng
|
||||
- 추가 동작:
|
||||
1. status를 '지시'로 변경
|
||||
2. work_order_no 자동채번 (WO-YYYYMMDD-0001)
|
||||
3. work_order_mng에 INSERT (작업지시 테이블)
|
||||
|
||||
【추가 요구사항】
|
||||
- 진행/완료 상태의 계획은 수정/삭제 불가
|
||||
- 계획번호(plan_no) 자동채번: PP-YYYYMMDD-0001
|
||||
- 품목코드 선택 시 품명 자동 입력
|
||||
- 설비 선택 시 시간당 생산능력 자동 입력
|
||||
- 시작일/종료일 기반 생산기간 자동 계산
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 제어관리 설정 (dataflow_diagrams)
|
||||
|
||||
### 11.1 신규 등록 버튼 (기본 저장)
|
||||
|
||||
> 기본 INSERT 액션은 화면 디자이너에서 처리되므로 별도 dataflow 불필요
|
||||
|
||||
### 11.2 삭제 버튼 (조건부 삭제)
|
||||
|
||||
```json
|
||||
{
|
||||
"diagram_name": "생산계획관리_삭제",
|
||||
"company_code": "{COMPANY_CODE}",
|
||||
"control": {
|
||||
"actionType": "delete",
|
||||
"triggerType": "before",
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond-status-not-in-progress",
|
||||
"type": "condition",
|
||||
"field": "status",
|
||||
"operator": "!=",
|
||||
"value": "진행",
|
||||
"dataType": "string"
|
||||
},
|
||||
{
|
||||
"id": "cond-status-not-completed",
|
||||
"type": "condition",
|
||||
"field": "status",
|
||||
"operator": "!=",
|
||||
"value": "완료",
|
||||
"dataType": "string"
|
||||
}
|
||||
],
|
||||
"conditionLogic": "AND",
|
||||
"errorMessage": "진행 중이거나 완료된 계획은 삭제할 수 없습니다."
|
||||
},
|
||||
"plan": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "action-delete",
|
||||
"actionType": "delete",
|
||||
"targetTable": "production_plan_mng"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 작업지시 생성 버튼
|
||||
|
||||
```json
|
||||
{
|
||||
"diagram_name": "생산계획관리_작업지시생성",
|
||||
"company_code": "{COMPANY_CODE}",
|
||||
"control": {
|
||||
"actionType": "update",
|
||||
"triggerType": "after",
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond-status-plan",
|
||||
"type": "condition",
|
||||
"field": "status",
|
||||
"operator": "=",
|
||||
"value": "계획",
|
||||
"dataType": "string"
|
||||
}
|
||||
],
|
||||
"errorMessage": "계획 상태인 항목만 작업지시를 생성할 수 있습니다."
|
||||
},
|
||||
"plan": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "action-update-status",
|
||||
"actionType": "update",
|
||||
"targetTable": "production_plan_mng",
|
||||
"fieldMappings": [
|
||||
{ "targetField": "status", "defaultValue": "지시" },
|
||||
{ "targetField": "updated_date", "defaultValue": "#NOW" },
|
||||
{ "targetField": "updated_by", "defaultValue": "#USER" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "action-insert-workorder",
|
||||
"actionType": "insert",
|
||||
"targetTable": "work_order_mng",
|
||||
"fieldMappings": [
|
||||
{ "sourceField": "plan_no", "targetField": "plan_no" },
|
||||
{ "sourceField": "item_code", "targetField": "item_code" },
|
||||
{ "sourceField": "item_name", "targetField": "item_name" },
|
||||
{ "sourceField": "plan_qty", "targetField": "order_qty" },
|
||||
{ "sourceField": "start_date", "targetField": "work_start_date" },
|
||||
{ "sourceField": "end_date", "targetField": "work_end_date" },
|
||||
{ "sourceField": "equipment_id", "targetField": "equipment_id" },
|
||||
{ "sourceField": "equipment_name", "targetField": "equipment_name" },
|
||||
{ "targetField": "status", "defaultValue": "대기" },
|
||||
{ "targetField": "company_code", "defaultValue": "#COMPANY" },
|
||||
{ "targetField": "created_date", "defaultValue": "#NOW" },
|
||||
{ "targetField": "created_by", "defaultValue": "#USER" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 컴포넌트 연동 설정 (간소화 버전)
|
||||
|
||||
### 12.1 이벤트 흐름
|
||||
|
||||
```
|
||||
[화면 진입]
|
||||
│
|
||||
▼
|
||||
v2-table-search-widget (검색 필터)
|
||||
│ onFilterChange
|
||||
▼
|
||||
v2-table-list (생산계획 목록)
|
||||
│ onRowSelect
|
||||
▼
|
||||
[버튼 활성화/비활성화]
|
||||
│
|
||||
├── [신규 등록] 클릭 → 등록 모달 오픈
|
||||
│ │ onSave
|
||||
│ ▼
|
||||
│ INSERT → 테이블 Refresh
|
||||
│
|
||||
├── [수정] 클릭 → 수정 모달 오픈
|
||||
│ │ onSave
|
||||
│ ▼
|
||||
│ UPDATE → 테이블 Refresh
|
||||
│
|
||||
├── [삭제] 클릭 → 조건 체크
|
||||
│ │ 통과 시
|
||||
│ ▼
|
||||
│ DELETE → 테이블 Refresh
|
||||
│
|
||||
└── [작업지시 생성] 클릭 → 조건 체크
|
||||
│ 통과 시
|
||||
▼
|
||||
UPDATE + INSERT → 테이블 Refresh
|
||||
```
|
||||
|
||||
### 12.2 연동 설정 테이블
|
||||
|
||||
| 소스 컴포넌트 | 이벤트 | 대상 컴포넌트 | 동작 |
|
||||
|---------------|--------|---------------|------|
|
||||
| search-widget | onFilterChange | main-table | 필터 적용 후 재조회 |
|
||||
| main-table | onRowSelect | btn-edit, btn-delete | 버튼 활성화 |
|
||||
| main-table | onRowDoubleClick | modal-edit | 수정 모달 오픈 |
|
||||
| btn-register | onClick | modal-register | 등록 모달 오픈 |
|
||||
| btn-edit | onClick | modal-edit | 수정 모달 오픈 |
|
||||
| btn-delete | onClick | dataflow | 삭제 실행 |
|
||||
| btn-work-order | onClick | dataflow | 작업지시 생성 실행 |
|
||||
| modal-* | onSave | main-table | 테이블 새로고침 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 사용자 사용 예시 시나리오 (간소화 버전)
|
||||
|
||||
### 시나리오 1: 생산계획 조회
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|:----:|-------------|-----------|
|
||||
| 1 | 화면 진입 | 전체 생산계획 목록 표시 (최근순) |
|
||||
| 2 | 품목코드 입력 후 [조회] 클릭 | 해당 품목의 생산계획만 필터링 |
|
||||
| 3 | 상태 드롭다운에서 '계획' 선택 | 계획 상태인 항목만 표시 |
|
||||
| 4 | 컬럼 헤더 클릭 | 해당 컬럼 기준 정렬 |
|
||||
|
||||
### 시나리오 2: 생산계획 등록
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|:----:|-------------|-----------|
|
||||
| 1 | [신규 등록] 버튼 클릭 | 등록 모달 오픈 |
|
||||
| 2 | 품목코드 검색/선택 | 품명 자동 입력 |
|
||||
| 3 | 계획수량, 시작일, 종료일 입력 | 입력 필드 채움 |
|
||||
| 4 | 설비 선택 | 시간당 생산능력 표시 |
|
||||
| 5 | [저장] 버튼 클릭 | 저장 완료, 목록에 신규 행 표시 |
|
||||
|
||||
### 시나리오 3: 생산계획 수정
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|:----:|-------------|-----------|
|
||||
| 1 | 목록에서 행 선택 | 행 하이라이트 |
|
||||
| 2 | [수정] 버튼 클릭 (또는 더블클릭) | 수정 모달 오픈 (기존 값 로드) |
|
||||
| 3 | 필요한 필드 수정 | 값 변경 |
|
||||
| 4 | [저장] 버튼 클릭 | 저장 완료, 목록 갱신 |
|
||||
|
||||
### 시나리오 4: 생산계획 삭제
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|:----:|-------------|-----------|
|
||||
| 1 | 삭제할 행 체크박스 선택 | 선택 표시 |
|
||||
| 2 | [삭제] 버튼 클릭 | 삭제 확인 다이얼로그 표시 |
|
||||
| 3-a | (상태가 계획/지시) 확인 클릭 | 삭제 완료, 목록에서 제거 |
|
||||
| 3-b | (상태가 진행/완료) 확인 클릭 | "삭제할 수 없습니다" 오류 메시지 |
|
||||
|
||||
### 시나리오 5: 작업지시 생성
|
||||
|
||||
| 단계 | 사용자 동작 | 기대 결과 |
|
||||
|:----:|-------------|-----------|
|
||||
| 1 | 계획 상태인 행 체크박스 선택 (다중 가능) | 선택 표시 |
|
||||
| 2 | [작업지시 생성] 버튼 클릭 | 확인 다이얼로그 표시 |
|
||||
| 3-a | (상태가 계획) 확인 클릭 | 상태가 '지시'로 변경, 작업지시번호 생성 |
|
||||
| 3-b | (상태가 계획 아님) 확인 클릭 | "계획 상태만 가능합니다" 오류 메시지 |
|
||||
|
||||
---
|
||||
|
||||
## 14. 검증 체크리스트
|
||||
|
||||
### 14.1 기능 테스트
|
||||
|
||||
- [ ] 데이터 조회가 정상 동작하는가?
|
||||
- [ ] 검색 필터가 정상 동작하는가?
|
||||
- [ ] 신규 등록이 정상 동작하는가?
|
||||
- [ ] 수정이 정상 동작하는가?
|
||||
- [ ] 삭제가 정상 동작하는가? (조건 포함)
|
||||
- [ ] 작업지시 생성이 정상 동작하는가? (조건 포함)
|
||||
- [ ] 페이지네이션이 정상 동작하는가?
|
||||
- [ ] 정렬이 정상 동작하는가?
|
||||
|
||||
### 14.2 데이터 검증
|
||||
|
||||
- [ ] 계획번호(plan_no) 자동채번이 중복 없이 생성되는가?
|
||||
- [ ] 품목코드 선택 시 품명이 자동 입력되는가?
|
||||
- [ ] 설비 선택 시 시간당 생산능력이 표시되는가?
|
||||
- [ ] company_code 필터링이 적용되는가? (멀티테넌시)
|
||||
|
||||
### 14.3 비즈니스 룰 검증
|
||||
|
||||
- [ ] 진행/완료 상태는 삭제 불가한가?
|
||||
- [ ] 계획 상태만 작업지시 생성 가능한가?
|
||||
- [ ] 작업지시 생성 시 work_order_mng에 데이터가 INSERT되는가?
|
||||
|
||||
---
|
||||
|
||||
## 15. 구현 체크리스트
|
||||
|
||||
### 15.1 간소화 버전 (현재 구현 가능)
|
||||
|
||||
| 체크 | 항목 | 설명 |
|
||||
|:----:|------|------|
|
||||
|
|
@ -576,26 +1189,37 @@ V2 컴포넌트로 기본 CRUD 화면 구현:
|
|||
| ☐ | **화면 정의** | `screen_definitions` INSERT |
|
||||
| ☐ | **모달 화면 생성** | 등록/수정 모달 화면 생성 |
|
||||
| ☐ | **메인 화면 생성** | `screen_layouts_v2` INSERT |
|
||||
| ☐ | **제어관리 설정** | `dataflow_diagrams` INSERT (삭제, 작업지시 생성) |
|
||||
| ☐ | **메뉴 연결** | 대상 메뉴에 화면 할당 |
|
||||
| ☐ | **기본 CRUD 테스트** | 등록/수정/삭제/조회 테스트 |
|
||||
| ☐ | **비즈니스 룰 테스트** | 조건부 삭제, 작업지시 생성 테스트 |
|
||||
|
||||
### 10.2 전체 버전 (향후 구현)
|
||||
### 15.2 전체 버전 (향후 구현)
|
||||
|
||||
| 체크 | 항목 | 설명 |
|
||||
|:----:|------|------|
|
||||
| ☐ | **v2-tabs 개발** | 탭 컴포넌트 개발 |
|
||||
| ☐ | **v2-split-panel 개발** | 분할 패널 컴포넌트 개발 |
|
||||
| ☐ | **v2-table-grouped 개발** | 그룹화 테이블 컴포넌트 개발 |
|
||||
| ☐ | **v2-timeline-scheduler 개발** | 타임라인 스케줄러 개발 |
|
||||
| ✅ | ~~**v2-split-panel-layout 확장**~~ | `displayMode: "custom"` + `components[]` 추가 **완료** |
|
||||
| ☐ | **v2-table-grouped 개발** | 그룹화 테이블 컴포넌트 (접기/펼치기) |
|
||||
| ☐ | **v2-timeline-scheduler 개발** | 타임라인/간트차트 스케줄러 |
|
||||
| ☐ | **드래그&드롭 기능** | 타임라인 바 드래그 이동/크기 조정 |
|
||||
| ☐ | **자동 스케줄 로직** | 자동 스케줄 생성 백엔드 API |
|
||||
| ☐ | **반제품 연동** | BOM 기반 반제품 계획 생성 |
|
||||
| ☐ | **드래그&드롭** | 타임라인 드래그 이동 기능 |
|
||||
| ☐ | **정식 버전 화면 구성** | 커스텀 모드로 탭+테이블+타임라인 조합 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 관련 문서
|
||||
## 16. 관련 문서
|
||||
|
||||
- [수주관리](../02_sales/order.md)
|
||||
- [품목정보](../01_master-data/item-info.md)
|
||||
- [설비관리](../05_equipment/equipment-info.md) (예정)
|
||||
- [BOM관리](../01_master-data/bom-info.md) (예정)
|
||||
- [설비관리](../05_equipment/equipment-info.md)
|
||||
- [BOM관리](../01_master-data/bom.md)
|
||||
- [작업지시](./work-order.md)
|
||||
|
||||
---
|
||||
|
||||
## 17. 참고: 표준 가이드
|
||||
|
||||
- [화면개발 표준 가이드](../화면개발_표준_가이드.md)
|
||||
- [V2 컴포넌트 사용 가이드](../00_analysis/v2-component-usage-guide.md)
|
||||
- [전체 화면 분석 보고서](../00_analysis/full-screen-analysis.md)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ interface RealtimePreviewProps {
|
|||
onUpdateComponent?: (updatedComponent: any) => void; // 🆕 컴포넌트 업데이트 콜백
|
||||
onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void; // 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
selectedTabComponentId?: string; // 🆕 선택된 탭 컴포넌트 ID
|
||||
onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: any) => void; // 🆕 분할 패널 내부 컴포넌트 선택 콜백
|
||||
selectedPanelComponentId?: string; // 🆕 선택된 분할 패널 컴포넌트 ID
|
||||
onResize?: (componentId: string, newSize: { width: number; height: number }) => void; // 🆕 리사이즈 콜백
|
||||
|
||||
// 버튼 액션을 위한 props
|
||||
|
|
@ -140,6 +142,8 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
|||
onUpdateComponent, // 🆕 컴포넌트 업데이트 콜백
|
||||
onSelectTabComponent, // 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
selectedTabComponentId, // 🆕 선택된 탭 컴포넌트 ID
|
||||
onSelectPanelComponent, // 🆕 분할 패널 내부 컴포넌트 선택 콜백
|
||||
selectedPanelComponentId, // 🆕 선택된 분할 패널 컴포넌트 ID
|
||||
onResize, // 🆕 리사이즈 콜백
|
||||
}) => {
|
||||
// 🆕 화면 다국어 컨텍스트
|
||||
|
|
@ -640,6 +644,8 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
|||
onUpdateComponent={onUpdateComponent}
|
||||
onSelectTabComponent={onSelectTabComponent}
|
||||
selectedTabComponentId={selectedTabComponentId}
|
||||
onSelectPanelComponent={onSelectPanelComponent}
|
||||
selectedPanelComponentId={selectedPanelComponentId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -177,13 +177,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
component: any; // 탭 내부 컴포넌트 데이터
|
||||
} | null>(null);
|
||||
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 상태
|
||||
const [selectedPanelComponentInfo, setSelectedPanelComponentInfo] = useState<{
|
||||
splitPanelId: string; // 분할 패널 컴포넌트 ID
|
||||
panelSide: "left" | "right"; // 좌측/우측 패널
|
||||
componentId: string; // 패널 내부 컴포넌트 ID
|
||||
component: any; // 패널 내부 컴포넌트 데이터
|
||||
} | null>(null);
|
||||
|
||||
// 컴포넌트 선택 시 통합 패널 자동 열기
|
||||
const handleComponentSelect = useCallback(
|
||||
(component: ComponentData | null) => {
|
||||
setSelectedComponent(component);
|
||||
// 일반 컴포넌트 선택 시 탭 내부 컴포넌트 선택 해제
|
||||
// 일반 컴포넌트 선택 시 탭 내부 컴포넌트/분할 패널 내부 컴포넌트 선택 해제
|
||||
if (component) {
|
||||
setSelectedTabComponentInfo(null);
|
||||
setSelectedPanelComponentInfo(null);
|
||||
}
|
||||
|
||||
// 컴포넌트가 선택되면 통합 패널 자동 열기
|
||||
|
|
@ -209,8 +218,32 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
componentId: compId,
|
||||
component: comp,
|
||||
});
|
||||
// 탭 내부 컴포넌트 선택 시 일반 컴포넌트 선택 해제
|
||||
// 탭 내부 컴포넌트 선택 시 일반 컴포넌트/분할 패널 내부 컴포넌트 선택 해제
|
||||
setSelectedComponent(null);
|
||||
setSelectedPanelComponentInfo(null);
|
||||
openPanel("v2");
|
||||
},
|
||||
[openPanel],
|
||||
);
|
||||
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 핸들러
|
||||
const handleSelectPanelComponent = useCallback(
|
||||
(splitPanelId: string, panelSide: "left" | "right", compId: string, comp: any) => {
|
||||
if (!compId) {
|
||||
// 패널 영역 빈 공간 클릭 시 선택 해제
|
||||
setSelectedPanelComponentInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedPanelComponentInfo({
|
||||
splitPanelId,
|
||||
panelSide,
|
||||
componentId: compId,
|
||||
component: comp,
|
||||
});
|
||||
// 분할 패널 내부 컴포넌트 선택 시 일반 컴포넌트/탭 내부 컴포넌트 선택 해제
|
||||
setSelectedComponent(null);
|
||||
setSelectedTabComponentInfo(null);
|
||||
openPanel("v2");
|
||||
},
|
||||
[openPanel],
|
||||
|
|
@ -2509,6 +2542,72 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
}
|
||||
}
|
||||
|
||||
// 🎯 분할 패널 커스텀 모드 컨테이너 내부 드롭 처리
|
||||
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
||||
if (splitPanelContainer) {
|
||||
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
||||
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
||||
if (containerId && panelSide) {
|
||||
const targetComponent = layout.components.find((c) => c.id === containerId);
|
||||
const compType = (targetComponent as any)?.componentType;
|
||||
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
||||
const currentConfig = (targetComponent as any).componentConfig || {};
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const currentComponents = panelConfig.components || [];
|
||||
|
||||
// 드롭 위치 계산
|
||||
const panelRect = splitPanelContainer.getBoundingClientRect();
|
||||
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
||||
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
||||
|
||||
// 새 컴포넌트 생성
|
||||
const componentType = component.id || component.componentType || "v2-text-display";
|
||||
|
||||
console.log("🎯 분할 패널에 컴포넌트 드롭:", {
|
||||
componentId: component.id,
|
||||
componentType: componentType,
|
||||
panelSide: panelSide,
|
||||
dropPosition: { x: dropX, y: dropY },
|
||||
});
|
||||
|
||||
const newPanelComponent = {
|
||||
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
componentType: componentType,
|
||||
label: component.name || component.label || "새 컴포넌트",
|
||||
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
||||
size: component.defaultSize || { width: 200, height: 100 },
|
||||
componentConfig: component.defaultConfig || {},
|
||||
};
|
||||
|
||||
const updatedPanelConfig = {
|
||||
...panelConfig,
|
||||
components: [...currentComponents, newPanelComponent],
|
||||
};
|
||||
|
||||
const updatedComponent = {
|
||||
...targetComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: updatedPanelConfig,
|
||||
},
|
||||
};
|
||||
|
||||
const newLayout = {
|
||||
...layout,
|
||||
components: layout.components.map((c) =>
|
||||
c.id === containerId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
toast.success(`컴포넌트가 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
||||
return; // 분할 패널 처리 완료
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
|
|
@ -2996,6 +3095,105 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
}
|
||||
}
|
||||
|
||||
// 🎯 분할 패널 커스텀 모드 컨테이너 내부에 컬럼 드롭 시 처리
|
||||
const splitPanelContainer = dropTarget.closest('[data-split-panel-container="true"]');
|
||||
if (splitPanelContainer && type === "column" && column) {
|
||||
const containerId = splitPanelContainer.getAttribute("data-component-id");
|
||||
const panelSide = splitPanelContainer.getAttribute("data-panel-side"); // "left" or "right"
|
||||
if (containerId && panelSide) {
|
||||
const targetComponent = layout.components.find((c) => c.id === containerId);
|
||||
const compType = (targetComponent as any)?.componentType;
|
||||
if (targetComponent && (compType === "split-panel-layout" || compType === "v2-split-panel-layout")) {
|
||||
const currentConfig = (targetComponent as any).componentConfig || {};
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const currentComponents = panelConfig.components || [];
|
||||
|
||||
// 드롭 위치 계산
|
||||
const panelRect = splitPanelContainer.getBoundingClientRect();
|
||||
const dropX = (e.clientX - panelRect.left) / zoomLevel;
|
||||
const dropY = (e.clientY - panelRect.top) / zoomLevel;
|
||||
|
||||
// V2 컴포넌트 매핑 사용
|
||||
const v2Mapping = createV2ConfigFromColumn({
|
||||
widgetType: column.widgetType,
|
||||
columnName: column.columnName,
|
||||
columnLabel: column.columnLabel,
|
||||
codeCategory: column.codeCategory,
|
||||
inputType: column.inputType,
|
||||
required: column.required,
|
||||
detailSettings: column.detailSettings,
|
||||
referenceTable: column.referenceTable,
|
||||
referenceColumn: column.referenceColumn,
|
||||
displayColumn: column.displayColumn,
|
||||
});
|
||||
|
||||
// 웹타입별 기본 크기 계산
|
||||
const getPanelComponentSize = (widgetType: string) => {
|
||||
const sizeMap: Record<string, { width: number; height: number }> = {
|
||||
text: { width: 200, height: 36 },
|
||||
number: { width: 150, height: 36 },
|
||||
decimal: { width: 150, height: 36 },
|
||||
date: { width: 180, height: 36 },
|
||||
datetime: { width: 200, height: 36 },
|
||||
select: { width: 200, height: 36 },
|
||||
category: { width: 200, height: 36 },
|
||||
code: { width: 200, height: 36 },
|
||||
entity: { width: 220, height: 36 },
|
||||
boolean: { width: 120, height: 36 },
|
||||
checkbox: { width: 120, height: 36 },
|
||||
textarea: { width: 300, height: 100 },
|
||||
file: { width: 250, height: 80 },
|
||||
};
|
||||
return sizeMap[widgetType] || { width: 200, height: 36 };
|
||||
};
|
||||
|
||||
const componentSize = getPanelComponentSize(column.widgetType);
|
||||
|
||||
const newPanelComponent = {
|
||||
id: `panel_comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
componentType: v2Mapping.componentType,
|
||||
label: column.columnLabel || column.columnName,
|
||||
position: { x: Math.max(0, dropX), y: Math.max(0, dropY) },
|
||||
size: componentSize,
|
||||
inputType: column.inputType || column.widgetType,
|
||||
widgetType: column.widgetType,
|
||||
componentConfig: {
|
||||
...v2Mapping.componentConfig,
|
||||
columnName: column.columnName,
|
||||
tableName: column.tableName,
|
||||
inputType: column.inputType || column.widgetType,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedPanelConfig = {
|
||||
...panelConfig,
|
||||
components: [...currentComponents, newPanelComponent],
|
||||
};
|
||||
|
||||
const updatedComponent = {
|
||||
...targetComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: updatedPanelConfig,
|
||||
},
|
||||
};
|
||||
|
||||
const newLayout = {
|
||||
...layout,
|
||||
components: layout.components.map((c) =>
|
||||
c.id === containerId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
toast.success(`컬럼이 ${panelSide === "left" ? "좌측" : "우측"} 패널에 추가되었습니다`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
|
|
@ -5123,6 +5321,158 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
</div>
|
||||
);
|
||||
})()
|
||||
) : selectedPanelComponentInfo ? (
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 시 V2PropertiesPanel 사용
|
||||
(() => {
|
||||
const panelComp = selectedPanelComponentInfo.component;
|
||||
|
||||
// 분할 패널 내부 컴포넌트를 ComponentData 형식으로 변환
|
||||
const panelComponentAsComponentData: ComponentData = {
|
||||
id: panelComp.id,
|
||||
type: "component",
|
||||
componentType: panelComp.componentType,
|
||||
label: panelComp.label,
|
||||
position: panelComp.position || { x: 0, y: 0 },
|
||||
size: panelComp.size || { width: 200, height: 100 },
|
||||
componentConfig: panelComp.componentConfig || {},
|
||||
style: panelComp.style || {},
|
||||
} as ComponentData;
|
||||
|
||||
// 분할 패널 내부 컴포넌트용 속성 업데이트 핸들러
|
||||
const updatePanelComponentProperty = (componentId: string, path: string, value: any) => {
|
||||
const { splitPanelId, panelSide } = selectedPanelComponentInfo;
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
|
||||
setLayout((prevLayout) => {
|
||||
const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId);
|
||||
if (!splitPanelComponent) return prevLayout;
|
||||
|
||||
const currentConfig = (splitPanelComponent as any).componentConfig || {};
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const components = panelConfig.components || [];
|
||||
|
||||
// 해당 컴포넌트 찾기
|
||||
const targetCompIndex = components.findIndex((c: any) => c.id === componentId);
|
||||
if (targetCompIndex === -1) return prevLayout;
|
||||
|
||||
// 컴포넌트 속성 업데이트
|
||||
const targetComp = components[targetCompIndex];
|
||||
const updatedComp = path === "style"
|
||||
? { ...targetComp, style: value }
|
||||
: path.includes(".")
|
||||
? (() => {
|
||||
const parts = path.split(".");
|
||||
let obj = { ...targetComp };
|
||||
let current: any = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
current[parts[i]] = { ...current[parts[i]] };
|
||||
current = current[parts[i]];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
return obj;
|
||||
})()
|
||||
: { ...targetComp, [path]: value };
|
||||
|
||||
const updatedComponents = [
|
||||
...components.slice(0, targetCompIndex),
|
||||
updatedComp,
|
||||
...components.slice(targetCompIndex + 1),
|
||||
];
|
||||
|
||||
const updatedComponent = {
|
||||
...splitPanelComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: {
|
||||
...panelConfig,
|
||||
components: updatedComponents,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// selectedPanelComponentInfo 업데이트
|
||||
setSelectedPanelComponentInfo(prev =>
|
||||
prev ? { ...prev, component: updatedComp } : null
|
||||
);
|
||||
|
||||
return {
|
||||
...prevLayout,
|
||||
components: prevLayout.components.map((c) =>
|
||||
c.id === splitPanelId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 분할 패널 내부 컴포넌트 삭제 핸들러
|
||||
const deletePanelComponent = (componentId: string) => {
|
||||
const { splitPanelId, panelSide } = selectedPanelComponentInfo;
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
|
||||
setLayout((prevLayout) => {
|
||||
const splitPanelComponent = prevLayout.components.find((c) => c.id === splitPanelId);
|
||||
if (!splitPanelComponent) return prevLayout;
|
||||
|
||||
const currentConfig = (splitPanelComponent as any).componentConfig || {};
|
||||
const panelConfig = currentConfig[panelKey] || {};
|
||||
const components = panelConfig.components || [];
|
||||
|
||||
const updatedComponents = components.filter((c: any) => c.id !== componentId);
|
||||
|
||||
const updatedComponent = {
|
||||
...splitPanelComponent,
|
||||
componentConfig: {
|
||||
...currentConfig,
|
||||
[panelKey]: {
|
||||
...panelConfig,
|
||||
components: updatedComponents,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setSelectedPanelComponentInfo(null);
|
||||
|
||||
return {
|
||||
...prevLayout,
|
||||
components: prevLayout.components.map((c) =>
|
||||
c.id === splitPanelId ? updatedComponent : c
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between border-b px-4 py-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
분할 패널 ({selectedPanelComponentInfo.panelSide === "left" ? "좌측" : "우측"}) 컴포넌트
|
||||
</span>
|
||||
<button
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setSelectedPanelComponentInfo(null)}
|
||||
>
|
||||
선택 해제
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<V2PropertiesPanel
|
||||
selectedComponent={panelComponentAsComponentData}
|
||||
tables={tables}
|
||||
onUpdateProperty={updatePanelComponentProperty}
|
||||
onDeleteComponent={deletePanelComponent}
|
||||
currentTable={tables.length > 0 ? tables[0] : undefined}
|
||||
currentTableName={selectedScreen?.tableName}
|
||||
currentScreenCompanyCode={selectedScreen?.companyCode}
|
||||
onStyleChange={(style) => {
|
||||
updatePanelComponentProperty(panelComp.id, "style", style);
|
||||
}}
|
||||
allComponents={layout.components}
|
||||
menuObjid={menuObjid}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<V2PropertiesPanel
|
||||
selectedComponent={selectedComponent || undefined}
|
||||
|
|
@ -5514,6 +5864,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU
|
|||
? selectedTabComponentInfo.componentId
|
||||
: undefined
|
||||
}
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 핸들러
|
||||
onSelectPanelComponent={(panelSide, compId, comp) =>
|
||||
handleSelectPanelComponent(component.id, panelSide, compId, comp)
|
||||
}
|
||||
selectedPanelComponentId={
|
||||
selectedPanelComponentInfo?.splitPanelId === component.id
|
||||
? selectedPanelComponentInfo.componentId
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{/* 컨테이너, 그룹, 영역, 컴포넌트의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */}
|
||||
{(component.type === "group" ||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,9 @@ export interface DynamicComponentRendererProps {
|
|||
// 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void;
|
||||
selectedTabComponentId?: string;
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 콜백
|
||||
onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: any) => void;
|
||||
selectedPanelComponentId?: string;
|
||||
flowSelectedStepId?: number | null;
|
||||
onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void;
|
||||
// 테이블 새로고침 키
|
||||
|
|
@ -494,6 +497,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
// 🆕 탭 내부 컴포넌트 선택 콜백
|
||||
onSelectTabComponent: props.onSelectTabComponent,
|
||||
selectedTabComponentId: props.selectedTabComponentId,
|
||||
// 🆕 분할 패널 내부 컴포넌트 선택 콜백
|
||||
onSelectPanelComponent: props.onSelectPanelComponent,
|
||||
selectedPanelComponentId: props.selectedPanelComponentId,
|
||||
};
|
||||
|
||||
// 렌더러가 클래스인지 함수인지 확인
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
ChevronRight,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Settings,
|
||||
Move,
|
||||
} from "lucide-react";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
|
|
@ -37,9 +39,16 @@ import { useTableOptions } from "@/contexts/TableOptionsContext";
|
|||
import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useSplitPanel } from "./SplitPanelContext";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { PanelInlineComponent } from "./types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||
// 추가 props
|
||||
onUpdateComponent?: (component: any) => void;
|
||||
// 🆕 패널 내부 컴포넌트 선택 콜백 (탭 컴포넌트와 동일 구조)
|
||||
onSelectPanelComponent?: (panelSide: "left" | "right", compId: string, comp: PanelInlineComponent) => void;
|
||||
selectedPanelComponentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -52,6 +61,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
isSelected = false,
|
||||
isPreview = false,
|
||||
onClick,
|
||||
onUpdateComponent,
|
||||
onSelectPanelComponent,
|
||||
selectedPanelComponentId: externalSelectedPanelComponentId,
|
||||
...props
|
||||
}) => {
|
||||
const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig;
|
||||
|
|
@ -181,6 +193,207 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
const [rightCategoryMappings, setRightCategoryMappings] = useState<
|
||||
Record<string, Record<string, { label: string; color?: string }>>
|
||||
>({}); // 우측 카테고리 매핑
|
||||
|
||||
// 🆕 커스텀 모드: 드래그/리사이즈 상태
|
||||
const [draggingCompId, setDraggingCompId] = useState<string | null>(null);
|
||||
const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
const [resizingCompId, setResizingCompId] = useState<string | null>(null);
|
||||
const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null);
|
||||
// 🆕 외부에서 전달받은 선택 상태 사용 (탭 컴포넌트와 동일 구조)
|
||||
const selectedPanelComponentId = externalSelectedPanelComponentId || null;
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
// 🆕 10px 단위 스냅 함수
|
||||
const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []);
|
||||
|
||||
// 🆕 커스텀 모드: 컴포넌트 삭제 핸들러
|
||||
const handleRemovePanelComponent = useCallback(
|
||||
(panelSide: "left" | "right", compId: string) => {
|
||||
if (!onUpdateComponent) return;
|
||||
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
const panelConfig = componentConfig[panelKey] || {};
|
||||
const updatedComponents = (panelConfig.components || []).filter(
|
||||
(c: PanelInlineComponent) => c.id !== compId
|
||||
);
|
||||
|
||||
onUpdateComponent({
|
||||
...component,
|
||||
componentConfig: {
|
||||
...componentConfig,
|
||||
[panelKey]: {
|
||||
...panelConfig,
|
||||
components: updatedComponents,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[component, componentConfig, onUpdateComponent]
|
||||
);
|
||||
|
||||
// 🆕 커스텀 모드: 드래그 시작 핸들러
|
||||
const handlePanelDragStart = useCallback(
|
||||
(e: React.MouseEvent, panelSide: "left" | "right", comp: PanelInlineComponent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const startMouseX = e.clientX;
|
||||
const startMouseY = e.clientY;
|
||||
const startLeft = comp.position?.x || 0;
|
||||
const startTop = comp.position?.y || 0;
|
||||
|
||||
setDraggingCompId(comp.id);
|
||||
setDragPosition({ x: startLeft, y: startTop });
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
const deltaX = moveEvent.clientX - startMouseX;
|
||||
const deltaY = moveEvent.clientY - startMouseY;
|
||||
const newX = Math.max(0, startLeft + deltaX);
|
||||
const newY = Math.max(0, startTop + deltaY);
|
||||
setDragPosition({ x: newX, y: newY });
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = (upEvent: MouseEvent) => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
|
||||
const deltaX = upEvent.clientX - startMouseX;
|
||||
const deltaY = upEvent.clientY - startMouseY;
|
||||
const newX = Math.max(0, startLeft + deltaX);
|
||||
const newY = Math.max(0, startTop + deltaY);
|
||||
|
||||
setDraggingCompId(null);
|
||||
setDragPosition(null);
|
||||
|
||||
if (onUpdateComponent) {
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
const panelConfig = componentConfig[panelKey] || {};
|
||||
const updatedComponents = (panelConfig.components || []).map((c: PanelInlineComponent) =>
|
||||
c.id === comp.id
|
||||
? { ...c, position: { x: Math.round(newX), y: Math.round(newY) } }
|
||||
: c
|
||||
);
|
||||
|
||||
onUpdateComponent({
|
||||
...component,
|
||||
componentConfig: {
|
||||
...componentConfig,
|
||||
[panelKey]: {
|
||||
...panelConfig,
|
||||
components: updatedComponents,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
},
|
||||
[component, componentConfig, onUpdateComponent]
|
||||
);
|
||||
|
||||
// 🆕 커스텀 모드: 리사이즈 시작 핸들러
|
||||
const handlePanelResizeStart = useCallback(
|
||||
(e: React.MouseEvent, panelSide: "left" | "right", comp: PanelInlineComponent, direction: "e" | "s" | "se") => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const startMouseX = e.clientX;
|
||||
const startMouseY = e.clientY;
|
||||
const startWidth = comp.size?.width || 200;
|
||||
const startHeight = comp.size?.height || 100;
|
||||
|
||||
setResizingCompId(comp.id);
|
||||
setResizeSize({ width: startWidth, height: startHeight });
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
const deltaX = moveEvent.clientX - startMouseX;
|
||||
const deltaY = moveEvent.clientY - startMouseY;
|
||||
|
||||
let newWidth = startWidth;
|
||||
let newHeight = startHeight;
|
||||
|
||||
if (direction === "e" || direction === "se") {
|
||||
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
|
||||
}
|
||||
if (direction === "s" || direction === "se") {
|
||||
newHeight = snapTo10(Math.max(30, startHeight + deltaY));
|
||||
}
|
||||
|
||||
setResizeSize({ width: newWidth, height: newHeight });
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = (upEvent: MouseEvent) => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
|
||||
const deltaX = upEvent.clientX - startMouseX;
|
||||
const deltaY = upEvent.clientY - startMouseY;
|
||||
|
||||
let newWidth = startWidth;
|
||||
let newHeight = startHeight;
|
||||
|
||||
if (direction === "e" || direction === "se") {
|
||||
newWidth = snapTo10(Math.max(50, startWidth + deltaX));
|
||||
}
|
||||
if (direction === "s" || direction === "se") {
|
||||
newHeight = snapTo10(Math.max(30, startHeight + deltaY));
|
||||
}
|
||||
|
||||
setResizingCompId(null);
|
||||
setResizeSize(null);
|
||||
|
||||
if (onUpdateComponent) {
|
||||
const panelKey = panelSide === "left" ? "leftPanel" : "rightPanel";
|
||||
const panelConfig = componentConfig[panelKey] || {};
|
||||
const updatedComponents = (panelConfig.components || []).map((c: PanelInlineComponent) =>
|
||||
c.id === comp.id
|
||||
? { ...c, size: { width: newWidth, height: newHeight } }
|
||||
: c
|
||||
);
|
||||
|
||||
onUpdateComponent({
|
||||
...component,
|
||||
componentConfig: {
|
||||
...componentConfig,
|
||||
[panelKey]: {
|
||||
...panelConfig,
|
||||
components: updatedComponents,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
},
|
||||
[component, componentConfig, onUpdateComponent, snapTo10]
|
||||
);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
// 추가 모달 상태
|
||||
|
|
@ -2079,8 +2292,191 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</div>
|
||||
)}
|
||||
<CardContent className="flex-1 overflow-auto p-4">
|
||||
{/* 좌측 데이터 목록/테이블 */}
|
||||
{componentConfig.leftPanel?.displayMode === "table" ? (
|
||||
{/* 좌측 데이터 목록/테이블/커스텀 */}
|
||||
{componentConfig.leftPanel?.displayMode === "custom" ? (
|
||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
||||
<div
|
||||
className="relative h-full w-full"
|
||||
data-split-panel-container="true"
|
||||
data-component-id={component.id}
|
||||
data-panel-side="left"
|
||||
>
|
||||
{/* 🆕 커스텀 모드: 디자인/실행 모드 통합 렌더링 */}
|
||||
{componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? (
|
||||
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||
{componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => {
|
||||
const isSelectedComp = selectedPanelComponentId === comp.id;
|
||||
const isDraggingComp = draggingCompId === comp.id;
|
||||
const isResizingComp = resizingCompId === comp.id;
|
||||
|
||||
// 드래그/리사이즈 중 표시할 크기/위치
|
||||
const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0);
|
||||
const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0);
|
||||
const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200);
|
||||
const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100);
|
||||
|
||||
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position || { x: 0, y: 0 },
|
||||
size: { width: displayWidth, height: displayHeight },
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style || {},
|
||||
};
|
||||
|
||||
if (isDesignMode) {
|
||||
// 디자인 모드: 탭 컴포넌트와 동일하게 실제 컴포넌트 렌더링
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
data-panel-comp-id={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: displayX,
|
||||
top: displayY,
|
||||
zIndex: isDraggingComp ? 100 : isSelectedComp ? 10 : 1,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectPanelComponent?.("left", comp.id, comp);
|
||||
}}
|
||||
>
|
||||
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
|
||||
isSelectedComp ? "border-primary" : "border-gray-200"
|
||||
)}
|
||||
style={{ width: displayWidth }}
|
||||
onMouseDown={(e) => handlePanelDragStart(e, "left", comp)}
|
||||
>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Move className="h-2.5 w-2.5 text-gray-400" />
|
||||
<span className="max-w-[100px] truncate text-[9px] text-gray-500">
|
||||
{comp.label || comp.componentType}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-gray-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectPanelComponent?.("left", comp.id, comp);
|
||||
}}
|
||||
title="설정"
|
||||
>
|
||||
<Settings className="h-2.5 w-2.5 text-gray-500" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-red-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemovePanelComponent("left", comp.id);
|
||||
}}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-2.5 w-2.5 text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-b border bg-white shadow-sm",
|
||||
isSelectedComp
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-gray-200",
|
||||
(isDraggingComp || isResizingComp) && "opacity-80 shadow-lg",
|
||||
!(isDraggingComp || isResizingComp) && "transition-all"
|
||||
)}
|
||||
style={{
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
}}
|
||||
>
|
||||
<div className="pointer-events-none h-full w-full">
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={true}
|
||||
formData={{}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 리사이즈 가장자리 영역 - 선택된 컴포넌트에만 표시 */}
|
||||
{isSelectedComp && (
|
||||
<>
|
||||
{/* 오른쪽 가장자리 (너비 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute right-0 top-0 z-10 h-full w-2 cursor-ew-resize hover:bg-primary/10"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "left", comp, "e")}
|
||||
/>
|
||||
{/* 아래 가장자리 (높이 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute bottom-0 left-0 z-10 h-2 w-full cursor-ns-resize hover:bg-primary/10"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "left", comp, "s")}
|
||||
/>
|
||||
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute bottom-0 right-0 z-20 h-3 w-3 cursor-nwse-resize hover:bg-primary/20"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "left", comp, "se")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// 실행 모드: DynamicComponentRenderer로 렌더링
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position || { x: 0, y: 0 },
|
||||
size: comp.size || { width: 400, height: 300 },
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style || {},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 400,
|
||||
height: comp.size?.height || 300,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={false}
|
||||
formData={{}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// 컴포넌트가 없을 때 드롭 영역 표시
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
|
||||
<Plus className="mb-2 h-8 w-8 text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
커스텀 모드
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
{isDesignMode ? "컴포넌트를 드래그하여 배치하세요" : "배치된 컴포넌트가 없습니다"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : componentConfig.leftPanel?.displayMode === "table" ? (
|
||||
// 테이블 모드
|
||||
<div className="w-full">
|
||||
{isDesignMode ? (
|
||||
|
|
@ -2577,8 +2973,180 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</div>
|
||||
)}
|
||||
<CardContent className="flex-1 overflow-auto p-4">
|
||||
{/* 우측 데이터 */}
|
||||
{isLoadingRight ? (
|
||||
{/* 우측 데이터/커스텀 */}
|
||||
{componentConfig.rightPanel?.displayMode === "custom" ? (
|
||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
||||
<div
|
||||
className="relative h-full w-full"
|
||||
data-split-panel-container="true"
|
||||
data-component-id={component.id}
|
||||
data-panel-side="right"
|
||||
>
|
||||
{/* 🆕 커스텀 모드: 디자인/실행 모드 통합 렌더링 */}
|
||||
{componentConfig.rightPanel?.components && componentConfig.rightPanel.components.length > 0 ? (
|
||||
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||
{componentConfig.rightPanel.components.map((comp: PanelInlineComponent) => {
|
||||
const isSelectedComp = selectedPanelComponentId === comp.id;
|
||||
const isDraggingComp = draggingCompId === comp.id;
|
||||
const isResizingComp = resizingCompId === comp.id;
|
||||
|
||||
// 드래그/리사이즈 중 표시할 크기/위치
|
||||
const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0);
|
||||
const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0);
|
||||
const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200);
|
||||
const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100);
|
||||
|
||||
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position || { x: 0, y: 0 },
|
||||
size: { width: displayWidth, height: displayHeight },
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style || {},
|
||||
};
|
||||
|
||||
if (isDesignMode) {
|
||||
// 디자인 모드: 탭 컴포넌트와 동일하게 실제 컴포넌트 렌더링
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
data-panel-comp-id={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: displayX,
|
||||
top: displayY,
|
||||
zIndex: isDraggingComp ? 100 : isSelectedComp ? 10 : 1,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectPanelComponent?.("right", comp.id, comp);
|
||||
}}
|
||||
>
|
||||
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
|
||||
isSelectedComp ? "border-primary" : "border-gray-200"
|
||||
)}
|
||||
style={{ width: displayWidth }}
|
||||
onMouseDown={(e) => handlePanelDragStart(e, "right", comp)}
|
||||
>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Move className="h-2.5 w-2.5 text-gray-400" />
|
||||
<span className="max-w-[100px] truncate text-[9px] text-gray-500">
|
||||
{comp.label || comp.componentType}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-gray-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectPanelComponent?.("right", comp.id, comp);
|
||||
}}
|
||||
title="설정"
|
||||
>
|
||||
<Settings className="h-2.5 w-2.5 text-gray-500" />
|
||||
</button>
|
||||
<button
|
||||
className="rounded p-0.5 hover:bg-red-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemovePanelComponent("right", comp.id);
|
||||
}}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-2.5 w-2.5 text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-b border bg-white shadow-sm",
|
||||
isSelectedComp
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-gray-200",
|
||||
(isDraggingComp || isResizingComp) && "opacity-80 shadow-lg",
|
||||
!(isDraggingComp || isResizingComp) && "transition-all"
|
||||
)}
|
||||
style={{
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
}}
|
||||
>
|
||||
<div className="pointer-events-none h-full w-full">
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={true}
|
||||
formData={{}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 리사이즈 가장자리 영역 - 선택된 컴포넌트에만 표시 */}
|
||||
{isSelectedComp && (
|
||||
<>
|
||||
{/* 오른쪽 가장자리 (너비 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute right-0 top-0 z-10 h-full w-2 cursor-ew-resize hover:bg-primary/10"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "e")}
|
||||
/>
|
||||
{/* 아래 가장자리 (높이 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute bottom-0 left-0 z-10 h-2 w-full cursor-ns-resize hover:bg-primary/10"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "s")}
|
||||
/>
|
||||
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute bottom-0 right-0 z-20 h-3 w-3 cursor-nwse-resize hover:bg-primary/20"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "se")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 400,
|
||||
height: comp.size?.height || 300,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={false}
|
||||
formData={{}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// 컴포넌트가 없을 때 드롭 영역 표시
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
|
||||
<Plus className="mb-2 h-8 w-8 text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
커스텀 모드
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
{isDesignMode ? "컴포넌트를 드래그하여 배치하세요" : "배치된 컴포넌트가 없습니다"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : isLoadingRight ? (
|
||||
// 로딩 중
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
|||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
// Accordion 제거 - 단순 섹션으로 변경
|
||||
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, Database, GripVertical } from "lucide-react";
|
||||
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, Database, GripVertical, Move } from "lucide-react";
|
||||
import { PanelInlineComponent } from "./types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SplitPanelLayoutConfig, AdditionalTabConfig } from "./types";
|
||||
import { TableInfo, ColumnInfo } from "@/types/screen";
|
||||
|
|
@ -1547,7 +1548,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
<Label>표시 모드</Label>
|
||||
<Select
|
||||
value={config.leftPanel?.displayMode || "list"}
|
||||
onValueChange={(value: "list" | "table") => updateLeftPanel({ displayMode: value })}
|
||||
onValueChange={(value: "list" | "table" | "custom") => updateLeftPanel({ displayMode: value })}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectValue placeholder="표시 모드 선택" />
|
||||
|
|
@ -1565,11 +1566,75 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
<span className="text-xs text-gray-500">컬럼 헤더가 있는 테이블 형식</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">커스텀 (CUSTOM)</span>
|
||||
<span className="text-xs text-gray-500">패널 안에 컴포넌트 자유 배치</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{config.leftPanel?.displayMode === "custom" && (
|
||||
<p className="text-xs text-amber-600">
|
||||
화면 디자이너에서 좌측 패널에 컴포넌트를 드래그하여 배치하세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 좌측 패널 표시 컬럼 설정 - 체크박스 방식 */}
|
||||
{/* 🆕 커스텀 모드: 배치된 컴포넌트 목록 */}
|
||||
{config.leftPanel?.displayMode === "custom" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">배치된 컴포넌트</Label>
|
||||
{!config.leftPanel?.components || config.leftPanel.components.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 p-4 text-center">
|
||||
<Move className="mx-auto mb-2 h-6 w-6 text-muted-foreground" />
|
||||
<p className="text-muted-foreground text-xs">
|
||||
디자인 화면에서 컴포넌트를 드래그하여 추가하세요
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{config.leftPanel.components.map((comp: PanelInlineComponent) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="flex items-center justify-between rounded-md border bg-background p-2"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-medium">
|
||||
{comp.label || comp.componentType}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
{comp.componentType} | 위치: ({comp.position?.x || 0}, {comp.position?.y || 0}) | 크기: {comp.size?.width || 0}x{comp.size?.height || 0}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const updatedComponents = (config.leftPanel?.components || []).filter(
|
||||
(c: PanelInlineComponent) => c.id !== comp.id
|
||||
);
|
||||
onChange({
|
||||
...config,
|
||||
leftPanel: {
|
||||
...config.leftPanel,
|
||||
components: updatedComponents,
|
||||
},
|
||||
});
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 좌측 패널 표시 컬럼 설정 - 체크박스 방식 (커스텀 모드가 아닐 때만) */}
|
||||
{config.leftPanel?.displayMode !== "custom" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">표시할 컬럼 선택</Label>
|
||||
|
||||
|
|
@ -1731,6 +1796,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 좌측 패널 데이터 필터링 */}
|
||||
|
|
@ -1851,7 +1917,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
<Label>표시 모드</Label>
|
||||
<Select
|
||||
value={config.rightPanel?.displayMode || "list"}
|
||||
onValueChange={(value: "list" | "table") => updateRightPanel({ displayMode: value })}
|
||||
onValueChange={(value: "list" | "table" | "custom") => updateRightPanel({ displayMode: value })}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectValue placeholder="표시 모드 선택" />
|
||||
|
|
@ -1869,11 +1935,74 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
<span className="text-xs text-gray-500">컬럼 헤더가 있는 테이블 형식</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">커스텀 (CUSTOM)</span>
|
||||
<span className="text-xs text-gray-500">패널 안에 컴포넌트 자유 배치</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{config.rightPanel?.displayMode === "custom" && (
|
||||
<p className="text-xs text-amber-600">
|
||||
화면 디자이너에서 우측 패널에 컴포넌트를 드래그하여 배치하세요.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 요약 표시 설정 (LIST 모드에서만) */}
|
||||
{/* 🆕 커스텀 모드: 배치된 컴포넌트 목록 */}
|
||||
{config.rightPanel?.displayMode === "custom" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">배치된 컴포넌트</Label>
|
||||
{!config.rightPanel?.components || config.rightPanel.components.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 p-4 text-center">
|
||||
<Move className="mx-auto mb-2 h-6 w-6 text-muted-foreground" />
|
||||
<p className="text-muted-foreground text-xs">
|
||||
디자인 화면에서 컴포넌트를 드래그하여 추가하세요
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{config.rightPanel.components.map((comp: PanelInlineComponent) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="flex items-center justify-between rounded-md border bg-background p-2"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-medium">
|
||||
{comp.label || comp.componentType}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
{comp.componentType} | 위치: ({comp.position?.x || 0}, {comp.position?.y || 0}) | 크기: {comp.size?.width || 0}x{comp.size?.height || 0}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const updatedComponents = (config.rightPanel?.components || []).filter(
|
||||
(c: PanelInlineComponent) => c.id !== comp.id
|
||||
);
|
||||
onChange({
|
||||
...config,
|
||||
rightPanel: {
|
||||
...config.rightPanel,
|
||||
components: updatedComponents,
|
||||
},
|
||||
});
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 요약 표시 설정 (LIST 모드에서만, 커스텀 모드가 아닐 때) */}
|
||||
{(config.rightPanel?.displayMode || "list") === "list" && (
|
||||
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<Label className="text-sm font-semibold">요약 표시 설정</Label>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@
|
|||
* SplitPanelLayout 컴포넌트 타입 정의
|
||||
*/
|
||||
|
||||
import { DataFilterConfig } from "@/types/screen-management";
|
||||
import { DataFilterConfig, TabInlineComponent } from "@/types/screen-management";
|
||||
|
||||
/**
|
||||
* 패널 내 인라인 컴포넌트 (커스텀 모드용)
|
||||
* TabInlineComponent와 동일한 구조 사용
|
||||
*/
|
||||
export type PanelInlineComponent = TabInlineComponent;
|
||||
|
||||
/**
|
||||
* 추가 탭 설정 (우측 패널과 동일한 구조 + tabId, label)
|
||||
|
|
@ -118,7 +124,9 @@ export interface SplitPanelLayoutConfig {
|
|||
useCustomTable?: boolean; // 화면 기본 테이블이 아닌 다른 테이블 사용 여부
|
||||
customTableName?: string; // 사용자 지정 테이블명 (useCustomTable이 true일 때)
|
||||
dataSource?: string; // API 엔드포인트
|
||||
displayMode?: "list" | "table"; // 표시 모드: 목록 또는 테이블
|
||||
displayMode?: "list" | "table" | "custom"; // 표시 모드: 목록, 테이블, 또는 커스텀
|
||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치 (탭 컴포넌트와 동일 구조)
|
||||
components?: PanelInlineComponent[];
|
||||
showSearch?: boolean;
|
||||
showAdd?: boolean;
|
||||
showEdit?: boolean; // 수정 버튼
|
||||
|
|
@ -185,7 +193,9 @@ export interface SplitPanelLayoutConfig {
|
|||
useCustomTable?: boolean; // 화면 기본 테이블이 아닌 다른 테이블 사용 여부
|
||||
customTableName?: string; // 사용자 지정 테이블명 (useCustomTable이 true일 때)
|
||||
dataSource?: string;
|
||||
displayMode?: "list" | "table"; // 표시 모드: 목록 또는 테이블
|
||||
displayMode?: "list" | "table" | "custom"; // 표시 모드: 목록, 테이블, 또는 커스텀
|
||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치 (탭 컴포넌트와 동일 구조)
|
||||
components?: PanelInlineComponent[];
|
||||
showSearch?: boolean;
|
||||
showAdd?: boolean;
|
||||
showEdit?: boolean; // 수정 버튼
|
||||
|
|
|
|||
Loading…
Reference in New Issue