Files
vexplor/backend-node/src/controllers/designController.ts
kjs 160b78e70f feat: add shipping order and design management features
- Introduced new routes and controllers for managing shipping orders, including listing, saving, and previewing next order numbers.
- Added design management routes and controller for handling design requests, projects, tasks, and work logs.
- Implemented company code filtering for multi-tenancy support in both shipping order and design request functionalities.
- Enhanced the shipping plan routes to include listing and updating plans, improving overall shipping management capabilities.

These changes aim to provide comprehensive management features for shipping orders and design processes, facilitating better organization and tracking within the application.
2026-03-19 15:08:31 +09:00

947 lines
43 KiB
TypeScript

import { Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { query, getPool } from "../database/db";
import { logger } from "../utils/logger";
// 회사코드 필터 조건 생성 헬퍼
function companyFilter(companyCode: string, paramIndex: number, alias?: string): { condition: string; param: string; nextIndex: number } {
const col = alias ? `${alias}.company_code` : "company_code";
if (companyCode === "*") {
return { condition: "", param: "", nextIndex: paramIndex };
}
return { condition: `${col} = $${paramIndex}`, param: companyCode, nextIndex: paramIndex + 1 };
}
// ============================================
// 설계의뢰/설변요청 (DR/ECR) CRUD
// ============================================
export async function getDesignRequestList(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { source_type, status, priority, search } = req.query;
const conditions: string[] = [];
const params: any[] = [];
let pi = 1;
if (companyCode !== "*") {
conditions.push(`r.company_code = $${pi}`);
params.push(companyCode);
pi++;
}
if (source_type) { conditions.push(`r.source_type = $${pi}`); params.push(source_type); pi++; }
if (status) { conditions.push(`r.status = $${pi}`); params.push(status); pi++; }
if (priority) { conditions.push(`r.priority = $${pi}`); params.push(priority); pi++; }
if (search) {
conditions.push(`(r.target_name ILIKE $${pi} OR r.request_no ILIKE $${pi} OR r.requester ILIKE $${pi})`);
params.push(`%${search}%`);
pi++;
}
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `
SELECT r.*,
COALESCE(json_agg(json_build_object('id', h.id, 'step', h.step, 'history_date', h.history_date, 'user_name', h.user_name, 'description', h.description)) FILTER (WHERE h.id IS NOT NULL), '[]') AS history,
COALESCE((SELECT json_agg(i.impact_type) FROM dsn_request_impact i WHERE i.request_id = r.id), '[]') AS impact
FROM dsn_design_request r
LEFT JOIN dsn_request_history h ON h.request_id = r.id
${where}
GROUP BY r.id
ORDER BY r.created_date DESC
`;
const result = await query(sql, params);
res.json({ success: true, data: result });
} catch (error: any) {
logger.error("설계의뢰 목록 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function getDesignRequestDetail(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { id } = req.params;
const conditions = [`r.id = $1`];
const params: any[] = [id];
if (companyCode !== "*") { conditions.push(`r.company_code = $2`); params.push(companyCode); }
const sql = `
SELECT r.*,
COALESCE((SELECT json_agg(json_build_object('id', h.id, 'step', h.step, 'history_date', h.history_date, 'user_name', h.user_name, 'description', h.description) ORDER BY h.created_date) FROM dsn_request_history h WHERE h.request_id = r.id), '[]') AS history,
COALESCE((SELECT json_agg(i.impact_type) FROM dsn_request_impact i WHERE i.request_id = r.id), '[]') AS impact
FROM dsn_design_request r
WHERE ${conditions.join(" AND ")}
`;
const result = await query(sql, params);
if (!result.length) { res.status(404).json({ success: false, message: "의뢰를 찾을 수 없습니다." }); return; }
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("설계의뢰 상세 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function createDesignRequest(req: AuthenticatedRequest, res: Response): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const {
request_no, source_type, request_date, due_date, priority, status,
target_name, customer, req_dept, requester, designer, order_no,
design_type, spec, change_type, drawing_no, urgency, reason,
content, apply_timing, review_memo, project_id, ecn_no,
impact, history,
} = req.body;
const sql = `
INSERT INTO dsn_design_request (
request_no, source_type, request_date, due_date, priority, status,
target_name, customer, req_dept, requester, designer, order_no,
design_type, spec, change_type, drawing_no, urgency, reason,
content, apply_timing, review_memo, project_id, ecn_no,
writer, company_code
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25)
RETURNING *
`;
const result = await client.query(sql, [
request_no, source_type || "dr", request_date, due_date, priority || "보통", status || "신규접수",
target_name, customer, req_dept, requester, designer, order_no,
design_type, spec, change_type, drawing_no, urgency || "보통", reason,
content, apply_timing, review_memo, project_id, ecn_no,
userId, companyCode,
]);
const requestId = result.rows[0].id;
if (impact?.length) {
for (const imp of impact) {
await client.query(
`INSERT INTO dsn_request_impact (request_id, impact_type, writer, company_code) VALUES ($1,$2,$3,$4)`,
[requestId, imp, userId, companyCode]
);
}
}
if (history?.length) {
for (const h of history) {
await client.query(
`INSERT INTO dsn_request_history (request_id, step, history_date, user_name, description, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[requestId, h.step, h.history_date, h.user_name, h.description, userId, companyCode]
);
}
}
await client.query("COMMIT");
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("설계의뢰 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
} finally {
client.release();
}
}
export async function updateDesignRequest(req: AuthenticatedRequest, res: Response): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { id } = req.params;
const {
request_no, source_type, request_date, due_date, priority, status, approval_step,
target_name, customer, req_dept, requester, designer, order_no,
design_type, spec, change_type, drawing_no, urgency, reason,
content, apply_timing, review_memo, project_id, ecn_no,
impact, history,
} = req.body;
const conditions = [`id = $1`];
const params: any[] = [id];
let pi = 2;
if (companyCode !== "*") { conditions.push(`company_code = $${pi}`); params.push(companyCode); pi++; }
const setClauses = [];
const setParams: any[] = [];
const fields: Record<string, any> = {
request_no, source_type, request_date, due_date, priority, status, approval_step,
target_name, customer, req_dept, requester, designer, order_no,
design_type, spec, change_type, drawing_no, urgency, reason,
content, apply_timing, review_memo, project_id, ecn_no,
};
for (const [key, val] of Object.entries(fields)) {
if (val !== undefined) {
setClauses.push(`${key} = $${pi}`);
setParams.push(val);
pi++;
}
}
setClauses.push(`updated_date = now()`);
const sql = `UPDATE dsn_design_request SET ${setClauses.join(", ")} WHERE ${conditions.join(" AND ")} RETURNING *`;
const result = await client.query(sql, [...params, ...setParams]);
if (!result.rowCount) { await client.query("ROLLBACK"); res.status(404).json({ success: false, message: "의뢰를 찾을 수 없습니다." }); return; }
if (impact !== undefined) {
await client.query(`DELETE FROM dsn_request_impact WHERE request_id = $1`, [id]);
for (const imp of impact) {
await client.query(
`INSERT INTO dsn_request_impact (request_id, impact_type, writer, company_code) VALUES ($1,$2,$3,$4)`,
[id, imp, userId, companyCode]
);
}
}
if (history !== undefined) {
await client.query(`DELETE FROM dsn_request_history WHERE request_id = $1`, [id]);
for (const h of history) {
await client.query(
`INSERT INTO dsn_request_history (request_id, step, history_date, user_name, description, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[id, h.step, h.history_date, h.user_name, h.description, userId, companyCode]
);
}
}
await client.query("COMMIT");
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("설계의뢰 수정 오류", error);
res.status(500).json({ success: false, message: error.message });
} finally {
client.release();
}
}
export async function deleteDesignRequest(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { id } = req.params;
const conditions = [`id = $1`];
const params: any[] = [id];
if (companyCode !== "*") { conditions.push(`company_code = $2`); params.push(companyCode); }
const sql = `DELETE FROM dsn_design_request WHERE ${conditions.join(" AND ")} RETURNING id`;
const result = await query(sql, params);
if (!result.length) { res.status(404).json({ success: false, message: "의뢰를 찾을 수 없습니다." }); return; }
res.json({ success: true });
} catch (error: any) {
logger.error("설계의뢰 삭제 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// 이력 추가 (단건)
export async function addRequestHistory(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { id } = req.params;
const { step, history_date, user_name, description } = req.body;
const sql = `INSERT INTO dsn_request_history (request_id, step, history_date, user_name, description, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`;
const result = await query(sql, [id, step, history_date, user_name, description, userId, companyCode]);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("의뢰 이력 추가 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// 설계 프로젝트 CRUD
// ============================================
export async function getProjectList(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { status, search } = req.query;
const conditions: string[] = [];
const params: any[] = [];
let pi = 1;
if (companyCode !== "*") { conditions.push(`p.company_code = $${pi}`); params.push(companyCode); pi++; }
if (status) { conditions.push(`p.status = $${pi}`); params.push(status); pi++; }
if (search) {
conditions.push(`(p.name ILIKE $${pi} OR p.project_no ILIKE $${pi} OR p.customer ILIKE $${pi})`);
params.push(`%${search}%`);
pi++;
}
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `
SELECT p.*,
COALESCE(
(SELECT json_agg(json_build_object(
'id', t.id, 'name', t.name, 'category', t.category, 'assignee', t.assignee,
'start_date', t.start_date, 'end_date', t.end_date, 'status', t.status,
'progress', t.progress, 'priority', t.priority, 'remark', t.remark, 'sort_order', t.sort_order
) ORDER BY t.sort_order, t.start_date)
FROM dsn_project_task t WHERE t.project_id = p.id), '[]'
) AS tasks
FROM dsn_project p
${where}
ORDER BY p.created_date DESC
`;
const result = await query(sql, params);
res.json({ success: true, data: result });
} catch (error: any) {
logger.error("프로젝트 목록 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function getProjectDetail(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { id } = req.params;
const conditions = [`p.id = $1`];
const params: any[] = [id];
if (companyCode !== "*") { conditions.push(`p.company_code = $2`); params.push(companyCode); }
const sql = `
SELECT p.*,
COALESCE(
(SELECT json_agg(json_build_object(
'id', t.id, 'name', t.name, 'category', t.category, 'assignee', t.assignee,
'start_date', t.start_date, 'end_date', t.end_date, 'status', t.status,
'progress', t.progress, 'priority', t.priority, 'remark', t.remark, 'sort_order', t.sort_order
) ORDER BY t.sort_order, t.start_date)
FROM dsn_project_task t WHERE t.project_id = p.id), '[]'
) AS tasks
FROM dsn_project p
WHERE ${conditions.join(" AND ")}
`;
const result = await query(sql, params);
if (!result.length) { res.status(404).json({ success: false, message: "프로젝트를 찾을 수 없습니다." }); return; }
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("프로젝트 상세 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function createProject(req: AuthenticatedRequest, res: Response): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { project_no, name, status: pStatus, pm, customer, start_date, end_date, source_no, description, progress, parent_id, relation_type, tasks } = req.body;
const result = await client.query(
`INSERT INTO dsn_project (project_no, name, status, pm, customer, start_date, end_date, source_no, description, progress, parent_id, relation_type, writer, company_code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`,
[project_no, name, pStatus || "계획", pm, customer, start_date, end_date, source_no, description, progress || "0", parent_id, relation_type, userId, companyCode]
);
const projectId = result.rows[0].id;
if (tasks?.length) {
for (let i = 0; i < tasks.length; i++) {
const t = tasks[i];
await client.query(
`INSERT INTO dsn_project_task (project_id, name, category, assignee, start_date, end_date, status, progress, priority, remark, sort_order, writer, company_code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`,
[projectId, t.name, t.category, t.assignee, t.start_date, t.end_date, t.status || "대기", t.progress || "0", t.priority || "보통", t.remark, String(i), userId, companyCode]
);
}
}
await client.query("COMMIT");
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("프로젝트 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
} finally {
client.release();
}
}
export async function updateProject(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { id } = req.params;
const { project_no, name, status: pStatus, pm, customer, start_date, end_date, source_no, description, progress, parent_id, relation_type } = req.body;
const conditions = [`id = $1`];
const params: any[] = [id];
let pi = 2;
if (companyCode !== "*") { conditions.push(`company_code = $${pi}`); params.push(companyCode); pi++; }
const sets: string[] = [];
const fields: Record<string, any> = { project_no, name, status: pStatus, pm, customer, start_date, end_date, source_no, description, progress, parent_id, relation_type };
for (const [key, val] of Object.entries(fields)) {
if (val !== undefined) { sets.push(`${key} = $${pi}`); params.push(val); pi++; }
}
sets.push(`updated_date = now()`);
const result = await query(`UPDATE dsn_project SET ${sets.join(", ")} WHERE ${conditions.join(" AND ")} RETURNING *`, params);
if (!result.length) { res.status(404).json({ success: false, message: "프로젝트를 찾을 수 없습니다." }); return; }
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("프로젝트 수정 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function deleteProject(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { id } = req.params;
const conditions = [`id = $1`];
const params: any[] = [id];
if (companyCode !== "*") { conditions.push(`company_code = $2`); params.push(companyCode); }
const result = await query(`DELETE FROM dsn_project WHERE ${conditions.join(" AND ")} RETURNING id`, params);
if (!result.length) { res.status(404).json({ success: false, message: "프로젝트를 찾을 수 없습니다." }); return; }
res.json({ success: true });
} catch (error: any) {
logger.error("프로젝트 삭제 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// 프로젝트 태스크 CRUD
// ============================================
export async function getTasksByProject(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { projectId } = req.params;
const conditions = [`t.project_id = $1`];
const params: any[] = [projectId];
if (companyCode !== "*") { conditions.push(`t.company_code = $2`); params.push(companyCode); }
const sql = `
SELECT t.*,
COALESCE((SELECT json_agg(json_build_object('id', w.id, 'start_dt', w.start_dt, 'end_dt', w.end_dt, 'hours', w.hours, 'description', w.description, 'progress_before', w.progress_before, 'progress_after', w.progress_after, 'author', w.author, 'sub_item_id', w.sub_item_id) ORDER BY w.start_dt) FROM dsn_work_log w WHERE w.task_id = t.id), '[]') AS work_logs,
COALESCE((SELECT json_agg(json_build_object('id', i.id, 'title', i.title, 'status', i.status, 'priority', i.priority, 'description', i.description, 'registered_by', i.registered_by, 'registered_date', i.registered_date, 'resolved_date', i.resolved_date)) FROM dsn_task_issue i WHERE i.task_id = t.id), '[]') AS issues,
COALESCE((SELECT json_agg(json_build_object('id', s.id, 'name', s.name, 'weight', s.weight, 'progress', s.progress, 'status', s.status) ORDER BY s.created_date) FROM dsn_task_sub_item s WHERE s.task_id = t.id), '[]') AS sub_items
FROM dsn_project_task t
WHERE ${conditions.join(" AND ")}
ORDER BY t.sort_order, t.start_date
`;
const result = await query(sql, params);
res.json({ success: true, data: result });
} catch (error: any) {
logger.error("태스크 목록 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function createTask(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { projectId } = req.params;
const { name, category, assignee, start_date, end_date, status, progress, priority, remark, sort_order } = req.body;
const result = await query(
`INSERT INTO dsn_project_task (project_id, name, category, assignee, start_date, end_date, status, progress, priority, remark, sort_order, writer, company_code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`,
[projectId, name, category, assignee, start_date, end_date, status || "대기", progress || "0", priority || "보통", remark, sort_order || "0", userId, companyCode]
);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("태스크 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function updateTask(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { taskId } = req.params;
const { name, category, assignee, start_date, end_date, status, progress, priority, remark, sort_order } = req.body;
const conditions = [`id = $1`];
const params: any[] = [taskId];
let pi = 2;
if (companyCode !== "*") { conditions.push(`company_code = $${pi}`); params.push(companyCode); pi++; }
const sets: string[] = [];
const fields: Record<string, any> = { name, category, assignee, start_date, end_date, status, progress, priority, remark, sort_order };
for (const [key, val] of Object.entries(fields)) {
if (val !== undefined) { sets.push(`${key} = $${pi}`); params.push(val); pi++; }
}
sets.push(`updated_date = now()`);
const result = await query(`UPDATE dsn_project_task SET ${sets.join(", ")} WHERE ${conditions.join(" AND ")} RETURNING *`, params);
if (!result.length) { res.status(404).json({ success: false, message: "태스크를 찾을 수 없습니다." }); return; }
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("태스크 수정 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function deleteTask(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { taskId } = req.params;
const conditions = [`id = $1`];
const params: any[] = [taskId];
if (companyCode !== "*") { conditions.push(`company_code = $2`); params.push(companyCode); }
const result = await query(`DELETE FROM dsn_project_task WHERE ${conditions.join(" AND ")} RETURNING id`, params);
if (!result.length) { res.status(404).json({ success: false, message: "태스크를 찾을 수 없습니다." }); return; }
res.json({ success: true });
} catch (error: any) {
logger.error("태스크 삭제 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// 작업일지 CRUD
// ============================================
export async function getWorkLogsByTask(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { taskId } = req.params;
const conditions = [`w.task_id = $1`];
const params: any[] = [taskId];
if (companyCode !== "*") { conditions.push(`w.company_code = $2`); params.push(companyCode); }
const sql = `
SELECT w.*,
COALESCE((SELECT json_agg(json_build_object('id', a.id, 'file_name', a.file_name, 'file_type', a.file_type, 'file_size', a.file_size)) FROM dsn_work_attachment a WHERE a.work_log_id = w.id), '[]') AS attachments,
COALESCE((SELECT json_agg(json_build_object('id', p.id, 'item', p.item, 'qty', p.qty, 'unit', p.unit, 'reason', p.reason, 'status', p.status)) FROM dsn_purchase_req p WHERE p.work_log_id = w.id), '[]') AS purchase_reqs,
COALESCE((SELECT json_agg(json_build_object(
'id', c.id, 'to_user', c.to_user, 'to_dept', c.to_dept, 'title', c.title, 'description', c.description, 'status', c.status, 'due_date', c.due_date,
'responses', COALESCE((SELECT json_agg(json_build_object('id', cr.id, 'response_date', cr.response_date, 'user_name', cr.user_name, 'content', cr.content)) FROM dsn_coop_response cr WHERE cr.coop_req_id = c.id), '[]')
)) FROM dsn_coop_req c WHERE c.work_log_id = w.id), '[]') AS coop_reqs
FROM dsn_work_log w
WHERE ${conditions.join(" AND ")}
ORDER BY w.start_dt DESC
`;
const result = await query(sql, params);
res.json({ success: true, data: result });
} catch (error: any) {
logger.error("작업일지 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function createWorkLog(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { taskId } = req.params;
const { start_dt, end_dt, hours, description, progress_before, progress_after, author, sub_item_id } = req.body;
const result = await query(
`INSERT INTO dsn_work_log (task_id, start_dt, end_dt, hours, description, progress_before, progress_after, author, sub_item_id, writer, company_code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING *`,
[taskId, start_dt, end_dt, hours || "0", description, progress_before || "0", progress_after || "0", author, sub_item_id, userId, companyCode]
);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("작업일지 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function deleteWorkLog(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { workLogId } = req.params;
const conditions = [`id = $1`];
const params: any[] = [workLogId];
if (companyCode !== "*") { conditions.push(`company_code = $2`); params.push(companyCode); }
const result = await query(`DELETE FROM dsn_work_log WHERE ${conditions.join(" AND ")} RETURNING id`, params);
if (!result.length) { res.status(404).json({ success: false, message: "작업일지를 찾을 수 없습니다." }); return; }
res.json({ success: true });
} catch (error: any) {
logger.error("작업일지 삭제 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// 태스크 하위항목 CRUD
// ============================================
export async function createSubItem(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { taskId } = req.params;
const { name, weight, progress, status } = req.body;
const result = await query(
`INSERT INTO dsn_task_sub_item (task_id, name, weight, progress, status, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
[taskId, name, weight || "0", progress || "0", status || "대기", userId, companyCode]
);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("하위항목 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function updateSubItem(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { subItemId } = req.params;
const { name, weight, progress, status } = req.body;
const conditions = [`id = $1`];
const params: any[] = [subItemId];
let pi = 2;
if (companyCode !== "*") { conditions.push(`company_code = $${pi}`); params.push(companyCode); pi++; }
const sets: string[] = [];
const fields: Record<string, any> = { name, weight, progress, status };
for (const [key, val] of Object.entries(fields)) {
if (val !== undefined) { sets.push(`${key} = $${pi}`); params.push(val); pi++; }
}
sets.push(`updated_date = now()`);
const result = await query(`UPDATE dsn_task_sub_item SET ${sets.join(", ")} WHERE ${conditions.join(" AND ")} RETURNING *`, params);
if (!result.length) { res.status(404).json({ success: false, message: "하위항목을 찾을 수 없습니다." }); return; }
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("하위항목 수정 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function deleteSubItem(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { subItemId } = req.params;
const conditions = [`id = $1`];
const params: any[] = [subItemId];
if (companyCode !== "*") { conditions.push(`company_code = $2`); params.push(companyCode); }
const result = await query(`DELETE FROM dsn_task_sub_item WHERE ${conditions.join(" AND ")} RETURNING id`, params);
if (!result.length) { res.status(404).json({ success: false, message: "하위항목을 찾을 수 없습니다." }); return; }
res.json({ success: true });
} catch (error: any) {
logger.error("하위항목 삭제 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// 태스크 이슈 CRUD
// ============================================
export async function createIssue(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { taskId } = req.params;
const { title, status, priority, description, registered_by, registered_date } = req.body;
const result = await query(
`INSERT INTO dsn_task_issue (task_id, title, status, priority, description, registered_by, registered_date, writer, company_code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`,
[taskId, title, status || "등록", priority || "보통", description, registered_by, registered_date, userId, companyCode]
);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("이슈 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function updateIssue(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { issueId } = req.params;
const { title, status, priority, description, resolved_date } = req.body;
const conditions = [`id = $1`];
const params: any[] = [issueId];
let pi = 2;
if (companyCode !== "*") { conditions.push(`company_code = $${pi}`); params.push(companyCode); pi++; }
const sets: string[] = [];
const fields: Record<string, any> = { title, status, priority, description, resolved_date };
for (const [key, val] of Object.entries(fields)) {
if (val !== undefined) { sets.push(`${key} = $${pi}`); params.push(val); pi++; }
}
sets.push(`updated_date = now()`);
const result = await query(`UPDATE dsn_task_issue SET ${sets.join(", ")} WHERE ${conditions.join(" AND ")} RETURNING *`, params);
if (!result.length) { res.status(404).json({ success: false, message: "이슈를 찾을 수 없습니다." }); return; }
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("이슈 수정 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// ECN (설변통보) CRUD
// ============================================
export async function getEcnList(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { status, search } = req.query;
const conditions: string[] = [];
const params: any[] = [];
let pi = 1;
if (companyCode !== "*") { conditions.push(`e.company_code = $${pi}`); params.push(companyCode); pi++; }
if (status) { conditions.push(`e.status = $${pi}`); params.push(status); pi++; }
if (search) {
conditions.push(`(e.ecn_no ILIKE $${pi} OR e.target ILIKE $${pi})`);
params.push(`%${search}%`);
pi++;
}
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `
SELECT e.*,
COALESCE((SELECT json_agg(json_build_object('id', h.id, 'status', h.status, 'history_date', h.history_date, 'user_name', h.user_name, 'description', h.description) ORDER BY h.created_date) FROM dsn_ecn_history h WHERE h.ecn_id = e.id), '[]') AS history,
COALESCE((SELECT json_agg(nd.dept_name) FROM dsn_ecn_notify_dept nd WHERE nd.ecn_id = e.id), '[]') AS notify_depts
FROM dsn_ecn e
${where}
ORDER BY e.created_date DESC
`;
const result = await query(sql, params);
res.json({ success: true, data: result });
} catch (error: any) {
logger.error("ECN 목록 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function createEcn(req: AuthenticatedRequest, res: Response): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { ecn_no, ecr_id, ecn_date, apply_date, status, target, drawing_before, drawing_after, designer, before_content, after_content, reason, remark, notify_depts, history } = req.body;
const result = await client.query(
`INSERT INTO dsn_ecn (ecn_no, ecr_id, ecn_date, apply_date, status, target, drawing_before, drawing_after, designer, before_content, after_content, reason, remark, writer, company_code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING *`,
[ecn_no, ecr_id, ecn_date, apply_date, status || "ECN발행", target, drawing_before, drawing_after, designer, before_content, after_content, reason, remark, userId, companyCode]
);
const ecnId = result.rows[0].id;
if (notify_depts?.length) {
for (const dept of notify_depts) {
await client.query(`INSERT INTO dsn_ecn_notify_dept (ecn_id, dept_name, writer, company_code) VALUES ($1,$2,$3,$4)`, [ecnId, dept, userId, companyCode]);
}
}
if (history?.length) {
for (const h of history) {
await client.query(
`INSERT INTO dsn_ecn_history (ecn_id, status, history_date, user_name, description, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[ecnId, h.status, h.history_date, h.user_name, h.description, userId, companyCode]
);
}
}
await client.query("COMMIT");
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("ECN 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
} finally {
client.release();
}
}
export async function updateEcn(req: AuthenticatedRequest, res: Response): Promise<void> {
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { id } = req.params;
const { ecn_no, ecn_date, apply_date, status, target, drawing_before, drawing_after, designer, before_content, after_content, reason, remark, notify_depts, history } = req.body;
const conditions = [`id = $1`];
const params: any[] = [id];
let pi = 2;
if (companyCode !== "*") { conditions.push(`company_code = $${pi}`); params.push(companyCode); pi++; }
const sets: string[] = [];
const fields: Record<string, any> = { ecn_no, ecn_date, apply_date, status, target, drawing_before, drawing_after, designer, before_content, after_content, reason, remark };
for (const [key, val] of Object.entries(fields)) {
if (val !== undefined) { sets.push(`${key} = $${pi}`); params.push(val); pi++; }
}
sets.push(`updated_date = now()`);
const result = await client.query(`UPDATE dsn_ecn SET ${sets.join(", ")} WHERE ${conditions.join(" AND ")} RETURNING *`, params);
if (!result.rowCount) { await client.query("ROLLBACK"); res.status(404).json({ success: false, message: "ECN을 찾을 수 없습니다." }); return; }
if (notify_depts !== undefined) {
await client.query(`DELETE FROM dsn_ecn_notify_dept WHERE ecn_id = $1`, [id]);
for (const dept of notify_depts) {
await client.query(`INSERT INTO dsn_ecn_notify_dept (ecn_id, dept_name, writer, company_code) VALUES ($1,$2,$3,$4)`, [id, dept, userId, companyCode]);
}
}
if (history !== undefined) {
await client.query(`DELETE FROM dsn_ecn_history WHERE ecn_id = $1`, [id]);
for (const h of history) {
await client.query(
`INSERT INTO dsn_ecn_history (ecn_id, status, history_date, user_name, description, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[id, h.status, h.history_date, h.user_name, h.description, userId, companyCode]
);
}
}
await client.query("COMMIT");
res.json({ success: true, data: result.rows[0] });
} catch (error: any) {
await client.query("ROLLBACK");
logger.error("ECN 수정 오류", error);
res.status(500).json({ success: false, message: error.message });
} finally {
client.release();
}
}
export async function deleteEcn(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const { id } = req.params;
const conditions = [`id = $1`];
const params: any[] = [id];
if (companyCode !== "*") { conditions.push(`company_code = $2`); params.push(companyCode); }
const result = await query(`DELETE FROM dsn_ecn WHERE ${conditions.join(" AND ")} RETURNING id`, params);
if (!result.length) { res.status(404).json({ success: false, message: "ECN을 찾을 수 없습니다." }); return; }
res.json({ success: true });
} catch (error: any) {
logger.error("ECN 삭제 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// 나의 업무 (My Work) - 로그인 사용자 기준
// ============================================
export async function getMyWork(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userName = req.user!.userName;
const { status, project_id } = req.query;
const conditions = [`t.assignee = $1`];
const params: any[] = [userName];
let pi = 2;
if (companyCode !== "*") { conditions.push(`t.company_code = $${pi}`); params.push(companyCode); pi++; }
if (status) { conditions.push(`t.status = $${pi}`); params.push(status); pi++; }
if (project_id) { conditions.push(`t.project_id = $${pi}`); params.push(project_id); pi++; }
const sql = `
SELECT t.*,
p.project_no, p.name AS project_name, p.customer AS project_customer, p.status AS project_status,
COALESCE((SELECT json_agg(json_build_object('id', s.id, 'name', s.name, 'weight', s.weight, 'progress', s.progress, 'status', s.status) ORDER BY s.created_date) FROM dsn_task_sub_item s WHERE s.task_id = t.id), '[]') AS sub_items,
COALESCE((SELECT json_agg(json_build_object(
'id', w.id, 'start_dt', w.start_dt, 'end_dt', w.end_dt, 'hours', w.hours, 'description', w.description, 'sub_item_id', w.sub_item_id,
'attachments', COALESCE((SELECT json_agg(json_build_object('id', a.id, 'file_name', a.file_name, 'file_type', a.file_type, 'file_size', a.file_size)) FROM dsn_work_attachment a WHERE a.work_log_id = w.id), '[]'),
'purchase_reqs', COALESCE((SELECT json_agg(json_build_object('id', pr.id, 'item', pr.item, 'qty', pr.qty, 'unit', pr.unit, 'reason', pr.reason, 'status', pr.status)) FROM dsn_purchase_req pr WHERE pr.work_log_id = w.id), '[]'),
'coop_reqs', COALESCE((SELECT json_agg(json_build_object(
'id', c.id, 'to_user', c.to_user, 'to_dept', c.to_dept, 'title', c.title, 'description', c.description, 'status', c.status, 'due_date', c.due_date,
'responses', COALESCE((SELECT json_agg(json_build_object('id', cr.id, 'response_date', cr.response_date, 'user_name', cr.user_name, 'content', cr.content)) FROM dsn_coop_response cr WHERE cr.coop_req_id = c.id), '[]')
)) FROM dsn_coop_req c WHERE c.work_log_id = w.id), '[]')
) ORDER BY w.start_dt DESC) FROM dsn_work_log w WHERE w.task_id = t.id), '[]') AS work_logs
FROM dsn_project_task t
JOIN dsn_project p ON p.id = t.project_id
WHERE ${conditions.join(" AND ")}
ORDER BY
CASE t.status WHEN '진행중' THEN 1 WHEN '대기' THEN 2 WHEN '검토중' THEN 3 ELSE 4 END,
t.end_date ASC
`;
const result = await query(sql, params);
res.json({ success: true, data: result });
} catch (error: any) {
logger.error("나의 업무 조회 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
// ============================================
// 구매요청 / 협업요청 CRUD (my-work에서 사용)
// ============================================
export async function createPurchaseReq(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { workLogId } = req.params;
const { item, qty, unit, reason, status } = req.body;
const result = await query(
`INSERT INTO dsn_purchase_req (work_log_id, item, qty, unit, reason, status, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
[workLogId, item, qty, unit, reason, status || "요청", userId, companyCode]
);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("구매요청 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function createCoopReq(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { workLogId } = req.params;
const { to_user, to_dept, title, description, due_date } = req.body;
const result = await query(
`INSERT INTO dsn_coop_req (work_log_id, to_user, to_dept, title, description, status, due_date, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`,
[workLogId, to_user, to_dept, title, description, "요청", due_date, userId, companyCode]
);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("협업요청 생성 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}
export async function addCoopResponse(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const companyCode = req.user!.companyCode!;
const userId = req.user!.userId;
const { coopReqId } = req.params;
const { response_date, user_name, content } = req.body;
const result = await query(
`INSERT INTO dsn_coop_response (coop_req_id, response_date, user_name, content, writer, company_code) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
[coopReqId, response_date, user_name, content, userId, companyCode]
);
res.json({ success: true, data: result[0] });
} catch (error: any) {
logger.error("협업응답 추가 오류", error);
res.status(500).json({ success: false, message: error.message });
}
}