From bb442f5478f5ffaf51a8b8df7343b34ace6ad038 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 12 Mar 2026 08:18:34 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260311225813-8hmk round-1 --- .../V2RepeatContainerConfigPanel.tsx | 688 +++++++++--------- .../config-panels/V2RepeaterConfigPanel.tsx | 541 ++++++++------ .../V2TimelineSchedulerConfigPanel.tsx | 161 ++-- 3 files changed, 782 insertions(+), 608 deletions(-) diff --git a/frontend/components/v2/config-panels/V2RepeatContainerConfigPanel.tsx b/frontend/components/v2/config-panels/V2RepeatContainerConfigPanel.tsx index d5d75f5e..5d391d2a 100644 --- a/frontend/components/v2/config-panels/V2RepeatContainerConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2RepeatContainerConfigPanel.tsx @@ -9,6 +9,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; @@ -57,6 +58,7 @@ export const V2RepeatContainerConfigPanel: React.FC {/* ─── 4단계: 아이템 제목/설명 ─── */} -
-
-

아이템 제목/설명

- onChange({ showItemTitle: checked })} - /> -
-

- 각 아이템에 제목과 설명을 표시할 수 있어요 -

-
- - {config.showItemTitle && ( -
- {/* 제목 컬럼 Combobox */} -
- 제목 컬럼 - - - - - - - - - 컬럼을 찾을 수 없습니다 - - { - onChange({ titleColumn: "" }); - setTitleColumnOpen(false); - }} - className="text-xs" - > - - 선택 안함 - - {availableColumns.map((col) => ( - { - onChange({ titleColumn: col.columnName }); - setTitleColumnOpen(false); - }} - className="text-xs" - > - -
- {col.displayName || col.columnName} - {col.displayName && col.displayName !== col.columnName && ( - {col.columnName} - )} -
-
- ))} -
-
-
-
-
-
- - {/* 설명 컬럼 Combobox */} -
- 설명 컬럼 (선택) - - - - - - - - - 컬럼을 찾을 수 없습니다 - - { - onChange({ descriptionColumn: "" }); - setDescriptionColumnOpen(false); - }} - className="text-xs" - > - - 선택 안함 - - {availableColumns.map((col) => ( - { - onChange({ descriptionColumn: col.columnName }); - setDescriptionColumnOpen(false); - }} - className="text-xs" - > - -
- {col.displayName || col.columnName} - {col.displayName && col.displayName !== col.columnName && ( - {col.columnName} - )} -
-
- ))} -
-
-
-
-
-
- - {/* 제목 템플릿 (titleColumn 미사용 시 대체) */} -
- 제목 템플릿 (레거시) - onChange({ itemTitleTemplate: e.target.value })} - placeholder="{field_name} - {field_code}" - className="h-7 text-xs" - /> -

- 제목 컬럼 미선택 시 사용. 중괄호로 필드 참조 -

-
- - {/* 제목 스타일 */} -
- 제목 스타일 -
-
- - -
-
- - onChange({ titleColor: e.target.value })} - className="h-7" - /> -
-
- - -
+ + + + + +
+
+
+

제목/설명 표시

+

각 아이템에 제목과 설명을 표시할 수 있어요

+
+ onChange({ showItemTitle: checked })} + />
-
- {config.descriptionColumn && ( -
- 설명 스타일 -
+ {config.showItemTitle && ( + <> + {/* 제목 컬럼 Combobox */}
- - + 제목 컬럼 + + + + + + + + + 컬럼을 찾을 수 없습니다 + + { + onChange({ titleColumn: "" }); + setTitleColumnOpen(false); + }} + className="text-xs" + > + + 선택 안함 + + {availableColumns.map((col) => ( + { + onChange({ titleColumn: col.columnName }); + setTitleColumnOpen(false); + }} + className="text-xs" + > + +
+ {col.displayName || col.columnName} + {col.displayName && col.displayName !== col.columnName && ( + {col.columnName} + )} +
+
+ ))} +
+
+
+
+
+ + {/* 설명 컬럼 Combobox */}
- + 설명 컬럼 (선택) + + + + + + + + + 컬럼을 찾을 수 없습니다 + + { + onChange({ descriptionColumn: "" }); + setDescriptionColumnOpen(false); + }} + className="text-xs" + > + + 선택 안함 + + {availableColumns.map((col) => ( + { + onChange({ descriptionColumn: col.columnName }); + setDescriptionColumnOpen(false); + }} + className="text-xs" + > + +
+ {col.displayName || col.columnName} + {col.displayName && col.displayName !== col.columnName && ( + {col.columnName} + )} +
+
+ ))} +
+
+
+
+
+
+ + {/* 제목 템플릿 (titleColumn 미사용 시 대체) */} +
+ 제목 템플릿 (레거시) onChange({ descriptionColor: e.target.value })} - className="h-7" + value={config.itemTitleTemplate || ""} + onChange={(e) => onChange({ itemTitleTemplate: e.target.value })} + placeholder="{field_name} - {field_code}" + className="h-7 text-xs" /> +

+ 제목 컬럼 미선택 시 사용. 중괄호로 필드 참조 +

-
-
- )} -
- )} + + {/* 제목 스타일 */} +
+ 제목 스타일 +
+
+ + +
+
+ + onChange({ titleColor: e.target.value })} + className="h-7" + /> +
+
+ + +
+
+
+ + {config.descriptionColumn && ( +
+ 설명 스타일 +
+
+ + +
+
+ + onChange({ descriptionColor: e.target.value })} + className="h-7" + /> +
+
+
+ )} + + )} +
+ + {/* ─── 5단계: 카드 스타일 (Collapsible) ─── */} @@ -603,6 +624,7 @@ export const V2RepeatContainerConfigPanel: React.FC 카드 스타일 + 6개
-
+
- 배경색 + 배경색
- 둥글기 + 둥글기 onChange({ borderRadius: e.target.value })} @@ -636,7 +658,7 @@ export const V2RepeatContainerConfigPanel: React.FC
- 내부 패딩 + 내부 패딩 onChange({ padding: e.target.value })} @@ -644,7 +666,7 @@ export const V2RepeatContainerConfigPanel: React.FC
- 아이템 높이 + 아이템 높이 onChange({ itemHeight: e.target.value })} @@ -688,6 +710,7 @@ export const V2RepeatContainerConfigPanel: React.FC 상호작용 & 페이징 + 7개
-
+

클릭 가능

@@ -868,6 +891,7 @@ function SlotChildrenSection({ }: SlotChildrenSectionProps) { const [columnComboboxOpen, setColumnComboboxOpen] = useState(false); const [expandedIds, setExpandedIds] = useState>(new Set()); + const [slotFieldsOpen, setSlotFieldsOpen] = useState(true); const children = config.children || []; @@ -930,17 +954,31 @@ function SlotChildrenSection({ }; return ( - <> -
-

반복 표시 필드

-

- 데이터의 어떤 컬럼을 각 아이템에 표시할지 선택해요 -

-
+ + + + + +
+

+ 데이터의 어떤 컬럼을 각 아이템에 표시할지 선택해요 +

- {children.length > 0 ? ( -
- {children.map((child, index) => { + {children.length > 0 ? ( +
+ {children.map((child, index) => { const isExpanded = expandedIds.has(child.id); return (
{index + 1}
-
-
+
+
{child.label || child.fieldName}
-
+
필드: {child.fieldName}
@@ -1090,81 +1128,83 @@ function SlotChildrenSection({ ); })}
- ) : ( -
- -
표시할 필드가 없어요
-
- 아래에서 컬럼을 선택하세요 -
-
- )} + ) : ( +
+ +
표시할 필드가 없어요
+
+ 아래에서 컬럼을 선택하세요 +
+
+ )} - {/* 컬럼 추가 Combobox */} - - - - - - - - - 컬럼을 찾을 수 없습니다 - - {availableColumns.map((col) => { - const isAdded = children.some((c) => c.fieldName === col.columnName); - return ( - { - if (!isAdded) { - addComponent(col.columnName, col.displayName || col.columnName); - } - }} - disabled={isAdded} - className={cn( - "text-xs cursor-pointer", - isAdded && "opacity-50 cursor-not-allowed" - )} - > - -
-
{col.displayName || col.columnName}
-
- {col.columnName} -
-
- {isAdded && ( - - )} -
- ); - })} -
-
-
-
-
- + {/* 컬럼 추가 Combobox */} + + + + + + + + + 컬럼을 찾을 수 없습니다 + + {availableColumns.map((col) => { + const isAdded = children.some((c) => c.fieldName === col.columnName); + return ( + { + if (!isAdded) { + addComponent(col.columnName, col.displayName || col.columnName); + } + }} + disabled={isAdded} + className={cn( + "text-xs cursor-pointer", + isAdded && "opacity-50 cursor-not-allowed" + )} + > + +
+
{col.displayName || col.columnName}
+
+ {col.columnName} +
+
+ {isAdded && ( + + )} +
+ ); + })} +
+
+
+
+
+
+ + ); } diff --git a/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx b/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx index e3ef080f..25e7414b 100644 --- a/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx @@ -14,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Switch } from "@/components/ui/switch"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; @@ -182,6 +183,14 @@ export const V2RepeaterConfigPanel: React.FC = ({ // 🆕 확장된 컬럼 (상세 설정 표시용) const [expandedColumn, setExpandedColumn] = useState(null); + // Collapsible 상태 + const [featureOptionsOpen, setFeatureOptionsOpen] = useState(true); + const [columnSelectOpen, setColumnSelectOpen] = useState(true); + const [selectedColumnsOpen, setSelectedColumnsOpen] = useState(true); + const [calcRulesOpen, setCalcRulesOpen] = useState(false); + const [entityJoinSubOpen, setEntityJoinSubOpen] = useState>({}); + const [configuredJoinsOpen, setConfiguredJoinsOpen] = useState(false); + // 🆕 채번 규칙 목록 const [numberingRules, setNumberingRules] = useState([]); const [loadingNumberingRules, setLoadingNumberingRules] = useState(false); @@ -1120,72 +1129,86 @@ export const V2RepeaterConfigPanel: React.FC = ({ )} - {/* 기능 옵션 - 토스식 Switch + 설명 */} -
- 기능 옵션 -
-
-
-

추가 버튼

-

새로운 행을 추가할 수 있어요

+ {/* 기능 옵션 - Collapsible */} + + +
-
-
-

삭제 버튼

-

선택한 행을 삭제할 수 있어요

+ + + + +
+
+
+

추가 버튼

+

새로운 행을 추가할 수 있어요

+
+ updateFeatures("showAddButton", checked)} + />
- updateFeatures("showDeleteButton", checked)} - /> -
-
-
-

인라인 편집

-

셀을 클릭하면 바로 수정할 수 있어요

+
+
+

삭제 버튼

+

선택한 행을 삭제할 수 있어요

+
+ updateFeatures("showDeleteButton", checked)} + />
- updateFeatures("inlineEdit", checked)} - /> -
-
-
-

다중 선택

-

여러 행을 동시에 선택할 수 있어요

+
+
+

인라인 편집

+

셀을 클릭하면 바로 수정할 수 있어요

+
+ updateFeatures("inlineEdit", checked)} + />
- updateFeatures("multiSelect", checked)} - /> -
-
-
-

행 번호

-

각 행에 순번을 표시해요

+
+
+

다중 선택

+

여러 행을 동시에 선택할 수 있어요

+
+ updateFeatures("multiSelect", checked)} + />
- updateFeatures("showRowNumber", checked)} - /> -
-
-
-

행 선택

-

체크박스로 행을 선택할 수 있어요

+
+
+

행 번호

+

각 행에 순번을 표시해요

+
+ updateFeatures("showRowNumber", checked)} + /> +
+
+
+

행 선택

+

체크박스로 행을 선택할 수 있어요

