전체실행조건 웹 타입별 조건분기

This commit is contained in:
hyeonsu
2025-09-21 09:53:05 +09:00
parent 81d760532b
commit 43e335d271
9 changed files with 686 additions and 37 deletions

View File

@@ -7,10 +7,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Trash2 } from "lucide-react";
import { ConditionNode, ColumnInfo } from "@/lib/api/dataflow";
import { getInputTypeForDataType } from "@/utils/connectionUtils";
import { WebTypeInput } from "./WebTypeInput";
interface ConditionRendererProps {
conditions: ConditionNode[];
fromTableColumns: ColumnInfo[];
fromTableName?: string;
onUpdateCondition: (index: number, field: keyof ConditionNode, value: string) => void;
onRemoveCondition: (index: number) => void;
getCurrentGroupLevel: (index: number) => number;
@@ -19,41 +21,43 @@ interface ConditionRendererProps {
export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
conditions,
fromTableColumns,
fromTableName,
onUpdateCondition,
onRemoveCondition,
getCurrentGroupLevel,
}) => {
const renderConditionValue = (condition: ConditionNode, index: number) => {
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
const inputType = getInputTypeForDataType(dataType);
if (dataType.includes("bool")) {
return (
<Select
value={String(condition.value || "")}
onValueChange={(value) => onUpdateCondition(index, "value", value)}
>
<SelectTrigger className="h-8 flex-1 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">TRUE</SelectItem>
<SelectItem value="false">FALSE</SelectItem>
</SelectContent>
</Select>
);
} else {
if (!selectedColumn) {
// 컬럼이 선택되지 않은 경우 기본 input
return (
<Input
type={inputType}
placeholder={inputType === "number" ? "숫자" : "값"}
type="text"
placeholder="값"
value={String(condition.value || "")}
onChange={(e) => onUpdateCondition(index, "value", e.target.value)}
className="h-8 flex-1 text-xs"
/>
);
}
// 테이블명 정보를 포함한 컬럼 객체 생성
const columnWithTableName = {
...selectedColumn,
tableName: fromTableName,
};
// WebType 기반 input 사용
return (
<WebTypeInput
column={columnWithTableName}
value={String(condition.value || "")}
onChange={(value) => onUpdateCondition(index, "value", value)}
className="h-8 flex-1 text-xs"
placeholder="값"
/>
);
};
return (

View File

@@ -10,6 +10,7 @@ import { ConditionRenderer } from "./ConditionRenderer";
interface ConditionalSettingsProps {
conditions: ConditionNode[];
fromTableColumns: ColumnInfo[];
fromTableName?: string;
onAddCondition: () => void;
onAddGroupStart: () => void;
onAddGroupEnd: () => void;
@@ -21,6 +22,7 @@ interface ConditionalSettingsProps {
export const ConditionalSettings: React.FC<ConditionalSettingsProps> = ({
conditions,
fromTableColumns,
fromTableName,
onAddCondition,
onAddGroupStart,
onAddGroupEnd,
@@ -57,6 +59,7 @@ export const ConditionalSettings: React.FC<ConditionalSettingsProps> = ({
<ConditionRenderer
conditions={conditions}
fromTableColumns={fromTableColumns}
fromTableName={fromTableName}
onUpdateCondition={onUpdateCondition}
onRemoveCondition={onRemoveCondition}
getCurrentGroupLevel={getCurrentGroupLevel}

View File

@@ -0,0 +1,322 @@
"use client";
import React, { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { CalendarIcon, Upload, Loader2 } from "lucide-react";
import { format } from "date-fns";
import { ko } from "date-fns/locale";
import { ColumnInfo } from "@/lib/api/dataflow";
import { EntityReferenceAPI, EntityReferenceOption } from "@/lib/api/entityReference";
interface WebTypeInputProps {
column: ColumnInfo;
value: string | undefined;
onChange: (value: string) => void;
className?: string;
placeholder?: string;
}
export const WebTypeInput: React.FC<WebTypeInputProps> = ({ column, value, onChange, className = "", placeholder }) => {
const webType = column.webType || "text";
const [entityOptions, setEntityOptions] = useState<EntityReferenceOption[]>([]);
const [codeOptions, setCodeOptions] = useState<EntityReferenceOption[]>([]);
const [loading, setLoading] = useState(false);
// detailSettings 안전하게 파싱
let detailSettings: any = {};
let fallbackCodeCategory = "";
if (column.detailSettings && typeof column.detailSettings === "string") {
// JSON 형태인지 확인 ('{' 또는 '[' 로 시작하는지)
const trimmed = column.detailSettings.trim();
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
try {
detailSettings = JSON.parse(column.detailSettings);
} catch (error) {
console.warn(`detailSettings JSON 파싱 실패 (${column.columnName}):`, column.detailSettings, error);
detailSettings = {};
}
} else {
// JSON이 아닌 일반 문자열인 경우, code 타입이면 codeCategory로 사용
if (webType === "code") {
// "공통코드: 상태" 형태에서 실제 코드 추출 시도
if (column.detailSettings.includes(":")) {
const parts = column.detailSettings.split(":");
if (parts.length >= 2) {
fallbackCodeCategory = parts[1].trim();
} else {
fallbackCodeCategory = column.detailSettings;
}
} else {
fallbackCodeCategory = column.detailSettings;
}
console.log(`📝 detailSettings에서 codeCategory 추출: "${column.detailSettings}" -> "${fallbackCodeCategory}"`);
}
detailSettings = {};
}
} else if (column.detailSettings && typeof column.detailSettings === "object") {
detailSettings = column.detailSettings;
}
// Entity 타입일 때 참조 데이터 로드
useEffect(() => {
console.log("🔍 WebTypeInput useEffect:", {
webType,
columnName: column.columnName,
tableName: column.tableName,
referenceTable: column.referenceTable,
displayColumn: column.displayColumn,
codeCategory: column.codeCategory,
});
if (webType === "entity" && column.tableName && column.columnName) {
console.log("🚀 Entity 데이터 로드 시작:", column.tableName, column.columnName);
loadEntityData();
} else if (webType === "code" && (column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory)) {
const codeCategory = column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory;
console.log("🚀 Code 데이터 로드 시작:", codeCategory);
loadCodeData();
} else {
console.log("❌ 조건 불충족 - API 호출 안함");
}
}, [webType, column.tableName, column.columnName, column.codeCategory, fallbackCodeCategory]);
const loadEntityData = async () => {
try {
setLoading(true);
console.log("📡 Entity API 호출:", column.tableName, column.columnName);
const data = await EntityReferenceAPI.getEntityReferenceData(column.tableName, column.columnName, { limit: 100 });
console.log("✅ Entity API 응답:", data);
setEntityOptions(data.options);
} catch (error) {
console.error("❌ 엔티티 참조 데이터 로드 실패:", error);
setEntityOptions([]);
} finally {
setLoading(false);
}
};
const loadCodeData = async () => {
try {
setLoading(true);
const codeCategory = column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory;
if (codeCategory) {
console.log("📡 Code API 호출:", codeCategory);
const data = await EntityReferenceAPI.getCodeReferenceData(codeCategory, { limit: 100 });
console.log("✅ Code API 응답:", data);
setCodeOptions(data.options);
} else {
console.warn("⚠️ codeCategory가 없어서 API 호출 안함");
}
} catch (error) {
console.error("공통 코드 데이터 로드 실패:", error);
setCodeOptions([]);
} finally {
setLoading(false);
}
};
// 공통 props
const commonProps = {
value: value || "",
className,
};
// WebType별 렌더링
switch (webType) {
case "text":
return (
<Input
{...commonProps}
type="text"
placeholder={placeholder || "텍스트 입력"}
onChange={(e) => onChange(e.target.value)}
/>
);
case "number":
return (
<Input
{...commonProps}
type="number"
placeholder={placeholder || "숫자 입력"}
onChange={(e) => onChange(e.target.value)}
min={detailSettings.min}
max={detailSettings.max}
step={detailSettings.step || "any"}
/>
);
case "date":
const dateValue = value ? new Date(value) : undefined;
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={`justify-start text-left font-normal ${className} ${!value && "text-muted-foreground"}`}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{dateValue ? format(dateValue, "PPP", { locale: ko }) : placeholder || "날짜 선택"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={dateValue}
onSelect={(date) => onChange(date ? format(date, "yyyy-MM-dd") : "")}
initialFocus
/>
</PopoverContent>
</Popover>
);
case "textarea":
return (
<Textarea
{...commonProps}
placeholder={placeholder || "여러 줄 텍스트 입력"}
onChange={(e) => onChange(e.target.value)}
rows={detailSettings.rows || 3}
/>
);
case "select":
const selectOptions = detailSettings.options || [];
return (
<Select value={value || ""} onValueChange={onChange}>
<SelectTrigger className={className}>
<SelectValue placeholder={placeholder || "선택하세요"} />
</SelectTrigger>
<SelectContent>
{selectOptions.map((option: any) => (
<SelectItem key={option.value} value={option.value}>
{option.label || option.value}
</SelectItem>
))}
</SelectContent>
</Select>
);
case "checkbox":
return (
<div className={`flex items-center space-x-2 ${className}`}>
<Checkbox
id={`checkbox-${column.columnName}`}
checked={value === "true" || value === "1"}
onCheckedChange={(checked) => onChange(checked ? "true" : "false")}
/>
<Label htmlFor={`checkbox-${column.columnName}`} className="text-sm">
{detailSettings.label || column.columnLabel || column.columnName}
</Label>
</div>
);
case "radio":
const radioOptions = detailSettings.options || [];
return (
<RadioGroup value={value || ""} onValueChange={onChange} className={className}>
{radioOptions.map((option: any) => (
<div key={option.value} className="flex items-center space-x-2">
<RadioGroupItem value={option.value} id={`radio-${column.columnName}-${option.value}`} />
<Label htmlFor={`radio-${column.columnName}-${option.value}`} className="text-sm">
{option.label || option.value}
</Label>
</div>
))}
</RadioGroup>
);
case "code":
// 공통코드 선택 - 실제 API에서 코드 목록 가져옴
const codeCategory = column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory;
return (
<Select value={value || ""} onValueChange={onChange} disabled={loading}>
<SelectTrigger className={className}>
<SelectValue placeholder={loading ? "코드 로딩 중..." : placeholder || `${codeCategory || "코드"} 선택`} />
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
</SelectTrigger>
<SelectContent>
{codeOptions.length === 0 && !loading ? (
<SelectItem value="__no_data__" disabled>
</SelectItem>
) : (
codeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))
)}
</SelectContent>
</Select>
);
case "entity":
// 엔티티 참조 - 실제 참조 테이블에서 데이터 가져옴
const referenceTable = column.referenceTable || (detailSettings as any).referenceTable;
return (
<Select value={value || ""} onValueChange={onChange} disabled={loading}>
<SelectTrigger className={className}>
<SelectValue placeholder={loading ? "데이터 로딩 중..." : placeholder || `${referenceTable} 선택`} />
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
</SelectTrigger>
<SelectContent>
{entityOptions.length === 0 && !loading ? (
<SelectItem value="__no_data__" disabled>
</SelectItem>
) : (
entityOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))
)}
</SelectContent>
</Select>
);
case "file":
return (
<div className={`space-y-2 ${className}`}>
<Input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
onChange(file.name); // 실제로는 파일 업로드 처리 필요
}
}}
accept={detailSettings.accept}
multiple={detailSettings.multiple}
/>
{value && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Upload className="h-4 w-4" />
<span> : {value}</span>
</div>
)}
</div>
);
default:
// 기본적으로 text input 사용
return (
<Input
{...commonProps}
type="text"
placeholder={placeholder || "값 입력"}
onChange={(e) => onChange(e.target.value)}
/>
);
}
};