diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 5c2415ea..652677ca 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -71,7 +71,6 @@ import tableCategoryValueRoutes from "./routes/tableCategoryValueRoutes"; // 카 import codeMergeRoutes from "./routes/codeMergeRoutes"; // 코드 병합 import numberingRuleRoutes from "./routes/numberingRuleRoutes"; // 채번 규칙 관리 import entitySearchRoutes from "./routes/entitySearchRoutes"; // 엔티티 검색 -import orderRoutes from "./routes/orderRoutes"; // 수주 관리 import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달 import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리 import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리 @@ -249,7 +248,6 @@ app.use("/api/table-categories", tableCategoryValueRoutes); // 카테고리 값 app.use("/api/code-merge", codeMergeRoutes); // 코드 병합 app.use("/api/numbering-rules", numberingRuleRoutes); // 채번 규칙 관리 app.use("/api/entity-search", entitySearchRoutes); // 엔티티 검색 -app.use("/api/orders", orderRoutes); // 수주 관리 app.use("/api/driver", driverRoutes); // 공차중계 운전자 관리 app.use("/api/tax-invoice", taxInvoiceRoutes); // 세금계산서 관리 app.use("/api/cascading-relations", cascadingRelationRoutes); // 연쇄 드롭다운 관계 관리 diff --git a/backend-node/src/controllers/orderController.ts b/backend-node/src/controllers/orderController.ts deleted file mode 100644 index 82043964..00000000 --- a/backend-node/src/controllers/orderController.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Response } from "express"; -import { AuthenticatedRequest } from "../types/auth"; -import { getPool } from "../database/db"; -import { logger } from "../utils/logger"; - -/** - * 수주 번호 생성 함수 - * 형식: ORD + YYMMDD + 4자리 시퀀스 - * 예: ORD250114001 - */ -async function generateOrderNumber(companyCode: string): Promise { - const pool = getPool(); - const today = new Date(); - const year = today.getFullYear().toString().slice(2); // 25 - const month = String(today.getMonth() + 1).padStart(2, "0"); // 01 - const day = String(today.getDate()).padStart(2, "0"); // 14 - const dateStr = `${year}${month}${day}`; // 250114 - - // 당일 수주 카운트 조회 - const countQuery = ` - SELECT COUNT(*) as count - FROM order_mng_master - WHERE objid LIKE $1 - AND writer LIKE $2 - `; - - const pattern = `ORD${dateStr}%`; - const result = await pool.query(countQuery, [pattern, `%${companyCode}%`]); - const count = parseInt(result.rows[0]?.count || "0"); - const seq = count + 1; - - return `ORD${dateStr}${String(seq).padStart(4, "0")}`; // ORD250114001 -} - -/** - * 수주 등록 API - * POST /api/orders - */ -export async function createOrder(req: AuthenticatedRequest, res: Response) { - const pool = getPool(); - - try { - const { - inputMode, // 입력 방식 - customerCode, // 거래처 코드 - deliveryDate, // 납품일 - items, // 품목 목록 - memo, // 메모 - } = req.body; - - // 멀티테넌시 - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - - // 유효성 검사 - if (!customerCode) { - return res.status(400).json({ - success: false, - message: "거래처 코드는 필수입니다", - }); - } - - if (!items || items.length === 0) { - return res.status(400).json({ - success: false, - message: "품목은 최소 1개 이상 필요합니다", - }); - } - - // 수주 번호 생성 - const orderNo = await generateOrderNumber(companyCode); - - // 전체 금액 계산 - const totalAmount = items.reduce( - (sum: number, item: any) => sum + (item.amount || 0), - 0 - ); - - // 수주 마스터 생성 - const masterQuery = ` - INSERT INTO order_mng_master ( - objid, - partner_objid, - final_delivery_date, - reason, - status, - reg_date, - writer - ) VALUES ($1, $2, $3, $4, $5, NOW(), $6) - RETURNING * - `; - - const masterResult = await pool.query(masterQuery, [ - orderNo, - customerCode, - deliveryDate || null, - memo || null, - "진행중", - `${userId}|${companyCode}`, - ]); - - const masterObjid = masterResult.rows[0].objid; - - // 수주 상세 (품목) 생성 - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const subObjid = `${orderNo}_${i + 1}`; - - const subQuery = ` - INSERT INTO order_mng_sub ( - objid, - order_mng_master_objid, - part_objid, - partner_objid, - partner_price, - partner_qty, - delivery_date, - status, - regdate, - writer - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), $9) - `; - - await pool.query(subQuery, [ - subObjid, - masterObjid, - item.item_code || item.id, // 품목 코드 - customerCode, - item.unit_price || 0, - item.quantity || 0, - item.delivery_date || deliveryDate || null, - "진행중", - `${userId}|${companyCode}`, - ]); - } - - logger.info("수주 등록 성공", { - companyCode, - orderNo, - masterObjid, - itemCount: items.length, - totalAmount, - }); - - res.json({ - success: true, - data: { - orderNo, - masterObjid, - itemCount: items.length, - totalAmount, - }, - message: "수주가 등록되었습니다", - }); - } catch (error: any) { - logger.error("수주 등록 오류", { - error: error.message, - stack: error.stack, - }); - res.status(500).json({ - success: false, - message: error.message || "수주 등록 중 오류가 발생했습니다", - }); - } -} - -/** - * 수주 목록 조회 API (마스터 + 품목 JOIN) - * GET /api/orders - */ -export async function getOrders(req: AuthenticatedRequest, res: Response) { - const pool = getPool(); - - try { - const { page = "1", limit = "20", searchText = "" } = req.query; - const companyCode = req.user!.companyCode; - - const offset = (parseInt(page as string) - 1) * parseInt(limit as string); - - // WHERE 조건 - const whereConditions: string[] = []; - const params: any[] = []; - let paramIndex = 1; - - // 멀티테넌시 (writer 필드에 company_code 포함) - if (companyCode !== "*") { - whereConditions.push(`m.writer LIKE $${paramIndex}`); - params.push(`%${companyCode}%`); - paramIndex++; - } - - // 검색 - if (searchText) { - whereConditions.push(`m.objid LIKE $${paramIndex}`); - params.push(`%${searchText}%`); - paramIndex++; - } - - const whereClause = - whereConditions.length > 0 - ? `WHERE ${whereConditions.join(" AND ")}` - : ""; - - // 카운트 쿼리 (고유한 수주 개수) - const countQuery = ` - SELECT COUNT(DISTINCT m.objid) as count - FROM order_mng_master m - ${whereClause} - `; - const countResult = await pool.query(countQuery, params); - const total = parseInt(countResult.rows[0]?.count || "0"); - - // 데이터 쿼리 (마스터 + 품목 JOIN) - const dataQuery = ` - SELECT - m.objid as order_no, - m.partner_objid, - m.final_delivery_date, - m.reason, - m.status, - m.reg_date, - m.writer, - COALESCE( - json_agg( - CASE WHEN s.objid IS NOT NULL THEN - json_build_object( - 'sub_objid', s.objid, - 'part_objid', s.part_objid, - 'partner_price', s.partner_price, - 'partner_qty', s.partner_qty, - 'delivery_date', s.delivery_date, - 'status', s.status, - 'regdate', s.regdate - ) - END - ORDER BY s.regdate - ) FILTER (WHERE s.objid IS NOT NULL), - '[]'::json - ) as items - FROM order_mng_master m - LEFT JOIN order_mng_sub s ON m.objid = s.order_mng_master_objid - ${whereClause} - GROUP BY m.objid, m.partner_objid, m.final_delivery_date, m.reason, m.status, m.reg_date, m.writer - ORDER BY m.reg_date DESC - LIMIT $${paramIndex} OFFSET $${paramIndex + 1} - `; - - params.push(parseInt(limit as string)); - params.push(offset); - - const dataResult = await pool.query(dataQuery, params); - - logger.info("수주 목록 조회 성공", { - companyCode, - total, - page: parseInt(page as string), - itemCount: dataResult.rows.length, - }); - - res.json({ - success: true, - data: dataResult.rows, - pagination: { - total, - page: parseInt(page as string), - limit: parseInt(limit as string), - }, - }); - } catch (error: any) { - logger.error("수주 목록 조회 오류", { error: error.message }); - res.status(500).json({ - success: false, - message: error.message, - }); - } -} diff --git a/backend-node/src/routes/cascadingAutoFillRoutes.ts b/backend-node/src/routes/cascadingAutoFillRoutes.ts index 5d922dd6..de4eb913 100644 --- a/backend-node/src/routes/cascadingAutoFillRoutes.ts +++ b/backend-node/src/routes/cascadingAutoFillRoutes.ts @@ -50,3 +50,4 @@ router.get("/data/:groupCode", getAutoFillData); export default router; + diff --git a/backend-node/src/routes/cascadingConditionRoutes.ts b/backend-node/src/routes/cascadingConditionRoutes.ts index 813dbff1..c2f12782 100644 --- a/backend-node/src/routes/cascadingConditionRoutes.ts +++ b/backend-node/src/routes/cascadingConditionRoutes.ts @@ -46,3 +46,4 @@ router.get("/filtered-options/:relationCode", getFilteredOptions); export default router; + diff --git a/backend-node/src/routes/cascadingHierarchyRoutes.ts b/backend-node/src/routes/cascadingHierarchyRoutes.ts index be37da49..71e6c418 100644 --- a/backend-node/src/routes/cascadingHierarchyRoutes.ts +++ b/backend-node/src/routes/cascadingHierarchyRoutes.ts @@ -62,3 +62,4 @@ router.get("/:groupCode/options/:levelOrder", getLevelOptions); export default router; + diff --git a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts index 46bbf427..d92d7d72 100644 --- a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts +++ b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts @@ -50,3 +50,4 @@ router.get("/options/:exclusionCode", getExcludedOptions); export default router; + diff --git a/backend-node/src/routes/orderRoutes.ts b/backend-node/src/routes/orderRoutes.ts deleted file mode 100644 index a59b5f43..00000000 --- a/backend-node/src/routes/orderRoutes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Router } from "express"; -import { authenticateToken } from "../middleware/authMiddleware"; -import { createOrder, getOrders } from "../controllers/orderController"; - -const router = Router(); - -/** - * 수주 등록 - * POST /api/orders - */ -router.post("/", authenticateToken, createOrder); - -/** - * 수주 목록 조회 - * GET /api/orders - */ -router.get("/", authenticateToken, getOrders); - -export default router; - diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 65efcd1b..7ec95626 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -903,7 +903,7 @@ export class DynamicFormService { return `${key} = $${index + 1}::numeric`; } else if (dataType === "boolean") { return `${key} = $${index + 1}::boolean`; - } else if (dataType === 'jsonb' || dataType === 'json') { + } else if (dataType === "jsonb" || dataType === "json") { // 🆕 JSONB/JSON 타입은 명시적 캐스팅 return `${key} = $${index + 1}::jsonb`; } else { @@ -917,9 +917,13 @@ export class DynamicFormService { const values: any[] = Object.keys(changedFields).map((key) => { const value = changedFields[key]; const dataType = columnTypes[key]; - + // JSONB/JSON 타입이고 배열/객체인 경우 JSON 문자열로 변환 - if ((dataType === 'jsonb' || dataType === 'json') && (Array.isArray(value) || (typeof value === 'object' && value !== null))) { + if ( + (dataType === "jsonb" || dataType === "json") && + (Array.isArray(value) || + (typeof value === "object" && value !== null)) + ) { return JSON.stringify(value); } return value; @@ -1588,6 +1592,7 @@ export class DynamicFormService { /** * 제어관리 실행 (화면에 설정된 경우) + * 다중 제어를 순서대로 순차 실행 지원 */ private async executeDataflowControlIfConfigured( screenId: number, @@ -1629,105 +1634,67 @@ export class DynamicFormService { hasDataflowConfig: !!properties?.webTypeConfig?.dataflowConfig, hasDiagramId: !!properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId, + hasFlowControls: + !!properties?.webTypeConfig?.dataflowConfig?.flowControls, }); // 버튼 컴포넌트이고 저장 액션이며 제어관리가 활성화된 경우 if ( properties?.componentType === "button-primary" && properties?.componentConfig?.action?.type === "save" && - properties?.webTypeConfig?.enableDataflowControl === true && - properties?.webTypeConfig?.dataflowConfig?.selectedDiagramId + properties?.webTypeConfig?.enableDataflowControl === true ) { - controlConfigFound = true; - const diagramId = - properties.webTypeConfig.dataflowConfig.selectedDiagramId; - const relationshipId = - properties.webTypeConfig.dataflowConfig.selectedRelationshipId; + const dataflowConfig = properties?.webTypeConfig?.dataflowConfig; - console.log(`🎯 제어관리 설정 발견:`, { - componentId: layout.component_id, - diagramId, - relationshipId, - triggerType, - }); + // 다중 제어 설정 확인 (flowControls 배열) + const flowControls = dataflowConfig?.flowControls || []; - // 노드 플로우 실행 (relationshipId가 없는 경우 노드 플로우로 간주) - let controlResult: any; + // flowControls가 있으면 다중 제어 실행, 없으면 기존 단일 제어 실행 + if (flowControls.length > 0) { + controlConfigFound = true; + console.log(`🎯 다중 제어관리 설정 발견: ${flowControls.length}개`); - if (!relationshipId) { - // 노드 플로우 실행 - console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`); - const { NodeFlowExecutionService } = await import( - "./nodeFlowExecutionService" + // 순서대로 정렬 + const sortedControls = [...flowControls].sort( + (a: any, b: any) => (a.order || 0) - (b.order || 0) ); - const executionResult = await NodeFlowExecutionService.executeFlow( + // 다중 제어 순차 실행 + await this.executeMultipleFlowControls( + sortedControls, + savedData, + screenId, + tableName, + triggerType, + userId, + companyCode + ); + } else if (dataflowConfig?.selectedDiagramId) { + // 기존 단일 제어 실행 (하위 호환성) + controlConfigFound = true; + const diagramId = dataflowConfig.selectedDiagramId; + const relationshipId = dataflowConfig.selectedRelationshipId; + + console.log(`🎯 단일 제어관리 설정 발견:`, { + componentId: layout.component_id, diagramId, - { - sourceData: [savedData], - dataSourceType: "formData", - buttonId: "save-button", - screenId: screenId, - userId: userId, - companyCode: companyCode, - formData: savedData, - } - ); + relationshipId, + triggerType, + }); - controlResult = { - success: executionResult.success, - message: executionResult.message, - executedActions: executionResult.nodes?.map((node) => ({ - nodeId: node.nodeId, - status: node.status, - duration: node.duration, - })), - errors: executionResult.nodes - ?.filter((node) => node.status === "failed") - .map((node) => node.error || "실행 실패"), - }; - } else { - // 관계 기반 제어관리 실행 - console.log( - `🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})` + await this.executeSingleFlowControl( + diagramId, + relationshipId, + savedData, + screenId, + tableName, + triggerType, + userId, + companyCode ); - controlResult = - await this.dataflowControlService.executeDataflowControl( - diagramId, - relationshipId, - triggerType, - savedData, - tableName, - userId - ); } - console.log(`🎯 제어관리 실행 결과:`, controlResult); - - if (controlResult.success) { - console.log(`✅ 제어관리 실행 성공: ${controlResult.message}`); - if ( - controlResult.executedActions && - controlResult.executedActions.length > 0 - ) { - console.log(`📊 실행된 액션들:`, controlResult.executedActions); - } - - // 오류가 있는 경우 경고 로그 출력 (성공이지만 일부 액션 실패) - if (controlResult.errors && controlResult.errors.length > 0) { - console.warn( - `⚠️ 제어관리 실행 중 일부 오류 발생:`, - controlResult.errors - ); - // 오류 정보를 별도로 저장하여 필요시 사용자에게 알림 가능 - // 현재는 로그만 출력하고 메인 저장 프로세스는 계속 진행 - } - } else { - console.warn(`⚠️ 제어관리 실행 실패: ${controlResult.message}`); - // 제어관리 실패는 메인 저장 프로세스에 영향을 주지 않음 - } - - // 첫 번째 설정된 제어관리만 실행 (여러 개가 있을 경우) + // 첫 번째 설정된 버튼의 제어관리만 실행 break; } } @@ -1741,6 +1708,218 @@ export class DynamicFormService { } } + /** + * 다중 제어 순차 실행 + */ + private async executeMultipleFlowControls( + flowControls: Array<{ + id: string; + flowId: number; + flowName: string; + executionTiming: string; + order: number; + }>, + savedData: Record, + screenId: number, + tableName: string, + triggerType: "insert" | "update" | "delete", + userId: string, + companyCode: string + ): Promise { + console.log(`🚀 다중 제어 순차 실행 시작: ${flowControls.length}개`); + + const { NodeFlowExecutionService } = await import( + "./nodeFlowExecutionService" + ); + + const results: Array<{ + order: number; + flowId: number; + flowName: string; + success: boolean; + message: string; + duration: number; + }> = []; + + for (let i = 0; i < flowControls.length; i++) { + const control = flowControls[i]; + const startTime = Date.now(); + + console.log( + `\n📍 [${i + 1}/${flowControls.length}] 제어 실행: ${control.flowName} (flowId: ${control.flowId})` + ); + + try { + // 유효하지 않은 flowId 스킵 + if (!control.flowId || control.flowId <= 0) { + console.warn(`⚠️ 유효하지 않은 flowId, 스킵: ${control.flowId}`); + results.push({ + order: control.order, + flowId: control.flowId, + flowName: control.flowName, + success: false, + message: "유효하지 않은 flowId", + duration: 0, + }); + continue; + } + + const executionResult = await NodeFlowExecutionService.executeFlow( + control.flowId, + { + sourceData: [savedData], + dataSourceType: "formData", + buttonId: "save-button", + screenId: screenId, + userId: userId, + companyCode: companyCode, + formData: savedData, + } + ); + + const duration = Date.now() - startTime; + + results.push({ + order: control.order, + flowId: control.flowId, + flowName: control.flowName, + success: executionResult.success, + message: executionResult.message, + duration, + }); + + if (executionResult.success) { + console.log( + `✅ [${i + 1}/${flowControls.length}] 제어 성공: ${control.flowName} (${duration}ms)` + ); + } else { + console.error( + `❌ [${i + 1}/${flowControls.length}] 제어 실패: ${control.flowName} - ${executionResult.message}` + ); + // 이전 제어 실패 시 다음 제어 실행 중단 + console.warn(`⚠️ 이전 제어 실패로 인해 나머지 제어 실행 중단`); + break; + } + } catch (error: any) { + const duration = Date.now() - startTime; + console.error( + `❌ [${i + 1}/${flowControls.length}] 제어 실행 오류: ${control.flowName}`, + error + ); + + results.push({ + order: control.order, + flowId: control.flowId, + flowName: control.flowName, + success: false, + message: error.message || "실행 오류", + duration, + }); + + // 오류 발생 시 다음 제어 실행 중단 + console.warn(`⚠️ 제어 실행 오류로 인해 나머지 제어 실행 중단`); + break; + } + } + + // 실행 결과 요약 + const successCount = results.filter((r) => r.success).length; + const failCount = results.filter((r) => !r.success).length; + const totalDuration = results.reduce((sum, r) => sum + r.duration, 0); + + console.log(`\n📊 다중 제어 실행 완료:`, { + total: flowControls.length, + executed: results.length, + success: successCount, + failed: failCount, + totalDuration: `${totalDuration}ms`, + }); + } + + /** + * 단일 제어 실행 (기존 로직, 하위 호환성) + */ + private async executeSingleFlowControl( + diagramId: number, + relationshipId: string | null, + savedData: Record, + screenId: number, + tableName: string, + triggerType: "insert" | "update" | "delete", + userId: string, + companyCode: string + ): Promise { + let controlResult: any; + + if (!relationshipId) { + // 노드 플로우 실행 + console.log(`🚀 노드 플로우 실행 (flowId: ${diagramId})`); + const { NodeFlowExecutionService } = await import( + "./nodeFlowExecutionService" + ); + + const executionResult = await NodeFlowExecutionService.executeFlow( + diagramId, + { + sourceData: [savedData], + dataSourceType: "formData", + buttonId: "save-button", + screenId: screenId, + userId: userId, + companyCode: companyCode, + formData: savedData, + } + ); + + controlResult = { + success: executionResult.success, + message: executionResult.message, + executedActions: executionResult.nodes?.map((node) => ({ + nodeId: node.nodeId, + status: node.status, + duration: node.duration, + })), + errors: executionResult.nodes + ?.filter((node) => node.status === "failed") + .map((node) => node.error || "실행 실패"), + }; + } else { + // 관계 기반 제어관리 실행 + console.log( + `🎯 관계 기반 제어관리 실행 (relationshipId: ${relationshipId})` + ); + controlResult = await this.dataflowControlService.executeDataflowControl( + diagramId, + relationshipId, + triggerType, + savedData, + tableName, + userId + ); + } + + console.log(`🎯 제어관리 실행 결과:`, controlResult); + + if (controlResult.success) { + console.log(`✅ 제어관리 실행 성공: ${controlResult.message}`); + if ( + controlResult.executedActions && + controlResult.executedActions.length > 0 + ) { + console.log(`📊 실행된 액션들:`, controlResult.executedActions); + } + + if (controlResult.errors && controlResult.errors.length > 0) { + console.warn( + `⚠️ 제어관리 실행 중 일부 오류 발생:`, + controlResult.errors + ); + } + } else { + console.warn(`⚠️ 제어관리 실행 실패: ${controlResult.message}`); + } + } + /** * 특정 테이블의 특정 필드 값만 업데이트 * (다른 테이블의 레코드 업데이트 지원) diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 4a9b53a4..8208ecc5 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -959,9 +959,10 @@ class NumberingRuleService { switch (part.partType) { case "sequence": { - // 순번 (자동 증가 숫자) + // 순번 (자동 증가 숫자 - 다음 번호 사용) const length = autoConfig.sequenceLength || 3; - return String(rule.currentSequence || 1).padStart(length, "0"); + const nextSequence = (rule.currentSequence || 0) + 1; + return String(nextSequence).padStart(length, "0"); } case "number": { diff --git a/docs/노드플로우_개선사항.md b/docs/노드플로우_개선사항.md index e80a1a61..985d730a 100644 --- a/docs/노드플로우_개선사항.md +++ b/docs/노드플로우_개선사항.md @@ -582,3 +582,4 @@ const result = await executeNodeFlow(flowId, { + diff --git a/docs/메일발송_기능_사용_가이드.md b/docs/메일발송_기능_사용_가이드.md index 2ef68524..285dc6ba 100644 --- a/docs/메일발송_기능_사용_가이드.md +++ b/docs/메일발송_기능_사용_가이드.md @@ -355,3 +355,4 @@ - [ ] 부모 화면에서 모달로 데이터가 전달되는가? - [ ] 발송 버튼의 데이터 소스가 올바르게 설정되어 있는가? + diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 51f3bf7b..31287e1e 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -390,9 +390,11 @@ export interface RowDetailPopupConfig { // 추가 데이터 조회 설정 additionalQuery?: { enabled: boolean; + queryMode?: "table" | "custom"; // 조회 모드: table(테이블 조회), custom(커스텀 쿼리) tableName: string; // 조회할 테이블명 (예: vehicles) matchColumn: string; // 매칭할 컬럼 (예: id) sourceColumn?: string; // 클릭한 행에서 가져올 컬럼 (기본: matchColumn과 동일) + customQuery?: string; // 커스텀 쿼리 ({id}, {vehicle_number} 등 파라미터 사용) // 팝업에 표시할 컬럼 목록 (비어있으면 전체 표시) displayColumns?: DisplayColumnConfig[]; }; diff --git a/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx b/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx index b10057cf..a7186d50 100644 --- a/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx +++ b/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx @@ -158,7 +158,7 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW checked={popupConfig.additionalQuery?.enabled || false} onCheckedChange={(enabled) => updatePopupConfig({ - additionalQuery: { ...popupConfig.additionalQuery, enabled, tableName: "", matchColumn: "" }, + additionalQuery: { ...popupConfig.additionalQuery, enabled, queryMode: "table", tableName: "", matchColumn: "" }, }) } aria-label="추가 데이터 조회 활성화" @@ -167,116 +167,230 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW {popupConfig.additionalQuery?.enabled && (
+ {/* 조회 모드 선택 */}
- - + + - updatePopupConfig({ - additionalQuery: { ...popupConfig.additionalQuery!, matchColumn: e.target.value }, - }) - } - placeholder="id" - className="mt-1 h-8 text-xs" - /> -
-
- - - updatePopupConfig({ - additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value }, - }) - } - placeholder="비워두면 매칭 컬럼과 동일" - className="mt-1 h-8 text-xs" - /> + > + + + + + 테이블 조회 + 커스텀 쿼리 + +
- {/* 표시할 컬럼 선택 (다중 선택 + 라벨 편집) */} + {/* 테이블 조회 모드 */} + {(popupConfig.additionalQuery?.queryMode || "table") === "table" && ( + <> +
+ + + updatePopupConfig({ + additionalQuery: { ...popupConfig.additionalQuery!, tableName: e.target.value }, + }) + } + placeholder="vehicles" + className="mt-1 h-8 text-xs" + /> +
+
+ + + updatePopupConfig({ + additionalQuery: { ...popupConfig.additionalQuery!, matchColumn: e.target.value }, + }) + } + placeholder="id" + className="mt-1 h-8 text-xs" + /> +
+
+ + + updatePopupConfig({ + additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value }, + }) + } + placeholder="비워두면 매칭 컬럼과 동일" + className="mt-1 h-8 text-xs" + /> +
+ + )} + + {/* 커스텀 쿼리 모드 */} + {popupConfig.additionalQuery?.queryMode === "custom" && ( + <> +
+ + + updatePopupConfig({ + additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value }, + }) + } + placeholder="id" + className="mt-1 h-8 text-xs" + /> +

쿼리에서 사용할 파라미터 컬럼

+
+
+ +