+
+ updateFeatures("selectable", checked)} + />
- updateFeatures("selectable", checked)} - />
-
-
+ + {/* 고급 설정 - Collapsible (소스 디테일 등) */} @@ -1328,103 +1351,131 @@ export const V2RepeaterConfigPanel: React.FC = ({ {/* 컬럼 설정 탭 */} - {/* 통합 컬럼 선택 */} -
-
- - - {isModalMode ? "어떤 컬럼을 표시하고 입력받을까요?" : "어떤 컬럼을 입력받을까요?"} - -
-

- {isModalMode - ? "소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용이에요" - : "체크한 컬럼이 리피터에 입력 필드로 표시돼요" - } -

- - {/* 모달 모드: 소스 테이블 컬럼 (표시용) */} - {isModalMode && config.dataSource?.sourceTable && ( - <> -
- - 소스 테이블 ({config.dataSource.sourceTable}) - 표시용 + {/* 통합 컬럼 선택 - Collapsible */} + + + + + +
+

+ {isModalMode + ? "소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용이에요" + : "체크한 컬럼이 리피터에 입력 필드로 표시돼요" + } +

+ + {/* 모달 모드: 소스 테이블 컬럼 (표시용) */} + {isModalMode && config.dataSource?.sourceTable && ( + <> +
+ + 소스 테이블 ({config.dataSource.sourceTable}) - 표시용 +
+ {loadingSourceColumns ? ( +

로딩 중...

+ ) : sourceTableColumns.length === 0 ? ( +

컬럼 정보가 없습니다

+ ) : ( +
+ {sourceTableColumns.map((column) => ( +
toggleSourceDisplayColumn(column)} + > + toggleSourceDisplayColumn(column)} + className="pointer-events-none h-3.5 w-3.5" + /> + + {column.displayName} + 표시 +
+ ))} +
+ )} + + )} + + {/* 저장 테이블 컬럼 (입력용) */} +
+ + 저장 테이블 ({targetTableForColumns || "미선택"}) - 입력용 +
+ {loadingColumns ? (

로딩 중...

- ) : sourceTableColumns.length === 0 ? ( -

컬럼 정보가 없습니다

+ ) : inputableColumns.length === 0 ? ( +

+ 컬럼 정보가 없습니다 +

) : ( -
- {sourceTableColumns.map((column) => ( +
+ {inputableColumns.map((column) => (
toggleSourceDisplayColumn(column)} + onClick={() => toggleInputColumn(column)} > toggleSourceDisplayColumn(column)} + checked={isColumnAdded(column.columnName)} + onCheckedChange={() => toggleInputColumn(column)} className="pointer-events-none h-3.5 w-3.5" /> - + {column.displayName} - 표시 + {column.inputType}
))}
)} - - )} - - {/* 저장 테이블 컬럼 (입력용) */} -
- - 저장 테이블 ({targetTableForColumns || "미선택"}) - 입력용 -
- {loadingColumns ? ( -

로딩 중...

- ) : inputableColumns.length === 0 ? ( -

- 컬럼 정보가 없습니다 -

- ) : ( -
- {inputableColumns.map((column) => ( -
toggleInputColumn(column)} - > - toggleInputColumn(column)} - className="pointer-events-none h-3.5 w-3.5" - /> - - {column.displayName} - {column.inputType} -
- ))}
- )} -
+ + - {/* 선택된 컬럼 상세 설정 */} + {/* 선택된 컬럼 상세 설정 - Collapsible */} {config.columns.length > 0 && ( - <> -
-
- 선택된 컬럼 ({config.columns.length}) - 드래그로 순서 변경 -
-
+ + + + + +
+

드래그로 순서 변경, 클릭하여 상세 설정

+
{config.columns.map((col, index) => (
{/* 컬럼 헤더 (드래그 가능) */} @@ -1744,25 +1795,38 @@ export const V2RepeaterConfigPanel: React.FC = ({ ))}
- + + )} - {/* 계산 규칙 */} + {/* 계산 규칙 - Collapsible (기본 닫힘) */} {(isModalMode || isInlineMode) && config.columns.length > 0 && ( - <> -
-
+ + + + + +
+
-
+
{calculationRules.map((rule) => (
@@ -1862,7 +1926,8 @@ export const V2RepeaterConfigPanel: React.FC = ({ )}
- + + )} @@ -1897,94 +1962,126 @@ export const V2RepeaterConfigPanel: React.FC = ({ )}
) : ( -
+
{entityJoinData.joinTables.map((joinTable, tableIndex) => { const sourceColumn = (joinTable as any).joinConfig?.sourceColumn || ""; + const activeCount = joinTable.availableColumns.filter(col => + isEntityJoinColumnActive(joinTable.tableName, sourceColumn, col.columnName) + ).length; + const isSubOpen = entityJoinSubOpen[tableIndex] ?? false; return ( -
-
- - {joinTable.tableName} - ({sourceColumn}) -
-
- {joinTable.availableColumns.map((column, colIndex) => { - const isActive = isEntityJoinColumnActive( - joinTable.tableName, - sourceColumn, - column.columnName, - ); - const matchingCol = config.columns.find((c) => c.key === column.columnName); - const displayField = matchingCol?.key || column.columnName; + setEntityJoinSubOpen((prev) => ({ ...prev, [tableIndex]: open }))}> + + + + +
+ {joinTable.availableColumns.map((column, colIndex) => { + const isActive = isEntityJoinColumnActive( + joinTable.tableName, + sourceColumn, + column.columnName, + ); + const matchingCol = config.columns.find((c) => c.key === column.columnName); + const displayField = matchingCol?.key || column.columnName; - return ( -
- toggleEntityJoinColumn( - joinTable.tableName, - sourceColumn, - column.columnName, - column.columnLabel, - displayField, - ) - } - > - - - {column.columnLabel} - - {column.inputType || column.dataType} - -
- ); - })} -
-
+ return ( +
+ toggleEntityJoinColumn( + joinTable.tableName, + sourceColumn, + column.columnName, + column.columnLabel, + displayField, + ) + } + > + + + {column.columnLabel} + + {column.inputType || column.dataType} + +
+ ); + })} +
+ + ); })}
)} - {/* 현재 설정된 Entity 조인 목록 */} + {/* 현재 설정된 Entity 조인 목록 - Collapsible */} {config.entityJoins && config.entityJoins.length > 0 && ( -
- 설정된 조인 ({config.entityJoins.length}) -
- {config.entityJoins.map((join, idx) => ( -
- - {join.sourceColumn} - - {join.referenceTable} - - ({join.columns.map((c) => c.referenceField).join(", ")}) - - + + +
-
+ + + + +
+ {config.entityJoins.map((join, idx) => ( +
+ + {join.sourceColumn} + + {join.referenceTable} + + ({join.columns.map((c) => c.referenceField).join(", ")}) + + +
+ ))} +
+
+ )}
diff --git a/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx b/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx index 9d7380e5..d01db5ba 100644 --- a/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx @@ -11,6 +11,7 @@ import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; @@ -47,6 +48,9 @@ export const V2TimelineSchedulerConfigPanel: React.FC {/* ─── 1단계: 스케줄 데이터 테이블 설정 ─── */} -
-
- -

스케줄 데이터 테이블

-
-

스케줄 데이터를 저장/조회할 테이블을 설정해요

-
- -
+ + + + + +
{/* 스케줄 타입 */}
- 스케줄 타입 + 스케줄 타입 updateConfig({ selectedTable: e.target.value })} @@ -301,10 +312,10 @@ export const V2TimelineSchedulerConfigPanel: React.FC -

스케줄 필드 매핑

+

스케줄 필드 매핑

- ID 필드 * + ID 필드 * updateFieldMapping("resourceId", v)} @@ -342,7 +353,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC
- 제목 필드 * + 제목 필드 * updateFieldMapping("startDate", v)} @@ -380,7 +391,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC
- 종료일 필드 * + 종료일 필드 * updateFieldMapping("status", v)} @@ -418,7 +429,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC
- 진행률 필드 + 진행률 필드 updateFieldMapping("color", v)} @@ -454,22 +465,33 @@ export const V2TimelineSchedulerConfigPanel: React.FC
+
-
+ + {/* ─── 2단계: 소스 데이터 설정 ─── */} -
-
- -

소스 데이터 설정

-
-

스케줄 자동 생성 시 참조할 원본 데이터를 설정해요

-
- -
+ + + + + +
{/* 소스 테이블 Combobox */}
- 소스 테이블 (수주/작업요청 등) + 소스 테이블 (수주/작업요청 등)
+ + {/* ─── 3단계: 리소스 설정 ─── */} -
-
- -

리소스 설정 (설비/작업자)

-
-

타임라인 Y축에 표시할 리소스를 설정해요

-
- -
+ + + + + +
{/* 리소스 테이블 Combobox */}
- 리소스 테이블 + 리소스 테이블