G8 견적/주문 삭제 가드 — 프로젝트가 생성된 건은 차단 (wace deleteContractMngInfo 1:1)
- salesOrderMgmtService에 assertNoProjectExists(client, contractObjid) 신설. wace 매퍼 getProjectListBycontractObjid:3909와 동일하게 SELECT 1 FROM project_mgmt WHERE contract_objid=$1 LIMIT 1 (status 필터 없음).
- salesEstimateService.remove + salesOrderMgmtService.remove 둘 다 BEGIN 직후 가드 호출. 차단 시 AppError("프로젝트가 생성된 건은 삭제할 수 없습니다.", 409) throw.
- 컨트롤러 remove 두 곳에서 error.statusCode ?? 500으로 응답 → 409 정확히 전달.
- DB 검증: 26C-0698(project 11건) 차단, project 없는 contract 통과.
G7(contract_item UPSERT)는 사실상 이미 구현 — upsertItems 두 서비스 모두 INSERT ... ON CONFLICT (objid) DO UPDATE 패턴으로 OBJID 유지 보장 중. 별도 코드 변경 없음.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)`,
|
||||
|
||||
@@ -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 이식) ──
|
||||
|
||||
Reference in New Issue
Block a user