feat: add responsive tests for Option Settings and Split Panel
- Implemented responsive testing scripts for Option Settings and ResponsiveSplitPanel components. - Added tests to verify layout behavior across various screen sizes (desktop, tablet, mobile). - Generated screenshots and reports for visual validation of responsive designs. Made-with: Cursor
This commit is contained in:
parent
00e15dd9f2
commit
45740c1457
|
|
@ -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<ButtonTabProps> = ({
|
|||
<span
|
||||
className="flex h-6 w-6 items-center justify-center [&>svg]:h-full [&>svg]:w-full"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(svgIcon.svg, {
|
||||
USE_PROFILES: { svg: true },
|
||||
}),
|
||||
__html: sanitizeSvg(svgIcon.svg),
|
||||
}}
|
||||
/>
|
||||
<span className="text-muted-foreground truncate text-[10px]">
|
||||
|
|
@ -525,9 +523,7 @@ export const BasicTab: React.FC<ButtonTabProps> = ({
|
|||
<span
|
||||
className="flex h-8 w-8 items-center justify-center [&>svg]:h-full [&>svg]:w-full"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(svgInput, {
|
||||
USE_PROFILES: { svg: true },
|
||||
}),
|
||||
__html: sanitizeSvg(svgInput),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -547,9 +543,7 @@ export const BasicTab: React.FC<ButtonTabProps> = ({
|
|||
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<ButtonTabProps> = ({
|
|||
<span
|
||||
className="flex h-6 w-6 items-center justify-center [&>svg]:h-full [&>svg]:w-full"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(svgIcon.svg, {
|
||||
USE_PROFILES: { svg: true },
|
||||
}),
|
||||
__html: sanitizeSvg(svgIcon.svg),
|
||||
}}
|
||||
/>
|
||||
<span className="text-muted-foreground truncate text-[10px]">
|
||||
|
|
@ -841,9 +833,7 @@ export const BasicTab: React.FC<ButtonTabProps> = ({
|
|||
<span
|
||||
className="flex h-8 w-8 items-center justify-center [&>svg]:h-full [&>svg]:w-full"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(svgInput, {
|
||||
USE_PROFILES: { svg: true },
|
||||
}),
|
||||
__html: sanitizeSvg(svgInput),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -863,9 +853,7 @@ export const BasicTab: React.FC<ButtonTabProps> = ({
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -679,7 +679,7 @@ export const V2AggregationWidgetConfigPanel: React.FC<V2AggregationWidgetConfigP
|
|||
<p className="text-xs text-muted-foreground">위의 추가 버튼으로 항목을 만들어보세요</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[300px] space-y-1.5 overflow-y-auto">
|
||||
<div className="space-y-1.5">
|
||||
{(config.items || []).map((item, index) => (
|
||||
<div key={item.id} className="rounded-md border">
|
||||
<button
|
||||
|
|
@ -861,7 +861,7 @@ export const V2AggregationWidgetConfigPanel: React.FC<V2AggregationWidgetConfigP
|
|||
<p className="text-xs text-muted-foreground">필터 없음 - 전체 데이터를 집계합니다</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[250px] space-y-2 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{(config.filters || []).map((filter, index) => (
|
||||
<div
|
||||
key={filter.id}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ import {
|
|||
getDefaultIconForAction,
|
||||
sanitizeSvg,
|
||||
} from "@/lib/button-icon-map";
|
||||
import { ButtonDataflowConfigPanel } from "@/components/screen/config-panels/ButtonDataflowConfigPanel";
|
||||
import { ImprovedButtonControlConfigPanel } from "@/components/screen/config-panels/ImprovedButtonControlConfigPanel";
|
||||
import { FlowVisibilityConfigPanel } from "@/components/screen/config-panels/FlowVisibilityConfigPanel";
|
||||
import type { ComponentData } from "@/types/screen";
|
||||
|
|
@ -257,7 +256,6 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
|
|||
// 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<V2ButtonConfigPanelProps> = ({
|
|||
[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<V2ButtonConfigPanelProps> = ({
|
|||
};
|
||||
|
||||
// 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<V2ButtonConfigPanelProps> = ({
|
|||
</Collapsible>
|
||||
)}
|
||||
|
||||
{/* ─── 데이터플로우 설정 (접기) ─── */}
|
||||
<Collapsible open={dataflowOpen} onOpenChange={setDataflowOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Workflow className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">데이터플로우</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
||||
dataflowOpen && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-4">
|
||||
<ButtonDataflowConfigPanel
|
||||
component={componentData}
|
||||
onUpdateProperty={handleUpdateProperty}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* ─── 고급 설정 (접기) ─── */}
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -542,7 +542,7 @@ export const V2CardDisplayConfigPanel: React.FC<V2CardDisplayConfigPanelProps> =
|
|||
</div>
|
||||
|
||||
{(config.columnMapping?.displayColumns || []).length > 0 ? (
|
||||
<div className="max-h-[250px] space-y-1.5 overflow-y-auto">
|
||||
<div className="space-y-1.5">
|
||||
{(config.columnMapping?.displayColumns || []).map(
|
||||
(column: string, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
|
|
@ -613,7 +613,7 @@ export const V2CardDisplayConfigPanel: React.FC<V2CardDisplayConfigPanelProps> =
|
|||
{/* ─── 4단계: 표시 요소 토글 ─── */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium truncate">표시 요소</p>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-lg border bg-muted/30 p-4 space-y-2">
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-2">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">타이틀</p>
|
||||
|
|
@ -733,7 +733,7 @@ export const V2CardDisplayConfigPanel: React.FC<V2CardDisplayConfigPanelProps> =
|
|||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-muted-foreground truncate">설명 최대 길이</span>
|
||||
|
|
|
|||
|
|
@ -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<V2InputConfigPanelProps> = ({ config,
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* ─── 1단계: 입력 타입 선택 ─── */}
|
||||
{/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">입력 타입</p>
|
||||
<Select
|
||||
value={inputType}
|
||||
onValueChange={(value) => updateConfig("inputType", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder="입력 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">텍스트</SelectItem>
|
||||
<SelectItem value="number">숫자</SelectItem>
|
||||
<SelectItem value="password">비밀번호</SelectItem>
|
||||
<SelectItem value="textarea">여러 줄 텍스트</SelectItem>
|
||||
<SelectItem value="slider">슬라이더</SelectItem>
|
||||
<SelectItem value="color">색상 선택</SelectItem>
|
||||
<SelectItem value="numbering">채번 (자동생성)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-2">
|
||||
<Type className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium">입력 타입</p>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">입력 필드의 종류를 선택해요</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ 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) => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => updateConfig("inputType", item.value)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-lg border p-2.5 text-left transition-all",
|
||||
inputType === item.value
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary/20"
|
||||
: "border-border hover:border-primary/30 hover:bg-muted/30"
|
||||
)}
|
||||
>
|
||||
<item.icon className={cn(
|
||||
"h-4 w-4 shrink-0",
|
||||
inputType === item.value ? "text-primary" : "text-muted-foreground"
|
||||
)} />
|
||||
<div className="min-w-0">
|
||||
<span className={cn(
|
||||
"text-xs font-medium block",
|
||||
inputType === item.value ? "text-primary" : "text-foreground"
|
||||
)}>{item.label}</span>
|
||||
<span className="text-[10px] text-muted-foreground block truncate">{item.desc}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ─── 채번 타입 전용 안내 ─── */}
|
||||
{inputType === "numbering" && (
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ export const V2ItemRoutingConfigPanel: React.FC<V2ItemRoutingConfigPanelProps> =
|
|||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
|
||||
<p className="text-[10px] text-muted-foreground mb-1">공정 순서 테이블에 표시할 컬럼</p>
|
||||
<div className="max-h-[250px] space-y-1 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{config.processColumns.map((col, idx) => (
|
||||
<Collapsible key={idx}>
|
||||
<div className="rounded-md border">
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ export const V2PivotGridConfigPanel: React.FC<V2PivotGridConfigPanelProps> = ({
|
|||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-4 rounded-b-lg border border-t-0 p-4">
|
||||
{/* 총계 설정 */}
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
|||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
|
||||
<p className="text-[10px] text-muted-foreground mb-1">공정별 작업 단계(Phase)를 정의</p>
|
||||
<div className="max-h-[250px] space-y-1 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{config.phases.map((phase, idx) => (
|
||||
<Collapsible key={idx}>
|
||||
<div className="rounded-md border">
|
||||
|
|
@ -218,7 +218,7 @@ export const V2ProcessWorkStandardConfigPanel: React.FC<V2ProcessWorkStandardCon
|
|||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-1.5">
|
||||
<p className="text-[10px] text-muted-foreground mb-1">작업 항목의 상세 유형 드롭다운 옵션</p>
|
||||
<div className="max-h-[250px] space-y-1 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{config.detailTypes.map((dt, idx) => (
|
||||
<Collapsible key={idx}>
|
||||
<div className="rounded-md border">
|
||||
|
|
|
|||
|
|
@ -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<V2RackStructureConfigPanelProp
|
|||
{ key: "statusField", label: "사용 여부", description: "사용/미사용 상태를 나타내는 필드예요" },
|
||||
];
|
||||
|
||||
const mappedCount = useMemo(
|
||||
() => fieldMappingItems.filter((item) => fieldMapping[item.key]).length,
|
||||
[fieldMapping]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* ─── 1단계: 필드 매핑 ─── */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">필드 매핑</p>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
<p className="text-sm font-medium">필드 매핑</p>
|
||||
<Badge variant="secondary" className="ml-auto text-[10px] px-1.5 py-0">
|
||||
{mappedCount}/{fieldMappingItems.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground pl-6">
|
||||
상위 폼의 필드 중 렉 생성에 사용할 필드를 선택해요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||
{fieldMappingItems.map((item) => (
|
||||
<div key={item.key} className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">{item.label}</span>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">{item.description}</p>
|
||||
</div>
|
||||
<Select
|
||||
value={fieldMapping[item.key] || "__none__"}
|
||||
onValueChange={(v) => handleFieldMappingChange(item.key, v)}
|
||||
<div className="space-y-1.5">
|
||||
{fieldMappingItems.map((item) => {
|
||||
const isMapped = !!fieldMapping[item.key];
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg border px-3 py-2 transition-colors",
|
||||
isMapped ? "border-primary/30 bg-primary/5" : "bg-muted/30"
|
||||
)}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-[140px] text-xs">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">선택 안함</SelectItem>
|
||||
{availableColumns.map((col) => (
|
||||
<SelectItem key={col.value} value={col.value}>
|
||||
{col.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
{isMapped ? (
|
||||
<CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
) : (
|
||||
<Circle className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40" />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium truncate">{item.label}</p>
|
||||
<p className="text-[10px] text-muted-foreground truncate">{item.description}</p>
|
||||
</div>
|
||||
<Select
|
||||
value={fieldMapping[item.key] || "__none__"}
|
||||
onValueChange={(v) => handleFieldMappingChange(item.key, v)}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-[120px] shrink-0 text-xs">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">선택 안함</SelectItem>
|
||||
{availableColumns.map((col) => (
|
||||
<SelectItem key={col.value} value={col.value}>
|
||||
{col.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* ─── 2단계: 제한 설정 ─── */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">제한 설정</p>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="h-4 w-4 text-primary" />
|
||||
<p className="text-sm font-medium">제한 설정</p>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground pl-6">
|
||||
렉 조건의 최대값을 설정해요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-muted-foreground">최대 조건 수</span>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
|
||||
<p className="text-[10px] text-muted-foreground">최대 조건</p>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={config.maxConditions || 10}
|
||||
onChange={(e) => handleChange("maxConditions", parseInt(e.target.value) || 10)}
|
||||
className="h-7 w-[100px] text-xs"
|
||||
className="h-7 text-xs text-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-muted-foreground">최대 열 수</span>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
|
||||
<p className="text-[10px] text-muted-foreground">최대 열</p>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
value={config.maxRows || 99}
|
||||
onChange={(e) => handleChange("maxRows", parseInt(e.target.value) || 99)}
|
||||
className="h-7 w-[100px] text-xs"
|
||||
className="h-7 text-xs text-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-muted-foreground">최대 단 수</span>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-center space-y-1.5">
|
||||
<p className="text-[10px] text-muted-foreground">최대 단</p>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={99}
|
||||
value={config.maxLevels || 20}
|
||||
onChange={(e) => handleChange("maxLevels", parseInt(e.target.value) || 20)}
|
||||
className="h-7 w-[100px] text-xs"
|
||||
className="h-7 text-xs text-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,9 +213,9 @@ export const V2RackStructureConfigPanel: React.FC<V2RackStructureConfigPanelProp
|
|||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">템플릿 기능</p>
|
||||
<p className="text-[11px] text-muted-foreground">조건을 템플릿으로 저장/불러오기할 수 있어요</p>
|
||||
<div className="min-w-0 flex-1 mr-3">
|
||||
<p className="text-xs font-medium">템플릿 기능</p>
|
||||
<p className="text-[10px] text-muted-foreground">조건을 템플릿으로 저장/불러오기할 수 있어요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.showTemplates ?? true}
|
||||
|
|
@ -197,9 +224,9 @@ export const V2RackStructureConfigPanel: React.FC<V2RackStructureConfigPanelProp
|
|||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">미리보기</p>
|
||||
<p className="text-[11px] text-muted-foreground">생성될 위치를 미리 확인할 수 있어요</p>
|
||||
<div className="min-w-0 flex-1 mr-3">
|
||||
<p className="text-xs font-medium">미리보기</p>
|
||||
<p className="text-[10px] text-muted-foreground">생성될 위치를 미리 확인할 수 있어요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.showPreview ?? true}
|
||||
|
|
@ -208,9 +235,9 @@ export const V2RackStructureConfigPanel: React.FC<V2RackStructureConfigPanelProp
|
|||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">통계 카드</p>
|
||||
<p className="text-[11px] text-muted-foreground">총 위치 수 등 통계를 카드로 표시해요</p>
|
||||
<div className="min-w-0 flex-1 mr-3">
|
||||
<p className="text-xs font-medium">통계 카드</p>
|
||||
<p className="text-[10px] text-muted-foreground">총 위치 수 등 통계를 카드로 표시해요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.showStatistics ?? true}
|
||||
|
|
@ -219,9 +246,9 @@ export const V2RackStructureConfigPanel: React.FC<V2RackStructureConfigPanelProp
|
|||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">읽기 전용</p>
|
||||
<p className="text-[11px] text-muted-foreground">조건을 수정할 수 없게 해요</p>
|
||||
<div className="min-w-0 flex-1 mr-3">
|
||||
<p className="text-xs font-medium">읽기 전용</p>
|
||||
<p className="text-[10px] text-muted-foreground">조건을 수정할 수 없게 해요</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.readonly ?? false}
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-3 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-3">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">제목/설명 표시</p>
|
||||
|
|
@ -635,7 +635,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="truncate text-xs text-muted-foreground">배경색</span>
|
||||
|
|
@ -721,7 +721,7 @@ export const V2RepeatContainerConfigPanel: React.FC<V2RepeatContainerConfigPanel
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">클릭 가능</p>
|
||||
|
|
@ -977,7 +977,7 @@ function SlotChildrenSection({
|
|||
</p>
|
||||
|
||||
{children.length > 0 ? (
|
||||
<div className="max-h-[250px] space-y-2 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{children.map((child, index) => {
|
||||
const isExpanded = expandedIds.has(child.id);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1145,7 +1145,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-3 space-y-1">
|
||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-1">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<p className="text-sm">추가 버튼</p>
|
||||
|
|
@ -1391,7 +1391,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
) : sourceTableColumns.length === 0 ? (
|
||||
<p className="text-muted-foreground py-2 text-xs">컬럼 정보가 없습니다</p>
|
||||
) : (
|
||||
<div className="max-h-[150px] space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2">
|
||||
<div className="space-y-0.5 rounded-md border border-primary/20 bg-primary/10/30 p-2">
|
||||
{sourceTableColumns.map((column) => (
|
||||
<div
|
||||
key={`source-${column.columnName}`}
|
||||
|
|
@ -1428,7 +1428,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
컬럼 정보가 없습니다
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[250px] space-y-0.5 overflow-y-auto rounded-md border p-2">
|
||||
<div className="space-y-0.5 rounded-md border p-2">
|
||||
{inputableColumns.map((column) => (
|
||||
<div
|
||||
key={`input-${column.columnName}`}
|
||||
|
|
@ -1475,7 +1475,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-3 space-y-3">
|
||||
<p className="text-[10px] text-muted-foreground">드래그로 순서 변경, 클릭하여 상세 설정</p>
|
||||
<div className="max-h-[250px] space-y-1 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{config.columns.map((col, index) => (
|
||||
<div key={col.key} className="space-y-1">
|
||||
{/* 컬럼 헤더 (드래그 가능) */}
|
||||
|
|
@ -1826,7 +1826,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[250px] space-y-2 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{calculationRules.map((rule) => (
|
||||
<div key={rule.id} className="space-y-1 rounded border p-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -1988,7 +1988,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] space-y-0.5 overflow-y-auto rounded-b-md border border-t-0 border-primary/20 bg-primary/5 p-2">
|
||||
<div className="space-y-0.5 rounded-b-md border border-t-0 border-primary/20 bg-primary/5 p-2">
|
||||
{joinTable.availableColumns.map((column, colIndex) => {
|
||||
const isActive = isEntityJoinColumnActive(
|
||||
joinTable.tableName,
|
||||
|
|
@ -2054,7 +2054,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] space-y-1 overflow-y-auto rounded-b-md border border-t-0 p-2">
|
||||
<div className="space-y-1 rounded-b-md border border-t-0 p-2">
|
||||
{config.entityJoins.map((join, idx) => (
|
||||
<div key={idx} className="flex items-center gap-1 rounded border bg-muted/30 px-2 py-1 text-[10px]">
|
||||
<Database className="h-3 w-3 text-primary flex-shrink-0" />
|
||||
|
|
|
|||
|
|
@ -807,7 +807,7 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
|
|||
</div>
|
||||
|
||||
{displayColumns.length > 0 && (
|
||||
<div className="max-h-[180px] space-y-0.5 overflow-y-auto">
|
||||
<div className="space-y-0.5">
|
||||
{displayColumns.map((col) => (
|
||||
<div
|
||||
key={col.name}
|
||||
|
|
@ -927,7 +927,7 @@ export const V2SelectedItemsDetailInputConfigPanel: React.FC<
|
|||
</p>
|
||||
|
||||
{localFields.length > 0 && (
|
||||
<div className="max-h-[300px] space-y-1 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{localFields.map((field, index) => (
|
||||
<Collapsible key={index}>
|
||||
<div className="rounded-md border">
|
||||
|
|
|
|||
|
|
@ -1036,7 +1036,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC<
|
|||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-4 rounded-b-lg border border-t-0 p-4">
|
||||
{/* 좌측 패널 제목 */}
|
||||
<div className="space-y-1.5">
|
||||
|
|
@ -1281,7 +1281,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC<
|
|||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-4 rounded-b-lg border border-t-0 p-4">
|
||||
{/* 우측 패널 제목 */}
|
||||
<div className="space-y-1.5">
|
||||
|
|
@ -2079,7 +2079,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC<
|
|||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-3 rounded-b-lg border border-t-0 p-4">
|
||||
{/* 탭 목록 */}
|
||||
{(config.rightPanel?.additionalTabs || []).map(
|
||||
|
|
@ -2296,7 +2296,7 @@ export const V2SplitPanelLayoutConfigPanel: React.FC<
|
|||
/>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="space-y-3 rounded-b-lg border border-t-0 p-4">
|
||||
<SwitchRow
|
||||
label="선택 동기화"
|
||||
|
|
|
|||
|
|
@ -556,7 +556,7 @@ export const V2StatusCountConfigPanel: React.FC<V2StatusCountConfigPanelProps> =
|
|||
<p className="text-xs text-muted-foreground">위의 추가 버튼으로 항목을 만들어보세요</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[250px] space-y-2 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{items.map((item: StatusCountItem, i: number) => (
|
||||
<div key={i} className="space-y-1.5 rounded-md border p-2.5">
|
||||
{/* 첫 번째 줄: 상태값 + 삭제 */}
|
||||
|
|
|
|||
|
|
@ -570,7 +570,7 @@ export const V2TableGroupedConfigPanel: React.FC<V2TableGroupedConfigPanelProps>
|
|||
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", displayOpen && "rotate-180")} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
|
||||
{/* 체크박스 */}
|
||||
|
|
@ -671,7 +671,7 @@ export const V2TableGroupedConfigPanel: React.FC<V2TableGroupedConfigPanelProps>
|
|||
<ChevronDown className={cn("h-4 w-4 text-muted-foreground transition-transform duration-200", linkedOpen && "rotate-180")} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="max-h-[250px] overflow-y-auto">
|
||||
<CollapsibleContent>
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -790,7 +790,7 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
|
|||
placeholder="컬럼 검색..."
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<div className="max-h-[250px] space-y-0.5 overflow-y-auto">
|
||||
<div className="space-y-0.5">
|
||||
{availableColumns
|
||||
.filter((column) => {
|
||||
if (!columnSearchText) return true;
|
||||
|
|
@ -919,7 +919,7 @@ export const V2TableListConfigPanel: React.FC<V2TableListConfigPanelProps> = ({
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[150px] space-y-0.5 overflow-y-auto rounded-b-md border border-t-0 border-primary/20 bg-primary/5 p-2">
|
||||
<div className="space-y-0.5 rounded-b-md border border-t-0 border-primary/20 bg-primary/5 p-2">
|
||||
{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<V2TableListConfigPanelProps> = ({
|
|||
items={(config.columns || []).map((c) => c.columnName)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="max-h-[300px] space-y-1 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{(config.columns || []).map((column, idx) => {
|
||||
const resolvedLabel =
|
||||
column.displayName && column.displayName !== column.columnName
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
{/* 스케줄 타입 */}
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-muted-foreground truncate">스케줄 타입</span>
|
||||
|
|
@ -488,7 +488,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
{/* 소스 테이블 Combobox */}
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground truncate">소스 테이블 (수주/작업요청 등)</span>
|
||||
|
|
@ -654,7 +654,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
{/* 리소스 테이블 Combobox */}
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs text-muted-foreground truncate">리소스 테이블</span>
|
||||
|
|
@ -800,7 +800,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
{/* 줌 레벨 */}
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<span className="text-xs text-muted-foreground truncate">기본 줌 레벨</span>
|
||||
|
|
@ -1063,7 +1063,7 @@ export const V2TimelineSchedulerConfigPanel: React.FC<V2TimelineSchedulerConfigP
|
|||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="max-h-[250px] overflow-y-auto rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
|
||||
{[
|
||||
{ key: "planned", label: "계획됨", defaultColor: "#3b82f6" },
|
||||
{ key: "in_progress", label: "진행중", defaultColor: "#f59e0b" },
|
||||
|
|
|
|||
|
|
@ -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 } });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue