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.
This commit is contained in:
946
backend-node/src/controllers/designController.ts
Normal file
946
backend-node/src/controllers/designController.ts
Normal file
@@ -0,0 +1,946 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
482
backend-node/src/controllers/shippingOrderController.ts
Normal file
482
backend-node/src/controllers/shippingOrderController.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* 출하지시 컨트롤러 (shipment_instruction + shipment_instruction_detail)
|
||||
*/
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { numberingRuleService } from "../services/numberingRuleService";
|
||||
|
||||
// ─── 출하지시 목록 조회 ───
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { dateFrom, dateTo, status, customer, keyword } = req.query;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (companyCode !== "*") {
|
||||
conditions.push(`si.company_code = $${idx}`);
|
||||
params.push(companyCode);
|
||||
idx++;
|
||||
}
|
||||
if (dateFrom) {
|
||||
conditions.push(`si.instruction_date >= $${idx}::date`);
|
||||
params.push(dateFrom);
|
||||
idx++;
|
||||
}
|
||||
if (dateTo) {
|
||||
conditions.push(`si.instruction_date <= $${idx}::date`);
|
||||
params.push(dateTo);
|
||||
idx++;
|
||||
}
|
||||
if (status) {
|
||||
conditions.push(`si.status = $${idx}`);
|
||||
params.push(status);
|
||||
idx++;
|
||||
}
|
||||
if (customer) {
|
||||
conditions.push(`(c.customer_name ILIKE $${idx} OR si.partner_id ILIKE $${idx})`);
|
||||
params.push(`%${customer}%`);
|
||||
idx++;
|
||||
}
|
||||
if (keyword) {
|
||||
conditions.push(`(si.instruction_no ILIKE $${idx} OR si.memo ILIKE $${idx})`);
|
||||
params.push(`%${keyword}%`);
|
||||
idx++;
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
si.*,
|
||||
COALESCE(c.customer_name, si.partner_id, '') AS customer_name,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', sid.id,
|
||||
'item_code', sid.item_code,
|
||||
'item_name', COALESCE(i.item_name, sid.item_name, sid.item_code),
|
||||
'spec', sid.spec,
|
||||
'material', sid.material,
|
||||
'order_qty', sid.order_qty,
|
||||
'plan_qty', sid.plan_qty,
|
||||
'ship_qty', sid.ship_qty,
|
||||
'source_type', sid.source_type,
|
||||
'shipment_plan_id', sid.shipment_plan_id,
|
||||
'sales_order_id', sid.sales_order_id,
|
||||
'detail_id', sid.detail_id
|
||||
)
|
||||
) FILTER (WHERE sid.id IS NOT NULL),
|
||||
'[]'
|
||||
) AS items
|
||||
FROM shipment_instruction si
|
||||
LEFT JOIN customer_mng c
|
||||
ON si.partner_id = c.customer_code AND si.company_code = c.company_code
|
||||
LEFT JOIN shipment_instruction_detail sid
|
||||
ON si.id = sid.instruction_id AND si.company_code = sid.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
WHERE item_number = sid.item_code AND company_code = si.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
${where}
|
||||
GROUP BY si.id, c.customer_name
|
||||
ORDER BY si.created_date DESC
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("출하지시 목록 조회", { companyCode, count: result.rowCount });
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("출하지시 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 다음 출하지시번호 미리보기 ───
|
||||
export async function previewNextNo(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
let instructionNo: string;
|
||||
|
||||
try {
|
||||
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
||||
companyCode, "shipment_instruction", "instruction_no"
|
||||
);
|
||||
if (rule) {
|
||||
instructionNo = await numberingRuleService.previewCode(
|
||||
rule.ruleId, companyCode, {}
|
||||
);
|
||||
} else {
|
||||
throw new Error("채번 규칙 없음");
|
||||
}
|
||||
} catch {
|
||||
const pool = getPool();
|
||||
const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
||||
const seqRes = await pool.query(
|
||||
`SELECT COUNT(*) + 1 AS seq FROM shipment_instruction WHERE company_code = $1 AND instruction_no LIKE $2`,
|
||||
[companyCode, `SI-${today}-%`]
|
||||
);
|
||||
const seq = String(seqRes.rows[0].seq).padStart(3, "0");
|
||||
instructionNo = `SI-${today}-${seq}`;
|
||||
}
|
||||
|
||||
return res.json({ success: true, instructionNo });
|
||||
} catch (error: any) {
|
||||
logger.error("출하지시번호 미리보기 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 출하지시 저장 (신규/수정) ───
|
||||
export async function save(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const {
|
||||
id: editId,
|
||||
instructionDate,
|
||||
partnerId,
|
||||
status: orderStatus,
|
||||
memo,
|
||||
carrierName,
|
||||
vehicleNo,
|
||||
driverName,
|
||||
driverContact,
|
||||
arrivalTime,
|
||||
deliveryAddress,
|
||||
items,
|
||||
} = req.body;
|
||||
|
||||
if (!instructionDate) {
|
||||
return res.status(400).json({ success: false, message: "출하지시일은 필수입니다" });
|
||||
}
|
||||
if (!items || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "품목을 선택해주세요" });
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
let instructionId: number;
|
||||
let instructionNo: string;
|
||||
|
||||
if (editId) {
|
||||
// 수정
|
||||
const check = await client.query(
|
||||
`SELECT id, instruction_no FROM shipment_instruction WHERE id = $1 AND company_code = $2`,
|
||||
[editId, companyCode]
|
||||
);
|
||||
if (check.rowCount === 0) {
|
||||
throw new Error("출하지시를 찾을 수 없습니다");
|
||||
}
|
||||
instructionId = editId;
|
||||
instructionNo = check.rows[0].instruction_no;
|
||||
|
||||
await client.query(
|
||||
`UPDATE shipment_instruction SET
|
||||
instruction_date = $1::date, partner_id = $2, status = $3, memo = $4,
|
||||
carrier_name = $5, vehicle_no = $6, driver_name = $7, driver_contact = $8,
|
||||
arrival_time = $9, delivery_address = $10,
|
||||
updated_date = NOW(), updated_by = $11
|
||||
WHERE id = $12 AND company_code = $13`,
|
||||
[
|
||||
instructionDate, partnerId, orderStatus || "READY", memo,
|
||||
carrierName, vehicleNo, driverName, driverContact,
|
||||
arrivalTime || null, deliveryAddress,
|
||||
userId, editId, companyCode,
|
||||
]
|
||||
);
|
||||
|
||||
// 기존 디테일 삭제 후 재삽입
|
||||
await client.query(
|
||||
`DELETE FROM shipment_instruction_detail WHERE instruction_id = $1 AND company_code = $2`,
|
||||
[editId, companyCode]
|
||||
);
|
||||
} else {
|
||||
// 신규 - 채번 규칙이 있으면 사용, 없으면 자체 생성
|
||||
try {
|
||||
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
||||
companyCode, "shipment_instruction", "instruction_no"
|
||||
);
|
||||
if (rule) {
|
||||
instructionNo = await numberingRuleService.allocateCode(
|
||||
rule.ruleId, companyCode, { instruction_date: instructionDate }
|
||||
);
|
||||
logger.info("채번 규칙으로 출하지시번호 생성", { ruleId: rule.ruleId, instructionNo });
|
||||
} else {
|
||||
throw new Error("채번 규칙 없음 - 폴백");
|
||||
}
|
||||
} catch {
|
||||
const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
||||
const seqRes = await client.query(
|
||||
`SELECT COUNT(*) + 1 AS seq FROM shipment_instruction WHERE company_code = $1 AND instruction_no LIKE $2`,
|
||||
[companyCode, `SI-${today}-%`]
|
||||
);
|
||||
const seq = String(seqRes.rows[0].seq).padStart(3, "0");
|
||||
instructionNo = `SI-${today}-${seq}`;
|
||||
logger.info("폴백으로 출하지시번호 생성", { instructionNo });
|
||||
}
|
||||
|
||||
const insertRes = await client.query(
|
||||
`INSERT INTO shipment_instruction
|
||||
(company_code, instruction_no, instruction_date, partner_id, status, memo,
|
||||
carrier_name, vehicle_no, driver_name, driver_contact, arrival_time, delivery_address,
|
||||
created_date, created_by)
|
||||
VALUES ($1, $2, $3::date, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), $13)
|
||||
RETURNING id`,
|
||||
[
|
||||
companyCode, instructionNo, instructionDate, partnerId,
|
||||
orderStatus || "READY", memo,
|
||||
carrierName, vehicleNo, driverName, driverContact,
|
||||
arrivalTime || null, deliveryAddress, userId,
|
||||
]
|
||||
);
|
||||
instructionId = insertRes.rows[0].id;
|
||||
}
|
||||
|
||||
// 디테일 삽입
|
||||
for (const item of items) {
|
||||
await client.query(
|
||||
`INSERT INTO shipment_instruction_detail
|
||||
(company_code, instruction_id, shipment_plan_id, sales_order_id, detail_id,
|
||||
item_code, item_name, spec, material, order_qty, plan_qty, ship_qty,
|
||||
source_type, created_date, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), $14)`,
|
||||
[
|
||||
companyCode, instructionId,
|
||||
item.shipmentPlanId || null, item.salesOrderId || null, item.detailId || null,
|
||||
item.itemCode, item.itemName, item.spec, item.material,
|
||||
item.orderQty || 0, item.planQty || 0, item.shipQty || 0,
|
||||
item.sourceType || "shipmentPlan", userId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("출하지시 저장 완료", { companyCode, instructionId, instructionNo, itemCount: items.length });
|
||||
return res.json({ success: true, data: { id: instructionId, instructionNo } });
|
||||
} catch (txErr) {
|
||||
await client.query("ROLLBACK");
|
||||
throw txErr;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("출하지시 저장 실패", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 출하지시 삭제 ───
|
||||
export async function remove(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "삭제할 ID가 필요합니다" });
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
// CASCADE로 디테일도 자동 삭제
|
||||
const result = await pool.query(
|
||||
`DELETE FROM shipment_instruction WHERE id = ANY($1::int[]) AND company_code = $2 RETURNING id`,
|
||||
[ids, companyCode]
|
||||
);
|
||||
|
||||
logger.info("출하지시 삭제", { companyCode, deletedCount: result.rowCount });
|
||||
return res.json({ success: true, deletedCount: result.rowCount });
|
||||
} catch (error: any) {
|
||||
logger.error("출하지시 삭제 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 출하계획 목록 (모달 왼쪽 패널용) ───
|
||||
export async function getShipmentPlanSource(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword, customer, page: pageStr, pageSize: pageSizeStr } = req.query;
|
||||
const page = Math.max(1, parseInt(pageStr as string) || 1);
|
||||
const pageSize = Math.min(100, Math.max(1, parseInt(pageSizeStr as string) || 20));
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const conditions = ["sp.company_code = $1", "sp.status = 'READY'"];
|
||||
const params: any[] = [companyCode];
|
||||
let idx = 2;
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(`(COALESCE(d.part_code, m.part_code, '') ILIKE $${idx} OR COALESCE(i.item_name, d.part_name, m.part_name, '') ILIKE $${idx})`);
|
||||
params.push(`%${keyword}%`);
|
||||
idx++;
|
||||
}
|
||||
if (customer) {
|
||||
conditions.push(`(c.customer_name ILIKE $${idx} OR COALESCE(m.partner_id, d.delivery_partner_code, '') ILIKE $${idx})`);
|
||||
params.push(`%${customer}%`);
|
||||
idx++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(" AND ");
|
||||
const fromClause = `
|
||||
FROM shipment_plan sp
|
||||
LEFT JOIN sales_order_detail d ON sp.detail_id = d.id AND sp.company_code = d.company_code
|
||||
LEFT JOIN sales_order_mng m ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
WHERE item_number = COALESCE(d.part_code, m.part_code) AND company_code = sp.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
LEFT JOIN customer_mng c
|
||||
ON COALESCE(m.partner_id, d.delivery_partner_code) = c.customer_code AND sp.company_code = c.company_code
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
const countResult = await pool.query(`SELECT COUNT(*) AS total ${fromClause}`, params);
|
||||
const totalCount = parseInt(countResult.rows[0].total);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
sp.id, sp.plan_qty, sp.plan_date, sp.status, sp.shipment_plan_no,
|
||||
COALESCE(m.order_no, d.order_no, '') AS order_no,
|
||||
COALESCE(d.part_code, m.part_code, '') AS item_code,
|
||||
COALESCE(i.item_name, d.part_name, m.part_name, COALESCE(d.part_code, m.part_code, '')) AS item_name,
|
||||
COALESCE(d.spec, m.spec, '') AS spec,
|
||||
COALESCE(m.material, '') AS material,
|
||||
COALESCE(c.customer_name, m.partner_id, d.delivery_partner_code, '') AS customer_name,
|
||||
COALESCE(m.partner_id, d.delivery_partner_code, '') AS partner_code,
|
||||
sp.detail_id, sp.sales_order_id
|
||||
${fromClause}
|
||||
ORDER BY sp.created_date DESC
|
||||
LIMIT $${idx} OFFSET $${idx + 1}
|
||||
`;
|
||||
params.push(pageSize, offset);
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
return res.json({ success: true, data: result.rows, totalCount, page, pageSize });
|
||||
} catch (error: any) {
|
||||
logger.error("출하계획 소스 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 수주 목록 (모달 왼쪽 패널용) ───
|
||||
export async function getSalesOrderSource(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword, customer, page: pageStr, pageSize: pageSizeStr } = req.query;
|
||||
const page = Math.max(1, parseInt(pageStr as string) || 1);
|
||||
const pageSize = Math.min(100, Math.max(1, parseInt(pageSizeStr as string) || 20));
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const conditions = ["d.company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let idx = 2;
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(`(d.part_code ILIKE $${idx} OR COALESCE(i.item_name, d.part_name, d.part_code) ILIKE $${idx} OR d.order_no ILIKE $${idx})`);
|
||||
params.push(`%${keyword}%`);
|
||||
idx++;
|
||||
}
|
||||
if (customer) {
|
||||
conditions.push(`(c.customer_name ILIKE $${idx} OR COALESCE(d.delivery_partner_code, m.partner_id, '') ILIKE $${idx})`);
|
||||
params.push(`%${customer}%`);
|
||||
idx++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(" AND ");
|
||||
const fromClause = `
|
||||
FROM sales_order_detail d
|
||||
LEFT JOIN sales_order_mng m ON d.order_no = m.order_no AND d.company_code = m.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
WHERE item_number = d.part_code AND company_code = d.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
LEFT JOIN customer_mng c
|
||||
ON COALESCE(d.delivery_partner_code, m.partner_id) = c.customer_code AND d.company_code = c.company_code
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
const countResult = await pool.query(`SELECT COUNT(*) AS total ${fromClause}`, params);
|
||||
const totalCount = parseInt(countResult.rows[0].total);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
d.id, d.order_no, d.part_code AS item_code,
|
||||
COALESCE(i.item_name, d.part_name, d.part_code) AS item_name,
|
||||
COALESCE(d.spec, '') AS spec, COALESCE(m.material, '') AS material,
|
||||
COALESCE(NULLIF(d.qty,'')::numeric, 0) AS qty,
|
||||
COALESCE(NULLIF(d.balance_qty,'')::numeric, 0) AS balance_qty,
|
||||
COALESCE(c.customer_name, COALESCE(d.delivery_partner_code, m.partner_id, '')) AS customer_name,
|
||||
COALESCE(d.delivery_partner_code, m.partner_id, '') AS partner_code,
|
||||
m.id AS master_id
|
||||
${fromClause}
|
||||
ORDER BY d.created_date DESC
|
||||
LIMIT $${idx} OFFSET $${idx + 1}
|
||||
`;
|
||||
params.push(pageSize, offset);
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
return res.json({ success: true, data: result.rows, totalCount, page, pageSize });
|
||||
} catch (error: any) {
|
||||
logger.error("수주 소스 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 품목 목록 (모달 왼쪽 패널용) ───
|
||||
export async function getItemSource(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword, page: pageStr, pageSize: pageSizeStr } = req.query;
|
||||
const page = Math.max(1, parseInt(pageStr as string) || 1);
|
||||
const pageSize = Math.min(100, Math.max(1, parseInt(pageSizeStr as string) || 20));
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const conditions = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let idx = 2;
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(`(item_number ILIKE $${idx} OR item_name ILIKE $${idx})`);
|
||||
params.push(`%${keyword}%`);
|
||||
idx++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(" AND ");
|
||||
|
||||
const pool = getPool();
|
||||
const countResult = await pool.query(`SELECT COUNT(*) AS total FROM item_info WHERE ${whereClause}`, params);
|
||||
const totalCount = parseInt(countResult.rows[0].total);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
item_number AS item_code, item_name,
|
||||
COALESCE(size, '') AS spec, COALESCE(material, '') AS material
|
||||
FROM item_info
|
||||
WHERE ${whereClause}
|
||||
ORDER BY item_name
|
||||
LIMIT $${idx} OFFSET $${idx + 1}
|
||||
`;
|
||||
params.push(pageSize, offset);
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
return res.json({ success: true, data: result.rows, totalCount, page, pageSize });
|
||||
} catch (error: any) {
|
||||
logger.error("품목 소스 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,218 @@ async function getNormalizedOrders(
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 출하계획 목록 조회 (관리 화면용) ───
|
||||
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { dateFrom, dateTo, status, customer, keyword } = req.query;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 멀티테넌시
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 전체 조회
|
||||
} else {
|
||||
conditions.push(`sp.company_code = $${paramIndex}`);
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
conditions.push(`sp.plan_date >= $${paramIndex}::date`);
|
||||
params.push(dateFrom);
|
||||
paramIndex++;
|
||||
}
|
||||
if (dateTo) {
|
||||
conditions.push(`sp.plan_date <= $${paramIndex}::date`);
|
||||
params.push(dateTo);
|
||||
paramIndex++;
|
||||
}
|
||||
if (status) {
|
||||
conditions.push(`sp.status = $${paramIndex}`);
|
||||
params.push(status);
|
||||
paramIndex++;
|
||||
}
|
||||
if (customer) {
|
||||
conditions.push(`(c.customer_name ILIKE $${paramIndex} OR COALESCE(m.partner_id, d.delivery_partner_code, '') ILIKE $${paramIndex})`);
|
||||
params.push(`%${customer}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
if (keyword) {
|
||||
conditions.push(`(
|
||||
COALESCE(m.order_no, d.order_no, '') ILIKE $${paramIndex}
|
||||
OR COALESCE(d.part_code, m.part_code, '') ILIKE $${paramIndex}
|
||||
OR COALESCE(i.item_name, d.part_name, m.part_name, '') ILIKE $${paramIndex}
|
||||
OR sp.shipment_plan_no ILIKE $${paramIndex}
|
||||
)`);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
sp.id,
|
||||
sp.plan_date,
|
||||
sp.plan_qty,
|
||||
sp.status,
|
||||
sp.memo,
|
||||
sp.shipment_plan_no,
|
||||
sp.created_date,
|
||||
sp.created_by,
|
||||
sp.detail_id,
|
||||
sp.sales_order_id,
|
||||
sp.remain_qty,
|
||||
COALESCE(m.order_no, d.order_no, '') AS order_no,
|
||||
COALESCE(d.part_code, m.part_code, '') AS part_code,
|
||||
COALESCE(i.item_name, d.part_name, m.part_name, COALESCE(d.part_code, m.part_code, '')) AS part_name,
|
||||
COALESCE(d.spec, m.spec, '') AS spec,
|
||||
COALESCE(m.material, '') AS material,
|
||||
COALESCE(c.customer_name, m.partner_id, d.delivery_partner_code, '') AS customer_name,
|
||||
COALESCE(m.partner_id, d.delivery_partner_code, '') AS partner_code,
|
||||
COALESCE(d.due_date, m.due_date::text, '') AS due_date,
|
||||
COALESCE(NULLIF(d.qty,'')::numeric, m.order_qty, 0) AS order_qty,
|
||||
COALESCE(NULLIF(d.ship_qty,'')::numeric, m.ship_qty, 0) AS shipped_qty
|
||||
FROM shipment_plan sp
|
||||
LEFT JOIN sales_order_detail d
|
||||
ON sp.detail_id = d.id AND sp.company_code = d.company_code
|
||||
LEFT JOIN sales_order_mng m
|
||||
ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
WHERE item_number = COALESCE(d.part_code, m.part_code)
|
||||
AND company_code = sp.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
LEFT JOIN customer_mng c
|
||||
ON COALESCE(m.partner_id, d.delivery_partner_code) = c.customer_code
|
||||
AND sp.company_code = c.company_code
|
||||
${whereClause}
|
||||
ORDER BY sp.created_date DESC
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("출하계획 목록 조회", {
|
||||
companyCode,
|
||||
rowCount: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("출하계획 목록 조회 실패", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 출하계획 단건 수정 ───
|
||||
|
||||
export async function updatePlan(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const { planQty, planDate, memo } = req.body;
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
const check = await pool.query(
|
||||
`SELECT id, status FROM shipment_plan WHERE id = $1 AND company_code = $2`,
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (check.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, message: "출하계획을 찾을 수 없습니다" });
|
||||
}
|
||||
|
||||
const setClauses: string[] = [];
|
||||
const updateParams: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (planQty !== undefined) {
|
||||
setClauses.push(`plan_qty = $${idx}`);
|
||||
updateParams.push(planQty);
|
||||
idx++;
|
||||
}
|
||||
if (planDate !== undefined) {
|
||||
setClauses.push(`plan_date = $${idx}::date`);
|
||||
updateParams.push(planDate);
|
||||
idx++;
|
||||
}
|
||||
if (memo !== undefined) {
|
||||
setClauses.push(`memo = $${idx}`);
|
||||
updateParams.push(memo);
|
||||
idx++;
|
||||
}
|
||||
|
||||
setClauses.push(`updated_date = NOW()`);
|
||||
setClauses.push(`updated_by = $${idx}`);
|
||||
updateParams.push(userId);
|
||||
idx++;
|
||||
|
||||
updateParams.push(id);
|
||||
updateParams.push(companyCode);
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE shipment_plan
|
||||
SET ${setClauses.join(", ")}
|
||||
WHERE id = $${idx - 1} AND company_code = $${idx}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
// 파라미터 인덱스 수정
|
||||
const finalParams: any[] = [];
|
||||
let pIdx = 1;
|
||||
const setClausesFinal: string[] = [];
|
||||
|
||||
if (planQty !== undefined) {
|
||||
setClausesFinal.push(`plan_qty = $${pIdx}`);
|
||||
finalParams.push(planQty);
|
||||
pIdx++;
|
||||
}
|
||||
if (planDate !== undefined) {
|
||||
setClausesFinal.push(`plan_date = $${pIdx}::date`);
|
||||
finalParams.push(planDate);
|
||||
pIdx++;
|
||||
}
|
||||
if (memo !== undefined) {
|
||||
setClausesFinal.push(`memo = $${pIdx}`);
|
||||
finalParams.push(memo);
|
||||
pIdx++;
|
||||
}
|
||||
setClausesFinal.push(`updated_date = NOW()`);
|
||||
setClausesFinal.push(`updated_by = $${pIdx}`);
|
||||
finalParams.push(userId);
|
||||
pIdx++;
|
||||
|
||||
finalParams.push(id);
|
||||
finalParams.push(companyCode);
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE shipment_plan
|
||||
SET ${setClausesFinal.join(", ")}
|
||||
WHERE id = $${pIdx} AND company_code = $${pIdx + 1}
|
||||
RETURNING *`,
|
||||
finalParams
|
||||
);
|
||||
|
||||
logger.info("출하계획 수정", { companyCode, planId: id, userId });
|
||||
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("출하계획 수정 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 품목별 집계 + 기존 출하계획 조회 ───
|
||||
|
||||
export async function getAggregate(req: AuthenticatedRequest, res: Response) {
|
||||
|
||||
Reference in New Issue
Block a user