[agent-pipeline] pipe-20260305174856-9ogt round-1

This commit is contained in:
DDD1542 2026-03-06 02:51:51 +09:00
parent f147a7608d
commit 24f5c179d8
1 changed files with 49 additions and 25 deletions

View File

@ -3,6 +3,14 @@ import { AuthenticatedRequest } from "../types/auth";
import { query, queryOne, transaction } from "../database/db";
import { PoolClient } from "pg";
// 트랜잭션 내부에서 throw하고 외부에서 instanceof로 구분하기 위한 커스텀 에러
class ValidationError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "ValidationError";
}
}
// ============================================================
// 결재 정의 (Approval Definitions) CRUD
// ============================================================
@ -18,24 +26,34 @@ export class ApprovalDefinitionController {
const { is_active, search } = req.query;
const conditions: string[] = ["company_code = $1"];
const params: any[] = [companyCode];
let idx = 2;
const conditions: string[] = [];
const params: any[] = [];
let idx = 1;
// SUPER_ADMIN은 전체 조회, 일반 회사는 자사 데이터만
if (companyCode === "*") {
// 전체 조회 (company_code 필터 없음)
} else {
conditions.push(`company_code = $${idx++}`);
params.push(companyCode);
}
if (is_active) {
conditions.push(`is_active = $${idx}`);
conditions.push(`is_active = $${idx++}`);
params.push(is_active);
idx++;
}
if (search) {
// ILIKE에서 같은 파라미터를 두 조건에서 참조 (파라미터는 1개만 push)
conditions.push(`(definition_name ILIKE $${idx} OR definition_name_eng ILIKE $${idx})`);
params.push(`%${search}%`);
idx++;
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const rows = await query<any>(
`SELECT * FROM approval_definitions WHERE ${conditions.join(" AND ")} ORDER BY definition_id ASC`,
`SELECT * FROM approval_definitions ${whereClause} ORDER BY company_code, definition_id ASC`,
params
);
@ -239,9 +257,15 @@ export class ApprovalTemplateController {
const { definition_id, is_active } = req.query;
const conditions: string[] = ["t.company_code = $1"];
const params: any[] = [companyCode];
let idx = 2;
const conditions: string[] = [];
const params: any[] = [];
let idx = 1;
// SUPER_ADMIN은 전체 조회, 일반 회사는 자사 데이터만
if (companyCode !== "*") {
conditions.push(`t.company_code = $${idx++}`);
params.push(companyCode);
}
if (definition_id) {
conditions.push(`t.definition_id = $${idx++}`);
@ -252,12 +276,14 @@ export class ApprovalTemplateController {
params.push(is_active);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const rows = await query<any>(
`SELECT t.*, d.definition_name
FROM approval_line_templates t
LEFT JOIN approval_definitions d ON t.definition_id = d.definition_id AND t.company_code = d.company_code
WHERE ${conditions.join(" AND ")}
ORDER BY t.template_id ASC`,
${whereClause}
ORDER BY t.company_code, t.template_id ASC`,
params
);
@ -576,9 +602,15 @@ export class ApprovalRequestController {
const { status, target_table, target_record_id, requester_id, my_approvals, page = "1", limit = "20" } = req.query;
const conditions: string[] = ["r.company_code = $1"];
const params: any[] = [companyCode];
let idx = 2;
const conditions: string[] = [];
const params: any[] = [];
let idx = 1;
// SUPER_ADMIN은 전체 조회, 일반 회사는 자사 데이터만
if (companyCode !== "*") {
conditions.push(`r.company_code = $${idx++}`);
params.push(companyCode);
}
if (status) {
conditions.push(`r.status = $${idx++}`);
@ -606,12 +638,12 @@ export class ApprovalRequestController {
}
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
// 카운트 쿼리는 LIMIT/OFFSET 파라미터 없이 실행
const countParams = [...params];
const [countRow] = await query<any>(
`SELECT COUNT(*) as total FROM approval_requests r
WHERE ${conditions.join(" AND ")}`,
`SELECT COUNT(*) as total FROM approval_requests r ${whereClause}`,
countParams
);
@ -624,7 +656,7 @@ export class ApprovalRequestController {
`SELECT r.*, d.definition_name
FROM approval_requests r
LEFT JOIN approval_definitions d ON r.definition_id = d.definition_id AND r.company_code = d.company_code
WHERE ${conditions.join(" AND ")}
${whereClause}
ORDER BY r.created_at DESC
LIMIT $${limitIdx} OFFSET $${offsetIdx}`,
params
@ -1057,14 +1089,6 @@ export class ApprovalLineController {
return res.status(400).json({ success: false, message: "액션은 approved 또는 rejected여야 합니다." });
}
// 검증 에러를 트랜잭션 바깥으로 전달하기 위한 커스텀 에러 클래스
class ValidationError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "ValidationError";
}
}
await transaction(async (client) => {
// FOR UPDATE로 결재 라인 잠금 (동시성 방어)
const { rows: [line] } = await client.query(