[agent-pipeline] pipe-20260305133525-uca5 round-2
This commit is contained in:
parent
74d0e730cd
commit
f93dc26338
|
|
@ -1,6 +1,7 @@
|
|||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import { PoolClient } from "pg";
|
||||
|
||||
// ============================================================
|
||||
// 결재 정의 (Approval Definitions) CRUD
|
||||
|
|
@ -467,6 +468,89 @@ export class ApprovalTemplateController {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 다음 step 활성화 헬퍼 (혼합형 결재선 대응)
|
||||
// notification step은 자동 통과 후 재귀적으로 다음 step 진행
|
||||
// ============================================================
|
||||
|
||||
async function activateNextStep(
|
||||
client: PoolClient,
|
||||
requestId: number,
|
||||
currentStep: number,
|
||||
totalSteps: number,
|
||||
companyCode: string,
|
||||
userId: string,
|
||||
comment: string | null,
|
||||
): Promise<void> {
|
||||
const nextStep = currentStep + 1;
|
||||
|
||||
if (nextStep > totalSteps) {
|
||||
// 최종 승인 처리
|
||||
await client.query(
|
||||
`UPDATE approval_requests
|
||||
SET status = CASE WHEN approval_type = 'post' THEN 'approved' ELSE 'approved' END,
|
||||
is_post_approved = CASE WHEN approval_type = 'post' THEN true ELSE is_post_approved END,
|
||||
post_approved_at = CASE WHEN approval_type = 'post' THEN NOW() ELSE post_approved_at END,
|
||||
final_approver_id = $1, final_comment = $2, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment, requestId, companyCode]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 다음 step의 결재 라인 조회 (FOR UPDATE로 동시성 방어)
|
||||
const { rows: nextLines } = await client.query(
|
||||
`SELECT * FROM approval_lines
|
||||
WHERE request_id = $1 AND step_order = $2 AND company_code = $3
|
||||
FOR UPDATE`,
|
||||
[requestId, nextStep, companyCode]
|
||||
);
|
||||
|
||||
if (nextLines.length === 0) {
|
||||
// 다음 step이 비어있으면 최종 승인 처리
|
||||
await client.query(
|
||||
`UPDATE approval_requests
|
||||
SET status = 'approved', final_approver_id = $1, final_comment = $2,
|
||||
completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment, requestId, companyCode]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStepType = nextLines[0].step_type || "approval";
|
||||
|
||||
if (nextStepType === "notification") {
|
||||
// 통보 단계: 자동 approved 처리 후 다음 step으로 재귀
|
||||
for (const nl of nextLines) {
|
||||
await client.query(
|
||||
`UPDATE approval_lines SET status = 'approved', comment = '자동 통보 처리', processed_at = NOW()
|
||||
WHERE line_id = $1 AND company_code = $2`,
|
||||
[nl.line_id, companyCode]
|
||||
);
|
||||
}
|
||||
await client.query(
|
||||
`UPDATE approval_requests SET current_step = $1, updated_at = NOW()
|
||||
WHERE request_id = $2 AND company_code = $3`,
|
||||
[nextStep, requestId, companyCode]
|
||||
);
|
||||
// 재귀: 통보 다음 step 활성화
|
||||
await activateNextStep(client, requestId, nextStep, totalSteps, companyCode, userId, comment);
|
||||
} else {
|
||||
// approval 또는 consensus: pending으로 전환
|
||||
await client.query(
|
||||
`UPDATE approval_lines SET status = 'pending'
|
||||
WHERE request_id = $1 AND step_order = $2 AND company_code = $3`,
|
||||
[requestId, nextStep, companyCode]
|
||||
);
|
||||
await client.query(
|
||||
`UPDATE approval_requests SET current_step = $1, updated_at = NOW()
|
||||
WHERE request_id = $2 AND company_code = $3`,
|
||||
[nextStep, requestId, companyCode]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 결재 요청 (Approval Requests) CRUD
|
||||
// ============================================================
|
||||
|
|
@ -587,7 +671,7 @@ export class ApprovalRequestController {
|
|||
}
|
||||
}
|
||||
|
||||
// 결재 요청 생성 (결재 라인 자동 생성)
|
||||
// 결재 요청 생성 (혼합형 결재선 지원 - self/escalation/consensus/post)
|
||||
static async createRequest(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
|
@ -598,83 +682,228 @@ export class ApprovalRequestController {
|
|||
const {
|
||||
title, description, definition_id, target_table, target_record_id,
|
||||
target_record_data, screen_id, button_component_id,
|
||||
approvers, // [{ approver_id, approver_name, approver_position, approver_dept, approver_label }]
|
||||
approval_mode, // "sequential" | "parallel"
|
||||
approvers,
|
||||
approval_mode,
|
||||
approval_type = "escalation",
|
||||
} = req.body;
|
||||
|
||||
if (!title || !target_table) {
|
||||
return res.status(400).json({ success: false, message: "제목과 대상 테이블은 필수입니다." });
|
||||
}
|
||||
|
||||
if (!Array.isArray(approvers) || approvers.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "결재자를 1명 이상 지정해야 합니다." });
|
||||
}
|
||||
|
||||
const userId = req.user?.userId || "system";
|
||||
const userName = req.user?.userName || "";
|
||||
const deptName = req.user?.deptName || "";
|
||||
|
||||
const isParallel = approval_mode === "parallel";
|
||||
const totalSteps = approvers.length;
|
||||
|
||||
// approval_mode를 target_record_data에 병합 저장
|
||||
// approval_mode를 target_record_data에 병합 저장 (하위호환)
|
||||
const mergedRecordData = {
|
||||
...(target_record_data || {}),
|
||||
approval_mode: approval_mode || "sequential",
|
||||
};
|
||||
|
||||
// ========== 자기결재(전결) ==========
|
||||
if (approval_type === "self") {
|
||||
// definition_id가 있으면 allow_self_approval 체크
|
||||
if (definition_id) {
|
||||
const def = await queryOne<any>(
|
||||
"SELECT allow_self_approval FROM approval_definitions WHERE definition_id = $1 AND company_code = $2",
|
||||
[definition_id, companyCode]
|
||||
);
|
||||
if (def && !def.allow_self_approval) {
|
||||
return res.status(400).json({ success: false, message: "해당 결재 유형은 자기결재(전결)를 허용하지 않습니다." });
|
||||
}
|
||||
}
|
||||
|
||||
let result: any;
|
||||
await transaction(async (client) => {
|
||||
const { rows: reqRows } = await client.query(
|
||||
`INSERT INTO approval_requests (
|
||||
title, description, definition_id, target_table, target_record_id,
|
||||
target_record_data, status, current_step, total_steps, approval_type,
|
||||
requester_id, requester_name, requester_dept,
|
||||
screen_id, button_component_id, company_code,
|
||||
final_approver_id, completed_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 'approved', 1, 1, 'self',
|
||||
$7, $8, $9, $10, $11, $12, $7, NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
title, description, definition_id, target_table, target_record_id || null,
|
||||
JSON.stringify(mergedRecordData),
|
||||
userId, userName, deptName,
|
||||
screen_id, button_component_id, companyCode,
|
||||
]
|
||||
);
|
||||
result = reqRows[0];
|
||||
|
||||
// 본인을 결재자로 INSERT (이미 approved)
|
||||
await client.query(
|
||||
`INSERT INTO approval_lines (
|
||||
request_id, step_order, approver_id, approver_name, approver_position,
|
||||
approver_dept, approver_label, status, step_type, processed_at, company_code
|
||||
) VALUES ($1, 1, $2, $3, $4, $5, '자기결재', 'approved', 'approval', NOW(), $6)`,
|
||||
[result.request_id, userId, userName, req.user?.positionName || null, deptName, companyCode]
|
||||
);
|
||||
});
|
||||
|
||||
return res.status(201).json({ success: true, data: result, message: "자기결재(전결) 처리되었습니다." });
|
||||
}
|
||||
|
||||
// ========== 그 외 유형: approvers 필수 검증 ==========
|
||||
if (!Array.isArray(approvers) || approvers.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "결재자를 1명 이상 지정해야 합니다." });
|
||||
}
|
||||
|
||||
// 각 approver에 step_type/step_order 할당 (혼합형 지원)
|
||||
const hasExplicitStepType = approvers.some((a: any) => a.step_type);
|
||||
|
||||
interface NormalizedApprover {
|
||||
approver_id: string;
|
||||
approver_name: string | null;
|
||||
approver_position: string | null;
|
||||
approver_dept: string | null;
|
||||
approver_label: string | null;
|
||||
step_order: number;
|
||||
step_type: string;
|
||||
}
|
||||
|
||||
let normalizedApprovers: NormalizedApprover[];
|
||||
|
||||
if (approval_type === "consensus" && !hasExplicitStepType) {
|
||||
// 단순 합의결재: 전원 step_order=1, step_type='consensus'
|
||||
normalizedApprovers = approvers.map((a: any) => ({
|
||||
approver_id: a.approver_id,
|
||||
approver_name: a.approver_name || null,
|
||||
approver_position: a.approver_position || null,
|
||||
approver_dept: a.approver_dept || null,
|
||||
approver_label: a.approver_label || "합의 결재",
|
||||
step_order: 1,
|
||||
step_type: "consensus",
|
||||
}));
|
||||
} else if (hasExplicitStepType) {
|
||||
// 혼합형: 각 approver에 명시된 step_type/step_order 사용
|
||||
normalizedApprovers = approvers.map((a: any, i: number) => ({
|
||||
approver_id: a.approver_id,
|
||||
approver_name: a.approver_name || null,
|
||||
approver_position: a.approver_position || null,
|
||||
approver_dept: a.approver_dept || null,
|
||||
approver_label: a.approver_label || null,
|
||||
step_order: a.step_order ?? (i + 1),
|
||||
step_type: a.step_type || "approval",
|
||||
}));
|
||||
} else {
|
||||
// escalation / post: 기본 sequential
|
||||
normalizedApprovers = approvers.map((a: any, i: number) => ({
|
||||
approver_id: a.approver_id,
|
||||
approver_name: a.approver_name || null,
|
||||
approver_position: a.approver_position || null,
|
||||
approver_dept: a.approver_dept || null,
|
||||
approver_label: a.approver_label || `${i + 1}차 결재`,
|
||||
step_order: a.step_order ?? (i + 1),
|
||||
step_type: "approval",
|
||||
}));
|
||||
}
|
||||
|
||||
// escalation 타입에서 같은 step_order에 2명 이상이면서 step_type이 approval인 경우 에러
|
||||
const stepOrderGroups = new Map<number, NormalizedApprover[]>();
|
||||
for (const a of normalizedApprovers) {
|
||||
const group = stepOrderGroups.get(a.step_order) || [];
|
||||
group.push(a);
|
||||
stepOrderGroups.set(a.step_order, group);
|
||||
}
|
||||
for (const [stepOrder, group] of stepOrderGroups) {
|
||||
if (group.length > 1) {
|
||||
const allApproval = group.every(g => g.step_type === "approval");
|
||||
if (allApproval) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `step_order ${stepOrder}에 approval 타입 결재자가 2명 이상입니다. consensus로 지정해주세요.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// total_steps = 고유한 step_order의 최대값
|
||||
const uniqueStepOrders = [...new Set(normalizedApprovers.map(a => a.step_order))].sort((a, b) => a - b);
|
||||
const totalSteps = Math.max(...uniqueStepOrders);
|
||||
|
||||
// 저장할 approval_type 결정 (혼합형은 escalation으로 저장)
|
||||
const storedApprovalType = hasExplicitStepType ? "escalation" : approval_type;
|
||||
const initialStatus = approval_type === "post" ? "post_pending" : "requested";
|
||||
|
||||
let result: any;
|
||||
await transaction(async (client) => {
|
||||
// 결재 요청 생성
|
||||
const { rows: reqRows } = await client.query(
|
||||
`INSERT INTO approval_requests (
|
||||
title, description, definition_id, target_table, target_record_id,
|
||||
target_record_data, status, current_step, total_steps,
|
||||
target_record_data, status, current_step, total_steps, approval_type,
|
||||
requester_id, requester_name, requester_dept,
|
||||
screen_id, button_component_id, company_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 'requested', 1, $7, $8, $9, $10, $11, $12, $13)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, 1, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING *`,
|
||||
[
|
||||
title, description, definition_id, target_table, target_record_id || null,
|
||||
JSON.stringify(mergedRecordData),
|
||||
totalSteps,
|
||||
JSON.stringify(mergedRecordData), initialStatus, totalSteps, storedApprovalType,
|
||||
userId, userName, deptName,
|
||||
screen_id, button_component_id, companyCode,
|
||||
]
|
||||
);
|
||||
result = reqRows[0];
|
||||
|
||||
// 결재 라인 생성
|
||||
// 동시결재: 모든 결재자 pending (step_order는 고유값) / 다단결재: 첫 번째만 pending
|
||||
for (let i = 0; i < approvers.length; i++) {
|
||||
const approver = approvers[i];
|
||||
const lineStatus = isParallel ? "pending" : (i === 0 ? "pending" : "waiting");
|
||||
const firstStep = uniqueStepOrders[0];
|
||||
|
||||
for (const approver of normalizedApprovers) {
|
||||
// 첫 번째 step의 결재자만 pending, 나머지는 waiting
|
||||
let lineStatus: string;
|
||||
if (approver.step_order === firstStep) {
|
||||
lineStatus = "pending";
|
||||
} else {
|
||||
lineStatus = "waiting";
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO approval_lines (
|
||||
request_id, step_order, approver_id, approver_name, approver_position,
|
||||
approver_dept, approver_label, status, company_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
approver_dept, approver_label, status, step_type, company_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
[
|
||||
result.request_id,
|
||||
i + 1,
|
||||
approver.step_order,
|
||||
approver.approver_id,
|
||||
approver.approver_name || null,
|
||||
approver.approver_position || null,
|
||||
approver.approver_dept || null,
|
||||
approver.approver_label || (isParallel ? "동시 결재" : `${i + 1}차 결재`),
|
||||
approver.approver_name,
|
||||
approver.approver_position,
|
||||
approver.approver_dept,
|
||||
approver.approver_label,
|
||||
lineStatus,
|
||||
approver.step_type,
|
||||
companyCode,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 상태를 in_progress로 업데이트
|
||||
await client.query(
|
||||
"UPDATE approval_requests SET status = 'in_progress' WHERE request_id = $1",
|
||||
[result.request_id]
|
||||
);
|
||||
result.status = "in_progress";
|
||||
// 첫 번째 step이 notification이면 자동 통과 처리
|
||||
const firstStepLines = normalizedApprovers.filter(a => a.step_order === firstStep);
|
||||
const firstStepType = firstStepLines[0]?.step_type;
|
||||
|
||||
if (firstStepType === "notification") {
|
||||
// notification은 자동 처리 → activateNextStep으로 재귀
|
||||
for (const nl of firstStepLines) {
|
||||
await client.query(
|
||||
`UPDATE approval_lines SET status = 'approved', comment = '자동 통보 처리', processed_at = NOW()
|
||||
WHERE request_id = $1 AND step_order = $2 AND approver_id = $3 AND company_code = $4`,
|
||||
[result.request_id, nl.step_order, nl.approver_id, companyCode]
|
||||
);
|
||||
}
|
||||
await activateNextStep(client, result.request_id, firstStep, totalSteps, companyCode, userId, null);
|
||||
}
|
||||
|
||||
// status를 in_progress로 업데이트 (post_pending 제외)
|
||||
if (approval_type !== "post") {
|
||||
await client.query(
|
||||
`UPDATE approval_requests SET status = 'in_progress' WHERE request_id = $1 AND company_code = $2`,
|
||||
[result.request_id, companyCode]
|
||||
);
|
||||
result.status = "in_progress";
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(201).json({ success: true, data: result, message: "결재 요청이 생성되었습니다." });
|
||||
|
|
@ -711,7 +940,7 @@ export class ApprovalRequestController {
|
|||
return res.status(403).json({ success: false, message: "본인이 요청한 건만 회수할 수 있습니다." });
|
||||
}
|
||||
|
||||
if (!["requested", "in_progress"].includes(request.status)) {
|
||||
if (!["requested", "in_progress", "post_pending"].includes(request.status)) {
|
||||
return res.status(400).json({ success: false, message: "이미 처리된 결재 요청은 회수할 수 없습니다." });
|
||||
}
|
||||
|
||||
|
|
@ -730,6 +959,65 @@ export class ApprovalRequestController {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 후결 처리 엔드포인트
|
||||
static async postApprove(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
const userId = req.user?.userId;
|
||||
if (!companyCode || !userId) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { comment } = req.body;
|
||||
|
||||
const request = await queryOne<any>(
|
||||
"SELECT * FROM approval_requests WHERE request_id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({ success: false, message: "결재 요청을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
if (request.approval_type !== "post") {
|
||||
return res.status(400).json({ success: false, message: "후결 유형의 결재 요청만 후결 처리할 수 있습니다." });
|
||||
}
|
||||
|
||||
if (request.is_post_approved) {
|
||||
return res.status(400).json({ success: false, message: "이미 후결 처리된 요청입니다." });
|
||||
}
|
||||
|
||||
// 결재선 전원 approved 확인
|
||||
const [pendingCount] = await query<any>(
|
||||
`SELECT COUNT(*) as cnt FROM approval_lines
|
||||
WHERE request_id = $1 AND status NOT IN ('approved', 'skipped') AND company_code = $2`,
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (parseInt(pendingCount?.cnt || "0") > 0) {
|
||||
return res.status(400).json({ success: false, message: "모든 결재자의 승인이 완료되지 않았습니다." });
|
||||
}
|
||||
|
||||
await query<any>(
|
||||
`UPDATE approval_requests
|
||||
SET status = 'approved', is_post_approved = true, post_approved_at = NOW(),
|
||||
final_approver_id = $1, final_comment = $2, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment || null, id, companyCode]
|
||||
);
|
||||
|
||||
return res.json({ success: true, message: "후결 처리가 완료되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("후결 처리 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "후결 처리 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -737,7 +1025,7 @@ export class ApprovalRequestController {
|
|||
// ============================================================
|
||||
|
||||
export class ApprovalLineController {
|
||||
// 결재 처리 (승인/반려)
|
||||
// 결재 처리 (승인/반려) - FOR UPDATE 동시성 방어 + 대결 + step_type 분기
|
||||
static async processApproval(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
|
@ -747,42 +1035,63 @@ export class ApprovalLineController {
|
|||
}
|
||||
|
||||
const { lineId } = req.params;
|
||||
const { action, comment } = req.body; // action: 'approved' | 'rejected'
|
||||
const { action, comment, proxy_reason } = req.body;
|
||||
|
||||
if (!["approved", "rejected"].includes(action)) {
|
||||
return res.status(400).json({ success: false, message: "액션은 approved 또는 rejected여야 합니다." });
|
||||
}
|
||||
|
||||
const line = await queryOne<any>(
|
||||
"SELECT * FROM approval_lines WHERE line_id = $1 AND company_code = $2",
|
||||
[lineId, companyCode]
|
||||
);
|
||||
|
||||
if (!line) {
|
||||
return res.status(404).json({ success: false, message: "결재 라인을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
if (line.approver_id !== userId) {
|
||||
return res.status(403).json({ success: false, message: "본인이 결재자로 지정된 건만 처리할 수 있습니다." });
|
||||
}
|
||||
|
||||
if (line.status !== "pending") {
|
||||
return res.status(400).json({ success: false, message: "대기 중인 결재만 처리할 수 있습니다." });
|
||||
}
|
||||
|
||||
await transaction(async (client) => {
|
||||
// 현재 라인 처리
|
||||
await client.query(
|
||||
`UPDATE approval_lines SET status = $1, comment = $2, processed_at = NOW()
|
||||
WHERE line_id = $3`,
|
||||
[action, comment || null, lineId]
|
||||
// FOR UPDATE로 결재 라인 잠금 (동시성 방어)
|
||||
const { rows: [line] } = await client.query(
|
||||
`SELECT * FROM approval_lines WHERE line_id = $1 AND company_code = $2 FOR UPDATE`,
|
||||
[lineId, companyCode]
|
||||
);
|
||||
|
||||
const { rows: reqRows } = await client.query(
|
||||
"SELECT * FROM approval_requests WHERE request_id = $1 AND company_code = $2",
|
||||
if (!line) {
|
||||
res.status(404).json({ success: false, message: "결재 라인을 찾을 수 없습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.status !== "pending") {
|
||||
res.status(400).json({ success: false, message: "대기 중인 결재만 처리할 수 있습니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
// 대결(proxy) 인증 로직
|
||||
let proxyFor: string | null = null;
|
||||
let proxyReasonVal: string | null = null;
|
||||
|
||||
if (line.approver_id !== userId) {
|
||||
const { rows: proxyRows } = await client.query(
|
||||
`SELECT * FROM approval_proxy_settings
|
||||
WHERE original_user_id = $1 AND proxy_user_id = $2
|
||||
AND is_active = 'Y' AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE
|
||||
AND company_code = $3`,
|
||||
[line.approver_id, userId, companyCode]
|
||||
);
|
||||
if (proxyRows.length === 0) {
|
||||
res.status(403).json({ success: false, message: "본인이 결재자로 지정된 건만 처리할 수 있습니다." });
|
||||
return;
|
||||
}
|
||||
proxyFor = line.approver_id;
|
||||
proxyReasonVal = proxy_reason || proxyRows[0].reason || "대결 처리";
|
||||
}
|
||||
|
||||
// 현재 라인 처리 (proxy_for, proxy_reason 포함)
|
||||
await client.query(
|
||||
`UPDATE approval_lines
|
||||
SET status = $1, comment = $2, processed_at = NOW(),
|
||||
proxy_for = $3, proxy_reason = $4
|
||||
WHERE line_id = $5 AND company_code = $6`,
|
||||
[action, comment || null, proxyFor, proxyReasonVal, lineId, companyCode]
|
||||
);
|
||||
|
||||
// 결재 요청 조회 (FOR UPDATE)
|
||||
const { rows: [request] } = await client.query(
|
||||
`SELECT * FROM approval_requests WHERE request_id = $1 AND company_code = $2 FOR UPDATE`,
|
||||
[line.request_id, companyCode]
|
||||
);
|
||||
const request = reqRows[0];
|
||||
|
||||
if (!request) return;
|
||||
|
||||
|
|
@ -791,73 +1100,82 @@ export class ApprovalLineController {
|
|||
await client.query(
|
||||
`UPDATE approval_requests SET status = 'rejected', final_approver_id = $1, final_comment = $2,
|
||||
completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3`,
|
||||
[userId, comment || null, line.request_id]
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment || null, line.request_id, companyCode]
|
||||
);
|
||||
// 남은 pending/waiting 라인도 skipped 처리
|
||||
await client.query(
|
||||
`UPDATE approval_lines SET status = 'skipped'
|
||||
WHERE request_id = $1 AND status IN ('pending', 'waiting') AND line_id != $2`,
|
||||
[line.request_id, lineId]
|
||||
WHERE request_id = $1 AND status IN ('pending', 'waiting') AND line_id != $2 AND company_code = $3`,
|
||||
[line.request_id, lineId, companyCode]
|
||||
);
|
||||
} else {
|
||||
// 승인: 동시결재 vs 다단결재 분기
|
||||
const recordData = request.target_record_data;
|
||||
const isParallelMode = recordData?.approval_mode === "parallel";
|
||||
// 승인 처리: step_type 기반 분기
|
||||
const currentStepType = line.step_type || "approval";
|
||||
|
||||
if (isParallelMode) {
|
||||
// 동시결재: 남은 pending 라인이 있는지 확인
|
||||
// 기존 isParallelMode 하위호환 (step_type이 없는 기존 데이터)
|
||||
const recordData = request.target_record_data;
|
||||
const isLegacyParallel = recordData?.approval_mode === "parallel" && !line.step_type;
|
||||
|
||||
if (isLegacyParallel) {
|
||||
// 레거시 동시결재 (하위호환)
|
||||
const { rows: remainingLines } = await client.query(
|
||||
`SELECT COUNT(*) as cnt FROM approval_lines
|
||||
WHERE request_id = $1 AND status = 'pending' AND line_id != $2 AND company_code = $3`,
|
||||
WHERE request_id = $1 AND status = 'pending' AND line_id != $2 AND company_code = $3
|
||||
FOR UPDATE`,
|
||||
[line.request_id, lineId, companyCode]
|
||||
);
|
||||
const remaining = parseInt(remainingLines[0]?.cnt || "0");
|
||||
|
||||
if (remaining === 0) {
|
||||
// 모든 동시 결재자 승인 완료 → 최종 승인
|
||||
await client.query(
|
||||
`UPDATE approval_requests SET status = 'approved', final_approver_id = $1, final_comment = $2,
|
||||
completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3`,
|
||||
[userId, comment || null, line.request_id]
|
||||
WHERE request_id = $3 AND company_code = $4`,
|
||||
[userId, comment || null, line.request_id, companyCode]
|
||||
);
|
||||
}
|
||||
// 아직 남은 결재자 있으면 대기 (상태 변경 없음)
|
||||
} else {
|
||||
// 다단결재: 다음 단계 활성화 또는 최종 완료
|
||||
const nextStep = line.step_order + 1;
|
||||
} else if (currentStepType === "consensus") {
|
||||
// 합의결재: 같은 step의 모든 결재자 승인 확인
|
||||
const { rows: remaining } = await client.query(
|
||||
`SELECT COUNT(*) as cnt FROM approval_lines
|
||||
WHERE request_id = $1 AND step_order = $2
|
||||
AND status NOT IN ('approved', 'skipped')
|
||||
AND line_id != $3 AND company_code = $4
|
||||
FOR UPDATE`,
|
||||
[line.request_id, line.step_order, lineId, companyCode]
|
||||
);
|
||||
|
||||
if (nextStep <= request.total_steps) {
|
||||
await client.query(
|
||||
`UPDATE approval_lines SET status = 'pending'
|
||||
WHERE request_id = $1 AND step_order = $2 AND company_code = $3`,
|
||||
[line.request_id, nextStep, companyCode]
|
||||
);
|
||||
await client.query(
|
||||
`UPDATE approval_requests SET current_step = $1, updated_at = NOW() WHERE request_id = $2`,
|
||||
[nextStep, line.request_id]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`UPDATE approval_requests SET status = 'approved', final_approver_id = $1, final_comment = $2,
|
||||
completed_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = $3`,
|
||||
[userId, comment || null, line.request_id]
|
||||
if (parseInt(remaining[0].cnt) === 0) {
|
||||
// 합의 완료 → 다음 step 활성화
|
||||
await activateNextStep(
|
||||
client, line.request_id, line.step_order, request.total_steps,
|
||||
companyCode, userId, comment || null,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// approval (기존 sequential 로직): 다음 step 활성화
|
||||
await activateNextStep(
|
||||
client, line.request_id, line.step_order, request.total_steps,
|
||||
companyCode, userId, comment || null,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({ success: true, message: action === "approved" ? "승인 처리되었습니다." : "반려 처리되었습니다." });
|
||||
// 트랜잭션이 res에 응답을 보내지 않은 경우 (정상 처리)
|
||||
if (!res.headersSent) {
|
||||
return res.json({ success: true, message: action === "approved" ? "승인 처리되었습니다." : "반려 처리되었습니다." });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("결재 처리 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "결재 처리 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "결재 처리 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -890,3 +1208,212 @@ export class ApprovalLineController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 대결 위임 설정 (Proxy Settings) CRUD
|
||||
// ============================================================
|
||||
|
||||
export class ApprovalProxyController {
|
||||
// 대결 위임 목록 조회
|
||||
static async getProxySettings(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { original_user_id, proxy_user_id, is_active } = req.query;
|
||||
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let idx = 2;
|
||||
|
||||
if (original_user_id) {
|
||||
conditions.push(`original_user_id = $${idx++}`);
|
||||
params.push(original_user_id);
|
||||
}
|
||||
if (proxy_user_id) {
|
||||
conditions.push(`proxy_user_id = $${idx++}`);
|
||||
params.push(proxy_user_id);
|
||||
}
|
||||
if (is_active) {
|
||||
conditions.push(`is_active = $${idx++}`);
|
||||
params.push(is_active);
|
||||
}
|
||||
|
||||
const rows = await query<any>(
|
||||
`SELECT * FROM approval_proxy_settings
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY created_at DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: rows });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 생성
|
||||
static async createProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { original_user_id, proxy_user_id, start_date, end_date, reason, is_active = "Y" } = req.body;
|
||||
|
||||
if (!original_user_id || !proxy_user_id) {
|
||||
return res.status(400).json({ success: false, message: "위임자와 대결자는 필수입니다." });
|
||||
}
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ success: false, message: "시작일과 종료일은 필수입니다." });
|
||||
}
|
||||
if (original_user_id === proxy_user_id) {
|
||||
return res.status(400).json({ success: false, message: "위임자와 대결자가 동일할 수 없습니다." });
|
||||
}
|
||||
|
||||
const [row] = await query<any>(
|
||||
`INSERT INTO approval_proxy_settings
|
||||
(original_user_id, proxy_user_id, start_date, end_date, reason, is_active, company_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[original_user_id, proxy_user_id, start_date, end_date, reason || null, is_active, companyCode]
|
||||
);
|
||||
|
||||
return res.status(201).json({ success: true, data: row, message: "대결 위임이 생성되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 생성 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 생성 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 수정
|
||||
static async updateProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const existing = await queryOne<any>(
|
||||
"SELECT id FROM approval_proxy_settings WHERE id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, message: "대결 위임 설정을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
const { original_user_id, proxy_user_id, start_date, end_date, reason, is_active } = req.body;
|
||||
|
||||
const fields: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (original_user_id !== undefined) { fields.push(`original_user_id = $${idx++}`); params.push(original_user_id); }
|
||||
if (proxy_user_id !== undefined) { fields.push(`proxy_user_id = $${idx++}`); params.push(proxy_user_id); }
|
||||
if (start_date !== undefined) { fields.push(`start_date = $${idx++}`); params.push(start_date); }
|
||||
if (end_date !== undefined) { fields.push(`end_date = $${idx++}`); params.push(end_date); }
|
||||
if (reason !== undefined) { fields.push(`reason = $${idx++}`); params.push(reason); }
|
||||
if (is_active !== undefined) { fields.push(`is_active = $${idx++}`); params.push(is_active); }
|
||||
|
||||
fields.push(`updated_at = NOW()`);
|
||||
params.push(id, companyCode);
|
||||
|
||||
const [row] = await query<any>(
|
||||
`UPDATE approval_proxy_settings SET ${fields.join(", ")}
|
||||
WHERE id = $${idx++} AND company_code = $${idx++}
|
||||
RETURNING *`,
|
||||
params
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: row, message: "대결 위임이 수정되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 수정 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 수정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 삭제
|
||||
static async deleteProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const existing = await queryOne<any>(
|
||||
"SELECT id FROM approval_proxy_settings WHERE id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, message: "대결 위임 설정을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
await query<any>(
|
||||
"DELETE FROM approval_proxy_settings WHERE id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
return res.json({ success: true, message: "대결 위임이 삭제되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 삭제 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 삭제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 사용자의 현재 활성 대결자 조회
|
||||
static async checkActiveProxy(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { userId: targetUserId } = req.query;
|
||||
|
||||
if (!targetUserId) {
|
||||
return res.status(400).json({ success: false, message: "userId 파라미터는 필수입니다." });
|
||||
}
|
||||
|
||||
const rows = await query<any>(
|
||||
`SELECT * FROM approval_proxy_settings
|
||||
WHERE original_user_id = $1 AND is_active = 'Y'
|
||||
AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE
|
||||
AND company_code = $2`,
|
||||
[targetUserId, companyCode]
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: rows });
|
||||
} catch (error) {
|
||||
console.error("활성 대결자 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "활성 대결자 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,212 @@
|
|||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { query, queryOne } from "../database/db";
|
||||
|
||||
// ============================================================
|
||||
// 대결 위임 설정 (Approval Proxy Settings) CRUD
|
||||
// ============================================================
|
||||
|
||||
export class ApprovalProxyController {
|
||||
// 대결 위임 목록 조회 (user_info JOIN으로 이름/부서 포함)
|
||||
static async getProxySettings(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const rows = await query<any>(
|
||||
`SELECT ps.*,
|
||||
u1.user_name AS original_user_name, u1.dept_name AS original_dept_name,
|
||||
u2.user_name AS proxy_user_name, u2.dept_name AS proxy_dept_name
|
||||
FROM approval_proxy_settings ps
|
||||
LEFT JOIN user_info u1 ON ps.original_user_id = u1.user_id AND ps.company_code = u1.company_code
|
||||
LEFT JOIN user_info u2 ON ps.proxy_user_id = u2.user_id AND ps.company_code = u2.company_code
|
||||
WHERE ps.company_code = $1
|
||||
ORDER BY ps.created_at DESC`,
|
||||
[companyCode]
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: rows });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 생성 (기간 중복 체크 포함)
|
||||
static async createProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { original_user_id, proxy_user_id, start_date, end_date, reason, is_active = "Y" } = req.body;
|
||||
|
||||
if (!original_user_id || !proxy_user_id) {
|
||||
return res.status(400).json({ success: false, message: "위임자와 대결자는 필수입니다." });
|
||||
}
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ success: false, message: "시작일과 종료일은 필수입니다." });
|
||||
}
|
||||
if (original_user_id === proxy_user_id) {
|
||||
return res.status(400).json({ success: false, message: "위임자와 대결자가 동일할 수 없습니다." });
|
||||
}
|
||||
|
||||
// 같은 기간 중복 체크 (daterange 오버랩)
|
||||
const overlap = await queryOne<any>(
|
||||
`SELECT COUNT(*) AS cnt FROM approval_proxy_settings
|
||||
WHERE original_user_id = $1 AND is_active = 'Y'
|
||||
AND daterange(start_date, end_date, '[]') && daterange($2::date, $3::date, '[]')
|
||||
AND company_code = $4`,
|
||||
[original_user_id, start_date, end_date, companyCode]
|
||||
);
|
||||
|
||||
if (overlap && parseInt(overlap.cnt) > 0) {
|
||||
return res.status(400).json({ success: false, message: "해당 기간에 이미 대결 설정이 존재합니다." });
|
||||
}
|
||||
|
||||
const [row] = await query<any>(
|
||||
`INSERT INTO approval_proxy_settings
|
||||
(original_user_id, proxy_user_id, start_date, end_date, reason, is_active, company_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[original_user_id, proxy_user_id, start_date, end_date, reason || null, is_active, companyCode]
|
||||
);
|
||||
|
||||
return res.status(201).json({ success: true, data: row, message: "대결 위임이 생성되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 생성 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 생성 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 수정
|
||||
static async updateProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const existing = await queryOne<any>(
|
||||
"SELECT id FROM approval_proxy_settings WHERE id = $1 AND company_code = $2",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, message: "대결 위임 설정을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
const { proxy_user_id, start_date, end_date, reason, is_active } = req.body;
|
||||
|
||||
const fields: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (proxy_user_id !== undefined) { fields.push(`proxy_user_id = $${idx++}`); params.push(proxy_user_id); }
|
||||
if (start_date !== undefined) { fields.push(`start_date = $${idx++}`); params.push(start_date); }
|
||||
if (end_date !== undefined) { fields.push(`end_date = $${idx++}`); params.push(end_date); }
|
||||
if (reason !== undefined) { fields.push(`reason = $${idx++}`); params.push(reason); }
|
||||
if (is_active !== undefined) { fields.push(`is_active = $${idx++}`); params.push(is_active); }
|
||||
|
||||
if (fields.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "수정할 필드가 없습니다." });
|
||||
}
|
||||
|
||||
fields.push(`updated_at = NOW()`);
|
||||
params.push(id, companyCode);
|
||||
|
||||
const [row] = await query<any>(
|
||||
`UPDATE approval_proxy_settings SET ${fields.join(", ")}
|
||||
WHERE id = $${idx++} AND company_code = $${idx++}
|
||||
RETURNING *`,
|
||||
params
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: row, message: "대결 위임이 수정되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 수정 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 수정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대결 위임 삭제
|
||||
static async deleteProxySetting(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await query<any>(
|
||||
"DELETE FROM approval_proxy_settings WHERE id = $1 AND company_code = $2 RETURNING id",
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (result.length === 0) {
|
||||
return res.status(404).json({ success: false, message: "대결 위임 설정을 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
return res.json({ success: true, message: "대결 위임이 삭제되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("대결 위임 삭제 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "대결 위임 삭제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 사용자의 현재 활성 대결자 조회
|
||||
static async checkActiveProxy(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
|
||||
}
|
||||
|
||||
const { userId } = req.params;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ success: false, message: "userId 파라미터는 필수입니다." });
|
||||
}
|
||||
|
||||
const rows = await query<any>(
|
||||
`SELECT ps.*, u.user_name AS proxy_user_name
|
||||
FROM approval_proxy_settings ps
|
||||
LEFT JOIN user_info u ON ps.proxy_user_id = u.user_id AND ps.company_code = u.company_code
|
||||
WHERE ps.original_user_id = $1 AND ps.is_active = 'Y'
|
||||
AND ps.start_date <= CURRENT_DATE AND ps.end_date >= CURRENT_DATE
|
||||
AND ps.company_code = $2`,
|
||||
[userId, companyCode]
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: rows });
|
||||
} catch (error) {
|
||||
console.error("활성 대결자 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "활성 대결자 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
ApprovalRequestController,
|
||||
ApprovalLineController,
|
||||
} from "../controllers/approvalController";
|
||||
import { ApprovalProxyController } from "../controllers/approvalProxyController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -30,9 +31,17 @@ router.get("/requests", ApprovalRequestController.getRequests);
|
|||
router.get("/requests/:id", ApprovalRequestController.getRequest);
|
||||
router.post("/requests", ApprovalRequestController.createRequest);
|
||||
router.post("/requests/:id/cancel", ApprovalRequestController.cancelRequest);
|
||||
router.post("/requests/:id/post-approve", ApprovalRequestController.postApprove);
|
||||
|
||||
// ==================== 결재 라인 처리 (Lines) ====================
|
||||
router.get("/my-pending", ApprovalLineController.getMyPendingLines);
|
||||
router.post("/lines/:lineId/process", ApprovalLineController.processApproval);
|
||||
|
||||
// ==================== 대결 위임 설정 (Proxy Settings) ====================
|
||||
router.get("/proxy-settings", ApprovalProxyController.getProxySettings);
|
||||
router.post("/proxy-settings", ApprovalProxyController.createProxySetting);
|
||||
router.put("/proxy-settings/:id", ApprovalProxyController.updateProxySetting);
|
||||
router.delete("/proxy-settings/:id", ApprovalProxyController.deleteProxySetting);
|
||||
router.get("/proxy-settings/check/:userId", ApprovalProxyController.checkActiveProxy);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
Loading…
Reference in New Issue