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 { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { 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 { 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 { 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 }); } }