전체실행조건 웹 타입별 조건분기
This commit is contained in:
parent
81d760532b
commit
43e335d271
|
|
@ -31,6 +31,7 @@ import layoutRoutes from "./routes/layoutRoutes";
|
|||
import dataRoutes from "./routes/dataRoutes";
|
||||
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
|
||||
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
||||
import entityReferenceRoutes from "./routes/entityReferenceRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
|
|
@ -125,6 +126,7 @@ app.use("/api/screen", screenStandardRoutes);
|
|||
app.use("/api/data", dataRoutes);
|
||||
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
|
||||
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
||||
app.use("/api/entity-reference", entityReferenceRoutes);
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
import { Request, Response } from "express";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface EntityReferenceOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface EntityReferenceData {
|
||||
options: EntityReferenceOption[];
|
||||
referenceInfo: {
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
displayColumn: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeReferenceData {
|
||||
options: EntityReferenceOption[];
|
||||
codeCategory: string;
|
||||
}
|
||||
|
||||
export class EntityReferenceController {
|
||||
/**
|
||||
* 엔티티 참조 데이터 조회
|
||||
* GET /api/entity-reference/:tableName/:columnName
|
||||
*/
|
||||
static async getEntityReferenceData(req: Request, res: Response) {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { limit = 100, search } = req.query;
|
||||
|
||||
logger.info(`엔티티 참조 데이터 조회 요청: ${tableName}.${columnName}`, {
|
||||
limit,
|
||||
search,
|
||||
});
|
||||
|
||||
// 컬럼 정보 조회
|
||||
const columnInfo = await prisma.column_labels.findFirst({
|
||||
where: {
|
||||
table_name: tableName,
|
||||
column_name: columnName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!columnInfo) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `컬럼 '${tableName}.${columnName}'을 찾을 수 없습니다.`,
|
||||
});
|
||||
}
|
||||
|
||||
// detailSettings에서 참조 테이블 정보 추출
|
||||
let referenceTable = "";
|
||||
let displayColumn = "name";
|
||||
|
||||
try {
|
||||
if (columnInfo.detail_settings) {
|
||||
const detailSettings = JSON.parse(columnInfo.detail_settings);
|
||||
referenceTable = detailSettings.referenceTable || "";
|
||||
displayColumn = detailSettings.displayColumn || "name";
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("detailSettings 파싱 실패:", error);
|
||||
}
|
||||
|
||||
if (!referenceTable) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `컬럼 '${columnName}'에 참조 테이블이 설정되지 않았습니다.`,
|
||||
});
|
||||
}
|
||||
|
||||
// 동적 쿼리로 참조 데이터 조회
|
||||
let query = `SELECT id, ${displayColumn} as display_name FROM ${referenceTable}`;
|
||||
const queryParams: any[] = [];
|
||||
|
||||
// 검색 조건 추가
|
||||
if (search) {
|
||||
query += ` WHERE ${displayColumn} ILIKE $1`;
|
||||
queryParams.push(`%${search}%`);
|
||||
}
|
||||
|
||||
query += ` ORDER BY ${displayColumn} LIMIT $${queryParams.length + 1}`;
|
||||
queryParams.push(Number(limit));
|
||||
|
||||
const referenceData = await prisma.$queryRawUnsafe(query, ...queryParams);
|
||||
|
||||
// 옵션 형태로 변환
|
||||
const options: EntityReferenceOption[] = (referenceData as any[]).map(
|
||||
(row) => ({
|
||||
value: String(row.id),
|
||||
label: String(row.display_name || row.id),
|
||||
})
|
||||
);
|
||||
|
||||
const result: EntityReferenceData = {
|
||||
options,
|
||||
referenceInfo: {
|
||||
referenceTable,
|
||||
referenceColumn: "id",
|
||||
displayColumn,
|
||||
},
|
||||
};
|
||||
|
||||
logger.info(`엔티티 참조 데이터 조회 완료: ${options.length}개 항목`);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "엔티티 참조 데이터 조회 성공",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("엔티티 참조 데이터 조회 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "엔티티 참조 데이터 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 코드 데이터 조회
|
||||
* GET /api/entity-reference/code/:codeCategory
|
||||
*/
|
||||
static async getCodeData(req: Request, res: Response) {
|
||||
try {
|
||||
const { codeCategory } = req.params;
|
||||
const { limit = 100, search } = req.query;
|
||||
|
||||
logger.info(`공통 코드 데이터 조회 요청: ${codeCategory}`, {
|
||||
limit,
|
||||
search,
|
||||
});
|
||||
|
||||
// code_info 테이블에서 공통 코드 조회
|
||||
let whereClause: any = {
|
||||
code_category: codeCategory,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
// 검색 조건 추가
|
||||
if (search) {
|
||||
whereClause.OR = [
|
||||
{ code_value: { contains: String(search), mode: "insensitive" } },
|
||||
{ code_name: { contains: String(search), mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
const codes = await prisma.code_info.findMany({
|
||||
where: whereClause,
|
||||
orderBy: { sort_order: "asc" },
|
||||
take: Number(limit),
|
||||
select: {
|
||||
code_value: true,
|
||||
code_name: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 옵션 형태로 변환
|
||||
const options: EntityReferenceOption[] = codes.map((code: any) => ({
|
||||
value: code.code_value || "",
|
||||
label: code.code_name || "",
|
||||
}));
|
||||
|
||||
const result: CodeReferenceData = {
|
||||
options,
|
||||
codeCategory,
|
||||
};
|
||||
|
||||
logger.info(`공통 코드 데이터 조회 완료: ${options.length}개 항목`);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "공통 코드 데이터 조회 성공",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("공통 코드 데이터 조회 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "공통 코드 데이터 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Router } from "express";
|
||||
import { EntityReferenceController } from "../controllers/entityReferenceController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/entity-reference/code/:codeCategory
|
||||
* 공통 코드 데이터 조회
|
||||
*/
|
||||
router.get(
|
||||
"/code/:codeCategory",
|
||||
authenticateToken,
|
||||
EntityReferenceController.getCodeData
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/entity-reference/:tableName/:columnName
|
||||
* 엔티티 참조 데이터 조회
|
||||
*/
|
||||
router.get(
|
||||
"/:tableName/:columnName",
|
||||
authenticateToken,
|
||||
EntityReferenceController.getEntityReferenceData
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
@ -675,6 +675,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
<ConditionalSettings
|
||||
conditions={conditions}
|
||||
fromTableColumns={fromTableColumns}
|
||||
fromTableName={selectedFromTable || connection.fromNode.tableName}
|
||||
onAddCondition={addCondition}
|
||||
onAddGroupStart={addGroupStart}
|
||||
onAddGroupEnd={addGroupEnd}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -16,6 +16,7 @@ import { Trash2 } from "lucide-react";
|
|||
import { ConditionNode, ColumnInfo } from "@/lib/api/dataflow";
|
||||
import { DataSaveSettings } from "@/types/connectionTypes";
|
||||
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
||||
import { WebTypeInput } from "../condition/WebTypeInput";
|
||||
|
||||
interface ActionConditionRendererProps {
|
||||
condition: ConditionNode;
|
||||
|
|
@ -70,32 +71,37 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
|||
// 선택된 테이블 타입에 따라 컬럼 찾기
|
||||
const targetColumns = condition.tableType === "from" ? fromTableColumns : toTableColumns;
|
||||
const selectedColumn = targetColumns.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) => updateCondition("value", value)}>
|
||||
<SelectTrigger className="h-6 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) => updateCondition("value", e.target.value)}
|
||||
className="h-6 flex-1 text-xs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 테이블명 정보를 포함한 컬럼 객체 생성
|
||||
const tableName = condition.tableType === "from" ? fromTableName : toTableName;
|
||||
const columnWithTableName = {
|
||||
...selectedColumn,
|
||||
tableName: tableName,
|
||||
};
|
||||
|
||||
// WebType 기반 input 사용
|
||||
return (
|
||||
<WebTypeInput
|
||||
column={columnWithTableName}
|
||||
value={String(condition.value || "")}
|
||||
onChange={(value) => updateCondition("value", value)}
|
||||
className="h-6 flex-1 text-xs"
|
||||
placeholder="값"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 그룹 시작 렌더링
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import { apiClient } from "./client";
|
||||
|
||||
export interface EntityReferenceOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface EntityReferenceData {
|
||||
options: EntityReferenceOption[];
|
||||
referenceInfo: {
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
displayColumn: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeReferenceData {
|
||||
options: EntityReferenceOption[];
|
||||
codeCategory: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class EntityReferenceAPI {
|
||||
/**
|
||||
* 엔티티 참조 데이터 조회
|
||||
*/
|
||||
static async getEntityReferenceData(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
options: {
|
||||
limit?: number;
|
||||
search?: string;
|
||||
} = {},
|
||||
): Promise<EntityReferenceData> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (options.limit) params.append("limit", options.limit.toString());
|
||||
if (options.search) params.append("search", options.search);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/entity-reference/${tableName}/${columnName}${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
const response = await apiClient.get<ApiResponse<EntityReferenceData>>(url);
|
||||
|
||||
if (!response.data.success || !response.data.data) {
|
||||
throw new Error(response.data.message || "엔티티 참조 데이터 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.error("엔티티 참조 데이터 조회 오류:", error);
|
||||
throw error instanceof Error ? error : new Error("엔티티 참조 데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 코드 데이터 조회
|
||||
*/
|
||||
static async getCodeReferenceData(
|
||||
codeCategory: string,
|
||||
options: {
|
||||
limit?: number;
|
||||
search?: string;
|
||||
} = {},
|
||||
): Promise<CodeReferenceData> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (options.limit) params.append("limit", options.limit.toString());
|
||||
if (options.search) params.append("search", options.search);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/entity-reference/code/${codeCategory}${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
const response = await apiClient.get<ApiResponse<CodeReferenceData>>(url);
|
||||
|
||||
if (!response.data.success || !response.data.data) {
|
||||
throw new Error(response.data.message || "공통 코드 데이터 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.error("공통 코드 데이터 조회 오류:", error);
|
||||
throw error instanceof Error ? error : new Error("공통 코드 데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue