diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 8a01bdaf..39262f81 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -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); diff --git a/backend-node/src/controllers/entityReferenceController.ts b/backend-node/src/controllers/entityReferenceController.ts new file mode 100644 index 00000000..1935065e --- /dev/null +++ b/backend-node/src/controllers/entityReferenceController.ts @@ -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", + }); + } + } +} diff --git a/backend-node/src/routes/entityReferenceRoutes.ts b/backend-node/src/routes/entityReferenceRoutes.ts new file mode 100644 index 00000000..996d569c --- /dev/null +++ b/backend-node/src/routes/entityReferenceRoutes.ts @@ -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; diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index cf3c55c0..7c70f09e 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -675,6 +675,7 @@ export const ConnectionSetupModal: React.FC = ({ void; onRemoveCondition: (index: number) => void; getCurrentGroupLevel: (index: number) => number; @@ -19,41 +21,43 @@ interface ConditionRendererProps { export const ConditionRenderer: React.FC = ({ 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 ( - - ); - } else { + if (!selectedColumn) { + // 컬럼이 선택되지 않은 경우 기본 input return ( onUpdateCondition(index, "value", e.target.value)} className="h-8 flex-1 text-xs" /> ); } + + // 테이블명 정보를 포함한 컬럼 객체 생성 + const columnWithTableName = { + ...selectedColumn, + tableName: fromTableName, + }; + + // WebType 기반 input 사용 + return ( + onUpdateCondition(index, "value", value)} + className="h-8 flex-1 text-xs" + placeholder="값" + /> + ); }; return ( diff --git a/frontend/components/dataflow/condition/ConditionalSettings.tsx b/frontend/components/dataflow/condition/ConditionalSettings.tsx index a863e36b..882ff09c 100644 --- a/frontend/components/dataflow/condition/ConditionalSettings.tsx +++ b/frontend/components/dataflow/condition/ConditionalSettings.tsx @@ -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 = ({ conditions, fromTableColumns, + fromTableName, onAddCondition, onAddGroupStart, onAddGroupEnd, @@ -57,6 +59,7 @@ export const ConditionalSettings: React.FC = ({ void; + className?: string; + placeholder?: string; +} + +export const WebTypeInput: React.FC = ({ column, value, onChange, className = "", placeholder }) => { + const webType = column.webType || "text"; + const [entityOptions, setEntityOptions] = useState([]); + const [codeOptions, setCodeOptions] = useState([]); + 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 ( + onChange(e.target.value)} + /> + ); + + case "number": + return ( + onChange(e.target.value)} + min={detailSettings.min} + max={detailSettings.max} + step={detailSettings.step || "any"} + /> + ); + + case "date": + const dateValue = value ? new Date(value) : undefined; + return ( + + + + + + onChange(date ? format(date, "yyyy-MM-dd") : "")} + initialFocus + /> + + + ); + + case "textarea": + return ( +