diff --git a/backend-node/src/controllers/salesEstimateController.ts b/backend-node/src/controllers/salesEstimateController.ts index 6467ebd2..a28df17f 100644 --- a/backend-node/src/controllers/salesEstimateController.ts +++ b/backend-node/src/controllers/salesEstimateController.ts @@ -68,7 +68,8 @@ export async function remove(req: AuthenticatedRequest, res: Response) { return res.json({ success: true, message: "견적이 삭제되었습니다." }); } catch (error: any) { logger.error("견적 삭제 실패", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); + // AppError(statusCode) 우선 — G8 guard 409 등 + return res.status(error?.statusCode ?? 500).json({ success: false, message: error.message }); } } diff --git a/backend-node/src/controllers/salesOrderMgmtController.ts b/backend-node/src/controllers/salesOrderMgmtController.ts index d086fbe3..831813db 100644 --- a/backend-node/src/controllers/salesOrderMgmtController.ts +++ b/backend-node/src/controllers/salesOrderMgmtController.ts @@ -49,7 +49,11 @@ export async function remove(req: AuthenticatedRequest, res: Response) { const { id } = req.params; await svc.remove(id); return res.json({ success: true, message: "주문서가 삭제되었습니다." }); - } catch (e: any) { logger.error("주문서 삭제 실패", { error: e.message }); return res.status(500).json({ success: false, message: e.message }); } + } catch (e: any) { + logger.error("주문서 삭제 실패", { error: e.message }); + // AppError(statusCode) 우선 — G8 guard 409 등 + return res.status(e?.statusCode ?? 500).json({ success: false, message: e.message }); + } } export async function getFormView(req: AuthenticatedRequest, res: Response) { diff --git a/backend-node/src/services/salesEstimateService.ts b/backend-node/src/services/salesEstimateService.ts index 7d630439..0d1376a8 100644 --- a/backend-node/src/services/salesEstimateService.ts +++ b/backend-node/src/services/salesEstimateService.ts @@ -10,7 +10,7 @@ import { PDFDocument } from "pdf-lib"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; import { sendMailUTF8 } from "../utils/mailUtil"; -import { generateContractNo } from "./salesOrderMgmtService"; +import { generateContractNo, assertNoProjectExists } from "./salesOrderMgmtService"; // ─── 타입 ───────────────────────────────────────────────────── @@ -837,13 +837,16 @@ function textToHtml(text: string): string { // ─── 삭제 ───────────────────────────────────────────────────── // 시리얼 → 라인 → 헤더(contract_mgmt) 순. -// (G8 — 프로젝트 존재 시 삭제 방지는 별도 PR) +// G8 — 프로젝트가 생성된 건은 삭제 차단 (wace ContractMgmtService.deleteContractMngInfo:794-808 동일). +// helper는 project_mgmt 도메인 주인인 salesOrderMgmtService에 정의 (assertNoProjectExists). export async function remove(objid: string) { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); + // G8 guard + await assertNoProjectExists(client, objid); await client.query( `UPDATE contract_item_serial SET status='INACTIVE' WHERE item_objid IN (SELECT objid FROM contract_item WHERE contract_objid=$1)`, diff --git a/backend-node/src/services/salesOrderMgmtService.ts b/backend-node/src/services/salesOrderMgmtService.ts index e6d84cc3..305d1d38 100644 --- a/backend-node/src/services/salesOrderMgmtService.ts +++ b/backend-node/src/services/salesOrderMgmtService.ts @@ -8,6 +8,7 @@ import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +import { AppError } from "../middleware/errorHandler"; export interface OrderListFilter { category_cd?: string; @@ -519,12 +520,38 @@ export async function update(userId: string, objid: string, body: OrderBody) { } finally { client.release(); } } +// G8 — 프로젝트가 생성된 contract는 견적/주문 모두 삭제 차단. +// wace deleteContractMngInfo:794-808 + mapper getProjectListBycontractObjid:3909 1:1. +// 원본 매퍼는 status 필터 없이 단순 contract_objid 매칭 → 그대로 따름. +// salesEstimateService.remove에서도 import해서 사용. +export async function assertNoProjectExists(client: any, contractObjid: string) { + const r = await client.query( + `SELECT 1 FROM project_mgmt WHERE contract_objid=$1 LIMIT 1`, + [contractObjid], + ); + if (r.rowCount > 0) { + throw new AppError("프로젝트가 생성된 건은 삭제할 수 없습니다.", 409); + } +} + export async function remove(objid: string) { const pool = getPool(); - await pool.query(`DELETE FROM contract_mgmt WHERE objid=$1`, [objid]); - // contract_item / contract_item_serial은 ON DELETE CASCADE 이지만, - // estimate_template (contract_objid 참조)는 FK가 없어 남을 수 있음 — 정책 결정 필요. - logger.info("주문서 삭제", { objid }); + const client = await pool.connect(); + try { + await client.query("BEGIN"); + // G8 guard + await assertNoProjectExists(client, objid); + await client.query(`DELETE FROM contract_mgmt WHERE objid=$1`, [objid]); + await client.query("COMMIT"); + // contract_item / contract_item_serial은 ON DELETE CASCADE. + // estimate_template (contract_objid 참조)는 FK 없어 남을 수 있음 — 별도 정책 결정 영역. + logger.info("주문서 삭제", { objid }); + } catch (err) { + await client.query("ROLLBACK"); + throw err; + } finally { + client.release(); + } } // ─── 수주확정 → 프로젝트 자동생성 (wace ContractMgmtService.updateOrderStatus 이식) ──