From 3803b7dce1d262f06a7bd9d46aa562e5a6727589 Mon Sep 17 00:00:00 2001 From: juseok2 Date: Thu, 29 Jan 2026 23:20:23 +0900 Subject: [PATCH] =?UTF-8?q?V2=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=84=EB=8B=AC=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=ED=98=84:=20UnifiedRepe?= =?UTF-8?q?ater=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=9C=EA=B3=B5=20=EB=B0=8F=20?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EC=99=80=EC=9D=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=EC=9D=84=20=EA=B0=9C=EC=84=A0=ED=95=98=EC=98=80?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EB=98=90=ED=95=9C,=20Aggregatio?= =?UTF-8?q?nWidgetComponent=EC=99=80=20RepeatContainerComponent=EC=97=90?= =?UTF-8?q?=EC=84=9C=20V2=20=ED=91=9C=EC=A4=80=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EA=B5=AC=EB=8F=85=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EB=A5=BC=20=ED=9A=A8=EC=9C=A8=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=98=EC=98=80=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20=EC=9D=B4=EB=A5=BC=20=ED=86=B5=ED=95=B4?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=84=EC=9D=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=9D=90=EB=A6=84=EA=B3=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B2=BD=ED=97=98=EC=9D=84=20?= =?UTF-8?q?=ED=96=A5=EC=83=81=EC=8B=9C=EC=BC=B0=EC=8A=B5=EB=8B=88=EB=8B=A4?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.frontend.win.yml | 2 +- docs/V2_컴포넌트_분석_가이드.md | 1039 ++++++++++++ docs/V2_컴포넌트_연동_가이드.md | 1496 +++++++++++++++++ .../01_master-data/item-info.md | 974 +++++++++++ docs/screen-implementation-guide/README.md | 126 ++ docs/screen-implementation-guide/_TEMPLATE.md | 212 +++ .../components/unified/UnifiedRepeater.tsx | 132 ++ .../AggregationWidgetComponent.tsx | 103 +- .../RepeatContainerComponent.tsx | 52 +- frontend/types/component-events.ts | 241 +++ my_layout.json | Bin 0 -> 17330 bytes scripts/dev/start-all-parallel.bat | 116 ++ scripts/dev/start-all-parallel.ps1 | 183 ++ scripts/dev/stop-all.bat | 30 + scripts/dev/stop-all.ps1 | 33 + working_layout.json | Bin 0 -> 32716 bytes 16 files changed, 4654 insertions(+), 85 deletions(-) create mode 100644 docs/V2_컴포넌트_분석_가이드.md create mode 100644 docs/V2_컴포넌트_연동_가이드.md create mode 100644 docs/screen-implementation-guide/01_master-data/item-info.md create mode 100644 docs/screen-implementation-guide/README.md create mode 100644 docs/screen-implementation-guide/_TEMPLATE.md create mode 100644 frontend/types/component-events.ts create mode 100644 my_layout.json create mode 100644 scripts/dev/start-all-parallel.bat create mode 100644 scripts/dev/start-all-parallel.ps1 create mode 100644 scripts/dev/stop-all.bat create mode 100644 scripts/dev/stop-all.ps1 create mode 100644 working_layout.json diff --git a/docker-compose.frontend.win.yml b/docker-compose.frontend.win.yml index db9722d8..f81e2287 100644 --- a/docker-compose.frontend.win.yml +++ b/docker-compose.frontend.win.yml @@ -5,7 +5,7 @@ services: frontend: build: context: ./frontend - dockerfile: Dockerfile.dev + dockerfile: ../docker/dev/frontend.Dockerfile container_name: pms-frontend-win ports: - "9771:3000" diff --git a/docs/V2_컴포넌트_분석_가이드.md b/docs/V2_컴포넌트_분석_가이드.md new file mode 100644 index 00000000..55598e02 --- /dev/null +++ b/docs/V2_컴포넌트_분석_가이드.md @@ -0,0 +1,1039 @@ +# V2 컴포넌트 분석 가이드 + +## 개요 + +V2 컴포넌트는 **화면관리 시스템 전용**으로 개발된 컴포넌트 세트입니다. 기존 컴포넌트와의 충돌을 방지하고, 새로운 기능(엔티티 조인, 다국어 지원, 커스텀 테이블 등)을 지원합니다. + +### 핵심 원칙 + +- 모든 V2 컴포넌트는 `v2-` 접두사를 사용 +- 원본 컴포넌트는 기존 화면 호환성 유지용으로 보존 +- 새로운 화면 개발 시 반드시 V2 컴포넌트만 사용 +- Definition 이름에 `V2` 접두사 사용 (예: `V2TableListDefinition`) + +### 파일 경로 + +``` +frontend/lib/registry/components/ +├── v2-button-primary/ ← V2 컴포넌트 (수정 대상) +├── v2-table-list/ ← V2 컴포넌트 (수정 대상) +├── v2-split-panel-layout/ ← V2 컴포넌트 (수정 대상) +├── ... +├── button-primary/ ← 원본 (수정 금지) +├── table-list/ ← 원본 (수정 금지) +└── ... +``` + +--- + +## V2 컴포넌트 목록 (17개) + +| 컴포넌트 ID | 이름 | 카테고리 | 용도 | +|------------|------|----------|------| +| `v2-table-list` | 테이블 리스트 | DISPLAY | 데이터 목록 표시 (테이블/카드 모드) | +| `v2-split-panel-layout` | 분할 패널 | DISPLAY | 마스터-디테일 레이아웃 | +| `v2-unified-repeater` | 통합 리피터 | UNIFIED | 반복 데이터 관리 (인라인/모달/버튼) | +| `v2-pivot-grid` | 피벗 그리드 | DISPLAY | 다차원 데이터 분석 피벗 테이블 | +| `v2-button-primary` | 기본 버튼 | ACTION | 저장/삭제 등 액션 버튼 | +| `v2-text-display` | 텍스트 표시 | DISPLAY | 텍스트/라벨 표시 | +| `v2-divider-line` | 구분선 | DISPLAY | 시각적 구분선 | +| `v2-card-display` | 카드 디스플레이 | DISPLAY | 카드 형태 데이터 표시 | +| `v2-numbering-rule` | 채번 규칙 | DISPLAY | 코드 자동 채번 설정 | +| `v2-table-search-widget` | 검색 필터 | DISPLAY | 테이블 검색/필터 위젯 | +| `v2-section-paper` | 섹션 페이퍼 | LAYOUT | 섹션 구분 컨테이너 | +| `v2-section-card` | 섹션 카드 | LAYOUT | 카드형 섹션 컨테이너 | +| `v2-tabs-widget` | 탭 위젯 | LAYOUT | 탭 기반 콘텐츠 전환 | +| `v2-location-swap-selector` | 위치 선택 | INPUT | 출발지/도착지 스왑 선택 | +| `v2-rack-structure` | 렉 구조 | DISPLAY | 창고 렉 시각화 | +| `v2-aggregation-widget` | 집계 위젯 | DISPLAY | 데이터 집계 (합계/평균/개수) | +| `v2-repeat-container` | 리피터 컨테이너 | LAYOUT | 데이터 수만큼 반복 렌더링 | + +--- + +## 주요 컴포넌트 상세 분석 + +### 1. v2-table-list (테이블 리스트) + +**용도**: 데이터베이스 테이블 데이터를 테이블/카드 형태로 표시 + +#### 주요 특징 + +- 테이블 모드 / 카드 모드 전환 가능 +- 페이지네이션, 정렬, 필터링 지원 +- 체크박스 선택 (단일/다중) +- 가로 스크롤 및 컬럼 고정 +- 엔티티 조인 컬럼 지원 +- 인라인 편집 기능 +- Excel 내보내기 + +#### 데이터 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ v2-table-list │ +├─────────────────────────────────────────────────────────────┤ +│ ① config.selectedTable / customTableName 확인 │ +│ ↓ │ +│ ② tableTypeApi.getData() 호출 │ +│ ↓ │ +│ ③ entityJoinApi.getEntityJoinColumns() 조인 컬럼 로드 │ +│ ↓ │ +│ ④ 데이터 + 조인 데이터 병합 │ +│ ↓ │ +│ ⑤ 테이블/카드 모드로 렌더링 │ +│ ↓ │ +│ ⑥ onRowClick / onSelectionChange 이벤트 발생 │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 주요 설정 인터페이스 + +```typescript +interface TableListConfig { + // 표시 모드 + displayMode: "table" | "card"; + + // 커스텀 테이블 설정 + customTableName?: string; // 커스텀 테이블 + useCustomTable?: boolean; // 커스텀 테이블 사용 여부 + isReadOnly?: boolean; // 읽기전용 + + // 컬럼 설정 + columns: ColumnConfig[]; + + // 페이지네이션 + pagination: { + enabled: boolean; + pageSize: number; + showSizeSelector: boolean; + pageSizeOptions: number[]; + }; + + // 체크박스 + checkbox: { + enabled: boolean; + multiple: boolean; // true: 체크박스, false: 라디오 + position: "left" | "right"; + selectAll: boolean; + }; + + // 필터 + filter: { + enabled: boolean; + filters: FilterConfig[]; + }; + + // 연결된 필터 (다른 컴포넌트 값으로 필터링) + linkedFilters?: LinkedFilterConfig[]; + + // 제외 필터 (다른 테이블에 존재하는 데이터 제외) + excludeFilter?: ExcludeFilterConfig; + + // 가로 스크롤 설정 + horizontalScroll: { + enabled: boolean; + maxVisibleColumns?: number; + minColumnWidth?: number; + maxColumnWidth?: number; + }; +} +``` + +#### 컬럼 설정 + +```typescript +interface ColumnConfig { + columnName: string; // 컬럼명 + displayName: string; // 표시명 + visible: boolean; // 표시 여부 + sortable: boolean; // 정렬 가능 + searchable: boolean; // 검색 가능 + width?: number; // 너비 + align: "left" | "center" | "right"; // 정렬 + format?: "text" | "number" | "date" | "currency" | "boolean"; + + // 엔티티 조인 + isEntityJoin?: boolean; // 조인 컬럼 여부 + entityJoinInfo?: { + sourceTable: string; + sourceColumn: string; + joinAlias: string; + }; + + // 컬럼 고정 + fixed?: "left" | "right" | false; + + // 자동생성 + autoGeneration?: { + type: "uuid" | "current_user" | "current_time" | "sequence" | "numbering_rule"; + enabled: boolean; + }; + + // 편집 가능 여부 + editable?: boolean; +} +``` + +--- + +### 2. v2-split-panel-layout (분할 패널) + +**용도**: 마스터-디테일 패턴의 좌우 분할 레이아웃 + +#### 주요 특징 + +- 좌측: 마스터 목록 (리스트/테이블 모드) +- 우측: 디테일 정보 (연관 데이터) +- 좌우 비율 조절 가능 (드래그 리사이즈) +- 다중 탭 지원 (우측 패널) +- N:M 관계 데이터 지원 +- 중복 제거 기능 + +#### 데이터 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ v2-split-panel-layout │ +├─────────────────────────────────────────────────────────────┤ +│ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ 좌측 패널 │ ───────→ │ 우측 패널 │ │ +│ │ (마스터) │ 선택 이벤트│ (디테일) │ │ +│ └──────────────┘ └──────────────────────┘ │ +│ │ │ │ +│ ↓ ↓ │ +│ leftPanel.tableName rightPanel.tableName │ +│ leftPanel.columns rightPanel.relation │ +│ │ │ │ +│ ↓ ↓ │ +│ 좌측 데이터 조회 ─────────→ 관계 설정에 따라 우측 필터링 │ +│ (독립 API 호출) (FK/조인 키 기반) │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 주요 설정 인터페이스 + +```typescript +interface SplitPanelLayoutConfig { + // 좌측 패널 + leftPanel: { + title: string; + tableName?: string; + useCustomTable?: boolean; + customTableName?: string; + displayMode?: "list" | "table"; + showSearch?: boolean; + showAdd?: boolean; + showEdit?: boolean; + showDelete?: boolean; + columns?: ColumnConfig[]; + tableConfig?: TableDisplayConfig; + dataFilter?: DataFilterConfig; + }; + + // 우측 패널 + rightPanel: { + title: string; + tableName?: string; + displayMode?: "list" | "table"; + columns?: ColumnConfig[]; + + // 관계 설정 + relation?: { + type?: "join" | "detail"; + leftColumn?: string; // 좌측 조인 컬럼 + rightColumn?: string; // 우측 조인 컬럼 + foreignKey?: string; // FK 컬럼 + keys?: Array<{ // 복합키 지원 + leftColumn: string; + rightColumn: string; + }>; + }; + + // 추가 설정 (N:M 관계) + addConfig?: { + targetTable?: string; // 실제 INSERT 테이블 + autoFillColumns?: Record; + leftPanelColumn?: string; + targetColumn?: string; + }; + + // 중복 제거 + deduplication?: { + enabled: boolean; + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + }; + + // 추가 탭 + additionalTabs?: AdditionalTabConfig[]; + }; + + // 레이아웃 + splitRatio?: number; // 좌우 비율 (0-100) + resizable?: boolean; // 크기 조절 가능 + minLeftWidth?: number; + minRightWidth?: number; + + // 동작 + autoLoad?: boolean; + syncSelection?: boolean; +} +``` + +--- + +### 3. v2-unified-repeater (통합 리피터) + +**용도**: 반복 데이터 관리 (기존 여러 리피터 통합) + +#### 주요 특징 + +- 3가지 렌더링 모드: 인라인/모달/버튼 +- 마스터-디테일 FK 자동 연결 +- 저장 테이블 분리 가능 +- 행 추가/삭제, 드래그 정렬 +- 선택 기능 (단일/다중) + +#### 데이터 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ v2-unified-repeater │ +├─────────────────────────────────────────────────────────────┤ +│ ① 마스터 저장 이벤트 수신 (repeaterSave) │ +│ ↓ │ +│ ② masterRecordId 전달받음 │ +│ ↓ │ +│ ③ foreignKeyColumn에 masterRecordId 자동 설정 │ +│ ↓ │ +│ ④ dataSource.tableName으로 데이터 저장 │ +│ ↓ │ +│ ⑤ 저장 완료 후 onDataChange 이벤트 발생 │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 주요 설정 인터페이스 + +```typescript +interface UnifiedRepeaterConfig { + // 렌더링 모드 + renderMode: "inline" | "modal" | "button" | "mixed"; + + // 데이터 소스 + dataSource: { + tableName: string; // 저장 테이블 + foreignKey: string; // FK 컬럼 + referenceKey: string; // 참조할 PK 컬럼 + }; + + // 컬럼 설정 + columns: ColumnConfig[]; + + // 모달 설정 + modal: { + size: "sm" | "md" | "lg" | "xl"; + }; + + // 버튼 설정 + button: { + sourceType: "manual" | "auto"; + manualButtons: ButtonConfig[]; + layout: "horizontal" | "vertical"; + style: "outline" | "solid"; + }; + + // 기능 설정 + features: { + showAddButton: boolean; + showDeleteButton: boolean; + inlineEdit: boolean; + dragSort: boolean; + showRowNumber: boolean; + selectable: boolean; + multiSelect: boolean; + }; +} +``` + +#### 데이터 전달 인터페이스 + +v2-unified-repeater는 **DataProvidable**과 **DataReceivable** 인터페이스를 구현하여 다른 컴포넌트와 데이터를 주고받을 수 있습니다. + +**DataProvidable 구현**: + +```typescript +// 다른 컴포넌트에서 이 리피터의 데이터를 가져갈 수 있음 +const dataProvider: DataProvidable = { + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "unified-repeater", + + // 선택된 행 데이터 반환 + getSelectedData: () => { + return Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean); + }, + + // 전체 데이터 반환 + getAllData: () => { + return [...data]; + }, + + // 선택 초기화 + clearSelection: () => { + setSelectedRows(new Set()); + }, +}; +``` + +**DataReceivable 구현**: + +```typescript +// 외부에서 이 리피터로 데이터를 전달받을 수 있음 +const dataReceiver: DataReceivable = { + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "repeater", + + // 데이터 수신 (append, replace, merge 모드 지원) + receiveData: async (incomingData: any[], config: DataReceiverConfig) => { + // 매핑 규칙 적용 후 모드에 따라 처리 + switch (config.mode) { + case "replace": setData(mappedData); break; + case "merge": /* 중복 제거 후 병합 */ break; + case "append": /* 기존 데이터에 추가 */ break; + } + }, + + // 현재 데이터 반환 + getData: () => [...data], +}; +``` + +**ScreenContext 자동 등록**: + +```typescript +// 컴포넌트 마운트 시 ScreenContext에 자동 등록 +useEffect(() => { + if (screenContext && componentId) { + screenContext.registerDataProvider(componentId, dataProvider); + screenContext.registerDataReceiver(componentId, dataReceiver); + + return () => { + screenContext.unregisterDataProvider(componentId); + screenContext.unregisterDataReceiver(componentId); + }; + } +}, [screenContext, componentId]); +``` + +#### V2 이벤트 시스템 + +**발행 이벤트**: + +| 이벤트 | 발행 시점 | 데이터 | +|--------|----------|--------| +| `repeaterDataChange` | 데이터 변경 시 | `{ componentId, tableName, data, selectedData }` | + +```typescript +// 데이터 변경 시 V2 표준 이벤트 발행 +import { V2_EVENTS, dispatchV2Event } from "@/types/component-events"; + +useEffect(() => { + if (data.length !== prevDataLengthRef.current) { + dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + componentId: parentId || config.fieldName || "unified-repeater", + tableName: config.dataSource?.tableName || "", + data: data, + selectedData: Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean), + }); + } +}, [data, selectedRows]); +``` + +**구독 이벤트**: + +| 이벤트 | 용도 | +|--------|------| +| `beforeFormSave` | 저장 전 데이터 수집 | +| `repeaterSave` | 마스터 저장 후 FK 설정 | +| `componentDataTransfer` | 컴포넌트 간 데이터 전달 수신 | +| `splitPanelDataTransfer` | 분할 패널 간 데이터 전달 수신 | + +--- + +### 4. v2-pivot-grid (피벗 그리드) + +**용도**: 다차원 데이터 분석용 피벗 테이블 + +#### 주요 특징 + +- 행/열/데이터/필터 영역 드래그앤드롭 +- 다양한 집계 함수 (합계, 평균, 개수, 최대, 최소, 고유 개수) +- 소계/총계 표시 (위치 설정 가능) +- 조건부 서식 (색상 스케일, 데이터 바, 아이콘) +- 차트 연동 +- Excel 내보내기 +- 날짜 그룹화 (연/분기/월/주/일) + +#### 데이터 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ v2-pivot-grid │ +├─────────────────────────────────────────────────────────────┤ +│ ① dataSource 설정 (테이블/API/정적 데이터) │ +│ ↓ │ +│ ② fields 설정 (행/열/데이터 필드 배치) │ +│ ↓ │ +│ ③ processPivotData() 로 피벗 계산 │ +│ ↓ │ +│ ④ 집계 함수 적용 (sum, avg, count 등) │ +│ ↓ │ +│ ⑤ PivotResult 생성 (rowHeaders, columnHeaders, dataMatrix)│ +│ ↓ │ +│ ⑥ 조건부 서식 적용 후 렌더링 │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 주요 설정 인터페이스 + +```typescript +interface PivotGridComponentConfig { + // 데이터 소스 + dataSource?: { + type: "table" | "api" | "static"; + tableName?: string; + apiEndpoint?: string; + staticData?: any[]; + filterConditions?: FilterCondition[]; + joinConfigs?: JoinConfig[]; + }; + + // 필드 설정 + fields?: Array<{ + field: string; // 데이터 필드명 + caption: string; // 표시 라벨 + area: "row" | "column" | "data" | "filter"; + areaIndex?: number; // 영역 내 순서 + + // 집계 (data 영역용) + summaryType?: "sum" | "count" | "avg" | "min" | "max" | "countDistinct"; + + // 날짜 그룹화 + groupInterval?: "year" | "quarter" | "month" | "week" | "day"; + + // 포맷 + format?: { + type: "number" | "currency" | "percent" | "date" | "text"; + precision?: number; + thousandSeparator?: boolean; + prefix?: string; + suffix?: string; + }; + }>; + + // 총합계 설정 + totals?: { + showRowGrandTotals?: boolean; + showRowTotals?: boolean; + showColumnGrandTotals?: boolean; + showColumnTotals?: boolean; + rowGrandTotalPosition?: "top" | "bottom"; + columnGrandTotalPosition?: "left" | "right"; + }; + + // 스타일 + style?: { + theme: "default" | "compact" | "modern"; + alternateRowColors?: boolean; + highlightTotals?: boolean; + conditionalFormats?: ConditionalFormatRule[]; + }; + + // 필드 선택기 + fieldChooser?: { + enabled: boolean; + allowSearch?: boolean; + }; + + // 차트 연동 + chart?: { + enabled: boolean; + type: "bar" | "line" | "area" | "pie" | "stackedBar"; + position: "top" | "bottom" | "left" | "right"; + }; +} +``` + +--- + +### 5. v2-aggregation-widget (집계 위젯) + +**용도**: 데이터 집계 결과 표시 (합계, 평균, 개수 등) + +#### 주요 특징 + +- 다양한 집계 타입 (SUM, AVG, COUNT, MIN, MAX) +- 필터링 지원 (폼 데이터 연동) +- 가로/세로 레이아웃 +- 아이콘 표시 +- 폼 변경 시 자동 새로고침 + +#### 데이터 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ v2-aggregation-widget │ +├─────────────────────────────────────────────────────────────┤ +│ ① dataSourceType 확인 (table / repeater) │ +│ ↓ │ +│ ② filters 적용 (필터 조건 구성) │ +│ ↓ │ +│ ③ items 순회하며 각 집계 함수 실행 │ +│ ↓ │ +│ ④ 집계 결과 포맷팅 (천단위 구분, 접두사/접미사) │ +│ ↓ │ +│ ⑤ layout에 따라 렌더링 (horizontal / vertical) │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 주요 설정 인터페이스 + +```typescript +interface AggregationWidgetConfig { + // 데이터 소스 + dataSourceType: "table" | "repeater"; + + // 집계 항목 + items: Array<{ + id: string; + label: string; + columnName: string; + aggregationType: "sum" | "avg" | "count" | "min" | "max"; + format?: { + prefix?: string; + suffix?: string; + thousandSeparator?: boolean; + decimalPlaces?: number; + }; + icon?: string; + color?: string; + }>; + + // 필터 조건 + filters: Array<{ + column: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN"; + value?: any; + valueSource?: "static" | "formData" | "url"; + valueField?: string; + }>; + filterLogic: "AND" | "OR"; + + // 레이아웃 + layout: "horizontal" | "vertical"; + gap: string; + + // 스타일 + showLabels: boolean; + showIcons: boolean; + backgroundColor: string; + borderRadius: string; + padding: string; + + // 동작 + autoRefresh: boolean; + refreshOnFormChange: boolean; +} +``` + +#### V2 이벤트 시스템 + +v2-aggregation-widget은 V2 표준 이벤트 시스템을 사용하여 다른 컴포넌트의 데이터 변경을 감지합니다. + +**구독 이벤트**: + +| 이벤트 | 용도 | 발행자 | +|--------|------|--------| +| `tableListDataChange` | 테이블 데이터 변경 시 집계 갱신 | v2-table-list | +| `repeaterDataChange` | 리피터 데이터 변경 시 집계 갱신 | v2-unified-repeater | + +```typescript +import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events"; + +useEffect(() => { + // 테이블 리스트 데이터 변경 이벤트 구독 + const unsubscribeTableList = subscribeV2Event( + V2_EVENTS.TABLE_LIST_DATA_CHANGE, + (event: CustomEvent) => { + const { data } = event.detail; + // 필터 적용 후 집계 재계산 + const filteredData = applyFilters(data, filters, filterLogic, formData, selectedRows); + setData(filteredData); + } + ); + + // 리피터 데이터 변경 이벤트 구독 + const unsubscribeRepeater = subscribeV2Event( + V2_EVENTS.REPEATER_DATA_CHANGE, + (event: CustomEvent) => { + const { data, selectedData } = event.detail; + const rows = selectedData || data || []; + const filteredData = applyFilters(rows, filters, filterLogic, formData, selectedRows); + setData(filteredData); + } + ); + + return () => { + unsubscribeTableList(); + unsubscribeRepeater(); + }; +}, [dataSourceType, isDesignMode, filterLogic]); +``` + +**참고**: 이전에 사용하던 중복 이벤트(`selectionChange`, `tableSelectionChange`, `rowSelectionChange` 등)는 제거되었습니다. V2 표준 이벤트만 사용합니다. + +--- + +### 6. v2-table-search-widget (검색 필터) + +**용도**: 테이블 데이터 검색 및 필터링 + +#### 주요 특징 + +- 동적/고정 필터 모드 +- 다중 테이블 지원 +- 탭별 필터 값 저장 +- 텍스트/숫자/날짜/셀렉트 필터 타입 +- 다중선택 지원 +- 대상 패널 지정 가능 + +#### 데이터 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ v2-table-search-widget │ +├─────────────────────────────────────────────────────────────┤ +│ ① TableOptionsContext에서 등록된 테이블 목록 조회 │ +│ ↓ │ +│ ② targetPanelPosition에 따라 대상 테이블 필터링 │ +│ ↓ │ +│ ③ 활성 필터 목록 로드 (localStorage에서 복원) │ +│ ↓ │ +│ ④ 필터 값 입력 → handleFilterChange() │ +│ ↓ │ +│ ⑤ currentTable.onFilterChange(filters) 호출 │ +│ ↓ │ +│ ⑥ 연결된 테이블이 자동으로 데이터 재조회 │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 주요 설정 인터페이스 + +```typescript +interface TableSearchWidgetConfig { + // 자동 선택 + autoSelectFirstTable?: boolean; + showTableSelector?: boolean; + + // 필터 모드 + filterMode?: "dynamic" | "preset"; + + // 고정 필터 (preset 모드) + presetFilters?: Array<{ + id: string; + columnName: string; + columnLabel: string; + filterType: "text" | "number" | "date" | "select"; + width?: number; + multiSelect?: boolean; + }>; + + // 대상 패널 위치 + targetPanelPosition?: "left" | "right" | "auto"; +} +``` + +--- + +### 7. v2-repeat-container (리피터 컨테이너) + +**용도**: 데이터 수만큼 내부 컴포넌트를 반복 렌더링 + +#### 주요 특징 + +- 수동/테이블/리피터 데이터 소스 +- 세로/가로/그리드 레이아웃 +- 페이징 지원 +- 클릭 이벤트 (단일/다중 선택) +- 아이템 제목 템플릿 + +#### 데이터 흐름 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ v2-repeat-container │ +├─────────────────────────────────────────────────────────────┤ +│ ① dataSourceType에 따라 데이터 로드 │ +│ - manual: 수동 입력 데이터 │ +│ - table: DB 테이블에서 조회 │ +│ - repeater: 리피터 컴포넌트 데이터 │ +│ ↓ │ +│ ② layout에 따라 배치 (vertical / horizontal / grid) │ +│ ↓ │ +│ ③ 각 아이템에 대해 children 렌더링 │ +│ (RepeatItemContext로 현재 아이템 데이터 전달) │ +│ ↓ │ +│ ④ 클릭 시 선택 상태 관리 (selectionMode: single/multi) │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 주요 설정 인터페이스 + +```typescript +interface RepeatContainerConfig { + // 데이터 소스 + dataSourceType: "manual" | "table" | "repeater"; + tableName?: string; + repeaterComponentId?: string; + manualData?: any[]; + + // 레이아웃 + layout: "vertical" | "horizontal" | "grid"; + gridColumns: number; + gap: string; + + // 스타일 + showBorder: boolean; + showShadow: boolean; + borderRadius: string; + backgroundColor: string; + padding: string; + + // 아이템 제목 + showItemTitle: boolean; + itemTitleTemplate: string; // 예: "${name} - ${code}" + titleFontSize: string; + titleColor: string; + titleFontWeight: string; + + // 빈 상태 + emptyMessage: string; + + // 페이징 + usePaging: boolean; + pageSize: number; + + // 선택 + clickable: boolean; + showSelectedState: boolean; + selectionMode: "single" | "multi"; +} +``` + +#### V2 이벤트 시스템 + +v2-repeat-container는 V2 표준 이벤트 시스템을 사용하여 다른 컴포넌트의 데이터 변경을 감지하고 반복 렌더링합니다. + +**구독 이벤트**: + +| 이벤트 | 용도 | 발행자 | +|--------|------|--------| +| `tableListDataChange` | 테이블 데이터 변경 시 반복 항목 갱신 | v2-table-list | +| `repeaterDataChange` | 리피터 데이터 변경 시 반복 항목 갱신 | v2-unified-repeater | + +```typescript +import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events"; + +useEffect(() => { + // 공통 데이터 처리 함수 + const processIncomingData = (componentId: string | undefined, tableName: string | undefined, eventData: any[]) => { + // dataSourceComponentId가 설정된 경우 해당 컴포넌트만 매칭 + if (dataSourceComponentId && componentId === dataSourceComponentId) { + setData(eventData); + setCurrentPage(1); + setSelectedIndices([]); + } + // 테이블명으로 매칭 + else if (effectiveTableName && tableName === effectiveTableName) { + setData(eventData); + setCurrentPage(1); + setSelectedIndices([]); + } + }; + + // V2 표준 이벤트 구독 + const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, (event) => { + const { componentId, tableName, data } = event.detail; + processIncomingData(componentId, tableName, data); + }); + + const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, (event) => { + const { componentId, tableName, data } = event.detail; + processIncomingData(componentId, tableName, data); + }); + + return () => { + unsubscribeTableList(); + unsubscribeRepeater(); + }; +}, [dataSourceComponentId, effectiveTableName, isDesignMode]); +``` + +--- + +## 공통 데이터 흐름 패턴 + +### 1. 엔티티 조인 데이터 로드 + +모든 테이블 기반 V2 컴포넌트는 엔티티 조인을 지원합니다. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 엔티티 조인 흐름 │ +├─────────────────────────────────────────────────────────────┤ +│ ① entityJoinApi.getEntityJoinColumns(tableName) │ +│ ↓ │ +│ ② 응답: { joinTables, availableColumns } │ +│ ↓ │ +│ ③ ConfigPanel에서 조인 컬럼 선택 │ +│ ↓ │ +│ ④ entityJoinApi.getTableDataWithJoins() 데이터 조회 │ +│ ↓ │ +│ ⑤ "테이블명.컬럼명" 형식으로 조인 데이터 포함 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2. 폼 데이터 관리 + +V2 컴포넌트는 통합 폼 시스템(UnifiedFormContext)을 사용합니다. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 폼 데이터 흐름 │ +├─────────────────────────────────────────────────────────────┤ +│ ① 컴포넌트에서 useFormCompatibility() 훅 사용 │ +│ ↓ │ +│ ② getValue(fieldName) - 값 읽기 │ +│ ③ setValue(fieldName, value) - 값 설정 │ +│ ↓ │ +│ ④ 값 변경이 전체 폼 시스템에 전파 │ +│ ↓ │ +│ ⑤ 저장 버튼 클릭 시 beforeFormSave 이벤트 발생 │ +│ ↓ │ +│ ⑥ 모든 컴포넌트가 현재 값을 formData에 추가 │ +│ ↓ │ +│ ⑦ API 호출하여 저장 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3. 컴포넌트 간 통신 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 컴포넌트 간 통신 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ 버튼 컴포넌트 │ ──────→ │ 리피터 컴포넌트│ │ +│ │ │ repeater│ │ │ +│ │ v2-button │ Save │ v2-unified- │ │ +│ │ -primary │ 이벤트 │ repeater │ │ +│ └──────────────┘ └──────────────┘ │ +│ │ │ │ +│ │ masterRecordId │ │ +│ └────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ 검색 위젯 │ ──────→ │ 테이블 리스트 │ │ +│ │ │ onFilter│ │ │ +│ │ v2-table- │ Change │ v2-table- │ │ +│ │ search-widget│ │ list │ │ +│ └──────────────┘ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 컴포넌트 등록 구조 + +```typescript +// frontend/lib/registry/components/index.ts + +// V2 컴포넌트들 (화면관리 전용) +import "./v2-unified-repeater/UnifiedRepeaterRenderer"; +import "./v2-button-primary/ButtonPrimaryRenderer"; +import "./v2-split-panel-layout/SplitPanelLayoutRenderer"; +import "./v2-aggregation-widget/AggregationWidgetRenderer"; +import "./v2-card-display/CardDisplayRenderer"; +import "./v2-numbering-rule/NumberingRuleRenderer"; +import "./v2-table-list/TableListRenderer"; +import "./v2-text-display/TextDisplayRenderer"; +import "./v2-pivot-grid/PivotGridRenderer"; +import "./v2-divider-line/DividerLineRenderer"; +import "./v2-repeat-container/RepeatContainerRenderer"; +import "./v2-section-card/SectionCardRenderer"; +import "./v2-section-paper/SectionPaperRenderer"; +import "./v2-rack-structure/RackStructureRenderer"; +import "./v2-location-swap-selector/LocationSwapSelectorRenderer"; +import "./v2-table-search-widget"; +import "./v2-tabs-widget/tabs-component"; +``` + +--- + +## 파일 구조 표준 + +각 V2 컴포넌트 폴더는 다음 구조를 따릅니다: + +``` +v2-{component-name}/ +├── index.ts # 컴포넌트 Definition (V2 접두사) +├── types.ts # TypeScript 타입 정의 +├── {Component}Component.tsx # 실제 컴포넌트 구현 +├── {Component}ConfigPanel.tsx # 설정 패널 +├── {Component}Renderer.tsx # 레지스트리 등록 및 래퍼 +├── config.ts # 기본 설정값 (선택) +└── README.md # 사용 가이드 (선택) +``` + +--- + +## 개발 가이드라인 + +### 새 V2 컴포넌트 생성 시 + +1. `v2-` 접두사로 폴더 생성 +2. Definition 이름에 `V2` 접두사 사용 (예: `V2NewComponentDefinition`) +3. `index.ts`에서 import 추가 +4. 엔티티 조인 지원 필수 구현 +5. 다국어 키 필드 추가 (`langKeyId`, `langKey`) + +### 체크리스트 + +- [ ] V2 폴더에서 작업 중인지 확인 +- [ ] 원본 폴더는 수정하지 않음 +- [ ] 컴포넌트 ID에 `v2-` 접두사 사용 +- [ ] Definition 이름에 `V2` 접두사 사용 +- [ ] 엔티티 조인 컬럼 지원 +- [ ] 커스텀 테이블 설정 지원 +- [ ] 다국어 필드 추가 + +--- + +## 관련 파일 목록 + +| 파일 | 역할 | +|------|------| +| `frontend/lib/api/entityJoin.ts` | 엔티티 조인 API | +| `frontend/hooks/useFormCompatibility.ts` | 폼 호환성 브릿지 | +| `frontend/components/unified/UnifiedFormContext.tsx` | 통합 폼 Context | +| `frontend/lib/utils/multilangLabelExtractor.ts` | 다국어 라벨 추출/매핑 | +| `frontend/contexts/ScreenMultiLangContext.tsx` | 다국어 번역 Context | +| `frontend/lib/registry/components/index.ts` | 컴포넌트 등록 | + +--- + +## 참고 문서 + +- [component-development-guide.mdc](.cursor/rules/component-development-guide.mdc) - 컴포넌트 개발 상세 가이드 +- [table-list-component-guide.mdc](.cursor/rules/table-list-component-guide.mdc) - 테이블 리스트 가이드 diff --git a/docs/V2_컴포넌트_연동_가이드.md b/docs/V2_컴포넌트_연동_가이드.md new file mode 100644 index 00000000..f6756a5e --- /dev/null +++ b/docs/V2_컴포넌트_연동_가이드.md @@ -0,0 +1,1496 @@ +# V2 컴포넌트 연동 가이드 + +## 목차 + +1. [개요](#1-개요) +2. [V2 표준 이벤트 시스템](#2-v2-표준-이벤트-시스템) +3. [이벤트 시스템 상세](#3-이벤트-시스템-상세) +4. [Context 시스템](#4-context-시스템) +5. [데이터 전달 인터페이스](#5-데이터-전달-인터페이스) +6. [컴포넌트별 연동 능력](#6-컴포넌트별-연동-능력) +7. [연동 가능한 조합](#7-연동-가능한-조합) +8. [연동 설정 방법](#8-연동-설정-방법) + +--- + +## 1. 개요 + +V2 컴포넌트들은 세 가지 메커니즘을 통해 상호 통신합니다: + +| 메커니즘 | 용도 | 특징 | +|----------|------|------| +| **이벤트 시스템** | 비동기 통신, 느슨한 결합 | V2 표준 이벤트 타입 사용 | +| **Context 시스템** | 상태 공유, 동기 통신 | React Context API | +| **데이터 전달 인터페이스** | 명시적 데이터 전송 | `DataProvidable` / `DataReceivable` | + +--- + +## 2. V2 표준 이벤트 시스템 + +### 2.1 이벤트 타입 정의 파일 + +**파일 위치**: `frontend/types/component-events.ts` + +모든 V2 컴포넌트는 이 파일에 정의된 **타입 안전한 이벤트 시스템**을 사용해야 합니다. + +### 2.2 이벤트 이름 상수 + +```typescript +import { V2_EVENTS } from "@/types/component-events"; + +// 사용 가능한 이벤트 +V2_EVENTS.TABLE_LIST_DATA_CHANGE // "tableListDataChange" +V2_EVENTS.REPEATER_DATA_CHANGE // "repeaterDataChange" +V2_EVENTS.BEFORE_FORM_SAVE // "beforeFormSave" +V2_EVENTS.AFTER_FORM_SAVE // "afterFormSave" +V2_EVENTS.REPEATER_SAVE // "repeaterSave" +V2_EVENTS.REFRESH_TABLE // "refreshTable" +V2_EVENTS.REFRESH_CARD_DISPLAY // "refreshCardDisplay" +V2_EVENTS.COMPONENT_DATA_TRANSFER // "componentDataTransfer" +V2_EVENTS.SPLIT_PANEL_DATA_TRANSFER // "splitPanelDataTransfer" +``` + +### 2.3 유틸리티 함수 + +#### 타입 안전한 이벤트 발행 + +```typescript +import { dispatchV2Event, V2_EVENTS } from "@/types/component-events"; + +// 올바른 방법 (타입 안전) +dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + componentId: "my-repeater", + tableName: "order_detail", + data: rows, + selectedData: selectedRows, +}); + +// 잘못된 방법 (타입 오류 발생) +dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + wrongField: "value", // 타입 에러! +}); +``` + +#### 타입 안전한 이벤트 구독 + +```typescript +import { subscribeV2Event, V2_EVENTS, type RepeaterDataChangeDetail } from "@/types/component-events"; + +useEffect(() => { + // 구독 (자동 cleanup 함수 반환) + const unsubscribe = subscribeV2Event( + V2_EVENTS.REPEATER_DATA_CHANGE, + (event: CustomEvent) => { + const { componentId, data } = event.detail; + // 타입 안전하게 데이터 접근 + } + ); + + return () => unsubscribe(); +}, []); +``` + +### 2.4 이벤트 상세 타입 + +```typescript +// 테이블 리스트 데이터 변경 +interface TableListDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedRows: string[] | number[]; +} + +// 리피터 데이터 변경 +interface RepeaterDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedData?: any[]; +} + +// 폼 저장 전 +interface BeforeFormSaveDetail { + formData: Record; + skipDefaultSave?: boolean; +} + +// 리피터 저장 (마스터-디테일 FK 연결용) +interface RepeaterSaveDetail { + parentId?: string | number; + masterRecordId: string | number; + mainFormData: Record; + tableName: string; +} + +// 컴포넌트 간 데이터 전달 +interface ComponentDataTransferDetail { + sourceComponentId: string; + targetComponentId: string; + data: any[]; + mode: "append" | "replace" | "merge"; + mappingRules?: MappingRule[]; +} +``` + +### 2.5 마이그레이션 가이드 + +**이전 방식 (사용 금지)**: + +```typescript +// ❌ 타입 안전하지 않음 +window.addEventListener("tableListDataChange" as any, handler); +window.dispatchEvent(new CustomEvent("repeaterDataChange", { detail })); +``` + +**새로운 방식 (권장)**: + +```typescript +// ✅ 타입 안전함 +import { subscribeV2Event, dispatchV2Event, V2_EVENTS } from "@/types/component-events"; + +const unsubscribe = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handler); +dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, detail); +``` + +--- + +## 3. 이벤트 시스템 상세 + +### 3.1 저장 관련 이벤트 + +#### `beforeFormSave` + +폼 저장 직전에 발생하여 각 컴포넌트가 데이터를 수집할 기회를 제공합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `buttonActions.ts`, `UnifiedFormContext.tsx` | +| **구독자** | `UnifiedRepeater`, `SimpleRepeaterTable`, `ModalRepeaterTable`, `SelectedItemsDetailInput`, `RepeatScreenModal`, `UniversalFormModal` | +| **데이터 구조** | `{ formData: Record, skipDefaultSave?: boolean }` | + +```typescript +// 발행 예시 +window.dispatchEvent(new CustomEvent("beforeFormSave", { + detail: { formData: {}, skipDefaultSave: false } +})); + +// 구독 예시 +window.addEventListener("beforeFormSave", (event: CustomEvent) => { + const { formData } = event.detail; + formData["myField"] = myValue; // 데이터 추가 +}); +``` + +#### `afterFormSave` + +폼 저장 완료 후 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `UnifiedFormContext.tsx` | +| **데이터 구조** | `{ success: boolean, data?: any }` | + +#### `repeaterSave` + +마스터 저장 후 리피터에 FK를 전달하기 위해 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `InteractiveScreenViewerDynamic.tsx` | +| **구독자** | `UnifiedRepeater.tsx` | +| **데이터 구조** | `{ parentId, masterRecordId, mainFormData, tableName }` | + +```typescript +// 마스터-디테일 저장 흐름 +// 1. 마스터 저장 완료 +// 2. repeaterSave 이벤트 발행 +window.dispatchEvent(new CustomEvent("repeaterSave", { + detail: { + masterRecordId: savedId, // 마스터 ID + tableName: "receiving_mng", + mainFormData: formData + } +})); + +// 3. UnifiedRepeater에서 수신 +// → 모든 행의 foreignKeyColumn에 masterRecordId 설정 +// → 디테일 테이블에 저장 +``` + +--- + +### 3.2 데이터 변경 이벤트 + +#### `tableListDataChange` + +테이블 리스트의 데이터가 변경될 때 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `v2-table-list`, `table-list` | +| **구독자** | `v2-repeat-container`, `v2-aggregation-widget`, `repeat-container`, `aggregation-widget` | +| **데이터 구조** | `{ componentId, tableName, data: any[], selectedRows: string[] }` | + +```typescript +// 테이블 리스트 → 집계 위젯 연동 +// 테이블 데이터 변경 시 자동으로 집계 갱신 +``` + +#### `repeaterDataChange` + +리피터 컴포넌트의 데이터가 변경될 때 발생합니다. + +| 항목 | 내용 | +|------|------| +| **구독자** | `v2-repeat-container`, `v2-aggregation-widget` | + +--- + +### 3.3 UI 갱신 이벤트 + +#### `refreshTable` + +테이블 데이터를 다시 로드합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `v2-button-primary`, `InteractiveScreenViewerDynamic`, `ScreenModal`, `buttonActions.ts` | +| **구독자** | `v2-table-list`, `v2-split-panel-layout`, `InteractiveDataTable` | + +```typescript +// 저장 후 테이블 새로고침 +window.dispatchEvent(new CustomEvent("refreshTable")); +``` + +#### `refreshCardDisplay` + +카드 디스플레이를 다시 로드합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `InteractiveScreenViewerDynamic`, `buttonActions.ts` | +| **구독자** | `v2-card-display`, `card-display` | + +--- + +### 3.4 모달 제어 이벤트 + +#### `openEditModal` + +편집 모달을 엽니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `SplitPanelLayout2`, `InteractiveScreenViewer`, `InteractiveDataTable` | +| **구독자** | `EditModal.tsx`, 화면 페이지 | + +#### `closeEditModal` + +편집 모달을 닫습니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `v2-button-primary`, `buttonActions.ts` | +| **구독자** | `EditModal.tsx` | + +#### `saveSuccessInModal` + +모달 내 저장 성공 시 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `v2-button-primary`, `buttonActions.ts` | +| **구독자** | `ScreenModal.tsx` | + +--- + +### 3.5 데이터 전달 이벤트 + +#### `componentDataTransfer` + +컴포넌트 간 데이터 전달 시 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `buttonActions.ts` | +| **구독자** | `UnifiedRepeater.tsx` | + +#### `splitPanelDataTransfer` + +분할 패널 간 데이터 전달 시 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `buttonActions.ts` | +| **구독자** | `UnifiedRepeater.tsx`, `RepeaterFieldGroupRenderer.tsx` | + +#### `screenDataTransfer` + +화면 간 데이터 전달 시 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `buttonActions.ts`, `useScreenDataTransfer.ts` | +| **구독자** | `useScreenDataTransfer.ts` | + +--- + +### 3.6 연관 데이터 버튼 이벤트 + +#### `related-button-select` + +연관 데이터 버튼 클릭 시 발생합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `RelatedDataButtonsComponent.tsx` | +| **구독자** | `v2-table-list`, `table-list`, `InteractiveDataTable` | +| **데이터 구조** | `{ targetTable, filterColumn, filterValue, selectedData }` | + +#### `related-button-register` / `related-button-unregister` + +연관 데이터 버튼이 대상 테이블을 등록/해제합니다. + +| 항목 | 내용 | +|------|------| +| **발행자** | `RelatedDataButtonsComponent.tsx` | +| **구독자** | `v2-table-list`, `table-list` | + +--- + +### 3.7 이벤트 흐름 다이어그램 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 저장 플로우 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ [저장 버튼 클릭] │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ beforeFormSave │ ────────────────────────────────────────────┐ │ +│ └────────┬────────┘ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ UnifiedRepeater │ │ SimpleRepeater │ │ ModalRepeater │ ... │ +│ │ (데이터 수집) │ │ (데이터 수집) │ │ (데이터 수집) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ API 저장 │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ afterFormSave │ │ repeaterSave │ (마스터-디테일 시) │ +│ └────────┬────────┘ └────────┬────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ refreshTable │ │ UnifiedRepeater │ │ +│ └─────────────────┘ │ (FK 설정 후 저장)│ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ + + +┌─────────────────────────────────────────────────────────────────────────┐ +│ 데이터 변경 플로우 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ │ +│ │ v2-table-list │ │ +│ │ (데이터 로드/변경)│ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────┐ │ +│ │ tableListDataChange │ │ +│ └────────┬────────────┘ │ +│ │ │ +│ ├─────────────────────┬─────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │v2-aggregation- │ │v2-repeat- │ │ 기타 구독자 │ │ +│ │widget (집계갱신) │ │container │ │ │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. Context 시스템 + +### 4.1 TableOptionsContext + +**역할**: 화면 내 테이블 컴포넌트 등록/관리 및 필터링 연동 + +**파일**: `frontend/contexts/TableOptionsContext.tsx` + +#### 제공하는 기능 + +| 함수/상태 | 설명 | +|-----------|------| +| `registeredTables` | 등록된 테이블 Map | +| `selectedTableId` | 현재 선택된 테이블 ID | +| `registerTable(tableId, registration)` | 테이블 등록 | +| `unregisterTable(tableId)` | 테이블 해제 | +| `getTable(tableId)` | 테이블 조회 | +| `setSelectedTableId(id)` | 선택 테이블 설정 | +| `updateTableDataCount(tableId, count)` | 데이터 건수 업데이트 | +| `getActiveTabTables()` | 활성 탭의 테이블만 반환 | + +#### TableRegistration 구조 + +```typescript +interface TableRegistration { + tableId: string; + tableName: string; + columns: ColumnInfo[]; + dataCount: number; + parentTabId?: string; // 소속 탭 ID + onFilterChange: (filters: TableFilter[]) => void; + getColumnUniqueValues: (columnName: string) => Promise; +} +``` + +#### 연동 흐름 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ TableOptionsContext 연동 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ │ +│ │ v2-table-list │ │ +│ └────────┬────────┘ │ +│ │ registerTable() │ +│ ▼ │ +│ ┌─────────────────────────────────────┐ │ +│ │ TableOptionsContext │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ registeredTables │ │ │ +│ │ │ - tableId │ │ │ +│ │ │ - onFilterChange() │ │ │ +│ │ │ - columns │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ └────────────────┬────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────┐ │ +│ │ v2-table-search-widget │ │ +│ │ - 등록된 테이블 목록 표시 │ │ +│ │ - 필터 입력 │ │ +│ │ - currentTable.onFilterChange() │ │ +│ └────────────────┬────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────┐ │ +│ │ v2-table-list (자동 재조회) │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### 사용 컴포넌트 + +| 컴포넌트 | 사용 방식 | +|----------|----------| +| `v2-table-list` | 테이블 등록/해제, 데이터 건수 업데이트 | +| `v2-table-search-widget` | 등록된 테이블 목록 조회, 필터 적용 | +| `v2-split-panel-layout` | 내부 테이블 등록/해제 | +| `v2-card-display` | 테이블 등록 (선택적) | + +--- + +### 4.2 SplitPanelContext + +**역할**: 좌우 분할 패널 간 데이터 전달 및 상태 관리 + +**파일**: `frontend/contexts/SplitPanelContext.tsx` + +#### 제공하는 기능 + +| 함수/상태 | 설명 | +|-----------|------| +| `splitPanelId` | 분할 패널 ID | +| `leftScreenId`, `rightScreenId` | 좌우 화면 ID | +| `selectedLeftData` | 좌측 선택 데이터 | +| `setSelectedLeftData(data)` | 좌측 선택 데이터 설정 | +| `addedItemIds` | 우측에 추가된 항목 ID Set | +| `addItemIds(ids)` | 항목 ID 추가 | +| `registerReceiver(receiver)` | 데이터 수신자 등록 | +| `transferToOtherSide(data)` | 반대편으로 데이터 전달 | +| `linkedFilters` | 연결 필터 설정 | +| `parentDataMapping` | 부모 데이터 매핑 설정 | + +#### 연동 흐름 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ SplitPanelContext 연동 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-split-panel-layout │ │ +│ │ (SplitPanelProvider) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 좌측 패널 │ │ 우측 패널 │ │ +│ │ (CardDisplay) │ │ (TableList) │ │ +│ └────────┬────────┘ └────────┬────────┘ │ +│ │ │ │ +│ │ 행 클릭 │ │ +│ ▼ │ │ +│ setSelectedLeftData(rowData) │ │ +│ │ │ │ +│ └──────────────────────────────────────────▶│ │ +│ │ │ +│ relation 설정에 따라 │ │ +│ 자동 필터링 (FK 기반) │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ 필터링된 데이터 │ │ +│ │ 표시 │ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### 사용 컴포넌트 + +| 컴포넌트 | 사용 방식 | +|----------|----------| +| `v2-split-panel-layout` | Provider 제공, 좌우 패널 관리 | +| `v2-table-list` | 분할 패널 데이터 수신자로 등록 | +| `v2-card-display` | 분할 패널 위치 확인, 데이터 수신 | +| `v2-button-primary` | 분할 패널 컨텍스트 확인 | + +--- + +### 4.3 ScreenContext + +**역할**: 같은 화면 내 컴포넌트 간 통신 (데이터 제공자/수신자 등록) + +**파일**: `frontend/contexts/ScreenContext.tsx` + +#### 제공하는 기능 + +| 함수/상태 | 설명 | +|-----------|------| +| `screenId` | 화면 ID | +| `tableName` | 테이블명 | +| `menuObjid` | 메뉴 OBJID (카테고리 값 조회용) | +| `splitPanelPosition` | 분할 패널 위치 (`left` \| `right`) | +| `formData` | 폼 데이터 | +| `updateFormData(field, value)` | 폼 데이터 업데이트 | +| `registerDataProvider(provider)` | 데이터 제공자 등록 | +| `registerDataReceiver(receiver)` | 데이터 수신자 등록 | +| `getDataProvider(id)` | 데이터 제공자 조회 | +| `getDataReceiver(id)` | 데이터 수신자 조회 | +| `getAllDataProviders()` | 모든 데이터 제공자 조회 | + +#### 사용 컴포넌트 + +| 컴포넌트 | 사용 방식 | +|----------|----------| +| `v2-table-list` | 데이터 제공자/수신자로 등록 | +| `v2-card-display` | 화면 컨텍스트 확인 | +| `v2-button-primary` | 화면 컨텍스트 확인, 데이터 전달 실행 | +| `repeater-field-group` | 데이터 수신자 등록, formData 사용 | + +--- + +### 4.4 UnifiedFormContext + +**역할**: 폼 상태 관리, 조건부 로직, 저장/검증/초기화 + +**파일**: `frontend/components/unified/UnifiedFormContext.tsx` + +#### 제공하는 기능 + +| 함수/상태 | 설명 | +|-----------|------| +| `formData` | 폼 데이터 | +| `originalData` | 원본 데이터 (수정 모드) | +| `status` | 폼 상태 (isSubmitting, isDirty, isValid 등) | +| `errors` | 필드 에러 배열 | +| `getValue(field)`, `setValue(field, value)` | 값 관리 | +| `submit(options)` | 폼 저장 | +| `reset()` | 폼 초기화 | +| `validate()` | 폼 검증 | +| `evaluateCondition(condition)` | 조건 평가 | +| `getRepeaterData(key)`, `setRepeaterData(key, data)` | 리피터 데이터 관리 | + +--- + +### 4.5 ActiveTabContext + +**역할**: 탭 컴포넌트의 활성 탭 추적 + +**파일**: `frontend/contexts/ActiveTabContext.tsx` + +#### 제공하는 기능 + +| 함수/상태 | 설명 | +|-----------|------| +| `activeTabs` | 활성 탭 정보 Map | +| `setActiveTab(tabsId, tabId)` | 활성 탭 설정 | +| `getActiveTabId(tabsId)` | 특정 탭 컴포넌트의 활성 탭 ID | +| `getAllActiveTabIds()` | 전체 활성 탭 ID 목록 | + +#### 사용 컴포넌트 + +| 컴포넌트 | 사용 방식 | +|----------|----------| +| `v2-table-search-widget` | 활성 탭 기반 테이블 필터링 | +| `v2-tabs-widget` | 탭 활성화 관리 | + +--- + +### 4.6 ScreenPreviewContext + +**역할**: 디자이너 모드와 실제 화면 모드 구분 + +**파일**: `frontend/contexts/ScreenPreviewContext.tsx` + +#### 제공하는 기능 + +| 함수/상태 | 설명 | +|-----------|------| +| `isPreviewMode` | 미리보기 모드 여부 | + +#### 사용 컴포넌트 + +| 컴포넌트 | 사용 방식 | +|----------|----------| +| `v2-table-search-widget` | 미리보기 모드에서 설정 버튼 비활성화 | +| `v2-button-primary` | 프리뷰 모드 확인 | + +--- + +### 4.7 Context 계층 구조 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Context 계층 구조 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ScreenPreviewContext (최상위 - 디자이너/실제 화면 구분) │ +│ │ │ +│ └─── ScreenContext (화면 레벨) │ +│ │ │ +│ ├─── TableOptionsContext (테이블 관리) │ +│ │ │ │ +│ │ └─── ActiveTabContext (탭 필터링) │ +│ │ │ +│ └─── SplitPanelContext (분할 패널 - 선택적) │ +│ │ +│ UnifiedFormContext (폼 상태 관리 - 독립적, 선택적 사용) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 5. 데이터 전달 인터페이스 + +### 5.1 DataProvidable 인터페이스 + +**데이터를 제공하는 컴포넌트**가 구현하는 인터페이스 + +```typescript +interface DataProvidable { + componentId: string; + componentType: string; + + // 선택된 데이터 반환 + getSelectedData(): any[]; + + // 모든 데이터 반환 + getAllData(): any[]; + + // 선택 초기화 + clearSelection(): void; +} +``` + +#### 구현 컴포넌트 + +| 컴포넌트 | 제공 데이터 | +|----------|------------| +| `v2-table-list` | 선택된 행 데이터, 전체 데이터 | +| `v2-card-display` | 선택된 카드 데이터 | +| `select-basic` | 선택된 값 | +| `conditional-container` | 조건부 컨테이너의 선택 값 | + +### 5.2 DataReceivable 인터페이스 + +**데이터를 수신하는 컴포넌트**가 구현하는 인터페이스 + +```typescript +interface DataReceivable { + componentId: string; + componentType: DataReceivableComponentType; + + // 데이터 수신 + receiveData(data: any[], config: DataReceiverConfig): Promise; + + // 현재 데이터 반환 + getData(): any; +} + +type DataReceivableComponentType = + | "table-list" + | "unified-repeater" + | "repeater-field-group" + | "simple-repeater-table"; +``` + +#### 구현 컴포넌트 + +| 컴포넌트 | 수신 모드 | +|----------|----------| +| `v2-table-list` | append, replace, merge | +| `repeater-field-group` | append | +| `embedded-screen` | 화면 임베딩 데이터 수신 | + +### 5.3 DataReceiverConfig + +데이터 전달 시 설정 + +```typescript +interface DataReceiverConfig { + // 타겟 컴포넌트 정보 + targetComponentId: string; + targetComponentType: DataReceivableComponentType; + + // 수신 모드 + mode: "append" | "replace" | "merge"; + + // 필드 매핑 규칙 + mappingRules: Array<{ + sourceField: string; // 소스 필드 + targetField: string; // 타겟 필드 + transform?: string; // 변환 함수 (선택) + defaultValue?: any; // 기본값 (선택) + }>; + + // 조건부 전달 + condition?: { + field: string; + operator: "=" | "!=" | ">" | "<"; + value: any; + }; + + // 검증 규칙 + validation?: { + required: string[]; // 필수 필드 + unique?: string[]; // 중복 불가 필드 + }; +} +``` + +### 5.4 데이터 전달 흐름 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 데이터 전달 흐름 (버튼 액션) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ │ +│ │ v2-button- │ │ +│ │ primary │ action.type = "transferData" │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ handleTransferDataAction() │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ 1. ScreenContext에서 소스 컴포넌트 조회 │ │ +│ │ getDataProvider(sourceComponentId) │ │ +│ │ │ │ +│ │ 2. 소스에서 데이터 가져오기 │ │ +│ │ source.getSelectedData() 또는 source.getAllData() │ │ +│ │ │ │ +│ │ 3. 매핑 규칙 적용 │ │ +│ │ mappingRules.forEach(rule => ...) │ │ +│ │ │ │ +│ │ 4. ScreenContext에서 타겟 컴포넌트 조회 │ │ +│ │ getDataReceiver(targetComponentId) │ │ +│ │ │ │ +│ │ 5. 타겟에 데이터 전달 │ │ +│ │ target.receiveData(mappedData, config) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ 타겟 컴포넌트 │ │ +│ │ (v2-table-list, │ │ +│ │ repeater 등) │ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 6. 컴포넌트별 연동 능력 + +### 6.1 연동 능력 매트릭스 + +| 컴포넌트 | 이벤트 발행 | 이벤트 구독 | DataProvider | DataReceiver | Context 사용 | +|----------|:-----------:|:-----------:|:------------:|:------------:|:------------:| +| `v2-table-list` | ✅ | ✅ | ✅ | ✅ | TableOptions, Screen, SplitPanel | +| `v2-split-panel-layout` | ✅ | ✅ | ❌ | ❌ | TableOptions (Provider) | +| `v2-unified-repeater` | ✅ | ✅ | ✅ | ✅ | Screen | +| `v2-button-primary` | ✅ | ❌ | ❌ | ❌ | Screen, SplitPanel | +| `v2-table-search-widget` | ❌ | ❌ | ❌ | ❌ | TableOptions, ActiveTab | +| `v2-aggregation-widget` | ❌ | ✅ | ❌ | ❌ | - | +| `v2-repeat-container` | ❌ | ✅ | ❌ | ❌ | - | +| `v2-card-display` | ✅ | ✅ | ✅ | ❌ | TableOptions, Screen, SplitPanel | +| `v2-pivot-grid` | ❌ | ❌ | ❌ | ❌ | - | +| `v2-tabs-widget` | ❌ | ❌ | ❌ | ❌ | ActiveTab | + +### 6.2 컴포넌트별 상세 + +#### v2-table-list + +**발행 이벤트**: +- `tableListDataChange` - 데이터 로드/변경 시 + +**구독 이벤트**: +- `refreshTable` - 테이블 새로고침 +- `related-button-select` - 연관 버튼 선택 +- `related-button-register/unregister` - 연관 버튼 등록/해제 + +**DataProvidable 구현**: +```typescript +getSelectedData(): any[] // 체크된 행 데이터 +getAllData(): any[] // 전체 데이터 +clearSelection(): void // 선택 초기화 +``` + +**DataReceivable 구현**: +```typescript +receiveData(data, config): Promise +// mode: "append" - 기존 데이터에 추가 +// mode: "replace" - 데이터 교체 +// mode: "merge" - 키 기준 병합 +``` + +--- + +#### v2-unified-repeater + +**발행 이벤트**: +- `repeaterDataChange` - 데이터 변경 시 (V2 표준 이벤트) + +**구독 이벤트**: +- `beforeFormSave` - 저장 전 데이터 수집 +- `repeaterSave` - 마스터 저장 후 FK 설정 +- `componentDataTransfer` - 컴포넌트 간 데이터 전달 +- `splitPanelDataTransfer` - 분할 패널 간 데이터 전달 + +**DataProvidable 구현**: +- `getSelectedData()` - 선택된 행 데이터 반환 +- `getAllData()` - 전체 데이터 반환 +- `clearSelection()` - 선택 초기화 + +**DataReceivable 구현**: +- `receiveData(data, config)` - 데이터 수신 (append, replace, merge 모드 지원) +- `getData()` - 현재 데이터 반환 + +**Context 등록**: +- ScreenContext에 DataProvider/DataReceiver 자동 등록 + +--- + +#### v2-button-primary + +**발행 이벤트**: +- `refreshTable` - 저장 후 테이블 갱신 +- `closeEditModal` - 모달 닫기 +- `saveSuccessInModal` - 모달 저장 성공 + +**역할**: +- 저장, 삭제, 데이터 전달 등 액션 실행 +- `buttonActions.ts`의 함수들 호출 + +--- + +#### v2-table-search-widget + +**Context 의존**: +- `TableOptionsContext` - 등록된 테이블 조회, 필터 적용 +- `ActiveTabContext` - 활성 탭 기반 테이블 필터링 + +**동작**: +1. `TableOptionsContext.registeredTables`에서 테이블 목록 조회 +2. 사용자가 필터 입력 +3. `currentTable.onFilterChange(filters)` 호출 +4. 해당 테이블이 자동으로 재조회 + +--- + +#### v2-aggregation-widget + +**구독 이벤트**: +- `tableListDataChange` - 테이블 데이터 변경 시 집계 갱신 +- `repeaterDataChange` - 리피터 데이터 변경 시 집계 갱신 + +--- + +#### v2-split-panel-layout + +**Provider 제공**: +- `SplitPanelContext` - 좌우 패널 데이터 전달 + +**발행 이벤트**: +- `openScreenModal` - 화면 모달 열기 + +**구독 이벤트**: +- `refreshTable` - 내부 테이블 갱신 + +--- + +## 7. 연동 가능한 조합 + +### 7.1 검색/필터 연동 + +#### v2-table-search-widget ↔ v2-table-list + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 검색 위젯 ↔ 테이블 리스트 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-search-widget │ │ +│ │ ┌─────┐ ┌─────────┐ ┌──────────┐ ┌────────┐ │ │ +│ │ │ 이름 │ │ 날짜범위 │ │ 상태선택 │ │ 초기화 │ │ │ +│ │ └──┬──┘ └────┬────┘ └────┬─────┘ └────────┘ │ │ +│ └─────┼─────────┼───────────┼──────────────────────────────────────┘ │ +│ │ │ │ │ +│ └─────────┴───────────┘ │ +│ │ onFilterChange(filters) │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-list │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 필터링된 데이터 표시 │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 연결 방식: TableOptionsContext │ +│ 설정: v2-table-search-widget의 targetPanelPosition 설정 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**설정 방법**: +- `v2-table-search-widget`의 `filterMode`: `"dynamic"` 또는 `"preset"` +- `v2-table-search-widget`의 `targetPanelPosition`: `"left"`, `"right"`, `"auto"` +- `v2-table-list`는 자동으로 `TableOptionsContext`에 등록됨 + +--- + +### 7.2 마스터-디테일 연동 + +#### v2-split-panel-layout (좌측 ↔ 우측) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 마스터-디테일 (분할 패널) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ v2-split-panel-layout │ │ +│ │ ┌────────────────────┐ │ ┌────────────────────────────────┐ │ │ +│ │ │ 좌측 패널 │ │ │ 우측 패널 │ │ │ +│ │ │ (마스터 목록) │ │ │ (디테일 정보) │ │ │ +│ │ │ │ │ │ │ │ │ +│ │ │ - 부서 목록 │ │ │ - 선택된 부서의 직원 목록 │ │ │ +│ │ │ - dept_info │ ──▶ │ - user_info │ │ │ +│ │ │ │ │ │ - dept_code = 선택값 │ │ │ +│ │ └────────────────────┘ │ └────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 연결 방식: SplitPanelContext + relation 설정 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**설정 방법**: +```typescript +rightPanel: { + tableName: "user_info", + relation: { + type: "detail", + leftColumn: "dept_code", // 좌측 테이블의 컬럼 + rightColumn: "dept_code", // 우측 테이블의 필터 컬럼 + // 또는 복합키 + keys: [ + { leftColumn: "company_id", rightColumn: "company_id" }, + { leftColumn: "dept_code", rightColumn: "dept_code" } + ] + } +} +``` + +--- + +### 7.3 폼 저장 연동 + +#### v2-button-primary → v2-unified-repeater + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 폼 저장 + 리피터 저장 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ [폼 입력 필드들] │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 입고번호 │ │ 입고일자 │ │ 거래처 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-unified-repeater (입고 상세) │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 품목코드 │ 품목명 │ 수량 │ 단가 │ 금액 │ │ │ +│ │ ├─────────────────────────────────────────────────────────┤ │ │ +│ │ │ ITEM001 │ ... │ 10 │ 1000 │ 10000 │ │ │ +│ │ │ ITEM002 │ ... │ 5 │ 2000 │ 10000 │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ [저장 버튼] │ │ +│ │ v2-button- │ │ +│ │ primary │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ 1. beforeFormSave 발행 → 리피터가 데이터 수집 │ +│ 2. 마스터 테이블 저장 (receiving_mng) │ +│ 3. repeaterSave 발행 → 리피터가 FK 설정 후 저장 │ +│ 4. 디테일 테이블 저장 (receiving_detail) │ +│ 5. refreshTable 발행 → 테이블 갱신 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**설정 방법**: +```typescript +// v2-button-primary +action: { + type: "save", + saveMode: "withRepeater", // 리피터와 함께 저장 + tableName: "receiving_mng" +} + +// v2-unified-repeater +dataSource: { + tableName: "receiving_detail", + foreignKey: "receiving_id", // 마스터 FK 컬럼 + referenceKey: "id" // 마스터 PK 컬럼 +} +``` + +--- + +### 7.4 데이터 전달 연동 + +#### v2-table-list → v2-unified-repeater (버튼으로 데이터 추가) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 테이블 선택 → 리피터 추가 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-list (품목 선택) │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ ☑ │ 품목코드 │ 품목명 │ 단가 │ │ │ +│ │ ├─────────────────────────────────────────────────────────┤ │ │ +│ │ │ ☑ │ ITEM001 │ 노트북 │ 1,000,000 │ │ │ +│ │ │ ☑ │ ITEM002 │ 마우스 │ 50,000 │ │ │ +│ │ │ ☐ │ ITEM003 │ 키보드 │ 100,000 │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ [추가 버튼] │ action.type = "transferData" │ +│ │ v2-button- │ │ +│ │ primary │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-unified-repeater (주문 상세) │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ 품목코드 │ 품목명 │ 수량 │ 단가 │ 금액 │ │ │ +│ │ ├─────────────────────────────────────────────────────────┤ │ │ +│ │ │ ITEM001 │ 노트북 │ 1 │ 1,000,000 │ 1,000,000│ │ │ +│ │ │ ITEM002 │ 마우스 │ 1 │ 50,000 │ 50,000 │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**설정 방법**: +```typescript +// v2-button-primary +action: { + type: "transferData", + sourceComponentId: "item-table-list", + targetComponentId: "order-detail-repeater", + mappingRules: [ + { sourceField: "item_code", targetField: "item_code" }, + { sourceField: "item_name", targetField: "item_name" }, + { sourceField: "unit_price", targetField: "unit_price" }, + { sourceField: "", targetField: "quantity", defaultValue: 1 } + ], + mode: "append" +} +``` + +--- + +### 7.5 데이터 집계 연동 + +#### v2-table-list → v2-aggregation-widget + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 테이블 데이터 → 집계 위젯 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-aggregation-widget │ │ +│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ +│ │ │ 📊 총 건수 │ │ 💰 총 금액 │ │ 📈 평균 단가 │ │ │ +│ │ │ 15건 │ │ ₩3,500,000 │ │ ₩233,333 │ │ │ +│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ ▲ │ +│ │ tableListDataChange 이벤트 │ +│ │ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-list │ │ +│ │ (데이터 변경 시 자동으로 이벤트 발행) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ 연결 방식: tableListDataChange 이벤트 자동 구독 │ +│ 설정: v2-aggregation-widget의 dataSourceType = "table" │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**설정 방법**: +```typescript +// v2-aggregation-widget +{ + dataSourceType: "table", + items: [ + { columnName: "id", aggregationType: "count", label: "총 건수" }, + { columnName: "amount", aggregationType: "sum", label: "총 금액" }, + { columnName: "unit_price", aggregationType: "avg", label: "평균 단가" } + ] +} +``` + +--- + +### 7.6 연관 데이터 버튼 연동 + +#### related-data-buttons ↔ v2-table-list + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 연관 데이터 버튼 ↔ 테이블 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 좌측 패널 (거래처 선택) │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-card-display (거래처 목록) │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ [선택됨] ABC상사 │ │ │ +│ │ │ DEF물산 │ │ │ +│ │ └─────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ 선택 데이터 전달 │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ related-data-buttons │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ 품목정보 │ │ 단가정보 │ │ 거래내역 │ │ │ +│ │ │ (5) │ │ (3) │ │ (12) │ │ │ +│ │ └────┬─────┘ └──────────┘ └──────────┘ │ │ +│ └───────┼─────────────────────────────────────────────────────────┘ │ +│ │ related-button-select 이벤트 │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-list (품목정보) │ │ +│ │ - customer_code = "ABC상사" 조건으로 필터링 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +### 7.7 탭 기반 테이블 필터링 + +#### v2-tabs-widget → v2-table-search-widget → v2-table-list + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 탭 기반 테이블 필터링 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-tabs-widget │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ [주문] │ │ 입고 │ │ 재고 │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ ActiveTabContext │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-search-widget │ │ +│ │ (활성 탭에 해당하는 테이블만 대상으로 표시) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ onFilterChange │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-list (주문 탭 내) │ │ +│ │ - parentTabId가 활성 탭과 일치하는 테이블만 필터 적용 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 8. 연동 설정 방법 + +### 8.1 검색 위젯 + 테이블 리스트 + +**필요한 컴포넌트**: +- `v2-table-search-widget` +- `v2-table-list` + +**설정**: + +```typescript +// v2-table-search-widget 설정 +{ + filterMode: "preset", // 또는 "dynamic" + presetFilters: [ + { columnName: "name", filterType: "text", columnLabel: "이름" }, + { columnName: "created_at", filterType: "date", columnLabel: "등록일" }, + { columnName: "status", filterType: "select", columnLabel: "상태" } + ], + targetPanelPosition: "auto" // "left" | "right" | "auto" +} + +// v2-table-list는 특별한 설정 불필요 (자동 등록) +``` + +--- + +### 8.2 분할 패널 마스터-디테일 + +**필요한 컴포넌트**: +- `v2-split-panel-layout` + +**설정**: + +```typescript +{ + leftPanel: { + title: "부서 목록", + tableName: "dept_info", + displayMode: "list", + columns: [ + { name: "dept_code", label: "부서코드" }, + { name: "dept_name", label: "부서명" } + ] + }, + rightPanel: { + title: "직원 목록", + tableName: "user_info", + displayMode: "table", + columns: [ + { name: "user_id", label: "사번" }, + { name: "user_name", label: "이름" }, + { name: "position", label: "직책" } + ], + relation: { + type: "detail", + leftColumn: "dept_code", + rightColumn: "dept_code" + } + }, + splitRatio: 30, + resizable: true +} +``` + +--- + +### 8.3 폼 + 리피터 저장 + +**필요한 컴포넌트**: +- 입력 컴포넌트들 (text-input, date-input 등) +- `v2-unified-repeater` +- `v2-button-primary` + +**설정**: + +```typescript +// v2-unified-repeater 설정 +{ + renderMode: "inline", + dataSource: { + tableName: "order_detail", + foreignKey: "order_id", // 마스터 테이블의 FK + referenceKey: "id" // 마스터 테이블의 PK + }, + columns: [ + { name: "item_code", label: "품목코드" }, + { name: "quantity", label: "수량" }, + { name: "unit_price", label: "단가" } + ], + features: { + showAddButton: true, + showDeleteButton: true, + inlineEdit: true + } +} + +// v2-button-primary 설정 +{ + text: "저장", + action: { + type: "save", + saveMode: "withRepeater" + } +} +``` + +--- + +### 8.4 데이터 전달 (테이블 → 리피터) + +**필요한 컴포넌트**: +- `v2-table-list` (소스) +- `v2-button-primary` (전달 트리거) +- `v2-unified-repeater` (타겟) + +**설정**: + +```typescript +// v2-button-primary 설정 +{ + text: "추가", + action: { + type: "transferData", + config: { + sourceComponentId: "item-selection-table", + targetComponentId: "order-detail-repeater", + mode: "append", + mappingRules: [ + { sourceField: "item_code", targetField: "item_code" }, + { sourceField: "item_name", targetField: "item_name" }, + { sourceField: "unit_price", targetField: "unit_price" }, + { sourceField: "", targetField: "quantity", defaultValue: 1 } + ], + validation: { + unique: ["item_code"] // 중복 방지 + } + } + } +} +``` + +--- + +### 8.5 집계 위젯 연동 + +**필요한 컴포넌트**: +- `v2-table-list` 또는 `v2-unified-repeater` +- `v2-aggregation-widget` + +**설정**: + +```typescript +// v2-aggregation-widget 설정 +{ + dataSourceType: "table", // 또는 "repeater" + // sourceComponentId는 자동 감지 (같은 화면의 첫 번째 테이블) + items: [ + { + id: "total-count", + label: "총 건수", + columnName: "id", + aggregationType: "count", + icon: "FileText" + }, + { + id: "total-amount", + label: "총 금액", + columnName: "amount", + aggregationType: "sum", + format: { + prefix: "₩", + thousandSeparator: true + } + } + ], + layout: "horizontal", + refreshOnFormChange: true +} +``` + +--- + +## 연동 조합 요약표 + +| 소스 컴포넌트 | 타겟 컴포넌트 | 연동 방식 | 용도 | +|--------------|--------------|----------|------| +| `v2-table-search-widget` | `v2-table-list` | TableOptionsContext | 검색/필터 | +| `v2-split-panel-layout` 좌 | `v2-split-panel-layout` 우 | SplitPanelContext | 마스터-디테일 | +| `v2-button-primary` | `v2-unified-repeater` | beforeFormSave/repeaterSave | 저장 | +| `v2-table-list` | `v2-unified-repeater` | DataProvidable/DataReceivable | 데이터 전달 | +| `v2-table-list` | `v2-aggregation-widget` | tableListDataChange | 집계 | +| `v2-unified-repeater` | `v2-aggregation-widget` | repeaterDataChange | 집계 | +| `v2-tabs-widget` | `v2-table-search-widget` | ActiveTabContext | 탭 필터링 | +| `related-data-buttons` | `v2-table-list` | related-button-select | 연관 데이터 | +| `v2-button-primary` | `v2-table-list` | refreshTable | 새로고침 | +| `v2-card-display` | `v2-table-list` | SplitPanelContext | 선택 연동 | + +--- + +## 관련 파일 참조 + +| 파일 | 역할 | +|------|------| +| `frontend/lib/utils/buttonActions.ts` | 버튼 액션 실행, 이벤트 발행 | +| `frontend/contexts/TableOptionsContext.tsx` | 테이블 관리 Context | +| `frontend/contexts/SplitPanelContext.tsx` | 분할 패널 Context | +| `frontend/contexts/ScreenContext.tsx` | 화면 Context | +| `frontend/contexts/ActiveTabContext.tsx` | 활성 탭 Context | +| `frontend/components/unified/UnifiedFormContext.tsx` | 폼 Context | +| `frontend/types/data-transfer.ts` | 데이터 전달 타입 | diff --git a/docs/screen-implementation-guide/01_master-data/item-info.md b/docs/screen-implementation-guide/01_master-data/item-info.md new file mode 100644 index 00000000..73ce4e38 --- /dev/null +++ b/docs/screen-implementation-guide/01_master-data/item-info.md @@ -0,0 +1,974 @@ +# 품목정보 (Item Info) + +> Screen ID: /screens/140 +> 메뉴 경로: 기준정보 > 품목정보 +> 테이블: `item_info` + +## 1. 테이블 선택 및 화면 구조 + +### 1.1 사용 테이블 + +| 테이블명 | 용도 | 비고 | +|----------|------|------| +| `item_info` | 품목 기본정보 | 주 테이블 | + +### 1.2 테이블 컬럼 정의 (실제 DB 기준) + +| 컬럼명 | 표시명 | 타입 | 필수 | 설명 | +|--------|--------|------|------|------| +| `id` | ID | varchar(500) | PK | UUID 자동 생성 | +| `item_number` | 품번코드 | varchar(500) | | 품목 고유 코드 | +| `item_name` | 품명 | varchar(500) | | 품목명 | +| `status` | 상태 | varchar(500) | | 정상, 품절, 대기, 단종 | +| `size` | 규격 | varchar(500) | | 규격 정보 | +| `material` | 재질 | varchar(500) | | 재질 정보 | +| `inventory_unit` | 재고단위 | varchar(500) | | EA, kg, L, Sheet, Box | +| `weight` | 중량 | varchar(500) | | 중량 값 | +| `unit` | 단위 | varchar(500) | | g, kg, kg/L, t | +| `image` | 이미지 | varchar(500) | | 품목 이미지 경로 | +| `division` | 구분 | varchar(500) | | 원자재, 중간재, 완제품, 포장재 (카테고리 코드) | +| `type` | 유형 | varchar(500) | | 용도별 유형 | +| `meno` | 메모 | varchar(500) | | 비고 (오타: memo) | +| `selling_price` | 판매가 | varchar(500) | | 기본값 '0' | +| `standard_price` | 기준가 | varchar(500) | | 기본값 '0' | +| `currency_code` | 통화코드 | varchar(500) | | 기본값 'KRW' | +| `writer` | 등록자 | varchar(500) | | 작성자 ID | +| `company_code` | 회사코드 | varchar(500) | | 멀티테넌시 | +| `created_date` | 등록일 | timestamp | | 자동 생성 | +| `updated_date` | 수정일 | timestamp | | 자동 갱신 | + +### 1.3 화면 구조 개요 + +- **화면 유형**: 목록형 (단일 테이블 CRUD) +- **주요 기능**: + - 품목 조회/검색/필터링 + - 품목 등록/수정/삭제 + - 그룹핑 (Group By) + - 코드 변경/합병 + - 엑셀 업로드 + - 컬럼 표시/숨기기 설정 + +--- + +## 2. 컴포넌트 배치도 + +### 2.1 전체 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ [검색 영역] │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-search-widget │ │ +│ │ ┌───────────┐ ┌───────────────┐ ┌───────────────┐ ┌─────────┐ │ │ +│ │ │ 상태 │ │ 품번코드 │ │ 품명 │ │ [검색] │ │ │ +│ │ │ (select) │ │ (text) │ │ (text) │ │ │ │ │ +│ │ └───────────┘ └───────────────┘ └───────────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ [테이블 헤더 + 액션 버튼] │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ [코드변경][업로드][다운로드] [등록][복사][수정][삭제] │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ [데이터 테이블] │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ v2-table-list │ │ +│ │ ┌──┬────┬────────┬────────┬──────┬──────┬────────┬─────┬─────┬────────┐ │ │ +│ │ │☐ │상태│품번코드│품명 │규격 │재질 │재고단위│중량 │단위 │구분 │ │ │ +│ │ ├──┼────┼────────┼────────┼──────┼──────┼────────┼─────┼─────┼────────┤ │ │ +│ │ │☐ │정상│R_001 │테스트A │100mm │SUS304│EA │1.5 │kg │원자재 │ │ │ +│ │ │☐ │대기│R_002 │테스트B │200mm │AL │kg │2.0 │kg │완제품 │ │ │ +│ │ └──┴────┴────────┴────────┴──────┴──────┴────────┴─────┴─────┴────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 컴포넌트 목록 + +| 컴포넌트 타입 | 역할 | +|---------------|------| +| v2-table-search-widget | 검색 필터 | +| v2-table-list | 품목 데이터 테이블 | +| v2-button-primary | 코드변경 | +| v2-button-primary | 업로드 (엑셀) | +| v2-button-primary | 다운로드 (엑셀) | +| v2-button-primary | 등록 (모달 열기) | +| v2-button-primary | 복사 (모달 열기) | +| v2-button-primary | 수정 (모달 열기) | +| v2-button-primary | 삭제 | + +--- + +## 3. 화면 디자이너 설정 가이드 + +### 3.1 v2-table-search-widget (검색 필터) 설정 + +1. 좌측 컴포넌트 패널에서 `v2-table-search-widget` 드래그하여 화면 상단에 배치 +2. 대상 테이블로 아래에 배치할 테이블 리스트 선택 + +> 💡 **참고**: 검색 필터는 사용자가 런타임에서 원하는 필드를 직접 추가/삭제하여 사용할 수 있습니다. 별도의 필드 설정이 필요 없습니다. + +--- + +### 3.2 v2-table-list (품목 테이블) 설정 + +#### Step 1: 컴포넌트 추가 +1. 좌측 컴포넌트 패널에서 `v2-table-list` 드래그하여 검색 필터 아래에 배치 + +#### Step 2: 데이터 소스 설정 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 테이블 선택 | `item_info` | +| 자동 컬럼 생성 | ✅ 체크 (테이블 컬럼 자동 로드) | + +#### Step 3: 컬럼 설정 + +**[컬럼 설정]** 패널에서 표시할 컬럼 선택 및 순서 조정: + +| 순서 | 컬럼 | 표시명 | 너비 | 정렬 | 표시 | 특수 설정 | +|------|------|--------|------|------|------|-----------| +| 1 | `status` | 상태 | 80 | 중앙 | ✅ | 뱃지 스타일 (색상별) | +| 2 | `item_number` | 품번코드 | 140 | 좌측 | ✅ | | +| 3 | `item_name` | 품명 | 200 | 좌측 | ✅ | 굵게 표시 | +| 4 | `size` | 규격 | 150 | 좌측 | ✅ | | +| 5 | `material` | 재질 | 150 | 좌측 | ✅ | | +| 6 | `inventory_unit` | 재고단위 | 100 | 중앙 | ✅ | | +| 7 | `weight` | 중량 | 80 | 우측 | ✅ | | +| 8 | `unit` | 단위 | 80 | 중앙 | ✅ | | +| 9 | `image` | 이미지 | 80 | 중앙 | ✅ | 이미지 미리보기 | +| 10 | `division` | 구분 | 100 | 중앙 | ✅ | 카테고리 표시 | +| 11 | `type` | 유형 | 100 | 중앙 | ✅ | | +| 12 | `selling_price` | 판매가 | 100 | 우측 | ☐ | 숫자 포맷 | +| 13 | `standard_price` | 기준가 | 100 | 우측 | ☐ | 숫자 포맷 | +| 14 | `meno` | 메모 | 180 | 좌측 | ☐ | | +| 15 | `writer` | 등록자 | 100 | 좌측 | ☐ | 읽기 전용 | +| 16 | `created_date` | 등록일 | 120 | 중앙 | ☐ | 읽기 전용 | +| 17 | `updated_date` | 수정일 | 120 | 중앙 | ☐ | 읽기 전용 | + +#### Step 4: 기능 설정 + +| 설정 항목 | 설정 값 | 설명 | +|-----------|---------|------| +| 체크박스 | ✅ 사용 | 다중 선택 활성화 | +| 페이지네이션 | ✅ 사용 | | +| 페이지 크기 | 20 | 기본 표시 행 수 | +| 정렬 | ✅ 사용 | 컬럼 헤더 클릭 정렬 | +| 컬럼 리사이즈 | ✅ 사용 | 컬럼 너비 조정 | +| 그룹핑 | ✅ 사용 | Group By 기능 | + +#### Step 5: 그룹핑 옵션 설정 + +Group By 드롭다운에 표시할 컬럼 선택: +- ✅ `status` (상태) +- ✅ `division` (구분) +- ✅ `type` (유형) +- ✅ `inventory_unit` (재고단위) +- ✅ `writer` (등록자) + +--- + +### 3.3 버튼 설정 + +#### 좌측 버튼 그룹 + +##### 코드변경 버튼 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 라벨 | `코드변경` | +| 액션 타입 | `code_merge` | +| 스타일 | `secondary` | +| 선택 필수 | ✅ 체크 (복수 선택) | +| 병합 대상 컬럼 | `item_number` | +| 데이터플로우 연결 | 품번코드 통합 (flow_id: 18) | + +##### 업로드 버튼 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 라벨 | `업로드` | +| 액션 타입 | `excel_upload` | +| 스타일 | `secondary` | +| 대상 테이블 | `item_info` | + +##### 다운로드 버튼 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 라벨 | `다운로드` | +| 액션 타입 | `excel_download` | +| 스타일 | `secondary` | +| 대상 | 현재 테이블 리스트 | + +#### 우측 버튼 그룹 + +##### 등록 버튼 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 라벨 | `등록` | +| 액션 타입 | `modal` | +| 스타일 | `default` | +| 연결 화면 | 품목 등록/수정 화면 (아래 3.4 참조) | +| 모달 제목 | 품목 등록 | +| 모달 사이즈 | `md` | + +##### 복사 버튼 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 라벨 | `복사` | +| 액션 타입 | `copy` | +| 스타일 | `default` | +| 선택 필수 | ✅ 체크 (1개만) | +| 연결 화면 | 품목 등록/수정 화면 (아래 3.4 참조) | +| 동작 | 선택된 데이터를 복사하여 신규 등록 폼에 채움 | + +##### 수정 버튼 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 라벨 | `수정` | +| 액션 타입 | `edit` | +| 스타일 | `default` | +| 선택 필수 | ✅ 체크 (1개만) | +| 연결 화면 | 품목 등록/수정 화면 (아래 3.4 참조) | +| 동작 | 선택된 데이터 수정 모드로 폼 열기 | + +##### 삭제 버튼 + +| 설정 항목 | 설정 값 | +|-----------|---------| +| 라벨 | `삭제` | +| 액션 타입 | `delete` | +| 스타일 | `default` | +| 선택 필수 | ✅ 체크 (복수 선택 가능) | +| 확인 메시지 | 선택한 품목을 삭제하시겠습니까? | +| 삭제 후 동작 | 테이블 새로고침 | + +--- + +### 3.4 품목 등록/수정 화면 (모달용 화면) + +> 📌 **별도 화면 생성 필요**: 등록/복사/수정 버튼에 연결할 모달 화면을 새로 생성합니다. +> +> 💡 **동일 화면 공유**: 등록, 복사, 수정 버튼 모두 동일한 폼 화면을 사용합니다. +> - **등록**: 빈 폼으로 열림 +> - **복사**: 선택된 데이터가 채워진 상태로 열림 (신규 등록) +> - **수정**: 선택된 데이터가 채워진 상태로 열림 (기존 데이터 업데이트) + +#### Step 1: 새 화면 생성 + +1. 화면 관리에서 **[+ 새 화면]** 클릭 +2. 화면 정보 입력: + - 화면명: `품목 등록/수정` + - 테이블: `item_info` + - 화면 유형: `모달` + +#### Step 2: 폼 필드 배치 + +**모달 레이아웃 배치도**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 품목 등록/수정 [✕] │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ 품번코드 * │ │ 품명 * │ │ +│ │ [____________________] │ │ [____________________] │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ 규격 │ │ 재질 │ │ +│ │ [____________________] │ │ [____________________] │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ ┌───────────┐ ┌───────────┐ │ +│ │ 재고단위 * │ │ 중량 │ │ 중량단위 │ │ +│ │ [EA ▼] │ │ [_______] │ │ [kg ▼] │ │ +│ └─────────────────────────┘ └───────────┘ └───────────┘ │ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ 구분 * │ │ 유형 │ │ +│ │ [원자재 ▼] │ │ [반도체용 ▼] │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ 판매가 │ │ 기준가 │ │ +│ │ [____________________] │ │ [____________________] │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 메모 │ │ +│ │ [__________________________________________________]│ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────┐ │ +│ │ 상태 * │ │ +│ │ [정상 ▼] │ │ +│ └─────────────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────┤ +│ [취소] [💾 저장] │ +└─────────────────────────────────────────────────────────────┘ +``` + +**필드 목록**: + +| 순서 | 필드 (컬럼명) | 라벨 | 입력 타입 | 필수 | 비고 | +|------|---------------|------|-----------|------|------| +| 1 | `item_number` | 품번코드 | text | ✅ | | +| 2 | `item_name` | 품명 | text | ✅ | | +| 3 | `size` | 규격 | text | | | +| 4 | `material` | 재질 | text | | | +| 5 | `inventory_unit` | 재고단위 | select | ✅ | 옵션: EA, kg, L, Sheet, Box | +| 6 | `weight` | 중량 | number | | | +| 7 | `unit` | 중량단위 | select | | 옵션: g, kg, kg/L, t | +| 8 | `division` | 구분 | category | ✅ | 품목 구분 카테고리 | +| 9 | `type` | 유형 | select | | 옵션: 반도체용, 태양광용, 산업용, 의료용, 건축용, 사출용, 화장품용 | +| 10 | `selling_price` | 판매가 | number | | | +| 11 | `standard_price` | 기준가 | number | | | +| 12 | `meno` | 메모 | text | | | +| 13 | `status` | 상태 | select | ✅ | 옵션: 정상, 품절, 대기, 단종 | + +#### Step 3: 버튼 배치 + +| 버튼 | 액션 타입 | 스타일 | 설정 | +|------|-----------|--------|------| +| 저장 | `저장` | primary | 저장 후 모달 닫기, 부모 화면 테이블 새로고침 | +| 취소 | `모달 닫기` | secondary | | + +#### Step 4: 버튼에 화면 연결 + +1. 메인 화면(품목정보)으로 돌아가기 +2. **등록 버튼** 선택 → 설정 패널에서: + - 액션 타입: `modal` + - 연결 화면: `품목 등록/수정` 선택 + - 모달 제목: `품목 등록` +3. **복사 버튼** 선택 → 설정 패널에서: + - 액션 타입: `copy` + - 연결 화면: `품목 등록/수정` 선택 + - 선택 필수: ✅ 체크 + - 동작: 선택된 데이터를 복사하여 폼에 채움 (신규 등록) +4. **수정 버튼** 선택 → 설정 패널에서: + - 액션 타입: `edit` + - 연결 화면: `품목 등록/수정` 선택 + - 선택 필수: ✅ 체크 + - 동작: 선택된 데이터를 수정 모드로 폼에 채움 + +> 💡 **참고**: 컬럼별 스타일(뱃지 색상, 카테고리 표시 등)은 컴포넌트 기본 스타일을 따릅니다. 필요시 테이블 관리에서 컬럼별 상세 설정을 조정할 수 있습니다. + +--- + +## 4. 컴포넌트 연동 설정 + +### 4.1 이벤트 흐름 + +``` +[검색 입력] + │ + ▼ +v2-table-search-widget + │ onFilterChange + ▼ +v2-table-list (자동 재조회) + │ + ▼ +[데이터 표시] + + +[등록/복사/수정 버튼 클릭] + │ + ▼ +[모달 열기] → [폼 입력] → [저장] + │ │ + │ ▼ + │ refreshTable 이벤트 + │ │ + └────────────────────────┘ + │ + ▼ + v2-table-list (재조회) +``` + +### 4.2 연동 설정 + +| 소스 컴포넌트 | 이벤트/액션 | 대상 컴포넌트 | 동작 | +|---------------|-------------|---------------|------| +| 검색 위젯 | onFilterChange | 테이블 리스트 | 필터 적용, 재조회 | +| 등록 버튼 | click | 모달 | 빈 폼으로 모달 열기 | +| 복사 버튼 | click | 모달 | 선택 데이터가 채워진 폼 열기 (신규) | +| 수정 버튼 | click | 모달 | 선택 데이터가 채워진 폼 열기 (수정) | +| 삭제 버튼 | click | 테이블 리스트 | 선택 항목 삭제 | +| 모달 저장 | afterSave | 테이블 리스트 | refreshTable | + +### 4.3 TableOptionsContext 연동 + +``` +v2-table-search-widget ──── TableOptionsContext ──── v2-table-list + │ │ │ + │ registeredTables에서 │ │ + │ item-table 참조 │ │ + │ │ │ + └── onFilterChange() ───────┼──────────────────────┘ + │ + ▼ + 필터 조건 전달 & 재조회 +``` + +--- + +## 5. 사용자 사용 예시 시나리오 + +### 시나리오 1: 품목 조회 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | 화면 진입 | 전체 품목 목록 표시 | +| 2 | 상태 필터를 "정상"으로 선택 | 자동 필터링 | +| 3 | 품명에 "폴리머" 입력 후 검색 | 품명에 "폴리머" 포함된 품목 표시 | +| 4 | Group by에서 "구분" 선택 | division별 그룹핑 | + +### 시나리오 2: 품목 등록 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | [등록] 버튼 클릭 | 빈 폼 모달 표시 | +| 2 | 데이터 입력 (품번코드, 품명, 규격 등) | 입력 필드 채움 | +| 3 | [저장] 버튼 클릭 | 저장 완료, 모달 닫힘, 목록 갱신 | + +### 시나리오 3: 품목 복사 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | 테이블에서 복사할 행 체크박스 선택 | 행 선택 표시 | +| 2 | [복사] 버튼 클릭 | 선택된 데이터가 채워진 폼 모달 표시 | +| 3 | 필요시 데이터 수정 (품번코드 등) | 필드 값 변경 | +| 4 | [저장] 버튼 클릭 | 신규 등록 완료, 목록 갱신 | + +### 시나리오 4: 품목 수정 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | 테이블에서 행 체크박스 선택 | 행 선택 표시 | +| 2 | [수정] 버튼 클릭 | 수정 모달 표시 (기존 데이터 로드) | +| 3 | 데이터 수정 | 필드 값 변경 | +| 4 | [저장] 버튼 클릭 | 저장 완료, 목록 갱신 | + +### 시나리오 5: 품목 삭제 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | 삭제할 행 체크박스 선택 (다중 가능) | 행 선택 표시 | +| 2 | [삭제] 버튼 클릭 | 삭제 확인 다이얼로그 표시 | +| 3 | 확인 | 삭제 완료, 목록 갱신 | + +--- + +## 6. 검증 체크리스트 + +### 기본 기능 +- [ ] 데이터 조회가 정상 동작하는가? +- [ ] 검색 필터 (상태, 품번코드, 품명)가 정상 동작하는가? +- [ ] 신규 등록이 정상 동작하는가? +- [ ] 복사 기능이 정상 동작하는가? +- [ ] 수정이 정상 동작하는가? +- [ ] 삭제가 정상 동작하는가? +- [ ] 코드변경이 정상 동작하는가? +- [ ] 엑셀 업로드가 정상 동작하는가? +- [ ] 엑셀 다운로드가 정상 동작하는가? + +### 테이블 기능 +- [ ] 페이지네이션이 정상 동작하는가? +- [ ] 정렬이 정상 동작하는가? +- [ ] 컬럼 너비 조정이 정상 동작하는가? +- [ ] 체크박스 선택이 정상 동작하는가? + +### 검색 위젯 연동 +- [ ] v2-table-search-widget과 v2-table-list 연동이 정상 동작하는가? +- [ ] 필터 변경 시 자동 재조회가 동작하는가? +- [ ] 초기화 버튼이 정상 동작하는가? + +### 그룹핑 기능 +- [ ] Group by 선택 시 그룹핑이 정상 동작하는가? +- [ ] 다중 그룹핑이 정상 동작하는가? + +--- + +## 7. 참고 사항 + +### 관련 테이블 +- `customer_item_mapping` - 거래처별 품목 매핑 +- `supplier_item_mapping` - 공급업체별 품목 매핑 +- `item_inspection_info` - 품목 검사 정보 +- `item_routing_version` - 품목별 공정 버전 +- `item_routing_detail` - 품목별 공정 상세 + +### 특이 사항 +- `division` 컬럼은 카테고리 코드 (예: CATEGORY_191259)로 저장됨 +- `meno` 컬럼은 오타로 보임 (원래 memo) +- `selling_price`, `standard_price`는 varchar로 저장됨 (숫자 형식 문자열) +- `company_code`는 멀티테넌시용 회사 코드 + +--- + +## 8. DB INSERT용 JSON 설정 (screen_layouts_v2 방식) + +> 📌 실제 화면 저장은 `screen_definitions` + `screen_layouts_v2` 테이블을 사용합니다. +> `screen_layouts_v2`는 전체 레이아웃을 하나의 JSON (`layout_data`)으로 저장합니다. + +### 8.1 테이블 구조 + +#### screen_definitions + +| 컬럼명 | 타입 | 필수 | 기본값 | 설명 | +|--------|------|------|--------|------| +| `screen_id` | integer | PK | 자동 생성 (시퀀스) | 화면 고유 ID | +| `screen_name` | varchar(100) | ✅ | - | 화면명 | +| `screen_code` | varchar(50) | ✅ | **자동 생성** | `{company_code}_{순번}` 형식 | +| `table_name` | varchar(100) | | - | 기본 테이블명 | +| `company_code` | varchar(50) | ✅ | - | 회사 코드 | +| `description` | text | | - | 화면 설명 | +| `is_active` | char(1) | | `'Y'` | Y=활성, N=비활성, D=삭제 | +| `created_date` | timestamp | | `CURRENT_TIMESTAMP` | 생성일시 | +| `db_source_type` | varchar(10) | | `'internal'` | internal/external | +| `data_source_type` | varchar(20) | | `'database'` | database/rest_api | + +#### screen_layouts_v2 + +| 컬럼명 | 타입 | 필수 | 기본값 | 설명 | +|--------|------|------|--------|------| +| `layout_id` | integer | PK | 자동 생성 (시퀀스) | 레이아웃 고유 ID | +| `screen_id` | integer | ✅ | - | 화면 ID (FK) | +| `company_code` | varchar(20) | ✅ | - | 회사 코드 | +| `layout_data` | jsonb | ✅ | `'{}'` | 전체 레이아웃 JSON | +| `created_at` | timestamp | | `now()` | 생성일시 | +| `updated_at` | timestamp | | `now()` | 수정일시 | + +### 8.2 화면 정의 (screen_definitions) + +> ⚠️ `screen_code`는 API 호출 시 자동 생성됩니다. (`{company_code}_{순번}` 형식) + +**필수 입력 필드:** + +```json +{ + "screenName": "품목정보", + "tableName": "item_info", + "companyCode": "COMPANY_7", + "description": "품목 기본정보 관리 화면" +} +``` + +**전체 필드 (자동 생성 포함):** + +```json +{ + "screen_id": 140, + "screen_name": "품목정보", + "screen_code": "COMPANY_7_3", + "table_name": "item_info", + "company_code": "COMPANY_7", + "description": "품목 기본정보 관리 화면", + "is_active": "Y", + "db_source_type": "internal", + "data_source_type": "database", + "created_date": "2025-01-29T00:00:00.000Z" +} +``` + +### 8.2 레이아웃 데이터 (screen_layouts_v2.layout_data) + +> 전체 레이아웃을 하나의 JSON으로 저장 + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_search", + "url": "@/lib/registry/components/v2-table-search-widget", + "size": { "width": 1920, "height": 80 }, + "position": { "x": 0, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-table-search-widget", + "label": "검색 필터", + "webTypeConfig": {} + }, + "displayOrder": 0 + }, + { + "id": "comp_table", + "url": "@/lib/registry/components/v2-table-list", + "size": { "width": 1920, "height": 930 }, + "position": { "x": 0, "y": 150, "z": 1 }, + "overrides": { + "type": "v2-table-list", + "label": "테이블 리스트", + "filter": { "enabled": true, "filters": [] }, + "height": "auto", + "actions": { "actions": [], "bulkActions": false, "showActions": false }, + "columns": [ + { "align": "left", "order": 0, "format": "text", "visible": true, "sortable": true, "columnName": "status", "searchable": true, "displayName": "status" }, + { "align": "left", "order": 1, "format": "text", "visible": true, "sortable": true, "columnName": "item_number", "searchable": true, "displayName": "item_number" }, + { "align": "left", "order": 2, "format": "text", "visible": true, "sortable": true, "columnName": "item_name", "searchable": true, "displayName": "item_name" }, + { "align": "left", "order": 3, "format": "text", "visible": true, "sortable": true, "columnName": "size", "searchable": true, "displayName": "size" }, + { "align": "left", "order": 4, "format": "text", "visible": true, "sortable": true, "columnName": "material", "searchable": true, "displayName": "material" }, + { "align": "left", "order": 5, "format": "text", "visible": true, "sortable": true, "columnName": "inventory_unit", "searchable": true, "displayName": "inventory_unit" }, + { "align": "left", "order": 6, "format": "text", "visible": true, "sortable": true, "columnName": "weight", "searchable": true, "displayName": "weight" }, + { "align": "left", "order": 7, "format": "text", "visible": true, "sortable": true, "columnName": "unit", "searchable": true, "displayName": "unit" }, + { "align": "left", "order": 8, "format": "text", "visible": true, "sortable": true, "columnName": "division", "searchable": true, "displayName": "division" }, + { "align": "left", "order": 9, "format": "text", "visible": true, "sortable": true, "columnName": "type", "searchable": true, "displayName": "type" }, + { "align": "left", "order": 10, "format": "text", "visible": true, "sortable": true, "columnName": "writer", "searchable": true, "displayName": "writer" } + ], + "autoLoad": true, + "checkbox": { "enabled": true, "multiple": true, "position": "left", "selectAll": true }, + "pagination": { "enabled": true, "pageSize": 20, "showPageInfo": true, "pageSizeOptions": [10, 20, 50, 100], "showSizeSelector": true }, + "showFooter": true, + "showHeader": true, + "tableStyle": { "theme": "default", "rowHeight": "normal", "borderStyle": "light", "headerStyle": "default", "hoverEffect": true, "alternateRows": true }, + "displayMode": "table", + "stickyHeader": false, + "selectedTable": "item_info", + "webTypeConfig": {}, + "horizontalScroll": { "enabled": true, "maxColumnWidth": 300, "minColumnWidth": 100, "maxVisibleColumns": 8 } + }, + "displayOrder": 0 + }, + { + "id": "comp_btn_code_merge", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 88, "height": 40 }, + "position": { "x": 10, "y": 100, "z": 1 }, + "overrides": { + "text": "코드변경", + "type": "v2-button-primary", + "label": "기본 버튼", + "action": { + "type": "code_merge", + "errorMessage": "저장 중 오류가 발생했습니다.", + "successMessage": "저장되었습니다.", + "mergeColumnName": "item_number" + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { + "variant": "default", + "actionType": "custom", + "dataflowConfig": { + "flowConfig": { "flowId": 18, "flowName": "품번코드 통합", "contextData": {}, "executionTiming": "after" }, + "selectedDiagramId": 18 + } + } + }, + "displayOrder": 0 + }, + { + "id": "comp_btn_upload", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 88, "height": 40 }, + "position": { "x": 110, "y": 100, "z": 1 }, + "overrides": { + "text": "업로드", + "type": "v2-button-primary", + "label": "기본 버튼", + "action": { + "type": "excel_upload", + "errorMessage": "저장 중 오류가 발생했습니다.", + "successMessage": "저장되었습니다." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 0 + }, + { + "id": "comp_btn_download", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 88, "height": 40 }, + "position": { "x": 210, "y": 100, "z": 1 }, + "overrides": { + "text": "다운로드", + "type": "v2-button-primary", + "label": "기본 버튼", + "action": { + "type": "excel_download", + "errorMessage": "저장 중 오류가 발생했습니다.", + "successMessage": "저장되었습니다." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 0 + }, + { + "id": "comp_btn_register", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1550, "y": 100, "z": 1 }, + "overrides": { + "text": "등록", + "type": "v2-button-primary", + "label": "기본 버튼", + "action": { + "type": "modal", + "modalSize": "md", + "modalTitle": "품목 등록", + "errorMessage": "저장 중 오류가 발생했습니다.", + "successMessage": "저장되었습니다.", + "targetScreenId": "{{modal_screen_id}}" + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 0 + }, + { + "id": "comp_btn_copy", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1640, "y": 100, "z": 1 }, + "overrides": { + "text": "복사", + "type": "v2-button-primary", + "label": "기본 버튼", + "action": { + "type": "copy", + "errorMessage": "저장 중 오류가 발생했습니다.", + "successMessage": "저장되었습니다.", + "targetScreenId": "{{modal_screen_id}}" + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 0 + }, + { + "id": "comp_btn_edit", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1730, "y": 100, "z": 1 }, + "overrides": { + "text": "수정", + "type": "v2-button-primary", + "label": "기본 버튼", + "action": { + "type": "edit", + "errorMessage": "저장 중 오류가 발생했습니다.", + "successMessage": "저장되었습니다.", + "targetScreenId": "{{modal_screen_id}}" + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 0 + }, + { + "id": "comp_btn_delete", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 1820, "y": 100, "z": 1 }, + "overrides": { + "text": "삭제", + "type": "v2-button-primary", + "label": "기본 버튼", + "action": { + "type": "delete", + "errorMessage": "저장 중 오류가 발생했습니다.", + "successMessage": "저장되었습니다." + }, + "variant": "primary", + "actionType": "button", + "webTypeConfig": { "variant": "default", "actionType": "custom" } + }, + "displayOrder": 0 + } + ] +} +``` + +### 8.3 모달 화면 (품목 등록/수정) + +#### 화면 정의 (필수 입력) + +```json +{ + "screenName": "품목 등록/수정", + "tableName": "item_info", + "companyCode": "COMPANY_7", + "description": "품목 등록/수정 폼 화면" +} +``` + +#### 레이아웃 데이터 (screen_layouts_v2.layout_data) + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_item_number", + "url": "@/lib/registry/components/v2-text-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-text-input", + "label": "품번코드", + "fieldName": "item_number", + "placeholder": "품번코드를 입력하세요", + "required": true + }, + "displayOrder": 0 + }, + { + "id": "comp_item_name", + "url": "@/lib/registry/components/v2-text-input", + "size": { "width": 300, "height": 60 }, + "position": { "x": 340, "y": 20, "z": 1 }, + "overrides": { + "type": "v2-text-input", + "label": "품명", + "fieldName": "item_name", + "placeholder": "품명을 입력하세요", + "required": true + }, + "displayOrder": 1 + }, + { + "id": "comp_status", + "url": "@/lib/registry/components/v2-select-basic", + "size": { "width": 300, "height": 60 }, + "position": { "x": 20, "y": 100, "z": 1 }, + "overrides": { + "type": "v2-select-basic", + "label": "상태", + "fieldName": "status", + "options": ["정상", "품절", "대기", "단종"] + }, + "displayOrder": 2 + }, + { + "id": "comp_btn_save", + "url": "@/lib/registry/components/v2-button-primary", + "size": { "width": 80, "height": 40 }, + "position": { "x": 400, "y": 500, "z": 1 }, + "overrides": { + "text": "저장", + "type": "v2-button-primary", + "label": "저장 버튼", + "action": { + "type": "save", + "closeModalAfterSave": true, + "refreshParentTable": true, + "successMessage": "저장되었습니다.", + "errorMessage": "저장 중 오류가 발생했습니다." + }, + "variant": "primary", + "actionType": "button" + }, + "displayOrder": 20 + } + ] +} +``` + +### 8.4 API 호출 방식 + +> 📌 실제 화면 생성은 API를 통해 진행됩니다. `screen_code`는 서버에서 자동 생성됩니다. + +#### Step 1: 화면 코드 자동 생성 API + +```http +GET /api/screens/generate-code?companyCode=COMPANY_7 +``` + +**응답:** +```json +{ + "success": true, + "data": { "screenCode": "COMPANY_7_4" } +} +``` + +#### Step 2: 화면 생성 API + +```http +POST /api/screens +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "screenName": "품목정보", + "screenCode": "COMPANY_7_4", + "tableName": "item_info", + "companyCode": "COMPANY_7", + "description": "품목 기본정보 관리 화면" +} +``` + +**응답:** +```json +{ + "success": true, + "data": { + "screenId": 141, + "screenCode": "COMPANY_7_4", + "screenName": "품목정보" + } +} +``` + +#### Step 3: 레이아웃 저장 API + +```http +PUT /api/screens/141/layout-v2 +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "layoutData": { + "version": "2.0", + "components": [ /* 8.2의 components 배열 */ ] + } +} +``` + +### 8.5 SQL 직접 INSERT (참고용) + +> ⚠️ 일반적으로 API를 사용하지만, 대량 마이그레이션 시 직접 SQL 사용 가능 + +```sql +-- Step 1: 화면 정의 (screen_code는 수동 지정 필요) +INSERT INTO screen_definitions ( + screen_name, screen_code, table_name, company_code, description +) VALUES ( + '품목정보', 'COMPANY_7_4', 'item_info', 'COMPANY_7', '품목 기본정보 관리 화면' +) RETURNING screen_id; + +-- Step 2: 레이아웃 저장 (screen_id 사용) +INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data) +VALUES ( + 141, -- 위에서 반환된 screen_id + 'COMPANY_7', + '{"version": "2.0", "components": [...]}'::jsonb +); +``` + +### 8.6 주의사항 + +| 항목 | 설명 | +|------|------| +| `screen_code` | API 사용 시 `generateScreenCode` 먼저 호출, 형식: `{company_code}_{순번}` | +| `screen_id` | 화면 생성 후 반환되는 값, 레이아웃 저장 시 필요 | +| `component.id` | 고유 ID (UUID 또는 `comp_` prefix), 중복 불가 | +| `component.url` | `@/lib/registry/components/v2-xxx` 형식 | +| `{{modal_screen_id}}` | 모달 화면 먼저 생성 후 실제 ID로 치환 | +| `version` | 반드시 `"2.0"` 사용 | +| UNIQUE 제약 | `screen_layouts_v2`는 `(screen_id, company_code)` 조합이 유니크 | diff --git a/docs/screen-implementation-guide/README.md b/docs/screen-implementation-guide/README.md new file mode 100644 index 00000000..18acd6db --- /dev/null +++ b/docs/screen-implementation-guide/README.md @@ -0,0 +1,126 @@ +# 화면 구현 가이드 + +V2 컴포넌트를 활용한 ERP 화면 구현 가이드입니다. + +## 폴더 구조 + +``` +screen-implementation-guide/ +├── 01_master-data/ # 기준정보 +│ ├── company-info.md # 회사정보 +│ ├── department.md # 부서관리 +│ ├── item-info.md # 품목정보 +│ └── options.md # 옵션설정 +│ +├── 02_sales/ # 영업관리 +│ ├── quotation.md # 견적관리 +│ ├── order.md # 수주관리 +│ ├── customer.md # 거래처관리 +│ ├── sales-item.md # 판매품목정보 +│ └── options.md # 영업옵션설정 +│ +├── 03_production/ # 생산관리 +│ ├── production-plan.md # 생산계획 +│ ├── work-order.md # 작업지시 +│ ├── production-result.md # 생산실적 +│ ├── process-info.md # 공정정보관리 +│ ├── bom.md # BOM관리 +│ └── options.md # 생산옵션설정 +│ +├── 04_purchase/ # 구매관리 +│ ├── purchase-order.md # 발주관리 +│ ├── purchase-item.md # 구매품목정보 +│ ├── supplier.md # 공급업체관리 +│ ├── receiving.md # 입고관리 +│ └── options.md # 구매옵션설정 +│ +├── 05_equipment/ # 설비관리 +│ ├── equipment-info.md # 설비정보 +│ └── options.md # 설비옵션설정 +│ +├── 06_logistics/ # 물류관리 +│ ├── logistics-info.md # 물류정보관리 +│ ├── inout.md # 입출고관리 +│ ├── inventory.md # 재고현황 +│ ├── warehouse.md # 창고정보관리 +│ ├── shipping.md # 출고관리 +│ └── options.md # 물류옵션설정 +│ +├── 07_quality/ # 품질관리 +│ ├── inspection-info.md # 검사정보관리 +│ ├── item-inspection.md # 품목검사정보 +│ └── options.md # 품질옵션설정 +│ +└── README.md +``` + +## 문서 작성 형식 + +각 화면별 문서는 다음 구조로 작성됩니다: + +### 1. 테이블 선택 및 화면 구조 +- 사용할 데이터베이스 테이블 +- 테이블 간 관계 (FK, 조인) +- 화면 전체 레이아웃 + +### 2. 컴포넌트 배치도 +- ASCII 다이어그램으로 컴포넌트 배치 +- 각 영역별 사용 컴포넌트 명시 + +### 3. 각 컴포넌트별 설정 +- 컴포넌트 타입 +- 상세 설정 (config) +- 연동 설정 + +### 4. 사용자 사용 예시 시나리오 +- 테스트 시나리오 +- 기대 동작 +- 검증 포인트 + +## 메뉴별 Screen ID 매핑 + +| 메뉴 | Screen ID | 상태 | +|------|-----------|------| +| **기준정보** | | | +| 회사정보 | /screens/138 | 활성화 | +| 부서관리 | /screens/1487 | 활성화 | +| 품목정보 | /screens/140 | 활성화 | +| 옵션설정 | /screens/1421 | 활성화 | +| **영업관리** | | | +| 견적관리 | - | 활성화 | +| 수주관리 | /screens/156 | 활성화 | +| 거래처관리 | - | 활성화 | +| 판매품목정보 | - | 활성화 | +| 영업옵션설정 | /screens/1552 | 활성화 | +| **생산관리** | | | +| 생산계획 | - | 활성화 | +| 작업지시 | - | 활성화 | +| 생산실적 | - | 활성화 | +| 공정정보관리 | /screens/1599 | 활성화 | +| BOM관리 | - | 활성화 | +| 생산옵션설정 | /screens/1606 | 활성화 | +| **구매관리** | | | +| 발주관리 | /screens/1244 | 활성화 | +| 구매품목정보 | /screens/1061 | 활성화 | +| 공급업체관리 | /screens/1053 | 활성화 | +| 입고관리 | /screens/1064 | 활성화 | +| 구매옵션설정 | /screens/1057 | 활성화 | +| **설비관리** | | | +| 설비정보 | /screens/1253 | 활성화 | +| 설비옵션설정 | /screens/1264 | 활성화 | +| **물류관리** | | | +| 물류정보관리 | /screens/1556 | 활성화 | +| 입출고관리 | - | 활성화 | +| 재고현황 | /screens/1587 | 활성화 | +| 창고정보관리 | /screens/1562 | 활성화 | +| 출고관리 | /screens/2296 | 활성화 | +| 물류옵션설정 | /screens/1559 | 활성화 | +| **품질관리** | | | +| 검사정보관리 | /screens/1616 | 활성화 | +| 품목검사정보 | /screens/2089 | 활성화 | +| 품질옵션설정 | /screens/1622 | 활성화 | + +## 참고 문서 + +- [V2 컴포넌트 분석 가이드](../V2_컴포넌트_분석_가이드.md) +- [V2 컴포넌트 연동 가이드](../V2_컴포넌트_연동_가이드.md) diff --git a/docs/screen-implementation-guide/_TEMPLATE.md b/docs/screen-implementation-guide/_TEMPLATE.md new file mode 100644 index 00000000..e9efd0ba --- /dev/null +++ b/docs/screen-implementation-guide/_TEMPLATE.md @@ -0,0 +1,212 @@ +# [화면명] + +> Screen ID: /screens/XXX +> 메뉴 경로: [L2 메뉴] > [L3 메뉴] + +## 1. 테이블 선택 및 화면 구조 + +### 1.1 사용 테이블 + +| 테이블명 | 용도 | 비고 | +|----------|------|------| +| `table_name` | 마스터 데이터 | 주 테이블 | +| `detail_table` | 디테일 데이터 | FK: master_id | + +### 1.2 테이블 관계 + +``` +┌─────────────────┐ ┌─────────────────┐ +│ master_table │ │ detail_table │ +├─────────────────┤ ├─────────────────┤ +│ id (PK) │──1:N──│ master_id (FK) │ +│ name │ │ id (PK) │ +│ ... │ │ ... │ +└─────────────────┘ └─────────────────┘ +``` + +### 1.3 화면 구조 개요 + +- **화면 유형**: [목록형 / 마스터-디테일 / 단일 폼 / 복합] +- **주요 기능**: [CRUD / 조회 / 집계 등] + +--- + +## 2. 컴포넌트 배치도 + +### 2.1 전체 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ [검색 영역] v2-table-search-widget │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ [메인 테이블] v2-table-list │ +│ │ +├─────────────────────────────────────────────────────────────┤ +│ [버튼 영역] v2-button-primary (신규, 저장, 삭제) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 컴포넌트 목록 + +| 컴포넌트 ID | 컴포넌트 타입 | 역할 | +|-------------|---------------|------| +| `search-widget` | v2-table-search-widget | 검색 필터 | +| `main-table` | v2-table-list | 데이터 목록 | +| `btn-new` | v2-button-primary | 신규 등록 | +| `btn-save` | v2-button-primary | 저장 | +| `btn-delete` | v2-button-primary | 삭제 | + +--- + +## 3. 각 컴포넌트별 설정 + +### 3.1 v2-table-search-widget + +```json +{ + "targetTableId": "main-table", + "searchFields": [ + { + "field": "name", + "label": "이름", + "type": "text" + }, + { + "field": "status", + "label": "상태", + "type": "select", + "options": [ + { "value": "active", "label": "활성" }, + { "value": "inactive", "label": "비활성" } + ] + } + ] +} +``` + +### 3.2 v2-table-list + +```json +{ + "tableName": "master_table", + "columns": [ + { + "field": "id", + "headerName": "ID", + "width": 80, + "visible": false + }, + { + "field": "name", + "headerName": "이름", + "width": 150 + }, + { + "field": "status", + "headerName": "상태", + "width": 100 + } + ], + "features": { + "checkbox": true, + "pagination": true, + "sorting": true + }, + "pagination": { + "pageSize": 20 + } +} +``` + +### 3.3 v2-button-primary (저장) + +```json +{ + "label": "저장", + "actionType": "save", + "variant": "default", + "afterSaveActions": ["refreshTable"] +} +``` + +--- + +## 4. 컴포넌트 연동 설정 + +### 4.1 이벤트 흐름 + +``` +[검색 입력] + │ + ▼ +v2-table-search-widget + │ onFilterChange + ▼ +v2-table-list (자동 재조회) + │ + ▼ +[데이터 표시] +``` + +### 4.2 연동 설정 + +| 소스 컴포넌트 | 이벤트/액션 | 대상 컴포넌트 | 동작 | +|---------------|-------------|---------------|------| +| search-widget | onFilterChange | main-table | 필터 적용 | +| btn-save | click | main-table | refreshTable | + +--- + +## 5. 사용자 사용 예시 시나리오 + +### 시나리오 1: 데이터 조회 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | 화면 진입 | 전체 목록 표시 | +| 2 | 검색어 입력 | 필터링된 결과 표시 | +| 3 | 정렬 클릭 | 정렬 순서 변경 | + +### 시나리오 2: 데이터 등록 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | [신규] 버튼 클릭 | 등록 모달/폼 표시 | +| 2 | 데이터 입력 | 입력 필드 채움 | +| 3 | [저장] 버튼 클릭 | 저장 완료, 목록 갱신 | + +### 시나리오 3: 데이터 수정 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | 행 더블클릭 | 수정 모달/폼 표시 | +| 2 | 데이터 수정 | 필드 값 변경 | +| 3 | [저장] 버튼 클릭 | 저장 완료, 목록 갱신 | + +### 시나리오 4: 데이터 삭제 + +| 단계 | 사용자 동작 | 기대 결과 | +|------|-------------|-----------| +| 1 | 행 체크박스 선택 | 선택 표시 | +| 2 | [삭제] 버튼 클릭 | 삭제 확인 다이얼로그 | +| 3 | 확인 | 삭제 완료, 목록 갱신 | + +--- + +## 6. 검증 체크리스트 + +- [ ] 데이터 조회가 정상 동작하는가? +- [ ] 검색 필터가 정상 동작하는가? +- [ ] 신규 등록이 정상 동작하는가? +- [ ] 수정이 정상 동작하는가? +- [ ] 삭제가 정상 동작하는가? +- [ ] 페이지네이션이 정상 동작하는가? +- [ ] 정렬이 정상 동작하는가? + +--- + +## 7. 참고 사항 + +- 관련 화면: [관련 화면명](./related-screen.md) +- 특이 사항: 없음 diff --git a/frontend/components/unified/UnifiedRepeater.tsx b/frontend/components/unified/UnifiedRepeater.tsx index 1437b5cf..6e6108d6 100644 --- a/frontend/components/unified/UnifiedRepeater.tsx +++ b/frontend/components/unified/UnifiedRepeater.tsx @@ -8,6 +8,11 @@ * - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼 * * RepeaterTable 및 ItemSelectionModal 재사용 + * + * 데이터 전달 인터페이스: + * - DataProvidable: 선택된 데이터 제공 + * - DataReceivable: 외부에서 데이터 수신 + * - repeaterDataChange 이벤트 발행 */ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; @@ -28,6 +33,13 @@ import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/Re import { ItemSelectionModal } from "@/lib/registry/components/modal-repeater-table/ItemSelectionModal"; import { RepeaterColumnConfig } from "@/lib/registry/components/modal-repeater-table/types"; +// 데이터 전달 인터페이스 +import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer"; +import { useScreenContextOptional } from "@/contexts/ScreenContext"; + +// V2 이벤트 시스템 +import { V2_EVENTS, dispatchV2Event } from "@/types/component-events"; + // 전역 UnifiedRepeater 등록 (buttonActions에서 사용) declare global { interface Window { @@ -55,6 +67,9 @@ export const UnifiedRepeater: React.FC = ({ [propConfig], ); + // ScreenContext (데이터 전달 인터페이스 등록용) + const screenContext = useScreenContextOptional(); + // 상태 const [data, setData] = useState(initialData || []); const [selectedRows, setSelectedRows] = useState>(new Set()); @@ -101,6 +116,123 @@ export const UnifiedRepeater: React.FC = ({ }; }, [config.dataSource?.tableName]); + // ============================================================ + // DataProvidable 인터페이스 구현 + // 다른 컴포넌트에서 이 리피터의 데이터를 가져갈 수 있게 함 + // ============================================================ + const dataProvider: DataProvidable = useMemo(() => ({ + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "unified-repeater", + + // 선택된 행 데이터 반환 + getSelectedData: () => { + return Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean); + }, + + // 전체 데이터 반환 + getAllData: () => { + return [...data]; + }, + + // 선택 초기화 + clearSelection: () => { + setSelectedRows(new Set()); + }, + }), [parentId, config.fieldName, data, selectedRows]); + + // ============================================================ + // DataReceivable 인터페이스 구현 + // 외부에서 이 리피터로 데이터를 전달받을 수 있게 함 + // ============================================================ + const dataReceiver: DataReceivable = useMemo(() => ({ + componentId: parentId || config.fieldName || "unified-repeater", + componentType: "repeater", + + // 데이터 수신 (append, replace, merge 모드 지원) + receiveData: async (incomingData: any[], receiverConfig: DataReceiverConfig) => { + if (!incomingData || incomingData.length === 0) return; + + // 매핑 규칙 적용 + const mappedData = incomingData.map((item, index) => { + const newRow: any = { _id: `received_${Date.now()}_${index}` }; + + if (receiverConfig.mappingRules && receiverConfig.mappingRules.length > 0) { + receiverConfig.mappingRules.forEach((rule) => { + const sourceValue = item[rule.sourceField]; + newRow[rule.targetField] = sourceValue !== undefined ? sourceValue : rule.defaultValue; + }); + } else { + // 매핑 규칙 없으면 그대로 복사 + Object.assign(newRow, item); + } + + return newRow; + }); + + // 모드에 따라 데이터 처리 + switch (receiverConfig.mode) { + case "replace": + setData(mappedData); + onDataChange?.(mappedData); + break; + case "merge": + // 중복 제거 후 병합 (id 또는 _id 기준) + const existingIds = new Set(data.map((row) => row.id || row._id)); + const newItems = mappedData.filter((row) => !existingIds.has(row.id || row._id)); + const mergedData = [...data, ...newItems]; + setData(mergedData); + onDataChange?.(mergedData); + break; + case "append": + default: + const appendedData = [...data, ...mappedData]; + setData(appendedData); + onDataChange?.(appendedData); + break; + } + }, + + // 현재 데이터 반환 + getData: () => { + return [...data]; + }, + }), [parentId, config.fieldName, data, onDataChange]); + + // ============================================================ + // ScreenContext에 DataProvider/DataReceiver 등록 + // ============================================================ + useEffect(() => { + if (screenContext && (parentId || config.fieldName)) { + const componentId = parentId || config.fieldName || "unified-repeater"; + + screenContext.registerDataProvider(componentId, dataProvider); + screenContext.registerDataReceiver(componentId, dataReceiver); + + return () => { + screenContext.unregisterDataProvider(componentId); + screenContext.unregisterDataReceiver(componentId); + }; + } + }, [screenContext, parentId, config.fieldName, dataProvider, dataReceiver]); + + // ============================================================ + // repeaterDataChange 이벤트 발행 + // 데이터 변경 시 다른 컴포넌트(aggregation-widget 등)에 알림 + // ============================================================ + const prevDataLengthRef = useRef(data.length); + useEffect(() => { + // 데이터가 변경되었을 때만 이벤트 발행 + if (typeof window !== "undefined" && data.length !== prevDataLengthRef.current) { + dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, { + componentId: parentId || config.fieldName || "unified-repeater", + tableName: config.dataSource?.tableName || "", + data: data, + selectedData: Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean), + }); + prevDataLengthRef.current = data.length; + } + }, [data, selectedRows, parentId, config.fieldName, config.dataSource?.tableName]); + // 저장 이벤트 리스너 useEffect(() => { const handleSaveEvent = async (event: CustomEvent) => { diff --git a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx index bab8e691..04d610ba 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx +++ b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetComponent.tsx @@ -8,6 +8,9 @@ import { cn } from "@/lib/utils"; import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext"; import { apiClient } from "@/lib/api/client"; +// V2 이벤트 시스템 +import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events"; + interface AggregationWidgetComponentProps extends ComponentRendererProps { config?: AggregationWidgetConfig; // 외부에서 데이터를 직접 전달받을 수 있음 @@ -269,19 +272,19 @@ export function AggregationWidgetComponent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataSourceType, selectedRowsKey]); + // ============================================================ // 전역 선택 이벤트 수신 (dataSourceType === "selection"일 때) + // V2 표준 이벤트만 사용 (중복 이벤트 제거됨) + // ============================================================ useEffect(() => { if (dataSourceType !== "selection" || isDesignMode) return; - // 테이블리스트에서 발생하는 선택 이벤트 수신 - // tableListDataChange 이벤트의 data가 선택된 행들임 - const handleTableListDataChange = (event: CustomEvent) => { - const { data: eventData, selectedRows: eventSelectedRows } = event.detail || {}; - // data가 선택된 행 데이터 배열 + // 테이블리스트 데이터 변경 이벤트 (V2 표준) + const handleTableListDataChange = (event: CustomEvent) => { + const { data: eventData } = event.detail || {}; const rows = eventData || []; if (Array.isArray(rows)) { - // 필터 적용 const filteredData = applyFilters( rows, filtersRef.current || [], @@ -293,8 +296,8 @@ export function AggregationWidgetComponent({ } }; - // 리피터에서 발생하는 이벤트 - const handleRepeaterDataChange = (event: CustomEvent) => { + // 리피터 데이터 변경 이벤트 (V2 표준) + const handleRepeaterDataChange = (event: CustomEvent) => { const { data: eventData, selectedData } = event.detail || {}; const rows = selectedData || eventData || []; @@ -310,38 +313,13 @@ export function AggregationWidgetComponent({ } }; - // 일반 선택 이벤트 - const handleSelectionChange = (event: CustomEvent) => { - const { selectedRows: eventSelectedRows, selectedData, checkedRows, selectedItems } = event.detail || {}; - const rows = selectedData || eventSelectedRows || checkedRows || selectedItems || []; - - if (Array.isArray(rows)) { - const filteredData = applyFilters( - rows, - filtersRef.current || [], - filterLogic, - formDataRef.current, - selectedRowsRef.current - ); - setData(filteredData); - } - }; - - // 다양한 선택 이벤트 수신 - window.addEventListener("tableListDataChange" as any, handleTableListDataChange); - window.addEventListener("repeaterDataChange" as any, handleRepeaterDataChange); - window.addEventListener("selectionChange" as any, handleSelectionChange); - window.addEventListener("tableSelectionChange" as any, handleSelectionChange); - window.addEventListener("rowSelectionChange" as any, handleSelectionChange); - window.addEventListener("checkboxSelectionChange" as any, handleSelectionChange); + // V2 표준 이벤트만 구독 (중복 이벤트 제거) + const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handleTableListDataChange); + const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, handleRepeaterDataChange); return () => { - window.removeEventListener("tableListDataChange" as any, handleTableListDataChange); - window.removeEventListener("repeaterDataChange" as any, handleRepeaterDataChange); - window.removeEventListener("selectionChange" as any, handleSelectionChange); - window.removeEventListener("tableSelectionChange" as any, handleSelectionChange); - window.removeEventListener("rowSelectionChange" as any, handleSelectionChange); - window.removeEventListener("checkboxSelectionChange" as any, handleSelectionChange); + unsubscribeTableList(); + unsubscribeRepeater(); }; }, [dataSourceType, isDesignMode, filterLogic]); @@ -362,14 +340,17 @@ export function AggregationWidgetComponent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [externalDataKey, filterLogic]); + // ============================================================ // 컴포넌트 데이터 변경 이벤트 리스닝 (dataSourceType === "component"일 때) + // V2 표준 이벤트만 사용 + // ============================================================ useEffect(() => { if (dataSourceType !== "component" || !dataSourceComponentId || isDesignMode) return; - const handleDataChange = (event: CustomEvent) => { + // 테이블 리스트 데이터 변경 + const handleTableListDataChange = (event: CustomEvent) => { const { componentId, data: eventData } = event.detail || {}; if (componentId === dataSourceComponentId && Array.isArray(eventData)) { - // 필터 적용 const filteredData = applyFilters( eventData, filtersRef.current || [], @@ -381,33 +362,31 @@ export function AggregationWidgetComponent({ } }; - // 선택 변경 이벤트 (체크박스 선택 등) - const handleSelectionChange = (event: CustomEvent) => { - const { componentId, selectedData } = event.detail || {}; - if (componentId === dataSourceComponentId && Array.isArray(selectedData)) { - // 선택된 데이터만 집계 - const filteredData = applyFilters( - selectedData, - filtersRef.current || [], - filterLogic, - formDataRef.current, - selectedRowsRef.current - ); - setData(filteredData); + // 리피터 데이터 변경 + const handleRepeaterDataChange = (event: CustomEvent) => { + const { componentId, data: eventData, selectedData } = event.detail || {}; + if (componentId === dataSourceComponentId) { + const rows = selectedData || eventData || []; + if (Array.isArray(rows)) { + const filteredData = applyFilters( + rows, + filtersRef.current || [], + filterLogic, + formDataRef.current, + selectedRowsRef.current + ); + setData(filteredData); + } } }; - // 리피터 데이터 변경 이벤트 - window.addEventListener("repeaterDataChange" as any, handleDataChange); - // 테이블 리스트 데이터 변경 이벤트 - window.addEventListener("tableListDataChange" as any, handleDataChange); - // 선택 변경 이벤트 - window.addEventListener("selectionChange" as any, handleSelectionChange); + // V2 표준 이벤트만 구독 + const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handleTableListDataChange); + const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, handleRepeaterDataChange); return () => { - window.removeEventListener("repeaterDataChange" as any, handleDataChange); - window.removeEventListener("tableListDataChange" as any, handleDataChange); - window.removeEventListener("selectionChange" as any, handleSelectionChange); + unsubscribeTableList(); + unsubscribeRepeater(); }; }, [dataSourceType, dataSourceComponentId, isDesignMode, filterLogic]); diff --git a/frontend/lib/registry/components/v2-repeat-container/RepeatContainerComponent.tsx b/frontend/lib/registry/components/v2-repeat-container/RepeatContainerComponent.tsx index b51b2448..b8c3aea6 100644 --- a/frontend/lib/registry/components/v2-repeat-container/RepeatContainerComponent.tsx +++ b/frontend/lib/registry/components/v2-repeat-container/RepeatContainerComponent.tsx @@ -8,6 +8,9 @@ import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import DynamicComponentRenderer from "@/lib/registry/DynamicComponentRenderer"; +// V2 이벤트 시스템 +import { V2_EVENTS, subscribeV2Event, type TableListDataChangeDetail, type RepeaterDataChangeDetail } from "@/types/component-events"; + interface RepeatContainerComponentProps extends ComponentRendererProps { config?: RepeatContainerConfig; // 외부에서 데이터를 직접 전달받을 수 있음 @@ -136,7 +139,10 @@ export function RepeatContainerComponent({ } }, [externalData]); - // 컴포넌트 데이터 변경 이벤트 리스닝 (componentId 또는 tableName으로 매칭) + // ============================================================ + // 컴포넌트 데이터 변경 이벤트 리스닝 (V2 표준 이벤트) + // componentId 또는 tableName으로 매칭 + // ============================================================ useEffect(() => { if (isDesignMode) return; @@ -147,19 +153,12 @@ export function RepeatContainerComponent({ effectiveTableName, }); - // dataSourceComponentId가 없어도 테이블명으로 매칭 가능 - const handleDataChange = (event: CustomEvent) => { - const { componentId, tableName: eventTableName, data: eventData } = event.detail || {}; - - console.log("📩 리피터 컨테이너 이벤트 수신:", { - eventType: event.type, - fromComponentId: componentId, - fromTableName: eventTableName, - dataCount: Array.isArray(eventData) ? eventData.length : 0, - myDataSourceComponentId: dataSourceComponentId, - myEffectiveTableName: effectiveTableName, - }); - + // 공통 데이터 처리 함수 + const processIncomingData = ( + componentId: string | undefined, + eventTableName: string | undefined, + eventData: any[] + ) => { // 1. 명시적으로 dataSourceComponentId가 설정된 경우 해당 컴포넌트만 매칭 if (dataSourceComponentId) { if (componentId === dataSourceComponentId && Array.isArray(eventData)) { @@ -167,8 +166,6 @@ export function RepeatContainerComponent({ setData(eventData); setCurrentPage(1); setSelectedIndices([]); - } else { - console.log("⚠️ 리피터: 컴포넌트 ID 불일치로 무시", { expected: dataSourceComponentId, received: componentId }); } return; } @@ -179,17 +176,28 @@ export function RepeatContainerComponent({ setData(eventData); setCurrentPage(1); setSelectedIndices([]); - } else if (effectiveTableName) { - console.log("⚠️ 리피터: 테이블명 불일치로 무시", { expected: effectiveTableName, received: eventTableName }); } }; - window.addEventListener("repeaterDataChange" as any, handleDataChange); - window.addEventListener("tableListDataChange" as any, handleDataChange); + // 테이블 리스트 데이터 변경 이벤트 (V2 표준) + const handleTableListDataChange = (event: CustomEvent) => { + const { componentId, tableName: eventTableName, data: eventData } = event.detail || {}; + processIncomingData(componentId, eventTableName, eventData); + }; + + // 리피터 데이터 변경 이벤트 (V2 표준) + const handleRepeaterDataChange = (event: CustomEvent) => { + const { componentId, tableName: eventTableName, data: eventData } = event.detail || {}; + processIncomingData(componentId, eventTableName, eventData); + }; + + // V2 표준 이벤트 구독 + const unsubscribeTableList = subscribeV2Event(V2_EVENTS.TABLE_LIST_DATA_CHANGE, handleTableListDataChange); + const unsubscribeRepeater = subscribeV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, handleRepeaterDataChange); return () => { - window.removeEventListener("repeaterDataChange" as any, handleDataChange); - window.removeEventListener("tableListDataChange" as any, handleDataChange); + unsubscribeTableList(); + unsubscribeRepeater(); }; }, [component?.id, dataSourceType, dataSourceComponentId, effectiveTableName, isDesignMode]); diff --git a/frontend/types/component-events.ts b/frontend/types/component-events.ts new file mode 100644 index 00000000..3cfc959b --- /dev/null +++ b/frontend/types/component-events.ts @@ -0,0 +1,241 @@ +/** + * V2 컴포넌트 간 통신 이벤트 타입 정의 + * + * 모든 V2 컴포넌트는 이 파일에 정의된 이벤트 타입을 사용해야 합니다. + * 이벤트 발행/구독 시 타입 안전성을 보장합니다. + */ + +// ============================================================ +// 이벤트 상세 데이터 타입 (event.detail) +// ============================================================ + +/** + * 테이블 리스트 데이터 변경 이벤트 + * 발행: v2-table-list + * 구독: v2-aggregation-widget, v2-repeat-container + */ +export interface TableListDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedRows: string[] | number[]; +} + +/** + * 리피터 데이터 변경 이벤트 + * 발행: v2-unified-repeater + * 구독: v2-aggregation-widget, v2-repeat-container + */ +export interface RepeaterDataChangeDetail { + componentId: string; + tableName: string; + data: any[]; + selectedData?: any[]; +} + +/** + * 폼 저장 전 이벤트 + * 발행: buttonActions, UnifiedFormContext + * 구독: v2-unified-repeater, simple-repeater-table, modal-repeater-table 등 + */ +export interface BeforeFormSaveDetail { + formData: Record; + skipDefaultSave?: boolean; +} + +/** + * 폼 저장 후 이벤트 + * 발행: UnifiedFormContext + * 구독: 저장 결과 처리 컴포넌트들 + */ +export interface AfterFormSaveDetail { + success: boolean; + data?: any; + error?: string; +} + +/** + * 리피터 저장 이벤트 (마스터-디테일 FK 연결용) + * 발행: InteractiveScreenViewerDynamic + * 구독: v2-unified-repeater + */ +export interface RepeaterSaveDetail { + parentId?: string | number; + masterRecordId: string | number; + mainFormData: Record; + tableName: string; +} + +/** + * 테이블 새로고침 이벤트 + * 발행: v2-button-primary, buttonActions + * 구독: v2-table-list, v2-split-panel-layout + */ +export interface RefreshTableDetail { + tableName?: string; + componentId?: string; +} + +/** + * 카드 디스플레이 새로고침 이벤트 + * 발행: buttonActions, InteractiveScreenViewerDynamic + * 구독: v2-card-display + */ +export interface RefreshCardDisplayDetail { + componentId?: string; +} + +/** + * 컴포넌트 간 데이터 전달 이벤트 + * 발행: buttonActions + * 구독: v2-unified-repeater + */ +export interface ComponentDataTransferDetail { + sourceComponentId: string; + targetComponentId: string; + data: any[]; + mode: "append" | "replace" | "merge"; + mappingRules?: Array<{ + sourceField: string; + targetField: string; + defaultValue?: any; + }>; +} + +/** + * 분할 패널 간 데이터 전달 이벤트 + * 발행: buttonActions + * 구독: v2-unified-repeater, repeater-field-group + */ +export interface SplitPanelDataTransferDetail { + sourcePosition: "left" | "right"; + targetPosition: "left" | "right"; + data: any[]; + mode: "append" | "replace" | "merge"; + mappingRules?: Array<{ + sourceField: string; + targetField: string; + defaultValue?: any; + }>; +} + +/** + * 연관 데이터 버튼 선택 이벤트 + * 발행: related-data-buttons + * 구독: v2-table-list + */ +export interface RelatedButtonSelectDetail { + targetTable: string; + filterColumn: string; + filterValue: any; + selectedData?: any; +} + +/** + * 모달 제어 이벤트 + */ +export interface EditModalDetail { + screenId?: number; + recordId?: string | number; + data?: any; +} + +// ============================================================ +// 이벤트 이름 상수 +// ============================================================ + +export const V2_EVENTS = { + // 데이터 변경 이벤트 + TABLE_LIST_DATA_CHANGE: "tableListDataChange", + REPEATER_DATA_CHANGE: "repeaterDataChange", + + // 폼 저장 이벤트 + BEFORE_FORM_SAVE: "beforeFormSave", + AFTER_FORM_SAVE: "afterFormSave", + REPEATER_SAVE: "repeaterSave", + + // UI 갱신 이벤트 + REFRESH_TABLE: "refreshTable", + REFRESH_CARD_DISPLAY: "refreshCardDisplay", + + // 데이터 전달 이벤트 + COMPONENT_DATA_TRANSFER: "componentDataTransfer", + SPLIT_PANEL_DATA_TRANSFER: "splitPanelDataTransfer", + + // 모달 제어 이벤트 + OPEN_EDIT_MODAL: "openEditModal", + CLOSE_EDIT_MODAL: "closeEditModal", + SAVE_SUCCESS_IN_MODAL: "saveSuccessInModal", + + // 연관 데이터 버튼 이벤트 + RELATED_BUTTON_SELECT: "related-button-select", + RELATED_BUTTON_REGISTER: "related-button-register", + RELATED_BUTTON_UNREGISTER: "related-button-unregister", +} as const; + +// ============================================================ +// Window EventMap 확장 (타입 안전한 이벤트 리스너) +// ============================================================ + +declare global { + interface WindowEventMap { + // 데이터 변경 이벤트 + [V2_EVENTS.TABLE_LIST_DATA_CHANGE]: CustomEvent; + [V2_EVENTS.REPEATER_DATA_CHANGE]: CustomEvent; + + // 폼 저장 이벤트 + [V2_EVENTS.BEFORE_FORM_SAVE]: CustomEvent; + [V2_EVENTS.AFTER_FORM_SAVE]: CustomEvent; + [V2_EVENTS.REPEATER_SAVE]: CustomEvent; + + // UI 갱신 이벤트 + [V2_EVENTS.REFRESH_TABLE]: CustomEvent; + [V2_EVENTS.REFRESH_CARD_DISPLAY]: CustomEvent; + + // 데이터 전달 이벤트 + [V2_EVENTS.COMPONENT_DATA_TRANSFER]: CustomEvent; + [V2_EVENTS.SPLIT_PANEL_DATA_TRANSFER]: CustomEvent; + + // 연관 데이터 버튼 이벤트 + [V2_EVENTS.RELATED_BUTTON_SELECT]: CustomEvent; + } +} + +// ============================================================ +// 유틸리티 함수 +// ============================================================ + +/** + * 타입 안전한 이벤트 발행 함수 + */ +export function dispatchV2Event( + eventName: K, + detail: WindowEventMap[K] extends CustomEvent ? D : never +): void { + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent(eventName, { detail })); + } +} + +/** + * 타입 안전한 이벤트 구독 함수 + */ +export function subscribeV2Event( + eventName: K, + handler: (event: WindowEventMap[K]) => void +): () => void { + if (typeof window === "undefined") { + return () => {}; + } + + window.addEventListener(eventName, handler as EventListener); + return () => { + window.removeEventListener(eventName, handler as EventListener); + }; +} + +// ============================================================ +// 내보내기 +// ============================================================ + +export type V2EventName = typeof V2_EVENTS[keyof typeof V2_EVENTS]; diff --git a/my_layout.json b/my_layout.json new file mode 100644 index 0000000000000000000000000000000000000000..39d27b251d811ee1ac8bbafabdeeddaed8c6f63b GIT binary patch literal 17330 zcmeI4OK%e~5Xa|?#CM2%g#sxc9w!h838{iZdqb#7n$Sk1DJm(YMg8i)f9%Y9cN1s4 z$$FDWD^$GOJpA*_*z>Tr|NeI94|TTY-vhc&_i{9be$kLF=_ifof+l3>HC{EePmkyc z+K0LKrW(aO(+OD4>4zm6f@%srNA{%w9v)|zNyffOU51#1%|M5POoJlBW5{kuJ?Qv^ z&gdAj9O5_Anslo(U7_c3M)igs<8DNKw3i?`#WmAh(hYti%xsCG_s6i%ecHo&dic~B zzuzm2XpgS#xKHtl8nNiY-Rna&`q5p;EC zCCn?tOArZjovp@nmp~30zs6+jO2sxc#gAV_Q`(irBx5r4duB%|EJ%8BZ9LP>j46#> z=c(eEZnOE8|9pyTjcGe={9+ac8Aqd3}cjyD;I0iXKU+cwL@l4m~txrcYX}z;+)i-dQ&TPLvUB)uqCGjGQ z^{6pz=e!$0KR;pJY)?1z8P>wHk8Otia0)LRlG7&SBbp2`)n~ENQ+yj@l5Ysb<35tD zI~ot?EuJYwJkAk)IH%C%)W+kiA5BhPhl}I!Cf5RC*~FTl$*GOUopv-ii4G0?isYKl zlVqCGQxzL;|OQ74Fuf zTb(IyU!P#@=NXx?@mdf0=asd*s)VaE74w|sTo zBDy@lPcPd`uP2Hta>mMuZJMh+C-fbj;v(D4OA#l;GRa5`xF%T+B_|K+l~#48bLf!w zRYtaEUXH|teVsOzsR=(P&y4WSwQ#T7 z#>#0s?TJqpamIUMe|2S+gOLA zoe56V{Xh;nLN?0h6L>cuKPH`Nm-g@Y;xMU#_!pRo~L@jTtL8vTj%!>z`& zi8VoE+Kv6YF1>)xQrS|B?nKY|{kyb8x;=TOcK>dd_wRvl@sq}aQrpG|NP90%)WALCSzEq)tH);kBmcO(sHT&HkV2u0nyI# zR@dF)XOkqC;xn!FtxS$pXKHgPmXszZ(Y%>GY2}|yYI7-(4Y%95X7W>%oS&zy_2%Ue zZ%^`UQrA9D*5*=qDf(-Jy0eP~QdA3lKYXR>ZaZy!yU#)vbE)^UdO6p`>x#sk4mI70 zrss15_qr dcTJoDDU$fRGSk-tnul +setlocal EnableDelayedExpansion + +REM 스크립트가 있는 디렉토리에서 루트로 이동 +cd /d "%~dp0\..\.." + +REM 시작 시간 기록 +set START_TIME=%DATE% %TIME% + +echo ============================================ +echo WACE 솔루션 - 전체 서비스 시작 (병렬 최적화) +echo ============================================ +echo [시작 시간] %START_TIME% +echo. + +REM Docker Desktop 실행 확인 +echo [1/5] Docker Desktop 상태 확인 중... +docker --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Docker Desktop이 실행되지 않았습니다! + echo Docker Desktop을 먼저 실행해주세요. + pause + exit /b 1 +) +echo [OK] Docker Desktop이 실행 중입니다. +echo. + +REM 기존 컨테이너 정리 +echo [2/5] 기존 컨테이너 정리 중... +docker rm -f pms-backend-win pms-frontend-win 2>nul +docker network rm pms-network 2>nul +docker network create pms-network 2>nul +echo [OK] 컨테이너 정리 완료 +echo. + +REM 병렬 빌드 (docker-compose 자체가 병렬 처리) +echo [3/5] 이미지 빌드 중... (백엔드 + 프론트엔드 병렬) +echo 이 작업은 시간이 걸릴 수 있습니다... +echo. + +REM 백엔드 빌드 +docker-compose -f docker-compose.backend.win.yml build +if %errorlevel% neq 0 ( + echo [ERROR] 백엔드 빌드 실패! + pause + exit /b 1 +) +echo [OK] 백엔드 빌드 완료 +echo. + +REM 프론트엔드 빌드 +docker-compose -f docker-compose.frontend.win.yml build +if %errorlevel% neq 0 ( + echo [ERROR] 프론트엔드 빌드 실패! + pause + exit /b 1 +) +echo [OK] 프론트엔드 빌드 완료 +echo. + +REM 기존 컨테이너 정리 후 서비스 시작 +echo [4/5] 서비스 시작 중... +docker-compose -f docker-compose.backend.win.yml down -v 2>nul +docker-compose -f docker-compose.frontend.win.yml down -v 2>nul + +REM 백엔드 시작 +echo 백엔드 서비스 시작... +docker-compose -f docker-compose.backend.win.yml up -d +if %errorlevel% neq 0 ( + echo [ERROR] 백엔드 시작 실패! + pause + exit /b 1 +) + +REM 프론트엔드 시작 +echo 프론트엔드 서비스 시작... +docker-compose -f docker-compose.frontend.win.yml up -d +if %errorlevel% neq 0 ( + echo [ERROR] 프론트엔드 시작 실패! + pause + exit /b 1 +) +echo [OK] 서비스 시작 완료 +echo. + +REM 안정화 대기 +echo [5/5] 서비스 안정화 대기 중... (10초) +timeout /t 10 /nobreak >nul +echo. + +echo ============================================ +echo [완료] 모든 서비스가 시작되었습니다! +echo ============================================ +echo. +echo [DATABASE] PostgreSQL: http://39.117.244.52:11132 +echo [BACKEND] Node.js API: http://localhost:8080/api +echo [FRONTEND] Next.js: http://localhost:9771 +echo. +echo [서비스 상태 확인] +echo docker-compose -f docker-compose.backend.win.yml ps +echo docker-compose -f docker-compose.frontend.win.yml ps +echo. +echo [로그 확인] +echo 백엔드: docker-compose -f docker-compose.backend.win.yml logs -f +echo 프론트엔드: docker-compose -f docker-compose.frontend.win.yml logs -f +echo. +echo [서비스 중지] +echo scripts\dev\stop-all.bat +echo. + +set END_TIME=%DATE% %TIME% +echo [종료 시간] %END_TIME% +echo ============================================ + +pause diff --git a/scripts/dev/start-all-parallel.ps1 b/scripts/dev/start-all-parallel.ps1 new file mode 100644 index 00000000..732106c6 --- /dev/null +++ b/scripts/dev/start-all-parallel.ps1 @@ -0,0 +1,183 @@ +# WACE 솔루션 - 전체 서비스 시작 (병렬 최적화) - PowerShell 버전 +# 실행 방법: powershell -ExecutionPolicy Bypass -File .\scripts\dev\start-all-parallel.ps1 + +# UTF-8 출력 설정 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$OutputEncoding = [System.Text.Encoding]::UTF8 + +# 스크립트 위치에서 루트 디렉토리로 이동 +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +Set-Location (Join-Path $scriptPath "..\..") + +# 시작 시간 기록 +$startTime = Get-Date +$startTimeFormatted = $startTime.ToString("yyyy-MM-dd HH:mm:ss") + +Write-Host "" +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "WACE 솔루션 - 전체 서비스 시작 (병렬 최적화)" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "[시작 시간] $startTimeFormatted" -ForegroundColor Yellow +Write-Host "" + +# Docker Desktop 실행 확인 +Write-Host "[1/5] Docker Desktop 상태 확인 중..." -ForegroundColor White +$dockerCheck = docker --version 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] Docker Desktop이 실행되지 않았습니다!" -ForegroundColor Red + Write-Host " Docker Desktop을 먼저 실행해주세요." -ForegroundColor Red + Read-Host "계속하려면 Enter를 누르세요" + exit 1 +} +Write-Host "[OK] Docker Desktop이 실행 중입니다." -ForegroundColor Green +Write-Host "" + +# 기존 컨테이너 정리 +Write-Host "[2/5] 기존 컨테이너 정리 중..." -ForegroundColor White +docker rm -f pms-backend-win pms-frontend-win 2>$null | Out-Null +docker network rm pms-network 2>$null | Out-Null +docker network create pms-network 2>$null | Out-Null +Write-Host "[OK] 컨테이너 정리 완료" -ForegroundColor Green +Write-Host "" + +# 병렬 빌드 시작 +$parallelStart = Get-Date +Write-Host "[3/5] 이미지 빌드 중... (백엔드 + 프론트엔드 병렬)" -ForegroundColor White +Write-Host " 이 작업은 시간이 걸릴 수 있습니다..." -ForegroundColor Gray +Write-Host "" + +# 병렬 빌드 실행 +$backendBuildJob = Start-Job -ScriptBlock { + param($workDir) + Set-Location $workDir + $output = docker-compose -f docker-compose.backend.win.yml build 2>&1 + return @{ + Success = $LASTEXITCODE -eq 0 + Output = $output + } +} -ArgumentList $PWD.Path + +$frontendBuildJob = Start-Job -ScriptBlock { + param($workDir) + Set-Location $workDir + $output = docker-compose -f docker-compose.frontend.win.yml build 2>&1 + return @{ + Success = $LASTEXITCODE -eq 0 + Output = $output + } +} -ArgumentList $PWD.Path + +Write-Host " 백엔드 빌드 진행 중..." -ForegroundColor Gray +Write-Host " 프론트엔드 빌드 진행 중..." -ForegroundColor Gray +Write-Host "" + +# 빌드 완료 대기 +$null = Wait-Job -Job $backendBuildJob, $frontendBuildJob + +$backendResult = Receive-Job -Job $backendBuildJob +$frontendResult = Receive-Job -Job $frontendBuildJob + +Remove-Job -Job $backendBuildJob, $frontendBuildJob -Force + +# 빌드 결과 확인 +$buildFailed = $false + +if ($backendResult.Success) { + Write-Host "[OK] 백엔드 빌드 완료" -ForegroundColor Green +} else { + Write-Host "[ERROR] 백엔드 빌드 실패!" -ForegroundColor Red + Write-Host $backendResult.Output -ForegroundColor Red + $buildFailed = $true +} + +if ($frontendResult.Success) { + Write-Host "[OK] 프론트엔드 빌드 완료" -ForegroundColor Green +} else { + Write-Host "[ERROR] 프론트엔드 빌드 실패!" -ForegroundColor Red + Write-Host $frontendResult.Output -ForegroundColor Red + $buildFailed = $true +} + +if ($buildFailed) { + Read-Host "빌드 실패. Enter를 누르면 종료됩니다" + exit 1 +} + +$parallelEnd = Get-Date +$parallelDuration = ($parallelEnd - $parallelStart).TotalSeconds +Write-Host "[INFO] 빌드 소요 시간: $([math]::Round($parallelDuration))초" -ForegroundColor Yellow +Write-Host "" + +# 서비스 시작 +$serviceStart = Get-Date +Write-Host "[4/5] 서비스 시작 중..." -ForegroundColor White + +# 기존 컨테이너 정리 +docker-compose -f docker-compose.backend.win.yml down -v 2>$null | Out-Null +docker-compose -f docker-compose.frontend.win.yml down -v 2>$null | Out-Null + +# 백엔드 시작 +Write-Host " 백엔드 서비스 시작..." -ForegroundColor Gray +docker-compose -f docker-compose.backend.win.yml up -d 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] 백엔드 시작 실패!" -ForegroundColor Red + Read-Host "계속하려면 Enter를 누르세요" + exit 1 +} + +# 프론트엔드 시작 +Write-Host " 프론트엔드 서비스 시작..." -ForegroundColor Gray +docker-compose -f docker-compose.frontend.win.yml up -d 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] 프론트엔드 시작 실패!" -ForegroundColor Red + Read-Host "계속하려면 Enter를 누르세요" + exit 1 +} + +Write-Host "[OK] 서비스 시작 완료" -ForegroundColor Green + +$serviceEnd = Get-Date +$serviceDuration = ($serviceEnd - $serviceStart).TotalSeconds +Write-Host "[INFO] 서비스 시작 소요 시간: $([math]::Round($serviceDuration))초" -ForegroundColor Yellow +Write-Host "" + +# 안정화 대기 +Write-Host "[5/5] 서비스 안정화 대기 중... (10초)" -ForegroundColor White +Start-Sleep -Seconds 10 +Write-Host "" + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "[완료] 모든 서비스가 시작되었습니다!" -ForegroundColor Green +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "[DATABASE] PostgreSQL: http://39.117.244.52:11132" -ForegroundColor White +Write-Host "[BACKEND] Node.js API: http://localhost:8080/api" -ForegroundColor White +Write-Host "[FRONTEND] Next.js: http://localhost:9771" -ForegroundColor White +Write-Host "" +Write-Host "[서비스 상태 확인]" -ForegroundColor Yellow +Write-Host " docker-compose -f docker-compose.backend.win.yml ps" -ForegroundColor Gray +Write-Host " docker-compose -f docker-compose.frontend.win.yml ps" -ForegroundColor Gray +Write-Host "" +Write-Host "[로그 확인]" -ForegroundColor Yellow +Write-Host " 백엔드: docker-compose -f docker-compose.backend.win.yml logs -f" -ForegroundColor Gray +Write-Host " 프론트엔드: docker-compose -f docker-compose.frontend.win.yml logs -f" -ForegroundColor Gray +Write-Host "" +Write-Host "[서비스 중지]" -ForegroundColor Yellow +Write-Host " .\scripts\dev\stop-all.ps1" -ForegroundColor Gray +Write-Host "" + +# 종료 시간 계산 +$endTime = Get-Date +$endTimeFormatted = $endTime.ToString("yyyy-MM-dd HH:mm:ss") +$totalDuration = ($endTime - $startTime).TotalSeconds +$minutes = [math]::Floor($totalDuration / 60) +$seconds = [math]::Round($totalDuration % 60) + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "[종료 시간] $endTimeFormatted" -ForegroundColor Yellow +Write-Host "[총 소요 시간] ${minutes}분 ${seconds}초" -ForegroundColor Yellow +Write-Host " - 빌드: $([math]::Round($parallelDuration))초" -ForegroundColor Gray +Write-Host " - 서비스 시작: $([math]::Round($serviceDuration))초" -ForegroundColor Gray +Write-Host "============================================" -ForegroundColor Cyan + +Read-Host "계속하려면 Enter를 누르세요" diff --git a/scripts/dev/stop-all.bat b/scripts/dev/stop-all.bat new file mode 100644 index 00000000..fa378988 --- /dev/null +++ b/scripts/dev/stop-all.bat @@ -0,0 +1,30 @@ +@echo off +chcp 65001 >nul + +REM 스크립트가 있는 디렉토리에서 루트로 이동 +cd /d "%~dp0\..\.." + +echo ============================================ +echo WACE 솔루션 - 전체 서비스 중지 +echo ============================================ +echo. + +echo 🛑 백엔드 서비스 중지 중... +docker-compose -f docker-compose.backend.win.yml down -v 2>nul +echo ✅ 백엔드 서비스 중지 완료 +echo. + +echo 🛑 프론트엔드 서비스 중지 중... +docker-compose -f docker-compose.frontend.win.yml down -v 2>nul +echo ✅ 프론트엔드 서비스 중지 완료 +echo. + +echo 🧹 네트워크 정리 중... +docker network rm pms-network 2>nul +echo. + +echo ============================================ +echo 🎉 모든 서비스가 중지되었습니다! +echo ============================================ + +pause diff --git a/scripts/dev/stop-all.ps1 b/scripts/dev/stop-all.ps1 new file mode 100644 index 00000000..5f2dc2c4 --- /dev/null +++ b/scripts/dev/stop-all.ps1 @@ -0,0 +1,33 @@ +# WACE 솔루션 - 전체 서비스 중지 - PowerShell 버전 +# 실행 방법: powershell -ExecutionPolicy Bypass -File .\scripts\dev\stop-all.ps1 + +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# 스크립트 위치에서 루트 디렉토리로 이동 +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +Set-Location (Join-Path $scriptPath "..\..") + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "WACE 솔루션 - 전체 서비스 중지" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "" + +Write-Host "🛑 백엔드 서비스 중지 중..." -ForegroundColor Yellow +docker-compose -f docker-compose.backend.win.yml down -v 2>$null +Write-Host "✅ 백엔드 서비스 중지 완료" -ForegroundColor Green +Write-Host "" + +Write-Host "🛑 프론트엔드 서비스 중지 중..." -ForegroundColor Yellow +docker-compose -f docker-compose.frontend.win.yml down -v 2>$null +Write-Host "✅ 프론트엔드 서비스 중지 완료" -ForegroundColor Green +Write-Host "" + +Write-Host "🧹 네트워크 정리 중..." -ForegroundColor Yellow +docker network rm pms-network 2>$null +Write-Host "" + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "🎉 모든 서비스가 중지되었습니다!" -ForegroundColor Green +Write-Host "============================================" -ForegroundColor Cyan + +Read-Host "계속하려면 Enter를 누르세요" diff --git a/working_layout.json b/working_layout.json new file mode 100644 index 0000000000000000000000000000000000000000..d2e241a2dce3241e557f42af7e5e78245485cc0f GIT binary patch literal 32716 zcmeHQ&x;(#6)vzZF)@MMa$7pN*|If~EjhW2e;{KejwQTK0x?3Yc4t?M_ZPFfme)o> zx%yzpC6^q0H^Q=!9k4?Pff%BYYskUp{2NK$*EQc(ckj&9s~SKoW} z-m6!ys{i-jCG&w<>cG!a=1KEJ1xI1tH6t@N`)1SZnjKS^pU7VebHO}g&P)4r<+J(m zpG31ND7MXBiDe{s4g{IS^meOA(6=TU_V;uoHwz6>6(X^{~=mv9HsoMXyOHPPOdnV&mEK+a85K~ zELU!umGVmJqaO|Q`F**!)tz=F=tJWqc1KXHN#AZya1Er#=QEEnJ(}CX&!$M`wji=R zJtt_0Hc#tAb2oGE=cD$aDN4%&-8qnd&K|a-KRz_qfZ_@`XYrB;MF5p5x?BF-P58W)6kl_w6?`AuL)=1D>XU|M}t^-EC|Nt=jFKC z7bF`MT^gEuqRT_`pEvOPgTcrD_`|R7i4?y5@b`b)`E+QmKK#oUL-X3hkG_1S#2=1e zU*e-x`N62(pNry8kA3~s$!{dH^)?*GK^c>w?+0dC@F2=VEO+%dD!pi)H!qkNyR1dC z7`v-Q151K9byI$0=3@O4Vv1=oY@L7oJ3X4}cz&xfimFj|$~e-aLCn7;y1Omhydey2 zi?)yO-Sv)kT)jPuq-Y8w+gIlQD64dp&dci_F(g{?#34jL6_@xsx} z)16g8gSi}L>M@AI(I6kNC*Qg!QbTOMRneUo1Y{j;IGP=i$GfGztO%Bo`Gt&c=e^VL zgrjlXSWO0^s`W|16OIO>d`C2PQ#iq_o${>K;gYH_@g^<@iG>h>~MC?OT zh*w4iW=}MAMSQ_Ekq>Okcw+PN&iiMfSu7_$ZgSXpeuuuD#SQN;Kd*;5cgl$7x3Kl2 z>GOKzYF1mAKCjp0X0apPuf_0^-OcM=lx%(OJTN8V<&n8Tz-Ia-~ejumAze3o;2>`AlLGO-wI zr$sX>BhqKnFrNEtT2srb?`P>wpG|}AxQtPsO{;26otr+JX1VFJX>}dJyFQzCl9RKU zO&cV~nN=oyu}vXs7}d3c_$oQ9zTPTh<#Y1e*48y6Wy`PV4J{{*8?3X_3$qd*74CaB57(lZ7S|w4jJ+4T*REH5>c+sz`GmV?qk7sGv zVHdAOq1@caa7+{*Q{SPe@?B9mr0!yzSDBU&*!#xysvFT(eP?mL1Y*<#9y*r z7i)Cu;whRL2VUcEf~|4w1@FJeqoK zsD_>VBfMP?5z0+(i~SNt2BmI`pX+V$B-+9XbZ?8N>(Kn_1?()rz5(np35hR4M>w$+ ztc6srWsa%xyN}LU(CL7 z6whn5Xxfd=iBO9Maj;!Ad|f27BC<)%V-Z2M)uTa1Z6p@vopRNeCuimL4O%qp0rr-3 zsa9aST)TtbVm~;?5_MG{=dpj)Hk_O3doxjclDf(eQ%8Z!EWR20rCyR(^iyP~+Skz6 zT5h^J!haHV@S)5 zLqm?HRl5x2bkGz$@o4tVbs1S>^QQ20XW}O=CSi#i;j?Q6QZ+i98(3j`qRlkzf!DC> zQIu^+t3}f_d$U>@N6po!)cQj$C#Z5h8RO-iX=>3>>RpQ@EJq_b0j_%9Zk|(Z!?|(u zZ+j}=Rf)Q=LZ2fd4M)@D=9*{}=L)GcC!uJlKd6mQvHiSHy^TeaM^EZl!aV}7f4)=P z*j)_7ma7ewRJ(bHS2;6S&F*N3unb@ zLO2?%e_Lx~v!J8NypFLAtq(OVq}8J7nk}wZ($#a-ShaGsez9`Wk8s#x#GA;49+l&p znrt;DEq6|7Y9$3U&R37b;=NU2!#;J!)nOvDZX@?Fp7dzCbP90Sv>5pjto*+(EzXj~ zNVjKvBfrIrXj)EPD=F$(*Ji#r-S)jQ2g!_IGdqfXzj3pzaBj9mT03*m#P5oZK?81n zDVSU}dNsR65AlMxK84bEtrktYbrZznL$jEXtwqzHTSBH>n`Xt_eZpS(^oHvO)-NI- zil?IS>0X!Ri6tTRSUzliG`Y?#K{WxMy@FNqa}t@*zrcFgnc~yl84MzcYPPXm3HrDp zwd~Bh#@FERXOI8p&p12fyZ6?A^!?9<#youLK@3}AD2~MAqFT#l0I*95Ue{&`Fw2WU zP-RMwrnx$15#?Q<(xX|GzS~TR%h14Q=E;#*hV*E#_h}@x5?p`gIVGoqX5MOisvhe( z;VQ?=qQT4$=gE9J{S8O6ESY>{)m%MGJ{jrJY?Rp=TM@-<1n1ZnGFf0f8mw$!rTb{+ z)9Ly_F1mYY%FFGI0ML5K*snfJ(@n-=tf!n z6><`o4X#Lr5xxm~;z!3%XQQ8Tne*y-m-Rc%d*_o>*Jdj9b%bjTi?^t!HO*tRt-m!T T)$aKt8h literal 0 HcmV?d00001