[agent-pipeline] pipe-20260303124213-d7zo round-2
This commit is contained in:
parent
d7ef26d679
commit
0d71e79c54
|
|
@ -115,6 +115,7 @@ import entitySearchRoutes, { entityOptionsRouter } from "./routes/entitySearchRo
|
|||
import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
|
||||
import screenGroupRoutes from "./routes/screenGroupRoutes"; // 화면 그룹 관리
|
||||
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
|
||||
import approvalRoutes from "./routes/approvalRoutes"; // 결재 시스템
|
||||
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
|
||||
import taxInvoiceRoutes from "./routes/taxInvoiceRoutes"; // 세금계산서 관리
|
||||
import cascadingRelationRoutes from "./routes/cascadingRelationRoutes"; // 연쇄 드롭다운 관계 관리
|
||||
|
|
@ -310,6 +311,7 @@ app.use("/api/category-tree", categoryTreeRoutes); // 카테고리 트리 (테
|
|||
app.use("/api/process-work-standard", processWorkStandardRoutes); // 공정 작업기준
|
||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||
app.use("/api/approval", approvalRoutes); // 결재 시스템
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||
// app.use('/api/users', userRoutes);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,846 @@
|
|||
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<any>(
|
||||
`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<any>(
|
||||
"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<any>(
|
||||
`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<any>(
|
||||
"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<any>(
|
||||
`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<any>(
|
||||
"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<any>(
|
||||
"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<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`,
|
||||
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<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 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<any>(
|
||||
"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<any>(
|
||||
"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<any>(
|
||||
"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<any>(
|
||||
"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<any>(
|
||||
`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<any>(
|
||||
`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<any>(
|
||||
`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<any>(
|
||||
"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<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.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<any>(
|
||||
"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<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]
|
||||
);
|
||||
|
||||
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<any>(
|
||||
`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 : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import express from "express";
|
||||
import {
|
||||
ApprovalDefinitionController,
|
||||
ApprovalTemplateController,
|
||||
ApprovalRequestController,
|
||||
ApprovalLineController,
|
||||
} from "../controllers/approvalController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
|
||||
// ==================== 결재 유형 (Definitions) ====================
|
||||
router.get("/definitions", ApprovalDefinitionController.getDefinitions);
|
||||
router.get("/definitions/:id", ApprovalDefinitionController.getDefinition);
|
||||
router.post("/definitions", ApprovalDefinitionController.createDefinition);
|
||||
router.put("/definitions/:id", ApprovalDefinitionController.updateDefinition);
|
||||
router.delete("/definitions/:id", ApprovalDefinitionController.deleteDefinition);
|
||||
|
||||
// ==================== 결재선 템플릿 (Templates) ====================
|
||||
router.get("/templates", ApprovalTemplateController.getTemplates);
|
||||
router.get("/templates/:id", ApprovalTemplateController.getTemplate);
|
||||
router.post("/templates", ApprovalTemplateController.createTemplate);
|
||||
router.put("/templates/:id", ApprovalTemplateController.updateTemplate);
|
||||
router.delete("/templates/:id", ApprovalTemplateController.deleteTemplate);
|
||||
|
||||
// ==================== 결재 요청 (Requests) ====================
|
||||
router.get("/requests", ApprovalRequestController.getRequests);
|
||||
router.get("/requests/:id", ApprovalRequestController.getRequest);
|
||||
router.post("/requests", ApprovalRequestController.createRequest);
|
||||
router.post("/requests/:id/cancel", ApprovalRequestController.cancelRequest);
|
||||
|
||||
// ==================== 결재 라인 처리 (Lines) ====================
|
||||
router.get("/my-pending", ApprovalLineController.getMyPendingLines);
|
||||
router.post("/lines/:lineId/process", ApprovalLineController.processApproval);
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,450 @@
|
|||
/**
|
||||
* 결재 시스템 API 클라이언트
|
||||
* 엔드포인트: /api/approval/*
|
||||
*/
|
||||
|
||||
// API URL 동적 설정
|
||||
const getApiBaseUrl = (): string => {
|
||||
if (process.env.NEXT_PUBLIC_API_URL) {
|
||||
return process.env.NEXT_PUBLIC_API_URL;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
if (currentHost === "v1.vexplor.com") {
|
||||
return "https://api.vexplor.com/api";
|
||||
}
|
||||
|
||||
if (currentHost === "localhost" || currentHost === "127.0.0.1") {
|
||||
return "http://localhost:8080/api";
|
||||
}
|
||||
}
|
||||
|
||||
return "/api";
|
||||
};
|
||||
|
||||
const API_BASE = getApiBaseUrl();
|
||||
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("authToken") || sessionStorage.getItem("authToken");
|
||||
}
|
||||
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const token = getAuthToken();
|
||||
const headers: HeadersInit = { "Content-Type": "application/json" };
|
||||
if (token) {
|
||||
(headers as Record<string, string>)["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 공통 타입 정의
|
||||
// ============================================================
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
total?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface ApprovalDefinition {
|
||||
definition_id: number;
|
||||
definition_name: string;
|
||||
definition_name_eng?: string;
|
||||
description?: string;
|
||||
default_template_id?: number;
|
||||
max_steps: number;
|
||||
allow_self_approval: boolean;
|
||||
allow_cancel: boolean;
|
||||
is_active: string;
|
||||
company_code: string;
|
||||
created_by?: string;
|
||||
created_at: string;
|
||||
updated_by?: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ApprovalLineTemplate {
|
||||
template_id: number;
|
||||
template_name: string;
|
||||
description?: string;
|
||||
definition_id?: number;
|
||||
definition_name?: string;
|
||||
is_active: string;
|
||||
company_code: string;
|
||||
created_by?: string;
|
||||
created_at: string;
|
||||
updated_by?: string;
|
||||
updated_at: string;
|
||||
steps?: ApprovalLineTemplateStep[];
|
||||
}
|
||||
|
||||
export interface ApprovalLineTemplateStep {
|
||||
step_id: number;
|
||||
template_id: number;
|
||||
step_order: number;
|
||||
approver_type: "user" | "position" | "dept";
|
||||
approver_user_id?: string;
|
||||
approver_position?: string;
|
||||
approver_dept_code?: string;
|
||||
approver_label?: string;
|
||||
company_code: string;
|
||||
}
|
||||
|
||||
export interface ApprovalRequest {
|
||||
request_id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
definition_id?: number;
|
||||
definition_name?: string;
|
||||
target_table: string;
|
||||
target_record_id: string;
|
||||
target_record_data?: Record<string, any>;
|
||||
status: "requested" | "in_progress" | "approved" | "rejected" | "cancelled";
|
||||
current_step: number;
|
||||
total_steps: number;
|
||||
requester_id: string;
|
||||
requester_name?: string;
|
||||
requester_dept?: string;
|
||||
completed_at?: string;
|
||||
final_approver_id?: string;
|
||||
final_comment?: string;
|
||||
screen_id?: number;
|
||||
button_component_id?: string;
|
||||
company_code: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
lines?: ApprovalLine[];
|
||||
}
|
||||
|
||||
export interface ApprovalLine {
|
||||
line_id: number;
|
||||
request_id: number;
|
||||
step_order: number;
|
||||
approver_id: string;
|
||||
approver_name?: string;
|
||||
approver_position?: string;
|
||||
approver_dept?: string;
|
||||
approver_label?: string;
|
||||
status: "waiting" | "pending" | "approved" | "rejected" | "skipped";
|
||||
comment?: string;
|
||||
processed_at?: string;
|
||||
company_code: string;
|
||||
created_at: string;
|
||||
// 요청 정보 (my-pending 조회 시 포함)
|
||||
title?: string;
|
||||
target_table?: string;
|
||||
target_record_id?: string;
|
||||
requester_name?: string;
|
||||
requester_dept?: string;
|
||||
request_created_at?: string;
|
||||
}
|
||||
|
||||
export interface CreateApprovalRequestInput {
|
||||
title: string;
|
||||
description?: string;
|
||||
definition_id?: number;
|
||||
target_table: string;
|
||||
target_record_id: string;
|
||||
target_record_data?: Record<string, any>;
|
||||
screen_id?: number;
|
||||
button_component_id?: string;
|
||||
approvers: {
|
||||
approver_id: string;
|
||||
approver_name?: string;
|
||||
approver_position?: string;
|
||||
approver_dept?: string;
|
||||
approver_label?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 결재 유형 (Definitions) API
|
||||
// ============================================================
|
||||
|
||||
export async function getApprovalDefinitions(params?: {
|
||||
is_active?: string;
|
||||
search?: string;
|
||||
}): Promise<ApiResponse<ApprovalDefinition[]>> {
|
||||
try {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.is_active) qs.append("is_active", params.is_active);
|
||||
if (params?.search) qs.append("search", params.search);
|
||||
|
||||
const response = await fetch(
|
||||
`${API_BASE}/approval/definitions${qs.toString() ? `?${qs}` : ""}`,
|
||||
{ headers: getAuthHeaders(), credentials: "include" }
|
||||
);
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApprovalDefinition(id: number): Promise<ApiResponse<ApprovalDefinition>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/definitions/${id}`, {
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createApprovalDefinition(data: {
|
||||
definition_name: string;
|
||||
definition_name_eng?: string;
|
||||
description?: string;
|
||||
default_template_id?: number;
|
||||
max_steps?: number;
|
||||
allow_self_approval?: boolean;
|
||||
allow_cancel?: boolean;
|
||||
is_active?: string;
|
||||
}): Promise<ApiResponse<ApprovalDefinition>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/definitions`, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateApprovalDefinition(
|
||||
id: number,
|
||||
data: Partial<ApprovalDefinition>
|
||||
): Promise<ApiResponse<ApprovalDefinition>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/definitions/${id}`, {
|
||||
method: "PUT",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteApprovalDefinition(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/definitions/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 결재선 템플릿 (Templates) API
|
||||
// ============================================================
|
||||
|
||||
export async function getApprovalTemplates(params?: {
|
||||
definition_id?: number;
|
||||
is_active?: string;
|
||||
}): Promise<ApiResponse<ApprovalLineTemplate[]>> {
|
||||
try {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.definition_id) qs.append("definition_id", String(params.definition_id));
|
||||
if (params?.is_active) qs.append("is_active", params.is_active);
|
||||
|
||||
const response = await fetch(
|
||||
`${API_BASE}/approval/templates${qs.toString() ? `?${qs}` : ""}`,
|
||||
{ headers: getAuthHeaders(), credentials: "include" }
|
||||
);
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApprovalTemplate(id: number): Promise<ApiResponse<ApprovalLineTemplate>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/templates/${id}`, {
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createApprovalTemplate(data: {
|
||||
template_name: string;
|
||||
description?: string;
|
||||
definition_id?: number;
|
||||
is_active?: string;
|
||||
steps?: Omit<ApprovalLineTemplateStep, "step_id" | "template_id" | "company_code">[];
|
||||
}): Promise<ApiResponse<ApprovalLineTemplate>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/templates`, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateApprovalTemplate(
|
||||
id: number,
|
||||
data: {
|
||||
template_name?: string;
|
||||
description?: string;
|
||||
definition_id?: number;
|
||||
is_active?: string;
|
||||
steps?: Omit<ApprovalLineTemplateStep, "step_id" | "template_id" | "company_code">[];
|
||||
}
|
||||
): Promise<ApiResponse<ApprovalLineTemplate>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/templates/${id}`, {
|
||||
method: "PUT",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteApprovalTemplate(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/templates/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 결재 요청 (Requests) API
|
||||
// ============================================================
|
||||
|
||||
export async function getApprovalRequests(params?: {
|
||||
status?: string;
|
||||
target_table?: string;
|
||||
requester_id?: string;
|
||||
my_approvals?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<ApiResponse<ApprovalRequest[]>> {
|
||||
try {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.status) qs.append("status", params.status);
|
||||
if (params?.target_table) qs.append("target_table", params.target_table);
|
||||
if (params?.requester_id) qs.append("requester_id", params.requester_id);
|
||||
if (params?.my_approvals !== undefined) qs.append("my_approvals", String(params.my_approvals));
|
||||
if (params?.page) qs.append("page", String(params.page));
|
||||
if (params?.limit) qs.append("limit", String(params.limit));
|
||||
|
||||
const response = await fetch(
|
||||
`${API_BASE}/approval/requests${qs.toString() ? `?${qs}` : ""}`,
|
||||
{ headers: getAuthHeaders(), credentials: "include" }
|
||||
);
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApprovalRequest(id: number): Promise<ApiResponse<ApprovalRequest>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/requests/${id}`, {
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createApprovalRequest(
|
||||
data: CreateApprovalRequestInput
|
||||
): Promise<ApiResponse<ApprovalRequest>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/requests`, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelApprovalRequest(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/requests/${id}/cancel`, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 결재 라인 처리 (Lines) API
|
||||
// ============================================================
|
||||
|
||||
export async function getMyPendingApprovals(): Promise<ApiResponse<ApprovalLine[]>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/my-pending`, {
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function processApprovalLine(
|
||||
lineId: number,
|
||||
data: { action: "approved" | "rejected"; comment?: string }
|
||||
): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/approval/lines/${lineId}/process`, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(),
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue