feat(repeat-screen-modal): 자유 레이아웃 구현 및 데이터 전달 버그 수정

- contentRows 기반 자유 레이아웃 지원 (header/aggregation/table/fields 타입)
- aggregationFields, tableColumns 직접 참조하도록 렌더링 로직 수정
- groupByField 없어도 grouping.enabled면 그룹핑 모드로 처리
- buttonActions에서 selectedRowsData를 모달 이벤트로 전달
- ScreenModal에서 selectedData를 groupedData props로 컴포넌트에 전달
- types.ts에 CardContentRowConfig, AggregationDisplayConfig 인터페이스 추가
This commit is contained in:
SeongHyun Kim
2025-11-28 16:02:29 +09:00
parent c94b9da813
commit 36ab484029
11 changed files with 2035 additions and 868 deletions

View File

@@ -120,10 +120,28 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
};
};
// 🆕 선택된 데이터 상태 추가 (RepeatScreenModal 등에서 사용)
const [selectedData, setSelectedData] = useState<Record<string, any>[]>([]);
// 전역 모달 이벤트 리스너
useEffect(() => {
const handleOpenModal = (event: CustomEvent) => {
const { screenId, title, description, size, urlParams } = event.detail;
const { screenId, title, description, size, urlParams, selectedData: eventSelectedData, selectedIds } = event.detail;
console.log("📦 [ScreenModal] 모달 열기 이벤트 수신:", {
screenId,
title,
selectedData: eventSelectedData,
selectedIds,
});
// 🆕 선택된 데이터 저장
if (eventSelectedData && Array.isArray(eventSelectedData)) {
setSelectedData(eventSelectedData);
console.log("📦 [ScreenModal] 선택된 데이터 저장:", eventSelectedData.length, "건");
} else {
setSelectedData([]);
}
// 🆕 URL 파라미터가 있으면 현재 URL에 추가
if (urlParams && typeof window !== "undefined") {
@@ -164,6 +182,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
});
setScreenData(null);
setFormData({});
setSelectedData([]); // 🆕 선택된 데이터 초기화
setContinuousMode(false);
localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장
console.log("🔄 연속 모드 초기화: false");
@@ -605,6 +624,8 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
userId={userId}
userName={userName}
companyCode={user?.companyCode}
// 🆕 선택된 데이터 전달 (RepeatScreenModal 등에서 사용)
groupedData={selectedData.length > 0 ? selectedData : undefined}
/>
);
})}

View File

