[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 screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달
|
||||||
import screenGroupRoutes from "./routes/screenGroupRoutes"; // 화면 그룹 관리
|
import screenGroupRoutes from "./routes/screenGroupRoutes"; // 화면 그룹 관리
|
||||||
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
|
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
|
||||||
|
import approvalRoutes from "./routes/approvalRoutes"; // 결재 시스템
|
||||||
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
|
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
|
||||||
import taxInvoiceRoutes from "./routes/taxInvoiceRoutes"; // 세금계산서 관리
|
import taxInvoiceRoutes from "./routes/taxInvoiceRoutes"; // 세금계산서 관리
|
||||||
import cascadingRelationRoutes from "./routes/cascadingRelationRoutes"; // 연쇄 드롭다운 관계 관리
|
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/process-work-standard", processWorkStandardRoutes); // 공정 작업기준
|
||||||
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달
|
||||||
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리
|
||||||
|
app.use("/api/approval", approvalRoutes); // 결재 시스템
|
||||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||||
// app.use('/api/users', userRoutes);
|
// 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