import { Response } from "express"; import { AuthenticatedRequest } from "../types/auth"; import { query, queryOne, transaction } from "../database/db"; // ============================================================ // 결재 정의 (Approval Definitions) CRUD // ============================================================ export class ApprovalDefinitionController { // 결재 유형 목록 조회 static async getDefinitions(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user?.companyCode; if (!companyCode) { return res.status(401).json({ success: false, message: "인증 정보가 없습니다." }); } const { is_active, search } = req.query; const conditions: string[] = ["company_code = $1"]; const params: any[] = [companyCode]; let idx = 2; if (is_active) { conditions.push(`is_active = $${idx}`); params.push(is_active); idx++; } if (search) { conditions.push(`(definition_name ILIKE $${idx} OR definition_name_eng ILIKE $${idx})`); params.push(`%${search}%`); idx++; } const rows = await query( `SELECT * FROM approval_definitions WHERE ${conditions.join(" AND ")} ORDER BY definition_id ASC`, 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 getDefinition(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 row = await queryOne( "SELECT * FROM approval_definitions WHERE definition_id = $1 AND company_code = $2", [id, companyCode] ); if (!row) { return res.status(404).json({ success: false, message: "결재 유형을 찾을 수 없습니다." }); } return res.json({ success: true, data: row }); } catch (error) { console.error("결재 유형 상세 조회 오류:", error); return res.status(500).json({ success: false, message: "결재 유형 상세 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } // 결재 유형 생성 static async createDefinition(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user?.companyCode; if (!companyCode) { return res.status(401).json({ success: false, message: "인증 정보가 없습니다." }); } const { definition_name, definition_name_eng, description, default_template_id, max_steps = 5, allow_self_approval = false, allow_cancel = true, is_active = "Y", } = req.body; if (!definition_name) { return res.status(400).json({ success: false, message: "결재 유형명은 필수입니다." }); } const userId = req.user?.userId || "system"; const [row] = await query( `INSERT INTO approval_definitions ( definition_name, definition_name_eng, description, default_template_id, max_steps, allow_self_approval, allow_cancel, is_active, company_code, created_by, updated_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $10) RETURNING *`, [ definition_name, definition_name_eng, description, default_template_id, max_steps, allow_self_approval, allow_cancel, is_active, companyCode, userId, ] ); 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 updateDefinition(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( "SELECT definition_id FROM approval_definitions WHERE definition_id = $1 AND company_code = $2", [id, companyCode] ); if (!existing) { return res.status(404).json({ success: false, message: "결재 유형을 찾을 수 없습니다." }); } const { definition_name, definition_name_eng, description, default_template_id, max_steps, allow_self_approval, allow_cancel, is_active, } = req.body; const fields: string[] = []; const params: any[] = []; let idx = 1; if (definition_name !== undefined) { fields.push(`definition_name = $${idx++}`); params.push(definition_name); } if (definition_name_eng !== undefined) { fields.push(`definition_name_eng = $${idx++}`); params.push(definition_name_eng); } if (description !== undefined) { fields.push(`description = $${idx++}`); params.push(description); } if (default_template_id !== undefined) { fields.push(`default_template_id = $${idx++}`); params.push(default_template_id); } if (max_steps !== undefined) { fields.push(`max_steps = $${idx++}`); params.push(max_steps); } if (allow_self_approval !== undefined) { fields.push(`allow_self_approval = $${idx++}`); params.push(allow_self_approval); } if (allow_cancel !== undefined) { fields.push(`allow_cancel = $${idx++}`); params.push(allow_cancel); } if (is_active !== undefined) { fields.push(`is_active = $${idx++}`); params.push(is_active); } fields.push(`updated_by = $${idx++}`, `updated_at = NOW()`); params.push(req.user?.userId || "system"); params.push(id, companyCode); const [row] = await query( `UPDATE approval_definitions SET ${fields.join(", ")} WHERE definition_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 deleteDefinition(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( "SELECT definition_id FROM approval_definitions WHERE definition_id = $1 AND company_code = $2", [id, companyCode] ); if (!existing) { return res.status(404).json({ success: false, message: "결재 유형을 찾을 수 없습니다." }); } await query( "DELETE FROM approval_definitions WHERE definition_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 : "알 수 없는 오류", }); } } } // ============================================================ // 결재선 템플릿 (Approval Line Templates) CRUD // ============================================================ export class ApprovalTemplateController { // 템플릿 목록 조회 static async getTemplates(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user?.companyCode; if (!companyCode) { return res.status(401).json({ success: false, message: "인증 정보가 없습니다." }); } const { definition_id, is_active } = req.query; const conditions: string[] = ["t.company_code = $1"]; const params: any[] = [companyCode]; let idx = 2; if (definition_id) { conditions.push(`t.definition_id = $${idx++}`); params.push(definition_id); } if (is_active) { conditions.push(`t.is_active = $${idx++}`); params.push(is_active); } const rows = await query( `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`, 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 getTemplate(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 template = await queryOne( `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 t.template_id = $1 AND t.company_code = $2`, [id, companyCode] ); if (!template) { return res.status(404).json({ success: false, message: "결재선 템플릿을 찾을 수 없습니다." }); } const steps = await query( "SELECT * FROM approval_line_template_steps WHERE template_id = $1 AND company_code = $2 ORDER BY step_order ASC", [id, companyCode] ); return res.json({ success: true, data: { ...template, steps } }); } catch (error) { console.error("결재선 템플릿 상세 조회 오류:", error); return res.status(500).json({ success: false, message: "결재선 템플릿 상세 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } // 템플릿 생성 (단계 포함 트랜잭션) static async createTemplate(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user?.companyCode; if (!companyCode) { return res.status(401).json({ success: false, message: "인증 정보가 없습니다." }); } const { template_name, description, definition_id, is_active = "Y", steps = [] } = req.body; if (!template_name) { return res.status(400).json({ success: false, message: "템플릿명은 필수입니다." }); } const userId = req.user?.userId || "system"; let result: any; await transaction(async (client) => { const { rows } = await client.query( `INSERT INTO approval_line_templates (template_name, description, definition_id, is_active, company_code, created_by, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING *`, [template_name, description, definition_id, is_active, companyCode, userId] ); result = rows[0]; // 단계 일괄 삽입 if (Array.isArray(steps) && steps.length > 0) { for (const step of steps) { await client.query( `INSERT INTO approval_line_template_steps (template_id, step_order, approver_type, approver_user_id, approver_position, approver_dept_code, approver_label, company_code) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [ result.template_id, step.step_order, step.approver_type || "user", step.approver_user_id || null, step.approver_position || null, step.approver_dept_code || null, step.approver_label || null, companyCode, ] ); } } }); return res.status(201).json({ success: true, data: result, message: "결재선 템플릿이 생성되었습니다." }); } catch (error) { console.error("결재선 템플릿 생성 오류:", error); return res.status(500).json({ success: false, message: "결재선 템플릿 생성 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } // 템플릿 수정 static async updateTemplate(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( "SELECT template_id FROM approval_line_templates WHERE template_id = $1 AND company_code = $2", [id, companyCode] ); if (!existing) { return res.status(404).json({ success: false, message: "결재선 템플릿을 찾을 수 없습니다." }); } const { template_name, description, definition_id, is_active, steps } = req.body; const userId = req.user?.userId || "system"; let result: any; await transaction(async (client) => { const fields: string[] = []; const params: any[] = []; let idx = 1; if (template_name !== undefined) { fields.push(`template_name = $${idx++}`); params.push(template_name); } if (description !== undefined) { fields.push(`description = $${idx++}`); params.push(description); } if (definition_id !== undefined) { fields.push(`definition_id = $${idx++}`); params.push(definition_id); } if (is_active !== undefined) { fields.push(`is_active = $${idx++}`); params.push(is_active); } fields.push(`updated_by = $${idx++}`, `updated_at = NOW()`); params.push(userId, id, companyCode); const { rows } = await client.query( `UPDATE approval_line_templates SET ${fields.join(", ")} WHERE template_id = $${idx++} AND company_code = $${idx++} RETURNING *`, params ); result = rows[0]; // 단계 재등록 (steps 배열이 주어진 경우 전체 교체) if (Array.isArray(steps)) { await client.query( "DELETE FROM approval_line_template_steps WHERE template_id = $1 AND company_code = $2", [id, companyCode] ); for (const step of steps) { await client.query( `INSERT INTO approval_line_template_steps (template_id, step_order, approver_type, approver_user_id, approver_position, approver_dept_code, approver_label, company_code) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [id, step.step_order, step.approver_type || "user", step.approver_user_id || null, step.approver_position || null, step.approver_dept_code || null, step.approver_label || null, companyCode] ); } } }); return res.json({ success: true, data: result, message: "결재선 템플릿이 수정되었습니다." }); } catch (error) { console.error("결재선 템플릿 수정 오류:", error); return res.status(500).json({ success: false, message: "결재선 템플릿 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } // 템플릿 삭제 static async deleteTemplate(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( "SELECT template_id FROM approval_line_templates WHERE template_id = $1 AND company_code = $2", [id, companyCode] ); if (!existing) { return res.status(404).json({ success: false, message: "결재선 템플릿을 찾을 수 없습니다." }); } await query( "DELETE FROM approval_line_templates WHERE template_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 : "알 수 없는 오류", }); } } } // ============================================================ // 결재 요청 (Approval Requests) CRUD // ============================================================ export class ApprovalRequestController { // 결재 요청 목록 조회 static async getRequests(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 { status, target_table, requester_id, my_approvals, page = "1", limit = "20" } = req.query; const conditions: string[] = ["r.company_code = $1"]; const params: any[] = [companyCode]; let idx = 2; if (status) { conditions.push(`r.status = $${idx++}`); params.push(status); } if (target_table) { conditions.push(`r.target_table = $${idx++}`); params.push(target_table); } if (requester_id) { conditions.push(`r.requester_id = $${idx++}`); params.push(requester_id); } // 내 결재 대기 목록: 현재 사용자가 결재자인 라인만 조회 if (my_approvals === "true") { conditions.push( `EXISTS (SELECT 1 FROM approval_lines l WHERE l.request_id = r.request_id AND l.approver_id = $${idx++} AND l.status = 'pending' AND l.company_code = r.company_code)` ); params.push(userId); } const offset = (parseInt(page as string) - 1) * parseInt(limit as string); params.push(parseInt(limit as string), offset); const rows = await query( `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 ")} ORDER BY r.created_at DESC LIMIT $${idx++} OFFSET $${idx++}`, params ); // 전체 건수 조회 const countParams = params.slice(0, params.length - 2); const [countRow] = await query( `SELECT COUNT(*) as total FROM approval_requests r WHERE ${conditions.join(" AND ")}`, countParams ); return res.json({ success: true, data: rows, total: parseInt(countRow?.total || "0"), page: parseInt(page as string), limit: parseInt(limit as string), }); } catch (error) { console.error("결재 요청 목록 조회 오류:", error); return res.status(500).json({ success: false, message: "결재 요청 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } // 결재 요청 상세 조회 (라인 포함) static async getRequest(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 request = await queryOne( `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 r.request_id = $1 AND r.company_code = $2`, [id, companyCode] ); if (!request) { return res.status(404).json({ success: false, message: "결재 요청을 찾을 수 없습니다." }); } const lines = await query( "SELECT * FROM approval_lines WHERE request_id = $1 AND company_code = $2 ORDER BY step_order ASC", [id, companyCode] ); return res.json({ success: true, data: { ...request, lines } }); } catch (error) { console.error("결재 요청 상세 조회 오류:", error); return res.status(500).json({ success: false, message: "결재 요청 상세 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } // 결재 요청 생성 (결재 라인 자동 생성) static async createRequest(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user?.companyCode; if (!companyCode) { return res.status(401).json({ success: false, message: "인증 정보가 없습니다." }); } 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 }] } = req.body; if (!title || !target_table || !target_record_id) { return res.status(400).json({ success: false, message: "제목, 대상 테이블, 대상 레코드 ID는 필수입니다." }); } 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 || ""; 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, 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) RETURNING *`, [ title, description, definition_id, target_table, target_record_id, target_record_data ? JSON.stringify(target_record_data) : null, approvers.length, userId, userName, deptName, screen_id, button_component_id, companyCode, ] ); result = reqRows[0]; // 결재 라인 생성 (첫 번째 단계는 pending, 나머지는 waiting) for (let i = 0; i < approvers.length; i++) { const approver = approvers[i]; 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)`, [ result.request_id, i + 1, approver.approver_id, approver.approver_name || null, approver.approver_position || null, approver.approver_dept || null, approver.approver_label || `${i + 1}차 결재`, i === 0 ? "pending" : "waiting", companyCode, ] ); } // 상태를 in_progress로 업데이트 await client.query( "UPDATE approval_requests SET status = 'in_progress' WHERE request_id = $1", [result.request_id] ); result.status = "in_progress"; }); return res.status(201).json({ success: true, data: result, message: "결재 요청이 생성되었습니다." }); } catch (error) { console.error("결재 요청 생성 오류:", error); return res.status(500).json({ success: false, message: "결재 요청 생성 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }); } } // 결재 요청 회수 (cancel) static async cancelRequest(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 request = await queryOne( "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.requester_id !== userId) { return res.status(403).json({ success: false, message: "본인이 요청한 건만 회수할 수 있습니다." }); } if (!["requested", "in_progress"].includes(request.status)) { return res.status(400).json({ success: false, message: "이미 처리된 결재 요청은 회수할 수 없습니다." }); } await query( "UPDATE approval_requests SET status = 'cancelled', updated_at = NOW() WHERE request_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 : "알 수 없는 오류", }); } } } // ============================================================ // 결재 라인 처리 (Approval Lines - 승인/반려) // ============================================================ export class ApprovalLineController { // 결재 처리 (승인/반려) static async processApproval(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 { lineId } = req.params; const { action, comment } = req.body; // action: 'approved' | 'rejected' if (!["approved", "rejected"].includes(action)) { return res.status(400).json({ success: false, message: "액션은 approved 또는 rejected여야 합니다." }); } const line = await queryOne( "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] ); const { rows: reqRows } = await client.query( "SELECT * FROM approval_requests WHERE request_id = $1 AND company_code = $2", [line.request_id, companyCode] ); const request = reqRows[0]; if (!request) return; if (action === "rejected") { // 반려: 전체 요청 반려 처리 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] ); } else { // 승인: 다음 단계 활성화 또는 최종 완료 const nextStep = line.step_order + 1; if (nextStep <= request.total_steps) { // 다음 결재자를 pending으로 변경 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] ); } } }); 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 : "알 수 없는 오류", }); } } // 내 결재 대기 목록 조회 static async getMyPendingLines(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 rows = await query( `SELECT l.*, r.title, r.target_table, r.target_record_id, r.requester_name, r.requester_dept, r.created_at as request_created_at FROM approval_lines l JOIN approval_requests r ON l.request_id = r.request_id AND l.company_code = r.company_code WHERE l.approver_id = $1 AND l.status = 'pending' AND l.company_code = $2 ORDER BY r.created_at ASC`, [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 : "알 수 없는 오류", }); } } }