diff --git a/frontend/components/screen/config-panels/button-config/BasicTab.tsx b/frontend/components/screen/config-panels/button-config/BasicTab.tsx index 152bc1c0..6bf05f70 100644 --- a/frontend/components/screen/config-panels/button-config/BasicTab.tsx +++ b/frontend/components/screen/config-panels/button-config/BasicTab.tsx @@ -26,7 +26,7 @@ import { } from "@/components/ui/command"; import { Check, Plus, X, Info, RotateCcw } from "lucide-react"; import { icons as allLucideIcons } from "lucide-react"; -import DOMPurify from "isomorphic-dompurify"; +import { sanitizeSvg } from "@/lib/button-icon-map"; import { cn } from "@/lib/utils"; import { ComponentData } from "@/types/screen"; import { ColorPickerWithTransparent } from "../../common/ColorPickerWithTransparent"; @@ -370,9 +370,7 @@ export const BasicTab: React.FC = ({ @@ -525,9 +523,7 @@ export const BasicTab: React.FC = ({ @@ -547,9 +543,7 @@ export const BasicTab: React.FC = ({ setSvgError("유효한 SVG 코드가 아닙니다."); return; } - const sanitized = DOMPurify.sanitize(svgInput, { - USE_PROFILES: { svg: true }, - }); + const sanitized = sanitizeSvg(svgInput); let finalName = svgName.trim(); const existingNames = new Set( customSvgIcons.map((s) => s.name) @@ -686,9 +680,7 @@ export const BasicTab: React.FC = ({ @@ -841,9 +833,7 @@ export const BasicTab: React.FC = ({ @@ -863,9 +853,7 @@ export const BasicTab: React.FC = ({ setSvgError("유효한 SVG 코드가 아닙니다."); return; } - const sanitized = DOMPurify.sanitize(svgInput, { - USE_PROFILES: { svg: true }, - }); + const sanitized = sanitizeSvg(svgInput); let finalName = svgName.trim(); const existingNames = new Set( customSvgIcons.map((s) => s.name) diff --git a/frontend/components/v2/config-panels/V2AggregationWidgetConfigPanel.tsx b/frontend/components/v2/config-panels/V2AggregationWidgetConfigPanel.tsx index 3385572d..6e30e17b 100644 --- a/frontend/components/v2/config-panels/V2AggregationWidgetConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2AggregationWidgetConfigPanel.tsx @@ -679,7 +679,7 @@ export const V2AggregationWidgetConfigPanel: React.FC위의 추가 버튼으로 항목을 만들어보세요

) : ( -
+
{(config.items || []).map((item, index) => (
) : ( -
+
{(config.filters || []).map((filter, index) => (
= ({ // UI 상태 const [iconSectionOpen, setIconSectionOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); - const [dataflowOpen, setDataflowOpen] = useState(false); const [lucideSearchOpen, setLucideSearchOpen] = useState(false); const [lucideSearchTerm, setLucideSearchTerm] = useState(""); const [svgPasteOpen, setSvgPasteOpen] = useState(false); @@ -304,26 +302,25 @@ export const V2ButtonConfigPanel: React.FC = ({ [config, onChange] ); - // onUpdateProperty 래퍼 (V2 패널에서도 기존 컨트롤 패널 사용 가능하도록) + // 기존 서브패널(ImprovedButtonControlConfigPanel 등)이 webTypeConfig.* 경로로 쓰므로 + // 항상 config 기반 onChange로 통일 (onUpdateProperty는 V2 경로 불일치 문제 있음) const handleUpdateProperty = useCallback( (path: string, value: any) => { - if (onUpdateProperty) { - onUpdateProperty(path, value); - } else { - // path를 파싱해서 config에 직접 반영 - const parts = path.replace("componentConfig.", "").split("."); - const newConfig = { ...config }; - let current: any = newConfig; - for (let i = 0; i < parts.length - 1; i++) { - if (!current[parts[i]]) current[parts[i]] = {}; - current[parts[i]] = { ...current[parts[i]] }; - current = current[parts[i]]; - } - current[parts[parts.length - 1]] = value; - onChange(newConfig); + const normalizedPath = path + .replace(/^componentConfig\./, "") + .replace(/^webTypeConfig\./, ""); + const parts = normalizedPath.split("."); + const newConfig = { ...config }; + let current: any = newConfig; + for (let i = 0; i < parts.length - 1; i++) { + if (!current[parts[i]]) current[parts[i]] = {}; + current[parts[i]] = { ...current[parts[i]] }; + current = current[parts[i]]; } + current[parts[parts.length - 1]] = value; + onChange(newConfig); }, - [config, onChange, onUpdateProperty] + [config, onChange] ); // prop 변경 시 아이콘 상태 동기화 @@ -392,14 +389,22 @@ export const V2ButtonConfigPanel: React.FC = ({ }; // componentData 생성 (기존 패널 재사용용) + // effectiveComponent가 있어도 config 변경분을 반드시 반영해야 토글 등이 동작함 const componentData = useMemo(() => { - if (effectiveComponent) return effectiveComponent; + if (effectiveComponent) { + return { + ...effectiveComponent, + componentConfig: config, + webTypeConfig: config, + } as ComponentData; + } return { id: "virtual", type: "widget" as const, position: { x: 0, y: 0 }, size: { width: 120, height: 40 }, componentConfig: config, + webTypeConfig: config, componentType: "v2-button-primary", } as ComponentData; }, [effectiveComponent, config]); @@ -582,35 +587,6 @@ export const V2ButtonConfigPanel: React.FC = ({ )} - {/* ─── 데이터플로우 설정 (접기) ─── */} - - - - - -
- -
-
-
- {/* ─── 고급 설정 (접기) ─── */} diff --git a/frontend/components/v2/config-panels/V2CardDisplayConfigPanel.tsx b/frontend/components/v2/config-panels/V2CardDisplayConfigPanel.tsx index 6570b5aa..c8537ae0 100644 --- a/frontend/components/v2/config-panels/V2CardDisplayConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2CardDisplayConfigPanel.tsx @@ -542,7 +542,7 @@ export const V2CardDisplayConfigPanel: React.FC =
{(config.columnMapping?.displayColumns || []).length > 0 ? ( -
+
{(config.columnMapping?.displayColumns || []).map( (column: string, index: number) => (
@@ -613,7 +613,7 @@ export const V2CardDisplayConfigPanel: React.FC = {/* ─── 4단계: 표시 요소 토글 ─── */}

표시 요소

-
+

타이틀

@@ -733,7 +733,7 @@ export const V2CardDisplayConfigPanel: React.FC = /> - +
설명 최대 길이 diff --git a/frontend/components/v2/config-panels/V2InputConfigPanel.tsx b/frontend/components/v2/config-panels/V2InputConfigPanel.tsx index de1f32e6..1b4462dd 100644 --- a/frontend/components/v2/config-panels/V2InputConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2InputConfigPanel.tsx @@ -11,7 +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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { Settings, ChevronDown, Loader2 } from "lucide-react"; +import { Settings, ChevronDown, Loader2, Type, Hash, Lock, AlignLeft, SlidersHorizontal, Palette, ListOrdered } from "lucide-react"; import { cn } from "@/lib/utils"; import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen"; import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; @@ -84,29 +84,51 @@ export const V2InputConfigPanel: React.FC = ({ config, return (
- {/* ─── 1단계: 입력 타입 선택 ─── */} + {/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */}
-

입력 타입

- +
+ +

입력 타입

+

입력 필드의 종류를 선택해요

+
+ {[ + { value: "text", icon: Type, label: "텍스트", desc: "일반 텍스트 입력" }, + { value: "number", icon: Hash, label: "숫자", desc: "숫자만 입력" }, + { value: "password", icon: Lock, label: "비밀번호", desc: "마스킹 처리" }, + { value: "textarea", icon: AlignLeft, label: "여러 줄", desc: "긴 텍스트 입력" }, + { value: "slider", icon: SlidersHorizontal, label: "슬라이더", desc: "범위 선택" }, + { value: "color", icon: Palette, label: "색상", desc: "색상 선택기" }, + { value: "numbering", icon: ListOrdered, label: "채번", desc: "자동 번호 생성" }, + ].map((item) => ( + + ))} +
+ {/* ─── 채번 타입 전용 안내 ─── */} {inputType === "numbering" && (
diff --git a/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx b/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx index 416b6975..e47c61ba 100644 --- a/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2ItemRoutingConfigPanel.tsx @@ -428,7 +428,7 @@ export const V2ItemRoutingConfigPanel: React.FC =

공정 순서 테이블에 표시할 컬럼

-
+
{config.processColumns.map((col, idx) => (
diff --git a/frontend/components/v2/config-panels/V2PivotGridConfigPanel.tsx b/frontend/components/v2/config-panels/V2PivotGridConfigPanel.tsx index 10c2991a..efbc2d36 100644 --- a/frontend/components/v2/config-panels/V2PivotGridConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2PivotGridConfigPanel.tsx @@ -467,7 +467,7 @@ export const V2PivotGridConfigPanel: React.FC = ({ /> - +
{/* 총계 설정 */}
diff --git a/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx b/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx index 6cd608fb..33a7ae33 100644 --- a/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2ProcessWorkStandardConfigPanel.tsx @@ -118,7 +118,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC

공정별 작업 단계(Phase)를 정의

-
+
{config.phases.map((phase, idx) => (
@@ -218,7 +218,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC

작업 항목의 상세 유형 드롭다운 옵션

-
+
{config.detailTypes.map((dt, idx) => (
diff --git a/frontend/components/v2/config-panels/V2RackStructureConfigPanel.tsx b/frontend/components/v2/config-panels/V2RackStructureConfigPanel.tsx index 842b503a..633702c8 100644 --- a/frontend/components/v2/config-panels/V2RackStructureConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2RackStructureConfigPanel.tsx @@ -5,12 +5,13 @@ * 토스식 단계별 UX: 필드 매핑 -> 제한 설정 -> UI 설정(접힘) */ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { Settings, ChevronDown } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Database, SlidersHorizontal, Settings, ChevronDown, CheckCircle2, Circle } from "lucide-react"; import { cn } from "@/lib/utils"; import type { RackStructureComponentConfig, FieldMapping } from "@/lib/registry/components/v2-rack-structure/types"; @@ -81,85 +82,111 @@ export const V2RackStructureConfigPanel: React.FC fieldMappingItems.filter((item) => fieldMapping[item.key]).length, + [fieldMapping] + ); + return (
{/* ─── 1단계: 필드 매핑 ─── */} -
-

필드 매핑

-

+

+
+ +

필드 매핑

+ + {mappedCount}/{fieldMappingItems.length} + +
+

상위 폼의 필드 중 렉 생성에 사용할 필드를 선택해요

-
- {fieldMappingItems.map((item) => ( -
-
- {item.label} -

{item.description}

-
- -
- ))} + {isMapped ? ( + + ) : ( + + )} +
+

{item.label}

+

{item.description}

+
+ +
+ ); + })}
{/* ─── 2단계: 제한 설정 ─── */} -
-

제한 설정

-

+

+
+ +

제한 설정

+
+

렉 조건의 최대값을 설정해요

-
-
- 최대 조건 수 +
+
+

최대 조건

handleChange("maxConditions", parseInt(e.target.value) || 10)} - className="h-7 w-[100px] text-xs" + className="h-7 text-xs text-center" />
- -
- 최대 열 수 +
+

최대 열

handleChange("maxRows", parseInt(e.target.value) || 99)} - className="h-7 w-[100px] text-xs" + className="h-7 text-xs text-center" />
- -
- 최대 단 수 +
+

최대 단

handleChange("maxLevels", parseInt(e.target.value) || 20)} - className="h-7 w-[100px] text-xs" + className="h-7 text-xs text-center" />
@@ -186,9 +213,9 @@ export const V2RackStructureConfigPanel: React.FC
-
-

템플릿 기능

-

조건을 템플릿으로 저장/불러오기할 수 있어요

+
+

템플릿 기능

+

조건을 템플릿으로 저장/불러오기할 수 있어요

-
-

미리보기

-

생성될 위치를 미리 확인할 수 있어요

+
+

미리보기

+

생성될 위치를 미리 확인할 수 있어요

-
-

통계 카드

-

총 위치 수 등 통계를 카드로 표시해요

+
+

통계 카드

+

총 위치 수 등 통계를 카드로 표시해요

-
-

읽기 전용

-

조건을 수정할 수 없게 해요

+
+

읽기 전용

+

조건을 수정할 수 없게 해요

-
+

제목/설명 표시

@@ -635,7 +635,7 @@ export const V2RepeatContainerConfigPanel: React.FC -
+
배경색 @@ -721,7 +721,7 @@ export const V2RepeatContainerConfigPanel: React.FC -
+

클릭 가능

@@ -977,7 +977,7 @@ function SlotChildrenSection({

{children.length > 0 ? ( -
+
{children.map((child, index) => { const isExpanded = expandedIds.has(child.id); return ( diff --git a/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx b/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx index 25e7414b..4f5e216b 100644 --- a/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2RepeaterConfigPanel.tsx @@ -1145,7 +1145,7 @@ export const V2RepeaterConfigPanel: React.FC = ({ -
+

추가 버튼

@@ -1391,7 +1391,7 @@ export const V2RepeaterConfigPanel: React.FC = ({ ) : sourceTableColumns.length === 0 ? (

컬럼 정보가 없습니다

) : ( -
+
{sourceTableColumns.map((column) => (
= ({ 컬럼 정보가 없습니다

) : ( -
+
{inputableColumns.map((column) => (
= ({

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

-
+
{config.columns.map((col, index) => (
{/* 컬럼 헤더 (드래그 가능) */} @@ -1826,7 +1826,7 @@ export const V2RepeaterConfigPanel: React.FC = ({
-
+
{calculationRules.map((rule) => (
@@ -1988,7 +1988,7 @@ export const V2RepeaterConfigPanel: React.FC = ({ -
+
{joinTable.availableColumns.map((column, colIndex) => { const isActive = isEntityJoinColumnActive( joinTable.tableName, @@ -2054,7 +2054,7 @@ export const V2RepeaterConfigPanel: React.FC = ({ -
+
{config.entityJoins.map((join, idx) => (
diff --git a/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx b/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx index 28e846d4..56a9b780 100644 --- a/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2SelectedItemsDetailInputConfigPanel.tsx @@ -807,7 +807,7 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
{displayColumns.length > 0 && ( -
+
{displayColumns.map((col) => (
{localFields.length > 0 && ( -
+
{localFields.map((field, index) => (
diff --git a/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx b/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx index 628976db..a490d6b6 100644 --- a/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx @@ -1036,7 +1036,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC< /> - +
{/* 좌측 패널 제목 */}
@@ -1281,7 +1281,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC< /> - +
{/* 우측 패널 제목 */}
@@ -2079,7 +2079,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC< /> - +
{/* 탭 목록 */} {(config.rightPanel?.additionalTabs || []).map( @@ -2296,7 +2296,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC< /> - +
=

위의 추가 버튼으로 항목을 만들어보세요

) : ( -
+
{items.map((item: StatusCountItem, i: number) => (
{/* 첫 번째 줄: 상태값 + 삭제 */} diff --git a/frontend/components/v2/config-panels/V2TableGroupedConfigPanel.tsx b/frontend/components/v2/config-panels/V2TableGroupedConfigPanel.tsx index d9e69e12..8ceacd5f 100644 --- a/frontend/components/v2/config-panels/V2TableGroupedConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TableGroupedConfigPanel.tsx @@ -570,7 +570,7 @@ export const V2TableGroupedConfigPanel: React.FC - +
{/* 체크박스 */} @@ -671,7 +671,7 @@ export const V2TableGroupedConfigPanel: React.FC - +

diff --git a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx index 7a65741e..a815b41c 100644 --- a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx @@ -790,7 +790,7 @@ export const V2TableListConfigPanel: React.FC = ({ placeholder="컬럼 검색..." className="h-7 text-xs" /> -

+
{availableColumns .filter((column) => { if (!columnSearchText) return true; @@ -919,7 +919,7 @@ export const V2TableListConfigPanel: React.FC = ({ -
+
{joinTable.availableColumns.map((column, colIndex) => { const matchingJoinColumn = entityJoinColumns.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, @@ -1034,7 +1034,7 @@ export const V2TableListConfigPanel: React.FC = ({ items={(config.columns || []).map((c) => c.columnName)} strategy={verticalListSortingStrategy} > -
+
{(config.columns || []).map((column, idx) => { const resolvedLabel = column.displayName && column.displayName !== column.columnName diff --git a/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx b/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx index d01db5ba..11815db8 100644 --- a/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx @@ -204,7 +204,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC -
+
{/* 스케줄 타입 */}
스케줄 타입 @@ -488,7 +488,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC -
+
{/* 소스 테이블 Combobox */}
소스 테이블 (수주/작업요청 등) @@ -654,7 +654,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC -
+
{/* 리소스 테이블 Combobox */}
리소스 테이블 @@ -800,7 +800,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC -
+
{/* 줌 레벨 */}
기본 줌 레벨 @@ -1063,7 +1063,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC -
+
{[ { key: "planned", label: "계획됨", defaultColor: "#3b82f6" }, { key: "in_progress", label: "진행중", defaultColor: "#f59e0b" }, diff --git a/frontend/lib/button-icon-map.tsx b/frontend/lib/button-icon-map.tsx index d8c38b25..4ddd2f7d 100644 --- a/frontend/lib/button-icon-map.tsx +++ b/frontend/lib/button-icon-map.tsx @@ -1,5 +1,8 @@ import React from "react"; -import DOMPurify from "isomorphic-dompurify"; +let DOMPurify: any = null; +if (typeof window !== "undefined") { + DOMPurify = require("isomorphic-dompurify"); +} import { Check, Save, CheckCircle, CircleCheck, FileCheck, ShieldCheck, Trash2, Trash, XCircle, X, Eraser, CircleX, @@ -119,6 +122,7 @@ export function addToIconMap(name: string, component: LucideIcon): void { // SVG 정화 // --------------------------------------------------------------------------- export function sanitizeSvg(svgString: string): string { + if (!DOMPurify) return svgString; return DOMPurify.sanitize(svgString, { USE_PROFILES: { svg: true } }); }