Compare commits
No commits in common. "1436c908a6080e8b9e639077e8144842890e6ddb" and "dfda1bcc24c600a450aaa728e6c9daf5788f2cbf" have entirely different histories.
1436c908a6
...
dfda1bcc24
|
|
@ -32,7 +32,6 @@ import dataRoutes from "./routes/dataRoutes";
|
||||||
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
|
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
|
||||||
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
||||||
import ddlRoutes from "./routes/ddlRoutes";
|
import ddlRoutes from "./routes/ddlRoutes";
|
||||||
import entityReferenceRoutes from "./routes/entityReferenceRoutes";
|
|
||||||
// import userRoutes from './routes/userRoutes';
|
// import userRoutes from './routes/userRoutes';
|
||||||
// import menuRoutes from './routes/menuRoutes';
|
// import menuRoutes from './routes/menuRoutes';
|
||||||
|
|
||||||
|
|
@ -128,7 +127,6 @@ app.use("/api/data", dataRoutes);
|
||||||
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
|
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
|
||||||
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
||||||
app.use("/api/ddl", ddlRoutes);
|
app.use("/api/ddl", ddlRoutes);
|
||||||
app.use("/api/entity-reference", entityReferenceRoutes);
|
|
||||||
// app.use('/api/users', userRoutes);
|
// app.use('/api/users', userRoutes);
|
||||||
// app.use('/api/menus', menuRoutes);
|
// app.use('/api/menus', menuRoutes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,36 +143,16 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
|
||||||
message: "관계도가 성공적으로 생성되었습니다.",
|
message: "관계도가 성공적으로 생성되었습니다.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 디버깅을 위한 에러 정보 출력
|
logger.error("관계도 생성 실패:", error);
|
||||||
logger.error("에러 디버깅:", {
|
|
||||||
errorType: typeof error,
|
|
||||||
errorCode: (error as any)?.code,
|
|
||||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
errorName: (error as any)?.name,
|
|
||||||
errorMeta: (error as any)?.meta,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 중복 이름 에러인지 먼저 확인 (로그 출력 전에)
|
// 중복 이름 에러 처리
|
||||||
const isDuplicateError =
|
if (error instanceof Error && error.message.includes("unique constraint")) {
|
||||||
(error && typeof error === "object" && (error as any).code === "P2002") || // Prisma unique constraint error code
|
|
||||||
(error instanceof Error &&
|
|
||||||
(error.message.includes("unique constraint") ||
|
|
||||||
error.message.includes("Unique constraint") ||
|
|
||||||
error.message.includes("duplicate key") ||
|
|
||||||
error.message.includes("UNIQUE constraint failed") ||
|
|
||||||
error.message.includes("unique_diagram_name_per_company")));
|
|
||||||
|
|
||||||
if (isDuplicateError) {
|
|
||||||
// 중복 에러는 콘솔에 로그 출력하지 않음
|
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "중복된 이름입니다.",
|
message: "이미 존재하는 관계도 이름입니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 다른 에러만 로그 출력
|
|
||||||
logger.error("관계도 생성 실패:", error);
|
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "관계도 생성 중 오류가 발생했습니다.",
|
message: "관계도 생성 중 오류가 발생했습니다.",
|
||||||
|
|
@ -234,25 +214,6 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => {
|
||||||
message: "관계도가 성공적으로 수정되었습니다.",
|
message: "관계도가 성공적으로 수정되었습니다.",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 중복 이름 에러인지 먼저 확인 (로그 출력 전에)
|
|
||||||
const isDuplicateError =
|
|
||||||
(error && typeof error === "object" && (error as any).code === "P2002") || // Prisma unique constraint error code
|
|
||||||
(error instanceof Error &&
|
|
||||||
(error.message.includes("unique constraint") ||
|
|
||||||
error.message.includes("Unique constraint") ||
|
|
||||||
error.message.includes("duplicate key") ||
|
|
||||||
error.message.includes("UNIQUE constraint failed") ||
|
|
||||||
error.message.includes("unique_diagram_name_per_company")));
|
|
||||||
|
|
||||||
if (isDuplicateError) {
|
|
||||||
// 중복 에러는 콘솔에 로그 출력하지 않음
|
|
||||||
return res.status(409).json({
|
|
||||||
success: false,
|
|
||||||
message: "중복된 이름입니다.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다른 에러만 로그 출력
|
|
||||||
logger.error("관계도 수정 실패:", error);
|
logger.error("관계도 수정 실패:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
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}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// webType 확인
|
|
||||||
if (columnInfo.web_type !== "entity") {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: `컬럼 '${tableName}.${columnName}'은 entity 타입이 아닙니다. webType: ${columnInfo.web_type}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// column_labels에서 직접 참조 정보 가져오기
|
|
||||||
const referenceTable = columnInfo.reference_table;
|
|
||||||
const referenceColumn = columnInfo.reference_column;
|
|
||||||
const displayColumn = columnInfo.display_column || "name";
|
|
||||||
|
|
||||||
// entity 타입인데 참조 테이블 정보가 없으면 오류
|
|
||||||
if (!referenceTable || !referenceColumn) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: `Entity 타입 컬럼 '${tableName}.${columnName}'에 참조 테이블 정보가 설정되지 않았습니다. column_labels에서 reference_table과 reference_column을 확인해주세요.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 참조 테이블이 실제로 존재하는지 확인
|
|
||||||
try {
|
|
||||||
await prisma.$queryRawUnsafe(`SELECT 1 FROM ${referenceTable} LIMIT 1`);
|
|
||||||
logger.info(
|
|
||||||
`Entity 참조 설정: ${tableName}.${columnName} -> ${referenceTable}.${referenceColumn} (display: ${displayColumn})`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`참조 테이블 '${referenceTable}'이 존재하지 않습니다:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: `참조 테이블 '${referenceTable}'이 존재하지 않습니다. column_labels의 reference_table 설정을 확인해주세요.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 동적 쿼리로 참조 데이터 조회
|
|
||||||
let query = `SELECT ${referenceColumn}, ${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));
|
|
||||||
|
|
||||||
logger.info(`실행할 쿼리: ${query}`, {
|
|
||||||
queryParams,
|
|
||||||
referenceTable,
|
|
||||||
referenceColumn,
|
|
||||||
displayColumn,
|
|
||||||
});
|
|
||||||
|
|
||||||
const referenceData = await prisma.$queryRawUnsafe(query, ...queryParams);
|
|
||||||
|
|
||||||
// 옵션 형태로 변환
|
|
||||||
const options: EntityReferenceOption[] = (referenceData as any[]).map(
|
|
||||||
(row) => ({
|
|
||||||
value: String(row[referenceColumn]),
|
|
||||||
label: String(row.display_name || row[referenceColumn]),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info(`엔티티 참조 데이터 조회 완료: ${options.length}개 항목`);
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
options,
|
|
||||||
referenceInfo: {
|
|
||||||
referenceTable,
|
|
||||||
referenceColumn,
|
|
||||||
displayColumn,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("엔티티 참조 데이터 조회 실패:", error);
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "엔티티 참조 데이터 조회 중 오류가 발생했습니다.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 공통 코드 데이터 조회
|
|
||||||
* 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 whereCondition: any = {
|
|
||||||
code_category: codeCategory,
|
|
||||||
is_active: "Y",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
whereCondition.code_name = {
|
|
||||||
contains: String(search),
|
|
||||||
mode: "insensitive",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeData = await prisma.code_info.findMany({
|
|
||||||
where: whereCondition,
|
|
||||||
select: {
|
|
||||||
code_value: true,
|
|
||||||
code_name: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
code_name: "asc",
|
|
||||||
},
|
|
||||||
take: Number(limit),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 옵션 형태로 변환
|
|
||||||
const options: EntityReferenceOption[] = codeData.map((code) => ({
|
|
||||||
value: code.code_value,
|
|
||||||
label: code.code_name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
logger.info(`공통 코드 데이터 조회 완료: ${options.length}개 항목`);
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
options,
|
|
||||||
codeCategory,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("공통 코드 데이터 조회 실패:", error);
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: "공통 코드 데이터 조회 중 오류가 발생했습니다.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -19,7 +19,6 @@ export interface ControlAction {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
actionType: "insert" | "update" | "delete";
|
actionType: "insert" | "update" | "delete";
|
||||||
logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외)
|
|
||||||
conditions: ControlCondition[];
|
conditions: ControlCondition[];
|
||||||
fieldMappings: {
|
fieldMappings: {
|
||||||
sourceField?: string;
|
sourceField?: string;
|
||||||
|
|
@ -137,41 +136,17 @@ export class DataflowControlService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 액션 실행 (논리 연산자 지원)
|
// 액션 실행
|
||||||
const executedActions = [];
|
const executedActions = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
let previousActionSuccess = false;
|
|
||||||
let shouldSkipRemainingActions = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < targetPlan.actions.length; i++) {
|
|
||||||
const action = targetPlan.actions[i];
|
|
||||||
|
|
||||||
|
for (const action of targetPlan.actions) {
|
||||||
try {
|
try {
|
||||||
// 논리 연산자에 따른 실행 여부 결정
|
|
||||||
if (
|
|
||||||
i > 0 &&
|
|
||||||
action.logicalOperator === "OR" &&
|
|
||||||
previousActionSuccess
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`⏭️ OR 조건으로 인해 액션 건너뛰기: ${action.name} (이전 액션 성공)`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSkipRemainingActions && action.logicalOperator === "AND") {
|
|
||||||
console.log(
|
|
||||||
`⏭️ 이전 액션 실패로 인해 AND 체인 액션 건너뛰기: ${action.name}`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`⚡ 액션 실행: ${action.name} (${action.actionType})`);
|
console.log(`⚡ 액션 실행: ${action.name} (${action.actionType})`);
|
||||||
console.log(`📋 액션 상세 정보:`, {
|
console.log(`📋 액션 상세 정보:`, {
|
||||||
actionId: action.id,
|
actionId: action.id,
|
||||||
actionName: action.name,
|
actionName: action.name,
|
||||||
actionType: action.actionType,
|
actionType: action.actionType,
|
||||||
logicalOperator: action.logicalOperator,
|
|
||||||
conditions: action.conditions,
|
conditions: action.conditions,
|
||||||
fieldMappings: action.fieldMappings,
|
fieldMappings: action.fieldMappings,
|
||||||
});
|
});
|
||||||
|
|
@ -188,10 +163,6 @@ export class DataflowControlService {
|
||||||
console.log(
|
console.log(
|
||||||
`⚠️ 액션 조건 미충족: ${actionConditionResult.reason}`
|
`⚠️ 액션 조건 미충족: ${actionConditionResult.reason}`
|
||||||
);
|
);
|
||||||
previousActionSuccess = false;
|
|
||||||
if (action.logicalOperator === "AND") {
|
|
||||||
shouldSkipRemainingActions = true;
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,19 +173,11 @@ export class DataflowControlService {
|
||||||
actionName: action.name,
|
actionName: action.name,
|
||||||
result: actionResult,
|
result: actionResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
previousActionSuccess = true;
|
|
||||||
shouldSkipRemainingActions = false; // 성공했으므로 다시 실행 가능
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ 액션 실행 오류: ${action.name}`, error);
|
console.error(`❌ 액션 실행 오류: ${action.name}`, error);
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`);
|
errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`);
|
||||||
|
|
||||||
previousActionSuccess = false;
|
|
||||||
if (action.logicalOperator === "AND") {
|
|
||||||
shouldSkipRemainingActions = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import React, { useState, useEffect } from "react";
|
||||||
import { Plus, Search, Pencil, Trash2, Database } from "lucide-react";
|
import { Plus, Search, Pencil, Trash2, Database } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
|
@ -23,7 +23,6 @@ import {
|
||||||
ExternalDbConnectionAPI,
|
ExternalDbConnectionAPI,
|
||||||
ExternalDbConnection,
|
ExternalDbConnection,
|
||||||
ExternalDbConnectionFilter,
|
ExternalDbConnectionFilter,
|
||||||
ConnectionTestRequest,
|
|
||||||
} from "@/lib/api/externalDbConnection";
|
} from "@/lib/api/externalDbConnection";
|
||||||
import { ExternalDbConnectionModal } from "@/components/admin/ExternalDbConnectionModal";
|
import { ExternalDbConnectionModal } from "@/components/admin/ExternalDbConnectionModal";
|
||||||
|
|
||||||
|
|
@ -57,8 +56,6 @@ export default function ExternalConnectionsPage() {
|
||||||
const [supportedDbTypes, setSupportedDbTypes] = useState<Array<{ value: string; label: string }>>([]);
|
const [supportedDbTypes, setSupportedDbTypes] = useState<Array<{ value: string; label: string }>>([]);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [connectionToDelete, setConnectionToDelete] = useState<ExternalDbConnection | null>(null);
|
const [connectionToDelete, setConnectionToDelete] = useState<ExternalDbConnection | null>(null);
|
||||||
const [testingConnections, setTestingConnections] = useState<Set<number>>(new Set());
|
|
||||||
const [testResults, setTestResults] = useState<Map<number, boolean>>(new Map());
|
|
||||||
|
|
||||||
// 데이터 로딩
|
// 데이터 로딩
|
||||||
const loadConnections = async () => {
|
const loadConnections = async () => {
|
||||||
|
|
@ -163,57 +160,6 @@ export default function ExternalConnectionsPage() {
|
||||||
setConnectionToDelete(null);
|
setConnectionToDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 연결 테스트
|
|
||||||
const handleTestConnection = async (connection: ExternalDbConnection) => {
|
|
||||||
if (!connection.id) return;
|
|
||||||
|
|
||||||
setTestingConnections((prev) => new Set(prev).add(connection.id!));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const testData: ConnectionTestRequest = {
|
|
||||||
db_type: connection.db_type,
|
|
||||||
host: connection.host,
|
|
||||||
port: connection.port,
|
|
||||||
database_name: connection.database_name,
|
|
||||||
username: connection.username,
|
|
||||||
password: connection.password,
|
|
||||||
connection_timeout: connection.connection_timeout,
|
|
||||||
ssl_enabled: connection.ssl_enabled,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await ExternalDbConnectionAPI.testConnection(testData);
|
|
||||||
|
|
||||||
setTestResults((prev) => new Map(prev).set(connection.id!, result.success));
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
toast({
|
|
||||||
title: "연결 성공",
|
|
||||||
description: `${connection.connection_name} 연결이 성공했습니다.`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: "연결 실패",
|
|
||||||
description: `${connection.connection_name} 연결에 실패했습니다.`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("연결 테스트 오류:", error);
|
|
||||||
setTestResults((prev) => new Map(prev).set(connection.id!, false));
|
|
||||||
toast({
|
|
||||||
title: "연결 테스트 오류",
|
|
||||||
description: "연결 테스트 중 오류가 발생했습니다.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setTestingConnections((prev) => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(connection.id!);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 모달 저장 처리
|
// 모달 저장 처리
|
||||||
const handleModalSave = () => {
|
const handleModalSave = () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
|
|
@ -318,7 +264,6 @@ export default function ExternalConnectionsPage() {
|
||||||
<TableHead className="w-[120px]">사용자</TableHead>
|
<TableHead className="w-[120px]">사용자</TableHead>
|
||||||
<TableHead className="w-[80px]">상태</TableHead>
|
<TableHead className="w-[80px]">상태</TableHead>
|
||||||
<TableHead className="w-[100px]">생성일</TableHead>
|
<TableHead className="w-[100px]">생성일</TableHead>
|
||||||
<TableHead className="w-[100px]">연결 테스트</TableHead>
|
|
||||||
<TableHead className="w-[120px] text-right">작업</TableHead>
|
<TableHead className="w-[120px] text-right">작업</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|
@ -326,7 +271,14 @@ export default function ExternalConnectionsPage() {
|
||||||
{connections.map((connection) => (
|
{connections.map((connection) => (
|
||||||
<TableRow key={connection.id} className="hover:bg-gray-50">
|
<TableRow key={connection.id} className="hover:bg-gray-50">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
<div>
|
||||||
<div className="font-medium">{connection.connection_name}</div>
|
<div className="font-medium">{connection.connection_name}</div>
|
||||||
|
{connection.description && (
|
||||||
|
<div className="max-w-[180px] truncate text-sm text-gray-500" title={connection.description}>
|
||||||
|
{connection.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
|
|
@ -346,27 +298,6 @@ export default function ExternalConnectionsPage() {
|
||||||
<TableCell className="text-sm">
|
<TableCell className="text-sm">
|
||||||
{connection.created_date ? new Date(connection.created_date).toLocaleDateString() : "N/A"}
|
{connection.created_date ? new Date(connection.created_date).toLocaleDateString() : "N/A"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleTestConnection(connection)}
|
|
||||||
disabled={testingConnections.has(connection.id!)}
|
|
||||||
className="h-7 px-2 text-xs"
|
|
||||||
>
|
|
||||||
{testingConnections.has(connection.id!) ? "테스트 중..." : "테스트"}
|
|
||||||
</Button>
|
|
||||||
{testResults.has(connection.id!) && (
|
|
||||||
<Badge
|
|
||||||
variant={testResults.get(connection.id!) ? "default" : "destructive"}
|
|
||||||
className="text-xs text-white"
|
|
||||||
>
|
|
||||||
{testResults.get(connection.id!) ? "성공" : "실패"}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end gap-1">
|
<div className="flex justify-end gap-1">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,16 @@
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Link, CheckCircle } from "lucide-react";
|
import { Link } from "lucide-react";
|
||||||
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow";
|
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow";
|
||||||
import {
|
import {
|
||||||
ConnectionConfig,
|
ConnectionConfig,
|
||||||
SimpleKeySettings,
|
SimpleKeySettings,
|
||||||
DataSaveSettings,
|
DataSaveSettings,
|
||||||
|
ExternalCallSettings,
|
||||||
SimpleExternalCallSettings,
|
SimpleExternalCallSettings,
|
||||||
ConnectionSetupModalProps,
|
ConnectionSetupModalProps,
|
||||||
} from "@/types/connectionTypes";
|
} from "@/types/connectionTypes";
|
||||||
|
|
@ -30,7 +22,7 @@ import { ConnectionTypeSelector } from "./connection/ConnectionTypeSelector";
|
||||||
import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings";
|
import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings";
|
||||||
import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings";
|
import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings";
|
||||||
import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings";
|
import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings";
|
||||||
import { toast } from "sonner";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|
@ -69,9 +61,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
|
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
|
||||||
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
|
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
|
||||||
const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
|
const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
|
||||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
|
||||||
const [createdConnectionName, setCreatedConnectionName] = useState("");
|
|
||||||
const [pendingRelationshipData, setPendingRelationshipData] = useState<TableRelationship | null>(null);
|
|
||||||
|
|
||||||
// 조건 관리 훅 사용
|
// 조건 관리 훅 사용
|
||||||
const {
|
const {
|
||||||
|
|
@ -476,10 +465,11 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 성공 모달 표시를 위한 상태 설정
|
toast.success("관계가 생성되었습니다!");
|
||||||
setCreatedConnectionName(config.relationshipName);
|
|
||||||
setPendingRelationshipData(relationshipData);
|
// 부모 컴포넌트로 관계 데이터 전달 (DB 저장 없이)
|
||||||
setShowSuccessModal(true);
|
onConfirm(relationshipData);
|
||||||
|
handleCancel(); // 모달 닫기
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
|
@ -492,19 +482,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuccessModalClose = () => {
|
|
||||||
setShowSuccessModal(false);
|
|
||||||
setCreatedConnectionName("");
|
|
||||||
|
|
||||||
// 저장된 관계 데이터를 부모에게 전달
|
|
||||||
if (pendingRelationshipData) {
|
|
||||||
onConfirm(pendingRelationshipData);
|
|
||||||
setPendingRelationshipData(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel(); // 원래 모달도 닫기
|
|
||||||
};
|
|
||||||
|
|
||||||
// 연결 종류별 설정 패널 렌더링
|
// 연결 종류별 설정 패널 렌더링
|
||||||
const renderConnectionTypeSettings = () => {
|
const renderConnectionTypeSettings = () => {
|
||||||
switch (config.connectionType) {
|
switch (config.connectionType) {
|
||||||
|
|
@ -642,7 +619,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
if (!connection) return null;
|
if (!connection) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
||||||
<DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
|
<DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|
@ -675,7 +651,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
<ConditionalSettings
|
<ConditionalSettings
|
||||||
conditions={conditions}
|
conditions={conditions}
|
||||||
fromTableColumns={fromTableColumns}
|
fromTableColumns={fromTableColumns}
|
||||||
fromTableName={selectedFromTable || connection.fromNode.tableName}
|
|
||||||
onAddCondition={addCondition}
|
onAddCondition={addCondition}
|
||||||
onAddGroupStart={addGroupStart}
|
onAddGroupStart={addGroupStart}
|
||||||
onAddGroupEnd={addGroupEnd}
|
onAddGroupEnd={addGroupEnd}
|
||||||
|
|
@ -699,26 +674,5 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<AlertDialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
|
||||||
연결 생성 완료
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="text-base">
|
|
||||||
<span className="font-medium text-green-600">{createdConnectionName}</span> 연결이 생성되었습니다.
|
|
||||||
<br />
|
|
||||||
<span className="mt-2 block text-sm text-gray-500">
|
|
||||||
생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다.
|
|
||||||
</span>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogAction onClick={handleSuccessModalClose}>확인</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -606,10 +606,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
|
|
||||||
// 관계도 저장 함수
|
// 관계도 저장 함수
|
||||||
const handleSaveDiagram = useCallback(
|
const handleSaveDiagram = useCallback(
|
||||||
async (diagramName: string): Promise<{ success: boolean; error?: string }> => {
|
async (diagramName: string) => {
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
toast.error("저장할 테이블이 없습니다.");
|
toast.error("저장할 테이블이 없습니다.");
|
||||||
return { success: false, error: "저장할 테이블이 없습니다." };
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
@ -669,7 +669,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
id: action.id as string,
|
id: action.id as string,
|
||||||
name: action.name as string,
|
name: action.name as string,
|
||||||
actionType: action.actionType as "insert" | "update" | "delete" | "upsert",
|
actionType: action.actionType as "insert" | "update" | "delete" | "upsert",
|
||||||
logicalOperator: action.logicalOperator as "AND" | "OR" | undefined, // 논리 연산자 추가
|
|
||||||
fieldMappings: ((action.fieldMappings as Record<string, unknown>[]) || []).map(
|
fieldMappings: ((action.fieldMappings as Record<string, unknown>[]) || []).map(
|
||||||
(mapping: Record<string, unknown>) => {
|
(mapping: Record<string, unknown>) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|
@ -705,49 +704,12 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
setCurrentDiagramName(newDiagram.diagram_name);
|
setCurrentDiagramName(newDiagram.diagram_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast.success(`관계도 "${diagramName}"가 성공적으로 저장되었습니다.`);
|
||||||
setHasUnsavedChanges(false);
|
setHasUnsavedChanges(false);
|
||||||
// 성공 모달은 SaveDiagramModal에서 처리하므로 여기서는 toast 제거
|
setShowSaveModal(false);
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 에러 메시지 분석
|
|
||||||
let errorMessage = "관계도 저장 중 오류가 발생했습니다.";
|
|
||||||
let isDuplicateError = false;
|
|
||||||
|
|
||||||
// Axios 에러 처리
|
|
||||||
if (error && typeof error === "object" && "response" in error) {
|
|
||||||
const axiosError = error as any;
|
|
||||||
if (axiosError.response?.status === 409) {
|
|
||||||
// 중복 이름 에러 (409 Conflict)
|
|
||||||
errorMessage = "중복된 이름입니다.";
|
|
||||||
isDuplicateError = true;
|
|
||||||
} else if (axiosError.response?.data?.message) {
|
|
||||||
// 백엔드에서 제공한 에러 메시지 사용
|
|
||||||
if (axiosError.response.data.message.includes("중복된 이름입니다")) {
|
|
||||||
errorMessage = "중복된 이름입니다.";
|
|
||||||
isDuplicateError = true;
|
|
||||||
} else {
|
|
||||||
errorMessage = axiosError.response.data.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
if (
|
|
||||||
error.message.includes("중복") ||
|
|
||||||
error.message.includes("duplicate") ||
|
|
||||||
error.message.includes("already exists")
|
|
||||||
) {
|
|
||||||
errorMessage = "중복된 이름입니다.";
|
|
||||||
isDuplicateError = true;
|
|
||||||
} else {
|
|
||||||
errorMessage = error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 중복 에러가 아닌 경우만 콘솔에 로그 출력
|
|
||||||
if (!isDuplicateError) {
|
|
||||||
console.error("관계도 저장 실패:", error);
|
console.error("관계도 저장 실패:", error);
|
||||||
}
|
toast.error("관계도 저장 중 오류가 발생했습니다.");
|
||||||
|
|
||||||
return { success: false, error: errorMessage };
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,17 @@
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { CheckCircle } from "lucide-react";
|
|
||||||
import { JsonRelationship } from "@/lib/api/dataflow";
|
import { JsonRelationship } from "@/lib/api/dataflow";
|
||||||
|
|
||||||
interface SaveDiagramModalProps {
|
interface SaveDiagramModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (diagramName: string) => Promise<{ success: boolean; error?: string }>;
|
onSave: (diagramName: string) => void;
|
||||||
relationships: JsonRelationship[];
|
relationships: JsonRelationship[];
|
||||||
defaultName?: string;
|
defaultName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
|
@ -38,15 +28,13 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [diagramName, setDiagramName] = useState(defaultName);
|
const [diagramName, setDiagramName] = useState(defaultName);
|
||||||
const [nameError, setNameError] = useState("");
|
const [nameError, setNameError] = useState("");
|
||||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
|
||||||
const [savedDiagramName, setSavedDiagramName] = useState("");
|
|
||||||
|
|
||||||
// defaultName이 변경될 때마다 diagramName 업데이트
|
// defaultName이 변경될 때마다 diagramName 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDiagramName(defaultName);
|
setDiagramName(defaultName);
|
||||||
}, [defaultName]);
|
}, [defaultName]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = () => {
|
||||||
const trimmedName = diagramName.trim();
|
const trimmedName = diagramName.trim();
|
||||||
|
|
||||||
if (!trimmedName) {
|
if (!trimmedName) {
|
||||||
|
|
@ -65,39 +53,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
setNameError("");
|
setNameError("");
|
||||||
|
onSave(trimmedName);
|
||||||
try {
|
|
||||||
// 부모에게 저장 요청하고 결과 받기
|
|
||||||
const result = await onSave(trimmedName);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// 성공 시 성공 모달 표시
|
|
||||||
setSavedDiagramName(trimmedName);
|
|
||||||
setShowSuccessModal(true);
|
|
||||||
} else {
|
|
||||||
// 실패 시 에러 메시지 표시
|
|
||||||
if (
|
|
||||||
result.error?.includes("중복된 이름입니다") ||
|
|
||||||
result.error?.includes("중복") ||
|
|
||||||
result.error?.includes("duplicate") ||
|
|
||||||
result.error?.includes("already exists")
|
|
||||||
) {
|
|
||||||
setNameError("중복된 이름입니다.");
|
|
||||||
} else {
|
|
||||||
setNameError(result.error || "저장 중 오류가 발생했습니다.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 중복 에러가 아닌 경우만 콘솔에 로그 출력
|
|
||||||
const isDuplicateError =
|
|
||||||
error && typeof error === "object" && "response" in error && (error as any).response?.status === 409;
|
|
||||||
|
|
||||||
if (!isDuplicateError) {
|
|
||||||
console.error("저장 오류:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setNameError("저장 중 오류가 발생했습니다.");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
|
@ -108,12 +64,6 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuccessModalClose = () => {
|
|
||||||
setShowSuccessModal(false);
|
|
||||||
setSavedDiagramName("");
|
|
||||||
handleClose(); // 원래 모달도 닫기
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter" && !isLoading) {
|
if (e.key === "Enter" && !isLoading) {
|
||||||
handleSave();
|
handleSave();
|
||||||
|
|
@ -126,7 +76,6 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
).sort();
|
).sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
<DialogContent className="max-h-[80vh] max-w-2xl overflow-y-auto">
|
<DialogContent className="max-h-[80vh] max-w-2xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|
@ -251,35 +200,12 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
||||||
저장 중...
|
저장 중...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"저장하기"
|
"💾 저장하기"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 저장 성공 알림 모달 */}
|
|
||||||
<AlertDialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
|
||||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
|
||||||
관계도 저장 완료
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="text-base">
|
|
||||||
<span className="font-medium text-green-600">{savedDiagramName}</span> 관계도가 성공적으로 저장되었습니다.
|
|
||||||
<br />
|
|
||||||
<span className="mt-2 block text-sm text-gray-500">
|
|
||||||
저장된 관계도는 관리 메뉴에서 확인하고 수정할 수 있습니다.
|
|
||||||
</span>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogAction onClick={handleSuccessModalClose}>확인</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
import { ConditionNode, ColumnInfo } from "@/lib/api/dataflow";
|
import { ConditionNode, ColumnInfo } from "@/lib/api/dataflow";
|
||||||
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
||||||
import { WebTypeInput } from "./WebTypeInput";
|
|
||||||
|
|
||||||
interface ConditionRendererProps {
|
interface ConditionRendererProps {
|
||||||
conditions: ConditionNode[];
|
conditions: ConditionNode[];
|
||||||
fromTableColumns: ColumnInfo[];
|
fromTableColumns: ColumnInfo[];
|
||||||
fromTableName?: string;
|
|
||||||
onUpdateCondition: (index: number, field: keyof ConditionNode, value: string) => void;
|
onUpdateCondition: (index: number, field: keyof ConditionNode, value: string) => void;
|
||||||
onRemoveCondition: (index: number) => void;
|
onRemoveCondition: (index: number) => void;
|
||||||
getCurrentGroupLevel: (index: number) => number;
|
getCurrentGroupLevel: (index: number) => number;
|
||||||
|
|
@ -21,43 +19,41 @@ interface ConditionRendererProps {
|
||||||
export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
||||||
conditions,
|
conditions,
|
||||||
fromTableColumns,
|
fromTableColumns,
|
||||||
fromTableName,
|
|
||||||
onUpdateCondition,
|
onUpdateCondition,
|
||||||
onRemoveCondition,
|
onRemoveCondition,
|
||||||
getCurrentGroupLevel,
|
getCurrentGroupLevel,
|
||||||
}) => {
|
}) => {
|
||||||
const renderConditionValue = (condition: ConditionNode, index: number) => {
|
const renderConditionValue = (condition: ConditionNode, index: number) => {
|
||||||
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
|
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
|
||||||
|
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
||||||
|
const inputType = getInputTypeForDataType(dataType);
|
||||||
|
|
||||||
if (!selectedColumn) {
|
if (dataType.includes("bool")) {
|
||||||
// 컬럼이 선택되지 않은 경우 기본 input
|
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 {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type={inputType}
|
||||||
placeholder="값"
|
placeholder={inputType === "number" ? "숫자" : "값"}
|
||||||
value={String(condition.value || "")}
|
value={String(condition.value || "")}
|
||||||
onChange={(e) => onUpdateCondition(index, "value", e.target.value)}
|
onChange={(e) => onUpdateCondition(index, "value", e.target.value)}
|
||||||
className="h-8 flex-1 text-xs"
|
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 (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { ConditionRenderer } from "./ConditionRenderer";
|
||||||
interface ConditionalSettingsProps {
|
interface ConditionalSettingsProps {
|
||||||
conditions: ConditionNode[];
|
conditions: ConditionNode[];
|
||||||
fromTableColumns: ColumnInfo[];
|
fromTableColumns: ColumnInfo[];
|
||||||
fromTableName?: string;
|
|
||||||
onAddCondition: () => void;
|
onAddCondition: () => void;
|
||||||
onAddGroupStart: () => void;
|
onAddGroupStart: () => void;
|
||||||
onAddGroupEnd: () => void;
|
onAddGroupEnd: () => void;
|
||||||
|
|
@ -22,7 +21,6 @@ interface ConditionalSettingsProps {
|
||||||
export const ConditionalSettings: React.FC<ConditionalSettingsProps> = ({
|
export const ConditionalSettings: React.FC<ConditionalSettingsProps> = ({
|
||||||
conditions,
|
conditions,
|
||||||
fromTableColumns,
|
fromTableColumns,
|
||||||
fromTableName,
|
|
||||||
onAddCondition,
|
onAddCondition,
|
||||||
onAddGroupStart,
|
onAddGroupStart,
|
||||||
onAddGroupEnd,
|
onAddGroupEnd,
|
||||||
|
|
@ -59,7 +57,6 @@ export const ConditionalSettings: React.FC<ConditionalSettingsProps> = ({
|
||||||
<ConditionRenderer
|
<ConditionRenderer
|
||||||
conditions={conditions}
|
conditions={conditions}
|
||||||
fromTableColumns={fromTableColumns}
|
fromTableColumns={fromTableColumns}
|
||||||
fromTableName={fromTableName}
|
|
||||||
onUpdateCondition={onUpdateCondition}
|
onUpdateCondition={onUpdateCondition}
|
||||||
onRemoveCondition={onRemoveCondition}
|
onRemoveCondition={onRemoveCondition}
|
||||||
getCurrentGroupLevel={getCurrentGroupLevel}
|
getCurrentGroupLevel={getCurrentGroupLevel}
|
||||||
|
|
|
||||||
|
|
@ -1,422 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useMemo } 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;
|
|
||||||
tableName?: string; // 테이블명을 별도로 전달받음
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WebTypeInput: React.FC<WebTypeInputProps> = ({
|
|
||||||
column,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
className = "",
|
|
||||||
placeholder,
|
|
||||||
tableName,
|
|
||||||
}) => {
|
|
||||||
// tableName은 props 또는 column.tableName에서 가져옴
|
|
||||||
const effectiveTableName = tableName || (column as ColumnInfo & { tableName?: string }).tableName;
|
|
||||||
const webType = column.webType || "text";
|
|
||||||
const [entityOptions, setEntityOptions] = useState<EntityReferenceOption[]>([]);
|
|
||||||
const [codeOptions, setCodeOptions] = useState<EntityReferenceOption[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
// detailSettings 안전하게 파싱 (메모이제이션)
|
|
||||||
const { detailSettings, fallbackCodeCategory } = useMemo(() => {
|
|
||||||
let parsedSettings: Record<string, unknown> = {};
|
|
||||||
let fallbackCategory = "";
|
|
||||||
|
|
||||||
if (column.detailSettings && typeof column.detailSettings === "string") {
|
|
||||||
// JSON 형태인지 확인 ('{' 또는 '[' 로 시작하는지)
|
|
||||||
const trimmed = column.detailSettings.trim();
|
|
||||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
||||||
try {
|
|
||||||
parsedSettings = JSON.parse(column.detailSettings);
|
|
||||||
} catch {
|
|
||||||
parsedSettings = {};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// JSON이 아닌 일반 문자열인 경우, code 타입이면 codeCategory로 사용
|
|
||||||
if (webType === "code") {
|
|
||||||
// "공통코드: 상태" 형태에서 실제 코드 추출 시도
|
|
||||||
if (column.detailSettings.includes(":")) {
|
|
||||||
const parts = column.detailSettings.split(":");
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
fallbackCategory = parts[1].trim();
|
|
||||||
} else {
|
|
||||||
fallbackCategory = column.detailSettings;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fallbackCategory = column.detailSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parsedSettings = {};
|
|
||||||
}
|
|
||||||
} else if (column.detailSettings && typeof column.detailSettings === "object") {
|
|
||||||
parsedSettings = column.detailSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { detailSettings: parsedSettings, fallbackCodeCategory: fallbackCategory };
|
|
||||||
}, [column.detailSettings, webType]);
|
|
||||||
|
|
||||||
const loadEntityData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// entity 타입은 반드시 effectiveTableName과 columnName이 있어야 함
|
|
||||||
if (!effectiveTableName || !column.columnName) {
|
|
||||||
throw new Error("Entity 타입에는 tableName과 columnName이 필요합니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await EntityReferenceAPI.getEntityReferenceData(effectiveTableName, column.columnName, {
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
setEntityOptions(data.options);
|
|
||||||
} catch {
|
|
||||||
setEntityOptions([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [effectiveTableName, column.columnName]);
|
|
||||||
|
|
||||||
const loadCodeData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const codeCategory = column.codeCategory || (detailSettings.codeCategory as string) || fallbackCodeCategory;
|
|
||||||
if (codeCategory) {
|
|
||||||
const data = await EntityReferenceAPI.getCodeReferenceData(codeCategory, { limit: 100 });
|
|
||||||
setCodeOptions(data.options);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setCodeOptions([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [column.codeCategory, detailSettings.codeCategory, fallbackCodeCategory]);
|
|
||||||
|
|
||||||
// webType에 따른 데이터 로드
|
|
||||||
useEffect(() => {
|
|
||||||
// 디버깅: entity 타입 필드 정보 확인
|
|
||||||
if (column.columnName === "manager_name" || webType === "entity") {
|
|
||||||
console.log("🔍 Entity 필드 디버깅:", {
|
|
||||||
columnName: column.columnName,
|
|
||||||
webType: webType,
|
|
||||||
tableName: tableName,
|
|
||||||
effectiveTableName: effectiveTableName,
|
|
||||||
referenceTable: column.referenceTable,
|
|
||||||
referenceColumn: column.referenceColumn,
|
|
||||||
displayColumn: (column as any).displayColumn,
|
|
||||||
shouldLoadEntity: webType === "entity" && effectiveTableName && column.columnName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webType === "entity" && effectiveTableName && column.columnName) {
|
|
||||||
// entity 타입: 다른 테이블 참조
|
|
||||||
console.log("🚀 Entity 데이터 로드 시작:", effectiveTableName, column.columnName);
|
|
||||||
loadEntityData();
|
|
||||||
} else if (webType === "code" && (column.codeCategory || detailSettings.codeCategory || fallbackCodeCategory)) {
|
|
||||||
// code 타입: code_info 테이블에서 공통 코드 조회
|
|
||||||
loadCodeData();
|
|
||||||
}
|
|
||||||
// text 타입: 일반 텍스트 입력
|
|
||||||
// file 타입: 파일 업로드
|
|
||||||
}, [
|
|
||||||
webType,
|
|
||||||
effectiveTableName,
|
|
||||||
column.columnName,
|
|
||||||
column.codeCategory,
|
|
||||||
column.referenceTable,
|
|
||||||
column.referenceColumn,
|
|
||||||
(column as any).displayColumn,
|
|
||||||
tableName,
|
|
||||||
fallbackCodeCategory,
|
|
||||||
detailSettings.codeCategory,
|
|
||||||
loadEntityData,
|
|
||||||
loadCodeData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 날짜/시간 타입일 때 기본값으로 현재 날짜/시간 설정
|
|
||||||
useEffect(() => {
|
|
||||||
const dateTimeTypes = ["date", "datetime", "timestamp"];
|
|
||||||
|
|
||||||
// 컬럼명이나 데이터 타입으로 날짜 필드 판단
|
|
||||||
const isDateColumn =
|
|
||||||
dateTimeTypes.includes(webType) ||
|
|
||||||
column.columnName?.toLowerCase().includes("date") ||
|
|
||||||
column.columnName?.toLowerCase().includes("time") ||
|
|
||||||
column.columnName === "regdate" ||
|
|
||||||
column.columnName === "created_at" ||
|
|
||||||
column.columnName === "updated_at";
|
|
||||||
|
|
||||||
if (isDateColumn && (!value || value === "")) {
|
|
||||||
const now = new Date();
|
|
||||||
let formattedValue = "";
|
|
||||||
|
|
||||||
if (webType === "date") {
|
|
||||||
// 데이터베이스 타입이나 컬럼명으로 시간 포함 여부 판단
|
|
||||||
const isTimestampType =
|
|
||||||
column.dataType?.toLowerCase().includes("timestamp") ||
|
|
||||||
column.columnName?.toLowerCase().includes("time") ||
|
|
||||||
column.columnName === "regdate" ||
|
|
||||||
column.columnName === "created_at" ||
|
|
||||||
column.columnName === "updated_at";
|
|
||||||
|
|
||||||
if (isTimestampType) {
|
|
||||||
formattedValue = format(now, "yyyy-MM-dd HH:mm:ss");
|
|
||||||
} else {
|
|
||||||
formattedValue = format(now, "yyyy-MM-dd");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 컬럼명 기반 판단 시에도 시간 포함
|
|
||||||
formattedValue = format(now, "yyyy-MM-dd HH:mm:ss");
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(formattedValue);
|
|
||||||
}
|
|
||||||
}, [webType, value, onChange, column.columnName, column.dataType]);
|
|
||||||
|
|
||||||
// 공통 props
|
|
||||||
const commonProps = {
|
|
||||||
value: value || "",
|
|
||||||
className,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebType별 렌더링 (column_labels의 webType을 정확히 따름)
|
|
||||||
const actualWebType = webType;
|
|
||||||
|
|
||||||
switch (actualWebType) {
|
|
||||||
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 as number}
|
|
||||||
max={detailSettings.max as number}
|
|
||||||
step={(detailSettings.step as string) || "any"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "date":
|
|
||||||
// 데이터베이스 타입이나 컬럼명으로 시간 포함 여부 판단
|
|
||||||
const isTimestampType =
|
|
||||||
column.dataType?.toLowerCase().includes("timestamp") ||
|
|
||||||
column.columnName?.toLowerCase().includes("time") ||
|
|
||||||
column.columnName === "regdate" ||
|
|
||||||
column.columnName === "created_at" ||
|
|
||||||
column.columnName === "updated_at";
|
|
||||||
|
|
||||||
if (isTimestampType) {
|
|
||||||
// timestamp 타입이면 datetime-local input 사용 (시간까지 입력 가능)
|
|
||||||
const datetimeValue = value ? value.replace(" ", "T").substring(0, 16) : "";
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
{...commonProps}
|
|
||||||
type="datetime-local"
|
|
||||||
value={datetimeValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
const inputValue = e.target.value;
|
|
||||||
// datetime-local 형식 (YYYY-MM-DDTHH:mm)을 DB 형식 (YYYY-MM-DD HH:mm:ss)으로 변환
|
|
||||||
const formattedValue = inputValue ? `${inputValue.replace("T", " ")}:00` : "";
|
|
||||||
onChange(formattedValue);
|
|
||||||
}}
|
|
||||||
placeholder={placeholder || "날짜와 시간 선택"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 순수 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 as number) || 3}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "select":
|
|
||||||
const selectOptions = (detailSettings.options as { value: string; label?: string }[]) || [];
|
|
||||||
return (
|
|
||||||
<Select value={value || ""} onValueChange={onChange}>
|
|
||||||
<SelectTrigger className={className}>
|
|
||||||
<SelectValue placeholder={placeholder || "선택하세요"} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{selectOptions.map((option: { value: string; label?: string }) => (
|
|
||||||
<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 as string) || column.columnLabel || column.columnName}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "radio":
|
|
||||||
const radioOptions = (detailSettings.options as { value: string; label?: string }[]) || [];
|
|
||||||
return (
|
|
||||||
<RadioGroup value={value || ""} onValueChange={onChange} className={className}>
|
|
||||||
{radioOptions.map((option: { value: string; label?: string }) => (
|
|
||||||
<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 as string) || 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.referenceTable as string);
|
|
||||||
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 as string}
|
|
||||||
multiple={detailSettings.multiple as boolean}
|
|
||||||
/>
|
|
||||||
{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,7 +16,6 @@ import { Trash2 } from "lucide-react";
|
||||||
import { ConditionNode, ColumnInfo } from "@/lib/api/dataflow";
|
import { ConditionNode, ColumnInfo } from "@/lib/api/dataflow";
|
||||||
import { DataSaveSettings } from "@/types/connectionTypes";
|
import { DataSaveSettings } from "@/types/connectionTypes";
|
||||||
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
||||||
import { WebTypeInput } from "../condition/WebTypeInput";
|
|
||||||
|
|
||||||
interface ActionConditionRendererProps {
|
interface ActionConditionRendererProps {
|
||||||
condition: ConditionNode;
|
condition: ConditionNode;
|
||||||
|
|
@ -71,37 +70,32 @@ export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = (
|
||||||
// 선택된 테이블 타입에 따라 컬럼 찾기
|
// 선택된 테이블 타입에 따라 컬럼 찾기
|
||||||
const targetColumns = condition.tableType === "from" ? fromTableColumns : toTableColumns;
|
const targetColumns = condition.tableType === "from" ? fromTableColumns : toTableColumns;
|
||||||
const selectedColumn = targetColumns.find((col) => col.columnName === condition.field);
|
const selectedColumn = targetColumns.find((col) => col.columnName === condition.field);
|
||||||
|
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
||||||
|
const inputType = getInputTypeForDataType(dataType);
|
||||||
|
|
||||||
if (!selectedColumn) {
|
if (dataType.includes("bool")) {
|
||||||
// 컬럼이 선택되지 않은 경우 기본 input
|
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 {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type={inputType}
|
||||||
placeholder="값"
|
placeholder={inputType === "number" ? "숫자" : "값"}
|
||||||
value={String(condition.value || "")}
|
value={String(condition.value || "")}
|
||||||
onChange={(e) => updateCondition("value", e.target.value)}
|
onChange={(e) => updateCondition("value", e.target.value)}
|
||||||
className="h-6 flex-1 text-xs"
|
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="값"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 그룹 시작 렌더링
|
// 그룹 시작 렌더링
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { Badge } from "@/components/ui/badge";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { ColumnInfo } from "@/lib/api/dataflow";
|
import { ColumnInfo } from "@/lib/api/dataflow";
|
||||||
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
import { getInputTypeForDataType } from "@/utils/connectionUtils";
|
||||||
import { WebTypeInput } from "../condition/WebTypeInput";
|
|
||||||
|
|
||||||
interface ColumnMapping {
|
interface ColumnMapping {
|
||||||
toColumnName: string;
|
toColumnName: string;
|
||||||
|
|
@ -304,13 +303,14 @@ export const ColumnTableSection: React.FC<ColumnTableSectionProps> = ({
|
||||||
|
|
||||||
{!isMapped && onDefaultValueChange && (
|
{!isMapped && onDefaultValueChange && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<WebTypeInput
|
<Input
|
||||||
column={column}
|
type={getInputTypeForDataType(column.dataType?.toLowerCase() || "string")}
|
||||||
value={mapping?.defaultValue || ""}
|
|
||||||
onChange={(value) => onDefaultValueChange(column.columnName, value)}
|
|
||||||
className="h-6 border-gray-200 text-xs focus:border-green-400 focus:ring-0"
|
|
||||||
placeholder="기본값 입력..."
|
placeholder="기본값 입력..."
|
||||||
tableName={tableName}
|
value={mapping?.defaultValue || ""}
|
||||||
|
onChange={(e) => onDefaultValueChange(column.columnName, e.target.value)}
|
||||||
|
className="h-6 border-gray-200 text-xs focus:border-green-400 focus:ring-0"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
disabled={isSelected || !!oppositeSelectedColumn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
||||||
id: `action_${settings.actions.length + 1}`,
|
id: `action_${settings.actions.length + 1}`,
|
||||||
name: `액션 ${settings.actions.length + 1}`,
|
name: `액션 ${settings.actions.length + 1}`,
|
||||||
actionType: "insert" as const,
|
actionType: "insert" as const,
|
||||||
// 첫 번째 액션이 아니면 기본적으로 AND 연산자 추가
|
|
||||||
...(settings.actions.length > 0 && { logicalOperator: "AND" as const }),
|
|
||||||
fieldMappings: [],
|
fieldMappings: [],
|
||||||
conditions: [],
|
conditions: [],
|
||||||
splitConfig: {
|
splitConfig: {
|
||||||
|
|
@ -62,12 +60,6 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
||||||
|
|
||||||
const removeAction = (actionIndex: number) => {
|
const removeAction = (actionIndex: number) => {
|
||||||
const newActions = settings.actions.filter((_, i) => i !== actionIndex);
|
const newActions = settings.actions.filter((_, i) => i !== actionIndex);
|
||||||
|
|
||||||
// 첫 번째 액션을 삭제했다면, 새로운 첫 번째 액션의 logicalOperator 제거
|
|
||||||
if (actionIndex === 0 && newActions.length > 0) {
|
|
||||||
delete newActions[0].logicalOperator;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSettingsChange({ ...settings, actions: newActions });
|
onSettingsChange({ ...settings, actions: newActions });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -95,29 +87,7 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{settings.actions.map((action, actionIndex) => (
|
{settings.actions.map((action, actionIndex) => (
|
||||||
<div key={action.id}>
|
<div key={action.id} className="rounded border bg-white p-3">
|
||||||
{/* 첫 번째 액션이 아닌 경우 논리 연산자 표시 */}
|
|
||||||
{actionIndex > 0 && (
|
|
||||||
<div className="mb-2 flex items-center justify-center">
|
|
||||||
<div className="flex items-center gap-2 rounded-lg bg-gray-100 px-3 py-1">
|
|
||||||
<span className="text-xs text-gray-600">이전 액션과의 관계:</span>
|
|
||||||
<Select
|
|
||||||
value={action.logicalOperator || "AND"}
|
|
||||||
onValueChange={(value: "AND" | "OR") => updateAction(actionIndex, "logicalOperator", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 w-20 text-sm">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="AND">AND</SelectItem>
|
|
||||||
<SelectItem value="OR">OR</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="rounded border bg-white p-3">
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<Input
|
<Input
|
||||||
value={action.name}
|
value={action.name}
|
||||||
|
|
@ -125,12 +95,7 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
||||||
className="h-7 flex-1 text-xs font-medium"
|
className="h-7 flex-1 text-xs font-medium"
|
||||||
placeholder="액션 이름"
|
placeholder="액션 이름"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button size="sm" variant="ghost" onClick={() => removeAction(actionIndex)} className="h-7 w-7 p-0">
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => removeAction(actionIndex)}
|
|
||||||
className="h-7 w-7 p-0"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -221,7 +186,6 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,9 @@ apiClient.interceptors.response.use(
|
||||||
|
|
||||||
// 409 에러 (중복 데이터)는 조용하게 처리
|
// 409 에러 (중복 데이터)는 조용하게 처리
|
||||||
if (status === 409) {
|
if (status === 409) {
|
||||||
// 중복 검사 API와 관계도 저장은 완전히 조용하게 처리
|
// 중복 검사 API는 완전히 조용하게 처리
|
||||||
if (url?.includes("/check-duplicate") || url?.includes("/dataflow-diagrams")) {
|
if (url?.includes("/check-duplicate")) {
|
||||||
// 중복 검사와 관계도 중복 이름은 정상적인 비즈니스 로직이므로 콘솔 출력 없음
|
// 중복 검사는 정상적인 비즈니스 로직이므로 콘솔 출력 없음
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -824,15 +824,7 @@ export class DataFlowAPI {
|
||||||
|
|
||||||
return response.data.data as JsonDataFlowDiagram;
|
return response.data.data as JsonDataFlowDiagram;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 409 에러(중복 이름)는 콘솔 로그 출력하지 않음
|
|
||||||
if (error && typeof error === "object" && "response" in error) {
|
|
||||||
const axiosError = error as any;
|
|
||||||
if (axiosError.response?.status !== 409) {
|
|
||||||
console.error("JSON 관계도 생성 오류:", error);
|
console.error("JSON 관계도 생성 오류:", error);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("JSON 관계도 생성 오류:", error);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -867,15 +859,7 @@ export class DataFlowAPI {
|
||||||
|
|
||||||
return response.data.data as JsonDataFlowDiagram;
|
return response.data.data as JsonDataFlowDiagram;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 409 에러(중복 이름)는 콘솔 로그 출력하지 않음
|
|
||||||
if (error && typeof error === "object" && "response" in error) {
|
|
||||||
const axiosError = error as any;
|
|
||||||
if (axiosError.response?.status !== 409) {
|
|
||||||
console.error("JSON 관계도 수정 오류:", error);
|
console.error("JSON 관계도 수정 오류:", error);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("JSON 관계도 수정 오류:", error);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
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("공통 코드 데이터 조회 중 오류가 발생했습니다.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -48,7 +48,6 @@ export interface DataSaveSettings {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
actionType: "insert" | "update" | "delete" | "upsert";
|
actionType: "insert" | "update" | "delete" | "upsert";
|
||||||
logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외)
|
|
||||||
conditions?: ConditionNode[];
|
conditions?: ConditionNode[];
|
||||||
fieldMappings: Array<{
|
fieldMappings: Array<{
|
||||||
sourceTable?: string;
|
sourceTable?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue