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/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/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/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" + /> +

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

+
+
+ +