@@ -408,6 +408,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
value: currentValue,
onChange: (value: any) => handleFormDataChange(fieldName, value),
onFormDataChange: handleFormDataChange,
formData: formData, // 🆕 전체 formData 전달
isInteractive: true,
readonly: readonly,
required: required,
@@ -415,6 +416,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
className: "w-full h-full",
isInModal: isInModal, // 🆕 EditModal 내부 여부 전달
onSave: onSave, // 🆕 EditModal의 handleSave 콜백 전달
groupedData: groupedData, // 🆕 그룹 데이터 전달 (RepeatScreenModal용)
}}
config={widget.webTypeConfig}
onEvent={(event: string, data: any) => {

View File

@@ -863,27 +863,23 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
});
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
</div>
);
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
return (
<div className="space-y-4" key={selectedComponent.id}>
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
</div>
);
} else {
console.warn("⚠️ ConfigPanel 없음:", {
componentId,

View File

@@ -326,40 +326,36 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
});
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
const ConfigPanelWrapper = () => {
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<Suspense fallback={
<div className="flex items-center justify-center py-8">
<div className="text-sm text-muted-foreground"> ...</div>
</div>
}>
<ConfigPanelComponent
config={config}
onChange={handleConfigChange}
tables={tables} // 테이블 정보 전달
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
/>
</Suspense>
</div>
);
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
const config = currentConfig || definition.defaultProps?.componentConfig || {};
const handleConfigChange = (newConfig: any) => {
// componentConfig 전체를 업데이트
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
};
return <ConfigPanelWrapper key={selectedComponent.id} />;
return (
<div className="space-y-4" key={selectedComponent.id}>
<div className="flex items-center gap-2 border-b pb-2">
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<Suspense fallback={
<div className="flex items-center justify-center py-8">
<div className="text-sm text-muted-foreground"> ...</div>
</div>
}>
<ConfigPanelComponent
config={config}
onChange={handleConfigChange}
tables={tables} // 테이블 정보 전달
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
/>
</Suspense>
</div>
);
} else {
console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", {
componentId,

View File

@@ -1,71 +1,145 @@
# RepeatScreenModal 컴포넌트 v2
# RepeatScreenModal 컴포넌트 v3
## 개요
`RepeatScreenModal`은 선택한 데이터를 그룹핑하여 카드 형태로 표시하고, 각 카드 내에서 데이터를 편집할 수 있는 **만능 폼 컴포넌트**입니다.
`RepeatScreenModal`은 선택한 데이터를 기반으로 여러 개의 카드를 생성하고, 각 카드부 레이아웃을 자유롭게 구성할 수 있는 컴포넌트입니다.
**이 컴포넌트 하나로 대부분의 ERP 화면을 설정만으로 구현할 수 있습니다.**
## v3 주요 변경사항
## 핵심 철학
### 자유 레이아웃 시스템
기존의 "simple 모드 / withTable 모드" 구분을 없애고, **행(Row)을 추가하고 각 행마다 타입을 선택**하는 방식으로 변경되었습니다.
```
┌─────────────────────────────────────────────────────────────────┐
ERP 화면 구성의 핵심
카드
├─────────────────────────────────────────────────────────────────┤
│ 1. 어떤 테이블에서 → 어떤 컬럼을 → 어떻게 보여줄 것인가? │
│ 2. 보기만 할 것인가? vs 수정 가능하게 할 것인가? │
│ 3. 수정한다면 → 어떤 테이블의 → 어떤 컬럼에 저장할 것인가? │
│ 4. 데이터를 어떻게 그룹화해서 보여줄 것인가? │
│ │
[행 1] 타입: 헤더 → 품목코드, 품목명, 규격
├─────────────────────────────────────────────────────────────────┤
[행 2] 타입: 집계 → 총수주잔량, 현재고, 가용재고
├─────────────────────────────────────────────────────────────────┤
[행 3] 타입: 테이블 → 수주번호, 거래처, 납기일, 출하계획
├─────────────────────────────────────────────────────────────────┤
[행 4] 타입: 테이블 → 또 다른 테이블도 가능!
└─────────────────────────────────────────────────────────────────┘
```
## 카드 모드
### 행 타입
### 1. Simple 모드 (단순)
| 타입 | 설명 | 사용 시나리오 |
|------|------|---------------|
| **헤더 (header)** | 필드들을 가로/세로로 나열 | 품목정보, 거래처정보 표시 |
| **필드 (fields)** | 헤더와 동일, 편집 가능 | 폼 입력 영역 |
| **집계 (aggregation)** | 그룹 내 데이터 집계값 표시 | 총수량, 합계금액 등 |
| **테이블 (table)** | 그룹 내 각 행을 테이블로 표시 | 수주목록, 품목목록 등 |
- **1행 = 1카드**: 선택한 각 행이 독립적인 카드로 표시
- 자유로운 레이아웃 구성 (행/컬럼 기반)
- 적합한 상황: 단순 데이터 편집, 개별 레코드 수정
### 2. WithTable 모드 (테이블 포함)
- **N행 = 1카드**: 그룹핑된 여러 행이 하나의 카드로 표시
- 카드 = 헤더 영역 + 테이블 영역
- 헤더: 그룹 대표값, 집계값 표시
- 테이블: 그룹 내 각 행을 테이블로 표시
- 적합한 상황: 출하계획, 구매발주, 생산계획 등 일괄 등록
## 주요 기능
| 기능 | 설명 |
|------|------|
| 그룹핑 | 특정 필드 기준으로 여러 행을 하나의 카드로 묶음 |
| 집계 | 그룹 내 데이터의 합계/개수/평균/최소/최대 자동 계산 |
| 카드 내 테이블 | 그룹 내 각 행을 테이블 형태로 표시 |
| 테이블 내 편집 | 테이블의 특정 컬럼을 편집 가능하게 설정 |
| 다중 테이블 저장 | 하나의 카드에서 여러 테이블 동시 저장 |
| 컬럼별 소스 설정 | 직접 조회/조인 조회/수동 입력 선택 |
| 컬럼별 타겟 설정 | 저장할 테이블과 컬럼 지정 |
## 사용 시나리오
### 시나리오 1: 출하계획 동시 등록
### 자유로운 조합
```
그룹핑: part_code (품목코드)
헤더: 품목정보 + 총수주잔량 + 현재고
테이블: 수주별 출하계획 입력
예시 1: 헤더 + 집계 + 테이블 (출하계획)
├── [행 1] 헤더: 품목코드, 품목명
├── [행 2] 집계: 수주잔량, 현재고
└── [행 3] 테이블: 수주별 출하계획
예시 2: 집계만
└── [행 1] 집계: 총매출, 총비용, 순이익
예시 3: 테이블만
└── [행 1] 테이블: 품목 목록
예시 4: 테이블 2개
├── [행 1] 테이블: 입고 내역
└── [행 2] 테이블: 출고 내역
예시 5: 헤더 + 헤더 + 필드
├── [행 1] 헤더: 기본 정보 (읽기전용)
├── [행 2] 헤더: 상세 정보 (읽기전용)
└── [행 3] 필드: 입력 필드 (편집가능)
```
**설정 예시:**
## 설정 방법
### 1. 기본 설정 탭
- **카드 제목 표시**: 카드 상단에 제목을 표시할지 여부
- **카드 제목 템플릿**: `{field_name}` 형식으로 동적 제목 생성
- **카드 간격**: 카드 사이의 간격 (8px ~ 32px)
- **테두리**: 카드 테두리 표시 여부
- **저장 모드**: 전체 저장 / 개별 저장
### 2. 데이터 소스 탭
- **소스 테이블**: 데이터를 조회할 테이블
- **필터 필드**: formData에서 필터링할 필드 (예: selectedIds)
### 3. 그룹 탭
- **그룹핑 활성화**: 여러 행을 하나의 카드로 묶을지 여부
- **그룹 기준 필드**: 그룹핑할 필드 (예: part_code)
- **집계 설정**:
- 원본 필드: 합계할 필드 (예: balance_qty)
- 집계 타입: sum, count, avg, min, max
- 결과 필드명: 집계 결과를 저장할 필드명
- 라벨: 표시될 라벨
### 4. 레이아웃 탭
#### 행 추가
4가지 타입의 행을 추가할 수 있습니다:
- **헤더**: 필드 정보 표시 (읽기전용)
- **집계**: 그룹 집계값 표시
- **테이블**: 그룹 내 행들을 테이블로 표시
- **필드**: 입력 필드 (편집가능)
#### 헤더/필드 행 설정
- **방향**: 가로 / 세로
- **배경색**: 없음, 파랑, 초록, 보라, 주황
- **컬럼**: 필드명, 라벨, 타입, 너비, 편집 가능, 필수
- **소스 설정**: 직접 / 조인 / 수동
- **저장 설정**: 저장할 테이블과 컬럼
#### 집계 행 설정
- **레이아웃**: 가로 나열 / 그리드
- **그리드 컬럼 수**: 2, 3, 4개
- **집계 필드**: 그룹 탭에서 정의한 집계 결과 선택
- **스타일**: 배경색, 폰트 크기
#### 테이블 행 설정
- **테이블 제목**: 선택사항
- **헤더 표시**: 테이블 헤더 표시 여부
- **테이블 컬럼**: 필드명, 라벨, 타입, 너비, 편집 가능
- **저장 설정**: 편집 가능한 컬럼의 저장 위치
## 데이터 흐름
```
1. formData에서 selectedIds 가져오기
2. 소스 테이블에서 해당 ID들의 데이터 조회
3. 그룹핑 활성화 시 groupByField 기준으로 그룹화
4. 각 그룹에 대해 집계값 계산
5. 카드 렌더링 (contentRows 기반)
6. 사용자 편집
7. 저장 시 targetConfig에 따라 테이블별로 데이터 분류 후 저장
```
## 사용 예시
### 출하계획 등록
```typescript
{
cardMode: "withTable",
showCardTitle: true,
cardTitle: "{part_code} - {part_name}",
dataSource: {
sourceTable: "sales_order_mng",
filterField: "selectedIds"
@@ -78,159 +152,55 @@
{ sourceField: "id", type: "count", resultField: "order_count", label: "수주건수" }
]
},
tableLayout: {
headerRows: [
{
columns: [
{ field: "part_code", label: "품목코드", type: "text", editable: false },
{ field: "part_name", label: "품목", type: "text", editable: false },
{ field: "total_balance", label: "총수주잔량", type: "aggregation", aggregationField: "total_balance" }
]
}
],
tableColumns: [
{ field: "order_no", label: "수주번호", type: "text", editable: false },
{ field: "partner_id", label: "거래처", type: "text", editable: false },
{ field: "due_date", label: "납기일", type: "date", editable: false },
{ field: "balance_qty", label: "미출하", type: "number", editable: false },
{
field: "plan_qty",
label: "출하계획",
type: "number",
editable: true,
targetConfig: { targetTable: "shipment_plan", targetColumn: "plan_qty", saveEnabled: true }
}
]
}
contentRows: [
{
id: "row-1",
type: "header",
columns: [
{ id: "c1", field: "part_code", label: "품목코드", type: "text", editable: false },
{ id: "c2", field: "part_name", label: "품목명", type: "text", editable: false }
],
layout: "horizontal"
},
{
id: "row-2",
type: "aggregation",
aggregationLayout: "horizontal",
aggregationFields: [
{ aggregationResultField: "total_balance", label: "총수주잔량", backgroundColor: "blue" },
{ aggregationResultField: "order_count", label: "수주건수", backgroundColor: "green" }
]
},
{
id: "row-3",
type: "table",
tableTitle: "수주 목록",
showTableHeader: true,
tableColumns: [
{ id: "tc1", field: "order_no", label: "수주번호", type: "text", editable: false },
{ id: "tc2", field: "partner_name", label: "거래처", type: "text", editable: false },
{ id: "tc3", field: "balance_qty", label: "미출하", type: "number", editable: false },
{
id: "tc4",
field: "plan_qty",
label: "출하계획",
type: "number",
editable: true,
targetConfig: { targetTable: "shipment_plan", targetColumn: "plan_qty", saveEnabled: true }
}
]
}
]
}
```
### 시나리오 2: 구매발주 일괄 등록
## 레거시 호환
```
그룹핑: supplier_id (공급업체)
헤더: 공급업체정보 + 총발주금액
테이블: 품목별 발주수량 입력
```
v2에서 사용하던 `cardMode`, `cardLayout`, `tableLayout` 설정도 계속 지원됩니다.
새로운 프로젝트에서는 `contentRows`를 사용하는 것을 권장합니다.
### 시나리오 3: 생산계획 일괄 등록
## 주의사항
```
그룹핑: product_code (제품코드)
헤더: 제품정보 + 현재고 + 필요수량
테이블: 작업지시별 생산수량 입력
```
### 시나리오 4: 입고검사 일괄 처리
```
그룹핑: po_no (발주번호)
헤더: 발주정보 + 공급업체
테이블: 품목별 검사결과 입력
```
## ConfigPanel 사용법
### 1. 기본 설정 탭
- **카드 제목**: `{field}` 형식으로 동적 제목 설정
- **카드 간격**: 카드 사이 간격 (8px ~ 32px)
- **테두리**: 카드 테두리 표시 여부
- **저장 모드**: 전체 저장 / 개별 저장
- **카드 모드**: 단순 / 테이블
### 2. 데이터 소스 탭
- **소스 테이블**: 데이터를 조회할 테이블
- **필터 필드**: formData에서 가져올 필터 필드명 (예: `selectedIds`)
### 3. 그룹핑 탭 (테이블 모드에서 활성화)
- **그룹핑 활성화**: ON/OFF
- **그룹 기준 필드**: 그룹핑할 필드 선택
- **집계 설정**: 합계/개수/평균 등 집계 추가
### 4. 레이아웃 탭
**Simple 모드:**
- 행 추가/삭제
- 각 행에 컬럼 추가/삭제
- 컬럼별 필드명, 라벨, 타입, 너비, 편집 가능 여부 설정
**WithTable 모드:**
- 헤더 영역: 그룹 대표값, 집계값 표시용 행/컬럼 설정
- 테이블 영역: 그룹 내 각 행을 표시할 테이블 컬럼 설정
## 컬럼 설정 상세
### 소스 설정 (데이터 조회)
| 타입 | 설명 |
|------|------|
| direct | 소스 테이블에서 직접 조회 |
| join | 다른 테이블과 조인하여 조회 |
| manual | 사용자 직접 입력 |
### 타겟 설정 (데이터 저장)
- **저장 테이블**: 데이터를 저장할 테이블
- **저장 컬럼**: 데이터를 저장할 컬럼
- **저장 활성화**: 저장 여부
## 타입 정의
```typescript
interface RepeatScreenModalProps {
// 기본 설정
cardTitle?: string;
cardSpacing?: string;
showCardBorder?: boolean;
saveMode?: "all" | "individual";
cardMode?: "simple" | "withTable";
// 데이터 소스
dataSource?: DataSourceConfig;
// 그룹핑 설정
grouping?: GroupingConfig;
// 레이아웃
cardLayout?: CardRowConfig[]; // simple 모드
tableLayout?: TableLayoutConfig; // withTable 모드
}
interface GroupingConfig {
enabled: boolean;
groupByField: string;
aggregations?: AggregationConfig[];
}
interface AggregationConfig {
sourceField: string;
type: "sum" | "count" | "avg" | "min" | "max";
resultField: string;
label: string;
}
interface TableLayoutConfig {
headerRows: CardRowConfig[];
tableColumns: TableColumnConfig[];
}
```
## 파일 구조
```
repeat-screen-modal/
├── index.ts # 컴포넌트 정의 및 export
├── types.ts # 타입 정의
├── RepeatScreenModalComponent.tsx # 메인 컴포넌트
├── RepeatScreenModalConfigPanel.tsx # 설정 패널
├── RepeatScreenModalRenderer.tsx # 자동 등록
└── README.md # 문서
```
## 버전 히스토리
- **v2.0.0**: 그룹핑, 집계, 테이블 모드 추가
- **v1.0.0**: 초기 버전 (Simple 모드)
1. **집계는 그룹핑 필수**: 집계 행은 그룹핑이 활성화되어 있어야 의미가 있습니다.
2. **테이블은 그룹핑 필수**: 테이블 행도 그룹핑이 활성화되어 있어야 그룹 내 행들을 표시할 수 있습니다.
3. **단순 모드**: 그룹핑 없이 사용하면 1행 = 1카드로 동작합니다. 이 경우 헤더/필드 타입만 사용 가능합니다.

View File

@@ -18,18 +18,20 @@ import type {
TableColumnConfig,
GroupedCardData,
CardRowData,
CardContentRowConfig,
AggregationDisplayConfig,
} from "./types";
/**
* RepeatScreenModal 컴포넌트 정의 v2
* RepeatScreenModal 컴포넌트 정의 v3
* 반복 화면 모달 - 선택한 행 개수만큼 카드를 생성하며, 각 카드는 커스터마이징 가능한 레이아웃
*
* 주요 기능:
* - 🆕 카드 모드: 단순(simple) / 테이블(withTable) 모드 선택
* - 🆕 그룹핑: 특정 필드 기준으로 여러 행을 하나의 카드로 묶기
* - 🆕 집계: 그룹 내 데이터의 합계/평균/개수 등 자동 계산
* - 🆕 카드 내 테이블: 그룹 내 각 행을 테이블 형태로 표시
* - 유연한 레이아웃: 행/컬럼 기반 자유로운 구조
* - 🆕 v3: 자유 레이아웃 - 행(Row)을 추가하고 각 행마다 타입(헤더/집계/테이블/필드) 선택
* - 그룹핑: 특정 필드 기준으로 여러 행을 하나의 카드로 묶기
* - 집계: 그룹 내 데이터의 합계/평균/개수 등 자동 계산
* - 카드 내 테이블: 그룹 내 각 행을 테이블 형태로 표시
* - 유연한 레이아웃: 행 타입 자유 선택, 순서 자유 배치
* - 컬럼별 소스 설정: 직접 조회/조인 조회/수동 입력
* - 컬럼별 타겟 설정: 어느 테이블의 어느 컬럼에 저장할지 설정
* - 다중 테이블 저장: 하나의 카드에서 여러 테이블 동시 저장
@@ -45,67 +47,37 @@ export const RepeatScreenModalDefinition = createComponentDefinition({
name: "반복 화면 모달",
nameEng: "Repeat Screen Modal",
description:
"선택한 행을 그룹핑하여 카드로 표시하고, 각 카드는 헤더+테이블 구조로 편집 가능한 폼 (출하계획, 구매발주 등)",
"선택한 행을 그룹핑하여 카드로 표시하고, 각 카드는 헤더/집계/테이블을 자유롭게 구성 가능한 폼 (출하계획, 구매발주 등)",
category: ComponentCategory.DATA,
webType: "form",
component: RepeatScreenModalComponent,
defaultConfig: {
// 기본 설정
cardTitle: "{part_code} - {part_name}",
showCardTitle: true,
cardTitle: "카드 {index}",
cardSpacing: "24px",
showCardBorder: true,
saveMode: "all",
// 카드 모드 (simple: 1행=1카드, withTable: 그룹핑+테이블)
cardMode: "simple",
// 데이터 소스
dataSource: {
sourceTable: "",
filterField: "selectedIds",
},
// 그룹핑 설정 (withTable 모드에서 사용)
// 그룹핑 설정
grouping: {
enabled: false,
groupByField: "",
aggregations: [],
},
// Simple 모드 레이아웃
cardLayout: [
{
id: "row-1",
columns: [
{
id: "col-1",
field: "name",
label: "이름",
type: "text",
width: "50%",
editable: true,
required: true,
},
{
id: "col-2",
field: "status",
label: "상태",
type: "select",
width: "50%",
editable: true,
required: false,
selectOptions: [
{ value: "active", label: "활성" },
{ value: "inactive", label: "비활성" },
],
},
],
gap: "16px",
layout: "horizontal",
},
],
// 🆕 v3: 자유 레이아웃 (행 추가 후 타입 선택)
contentRows: [],
// WithTable 모드 레이아웃
// (레거시 호환)
cardMode: "simple",
cardLayout: [],
tableLayout: {
headerRows: [],
tableColumns: [],
@@ -114,8 +86,8 @@ export const RepeatScreenModalDefinition = createComponentDefinition({
defaultSize: { width: 1000, height: 800 },
configPanel: RepeatScreenModalConfigPanel,
icon: "LayoutGrid",
tags: ["모달", "폼", "반복", "카드", "그룹핑", "집계", "테이블", "편집", "데이터", "출하계획", "일괄등록"],
version: "2.0.0",
tags: ["모달", "폼", "반복", "카드", "그룹핑", "집계", "테이블", "편집", "데이터", "출하계획", "일괄등록", "자유레이아웃"],
version: "3.0.0",
author: "개발팀",
});
@@ -134,6 +106,8 @@ export type {
TableColumnConfig,
GroupedCardData,
CardRowData,
CardContentRowConfig,
AggregationDisplayConfig,
};
// 컴포넌트 재 export

View File

@@ -4,10 +4,11 @@ import { ComponentRendererProps } from "@/types/component";
* RepeatScreenModal Props
* 선택한 행 개수만큼 카드를 생성하며, 각 카드는 커스터마이징 가능한 레이아웃을 가짐
*
* 🆕 v2: 그룹핑, 집계, 카드 내 테이블 기능 추가
* 🆕 v3: 행(Row) 기반 자유 레이아웃 - 각 행마다 타입(헤더/집계/테이블) 선택 가능
*/
export interface RepeatScreenModalProps {
// === 기본 설정 ===
showCardTitle?: boolean; // 카드 제목 표시 여부
cardTitle?: string; // 카드 제목 템플릿 (예: "{order_no} - {item_code}")
cardSpacing?: string; // 카드 간 간격 (기본: 24px)
showCardBorder?: boolean; // 카드 테두리 표시 여부
@@ -16,17 +17,16 @@ export interface RepeatScreenModalProps {
// === 데이터 소스 ===
dataSource?: DataSourceConfig; // 데이터 소스 설정
// === 🆕 그룹핑 설정 ===
// === 그룹핑 설정 ===
grouping?: GroupingConfig; // 그룹핑 설정
// === 🆕 카드 모드 ===
cardMode?: "simple" | "withTable"; // 단순 필드 vs 테이블 포함
// === 🆕 v3: 자유 레이아웃 ===
contentRows?: CardContentRowConfig[]; // 카드 내부 행들 (각 행마다 타입 선택)
// === 레이아웃 (simple 모드) ===
cardLayout?: CardRowConfig[]; // 카드 내부 레이아웃 (행/컬럼 구조)
// === 🆕 레이아웃 (withTable 모드) ===
tableLayout?: TableLayoutConfig; // 테이블 포함 레이아웃
// === (레거시 호환) ===
cardMode?: "simple" | "withTable"; // @deprecated - contentRows 사용 권장
cardLayout?: CardRowConfig[]; // @deprecated - contentRows 사용 권장
tableLayout?: TableLayoutConfig; // @deprecated - contentRows 사용 권장
// === 값 ===
value?: any[];
@@ -43,7 +43,7 @@ export interface DataSourceConfig {
}
/**
* 🆕 그룹핑 설정
* 그룹핑 설정
* 특정 필드 기준으로 여러 행을 하나의 카드로 묶음
*/
export interface GroupingConfig {
@@ -55,7 +55,46 @@ export interface GroupingConfig {
}
/**
* 🆕 집계 설정
* 🆕 v3: 카드 내부 행 설정
* 각 행마다 타입(헤더/집계/테이블)을 선택할 수 있음
*/
export interface CardContentRowConfig {
id: string; // 행 고유 ID
type: "header" | "aggregation" | "table" | "fields"; // 행 타입
// === header/fields 타입일 때 ===
columns?: CardColumnConfig[]; // 컬럼 설정
layout?: "horizontal" | "vertical"; // 레이아웃 방향
gap?: string; // 컬럼 간 간격
backgroundColor?: string; // 배경색
padding?: string; // 패딩
// === aggregation 타입일 때 ===
aggregationFields?: AggregationDisplayConfig[]; // 표시할 집계 필드들
aggregationLayout?: "horizontal" | "grid"; // 집계 레이아웃 (가로 나열 / 그리드)
aggregationColumns?: number; // grid일 때 컬럼 수 (기본: 4)
// === table 타입일 때 ===
tableColumns?: TableColumnConfig[]; // 테이블 컬럼 설정
tableTitle?: string; // 테이블 제목
showTableHeader?: boolean; // 테이블 헤더 표시 여부
tableMaxHeight?: string; // 테이블 최대 높이
}
/**
* 🆕 v3: 집계 표시 설정
*/
export interface AggregationDisplayConfig {
aggregationResultField: string; // 그룹핑 설정의 resultField 참조
label: string; // 표시 라벨
icon?: string; // 아이콘 (lucide 아이콘명)
backgroundColor?: string; // 배경색
textColor?: string; // 텍스트 색상
fontSize?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl"; // 폰트 크기
}
/**
* 집계 설정
*/
export interface AggregationConfig {
sourceField: string; // 원본 필드 (예: "balance_qty")
@@ -65,24 +104,19 @@ export interface AggregationConfig {
}
/**
* 🆕 테이블 포함 레이아웃 설정
* 카드 = 헤더 영역 + 테이블 영역
* @deprecated v3에서는 contentRows 사용 권장
* 테이블 포함 레이아웃 설정
*/
export interface TableLayoutConfig {
// 헤더 영역: 그룹 대표값, 집계값 표시
headerRows: CardRowConfig[];
// 테이블 영역: 그룹 내 각 행을 테이블로 표시
tableColumns: TableColumnConfig[];
// 테이블 설정
tableTitle?: string; // 테이블 제목
showTableHeader?: boolean; // 테이블 헤더 표시 여부 (기본: true)
tableMaxHeight?: string; // 테이블 최대 높이 (스크롤용)
tableTitle?: string;
showTableHeader?: boolean;
tableMaxHeight?: string;
}
/**
* 🆕 테이블 컬럼 설정
* 테이블 컬럼 설정
*/
export interface TableColumnConfig {
id: string; // 컬럼 고유 ID

View File

@@ -1,132 +1 @@
/**
* SimpleRepeaterTable 컴포넌트 타입 정의
* 데이터 검색/추가 없이 주어진 데이터를 표시하고 편집하는 경량 테이블
*/
export interface SimpleRepeaterTableProps {
// 데이터
value?: any[]; // 현재 표시할 데이터
onChange?: (newData: any[]) => void; // 데이터 변경 콜백
// 테이블 설정
columns: SimpleRepeaterColumnConfig[]; // 테이블 컬럼 설정
// 🆕 초기 데이터 로드 설정
initialDataConfig?: InitialDataConfig;
// 계산 규칙
calculationRules?: CalculationRule[]; // 자동 계산 규칙 (수량 * 단가 = 금액)
// 옵션
readOnly?: boolean; // 읽기 전용 모드 (편집 불가)
showRowNumber?: boolean; // 행 번호 표시 (기본: true)
allowDelete?: boolean; // 삭제 버튼 표시 (기본: true)
maxHeight?: string; // 테이블 최대 높이 (기본: "240px")
// 스타일
className?: string;
}
export interface SimpleRepeaterColumnConfig {
field: string; // 필드명 (화면에 표시용 임시 키)
label: string; // 컬럼 헤더 라벨
type?: "text" | "number" | "date" | "select"; // 입력 타입
editable?: boolean; // 편집 가능 여부
calculated?: boolean; // 계산 필드 여부 (자동 계산되는 필드)
width?: string; // 컬럼 너비
required?: boolean; // 필수 입력 여부
defaultValue?: any; // 기본값
selectOptions?: { value: string; label: string }[]; // select일 때 옵션
// 🆕 데이터 조회 설정 (어디서 가져올지)
sourceConfig?: ColumnSourceConfig;
// 🆕 데이터 저장 설정 (어디에 저장할지)
targetConfig?: ColumnTargetConfig;
}
/**
* 🆕 데이터 조회 설정
* 어떤 테이블에서 어떤 컬럼을 어떤 조건으로 조회할지 정의
*/
export interface ColumnSourceConfig {
/** 조회 타입 */
type: "direct" | "join" | "manual";
// type: "direct" - 직접 조회 (단일 테이블에서 바로 가져오기)
sourceTable?: string; // 조회할 테이블 (예: "sales_order_mng")
sourceColumn?: string; // 조회할 컬럼 (예: "item_name")
// type: "join" - 조인 조회 (다른 테이블과 조인하여 가져오기)
joinTable?: string; // 조인할 테이블 (예: "customer_item_mapping")
joinColumn?: string; // 조인 테이블에서 가져올 컬럼 (예: "basic_price")
joinKey?: string; // 🆕 조인 키 (현재 테이블의 컬럼, 예: "sales_order_id")
joinRefKey?: string; // 🆕 참조 키 (조인 테이블의 컬럼, 예: "id")
joinConditions?: SourceJoinCondition[]; // 조인 조건 (어떤 키로 조인할지)
// type: "manual" - 사용자 직접 입력 (조회 안 함)
}
/**
* 🆕 데이터 저장 설정
* 어떤 테이블의 어떤 컬럼에 저장할지 정의
*/
export interface ColumnTargetConfig {
targetTable?: string; // 저장할 테이블 (예: "shipment_plan")
targetColumn?: string; // 저장할 컬럼 (예: "plan_qty")
saveEnabled?: boolean; // 저장 활성화 여부 (false면 읽기 전용)
}
/**
* 🆕 소스 조인 조건
* 데이터를 조회할 때 어떤 키로 조인할지 정의
*/
export interface SourceJoinCondition {
/** 기준 테이블 */
baseTable: string; // 기준이 되는 테이블 (예: "sales_order_mng")
/** 기준 컬럼 */
baseColumn: string; // 기준 테이블의 컬럼 (예: "item_code")
/** 조인 테이블의 컬럼 */
joinColumn: string; // 조인 테이블에서 매칭할 컬럼 (예: "item_code")
/** 비교 연산자 */
operator?: "=" | "!=" | ">" | "<" | ">=" | "<=";
}
/**
* 🆕 초기 데이터 로드 설정
* 컴포넌트가 로드될 때 어떤 데이터를 가져올지
*/
export interface InitialDataConfig {
/** 로드할 테이블 */
sourceTable: string; // 예: "sales_order_mng"
/** 필터 조건 */
filterConditions?: DataFilterCondition[];
/** 선택할 컬럼 목록 */
selectColumns?: string[];
}
/**
* 데이터 필터 조건
*/
export interface DataFilterCondition {
/** 필드명 */
field: string;
/** 연산자 */
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN";
/** 값 (또는 다른 필드 참조) */
value: any;
/** 값을 다른 필드에서 가져올지 */
valueFromField?: string; // 예: "order_no" (formData에서 가져오기)
}
/**
* 계산 규칙 (자동 계산)
*/
export interface CalculationRule {
result: string; // 결과를 저장할 필드 (예: "total_amount")
formula: string; // 계산 공식 (예: "quantity * unit_price")
dependencies: string[]; // 의존하는 필드들 (예: ["quantity", "unit_price"])
}

View File

@@ -942,6 +942,7 @@ export class ButtonActionExecutor {
title: config.modalTitle,
size: config.modalSize,
targetScreenId: config.targetScreenId,
selectedRowsData: context.selectedRowsData,
});
if (config.targetScreenId) {
@@ -958,6 +959,10 @@ export class ButtonActionExecutor {
}
}
// 🆕 선택된 행 데이터 수집
const selectedData = context.selectedRowsData || [];
console.log("📦 [handleModal] 선택된 데이터:", selectedData);
// 전역 모달 상태 업데이트를 위한 이벤트 발생
const modalEvent = new CustomEvent("openScreenModal", {
detail: {
@@ -965,6 +970,9 @@ export class ButtonActionExecutor {
title: config.modalTitle || "화면",
description: description,
size: config.modalSize || "md",
// 🆕 선택된 행 데이터 전달
selectedData: selectedData,
selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
},
});