- src/app/api/* (FITO 백엔드 API 약 70개) 복원 — (main) 페이지들이 /api/product/bom, /api/sales/* 등을 호출하므로 복원 필요 - /api/admin/menus/delete: rel_menu_auth 컬럼명 오타 수정 (menu_obj_id → menu_objid) - /api/admin/menus/delete: cascade 옵션 — 하위 메뉴까지 재귀 일괄 삭제 - /api/admin/users/delete 신규 (soft delete: status=inActive) - /api/admin/dept/delete 신규 (자식 부서/소속 사용자 검사 후 soft delete) - admin-panel 사용자 관리: handleDelete 빈 껍데기였음 → 실제 API 호출 + 체크박스 선택 연결 - admin-panel 부서 관리 모달: 삭제 버튼 추가
This commit is contained in:
@@ -159,6 +159,7 @@ function UserManagement() {
|
||||
const [searchDept, setSearchDept] = useState("");
|
||||
const [searchType, setSearchType] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "부서명", field: "DEPT_NAME", width: 120 },
|
||||
@@ -189,7 +190,29 @@ function UserManagement() {
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire({ icon: "warning", title: "삭제할 사용자를 선택하세요." });
|
||||
return;
|
||||
}
|
||||
const r = await Swal.fire({
|
||||
icon: "warning", title: `${selectedRows.length}명 비활성화`,
|
||||
text: "선택한 사용자를 비활성 상태로 전환합니다.",
|
||||
showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소",
|
||||
confirmButtonColor: "#dc2626",
|
||||
});
|
||||
if (!r.isConfirmed) return;
|
||||
const userIds = selectedRows.map((row) => String(row.USER_ID)).filter(Boolean);
|
||||
const res = await fetch("/api/admin/users/delete", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userIds }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) {
|
||||
Swal.fire({ icon: "success", title: `${j.count}명 비활성화 완료`, timer: 1200, showConfirmButton: false });
|
||||
setSelectedRows([]); fetchData();
|
||||
} else {
|
||||
Swal.fire({ icon: "error", title: "오류", text: j.message });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -217,7 +240,7 @@ function UserManagement() {
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
<DataGrid columns={columns} data={data} showCheckbox height="calc(100vh - 300px)" />
|
||||
<DataGrid columns={columns} data={data} showCheckbox onSelectionChange={setSelectedRows} height="calc(100vh - 300px)" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1028,6 +1051,18 @@ function DeptManagement() {
|
||||
</table>
|
||||
<div className="flex gap-2 justify-center mt-5 pt-3">
|
||||
<Button onClick={handleSave} disabled={saving}>{saving ? "저장 중..." : "저장"}</Button>
|
||||
{editForm.actionType !== "regist" && (
|
||||
<Button variant="danger" onClick={async () => {
|
||||
const r = await Swal.fire({ icon: "warning", title: `"${editForm.dept_name}" 비활성화`, showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소", confirmButtonColor: "#dc2626" });
|
||||
if (!r.isConfirmed) return;
|
||||
const res = await fetch("/api/admin/dept/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ deptCode: editForm.dept_code }) });
|
||||
const j = await res.json();
|
||||
if (j.success) {
|
||||
Swal.fire({ icon: "success", title: "삭제됨", timer: 1200, showConfirmButton: false });
|
||||
setShowForm(false); fetchData();
|
||||
} else { Swal.fire("오류", j.message, "error"); }
|
||||
}}>삭제</Button>
|
||||
)}
|
||||
<Button variant="secondary" onClick={() => setShowForm(false)}>닫기</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { deptCode, deptCodes } = await req.json() as { deptCode?: string; deptCodes?: string[] };
|
||||
const targets = deptCodes && deptCodes.length > 0 ? deptCodes : (deptCode ? [deptCode] : []);
|
||||
if (targets.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "삭제할 부서를 선택하세요." }, { status: 400 });
|
||||
}
|
||||
|
||||
// 자식 부서 검사
|
||||
const placeholders = targets.map((_, i) => `$${i + 1}`).join(",");
|
||||
const child = await queryOne<{ cnt: string }>(
|
||||
`SELECT COUNT(*) AS cnt FROM dept_info WHERE parent_dept_code IN (${placeholders}) AND COALESCE(status,'active') != 'inActive'`,
|
||||
targets
|
||||
);
|
||||
if (Number(child?.cnt || 0) > 0) {
|
||||
return NextResponse.json({ success: false, message: `하위 부서 ${child!.cnt}개가 있어 삭제할 수 없습니다.` }, { status: 400 });
|
||||
}
|
||||
|
||||
// 사용자 검사
|
||||
const userCnt = await queryOne<{ cnt: string }>(
|
||||
`SELECT COUNT(*) AS cnt FROM user_info WHERE dept_code IN (${placeholders}) AND COALESCE(status,'active') != 'inActive'`,
|
||||
targets
|
||||
);
|
||||
if (Number(userCnt?.cnt || 0) > 0) {
|
||||
return NextResponse.json({ success: false, message: `소속 사용자 ${userCnt!.cnt}명이 있어 삭제할 수 없습니다.` }, { status: 400 });
|
||||
}
|
||||
|
||||
await execute(`UPDATE dept_info SET status='inActive' WHERE dept_code IN (${placeholders})`, targets);
|
||||
return NextResponse.json({ success: true, count: targets.length });
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export async function POST(request: NextRequest) {
|
||||
await client.query("BEGIN");
|
||||
const target = Number(objid);
|
||||
|
||||
// 자손 모두 수집 (재귀)
|
||||
const descendants = await client.query<{ objid: number }>(
|
||||
`WITH RECURSIVE tree AS (
|
||||
SELECT objid FROM menu_info WHERE objid = $1
|
||||
@@ -24,23 +23,20 @@ export async function POST(request: NextRequest) {
|
||||
SELECT objid FROM tree`,
|
||||
[target]
|
||||
);
|
||||
|
||||
const ids = descendants.rows.map((r) => r.objid);
|
||||
const childCount = ids.length - 1;
|
||||
|
||||
// cascade 미지정인데 하위 있으면 거부
|
||||
if (childCount > 0 && !cascade) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: `하위 메뉴 ${childCount}개가 있습니다. 함께 삭제하려면 다시 시도하세요.`,
|
||||
childCount,
|
||||
needsCascade: true,
|
||||
childCount, needsCascade: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 권한-메뉴 매핑 정리 후 메뉴 삭제
|
||||
await client.query(`DELETE FROM rel_menu_auth WHERE menu_obj_id = ANY($1::bigint[])`, [ids]);
|
||||
// rel_menu_auth 의 정확한 컬럼은 menu_objid (언더스코어 없음)
|
||||
await client.query(`DELETE FROM rel_menu_auth WHERE menu_objid = ANY($1::bigint[])`, [ids]);
|
||||
const r = await client.query(`DELETE FROM menu_info WHERE objid = ANY($1::bigint[])`, [ids]);
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, deleted: r.rowCount, ids });
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { ids, userIds } = await req.json() as { ids?: string[]; userIds?: string[] };
|
||||
const targets = (userIds || ids || []).filter(Boolean);
|
||||
if (targets.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "삭제할 사용자를 선택하세요." }, { status: 400 });
|
||||
}
|
||||
// soft delete (status='inActive') — 안전. 데이터 보존
|
||||
const placeholders = targets.map((_, i) => `$${i + 1}`).join(",");
|
||||
await execute(`UPDATE user_info SET status='inActive' WHERE user_id IN (${placeholders})`, targets);
|
||||
return NextResponse.json({ success: true, count: targets.length });
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { updateTargetStatus } from "@/lib/approval-target";
|
||||
|
||||
// 결재 승인 (approval.xml: setInboxtaskResult + getNextApprovalObjId +
|
||||
// setNextInboxtaskStatus + getNotCompleteInboxtaskCnt + completeRoute + completeApproval 대응)
|
||||
// 원본 ApprovalService.setApprovalResult + completeRouteInfo
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const inboxObjId = body.inboxObjId;
|
||||
if (!inboxObjId) return NextResponse.json({ success: false, message: "inboxObjId 필요" });
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. 현재 결재자 검증
|
||||
const cur = await client.query(
|
||||
`SELECT IT.*, A.target_objid, A.target_type, A.objid AS approval_objid, R.objid AS route_objid
|
||||
FROM inboxtask IT
|
||||
JOIN approval A ON A.objid = IT.approval_objid
|
||||
LEFT JOIN route R ON R.objid = IT.route_objid
|
||||
WHERE IT.objid = $1::numeric AND IT.target_user_id = $2 AND IT.status = 'ready'`,
|
||||
[inboxObjId, user.userId]
|
||||
);
|
||||
if (cur.rows.length === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "결재 권한이 없거나 이미 처리된 건입니다." });
|
||||
}
|
||||
const task = cur.rows[0];
|
||||
|
||||
// setInboxtaskResult — RESULT='Y' (원본), STATUS='complete'
|
||||
await client.query(
|
||||
`UPDATE inboxtask SET result = 'Y', status = 'complete', proc_date = now(), result_message = $1,
|
||||
sign = $2, sign_width = $3, sign_height = $4
|
||||
WHERE objid = $5::numeric`,
|
||||
[body.message || "", body.sign || null, body.sign_width || null, body.sign_height || null, inboxObjId]
|
||||
);
|
||||
|
||||
// 2. 다음 차례의 normal 결재자가 있는지 — APPROVAL_TYPE='normal' + seq+1
|
||||
// getNextApprovalObjId 대응
|
||||
const next = await client.query(
|
||||
`SELECT objid FROM inboxtask
|
||||
WHERE UPPER(approval_type) = 'NORMAL'
|
||||
AND approval_objid = $1::numeric AND route_objid = $2::numeric
|
||||
AND seq::numeric = $3::numeric + 1`,
|
||||
[task.approval_objid, task.route_objid, task.seq]
|
||||
);
|
||||
if ((next.rowCount ?? 0) > 0) {
|
||||
// 다음 결재자 ready로 변경 (setNextInboxtaskStatus)
|
||||
await client.query(
|
||||
`UPDATE inboxtask SET status = 'ready', regdate = now() WHERE objid = $1::numeric`,
|
||||
[next.rows[0].objid]
|
||||
);
|
||||
}
|
||||
|
||||
// 3. route별 남은 결재자(ready/standby) 없으면 완료 처리
|
||||
// getNotCompleteInboxtaskCnt 대응
|
||||
const remaining = await client.query(
|
||||
`SELECT COUNT(*)::int AS cnt FROM inboxtask
|
||||
WHERE route_objid = $1::numeric AND status IN ('ready', 'standby')`,
|
||||
[task.route_objid]
|
||||
);
|
||||
if (remaining.rows[0].cnt === 0) {
|
||||
// completeRoute + completeApproval
|
||||
await client.query(
|
||||
`UPDATE route SET status = 'complete' WHERE objid = $1::numeric`,
|
||||
[task.route_objid]
|
||||
);
|
||||
await client.query(
|
||||
`UPDATE approval SET status = 'complete', complete_date = now() WHERE objid = $1::numeric`,
|
||||
[task.approval_objid]
|
||||
);
|
||||
// 대상 모듈 approvalComplete (XxxApprovalStatus)
|
||||
await updateTargetStatus(client, task.target_type, String(task.target_objid), "approvalComplete", user.userId);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "결재 승인되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Approve:", e);
|
||||
return NextResponse.json({ success: false, message: "승인 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally { client.release(); }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// approval/getApprovalCnt.do 대응
|
||||
export async function GET() {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ count: 0 });
|
||||
|
||||
try {
|
||||
// 원본 getApprovalCnt: INBOXTASK + APPROVAL + ROUTE JOIN,
|
||||
// SYSTEM_TYPE='PLM' + INBOX.STATUS='READY' + ROUTE.STATUS='inProcess'
|
||||
const result = await queryOne<{ CNT: string }>(
|
||||
`SELECT COUNT(1) AS "CNT"
|
||||
FROM inboxtask INBOX, approval APP, route ROU
|
||||
WHERE INBOX.target_user_id = $1
|
||||
AND INBOX.approval_objid = APP.objid
|
||||
AND INBOX.route_objid = ROU.objid
|
||||
AND APP.system_type = 'PLM'
|
||||
AND UPPER(INBOX.status) = 'READY'
|
||||
AND ROU.status = 'inProcess'`,
|
||||
[user.userId]
|
||||
);
|
||||
return NextResponse.json({ count: Number(result?.CNT || 0) });
|
||||
} catch {
|
||||
return NextResponse.json({ count: 0 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne, queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 결재 상세 + 결재선 (getApprovalLine 대응)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId required" });
|
||||
|
||||
// 원본 getRouteInfo 대응 — APPROVAL-NNNN/ROUTE-NNNN 번호, 이메일, 부서, reject 제외
|
||||
const info = await queryOne(
|
||||
`SELECT A.objid::text AS "OBJID", A.target_objid AS "TARGET_OBJID",
|
||||
A.target_type AS "TARGET_TYPE",
|
||||
A.approval_seq AS "APPROVAL_SEQ",
|
||||
'APPROVAL-' || LPAD(A.approval_seq::text, 4, '0') AS "APPROVAL_NO",
|
||||
A.status AS "STATUS",
|
||||
R.objid::text AS "ROUTE_OBJID",
|
||||
R.route_seq AS "ROUTE_SEQ",
|
||||
'ROUTE-' || LPAD(R.route_seq::text, 4, '0') AS "ROUTE_NO",
|
||||
R.approval_title AS "TITLE", R.approval_desc AS "DESCRIPTION",
|
||||
R.writer AS "WRITER",
|
||||
COALESCE((SELECT email FROM user_info WHERE user_id = R.writer LIMIT 1), '') AS "EMAIL",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = R.writer LIMIT 1), R.writer) AS "WRITER_NAME",
|
||||
COALESCE((SELECT dept_name FROM user_info WHERE user_id = R.writer LIMIT 1), '') AS "DEPT_NAME",
|
||||
TO_CHAR(A.regdate, 'YYYY-MM-DD HH24:MI') AS "REGDATE",
|
||||
TO_CHAR(A.complete_date, 'YYYY-MM-DD HH24:MI') AS "COMPLETE_DATE"
|
||||
FROM approval A
|
||||
LEFT JOIN route R ON R.approval_objid = A.objid AND R.status != 'reject'
|
||||
WHERE A.objid::text = $1
|
||||
ORDER BY R.regdate DESC LIMIT 1`, [objId]
|
||||
);
|
||||
if (!info) return NextResponse.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
|
||||
// 결재선 (INBOXTASK)
|
||||
const line = await queryRows(
|
||||
`SELECT IT.objid::text AS "OBJID", IT.seq AS "SEQ",
|
||||
IT.target_user_id AS "USER_ID",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = IT.target_user_id LIMIT 1), IT.target_user_id) AS "USER_NAME",
|
||||
COALESCE((SELECT dept_name FROM user_info WHERE user_id = IT.target_user_id LIMIT 1), '') AS "DEPT_NAME",
|
||||
IT.approval_type AS "APPROVAL_TYPE",
|
||||
IT.status AS "STATUS",
|
||||
CASE IT.status
|
||||
WHEN 'ready' THEN '대기중'
|
||||
WHEN 'standby' THEN '대기'
|
||||
WHEN 'complete' THEN '승인'
|
||||
WHEN 'reject' THEN '반려'
|
||||
WHEN 'cancel' THEN '취소'
|
||||
ELSE IT.status END AS "STATUS_NAME",
|
||||
TO_CHAR(IT.proc_date, 'YYYY-MM-DD HH24:MI') AS "PROC_DATE",
|
||||
IT.result_message AS "RESULT_MESSAGE"
|
||||
FROM inboxtask IT
|
||||
WHERE IT.approval_objid::text = $1
|
||||
ORDER BY IT.seq::numeric`, [objId]
|
||||
);
|
||||
|
||||
// 현재 사용자가 결재자인지 확인
|
||||
const myTask = line.find((l: Record<string, unknown>) =>
|
||||
l.USER_ID === user.userId && (l.STATUS === "ready")
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true, data: info, LINE: line,
|
||||
canApprove: !!myTask, myInboxObjId: myTask?.OBJID || null,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 동일 target_objid 에 대한 결재번호/이력 목록 (approvalDetail.jsp 의 routeNo 드롭다운)
|
||||
// 상신 → 반려 → 재상신 흐름 시 이전 라우트들을 보존하므로 여러 건 존재 가능
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const { targetObjId, approvalObjId } = await request.json();
|
||||
if (!targetObjId && !approvalObjId) {
|
||||
return NextResponse.json({ success: false, message: "targetObjId 또는 approvalObjId 필요" });
|
||||
}
|
||||
|
||||
// targetObjId 가 있으면 같은 target 의 모든 route 이력
|
||||
// approvalObjId 만 있으면 같은 approval 의 모든 route 이력
|
||||
const sql = targetObjId
|
||||
? `SELECT R.objid::text AS "ROUTE_OBJID",
|
||||
A.objid::text AS "APPROVAL_OBJID",
|
||||
'APPROVAL-' || LPAD(A.approval_seq::text, 4, '0') AS "APPROVAL_NO",
|
||||
'ROUTE-' || LPAD(R.route_seq::text, 4, '0') AS "ROUTE_NO",
|
||||
R.status AS "STATUS",
|
||||
R.approval_title AS "TITLE",
|
||||
TO_CHAR(R.regdate, 'YYYY-MM-DD HH24:MI') AS "REGDATE"
|
||||
FROM approval A
|
||||
JOIN route R ON R.approval_objid = A.objid
|
||||
WHERE A.target_objid = $1::numeric
|
||||
ORDER BY R.regdate DESC`
|
||||
: `SELECT R.objid::text AS "ROUTE_OBJID",
|
||||
A.objid::text AS "APPROVAL_OBJID",
|
||||
'APPROVAL-' || LPAD(A.approval_seq::text, 4, '0') AS "APPROVAL_NO",
|
||||
'ROUTE-' || LPAD(R.route_seq::text, 4, '0') AS "ROUTE_NO",
|
||||
R.status AS "STATUS",
|
||||
R.approval_title AS "TITLE",
|
||||
TO_CHAR(R.regdate, 'YYYY-MM-DD HH24:MI') AS "REGDATE"
|
||||
FROM approval A
|
||||
JOIN route R ON R.approval_objid = A.objid
|
||||
WHERE A.objid = $1::numeric
|
||||
ORDER BY R.regdate DESC`;
|
||||
|
||||
const rows = await queryRows(sql, [String(targetObjId || approvalObjId)]);
|
||||
return NextResponse.json({ RESULTLIST: rows });
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { updateTargetStatus } from "@/lib/approval-target";
|
||||
|
||||
// 결재 반려 (approval.xml: setInboxtaskResult + cancelInboxtask +
|
||||
// updateRouteStatus + changeApprovalStatus 대응)
|
||||
// 원본 ApprovalService.rejectRouteInfo
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const inboxObjId = body.inboxObjId;
|
||||
if (!inboxObjId) return NextResponse.json({ success: false, message: "inboxObjId 필요" });
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const cur = await client.query(
|
||||
`SELECT IT.*, A.target_objid, A.target_type
|
||||
FROM inboxtask IT
|
||||
JOIN approval A ON A.objid = IT.approval_objid
|
||||
WHERE IT.objid = $1::numeric AND IT.target_user_id = $2 AND IT.status = 'ready'`,
|
||||
[inboxObjId, user.userId]
|
||||
);
|
||||
if (cur.rows.length === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({ success: false, message: "결재 권한이 없거나 이미 처리된 건입니다." });
|
||||
}
|
||||
const task = cur.rows[0];
|
||||
|
||||
// setInboxtaskResult — RESULT='N'(반려)
|
||||
await client.query(
|
||||
`UPDATE inboxtask SET result = 'N', status = 'reject', proc_date = now(), result_message = $1,
|
||||
sign = $2, sign_width = $3, sign_height = $4
|
||||
WHERE objid = $5::numeric`,
|
||||
[body.message || "", body.sign || null, body.sign_width || null, body.sign_height || null, inboxObjId]
|
||||
);
|
||||
|
||||
// cancelInboxtask — 해당 route의 reject/complete 아닌 건을 cancel
|
||||
await client.query(
|
||||
`UPDATE inboxtask SET status = 'cancel'
|
||||
WHERE status NOT IN ('complete', 'reject') AND route_objid = $1::numeric`,
|
||||
[task.route_objid]
|
||||
);
|
||||
|
||||
// updateRouteStatus — reject
|
||||
await client.query(
|
||||
`UPDATE route SET status = 'reject' WHERE objid = $1::numeric`,
|
||||
[task.route_objid]
|
||||
);
|
||||
// changeApprovalStatus — reject
|
||||
await client.query(
|
||||
`UPDATE approval SET status = 'reject', complete_date = now() WHERE objid = $1::numeric`,
|
||||
[task.approval_objid]
|
||||
);
|
||||
|
||||
// 대상 모듈 상태 반려 (XxxApprovalStatus)
|
||||
await updateTargetStatus(client, task.target_type, String(task.target_objid), "reject", user.userId);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "결재 반려되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Reject:", e);
|
||||
return NextResponse.json({ success: false, message: "반려 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally { client.release(); }
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
import { updateTargetStatus } from "@/lib/approval-target";
|
||||
|
||||
// 결재상신 (approval.xml: insertApprovalInfo + createRouteInfo + createInboxTaskInfo 대응)
|
||||
// 원본 ApprovalService.createApprovalInfo() 로직 포팅
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
if (!body.target_objid || !body.target_type) {
|
||||
return NextResponse.json({ success: false, message: "결재 대상과 유형이 필요합니다." });
|
||||
}
|
||||
const approvers: { user_id: string; approval_type?: string }[] = body.approvers || [];
|
||||
if (approvers.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "결재자를 지정하세요." });
|
||||
}
|
||||
|
||||
const systemType = body.system_type || "PLM";
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. APPROVAL 생성/재사용 — APPROVAL_SEQ 자동증가 (SELECT MAX+1)
|
||||
let approvalObjId: string;
|
||||
const existing = await client.query(
|
||||
`SELECT objid::text AS objid FROM approval
|
||||
WHERE target_objid = $1::numeric AND system_type = $2 AND status != 'complete'
|
||||
ORDER BY regdate DESC LIMIT 1`,
|
||||
[body.target_objid, systemType]
|
||||
);
|
||||
if (existing.rows.length > 0) {
|
||||
approvalObjId = existing.rows[0].objid;
|
||||
await client.query(
|
||||
`UPDATE approval SET status = 'inProcess' WHERE objid = $1::numeric`,
|
||||
[approvalObjId]
|
||||
);
|
||||
} else {
|
||||
approvalObjId = createObjectId();
|
||||
await client.query(
|
||||
`INSERT INTO approval (objid, target_objid, target_type, approval_seq, system_type, regdate, status)
|
||||
VALUES ($1::numeric, $2::numeric, $3,
|
||||
(SELECT COALESCE(MAX(approval_seq::numeric) + 1, 1) FROM approval),
|
||||
$4, now(), 'inProcess')`,
|
||||
[approvalObjId, body.target_objid, body.target_type, systemType]
|
||||
);
|
||||
}
|
||||
|
||||
// 2. ROUTE 생성 — ROUTE_SEQ 자동증가
|
||||
const routeObjId = createObjectId();
|
||||
await client.query(
|
||||
`INSERT INTO route (objid, target_objid, approval_objid, route_seq,
|
||||
approval_title, approval_desc, system_type, writer, regdate, status)
|
||||
VALUES ($1::numeric, $2::numeric, $3::numeric,
|
||||
(SELECT COALESCE(MAX(route_seq::numeric) + 1, 1) FROM route),
|
||||
$4, $5, $6, $7, now(), 'inProcess')`,
|
||||
[routeObjId, body.target_objid, approvalObjId,
|
||||
body.approval_title || "결재 요청", body.approval_desc || "", systemType, user.userId]
|
||||
);
|
||||
|
||||
// 3. INBOXTASK — normal 순차(1st=ready, 나머지=standby), help/ref=즉시 ready
|
||||
for (let i = 0; i < approvers.length; i++) {
|
||||
const a = approvers[i];
|
||||
const approvalType = a.approval_type || "normal";
|
||||
const isFirstNormal = approvalType === "normal" && i === 0;
|
||||
const isHelpOrRef = approvalType === "help" || approvalType === "ref";
|
||||
const status = (isFirstNormal || isHelpOrRef) ? "ready" : "standby";
|
||||
await client.query(
|
||||
`INSERT INTO inboxtask (objid, seq, approval_type, target_objid, approval_objid, route_objid,
|
||||
target_user_id, regdate, status)
|
||||
VALUES ($1::numeric, $2, $3, $4::numeric, $5::numeric, $6::numeric, $7, now(), $8)`,
|
||||
[createObjectId(), i + 1, approvalType, body.target_objid,
|
||||
approvalObjId, routeObjId, a.user_id, status]
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 대상 모듈 상태 업데이트 (XxxApprovalStatus 쿼리 대응)
|
||||
await updateTargetStatus(client, body.target_type, body.target_objid, "approvalRequest", user.userId);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({
|
||||
success: true, approvalObjId, routeObjId, message: "결재상신되었습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Approval request:", e);
|
||||
return NextResponse.json({ success: false, message: "결재상신 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 결재함 목록 (selectApprovalList 대응)
|
||||
// status: PENDING(ready/standby) | APPROVED(complete) | REJECTED(reject) | "" (전체)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const systemType = body.system_type || "PLM";
|
||||
const conditions: string[] = [
|
||||
"A.objid = R.approval_objid",
|
||||
"A.system_type = $1",
|
||||
"IT.approval_objid = A.objid",
|
||||
"IT.route_objid = R.objid",
|
||||
];
|
||||
const params: unknown[] = [systemType];
|
||||
let idx = 2;
|
||||
|
||||
// 원본: target_user_id = connectUserId (내 결재건만, 관리자는 전체)
|
||||
if (!user.isAdmin) {
|
||||
conditions.push(`IT.target_user_id = $${idx++}`);
|
||||
params.push(user.userId);
|
||||
}
|
||||
|
||||
// 원본: status 필터
|
||||
if (body.status === "PENDING") {
|
||||
conditions.push(`UPPER(IT.status) = 'READY'`);
|
||||
conditions.push(`R.status = 'inProcess'`);
|
||||
} else if (body.status === "APPROVED") {
|
||||
conditions.push(`UPPER(A.status) = 'COMPLETE'`);
|
||||
} else if (body.status === "REJECTED") {
|
||||
conditions.push(`UPPER(A.status) = 'REJECT'`);
|
||||
}
|
||||
|
||||
if (body.title) {
|
||||
conditions.push(`UPPER(R.approval_title) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.title);
|
||||
}
|
||||
if (body.writer_name) {
|
||||
conditions.push(`UPPER((SELECT user_name FROM user_info WHERE user_id = R.writer)) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.writer_name);
|
||||
}
|
||||
if (body.from_date) {
|
||||
conditions.push(`R.regdate >= $${idx++}::date`);
|
||||
params.push(body.from_date);
|
||||
}
|
||||
if (body.to_date) {
|
||||
conditions.push(`R.regdate <= $${idx++}::date`);
|
||||
params.push(body.to_date);
|
||||
}
|
||||
|
||||
// 원본 selectApprovalList와 동일한 컬럼 구조 + APPROVAL-NNNN / ROUTE-NNNN 번호
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
A.objid::text AS "OBJID",
|
||||
A.objid::text AS "APPROVAL_OBJID",
|
||||
A.target_objid AS "TARGET_OBJID",
|
||||
A.target_type AS "TARGET_TYPE",
|
||||
A.approval_seq AS "APPROVAL_SEQ",
|
||||
'APPROVAL-' || LPAD(A.approval_seq::text, 4, '0') AS "APPROVAL_NO",
|
||||
A.status AS "APPROVAL_STATUS",
|
||||
TO_CHAR(A.regdate, 'YYYY-MM-DD') AS "APPROVAL_REGDATE",
|
||||
R.objid::text AS "ROUTE_OBJID",
|
||||
R.route_seq AS "ROUTE_SEQ",
|
||||
'ROUTE-' || LPAD(R.route_seq::text, 4, '0') AS "ROUTE_NO",
|
||||
R.approval_title AS "TITLE",
|
||||
R.approval_desc AS "DESCRIPTION",
|
||||
R.writer AS "WRITER",
|
||||
COALESCE((SELECT dept_name FROM user_info WHERE user_id = R.writer LIMIT 1), '') AS "DEPT_NAME",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = R.writer LIMIT 1), R.writer) AS "WRITER_NAME",
|
||||
TO_CHAR(R.regdate, 'YYYY-MM-DD') AS "REGDATE",
|
||||
R.status AS "STATUS",
|
||||
CASE
|
||||
WHEN UPPER(A.status) = 'COMPLETE' THEN '결재완료'
|
||||
WHEN UPPER(A.status) = 'REJECT' THEN '반려'
|
||||
WHEN UPPER(IT.status) = 'READY' THEN '미결재'
|
||||
WHEN UPPER(IT.status) = 'STANDBY' THEN '대기'
|
||||
ELSE A.status END AS "STATUS_NAME",
|
||||
IT.objid::text AS "INBOX_OBJID",
|
||||
IT.seq AS "SEQ",
|
||||
IT.approval_type AS "APPROVAL_TYPE",
|
||||
IT.status AS "INBOX_STATUS",
|
||||
IT.target_user_id AS "TARGET_USER_ID",
|
||||
CASE A.target_type
|
||||
WHEN 'PURCHASE_ORDER' THEN '발주'
|
||||
WHEN 'EXPENSE_APPLY' THEN '경비'
|
||||
WHEN 'MATERIAL_APPLY' THEN '자재'
|
||||
WHEN 'AS_MNG' THEN 'CS/AS'
|
||||
WHEN 'CSM' THEN '고객서비스'
|
||||
WHEN 'ISSUE_RELEASE' THEN '이슈'
|
||||
WHEN 'EO_MNG' THEN '설변(EO)'
|
||||
WHEN 'ECR_MNG' THEN '변경요청(ECR)'
|
||||
WHEN 'SALES_REQUEST' THEN '영업요청'
|
||||
ELSE A.target_type END AS "TYPE_NAME"
|
||||
FROM approval A, route R, inboxtask IT
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY R.regdate DESC
|
||||
`,
|
||||
params
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -15,27 +15,32 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// 이메일 형태이면 MOMO 사용자 우선 시도, 그 외에는 FITO 우선 시도
|
||||
const looksLikeEmail = /@/.test(userId);
|
||||
|
||||
if (looksLikeEmail) {
|
||||
const momo = await verifyMomoCredentials(userId, password);
|
||||
if (momo.success && momo.user) {
|
||||
await createSession(momoToSessionUser(momo.user));
|
||||
return NextResponse.json({ success: true, redirectTo: "/m/dashboard" });
|
||||
const sessionUser: User = momoToSessionUser(momo.user);
|
||||
await createSession(sessionUser);
|
||||
return NextResponse.json({ success: true, user: sessionUser, redirectTo: "/m/dashboard" });
|
||||
}
|
||||
// MOMO 실패 시 FITO 폴백 시도 (관리자 마이그레이션 케이스)
|
||||
}
|
||||
|
||||
const fito = await verifyCredentials(userId, password);
|
||||
if (fito.success && fito.user) {
|
||||
await createSession(fito.user);
|
||||
return NextResponse.json({ success: true, redirectTo: "/dashboard" });
|
||||
return NextResponse.json({ success: true, user: fito.user, redirectTo: "/dashboard" });
|
||||
}
|
||||
|
||||
// FITO 도 실패하면 MOMO를 한 번 더 시도 (이메일 형태가 아니지만 MOMO 계정인 경우)
|
||||
if (!looksLikeEmail) {
|
||||
const momo = await verifyMomoCredentials(userId, password);
|
||||
if (momo.success && momo.user) {
|
||||
await createSession(momoToSessionUser(momo.user));
|
||||
return NextResponse.json({ success: true, redirectTo: "/m/dashboard" });
|
||||
const sessionUser: User = momoToSessionUser(momo.user);
|
||||
await createSession(sessionUser);
|
||||
return NextResponse.json({ success: true, user: sessionUser, redirectTo: "/m/dashboard" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +51,33 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
function momoToSessionUser(u: {
|
||||
objid: string; email: string; companyName: string; phone: string;
|
||||
role: "USER" | "ADMIN"; isAdmin: boolean;
|
||||
objid: string;
|
||||
email: string;
|
||||
companyName: string;
|
||||
phone: string;
|
||||
role: "USER" | "ADMIN";
|
||||
isAdmin: boolean;
|
||||
}): User {
|
||||
return {
|
||||
sabun: "", userId: u.email, userName: u.companyName, userNameEng: "", userNameCn: "",
|
||||
deptCode: "", deptName: "", positionCode: "", positionName: "", email: u.email,
|
||||
tel: "", cellPhone: u.phone, userType: "MOMO",
|
||||
sabun: "",
|
||||
userId: u.email,
|
||||
userName: u.companyName,
|
||||
userNameEng: "",
|
||||
userNameCn: "",
|
||||
deptCode: "",
|
||||
deptName: "",
|
||||
positionCode: "",
|
||||
positionName: "",
|
||||
email: u.email,
|
||||
tel: "",
|
||||
cellPhone: u.phone,
|
||||
userType: "MOMO",
|
||||
userTypeName: u.role === "ADMIN" ? "관리자" : "거래처",
|
||||
authName: u.role, partnerCd: "", isAdmin: u.isAdmin,
|
||||
role: u.role, objid: u.objid, companyName: u.companyName,
|
||||
authName: u.role,
|
||||
partnerCd: "",
|
||||
isAdmin: u.isAdmin,
|
||||
role: u.role,
|
||||
objid: u.objid,
|
||||
companyName: u.companyName,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원가관리 메인 (expenseDashBoardGrid 대응)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["CM.contract_result = '0000964'"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(TO_DATE(CM.contract_date, 'YYYY-MM-DD'), 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`CM.contract_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT CM.objid::text AS "OBJID",
|
||||
CM.contract_no AS "PROJECT_NO",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = CM.product LIMIT 1), '') AS "PRODUCT_NAME",
|
||||
-- 목표원가 (input_cost_goal)
|
||||
COALESCE((SELECT (COALESCE(NULLIF(material_cost_goal,'')::numeric,0)
|
||||
+ COALESCE(NULLIF(labor_cost_goal,'')::numeric,0)
|
||||
+ COALESCE(NULLIF(expense_cost_goal,'')::numeric,0))
|
||||
FROM input_cost_goal WHERE contract_objid = CM.objid::text LIMIT 1), 0) AS "TARGET_COST",
|
||||
-- 실적원가 = 재료비 + 노무비 + 경비
|
||||
(COALESCE((SELECT SUM(COALESCE(NULLIF(POP.supply_unit_price,'')::numeric,0))
|
||||
FROM purchase_order_part POP
|
||||
JOIN purchase_order_master POM ON POM.objid = POP.purchase_order_master_objid
|
||||
WHERE POM.contract_mgmt_objid = CM.objid AND POM.status = 'approvalComplete'), 0)
|
||||
+ COALESCE((SELECT SUM(COALESCE(NULLIF(amount_payment,'')::numeric,0))
|
||||
FROM expense_master WHERE project_mgmt_objid = CM.objid::text), 0)
|
||||
) AS "ACTUAL_COST",
|
||||
TO_CHAR(CM.regdate, 'YYYY-MM-DD') AS "REGDATE"
|
||||
FROM contract_mgmt CM
|
||||
WHERE ${where}
|
||||
ORDER BY CM.regdate DESC
|
||||
`,
|
||||
params
|
||||
);
|
||||
|
||||
// JS에서 차이/달성율 계산
|
||||
const result = rows.map((r: Record<string, unknown>) => {
|
||||
const target = Number(r.TARGET_COST || 0);
|
||||
const actual = Number(r.ACTUAL_COST || 0);
|
||||
return {
|
||||
...r,
|
||||
DIFF_COST: target - actual,
|
||||
ACHIEVE_RATE: target > 0 ? Math.round(actual / target * 100 * 10) / 10 : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ RESULTLIST: result, TOTAL_CNT: result.length });
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 경비관리 (expenseDashBoardGrid 원본 기반)
|
||||
// 주 테이블: project_mgmt. SETTLE_AMOUNT = CARD_USED + CASH_USED - PAYMENT
|
||||
// exp_status_cd별: 0001548 조립, 0001549 셋업, 0001629 외주(Turn-key)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(T.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`T.project_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.customer_objid) {
|
||||
conditions.push(`T.customer_objid = $${idx++}`);
|
||||
params.push(body.customer_objid);
|
||||
}
|
||||
if (body.pm_user_id) {
|
||||
conditions.push(`T.pm_user_id = $${idx++}`);
|
||||
params.push(body.pm_user_id);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
WITH settle AS (
|
||||
SELECT
|
||||
M.project_mgmt_objid,
|
||||
M.exp_status_cd,
|
||||
SUM(COALESCE(NULLIF(D.card_used, '')::numeric, 0)
|
||||
+ COALESCE(NULLIF(D.cash_used, '')::numeric, 0)
|
||||
- COALESCE(NULLIF(D.payment, '')::numeric, 0)) AS settle_amount
|
||||
FROM expense_master M
|
||||
LEFT JOIN expense_detail D ON M.expense_master_objid = D.expense_master_objid
|
||||
GROUP BY M.project_mgmt_objid, M.exp_status_cd
|
||||
)
|
||||
SELECT T.objid::text AS "OBJID",
|
||||
T.project_no AS "PROJECT_NO",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = T.customer_objid LIMIT 1), '') AS "CUSTOMER_NAME",
|
||||
T.project_name AS "PROJECT_NAME",
|
||||
T.req_del_date AS "REQ_DEL_DATE",
|
||||
T.setup AS "SETUP",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = T.pm_user_id LIMIT 1), '') AS "PM_USER_NAME",
|
||||
COALESCE((SELECT NULLIF(expense_cost_goal,'')::numeric FROM input_cost_goal WHERE contract_objid = T.objid::text LIMIT 1), 0) AS "EXPENSE_COST_GOAL",
|
||||
COALESCE((SELECT settle_amount FROM settle WHERE project_mgmt_objid = T.objid::text AND exp_status_cd = '0001548'), 0) AS "SETTLE_AMOUNT_ASSEMBLE",
|
||||
COALESCE((SELECT settle_amount FROM settle WHERE project_mgmt_objid = T.objid::text AND exp_status_cd = '0001549'), 0) AS "SETTLE_AMOUNT_SETUP",
|
||||
COALESCE((SELECT settle_amount FROM settle WHERE project_mgmt_objid = T.objid::text AND exp_status_cd = '0001629'), 0) AS "SETTLE_AMOUNT_CS",
|
||||
COALESCE((SELECT SUM(settle_amount) FROM settle WHERE project_mgmt_objid = T.objid::text), 0) AS "TOTAL_SETTLE_AMOUNT"
|
||||
FROM project_mgmt T
|
||||
WHERE ${where}
|
||||
ORDER BY SUBSTRING(T.project_no, POSITION('-' IN T.project_no)+1) DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
|
||||
const result = rows.map((r: Record<string, unknown>) => {
|
||||
const goal = Number(r.EXPENSE_COST_GOAL || 0);
|
||||
const total = Number(r.TOTAL_SETTLE_AMOUNT || 0);
|
||||
return {
|
||||
...r,
|
||||
INPUT_RATE: goal > 0 ? Math.round((total / goal) * 1000) / 10 : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ RESULTLIST: result, TOTAL_CNT: result.length });
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 경비신청 저장 (saveExpense 대응, 단순화)
|
||||
// body: { actionType, expense_master_objid?, project_mgmt_objid, bus_title, bns_start_date, bns_end_date,
|
||||
// exp_area_cd, exp_status_cd, bus_content, reason, amount_payment, status }
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const isNew = body.actionType === "regist" || !body.expense_master_objid;
|
||||
const objId = isNew ? createObjectId() : String(body.expense_master_objid);
|
||||
const projectMgmtObjid = String(body.project_mgmt_objid || "").trim() || "0";
|
||||
|
||||
try {
|
||||
// expense_master의 많은 컬럼이 'null::character varying' 기본값이라
|
||||
// 명시적으로 빈 문자열 전달 필요
|
||||
await execute(
|
||||
`INSERT INTO expense_master (
|
||||
expense_master_objid, project_mgmt_objid, expense_id,
|
||||
bus_title, bns_start_date, bns_end_date,
|
||||
exp_status_cd, exp_company_cd, exp_area_cd,
|
||||
vehicel_used, bus_users_id, bus_content, reason, instructions,
|
||||
amount_payment, reg_user_id, reg_date, exp_sort_cd, status
|
||||
) VALUES ($1,$2,'',$3,$4,$5,$6,'',$7,'',$8,$9,$10,'',$11,$12,TO_CHAR(NOW(),'YYYY-MM-DD'),'',$13)
|
||||
ON CONFLICT (expense_master_objid) DO UPDATE SET
|
||||
project_mgmt_objid = EXCLUDED.project_mgmt_objid,
|
||||
bus_title = EXCLUDED.bus_title,
|
||||
bns_start_date = EXCLUDED.bns_start_date,
|
||||
bns_end_date = EXCLUDED.bns_end_date,
|
||||
exp_status_cd = EXCLUDED.exp_status_cd,
|
||||
exp_area_cd = EXCLUDED.exp_area_cd,
|
||||
bus_content = EXCLUDED.bus_content,
|
||||
reason = EXCLUDED.reason,
|
||||
amount_payment = EXCLUDED.amount_payment`,
|
||||
[
|
||||
objId, projectMgmtObjid,
|
||||
body.bus_title || "", body.bns_start_date || "", body.bns_end_date || "",
|
||||
body.exp_status_cd || "", body.exp_area_cd || "",
|
||||
body.bus_users_id || user.userId, body.bus_content || "", body.reason || "",
|
||||
body.amount_payment || "", user.userId, body.status || "created",
|
||||
]
|
||||
);
|
||||
return NextResponse.json({ success: true, objId, message: isNew ? "등록되었습니다." : "수정되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("Expense save:", error);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 목표가 단건 조회 + 프로젝트 정보
|
||||
// body: { contract_objid } — 실제는 project_mgmt.objid (DB 컬럼명이 오해의 소지 있음)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const pmObjid = String(body.contract_objid || "");
|
||||
if (!pmObjid) return NextResponse.json({ success: true, data: null });
|
||||
|
||||
const row = await queryOne(
|
||||
`SELECT PM.objid::text AS "OBJID",
|
||||
PM.project_no AS "PROJECT_NO",
|
||||
PM.project_name AS "PROJECT_NAME",
|
||||
COALESCE(NULLIF(G.material_cost_goal, ''), '0') AS "MATERIAL_COST_GOAL",
|
||||
COALESCE(NULLIF(G.labor_cost_goal, ''), '0') AS "LABOR_COST_GOAL",
|
||||
COALESCE(NULLIF(G.expense_cost_goal, ''), '0') AS "EXPENSE_COST_GOAL"
|
||||
FROM project_mgmt PM
|
||||
LEFT JOIN input_cost_goal G ON G.contract_objid = PM.objid::text
|
||||
WHERE PM.objid::text = $1`,
|
||||
[pmObjid]
|
||||
);
|
||||
return NextResponse.json({ success: true, data: row || null });
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 목표가 저장 (procurStandMgmt.saveCostGoalInfo 대응)
|
||||
// body: { contract_objid, material_cost_goal, labor_cost_goal, expense_cost_goal }
|
||||
// input_cost_goal 테이블에 contract_objid 유니크 제약 없으므로 수동 upsert
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const contractObjid = String(body.contract_objid || "").trim();
|
||||
if (!contractObjid) {
|
||||
return NextResponse.json({ success: false, message: "프로젝트가 선택되지 않았습니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
// 콤마 제거
|
||||
const strip = (v: unknown) => String(v ?? "").replace(/,/g, "");
|
||||
const material = strip(body.material_cost_goal) || "0";
|
||||
const labor = strip(body.labor_cost_goal) || "0";
|
||||
const expense = strip(body.expense_cost_goal) || "0";
|
||||
|
||||
try {
|
||||
const existing = await queryOne<{ OBJID: string }>(
|
||||
`SELECT objid::text AS "OBJID" FROM input_cost_goal WHERE contract_objid = $1 LIMIT 1`,
|
||||
[contractObjid]
|
||||
);
|
||||
if (existing) {
|
||||
await execute(
|
||||
`UPDATE input_cost_goal
|
||||
SET material_cost_goal = $1,
|
||||
labor_cost_goal = $2,
|
||||
expense_cost_goal = $3,
|
||||
writer = $4,
|
||||
regdate = NOW()
|
||||
WHERE objid::text = $5`,
|
||||
[material, labor, expense, user.userId, existing.OBJID]
|
||||
);
|
||||
return NextResponse.json({ success: true, message: "수정되었습니다." });
|
||||
}
|
||||
await execute(
|
||||
`INSERT INTO input_cost_goal
|
||||
(objid, contract_objid, material_cost_goal, labor_cost_goal, expense_cost_goal, writer, regdate)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())`,
|
||||
[createObjectId(), contractObjid, material, labor, expense, user.userId]
|
||||
);
|
||||
return NextResponse.json({ success: true, message: "등록되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("Cost goal save:", error);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 노무비관리 (costTotaltGridList의 LC 노무비 계산 부분 포팅)
|
||||
// LABOR_COST_ACTUAL = LABOR_DESIGN_COST + LABOR_ASSEMBLY_COST
|
||||
// 설계: PMS_WBS_TASK (design_act_end - design_act_start) × 300000
|
||||
// 조립 insourcing: WORK_DIARY dept DPT005/013/023 work_hour/8 × 250000
|
||||
// 조립 outsourcing: WORK_DIARY sourcing_type='outsourcing' work_hour/8 × 350000
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(T.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`T.project_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.customer_objid) {
|
||||
conditions.push(`T.customer_objid = $${idx++}`);
|
||||
params.push(body.customer_objid);
|
||||
}
|
||||
if (body.product) {
|
||||
conditions.push(`T.product = $${idx++}`);
|
||||
params.push(body.product);
|
||||
}
|
||||
if (body.pm_user_id) {
|
||||
conditions.push(`T.pm_user_id = $${idx++}`);
|
||||
params.push(body.pm_user_id);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
WITH
|
||||
labor_design AS (
|
||||
SELECT contract_objid,
|
||||
(MAX(TO_DATE(NULLIF(design_act_end,''),'YYYY-MM-DD'))
|
||||
- MIN(TO_DATE(NULLIF(design_act_start,''),'YYYY-MM-DD'))) * 300000 AS cost
|
||||
FROM pms_wbs_task
|
||||
WHERE contract_objid IS NOT NULL
|
||||
AND NULLIF(design_act_start,'') IS NOT NULL
|
||||
AND NULLIF(design_act_end,'') IS NOT NULL
|
||||
GROUP BY contract_objid
|
||||
),
|
||||
labor_in AS (
|
||||
SELECT wd.contract_objid,
|
||||
SUM(COALESCE(NULLIF(wd.work_hour,''),'0')::numeric) AS hour
|
||||
FROM work_diary wd
|
||||
JOIN user_info ui ON ui.user_id = wd.worker_id
|
||||
WHERE wd.status = 'complete'
|
||||
AND wd.contract_objid IS NOT NULL AND wd.contract_objid <> ''
|
||||
AND ui.dept_code IN ('DPT005','DPT023','DPT013')
|
||||
GROUP BY wd.contract_objid
|
||||
),
|
||||
labor_out AS (
|
||||
SELECT contract_objid,
|
||||
SUM(COALESCE(NULLIF(work_hour,''),'0')::numeric) AS hour
|
||||
FROM work_diary
|
||||
WHERE status = 'complete'
|
||||
AND sourcing_type = 'outsourcing'
|
||||
AND contract_objid IS NOT NULL AND contract_objid <> ''
|
||||
GROUP BY contract_objid
|
||||
)
|
||||
SELECT T.objid::text AS "OBJID",
|
||||
T.project_no AS "PROJECT_NO",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = T.customer_objid LIMIT 1), '') AS "CUSTOMER_NAME",
|
||||
T.project_name AS "PROJECT_NAME",
|
||||
T.req_del_date AS "REQ_DEL_DATE",
|
||||
T.setup AS "SETUP",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = T.pm_user_id LIMIT 1), '') AS "PM_USER_NAME",
|
||||
COALESCE(NULLIF(ICG.labor_cost_goal,'')::numeric, 0) AS "LABOR_COST_GOAL",
|
||||
(COALESCE(LBD.cost, 0)
|
||||
+ COALESCE(LI.hour, 0) / 8 * 250000
|
||||
+ COALESCE(LO.hour, 0) / 8 * 350000) AS "LABOR_COST_ACTUAL",
|
||||
(COALESCE(LI.hour, 0) + COALESCE(LO.hour, 0)) AS "LABOR_HOURS"
|
||||
FROM project_mgmt T
|
||||
LEFT JOIN input_cost_goal ICG ON ICG.contract_objid = T.objid::text
|
||||
LEFT JOIN labor_design LBD ON LBD.contract_objid = T.objid::text
|
||||
LEFT JOIN labor_in LI ON LI.contract_objid = T.objid::text
|
||||
LEFT JOIN labor_out LO ON LO.contract_objid = T.objid::text
|
||||
WHERE ${where}
|
||||
ORDER BY SUBSTRING(T.project_no, POSITION('-' IN T.project_no)+1) DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
|
||||
const result = rows.map((r: Record<string, unknown>) => {
|
||||
const goal = Number(r.LABOR_COST_GOAL || 0);
|
||||
const actual = Number(r.LABOR_COST_ACTUAL || 0);
|
||||
return {
|
||||
...r,
|
||||
LABOR_INPUT_RATE: goal > 0 ? Math.round((actual / goal) * 1000) / 10 : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ RESULTLIST: result, TOTAL_CNT: result.length });
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 재료비관리 (materialCostTotaltGridList 원본 1:1 포팅)
|
||||
// ALL_TOTAL_PRICE = BOM + 재발주(0001408) + 장납기
|
||||
// NEW_TOTAL_PRICE = 발주(0001407)
|
||||
// ALL_TOTAL_PRICE_RE = 재발주(0001408)
|
||||
// MATERIAL_COST_GOAL_RATE = ALL_TOTAL_PRICE × 100 / MATERIAL_COST_GOAL
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(T.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`T.project_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.customer_objid) {
|
||||
conditions.push(`T.customer_objid = $${idx++}`);
|
||||
params.push(body.customer_objid);
|
||||
}
|
||||
if (body.product) {
|
||||
conditions.push(`T.product = $${idx++}`);
|
||||
params.push(body.product);
|
||||
}
|
||||
if (body.pm_user_id) {
|
||||
conditions.push(`T.pm_user_id = $${idx++}`);
|
||||
params.push(body.pm_user_id);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
WITH RECURSIVE view_bom AS (
|
||||
SELECT pbr.contract_objid, a.bom_report_objid, a.child_objid,
|
||||
1 AS lev, ARRAY[a.child_objid::text] AS path, false AS cycle,
|
||||
COALESCE(NULLIF(a.qty,''),'0')::numeric AS aggregate_qty
|
||||
FROM part_bom_report pbr
|
||||
JOIN bom_part_qty a ON pbr.objid = a.bom_report_objid
|
||||
WHERE (a.parent_objid IS NULL OR a.parent_objid = '')
|
||||
UNION ALL
|
||||
SELECT v.contract_objid, b.bom_report_objid, b.child_objid,
|
||||
v.lev + 1, v.path || b.child_objid::text,
|
||||
b.parent_objid = ANY(v.path),
|
||||
v.aggregate_qty * COALESCE(NULLIF(b.qty,''),'0')::numeric
|
||||
FROM bom_part_qty b
|
||||
JOIN view_bom v ON b.parent_objid = v.child_objid AND v.bom_report_objid = b.bom_report_objid
|
||||
WHERE NOT v.cycle
|
||||
),
|
||||
bom_material AS (
|
||||
SELECT pbr.contract_objid,
|
||||
SUM((COALESCE(NULLIF(sp.price,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price1,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price2,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price3,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price4,''),'0')::numeric) * v.aggregate_qty) AS total
|
||||
FROM part_bom_report pbr
|
||||
JOIN sales_bom_report sb ON sb.parent_objid = pbr.objid
|
||||
JOIN sales_bom_report_part sp ON sp.parent_objid = pbr.objid
|
||||
JOIN view_bom v ON v.child_objid = sp.bom_part_qty_objid
|
||||
GROUP BY pbr.contract_objid
|
||||
),
|
||||
new_order AS (
|
||||
SELECT contract_mgmt_objid,
|
||||
SUM(COALESCE(NULLIF(total_supply_unit_price,'')::numeric, 0)) AS total
|
||||
FROM purchase_order_master
|
||||
WHERE status = 'approvalComplete' AND order_type_cd = '0001407'
|
||||
AND contract_mgmt_objid IS NOT NULL
|
||||
GROUP BY contract_mgmt_objid
|
||||
),
|
||||
re_order AS (
|
||||
SELECT contract_mgmt_objid,
|
||||
SUM(COALESCE(NULLIF(total_supply_unit_price,'')::numeric, 0)) AS total
|
||||
FROM purchase_order_master
|
||||
WHERE status = 'approvalComplete' AND order_type_cd = '0001408'
|
||||
AND contract_mgmt_objid IS NOT NULL
|
||||
GROUP BY contract_mgmt_objid
|
||||
),
|
||||
long_delivery AS (
|
||||
SELECT li.contract_objid,
|
||||
SUM(NULLIF(li.input_qty,'')::numeric * NULLIF(l.price,'')::numeric) AS total
|
||||
FROM sales_long_delivery_input li
|
||||
JOIN sales_long_delivery l ON l.objid = li.parent_objid
|
||||
WHERE NULLIF(li.input_qty,'') IS NOT NULL
|
||||
AND NULLIF(l.price,'') IS NOT NULL
|
||||
GROUP BY li.contract_objid
|
||||
)
|
||||
SELECT T.objid::text AS "OBJID",
|
||||
T.project_no AS "PROJECT_NO",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = T.customer_objid LIMIT 1), '') AS "CUSTOMER_NAME",
|
||||
T.project_name AS "PROJECT_NAME",
|
||||
T.req_del_date AS "REQ_DEL_DATE",
|
||||
T.setup AS "SETUP",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = T.pm_user_id LIMIT 1), '') AS "PM_USER_NAME",
|
||||
COALESCE(NULLIF(ICG.material_cost_goal,'')::numeric, 0) AS "MATERIAL_COST_GOAL",
|
||||
COALESCE(BM.total, 0) + COALESCE(RO.total, 0) + COALESCE(LD.total, 0) AS "ALL_TOTAL_PRICE",
|
||||
COALESCE(NO_.total, 0) AS "NEW_TOTAL_PRICE",
|
||||
COALESCE(RO.total, 0) AS "ALL_TOTAL_PRICE_RE"
|
||||
FROM project_mgmt T
|
||||
LEFT JOIN input_cost_goal ICG ON ICG.contract_objid = T.objid::text
|
||||
LEFT JOIN bom_material BM ON BM.contract_objid = T.objid::text
|
||||
LEFT JOIN new_order NO_ ON NO_.contract_mgmt_objid = T.objid::text
|
||||
LEFT JOIN re_order RO ON RO.contract_mgmt_objid = T.objid::text
|
||||
LEFT JOIN long_delivery LD ON LD.contract_objid = T.objid::text
|
||||
WHERE ${where}
|
||||
ORDER BY SUBSTRING(T.project_no, POSITION('-' IN T.project_no)+1) DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
|
||||
const result = rows.map((r: Record<string, unknown>) => {
|
||||
const goal = Number(r.MATERIAL_COST_GOAL || 0);
|
||||
const actual = Number(r.ALL_TOTAL_PRICE || 0);
|
||||
return {
|
||||
...r,
|
||||
MATERIAL_COST_GOAL_RATE: goal > 0 ? Math.round((actual / goal) * 1000) / 10 : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ RESULTLIST: result, TOTAL_CNT: result.length });
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 투입원가현황 (costTotaltGridList 원본 1:1 포팅)
|
||||
// 주 테이블: project_mgmt
|
||||
// 재료비 실적 = BOM(SALES_BOM_REPORT_PART price × 누적수량) + 재발주(0001408) + 장납기
|
||||
// 노무비 실적 = 설계(PMS_WBS_TASK 기간×300K) + 조립(WORK_DIARY DPT005/013/023 work_hour/8*250K + sourcing_type='outsourcing' /8*350K)
|
||||
// 경비 실적 = EXPENSE_DETAIL CARD+CASH-PAYMENT
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(T.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`T.project_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.customer_objid) {
|
||||
conditions.push(`T.customer_objid = $${idx++}`);
|
||||
params.push(body.customer_objid);
|
||||
}
|
||||
if (body.product) {
|
||||
conditions.push(`T.product = $${idx++}`);
|
||||
params.push(body.product);
|
||||
}
|
||||
if (body.pm_user_id) {
|
||||
conditions.push(`T.pm_user_id = $${idx++}`);
|
||||
params.push(body.pm_user_id);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
WITH RECURSIVE view_bom AS (
|
||||
SELECT pbr.contract_objid,
|
||||
a.bom_report_objid,
|
||||
a.child_objid,
|
||||
1 AS lev,
|
||||
ARRAY[a.child_objid::text] AS path,
|
||||
false AS cycle,
|
||||
COALESCE(NULLIF(a.qty,''),'0')::numeric AS aggregate_qty
|
||||
FROM part_bom_report pbr
|
||||
JOIN bom_part_qty a ON pbr.objid = a.bom_report_objid
|
||||
WHERE (a.parent_objid IS NULL OR a.parent_objid = '')
|
||||
UNION ALL
|
||||
SELECT v.contract_objid,
|
||||
b.bom_report_objid,
|
||||
b.child_objid,
|
||||
v.lev + 1,
|
||||
v.path || b.child_objid::text,
|
||||
b.parent_objid = ANY(v.path),
|
||||
v.aggregate_qty * COALESCE(NULLIF(b.qty,''),'0')::numeric
|
||||
FROM bom_part_qty b
|
||||
JOIN view_bom v ON b.parent_objid = v.child_objid
|
||||
AND v.bom_report_objid = b.bom_report_objid
|
||||
WHERE NOT v.cycle
|
||||
),
|
||||
bom_material AS (
|
||||
SELECT pbr.contract_objid,
|
||||
SUM((COALESCE(NULLIF(sp.price,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price1,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price2,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price3,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(sp.price4,''),'0')::numeric) * v.aggregate_qty) AS all_total_price
|
||||
FROM part_bom_report pbr
|
||||
JOIN sales_bom_report sb ON sb.parent_objid = pbr.objid
|
||||
JOIN sales_bom_report_part sp ON sp.parent_objid = pbr.objid
|
||||
JOIN view_bom v ON v.child_objid = sp.bom_part_qty_objid
|
||||
GROUP BY pbr.contract_objid
|
||||
),
|
||||
re_order AS (
|
||||
SELECT contract_mgmt_objid,
|
||||
SUM(COALESCE(NULLIF(total_supply_unit_price,'')::numeric, 0)) AS total
|
||||
FROM purchase_order_master
|
||||
WHERE status = 'approvalComplete' AND order_type_cd = '0001408'
|
||||
AND contract_mgmt_objid IS NOT NULL
|
||||
GROUP BY contract_mgmt_objid
|
||||
),
|
||||
long_delivery AS (
|
||||
SELECT li.contract_objid,
|
||||
SUM(NULLIF(li.input_qty,'')::numeric * NULLIF(l.price,'')::numeric) AS total
|
||||
FROM sales_long_delivery_input li
|
||||
JOIN sales_long_delivery l ON l.objid = li.parent_objid
|
||||
WHERE NULLIF(li.input_qty,'') IS NOT NULL
|
||||
AND NULLIF(l.price,'') IS NOT NULL
|
||||
GROUP BY li.contract_objid
|
||||
),
|
||||
labor_design AS (
|
||||
SELECT contract_objid,
|
||||
(MAX(TO_DATE(NULLIF(design_act_end,''),'YYYY-MM-DD'))
|
||||
- MIN(TO_DATE(NULLIF(design_act_start,''),'YYYY-MM-DD'))) * 300000 AS cost
|
||||
FROM pms_wbs_task
|
||||
WHERE contract_objid IS NOT NULL
|
||||
AND NULLIF(design_act_start,'') IS NOT NULL
|
||||
AND NULLIF(design_act_end,'') IS NOT NULL
|
||||
GROUP BY contract_objid
|
||||
),
|
||||
labor_in AS (
|
||||
SELECT wd.contract_objid,
|
||||
SUM(COALESCE(NULLIF(wd.work_hour,''),'0')::numeric) AS hour
|
||||
FROM work_diary wd
|
||||
JOIN user_info ui ON ui.user_id = wd.worker_id
|
||||
WHERE wd.status = 'complete'
|
||||
AND wd.contract_objid IS NOT NULL AND wd.contract_objid <> ''
|
||||
AND ui.dept_code IN ('DPT005','DPT023','DPT013')
|
||||
GROUP BY wd.contract_objid
|
||||
),
|
||||
labor_out AS (
|
||||
SELECT contract_objid,
|
||||
SUM(COALESCE(NULLIF(work_hour,''),'0')::numeric) AS hour
|
||||
FROM work_diary
|
||||
WHERE status = 'complete'
|
||||
AND sourcing_type = 'outsourcing'
|
||||
AND contract_objid IS NOT NULL AND contract_objid <> ''
|
||||
GROUP BY contract_objid
|
||||
),
|
||||
expense_settle AS (
|
||||
SELECT M.project_mgmt_objid,
|
||||
SUM(COALESCE(NULLIF(D.card_used,'')::numeric, 0)
|
||||
+ COALESCE(NULLIF(D.cash_used,'')::numeric, 0)
|
||||
- COALESCE(NULLIF(D.payment,'')::numeric, 0)) AS amount
|
||||
FROM expense_master M
|
||||
LEFT JOIN expense_detail D ON M.expense_master_objid = D.expense_master_objid
|
||||
GROUP BY M.project_mgmt_objid
|
||||
)
|
||||
SELECT T.objid::text AS "OBJID",
|
||||
T.project_no AS "PROJECT_NO",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = T.customer_objid LIMIT 1), '') AS "CUSTOMER_NAME",
|
||||
T.project_name AS "PROJECT_NAME",
|
||||
T.req_del_date AS "REQ_DEL_DATE",
|
||||
T.setup AS "SETUP",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = T.pm_user_id LIMIT 1), '') AS "PM_USER_NAME",
|
||||
COALESCE(NULLIF(T.contract_price,'')::numeric, 0) AS "CONTRACT_PRICE",
|
||||
COALESCE(NULLIF(ICG.material_cost_goal,'')::numeric, 0) AS "MATERIAL_COST_GOAL",
|
||||
COALESCE(NULLIF(ICG.labor_cost_goal,'')::numeric, 0) AS "LABOR_COST_GOAL",
|
||||
COALESCE(NULLIF(ICG.expense_cost_goal,'')::numeric, 0) AS "EXPENSE_COST_GOAL",
|
||||
(COALESCE(NULLIF(ICG.material_cost_goal,'')::numeric, 0)
|
||||
+ COALESCE(NULLIF(ICG.labor_cost_goal,'')::numeric, 0)
|
||||
+ COALESCE(NULLIF(ICG.expense_cost_goal,'')::numeric, 0)) AS "TOTAL_COST_GOAL",
|
||||
(COALESCE(BM.all_total_price, 0)
|
||||
+ COALESCE(RO.total, 0)
|
||||
+ COALESCE(LD.total, 0)) AS "ACCRUAL_MATERIAL_COST",
|
||||
(COALESCE(LBD.cost, 0)
|
||||
+ COALESCE(LI.hour, 0) / 8 * 250000
|
||||
+ COALESCE(LO.hour, 0) / 8 * 350000) AS "LABOR_COST_ACTUAL",
|
||||
COALESCE(ES.amount, 0) AS "ACCRUAL_EXPENSE"
|
||||
FROM project_mgmt T
|
||||
LEFT JOIN input_cost_goal ICG ON ICG.contract_objid = T.objid::text
|
||||
LEFT JOIN bom_material BM ON BM.contract_objid = T.objid::text
|
||||
LEFT JOIN re_order RO ON RO.contract_mgmt_objid = T.objid::text
|
||||
LEFT JOIN long_delivery LD ON LD.contract_objid = T.objid::text
|
||||
LEFT JOIN labor_design LBD ON LBD.contract_objid = T.objid::text
|
||||
LEFT JOIN labor_in LI ON LI.contract_objid = T.objid::text
|
||||
LEFT JOIN labor_out LO ON LO.contract_objid = T.objid::text
|
||||
LEFT JOIN expense_settle ES ON ES.project_mgmt_objid = T.objid::text
|
||||
WHERE ${where}
|
||||
ORDER BY SUBSTRING(T.project_no, POSITION('-' IN T.project_no)+1) DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
|
||||
// 원본 공식 그대로:
|
||||
// TOTAL_COST_ACTUAL = 재료비실적 + 노무비실적 + 경비실적
|
||||
// TOTAL_INPUT_RATE = TOTAL_ACTUAL / TOTAL_GOAL × 100
|
||||
// MC_RATE = 재료비실적 / TOTAL_GOAL × 100 (실제 원본 SQL: 실투입재료 / 목표합계)
|
||||
// MATERIAL_COST_GOAL_RATE = 재료비실적 / CONTRACT_PRICE × 100
|
||||
// LABOR_INPUT_RATE = 노무비실적 / 노무비목표 × 100
|
||||
// EXPENSE_RATE = 경비실적 / 경비목표 × 100
|
||||
const result = rows.map((r: Record<string, unknown>) => {
|
||||
const tg = Number(r.TOTAL_COST_GOAL || 0);
|
||||
const lg = Number(r.LABOR_COST_GOAL || 0);
|
||||
const eg = Number(r.EXPENSE_COST_GOAL || 0);
|
||||
const ma = Number(r.ACCRUAL_MATERIAL_COST || 0);
|
||||
const la = Number(r.LABOR_COST_ACTUAL || 0);
|
||||
const ea = Number(r.ACCRUAL_EXPENSE || 0);
|
||||
const ta = ma + la + ea;
|
||||
const cp = Number(r.CONTRACT_PRICE || 0);
|
||||
return {
|
||||
...r,
|
||||
TOTAL_COST_ACTUAL: ta,
|
||||
TOTAL_INPUT_RATE: tg > 0 ? Math.round((ta / tg) * 1000) / 10 : 0,
|
||||
MC_RATE: tg > 0 ? Math.round((ma / tg) * 1000) / 10 : 0,
|
||||
MATERIAL_COST_GOAL_RATE: cp > 0 ? Math.round((ma / cp) * 1000) / 10 : 0,
|
||||
LABOR_INPUT_RATE: lg > 0 ? Math.round((la / lg) * 1000) / 10 : 0,
|
||||
EXPENSE_RATE: eg > 0 ? Math.round((ea / eg) * 1000) / 10 : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ RESULTLIST: result, TOTAL_CNT: result.length });
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// CS 차트 데이터 (getAsTotalList_CS / getASDashboardList 대응)
|
||||
// 년도/제품별 CS 건수 집계 + 유/무상 구분
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`A.year = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.product_code) {
|
||||
conditions.push(`A.product_code = $${idx++}`);
|
||||
params.push(body.product_code);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
// 제품별 CS 건수 + 유/무상 구분
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
COALESCE(A.product_name, (SELECT code_name FROM comm_code WHERE code_id = A.product_code LIMIT 1), '미분류') AS "PRODUCT_NAME",
|
||||
COUNT(*) AS "TOTAL_CNT",
|
||||
COUNT(CASE WHEN A.paid_free = '유상' OR A.warranty_code = '0000157' THEN 1 END) AS "PAID_CNT",
|
||||
COUNT(CASE WHEN A.paid_free = '무상' OR A.warranty_code = '0000158' THEN 1 END) AS "FREE_CNT",
|
||||
COUNT(CASE WHEN A.status_cd = 'AS_STATUS_03' OR A.status_cd = '0000102' THEN 1 END) AS "COMPLETE_CNT",
|
||||
COUNT(CASE WHEN A.status_cd IS NULL OR (A.status_cd != 'AS_STATUS_03' AND A.status_cd != '0000102') THEN 1 END) AS "INCOMPLETE_CNT",
|
||||
-- 월별 건수 (차트용)
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 1 THEN 1 END) AS "M01",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 2 THEN 1 END) AS "M02",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 3 THEN 1 END) AS "M03",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 4 THEN 1 END) AS "M04",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 5 THEN 1 END) AS "M05",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 6 THEN 1 END) AS "M06",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 7 THEN 1 END) AS "M07",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 8 THEN 1 END) AS "M08",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 9 THEN 1 END) AS "M09",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 10 THEN 1 END) AS "M10",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 11 THEN 1 END) AS "M11",
|
||||
COUNT(CASE WHEN EXTRACT(MONTH FROM A.reg_date) = 12 THEN 1 END) AS "M12"
|
||||
FROM as_mng A
|
||||
WHERE ${where}
|
||||
GROUP BY A.product_code, A.product_name
|
||||
ORDER BY "TOTAL_CNT" DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// CS 결재상신 처리 — status_cd='approvalRequest' 업데이트
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objIds } = await request.json();
|
||||
if (!objIds?.length) return NextResponse.json({ success: false, message: "상신할 항목을 선택하세요." });
|
||||
try {
|
||||
const ph = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
await execute(
|
||||
`UPDATE as_mng SET status_cd = 'approvalRequest' WHERE objid IN (${ph})`,
|
||||
objIds
|
||||
);
|
||||
return NextResponse.json({ success: true, message: `${objIds.length}건 결재상신되었습니다.` });
|
||||
} catch (error) {
|
||||
console.error("CS approval:", error);
|
||||
return NextResponse.json({ success: false, message: "상신 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// CS관리 삭제 (asDelete 대응) — CSM + 부품/작업시간 + 첨부파일 일괄 삭제
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const { objIds } = await request.json();
|
||||
if (!objIds?.length) {
|
||||
return NextResponse.json({ success: false, message: "삭제할 항목을 선택하세요." });
|
||||
}
|
||||
|
||||
try {
|
||||
const ph = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
await execute(`DELETE FROM customer_service_part WHERE parent_objid IN (${ph})`, objIds);
|
||||
await execute(`DELETE FROM customer_service_workingtime WHERE parent_objid IN (${ph})`, objIds);
|
||||
await execute(`DELETE FROM attach_file_info WHERE target_objid IN (${ph})`, objIds);
|
||||
await execute(`DELETE FROM customer_service_mgmt WHERE objid IN (${ph})`, objIds);
|
||||
return NextResponse.json({ success: true, message: `${objIds.length}건이 삭제되었습니다.` });
|
||||
} catch (error) {
|
||||
console.error("CS delete:", error);
|
||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne, queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// CS 단건 조회 (getCSMInfo + getCSPList + getCSWList 대응)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId required" });
|
||||
|
||||
const info = await queryOne(
|
||||
`SELECT CSM.objid::text AS "OBJID",
|
||||
CSM.service_no AS "SERVICE_NO",
|
||||
CSM.product AS "PRODUCT",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.product) AS "PRODUCT_NAME",
|
||||
CSM.contract_objid AS "CONTRACT_OBJID",
|
||||
CSM.cs_category AS "CS_CATEGORY",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.cs_category) AS "CATEGORY_NAME",
|
||||
CSM.warranty AS "WARRANTY",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.warranty) AS "WARRANTY_NAME",
|
||||
CSM.manager_id AS "MANAGER_ID",
|
||||
(SELECT user_name FROM user_info WHERE user_id = CSM.manager_id) AS "MANAGER_NAME",
|
||||
CSM.act_date AS "ACT_DATE",
|
||||
CSM.category_h AS "CATEGORY_H",
|
||||
CSM.category_m AS "CATEGORY_M",
|
||||
CSM.category_l AS "CATEGORY_L",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.category_h) AS "CATEGORY_H_NAME",
|
||||
CSM.title AS "TITLE",
|
||||
CSM.before_contents AS "BEFORE_CONTENTS",
|
||||
CSM.after_contents AS "AFTER_CONTENTS",
|
||||
CSM.writer AS "WRITER",
|
||||
CSM.status AS "STATUS",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.status) AS "STATUS_NAME",
|
||||
TO_CHAR(CSM.regdate, 'YYYY-MM-DD') AS "REC_DT",
|
||||
CM.project_no AS "PROJECT_NO",
|
||||
CM.customer_objid AS "CUSTOMER_OBJID",
|
||||
(SELECT supply_name FROM supply_mng WHERE objid::text = CM.customer_objid) AS "CUSTOMER_NAME",
|
||||
CM.setup AS "SETUP",
|
||||
RM.release_date AS "RELEASE_DATE",
|
||||
APPR.APPR_STATUS AS "APPR_STATUS",
|
||||
APPR.APPR_STATUS_NAME AS "APPR_STATUS_NAME",
|
||||
APPR.APPROVAL_OBJID AS "APPROVAL_OBJID",
|
||||
APPR.ROUTE_OBJID AS "ROUTE_OBJID"
|
||||
FROM customer_service_mgmt CSM
|
||||
LEFT JOIN project_mgmt CM ON CSM.contract_objid = CM.objid
|
||||
LEFT JOIN release_mgmt RM ON CM.objid = RM.parent_objid
|
||||
LEFT JOIN (
|
||||
SELECT B.objid AS ROUTE_OBJID,
|
||||
B.status AS APPR_STATUS,
|
||||
CASE UPPER(B.status)
|
||||
WHEN 'INPROCESS' THEN '결재중'
|
||||
WHEN 'COMPLETE' THEN '결재완료'
|
||||
WHEN 'REJECT' THEN '반려'
|
||||
ELSE '' END AS APPR_STATUS_NAME,
|
||||
A.objid AS APPROVAL_OBJID,
|
||||
A.target_objid
|
||||
FROM approval A,
|
||||
(SELECT T1.* FROM
|
||||
(SELECT target_objid, MAX(route_seq) AS route_seq FROM route GROUP BY target_objid) T,
|
||||
route T1
|
||||
WHERE T.target_objid = T1.target_objid AND T.route_seq = T1.route_seq) B
|
||||
WHERE A.objid = B.approval_objid AND A.target_type = 'CSM'
|
||||
) APPR ON CSM.objid::numeric = APPR.target_objid
|
||||
WHERE CSM.objid = $1`,
|
||||
[objId]
|
||||
);
|
||||
|
||||
if (!info) return NextResponse.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
|
||||
const parts = await queryRows(
|
||||
`SELECT objid::text AS "OBJID", parent_objid AS "PARENT_OBJID",
|
||||
part_no AS "PART_NO", part_name AS "PART_NAME", spec AS "SPEC",
|
||||
qty AS "QTY", cur_qty AS "CUR_QTY", price AS "PRICE", sup_price AS "SUP_PRICE"
|
||||
FROM customer_service_part WHERE parent_objid = $1 ORDER BY objid`,
|
||||
[objId]
|
||||
);
|
||||
|
||||
const works = await queryRows(
|
||||
`SELECT objid::text AS "OBJID", parent_objid AS "PARENT_OBJID",
|
||||
supply_objid AS "SUPPLY_OBJID", form_date AS "FORM_DATE", to_date AS "TO_DATE",
|
||||
work_day AS "WORK_DAY", work_person AS "WORK_PERSON", work_day_m AS "WORK_DAY_M",
|
||||
labor_cost AS "LABOR_COST", expenses AS "EXPENSES"
|
||||
FROM customer_service_workingtime WHERE parent_objid = $1 ORDER BY objid`,
|
||||
[objId]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true, data: info, parts, works });
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// CS관리 - 등록 및 조치 목록 (asMngList_CS.jsp + getECRList_CS 대응)
|
||||
// FITO: /as/asMngGridList.do → CUSTOMER_SERVICE_MGMT + PROJECT_MGMT + RELEASE_MGMT + CUSTOMER_SERVICE_WORKINGTIME + APPROVAL/ROUTE
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.Year || body.year) {
|
||||
conditions.push(`TO_CHAR(CSM.regdate,'YYYY') = $${idx++}`);
|
||||
params.push(String(body.Year || body.year));
|
||||
}
|
||||
if (body.product_cd) {
|
||||
conditions.push(`CSM.product = $${idx++}`);
|
||||
params.push(body.product_cd);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`CSM.contract_objid = $${idx++}`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.project_nos && Array.isArray(body.project_nos) && body.project_nos.length > 0) {
|
||||
const placeholders = body.project_nos.map(() => `$${idx++}`).join(",");
|
||||
conditions.push(`CSM.contract_objid IN (${placeholders})`);
|
||||
params.push(...body.project_nos);
|
||||
}
|
||||
if (body.warranty) {
|
||||
conditions.push(`CSM.warranty = $${idx++}`);
|
||||
params.push(body.warranty);
|
||||
}
|
||||
if (body.category_h) {
|
||||
conditions.push(`CSM.category_h = $${idx++}`);
|
||||
params.push(body.category_h);
|
||||
}
|
||||
if (body.rec_start_date) {
|
||||
conditions.push(`TO_DATE(TO_CHAR(CSM.regdate,'YYYY-MM-DD'),'YYYY-MM-DD') >= TO_DATE($${idx++},'YYYY-MM-DD')`);
|
||||
params.push(body.rec_start_date);
|
||||
}
|
||||
if (body.rec_end_date) {
|
||||
conditions.push(`TO_DATE(TO_CHAR(CSM.regdate,'YYYY-MM-DD'),'YYYY-MM-DD') <= TO_DATE($${idx++},'YYYY-MM-DD')`);
|
||||
params.push(body.rec_end_date);
|
||||
}
|
||||
if (body.manager_id) {
|
||||
conditions.push(`CSM.manager_id = $${idx++}`);
|
||||
params.push(body.manager_id);
|
||||
}
|
||||
if (body.act_start_date) {
|
||||
conditions.push(`TO_DATE(CSM.act_date,'YYYY-MM-DD') >= TO_DATE($${idx++},'YYYY-MM-DD')`);
|
||||
params.push(body.act_start_date);
|
||||
}
|
||||
if (body.act_end_date) {
|
||||
conditions.push(`TO_DATE(CSM.act_date,'YYYY-MM-DD') <= TO_DATE($${idx++},'YYYY-MM-DD')`);
|
||||
params.push(body.act_end_date);
|
||||
}
|
||||
if (body.appr_status) {
|
||||
conditions.push(`APPR.APPR_STATUS = $${idx++}`);
|
||||
params.push(body.appr_status);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
CSM.objid::text AS "OBJID",
|
||||
CSM.service_no AS "SERVICE_NO",
|
||||
CSM.product AS "PRODUCT",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.product) AS "PRODUCT_NAME",
|
||||
CSM.contract_objid AS "CONTRACT_OBJID",
|
||||
CSM.cs_category AS "CS_CATEGORY",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.cs_category) AS "CATEGORY_NAME",
|
||||
CSM.warranty AS "WARRANTY",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.warranty) AS "WARRANTY_NAME",
|
||||
CSM.manager_id AS "MANAGER_ID",
|
||||
(SELECT user_name FROM user_info WHERE user_id = CSM.manager_id) AS "MANAGER_NAME",
|
||||
CSM.act_date AS "ACT_DATE",
|
||||
CSM.category_h AS "CATEGORY_H",
|
||||
CSM.category_m AS "CATEGORY_M",
|
||||
CSM.category_l AS "CATEGORY_L",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.category_h) AS "CATEGORY_H_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.category_m) AS "CATEGORY_M_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.category_l) AS "CATEGORY_L_NAME",
|
||||
CSM.title AS "TITLE",
|
||||
CSM.writer AS "WRITER",
|
||||
CSM.status AS "STATUS",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CSM.status) AS "STATUS_NAME",
|
||||
TO_CHAR(CSM.regdate,'YYYY-MM-DD') AS "REC_DT",
|
||||
CM.project_no AS "PROJECT_NO",
|
||||
CM.setup AS "SETUP",
|
||||
CM.customer_objid AS "CUSTOMER_OBJID",
|
||||
(SELECT supply_name FROM supply_mng WHERE objid::text = CM.customer_objid) AS "CUSTOMER_NAME",
|
||||
RM.release_date AS "RELEASE_DATE",
|
||||
CSW.PLAN_COST AS "PLAN_COST",
|
||||
(SELECT COUNT(1) FROM attach_file_info WHERE target_objid = CSM.objid AND doc_type='AS_DOC_01' AND UPPER(status) = 'ACTIVE') AS "CU03_CNT",
|
||||
APPR.APPROVAL_OBJID AS "APPROVAL_OBJID",
|
||||
APPR.ROUTE_OBJID AS "ROUTE_OBJID",
|
||||
APPR.APPR_STATUS AS "APPR_STATUS",
|
||||
APPR.APPR_STATUS_NAME AS "APPR_STATUS_NAME"
|
||||
FROM customer_service_mgmt AS CSM
|
||||
LEFT OUTER JOIN project_mgmt AS CM ON CSM.contract_objid = CM.objid
|
||||
LEFT OUTER JOIN release_mgmt AS RM ON CM.objid = RM.parent_objid
|
||||
LEFT OUTER JOIN (
|
||||
SELECT parent_objid,
|
||||
SUM(COALESCE(NULLIF(labor_cost,''),'0')::numeric + COALESCE(NULLIF(expenses,''),'0')::numeric) AS PLAN_COST
|
||||
FROM customer_service_workingtime
|
||||
GROUP BY parent_objid
|
||||
) AS CSW ON CSM.objid = CSW.parent_objid
|
||||
LEFT OUTER JOIN (
|
||||
SELECT B.objid AS ROUTE_OBJID,
|
||||
B.status AS APPR_STATUS,
|
||||
CASE UPPER(B.status)
|
||||
WHEN 'INPROCESS' THEN '결재중'
|
||||
WHEN 'COMPLETE' THEN '결재완료'
|
||||
WHEN 'REJECT' THEN '반려'
|
||||
ELSE '' END AS APPR_STATUS_NAME,
|
||||
A.objid AS APPROVAL_OBJID,
|
||||
A.target_objid,
|
||||
B.route_seq
|
||||
FROM approval A,
|
||||
(SELECT T1.*
|
||||
FROM (SELECT target_objid, MAX(route_seq) AS route_seq
|
||||
FROM route GROUP BY target_objid) T,
|
||||
route T1
|
||||
WHERE T.target_objid = T1.target_objid
|
||||
AND T.route_seq = T1.route_seq) B
|
||||
WHERE A.objid = B.approval_objid
|
||||
AND A.target_type = 'CSM'
|
||||
) AS APPR ON CSM.objid::numeric = APPR.target_objid
|
||||
WHERE ${where}
|
||||
ORDER BY CSM.regdate DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// CS관리 저장 (saveas.do → mergeCSM + deleteCSP/mergeCSP + deleteCSW/mergeCSW)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
let objId = String(body.objId || "").trim();
|
||||
const isNew = !objId;
|
||||
if (isNew) objId = createObjectId();
|
||||
|
||||
// SERVICE_NO 자동 채번 (신규일 때만)
|
||||
let serviceNo = String(body.service_no || "").trim();
|
||||
if (!serviceNo) {
|
||||
const r = await client.query(
|
||||
`SELECT 'CSM'||TO_CHAR(NOW(),'yy')::VARCHAR||'-'||LPAD(nextval('seq_as_no')::VARCHAR,4,'0') AS no`
|
||||
);
|
||||
serviceNo = r.rows[0].no;
|
||||
}
|
||||
|
||||
const status = body.status && body.status !== "" ? body.status : "0000100";
|
||||
|
||||
// MERGE CSM
|
||||
await client.query(
|
||||
`INSERT INTO customer_service_mgmt (
|
||||
objid, service_no, product, contract_objid, cs_category, warranty,
|
||||
manager_id, act_date, category_h, category_m, category_l, title,
|
||||
before_contents, after_contents, writer, regdate, status,
|
||||
total_sup_price, total_work_day, total_work_person, total_work_day_m,
|
||||
total_labor_cost, total_expenses
|
||||
) VALUES ($1,$2,CASE WHEN $3='' THEN NULL ELSE $3 END,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,now(),$16,$17,$18,$19,$20,$21,$22)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
service_no=EXCLUDED.service_no,
|
||||
product=EXCLUDED.product,
|
||||
contract_objid=EXCLUDED.contract_objid,
|
||||
cs_category=EXCLUDED.cs_category,
|
||||
warranty=EXCLUDED.warranty,
|
||||
manager_id=EXCLUDED.manager_id,
|
||||
act_date=EXCLUDED.act_date,
|
||||
category_h=EXCLUDED.category_h,
|
||||
category_m=EXCLUDED.category_m,
|
||||
category_l=EXCLUDED.category_l,
|
||||
title=EXCLUDED.title,
|
||||
before_contents=EXCLUDED.before_contents,
|
||||
after_contents=EXCLUDED.after_contents,
|
||||
writer=EXCLUDED.writer,
|
||||
regdate=now(),
|
||||
status=EXCLUDED.status,
|
||||
total_sup_price=EXCLUDED.total_sup_price,
|
||||
total_work_day=EXCLUDED.total_work_day,
|
||||
total_work_person=EXCLUDED.total_work_person,
|
||||
total_work_day_m=EXCLUDED.total_work_day_m,
|
||||
total_labor_cost=EXCLUDED.total_labor_cost,
|
||||
total_expenses=EXCLUDED.total_expenses`,
|
||||
[
|
||||
objId,
|
||||
serviceNo,
|
||||
body.product || "",
|
||||
body.contract_objid || "",
|
||||
body.cs_category || "",
|
||||
body.warranty || "",
|
||||
body.manager_id || "",
|
||||
body.act_date || "",
|
||||
body.category_h || "",
|
||||
body.category_m || "",
|
||||
body.category_l || "",
|
||||
body.title || "",
|
||||
body.before_contents || "",
|
||||
body.after_contents || "",
|
||||
body.writer || user.userId,
|
||||
status,
|
||||
String(body.TOTAL_SUP_PRICE || body.total_sup_price || "0").replace(/,/g, ""),
|
||||
String(body.TOTAL_WORK_DAY || body.total_work_day || "0").replace(/,/g, ""),
|
||||
String(body.TOTAL_WORK_PERSON || body.total_work_person || "0").replace(/,/g, ""),
|
||||
String(body.TOTAL_WORK_DAY_M || body.total_work_day_m || "0").replace(/,/g, ""),
|
||||
String(body.TOTAL_LABOR_COST || body.total_labor_cost || "0").replace(/,/g, ""),
|
||||
String(body.TOTAL_EXPENSES || body.total_expenses || "0").replace(/,/g, ""),
|
||||
]
|
||||
);
|
||||
|
||||
// CSP: 전체 삭제 후 재삽입
|
||||
await client.query(`DELETE FROM customer_service_part WHERE parent_objid = $1`, [objId]);
|
||||
const parts: Array<Record<string, unknown>> = Array.isArray(body.parts) ? body.parts : [];
|
||||
for (const p of parts) {
|
||||
const partObjId = String(p.OBJID || "").trim() || createObjectId();
|
||||
await client.query(
|
||||
`INSERT INTO customer_service_part
|
||||
(objid, parent_objid, part_no, part_name, spec, qty, cur_qty, price, sup_price)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
|
||||
[
|
||||
partObjId,
|
||||
objId,
|
||||
String(p.PART_NO || ""),
|
||||
String(p.PART_NAME || ""),
|
||||
String(p.SPEC || ""),
|
||||
String(p.QTY || "0").replace(/,/g, ""),
|
||||
String(p.CUR_QTY || "0").replace(/,/g, ""),
|
||||
String(p.PRICE || "0").replace(/,/g, ""),
|
||||
String(p.SUP_PRICE || "0").replace(/,/g, ""),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// CSW: 전체 삭제 후 재삽입
|
||||
await client.query(`DELETE FROM customer_service_workingtime WHERE parent_objid = $1`, [objId]);
|
||||
const works: Array<Record<string, unknown>> = Array.isArray(body.works) ? body.works : [];
|
||||
for (const w of works) {
|
||||
const workObjId = String(w.OBJID || "").trim() || createObjectId();
|
||||
await client.query(
|
||||
`INSERT INTO customer_service_workingtime
|
||||
(objid, parent_objid, supply_objid, form_date, to_date, work_day, work_person, work_day_m, labor_cost, expenses)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
|
||||
[
|
||||
workObjId,
|
||||
objId,
|
||||
String(w.SUPPLY_OBJID || ""),
|
||||
String(w.FORM_DATE || ""),
|
||||
String(w.TO_DATE || ""),
|
||||
String(w.WORK_DAY || "0").replace(/,/g, ""),
|
||||
String(w.WORK_PERSON || "0").replace(/,/g, ""),
|
||||
String(w.WORK_DAY_M || "0").replace(/,/g, ""),
|
||||
String(w.LABOR_COST || "0").replace(/,/g, ""),
|
||||
String(w.EXPENSES || "0").replace(/,/g, ""),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: isNew ? "등록되었습니다." : "수정되었습니다.",
|
||||
objId,
|
||||
serviceNo,
|
||||
});
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("CS save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// CS 메인 목록 (as_mng 기반 — customerServiceList.jsp 대응)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`A.year = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.customer_name) {
|
||||
conditions.push(`COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = A.custcd LIMIT 1), A.company_name) LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.customer_name);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT A.objid::text AS "OBJID",
|
||||
A.as_no AS "CS_NO",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = A.custcd LIMIT 1), A.company_name) AS "CUSTOMER_NAME",
|
||||
COALESCE(A.product_name, (SELECT code_name FROM comm_code WHERE code_id = A.product_code LIMIT 1)) AS "PRODUCT_NAME",
|
||||
A.release_date AS "RECEIPT_DATE",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = A.rec_type LIMIT 1), '') AS "CS_TYPE_NAME",
|
||||
A.problem_contents AS "DESCRIPTION",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = A.status_cd LIMIT 1), A.status_cd) AS "STATUS_NAME",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = A.writer LIMIT 1), '') AS "CHARGER_NAME",
|
||||
A.plan_date AS "COMPLETE_DATE"
|
||||
FROM as_mng A
|
||||
WHERE ${where}
|
||||
ORDER BY A.reg_date DESC NULLS LAST, A.as_no DESC
|
||||
`,
|
||||
params
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// CS관리 현황 (asList_CS.jsp + getASDashboardList + getHeaderList 대응)
|
||||
// 제품×프로젝트 기준으로 유상/무상 건수·비용을 집계하고, CATEGORY_H 유형별 카운트를 동적 컬럼으로 추가
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// 1) 동적 컬럼 헤더 (0000975 '조치내역서유형' 하위 재귀, LEV>1)
|
||||
const parentCode = body.category_root_code || "0000975";
|
||||
const columnList = await queryRows(
|
||||
`WITH RECURSIVE COM AS (
|
||||
SELECT parent_code_id, code_id, code_name AS name, code_cd, status, id, ext_val,
|
||||
'COL_'||code_id AS col_name, 1 AS lev
|
||||
FROM comm_code
|
||||
WHERE UPPER(status)='ACTIVE' AND parent_code_id = $1
|
||||
UNION ALL
|
||||
SELECT cc.parent_code_id, cc.code_id, cc.code_name AS name, cc.code_cd, cc.status, cc.id, cc.ext_val,
|
||||
'COL_'||cc.code_id AS col_name, com.lev+1
|
||||
FROM comm_code cc JOIN COM ON COM.code_id = cc.parent_code_id
|
||||
)
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (PARTITION BY parent_code_id ORDER BY code_id) AS "GROUP_SEQ",
|
||||
(SELECT COUNT(1) FROM comm_code WHERE parent_code_id = C.parent_code_id) AS "GROUP_CNT",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = C.parent_code_id) AS "GROUP_NAME",
|
||||
C.parent_code_id AS "PARENT_CODE_ID",
|
||||
C.code_id AS "CODE_ID",
|
||||
C.name AS "NAME",
|
||||
C.col_name AS "COL_NAME"
|
||||
FROM COM C
|
||||
WHERE lev > 1
|
||||
ORDER BY C.parent_code_id, C.code_id`,
|
||||
[parentCode]
|
||||
);
|
||||
|
||||
// 2) 집계 데이터 (getASDashboardList) — 동적 컬럼 SUM 추가
|
||||
const conditions: string[] = ["CSM.status = '0000102'"]; // 결재완료
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.Year || body.year) {
|
||||
conditions.push(`TO_CHAR(TO_DATE(CSM.act_date,'YYYY-MM-DD'),'YYYY') = $${idx++}`);
|
||||
params.push(String(body.Year || body.year));
|
||||
}
|
||||
if (body.product_cd) {
|
||||
conditions.push(`CSM.product = $${idx++}`);
|
||||
params.push(body.product_cd);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`CSM.contract_objid = $${idx++}`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.project_nos && Array.isArray(body.project_nos) && body.project_nos.length > 0) {
|
||||
const ph = body.project_nos.map(() => `$${idx++}`).join(",");
|
||||
conditions.push(`CSM.contract_objid IN (${ph})`);
|
||||
params.push(...body.project_nos);
|
||||
}
|
||||
if (body.warranty) {
|
||||
conditions.push(`CSM.warranty = $${idx++}`);
|
||||
params.push(body.warranty);
|
||||
}
|
||||
if (body.cs_category) {
|
||||
conditions.push(`CSM.cs_category = $${idx++}`);
|
||||
params.push(body.cs_category);
|
||||
}
|
||||
if (body.category_h) {
|
||||
conditions.push(`CSM.category_h = $${idx++}`);
|
||||
params.push(body.category_h);
|
||||
}
|
||||
|
||||
// 동적 컬럼 SUM 생성 — COL_NAME 은 영숫자+언더스코어만이므로 직접 삽입 안전 (CODE_ID도 서버 데이터)
|
||||
const dynSum = columnList
|
||||
.map((c) => {
|
||||
const codeId = String(c.CODE_ID).replace(/[^A-Za-z0-9_-]/g, "");
|
||||
const col = String(c.COL_NAME).replace(/[^A-Za-z0-9_]/g, "");
|
||||
return `,SUM(CASE WHEN T.category_h = '${codeId}' THEN 1 ELSE 0 END) AS "${col}"`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
T.product AS "PRODUCT",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = T.product) AS "PRODUCT_NAME",
|
||||
T.project_no AS "PROJECT_NO",
|
||||
SUM(CASE WHEN T.warranty = '0000157' THEN 1 ELSE 0 END) AS "WARRANTY1",
|
||||
SUM(CASE WHEN T.warranty = '0000157' THEN T.cost ELSE 0 END) AS "COST1",
|
||||
SUM(CASE WHEN T.warranty = '0000158' THEN 1 ELSE 0 END) AS "WARRANTY2",
|
||||
SUM(CASE WHEN T.warranty = '0000158' THEN T.cost ELSE 0 END) AS "COST2"
|
||||
${dynSum}
|
||||
FROM (
|
||||
SELECT CSM.product,
|
||||
TRIM(CM.project_no) AS project_no,
|
||||
CSM.warranty,
|
||||
CSM.act_date,
|
||||
CSM.category_h,
|
||||
(COALESCE(NULLIF(CSM.total_labor_cost,''),'0')::numeric
|
||||
+ COALESCE(NULLIF(CSM.total_expenses,''),'0')::numeric) AS cost
|
||||
FROM customer_service_mgmt AS CSM
|
||||
LEFT OUTER JOIN project_mgmt AS CM ON CSM.contract_objid = CM.objid
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
) AS T
|
||||
GROUP BY T.product, T.project_no
|
||||
ORDER BY T.project_no DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
RESULTLIST: rows,
|
||||
TOTAL_CNT: rows.length,
|
||||
COLUMN_LIST: columnList,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 대시보드 (project.dashBoardMain.do + dashboard.getmainDash_* 대응)
|
||||
// 데이터 소스: project_mgmt + contract_mgmt + planning_issue + pms_wbs_task + release_mgmt
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const year = String(body.year || new Date().getFullYear());
|
||||
|
||||
try {
|
||||
// 1) 프로젝트 상태별 카운트 (원본 contractMgmt.projectCount 5상태 이식)
|
||||
// 전체 = 계획미수립 + 진행중 + 지연 + 종료 (상호 배타적)
|
||||
// 계획미수립: PLAN_START/END 자체가 없음 (act이 아니라 plan 기준!)
|
||||
const projectStats = await queryOne<Record<string, unknown>>(
|
||||
`WITH pm_base AS (
|
||||
SELECT PM.objid::text AS objid,
|
||||
-- 셋업 완료율 (100%면 종료)
|
||||
(SELECT CASE
|
||||
WHEN COUNT(CASE WHEN COALESCE(parent_objid,'') != '' THEN 1 END) = 0 THEN 0
|
||||
ELSE ROUND((COUNT(CASE WHEN COALESCE(setup_act_end,'') != '' AND COALESCE(parent_objid,'') != '' THEN 1 END)::float
|
||||
/ COUNT(CASE WHEN COALESCE(parent_objid,'') != '' THEN 1 END) * 100)::numeric)::int
|
||||
END
|
||||
FROM setup_wbs_task WHERE contract_objid = PM.objid::text) AS setup_rate,
|
||||
-- WBS 계획(PLAN_START/END) 존재 여부
|
||||
(SELECT COUNT(*) FROM pms_wbs_task WHERE contract_objid = PM.objid::text AND (
|
||||
(COALESCE(design_plan_start,'') != '' AND COALESCE(design_plan_end,'') != '')
|
||||
OR (COALESCE(purchase_plan_start,'') != '' AND COALESCE(purchase_plan_end,'') != '')
|
||||
OR (COALESCE(produce_plan_start,'') != '' AND COALESCE(produce_plan_end,'') != '')
|
||||
)) AS wbs_plan_cnt,
|
||||
-- 셋업 계획 존재 여부
|
||||
(SELECT COUNT(*) FROM setup_wbs_task WHERE contract_objid = PM.objid::text
|
||||
AND COALESCE(setup_plan_start,'') != '' AND COALESCE(setup_plan_end,'') != '') AS setup_plan_cnt,
|
||||
-- 지연 task 존재 여부
|
||||
(EXISTS (SELECT 1 FROM pms_wbs_task WHERE contract_objid = PM.objid::text AND (
|
||||
(COALESCE(design_plan_end,'') != '' AND TO_DATE(design_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(design_act_end,'') = '')
|
||||
OR (COALESCE(purchase_plan_end,'') != '' AND TO_DATE(purchase_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(purchase_act_end,'') = '')
|
||||
OR (COALESCE(produce_plan_end,'') != '' AND TO_DATE(produce_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(produce_act_end,'') = '')
|
||||
)) OR EXISTS (SELECT 1 FROM setup_wbs_task WHERE contract_objid = PM.objid::text
|
||||
AND COALESCE(setup_plan_end,'') != '' AND TO_DATE(setup_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(setup_act_end,'') = '')
|
||||
) AS has_delay
|
||||
FROM project_mgmt PM
|
||||
INNER JOIN contract_mgmt CM ON CM.objid = PM.contract_objid
|
||||
WHERE CM.contract_result = '0000964'
|
||||
-- 원본 Year 필터: 해당 연도가 WBS PLAN 범위 내에 포함되는 프로젝트
|
||||
AND $1 BETWEEN (
|
||||
SELECT LEAST(
|
||||
MIN(TO_CHAR(TO_DATE(NULLIF(purchase_plan_start,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MIN(TO_CHAR(TO_DATE(NULLIF(produce_plan_start,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MIN(TO_CHAR(TO_DATE(NULLIF(design_plan_start,''),'YYYY-MM-DD'),'YYYY')),
|
||||
(SELECT MIN(TO_CHAR(TO_DATE(NULLIF(setup_plan_start,''),'YYYY-MM-DD'),'YYYY'))
|
||||
FROM setup_wbs_task S WHERE S.contract_objid = PM.objid::text)
|
||||
)
|
||||
FROM pms_wbs_task O WHERE O.contract_objid = PM.objid::text)
|
||||
AND (
|
||||
SELECT GREATEST(
|
||||
MAX(TO_CHAR(TO_DATE(NULLIF(purchase_plan_end,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MAX(TO_CHAR(TO_DATE(NULLIF(produce_plan_end,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MAX(TO_CHAR(TO_DATE(NULLIF(design_plan_end,''),'YYYY-MM-DD'),'YYYY')),
|
||||
(SELECT MAX(TO_CHAR(TO_DATE(NULLIF(setup_plan_end,''),'YYYY-MM-DD'),'YYYY'))
|
||||
FROM setup_wbs_task S WHERE S.contract_objid = PM.objid::text)
|
||||
)
|
||||
FROM pms_wbs_task O WHERE O.contract_objid = PM.objid::text)
|
||||
),
|
||||
pm_cat AS (
|
||||
SELECT objid,
|
||||
CASE
|
||||
WHEN setup_rate >= 100 THEN '종료'
|
||||
WHEN wbs_plan_cnt = 0 AND setup_plan_cnt = 0 THEN '계획미수립'
|
||||
WHEN has_delay THEN '지연'
|
||||
ELSE '진행중'
|
||||
END AS cat
|
||||
FROM pm_base
|
||||
)
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM pm_cat)::int AS "CNT_TOTAL",
|
||||
(SELECT COUNT(*) FROM pm_cat WHERE cat = '계획미수립')::int AS "CNT_NOPLAN",
|
||||
(SELECT COUNT(*) FROM pm_cat WHERE cat = '진행중')::int AS "CNT_ING",
|
||||
(SELECT COUNT(*) FROM pm_cat WHERE cat = '지연')::int AS "CNT_DELAY",
|
||||
(SELECT COUNT(*) FROM pm_cat WHERE cat = '종료')::int AS "CNT_END",
|
||||
-- 이슈/비용 집계는 전체 기준
|
||||
COALESCE((SELECT COUNT(*) FROM planning_issue WHERE status = 'release' AND project_objid IN (SELECT objid FROM pm_cat)), 0)::int AS "ISSUE_TOTAL",
|
||||
COALESCE((SELECT COUNT(*) FROM planning_issue WHERE status = 'release' AND project_objid IN (SELECT objid FROM pm_cat)
|
||||
AND (COALESCE(design_result,'') = '' OR COALESCE(design_date,'') = '')), 0)::int AS "ISSUE_MISS"
|
||||
`,
|
||||
[year],
|
||||
) || {};
|
||||
|
||||
// 2) 제품별 수주현황 - pie chart (contractMgmt.getContractCNTByProduct 대응)
|
||||
const productDist = await queryRows(
|
||||
`SELECT
|
||||
CM.product AS "CODE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = CM.product LIMIT 1) AS "NAME",
|
||||
COUNT(*)::int AS "CNT"
|
||||
FROM contract_mgmt CM
|
||||
WHERE SUBSTR(COALESCE(CM.contract_date,''), 1, 4) = $1
|
||||
AND CM.contract_result = '0000964'
|
||||
AND CM.product IS NOT NULL AND CM.product != ''
|
||||
GROUP BY CM.product
|
||||
ORDER BY COUNT(*) DESC`,
|
||||
[year],
|
||||
);
|
||||
|
||||
// 3) 고객사별 수주현황 - pie chart (contractMgmt.getContractCNTBySupply 대응)
|
||||
const supplyDist = await queryRows(
|
||||
`SELECT
|
||||
CM.customer_objid AS "CODE",
|
||||
(SELECT supply_name FROM supply_mng WHERE objid::text = CM.customer_objid LIMIT 1) AS "NAME",
|
||||
COUNT(*)::int AS "CNT"
|
||||
FROM contract_mgmt CM
|
||||
WHERE SUBSTR(COALESCE(CM.contract_date,''), 1, 4) = $1
|
||||
AND CM.contract_result = '0000964'
|
||||
AND CM.customer_objid IS NOT NULL AND CM.customer_objid != ''
|
||||
GROUP BY CM.customer_objid
|
||||
ORDER BY COUNT(*) DESC`,
|
||||
[year],
|
||||
);
|
||||
|
||||
// 3) 월별 계약금액 (년도별 매출현황)
|
||||
const monthlyContract = await queryRows(
|
||||
`SELECT
|
||||
SUBSTR(contract_date, 6, 2)::int AS "MONTH",
|
||||
COALESCE(SUM(COALESCE(NULLIF(contract_price,'')::numeric, 0)), 0) AS "AMOUNT"
|
||||
FROM contract_mgmt
|
||||
WHERE SUBSTR(COALESCE(contract_date,''), 1, 4) = $1
|
||||
AND contract_result = '0000964'
|
||||
AND SUBSTR(COALESCE(contract_date,''), 6, 2) ~ '^[0-9]{2}$'
|
||||
GROUP BY SUBSTR(contract_date, 6, 2)
|
||||
ORDER BY SUBSTR(contract_date, 6, 2)`,
|
||||
[year],
|
||||
);
|
||||
|
||||
// 4) 주요 프로젝트 리스트 + 진척율
|
||||
const projectList = await queryRows(
|
||||
`SELECT
|
||||
PM.objid::text AS "OBJID",
|
||||
PM.project_no AS "PROJECT_NO",
|
||||
PM.project_name AS "PROJECT_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = PM.category_cd LIMIT 1) AS "CATEGORY_NAME",
|
||||
(SELECT supply_name FROM supply_mng WHERE objid::text = PM.customer_objid LIMIT 1) AS "CUSTOMER_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = PM.product LIMIT 1) AS "PRODUCT_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = PM.manufacture_plant LIMIT 1) AS "MANUFACTURE_PLANT_NAME",
|
||||
PM.contract_del_date AS "CONTRACT_DEL_DATE",
|
||||
PM.req_del_date AS "REQ_DEL_DATE",
|
||||
COALESCE(NULLIF(PM.contract_price_currency,'')::numeric, 0) AS "CONTRACT_PRICE",
|
||||
CASE WHEN (SELECT COUNT(1) FROM pms_wbs_task WHERE contract_objid = PM.objid::text) = 0 THEN 0
|
||||
ELSE ROUND(
|
||||
((SELECT COUNT(1) FROM pms_wbs_task WHERE contract_objid = PM.objid::text AND COALESCE(design_act_end,'') != '')
|
||||
+(SELECT COUNT(1) FROM pms_wbs_task WHERE contract_objid = PM.objid::text AND COALESCE(purchase_act_end,'') != '')
|
||||
+(SELECT COUNT(1) FROM pms_wbs_task WHERE contract_objid = PM.objid::text AND COALESCE(produce_act_end,'') != ''))::numeric
|
||||
/ ((SELECT COUNT(1) FROM pms_wbs_task WHERE contract_objid = PM.objid::text) * 3)::numeric * 100, 1)
|
||||
END AS "TOTAL_RATE",
|
||||
-- 이슈: 발생/조치/미결
|
||||
(SELECT COUNT(1) FROM planning_issue WHERE project_objid = PM.objid::text AND status = 'release') AS "ISSUE_CNT",
|
||||
(SELECT COUNT(1) FROM planning_issue WHERE project_objid = PM.objid::text AND status = 'release'
|
||||
AND COALESCE(design_result,'') != '' AND COALESCE(design_date,'') != '') AS "ISSUE_DONE_CNT",
|
||||
(SELECT COUNT(1) FROM planning_issue WHERE project_objid = PM.objid::text AND status = 'release')
|
||||
- (SELECT COUNT(1) FROM planning_issue WHERE project_objid = PM.objid::text AND status = 'release'
|
||||
AND COALESCE(design_result,'') != '' AND COALESCE(design_date,'') != '') AS "ISSUE_MISS_CNT",
|
||||
-- 투입원가 항목별 (원본 costMgmt.costTotaltGridList 이식, 노무비 실적은 WORK_DIARY 복잡도로 0 처리)
|
||||
COALESCE((SELECT material_cost_goal::numeric FROM input_cost_goal WHERE contract_objid::text = PM.objid::text LIMIT 1), 0) AS "MATERIAL_COST_GOAL",
|
||||
COALESCE((SELECT labor_cost_goal::numeric FROM input_cost_goal WHERE contract_objid::text = PM.objid::text LIMIT 1), 0) AS "LABOR_COST_GOAL",
|
||||
COALESCE((SELECT expense_cost_goal::numeric FROM input_cost_goal WHERE contract_objid::text = PM.objid::text LIMIT 1), 0) AS "EXPENSE_COST_GOAL",
|
||||
-- 재료비 실적 = purchase_order_master(approvalComplete) total_supply_unit_price 합
|
||||
COALESCE((SELECT SUM(COALESCE(NULLIF(total_supply_unit_price,'')::numeric, 0))
|
||||
FROM purchase_order_master
|
||||
WHERE contract_mgmt_objid = PM.objid AND status = 'approvalComplete'), 0) AS "ACCRUAL_MATERIAL_COST",
|
||||
-- 노무비 실적 (WORK_DIARY 기반 계산 복잡 — 추후 보강)
|
||||
0 AS "LABOR_COST_ACTUAL",
|
||||
-- 경비 실적 = expense_master + expense_detail SETTLE_AMOUNT 합
|
||||
COALESCE((SELECT SUM(settle)
|
||||
FROM (SELECT (SUM(COALESCE(ED.card_used,'0')::numeric) + SUM(COALESCE(ED.cash_used,'0')::numeric) - SUM(COALESCE(ED.payment,'0')::numeric)) AS settle
|
||||
FROM expense_master EM
|
||||
LEFT OUTER JOIN expense_detail ED ON ED.expense_master_objid = EM.expense_master_objid
|
||||
WHERE EM.project_mgmt_objid = PM.objid::text
|
||||
GROUP BY EM.expense_master_objid) s), 0) AS "ACCRUAL_EXPENSE",
|
||||
RM.release_date AS "RELEASE_DATE",
|
||||
-- 셋업 완료일 (setup_wbs_task 중 마지막 act_end)
|
||||
(SELECT MAX(setup_act_end) FROM setup_wbs_task
|
||||
WHERE contract_objid = PM.objid::text AND COALESCE(parent_objid,'') != ''
|
||||
AND COALESCE(setup_act_end,'') != '') AS "SETUP_DONE_DATE",
|
||||
-- 셋업 진척율
|
||||
COALESCE((SELECT CASE
|
||||
WHEN COUNT(CASE WHEN COALESCE(parent_objid,'') != '' THEN 1 END) = 0 THEN 0
|
||||
ELSE ROUND((COUNT(CASE WHEN COALESCE(setup_act_end,'') != '' AND COALESCE(parent_objid,'') != '' THEN 1 END)::float
|
||||
/ COUNT(CASE WHEN COALESCE(parent_objid,'') != '' THEN 1 END) * 100)::numeric, 1) END
|
||||
FROM setup_wbs_task WHERE contract_objid = PM.objid::text), 0) AS "SETUP_RATE",
|
||||
-- 5상태 분류 (원본 projectCount 로직, 계획미수립 = PLAN_START/END 없음)
|
||||
CASE
|
||||
WHEN COALESCE((SELECT CASE
|
||||
WHEN COUNT(CASE WHEN COALESCE(parent_objid,'') != '' THEN 1 END) = 0 THEN 0
|
||||
ELSE ROUND((COUNT(CASE WHEN COALESCE(setup_act_end,'') != '' AND COALESCE(parent_objid,'') != '' THEN 1 END)::float
|
||||
/ COUNT(CASE WHEN COALESCE(parent_objid,'') != '' THEN 1 END) * 100)::numeric)::int END
|
||||
FROM setup_wbs_task WHERE contract_objid = PM.objid::text), 0) >= 100 THEN '종료'
|
||||
WHEN (SELECT COUNT(*) FROM pms_wbs_task WHERE contract_objid = PM.objid::text AND (
|
||||
(COALESCE(design_plan_start,'') != '' AND COALESCE(design_plan_end,'') != '')
|
||||
OR (COALESCE(purchase_plan_start,'') != '' AND COALESCE(purchase_plan_end,'') != '')
|
||||
OR (COALESCE(produce_plan_start,'') != '' AND COALESCE(produce_plan_end,'') != '')
|
||||
)) = 0
|
||||
AND (SELECT COUNT(*) FROM setup_wbs_task WHERE contract_objid = PM.objid::text
|
||||
AND COALESCE(setup_plan_start,'') != '' AND COALESCE(setup_plan_end,'') != '') = 0 THEN '계획미수립'
|
||||
WHEN (EXISTS (SELECT 1 FROM pms_wbs_task WHERE contract_objid = PM.objid::text AND (
|
||||
(COALESCE(design_plan_end,'') != '' AND TO_DATE(design_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(design_act_end,'') = '')
|
||||
OR (COALESCE(purchase_plan_end,'') != '' AND TO_DATE(purchase_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(purchase_act_end,'') = '')
|
||||
OR (COALESCE(produce_plan_end,'') != '' AND TO_DATE(produce_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(produce_act_end,'') = '')
|
||||
)) OR EXISTS (SELECT 1 FROM setup_wbs_task WHERE contract_objid = PM.objid::text
|
||||
AND COALESCE(setup_plan_end,'') != '' AND TO_DATE(setup_plan_end,'YYYY-MM-DD') < CURRENT_DATE AND COALESCE(setup_act_end,'') = ''))
|
||||
THEN '지연'
|
||||
ELSE '진행중'
|
||||
END AS "STATUS_TITLE"
|
||||
FROM project_mgmt PM
|
||||
INNER JOIN contract_mgmt CM ON CM.objid = PM.contract_objid
|
||||
LEFT OUTER JOIN release_mgmt RM ON RM.parent_objid = PM.objid::text
|
||||
WHERE CM.contract_result = '0000964'
|
||||
AND $1 BETWEEN (
|
||||
SELECT LEAST(
|
||||
MIN(TO_CHAR(TO_DATE(NULLIF(purchase_plan_start,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MIN(TO_CHAR(TO_DATE(NULLIF(produce_plan_start,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MIN(TO_CHAR(TO_DATE(NULLIF(design_plan_start,''),'YYYY-MM-DD'),'YYYY')),
|
||||
(SELECT MIN(TO_CHAR(TO_DATE(NULLIF(setup_plan_start,''),'YYYY-MM-DD'),'YYYY'))
|
||||
FROM setup_wbs_task S WHERE S.contract_objid = PM.objid::text)
|
||||
)
|
||||
FROM pms_wbs_task O WHERE O.contract_objid = PM.objid::text)
|
||||
AND (
|
||||
SELECT GREATEST(
|
||||
MAX(TO_CHAR(TO_DATE(NULLIF(purchase_plan_end,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MAX(TO_CHAR(TO_DATE(NULLIF(produce_plan_end,''),'YYYY-MM-DD'),'YYYY')),
|
||||
MAX(TO_CHAR(TO_DATE(NULLIF(design_plan_end,''),'YYYY-MM-DD'),'YYYY')),
|
||||
(SELECT MAX(TO_CHAR(TO_DATE(NULLIF(setup_plan_end,''),'YYYY-MM-DD'),'YYYY'))
|
||||
FROM setup_wbs_task S WHERE S.contract_objid = PM.objid::text)
|
||||
)
|
||||
FROM pms_wbs_task O WHERE O.contract_objid = PM.objid::text)
|
||||
ORDER BY PM.regdate DESC
|
||||
`,
|
||||
[year],
|
||||
);
|
||||
|
||||
// 5) 3년치 영업현황 (getYearGoalInfo 이식): 수주건수 국내/해외/비율, 계약금액(억원), 영업목표, 달성율
|
||||
const years = [Number(year), Number(year) - 1, Number(year) - 2].map(String);
|
||||
const yearGoalInfo = await Promise.all(
|
||||
years.map(async (y) => {
|
||||
const row = await queryOne<Record<string, unknown>>(
|
||||
`WITH W_CM AS (
|
||||
SELECT area_cd,
|
||||
CASE WHEN COALESCE(NULLIF(contract_price,''),'0') != '0' THEN contract_price
|
||||
WHEN contract_currency = '0001566' AND COALESCE(NULLIF(contract_price_currency,''),'0') != '0' THEN contract_price_currency
|
||||
ELSE COALESCE(NULLIF(contract_price,''),'0')
|
||||
END AS price
|
||||
FROM contract_mgmt
|
||||
WHERE contract_result = '0000964'
|
||||
AND TO_CHAR(TO_DATE(contract_date, 'YYYY-MM-DD'), 'YYYY') = $1
|
||||
)
|
||||
SELECT
|
||||
$1 AS "YEAR",
|
||||
(SELECT COUNT(*) FROM contract_mgmt WHERE TO_CHAR(TO_DATE(contract_date,'YYYY-MM-DD'),'YYYY') = $1) AS "CONTRACT_CNT_YEAR_ALL",
|
||||
(SELECT COUNT(*) FROM W_CM)::int AS "CONTRACT_CNT_YEAR",
|
||||
(SELECT COUNT(*) FROM W_CM WHERE area_cd = '0001220')::int AS "CONTRACT_CNT_YEAR_IN",
|
||||
(SELECT COUNT(*) FROM W_CM WHERE area_cd = '0001221')::int AS "CONTRACT_CNT_YEAR_OUT",
|
||||
COALESCE((SELECT (SUM(COALESCE(NULLIF(price,'')::numeric, 0))) / 100000000 FROM W_CM)::numeric(18,1), 0) AS "CONTRACT_COST_YEAR",
|
||||
COALESCE((SELECT price::numeric FROM pms_pjt_year_goal WHERE year = $1 LIMIT 1), 0) AS "PRICE",
|
||||
(SELECT objid::text FROM pms_pjt_year_goal WHERE year = $1 LIMIT 1) AS "YEAR_GOAL_OBJID"`,
|
||||
[y],
|
||||
);
|
||||
if (!row) return { YEAR: y };
|
||||
const total = Number(row.CONTRACT_CNT_YEAR_ALL || 0);
|
||||
const ok = Number(row.CONTRACT_CNT_YEAR || 0);
|
||||
const cost = Number(row.CONTRACT_COST_YEAR || 0);
|
||||
const goal = Number(row.PRICE || 0);
|
||||
return {
|
||||
...row,
|
||||
CONTRACT_CNT_YEAR_RATE: total > 0 ? Math.round((ok / total) * 1000) / 10 : 0,
|
||||
GOAL_RATE: goal > 0 ? Math.round((cost / goal) * 1000) / 10 : 0,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
projectStats: projectStats || {},
|
||||
productDist,
|
||||
supplyDist,
|
||||
monthlyContract,
|
||||
projectList,
|
||||
yearGoalInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Dashboard error:", error);
|
||||
return NextResponse.json({
|
||||
projectStats: {},
|
||||
productDist: [],
|
||||
supplyDist: [],
|
||||
monthlyContract: [],
|
||||
projectList: [],
|
||||
yearGoalInfo: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne, queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 입고결과등록 팝업 상세 조회
|
||||
// 원본:
|
||||
// - purchaseOrderService.getPurchaseOrderMasterInfo → info
|
||||
// - purchaseOrder.getPurchaseOrderDeliveryTargetPartList → partList (발주파트+기입고/미입고)
|
||||
// - supplyChainMgmt.arrivalResultList → arrivalList (차수별 arrival_plan)
|
||||
// - purchaseOrder.selectPurchaseOrderMasterList → multiMasterList (동시발주 슬레이브)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId가 필요합니다." });
|
||||
|
||||
// 1. 발주 마스터 정보 (readonly 표시용)
|
||||
const info = await queryOne(
|
||||
`SELECT POM.objid::text AS "OBJID",
|
||||
POM.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
POM.purchase_order_no_org AS "PURCHASE_ORDER_NO_ORG",
|
||||
POM.title AS "TITLE",
|
||||
POM.delivery_date AS "DELIVERY_DATE",
|
||||
TO_CHAR(POM.regdate, 'YYYY-MM-DD') AS "REGDATE",
|
||||
POM.type AS "TYPE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.type LIMIT 1) AS "TYPE_NAME",
|
||||
POM.order_type_cd AS "ORDER_TYPE_CD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.order_type_cd LIMIT 1) AS "ORDER_TYPE_CD_NAME",
|
||||
POM.delivery_place AS "DELIVERY_PLACE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.delivery_place LIMIT 1) AS "DELIVERY_PLACE_NAME",
|
||||
POM.inspect_method AS "INSPECT_METHOD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.inspect_method LIMIT 1) AS "INSPECT_METHOD_NAME",
|
||||
POM.payment_terms AS "PAYMENT_TERMS",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.payment_terms LIMIT 1) AS "PAYMENT_TERMS_NAME",
|
||||
POM.total_price_txt AS "TOTAL_PRICE_TXT",
|
||||
POM.remark AS "REMARK",
|
||||
POM.status AS "STATUS",
|
||||
COALESCE(POM.multi_yn, '') AS "MULTI_YN",
|
||||
COALESCE(POM.multi_master_yn, '') AS "MULTI_MASTER_YN",
|
||||
POM.multi_master_objid AS "MULTI_MASTER_OBJID",
|
||||
POM.bom_report_objid AS "BOM_REPORT_OBJID",
|
||||
POM.contract_mgmt_objid AS "CONTRACT_MGMT_OBJID",
|
||||
COALESCE(CM.project_no, CM.contract_no) AS "PROJECT_NO",
|
||||
CM.customer_project_name AS "CUSTOMER_PROJECT_NAME",
|
||||
POM.unit_code AS "UNIT_CODE",
|
||||
(SELECT COALESCE(O.unit_no,'') || '-' || COALESCE(O.task_name,'') FROM pms_wbs_task O WHERE O.objid = POM.unit_code LIMIT 1) AS "UNIT_NAME",
|
||||
POM.partner_objid AS "PARTNER_OBJID",
|
||||
(SELECT supply_name FROM admin_supply_mng WHERE objid::text = POM.partner_objid LIMIT 1) AS "PARTNER_NAME",
|
||||
(SELECT bus_reg_no FROM admin_supply_mng WHERE objid::text = POM.partner_objid LIMIT 1) AS "SUPPLY_BUS_NO",
|
||||
(SELECT supply_address FROM admin_supply_mng WHERE objid::text = POM.partner_objid LIMIT 1) AS "SUPPLY_ADDR",
|
||||
POM.supply_user_name AS "SUPPLY_USER_NAME",
|
||||
POM.supply_user_hp AS "SUPPLY_USER_HP",
|
||||
POM.supply_user_tel AS "SUPPLY_USER_TEL",
|
||||
POM.supply_user_fax AS "SUPPLY_USER_FAX",
|
||||
POM.supply_user_email AS "SUPPLY_USER_EMAIL",
|
||||
POM.sales_mng_user_id AS "SALES_MNG_USER_ID",
|
||||
(SELECT user_name FROM user_info WHERE user_id = POM.sales_mng_user_id LIMIT 1) AS "SALES_MNG_USER",
|
||||
(SELECT cell_phone FROM user_info WHERE user_id = POM.sales_mng_user_id LIMIT 1) AS "SALES_MNG_USER_CELL_PHONE"
|
||||
FROM purchase_order_master POM
|
||||
LEFT JOIN project_mgmt CM ON POM.contract_mgmt_objid = CM.objid
|
||||
WHERE POM.objid::text = $1`,
|
||||
[String(objId)],
|
||||
);
|
||||
if (!info) {
|
||||
return NextResponse.json({ success: false, message: "발주서를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
// 2. 파트 목록 + 기입고/미입고 수량
|
||||
const partList = await queryRows(
|
||||
`SELECT
|
||||
POP.objid::text AS "ORDER_PART_OBJID",
|
||||
POP.part_objid AS "PART_OBJID",
|
||||
POP.ld_part_objid AS "LD_PART_OBJID",
|
||||
CASE WHEN POM.type IN ('0001070','0001069') THEN COALESCE(PM.part_no, POP.part_no) ELSE POP.part_no END AS "PART_NO",
|
||||
POP.part_name AS "PART_NAME",
|
||||
COALESCE(PM.material, '') AS "MATERIAL",
|
||||
POP.spec AS "SPEC",
|
||||
POP.maker AS "MAKER",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POP.unit LIMIT 1) AS "UNIT_TITLE",
|
||||
POP.order_qty AS "ORDER_QTY",
|
||||
POP.real_order_qty AS "REAL_ORDER_QTY",
|
||||
POM.delivery_date AS "POM_DELIVERY_DATE",
|
||||
COALESCE(AP.total_delivery_qty, '0') AS "TOTAL_DELIVERY_QTY",
|
||||
(COALESCE(POP.real_order_qty,'0')::numeric - COALESCE(AP.total_delivery_qty, '0')::numeric) AS "NON_ARRIVAL_QTY"
|
||||
FROM purchase_order_master POM
|
||||
INNER JOIN purchase_order_part POP ON POM.objid::text = POP.purchase_order_master_objid
|
||||
LEFT JOIN part_mng PM ON POP.part_objid = PM.objid::text
|
||||
LEFT JOIN (
|
||||
SELECT order_part_objid,
|
||||
SUM(COALESCE(receipt_qty,'0')::numeric)::text AS total_delivery_qty
|
||||
FROM arrival_plan
|
||||
WHERE parent_objid = $1
|
||||
GROUP BY order_part_objid
|
||||
) AP ON AP.order_part_objid = POP.objid::text
|
||||
WHERE POM.objid::text = $1
|
||||
ORDER BY PM.part_no NULLS LAST, POP.regdate DESC`,
|
||||
[String(objId)],
|
||||
);
|
||||
|
||||
// 3. 기존 차수별 입고계획 (arrival_plan)
|
||||
const arrivalList = await queryRows(
|
||||
`SELECT
|
||||
AP.objid::text AS "OBJID",
|
||||
AP.part_objid AS "PART_OBJID",
|
||||
AP.order_part_objid AS "ORDER_PART_OBJID",
|
||||
AP.arrival_qty AS "ARRIVAL_QTY",
|
||||
AP.arrival_plan_date AS "ARRIVAL_PLAN_DATE",
|
||||
AP.group_seq AS "GROUP_SEQ",
|
||||
AP.seq AS "SEQ",
|
||||
AP.receipt_qty AS "RECEIPT_QTY",
|
||||
AP.receipt_date AS "RECEIPT_DATE",
|
||||
AP.location AS "LOCATION",
|
||||
AP.sub_location AS "SUB_LOCATION",
|
||||
AP.error_qty AS "ERROR_QTY",
|
||||
AP.error_reason AS "ERROR_REASON",
|
||||
AP.attribution AS "ATTRIBUTION",
|
||||
AP.inventory_status AS "INVENTORY_STATUS",
|
||||
IM.objid::text AS "INVOICE_OBJID"
|
||||
FROM arrival_plan AP
|
||||
LEFT JOIN invoice_mgmt IM ON IM.parent_objid::text = AP.parent_objid AND IM.group_seq = AP.group_seq
|
||||
WHERE AP.parent_objid = $1
|
||||
ORDER BY NULLIF(AP.seq,'')::numeric NULLS LAST, AP.objid`,
|
||||
[String(objId)],
|
||||
);
|
||||
|
||||
// 4. 동시발주 슬레이브 리스트 (현재 건이 마스터인 경우)
|
||||
let multiMasterList: Record<string, unknown>[] = [];
|
||||
if (String(info.MULTI_MASTER_YN) === "Y") {
|
||||
multiMasterList = await queryRows(
|
||||
`SELECT POM.objid::text AS "OBJID",
|
||||
POM.contract_mgmt_objid::text AS "CONTRACT_MGMT_OBJID",
|
||||
POM.unit_code AS "UNIT_CODE",
|
||||
(SELECT project_no FROM project_mgmt CNT WHERE CNT.objid = POM.contract_mgmt_objid LIMIT 1) AS "CONTRACT_NO",
|
||||
POM.delivery_date AS "DELIVERY_PLAN_DATE",
|
||||
(SELECT SUM(COALESCE(real_order_qty,'0')::numeric)::text FROM purchase_order_part WHERE purchase_order_master_objid = POM.objid::text) AS "DELIVERY_PLAN_QTY"
|
||||
FROM purchase_order_master POM
|
||||
WHERE POM.multi_master_objid = $1
|
||||
ORDER BY "CONTRACT_NO" DESC`,
|
||||
[String(objId)],
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
info,
|
||||
partList,
|
||||
arrivalList,
|
||||
multiMasterList,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: purchaseOrder.xml#deliveryMngList_new
|
||||
// 입고관리 > 입고결과등록 목록
|
||||
// - 결재완료 발주서만 (status = 'approvalComplete')
|
||||
// - 동시발주 마스터 + 일반 건만 (슬레이브 제외)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const conditions: string[] = [
|
||||
"POM.status = 'approvalComplete'",
|
||||
"(COALESCE(POM.multi_master_yn,'') = 'Y' OR (COALESCE(POM.multi_master_yn,'') != 'Y' AND COALESCE(POM.multi_yn,'') != 'Y'))",
|
||||
];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
// Year: 최종입고일(MAX receipt_date) 기준 — 원본과 동일
|
||||
if (body.year) {
|
||||
conditions.push(
|
||||
`TO_CHAR(TO_DATE(S1.cur_delivery_date, 'YYYY-MM-DD'), 'YYYY') = $${idx++}`,
|
||||
);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.customer_cd) {
|
||||
conditions.push(`CM.customer_objid::text = $${idx++}`);
|
||||
params.push(body.customer_cd);
|
||||
}
|
||||
if (body.customer_project_name) {
|
||||
conditions.push(
|
||||
`TRIM(UPPER(CM.customer_project_name)) LIKE '%' || TRIM(UPPER($${idx++})) || '%'`,
|
||||
);
|
||||
params.push(body.customer_project_name);
|
||||
}
|
||||
// project_nos: 복수 objid (array 또는 CSV 문자열)
|
||||
const projectNos: string[] = Array.isArray(body.project_nos)
|
||||
? body.project_nos.filter(Boolean).map(String)
|
||||
: body.project_nos
|
||||
? String(body.project_nos).split(",").filter(Boolean)
|
||||
: [];
|
||||
if (projectNos.length > 0) {
|
||||
const placeholders = projectNos.map(() => `$${idx++}`).join(", ");
|
||||
conditions.push(`CM.objid::text IN (${placeholders})`);
|
||||
params.push(...projectNos);
|
||||
}
|
||||
if (body.unit_code) {
|
||||
conditions.push(`POM.unit_code = $${idx++}`);
|
||||
params.push(body.unit_code);
|
||||
}
|
||||
if (body.po_client_id) {
|
||||
conditions.push(`POM.po_client_id = $${idx++}`);
|
||||
params.push(body.po_client_id);
|
||||
}
|
||||
if (body.purchase_order_no) {
|
||||
conditions.push(
|
||||
`TRIM(UPPER(POM.purchase_order_no)) LIKE '%' || TRIM(UPPER($${idx++})) || '%'`,
|
||||
);
|
||||
params.push(body.purchase_order_no);
|
||||
}
|
||||
if (body.type) {
|
||||
conditions.push(`POM.type = $${idx++}`);
|
||||
params.push(body.type);
|
||||
}
|
||||
if (body.delivery_start_date) {
|
||||
conditions.push(
|
||||
`TO_DATE(POM.delivery_date, 'YYYY-MM-DD') >= TO_DATE($${idx++}, 'YYYY-MM-DD')`,
|
||||
);
|
||||
params.push(body.delivery_start_date);
|
||||
}
|
||||
if (body.delivery_end_date) {
|
||||
conditions.push(
|
||||
`TO_DATE(POM.delivery_date, 'YYYY-MM-DD') <= TO_DATE($${idx++}, 'YYYY-MM-DD')`,
|
||||
);
|
||||
params.push(body.delivery_end_date);
|
||||
}
|
||||
if (body.partner_objid) {
|
||||
conditions.push(`POM.partner_objid = $${idx++}`);
|
||||
params.push(body.partner_objid);
|
||||
}
|
||||
// sales_mng_user_ids: 복수 user_id
|
||||
const userIds: string[] = Array.isArray(body.sales_mng_user_ids)
|
||||
? body.sales_mng_user_ids.filter(Boolean).map(String)
|
||||
: body.sales_mng_user_ids
|
||||
? String(body.sales_mng_user_ids).split(",").filter(Boolean)
|
||||
: [];
|
||||
if (userIds.length > 0) {
|
||||
const placeholders = userIds.map(() => `$${idx++}`).join(", ");
|
||||
conditions.push(`POM.sales_mng_user_id IN (${placeholders})`);
|
||||
params.push(...userIds);
|
||||
}
|
||||
if (body.reg_start_date) {
|
||||
conditions.push(`TO_CHAR(POM.regdate, 'YYYY-MM-DD') >= $${idx++}`);
|
||||
params.push(body.reg_start_date);
|
||||
}
|
||||
if (body.reg_end_date) {
|
||||
conditions.push(`TO_CHAR(POM.regdate, 'YYYY-MM-DD') <= $${idx++}`);
|
||||
params.push(body.reg_end_date);
|
||||
}
|
||||
if (body.delivery_status) {
|
||||
conditions.push(
|
||||
`(CASE WHEN 0 >= (COALESCE((SELECT SUM(COALESCE(O.real_order_qty,'0')::numeric) FROM purchase_order_part O WHERE POM.objid::text = O.purchase_order_master_objid), 0) - COALESCE(S1.total_delivery_qty, 0)) THEN '입고완료'
|
||||
WHEN TO_CHAR(NOW(), 'YYYY-MM-DD') > POM.delivery_date THEN '지연'
|
||||
ELSE '입고중' END) = $${idx++}`,
|
||||
);
|
||||
params.push(body.delivery_status);
|
||||
}
|
||||
if (body.SEARCH_PART_NO) {
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM purchase_order_part POP WHERE POP.purchase_order_master_objid = POM.objid::text
|
||||
AND TRIM(UPPER(COALESCE(POP.part_no,''))) LIKE '%' || TRIM(UPPER($${idx++})) || '%')`,
|
||||
);
|
||||
params.push(body.SEARCH_PART_NO);
|
||||
}
|
||||
if (body.SEARCH_PART_NAME) {
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM purchase_order_part POP WHERE POP.purchase_order_master_objid = POM.objid::text
|
||||
AND TRIM(UPPER(COALESCE(POP.part_name,''))) LIKE '%' || TRIM(UPPER($${idx++})) || '%')`,
|
||||
);
|
||||
params.push(body.SEARCH_PART_NAME);
|
||||
}
|
||||
if (body.SEARCH_PART_SPEC) {
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM purchase_order_part POP WHERE POP.purchase_order_master_objid = POM.objid::text
|
||||
AND TRIM(UPPER(COALESCE(POP.spec,''))) LIKE '%' || TRIM(UPPER($${idx++})) || '%')`,
|
||||
);
|
||||
params.push(body.SEARCH_PART_SPEC);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT POM.objid::text AS "OBJID",
|
||||
TO_CHAR(POM.regdate, 'YYYY') AS "POM_YEAR",
|
||||
(SELECT supply_name FROM supply_mng SM WHERE SM.objid::text = CM.customer_objid LIMIT 1) AS "CUSTOMER_NAME",
|
||||
CM.customer_project_name AS "CUSTOMER_PROJECT_NAME",
|
||||
(SELECT COALESCE(O.unit_no,'') || '-' || COALESCE(O.task_name,'') FROM pms_wbs_task O WHERE O.objid = POM.unit_code LIMIT 1) AS "UNIT_NAME",
|
||||
COALESCE(CM.project_no, CM.contract_no) AS "PROJECT_NO",
|
||||
POM.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
POM.title AS "TITLE",
|
||||
POM.delivery_place AS "DELIVERY_PLACE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.delivery_place LIMIT 1) AS "DELIVERY_PLACE_NAME",
|
||||
POM.inspect_method AS "INSPECT_METHOD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.inspect_method LIMIT 1) AS "INSPECT_METHOD_NAME",
|
||||
POM.payment_terms AS "PAYMENT_TERMS",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.payment_terms LIMIT 1) AS "PAYMENT_TERMS_NAME",
|
||||
POM.delivery_date AS "DELIVERY_DATE",
|
||||
POM.partner_objid AS "PARTNER_OBJID",
|
||||
(SELECT supply_name FROM admin_supply_mng WHERE objid::text = POM.partner_objid LIMIT 1) AS "PARTNER_NAME",
|
||||
POM.sales_mng_user_id AS "SALES_MNG_USER_ID",
|
||||
(SELECT user_name FROM user_info WHERE user_id = POM.sales_mng_user_id LIMIT 1) AS "SALES_MNG_USER_NAME",
|
||||
TO_CHAR(POM.regdate, 'YYYY-MM-DD') AS "REGDATE",
|
||||
POM.total_price AS "TOTAL_PRICE",
|
||||
POM.discount_price AS "DISCOUNT_PRICE",
|
||||
POM.total_supply_unit_price AS "TOTAL_SUPPLY_UNIT_PRICE",
|
||||
POM.nego_rate AS "NEGO_RATE",
|
||||
COALESCE(POM.multi_master_yn,'') AS "MULTI_MASTER_YN",
|
||||
COALESCE(POM.multi_yn,'') AS "MULTI_YN",
|
||||
CASE WHEN COALESCE(POM.multi_master_yn,'') = 'Y' THEN '' ELSE COALESCE(POM.multi_yn,'') END AS "MULTI_YN_MAKED",
|
||||
COALESCE((SELECT SUM(COALESCE(O.real_order_qty,'0')::numeric) FROM purchase_order_part O WHERE POM.objid::text = O.purchase_order_master_objid), 0) AS "TOTAL_PO_QTY",
|
||||
S1.cur_delivery_date AS "CUR_DELIVERY_DATE",
|
||||
COALESCE(S1.total_delivery_qty, 0) AS "TOTAL_DELIVERY_QTY",
|
||||
(COALESCE((SELECT SUM(COALESCE(O.real_order_qty,'0')::numeric) FROM purchase_order_part O WHERE POM.objid::text = O.purchase_order_master_objid), 0)
|
||||
- COALESCE(S1.total_delivery_qty, 0)) AS "NON_DELIVERY_QTY",
|
||||
(CASE
|
||||
WHEN 0 >= (COALESCE((SELECT SUM(COALESCE(O.real_order_qty,'0')::numeric) FROM purchase_order_part O WHERE POM.objid::text = O.purchase_order_master_objid), 0) - COALESCE(S1.total_delivery_qty, 0))
|
||||
THEN '입고완료'
|
||||
WHEN TO_CHAR(NOW(), 'YYYY-MM-DD') > POM.delivery_date THEN '지연'
|
||||
ELSE '입고중'
|
||||
END) AS "DELIVERY_STATUS",
|
||||
POM.type AS "TYPE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.type LIMIT 1) AS "TYPE_NAME",
|
||||
POM.order_type_cd AS "ORDER_TYPE_CD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.order_type_cd LIMIT 1) AS "ORDER_TYPE_CD_NAME",
|
||||
(SELECT UI.user_name FROM arrival_plan AP JOIN user_info UI ON UI.user_id = AP.receiver_id
|
||||
WHERE AP.parent_objid = POM.objid::text AND AP.receipt_date IS NOT NULL AND AP.receipt_date <> ''
|
||||
ORDER BY AP.receipt_date DESC LIMIT 1) AS "CUR_RECEIVER_NAME"
|
||||
FROM purchase_order_master POM
|
||||
LEFT OUTER JOIN (
|
||||
SELECT POP.purchase_order_master_objid,
|
||||
MAX(DH.receipt_date) AS cur_delivery_date,
|
||||
SUM(COALESCE(DH.receipt_qty,'0')::numeric) AS total_delivery_qty
|
||||
FROM purchase_order_part POP
|
||||
LEFT OUTER JOIN arrival_plan DH ON POP.objid::text = DH.order_part_objid
|
||||
GROUP BY POP.purchase_order_master_objid
|
||||
) S1 ON POM.objid::text = S1.purchase_order_master_objid
|
||||
LEFT OUTER JOIN project_mgmt CM ON POM.contract_mgmt_objid = CM.objid
|
||||
WHERE ${where}
|
||||
ORDER BY POM.regdate DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
} catch (e) {
|
||||
console.error("delivery/acceptance:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "조회 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import type { PoolClient } from "pg";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 입고결과 저장 (원본: purchaseOrder/saveDeliveryInfo.do → supplyChainMgmt.saveDeliveryInfo)
|
||||
// 처리 순서:
|
||||
// 1) 파트×차수 행마다 ARRIVAL_PLAN UPSERT
|
||||
// 2) RECEIPT_QTY > RECEIPT_INV_QTY(기입고) 인 경우에만 inventory 반영
|
||||
// - ARRIVAL_PLAN.INVENTORY_STATUS='Y' 로 마킹
|
||||
// - INVENTORY_MGMT(contract_objid, unit, part_objid) UPSERT
|
||||
// - 단일발주: INVENTORY_MGMT_IN 1행
|
||||
// - 동시발주(MULTI_YN='Y'): 마스터부터 슬레이브 순으로 프로젝트별 ORDER_QTY 만큼 분배
|
||||
//
|
||||
// Request body 형식:
|
||||
// {
|
||||
// ORDER_OBJID / PARENT_OBJID : 발주 마스터 objId (필수)
|
||||
// TYPE : POM.type
|
||||
// MULTI_YN : 'Y' | 'N'
|
||||
// CONTRACT_MGMT_OBJID : 프로젝트 objId
|
||||
// UNIT_CODE : 유닛
|
||||
// // 파트 정보 (DETAIL_GROUP 번호로 그룹핑)
|
||||
// parts: [{ ORDER_PART_OBJID, PART_OBJID, LD_PART_OBJID, ORDER_QTY, REAL_ORDER_QTY }]
|
||||
// // 차수별 입고 행들 (파트 수 × 차수 수)
|
||||
// items: [{
|
||||
// OBJID?, GROUP_SEQ, SEQ, DETAIL_GROUP, // DETAIL_GROUP = 파트 index (1-based)
|
||||
// ARRIVAL_QTY, ARRIVAL_PLAN_DATE,
|
||||
// RECEIPT_QTY, RECEIPT_INV_QTY, RECEIPT_DATE,
|
||||
// LOCATION, SUB_LOCATION,
|
||||
// INVENTORY_STATUS,
|
||||
// }]
|
||||
// }
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// 구(舊) 호환 (단건 입고): body.items 없을 때 body.objId 단건 처리
|
||||
const legacyItems: Record<string, unknown>[] = Array.isArray(body.items) ? body.items : [];
|
||||
if (legacyItems.length === 0 && !body.parts && body.objId) {
|
||||
return legacySingleSave(body, user.userId);
|
||||
}
|
||||
|
||||
const orderObjId: string = String(body.ORDER_OBJID ?? body.PARENT_OBJID ?? body.objId ?? "");
|
||||
const parentObjId: string = String(body.PARENT_OBJID ?? body.ORDER_OBJID ?? orderObjId);
|
||||
const contractMgmtObjId: string = String(body.CONTRACT_MGMT_OBJID ?? "");
|
||||
const unitCode: string = String(body.UNIT_CODE ?? "");
|
||||
const multiYn: string = String(body.MULTI_YN ?? "");
|
||||
const parts: Record<string, unknown>[] = Array.isArray(body.parts) ? body.parts : [];
|
||||
const items: Record<string, unknown>[] = Array.isArray(body.items) ? body.items : [];
|
||||
|
||||
if (!orderObjId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "발주 objId가 없습니다." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (items.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "저장할 입고 데이터가 없습니다." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const partByIdx = new Map<number, Record<string, unknown>>();
|
||||
parts.forEach((p, i) => partByIdx.set(i + 1, p));
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
for (const item of items) {
|
||||
const arrivalObjId = String(item.OBJID ?? "") || createObjectId();
|
||||
const detailGroup = Number(item.DETAIL_GROUP ?? 0) || 1;
|
||||
const part = partByIdx.get(detailGroup);
|
||||
const orderPartObjId = String(part?.ORDER_PART_OBJID ?? "");
|
||||
const partObjId = String(part?.PART_OBJID ?? "");
|
||||
const ldPartObjId = String(part?.LD_PART_OBJID ?? "");
|
||||
const realOrderQty = Number(part?.REAL_ORDER_QTY ?? 0) || 0;
|
||||
const orderQty = Number(part?.ORDER_QTY ?? realOrderQty) || realOrderQty;
|
||||
|
||||
const receiptQty = Number(item.RECEIPT_QTY ?? 0) || 0;
|
||||
const receiptInvQty = Number(item.RECEIPT_INV_QTY ?? 0) || 0;
|
||||
const arrivalQty = Number(item.ARRIVAL_QTY ?? 0) || 0;
|
||||
const receiptDate = String(item.RECEIPT_DATE ?? "");
|
||||
const arrivalPlanDate = String(item.ARRIVAL_PLAN_DATE ?? "");
|
||||
const location = String(item.LOCATION ?? "");
|
||||
const subLocation = String(item.SUB_LOCATION ?? "");
|
||||
const groupSeq = String(item.GROUP_SEQ ?? "1");
|
||||
const seq = String(item.SEQ ?? "1");
|
||||
|
||||
// 1) ARRIVAL_PLAN UPSERT
|
||||
await client.query(
|
||||
`INSERT INTO arrival_plan (
|
||||
objid, parent_objid, order_part_objid, part_objid,
|
||||
receipt_qty, receipt_date, location, sub_location,
|
||||
writer, receiver_id, group_seq, seq, arrival_qty, arrival_plan_date
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$9,$10,$11,$12,$13)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
receipt_qty = EXCLUDED.receipt_qty,
|
||||
receipt_date = EXCLUDED.receipt_date,
|
||||
location = EXCLUDED.location,
|
||||
sub_location = EXCLUDED.sub_location,
|
||||
arrival_qty = EXCLUDED.arrival_qty,
|
||||
arrival_plan_date = EXCLUDED.arrival_plan_date,
|
||||
receiver_id = EXCLUDED.receiver_id`,
|
||||
[
|
||||
arrivalObjId, parentObjId, orderPartObjId, partObjId,
|
||||
String(receiptQty), receiptDate, location, subLocation,
|
||||
user.userId, groupSeq, seq, String(arrivalQty), arrivalPlanDate,
|
||||
],
|
||||
);
|
||||
|
||||
// 2) 실제 신규 입고가 없으면(receiptQty=0 또는 기입고와 동일) 재고 반영 skip
|
||||
const deltaQty = receiptQty - receiptInvQty;
|
||||
if (receiptQty <= 0 || deltaQty <= 0) continue;
|
||||
|
||||
// INVENTORY_STATUS='Y'
|
||||
await client.query(
|
||||
`UPDATE arrival_plan SET inventory_status = 'Y', receiver_id = $1 WHERE objid = $2`,
|
||||
[user.userId, arrivalObjId],
|
||||
);
|
||||
|
||||
// 3) 재고 반영 — 마스터(본인 프로젝트)부터 처리
|
||||
let remainingQty = deltaQty;
|
||||
|
||||
// 마스터 inventory_mgmt 행 확보(없으면 생성)
|
||||
const masterInvParent = await ensureInventoryMgmt(client, {
|
||||
contractObjId: contractMgmtObjId,
|
||||
unit: unitCode,
|
||||
partObjId,
|
||||
orderObjId,
|
||||
ldPartObjId,
|
||||
receiptQty: String(deltaQty),
|
||||
location,
|
||||
subLocation,
|
||||
writer: user.userId,
|
||||
});
|
||||
|
||||
if (multiYn !== "Y") {
|
||||
// 단일발주: 마스터에 전부 기록
|
||||
await insertInventoryIn(client, {
|
||||
parentObjId: masterInvParent,
|
||||
receiptQty: String(deltaQty),
|
||||
location, subLocation,
|
||||
writer: user.userId,
|
||||
contractMgmtObjId,
|
||||
poMasterObjId: orderObjId,
|
||||
poSubObjId: orderObjId,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 동시발주: 마스터부터 min(ORDER_QTY - 기입고, remaining) 분배
|
||||
// 마스터 기존 입고수량 (동일 purchase_order_master_objid + contract + parent)
|
||||
const masterExistingQty = await getExistingInventoryInQty(client, {
|
||||
poMasterObjId: orderObjId,
|
||||
contractMgmtObjId,
|
||||
parentObjId: masterInvParent,
|
||||
});
|
||||
const masterInsertable = Math.max(0, orderQty - masterExistingQty);
|
||||
const masterInsert = Math.min(masterInsertable, remainingQty);
|
||||
if (masterInsert > 0) {
|
||||
await insertInventoryIn(client, {
|
||||
parentObjId: masterInvParent,
|
||||
receiptQty: String(masterInsert),
|
||||
location, subLocation,
|
||||
writer: user.userId,
|
||||
contractMgmtObjId,
|
||||
poMasterObjId: orderObjId,
|
||||
poSubObjId: orderObjId,
|
||||
});
|
||||
remainingQty -= masterInsert;
|
||||
}
|
||||
|
||||
if (remainingQty <= 0) continue;
|
||||
|
||||
// 슬레이브들 순차 분배
|
||||
const slaves = await client.query(
|
||||
`SELECT objid::text AS objid,
|
||||
contract_mgmt_objid::text AS contract_mgmt_objid,
|
||||
unit_code
|
||||
FROM purchase_order_master
|
||||
WHERE multi_master_objid = $1
|
||||
ORDER BY (SELECT project_no FROM project_mgmt CNT WHERE CNT.objid = purchase_order_master.contract_mgmt_objid LIMIT 1)`,
|
||||
[orderObjId],
|
||||
);
|
||||
|
||||
for (const slave of slaves.rows as Array<{ objid: string; contract_mgmt_objid: string; unit_code: string }>) {
|
||||
if (remainingQty <= 0) break;
|
||||
|
||||
const slaveContractObjId = slave.contract_mgmt_objid ?? "";
|
||||
const slaveUnit = slave.unit_code ?? "";
|
||||
const slavePoObjId = slave.objid;
|
||||
|
||||
// 슬레이브 inventory_mgmt 확보
|
||||
const slaveInvParent = await ensureInventoryMgmt(client, {
|
||||
contractObjId: slaveContractObjId,
|
||||
unit: slaveUnit,
|
||||
partObjId,
|
||||
orderObjId,
|
||||
ldPartObjId,
|
||||
receiptQty: String(remainingQty),
|
||||
location,
|
||||
subLocation,
|
||||
writer: user.userId,
|
||||
});
|
||||
|
||||
const slaveExistingQty = await getExistingInventoryInQty(client, {
|
||||
poMasterObjId: orderObjId,
|
||||
contractMgmtObjId: slaveContractObjId,
|
||||
parentObjId: slaveInvParent,
|
||||
});
|
||||
const slaveInsertable = Math.max(0, orderQty - slaveExistingQty);
|
||||
const slaveInsert = Math.min(slaveInsertable, remainingQty);
|
||||
if (slaveInsert <= 0) continue;
|
||||
|
||||
await insertInventoryIn(client, {
|
||||
parentObjId: slaveInvParent,
|
||||
receiptQty: String(slaveInsert),
|
||||
location, subLocation,
|
||||
writer: user.userId,
|
||||
contractMgmtObjId: slaveContractObjId,
|
||||
poMasterObjId: orderObjId,
|
||||
poSubObjId: slavePoObjId,
|
||||
});
|
||||
remainingQty -= slaveInsert;
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "저장되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("delivery/acceptance/save:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "저장 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureInventoryMgmt(
|
||||
client: PoolClient,
|
||||
p: {
|
||||
contractObjId: string; unit: string; partObjId: string;
|
||||
orderObjId: string; ldPartObjId: string;
|
||||
receiptQty: string; location: string; subLocation: string; writer: string;
|
||||
},
|
||||
): Promise<string> {
|
||||
if (!p.partObjId) return "";
|
||||
const found = await client.query(
|
||||
`SELECT objid::text AS objid FROM inventory_mgmt
|
||||
WHERE contract_objid = $1 AND unit = $2 AND part_objid = $3 LIMIT 1`,
|
||||
[p.contractObjId, p.unit, p.partObjId],
|
||||
);
|
||||
if (found.rows[0]?.objid) return String(found.rows[0].objid);
|
||||
|
||||
const newObjId = createObjectId();
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt (
|
||||
objid, contract_objid, unit, part_objid,
|
||||
cls_cd, qty, location, sub_location,
|
||||
reg_date, price, writer
|
||||
)
|
||||
VALUES ($1,$2,$3,$4,'0001205',$5,$6,$7,
|
||||
TO_CHAR(NOW(),'YYYY-MM-DD'),
|
||||
(SELECT partner_price FROM purchase_order_part
|
||||
WHERE purchase_order_master_objid = $8 AND part_objid = $4 LIMIT 1),
|
||||
$9)
|
||||
ON CONFLICT (contract_objid, unit, part_objid) DO NOTHING`,
|
||||
[
|
||||
newObjId, p.contractObjId, p.unit, p.partObjId,
|
||||
p.receiptQty, p.location, p.subLocation,
|
||||
p.orderObjId, p.writer,
|
||||
],
|
||||
);
|
||||
// 경합 시 기존 값 재조회
|
||||
const again = await client.query(
|
||||
`SELECT objid::text AS objid FROM inventory_mgmt
|
||||
WHERE contract_objid = $1 AND unit = $2 AND part_objid = $3 LIMIT 1`,
|
||||
[p.contractObjId, p.unit, p.partObjId],
|
||||
);
|
||||
return String(again.rows[0]?.objid ?? newObjId);
|
||||
}
|
||||
|
||||
async function getExistingInventoryInQty(
|
||||
client: PoolClient,
|
||||
p: { poMasterObjId: string; contractMgmtObjId: string; parentObjId: string },
|
||||
): Promise<number> {
|
||||
const r = await client.query(
|
||||
`SELECT SUM(CASE WHEN receipt_qty IS NULL OR receipt_qty = '' THEN 0 ELSE receipt_qty::numeric END) AS qty
|
||||
FROM inventory_mgmt_in
|
||||
WHERE purchase_order_master_objid = $1
|
||||
AND contract_mgmt_objid = $2
|
||||
AND parent_objid = $3`,
|
||||
[p.poMasterObjId, p.contractMgmtObjId, p.parentObjId],
|
||||
);
|
||||
return Number(r.rows[0]?.qty ?? 0) || 0;
|
||||
}
|
||||
|
||||
async function insertInventoryIn(
|
||||
client: PoolClient,
|
||||
p: {
|
||||
parentObjId: string; receiptQty: string;
|
||||
location: string; subLocation: string; writer: string;
|
||||
contractMgmtObjId: string; poMasterObjId: string; poSubObjId: string;
|
||||
},
|
||||
) {
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_in (
|
||||
objid, parent_objid, receipt_qty, location, sub_location,
|
||||
writer, regdate, contract_mgmt_objid,
|
||||
purchase_order_master_objid, purchase_order_sub_objid
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,NOW(),$7,$8,$9)`,
|
||||
[
|
||||
createObjectId(), p.parentObjId, p.receiptQty, p.location, p.subLocation,
|
||||
p.writer, p.contractMgmtObjId, p.poMasterObjId, p.poSubObjId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 구(舊) 단건 호환 — 기존 arrival-plan 팝업이 호출하는 경로 유지
|
||||
async function legacySingleSave(body: Record<string, unknown>, writer: string) {
|
||||
const objId = String(body.objId ?? "") || createObjectId();
|
||||
const parentObjId = String(body.purchase_order_master_objid ?? body.parent_objid ?? "");
|
||||
const orderPartObjId = String(body.purchase_order_part_objid ?? body.order_part_objid ?? "");
|
||||
const partObjId = String(body.part_objid ?? "");
|
||||
const receiptQty = String(body.receipt_qty ?? "0");
|
||||
const receiptDate = String(body.receipt_date ?? "");
|
||||
const arrivalQty = String(body.arrival_qty ?? receiptQty);
|
||||
const arrivalPlanDate = String(body.arrival_plan_date ?? receiptDate);
|
||||
const location = String(body.location ?? "");
|
||||
const subLocation = String(body.sub_location ?? "");
|
||||
const groupSeq = String(body.group_seq ?? "1");
|
||||
const seq = String(body.seq ?? "1");
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
await client.query(
|
||||
`INSERT INTO arrival_plan (
|
||||
objid, parent_objid, order_part_objid, part_objid,
|
||||
receipt_qty, receipt_date, location, sub_location,
|
||||
writer, receiver_id, group_seq, seq, arrival_qty, arrival_plan_date
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$9,$10,$11,$12,$13)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
receipt_qty=EXCLUDED.receipt_qty, receipt_date=EXCLUDED.receipt_date,
|
||||
location=EXCLUDED.location, sub_location=EXCLUDED.sub_location,
|
||||
arrival_qty=EXCLUDED.arrival_qty, arrival_plan_date=EXCLUDED.arrival_plan_date,
|
||||
receiver_id=EXCLUDED.receiver_id`,
|
||||
[
|
||||
objId, parentObjId, orderPartObjId, partObjId,
|
||||
receiptQty, receiptDate, location, subLocation,
|
||||
writer, groupSeq, seq, arrivalQty, arrivalPlanDate,
|
||||
],
|
||||
);
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "저장되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("delivery/acceptance/save legacy:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "저장 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId required" });
|
||||
|
||||
const info = await queryOne(
|
||||
`SELECT AP.objid::text AS "OBJID",
|
||||
POM.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
POP.part_no AS "PART_NO", POP.part_name AS "PART_NAME",
|
||||
AP.error_qty AS "ERROR_QTY", AP.error_reason AS "ERROR_REASON",
|
||||
AP.attribution AS "ATTRIBUTION", AP.receipt_qty AS "RECEIPT_QTY",
|
||||
AP.receipt_date AS "RECEIPT_DATE", AP.re_arrival_plan_date AS "RE_ARRIVAL_PLAN_DATE",
|
||||
AP.group_seq AS "GROUP_SEQ", AP.seq AS "SEQ"
|
||||
FROM arrival_plan AP
|
||||
JOIN purchase_order_part POP ON POP.objid::text = AP.order_part_objid
|
||||
JOIN purchase_order_master POM ON POM.objid = POP.purchase_order_master_objid
|
||||
WHERE AP.objid::text = $1`, [objId]
|
||||
);
|
||||
if (!info) return NextResponse.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
return NextResponse.json({ success: true, data: info });
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 입고관리 > 부적합리스트 목록 조회
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// arrival_plan의 error_qty > 0 인 건이 부적합 (invalidMgmtGridList 대응)
|
||||
const conditions: string[] = ["AP.error_qty IS NOT NULL", "AP.error_qty != ''", "AP.error_qty != '0'"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(CM.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.partner_objid) {
|
||||
conditions.push(`POM.partner_objid = $${idx++}`);
|
||||
params.push(body.partner_objid);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`CM.contract_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.defect_type || body.error_reason) {
|
||||
conditions.push(`AP.error_reason = $${idx++}`);
|
||||
params.push(body.defect_type || body.error_reason);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT AP.objid::text AS "OBJID",
|
||||
POM.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
CM.contract_no AS "PROJECT_NO",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = CM.customer_objid LIMIT 1), '') AS "CUSTOMER_NAME",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = POM.partner_objid LIMIT 1), '') AS "PARTNER_NAME",
|
||||
POP.part_no AS "PART_NO",
|
||||
POP.part_name AS "PART_NAME",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = AP.error_reason LIMIT 1), '') AS "DEFECT_TYPE_NAME",
|
||||
AP.error_qty AS "DEFECT_QTY",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = AP.attribution LIMIT 1), '') AS "ATTRIBUTION_NAME",
|
||||
AP.receipt_date AS "REG_DATE",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = AP.receiver_id LIMIT 1), '') AS "REG_USER_NAME",
|
||||
AP.re_arrival_plan_date AS "RE_ARRIVAL_PLAN_DATE",
|
||||
CASE WHEN COALESCE(AP.assembly_status, '') != '' THEN '처리완료' ELSE '미처리' END AS "STATUS_NAME",
|
||||
AP.group_seq AS "GROUP_SEQ",
|
||||
AP.seq AS "SEQ"
|
||||
FROM arrival_plan AP
|
||||
JOIN purchase_order_part POP ON POP.objid::text = AP.order_part_objid
|
||||
JOIN purchase_order_master POM ON POM.objid = POP.purchase_order_master_objid
|
||||
LEFT JOIN contract_mgmt CM ON CM.objid = POM.contract_mgmt_objid
|
||||
WHERE ${where}
|
||||
ORDER BY AP.group_seq, AP.seq
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 부적합 등록/수정 (supplyChainMgmt.saveDeliveryInvalidInfo 대응)
|
||||
// ARRIVAL_PLAN의 error_qty, error_reason, attribution 업데이트
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
if (!body.objId) {
|
||||
return NextResponse.json({ success: false, message: "대상 항목을 선택하세요." });
|
||||
}
|
||||
|
||||
try {
|
||||
await execute(
|
||||
`UPDATE arrival_plan SET
|
||||
error_qty = $1,
|
||||
error_reason = $2,
|
||||
attribution = $3
|
||||
WHERE objid = $4`,
|
||||
[
|
||||
body.error_qty || body.defect_qty || "",
|
||||
body.error_reason || body.defect_reason || "",
|
||||
body.attribution || "",
|
||||
body.objId,
|
||||
]
|
||||
);
|
||||
return NextResponse.json({ success: true, message: "저장되었습니다." });
|
||||
} catch (e) {
|
||||
console.error("Defect save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 입고관리 > 단가관리 목록 조회
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`EXTRACT(YEAR FROM POM.regdate) = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.partner_objid) {
|
||||
conditions.push(`POM.partner_objid = $${idx++}`);
|
||||
params.push(body.partner_objid);
|
||||
}
|
||||
if (body.part_no) {
|
||||
conditions.push(`POP.part_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.part_no);
|
||||
}
|
||||
if (body.part_name) {
|
||||
conditions.push(`POP.part_name LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.part_name);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT POP.objid::text AS "OBJID",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = POM.partner_objid LIMIT 1), '') AS "PARTNER_NAME",
|
||||
POP.part_no AS "PART_NO",
|
||||
POP.part_name AS "PART_NAME",
|
||||
POP.spec AS "SPEC",
|
||||
POP.material AS "MATERIAL",
|
||||
POP.unit AS "UNIT_NAME",
|
||||
COALESCE(POP.partner_price, '0') AS "UNIT_PRICE",
|
||||
POM.delivery_date AS "START_DATE",
|
||||
'' AS "END_DATE",
|
||||
TO_CHAR(POM.regdate, 'YYYY-MM-DD') AS "REG_DATE",
|
||||
POP.remark AS "REMARK"
|
||||
FROM purchase_order_part POP
|
||||
JOIN purchase_order_master POM ON POM.objid = POP.purchase_order_master_objid
|
||||
WHERE ${where}
|
||||
ORDER BY POM.regdate DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: purchaseOrder.xml#deliveryMngStatus
|
||||
// 입고관리 > 현황 — 프로젝트 BOM 1개(part_bom_report) 단위 집계 리포트
|
||||
// 4개 그룹: 프로젝트정보 / 발주내역 / 입고현황 / 수입검사결과(불량현황)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = [
|
||||
"SBR.parent_objid = PBR.objid",
|
||||
"PBR.contract_objid = CM.objid",
|
||||
];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.Year) {
|
||||
conditions.push(`TO_CHAR(CM.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.Year);
|
||||
}
|
||||
if (body.customer_objid) {
|
||||
conditions.push(`CM.customer_objid = $${idx++}`);
|
||||
params.push(body.customer_objid);
|
||||
}
|
||||
|
||||
// project_nos (array or CSV)
|
||||
const projectNos: string[] = Array.isArray(body.project_nos)
|
||||
? body.project_nos.filter(Boolean).map(String)
|
||||
: body.project_nos
|
||||
? String(body.project_nos).split(",").filter(Boolean)
|
||||
: [];
|
||||
if (projectNos.length > 0) {
|
||||
const placeholders = projectNos.map(() => `$${idx++}`).join(", ");
|
||||
conditions.push(`CM.objid::text IN (${placeholders})`);
|
||||
params.push(...projectNos);
|
||||
}
|
||||
|
||||
if (body.unit_code) {
|
||||
conditions.push(`PBR.unit_code = $${idx++}`);
|
||||
params.push(body.unit_code);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT T.*,
|
||||
CASE WHEN "TOTAL_PO_PART_CNT"::numeric = 0 THEN 0
|
||||
WHEN "TOTAL_BOM_PART_CNT"::numeric = 0 THEN 0
|
||||
ELSE ROUND("TOTAL_PO_PART_CNT"::numeric / "TOTAL_BOM_PART_CNT"::numeric * 100, 1)
|
||||
END AS "PO_RATE",
|
||||
CASE WHEN "TOTAL_BOM_PART_CNT"::numeric - "TOTAL_PO_PART_CNT"::numeric < 0 THEN 0
|
||||
ELSE "TOTAL_BOM_PART_CNT"::numeric - "TOTAL_PO_PART_CNT"::numeric
|
||||
END AS "NON_PO_PART_CNT",
|
||||
"TOTAL_DELIVERY_QTY"::numeric AS "DELIVERY_QTY",
|
||||
("TOTAL_PO_QTY"::numeric - "TOTAL_DELIVERY_QTY"::numeric) AS "NON_DELIVERY_QTY",
|
||||
CASE WHEN "TOTAL_DEFECT_QTY"::numeric = 0 THEN 0
|
||||
WHEN "TOTAL_PO_QTY"::numeric = 0 THEN 0
|
||||
ELSE ROUND("TOTAL_DEFECT_QTY"::numeric / "TOTAL_PO_QTY"::numeric * 100, 1)
|
||||
END AS "DELIVERY_RATE"
|
||||
FROM (
|
||||
SELECT CM.objid::text AS "OBJID",
|
||||
SBR.objid::text AS "SALES_OBJID",
|
||||
PBR.objid::text AS "BOM_REPORT_OBJID",
|
||||
TO_CHAR(CM.regdate, 'YYYY') AS "CM_YEAR",
|
||||
(SELECT supply_name FROM supply_mng SM WHERE SM.objid::text = CM.customer_objid LIMIT 1) AS "CUSTOMER_NAME",
|
||||
CM.customer_project_name AS "CUSTOMER_PROJECT_NAME",
|
||||
COALESCE(CM.project_no, CM.contract_no) AS "PROJECT_NO",
|
||||
(SELECT COALESCE(O.unit_no,'') || '-' || COALESCE(O.task_name,'') FROM pms_wbs_task O WHERE O.objid = PBR.unit_code LIMIT 1) AS "UNIT_PART_NAME",
|
||||
|
||||
-- BOM 부품개수
|
||||
(SELECT COUNT(DISTINCT CASE WHEN PM.part_type IN (SELECT code_id FROM comm_code WHERE parent_code_id = '0000062') THEN BPQ.part_no ELSE NULL END)
|
||||
FROM bom_part_qty BPQ
|
||||
JOIN part_mng PM ON BPQ.part_no = PM.objid::text
|
||||
WHERE BPQ.bom_report_objid = PBR.objid::text
|
||||
AND BPQ.status IN ('beforeEdit','editing','deleting','deploy')
|
||||
) AS "TOTAL_BOM_PART_CNT",
|
||||
|
||||
-- BOM 총수량 (재귀 CTE) — LEV>2 & IS_LEAF
|
||||
(WITH RECURSIVE V(bom_report_objid, child_objid, qty, lev, path, is_leaf, aggregate_qty) AS (
|
||||
SELECT A.bom_report_objid, A.child_objid, A.qty, 1,
|
||||
ARRAY[A.child_objid::text], FALSE,
|
||||
COALESCE(NULLIF(A.qty,''),'0')::numeric
|
||||
FROM bom_part_qty A
|
||||
WHERE A.bom_report_objid = PBR.objid::text
|
||||
AND (A.parent_objid IS NULL OR A.parent_objid = '')
|
||||
UNION ALL
|
||||
SELECT B.bom_report_objid, B.child_objid, B.qty, V.lev + 1,
|
||||
V.path || B.child_objid::text,
|
||||
B.parent_objid = ANY(V.path),
|
||||
V.aggregate_qty * COALESCE(NULLIF(B.qty,''),'0')::numeric
|
||||
FROM bom_part_qty B
|
||||
JOIN V ON B.parent_objid = V.child_objid AND B.bom_report_objid = V.bom_report_objid
|
||||
WHERE NOT (B.parent_objid = ANY(V.path))
|
||||
)
|
||||
SELECT SUM(V.aggregate_qty) FROM V
|
||||
WHERE V.lev > 2
|
||||
AND NOT EXISTS (SELECT 1 FROM bom_part_qty C WHERE C.parent_objid = V.child_objid AND C.bom_report_objid = V.bom_report_objid)
|
||||
) AS "TOTAL_BOM_PART_QTY_SUM",
|
||||
|
||||
-- 발주품 개수 (발주서 기준)
|
||||
(SELECT COUNT(DISTINCT PO.part_no)
|
||||
FROM bom_part_qty Q
|
||||
JOIN purchase_order_part PO ON COALESCE(NULLIF(Q.last_part_objid,''), Q.part_no) = PO.part_objid
|
||||
JOIN purchase_order_master POM ON POM.objid::text = PO.purchase_order_master_objid
|
||||
JOIN part_mng P ON P.objid::text = PO.part_objid
|
||||
WHERE POM.bom_report_objid = PBR.objid::text
|
||||
AND Q.bom_report_objid = PBR.objid::text
|
||||
AND Q.status IN ('beforeEdit','editing','deleting','deploy')
|
||||
AND POM.status = 'approvalComplete'
|
||||
AND COALESCE(P.part_type,'') != ''
|
||||
) AS "TOTAL_PO_PART_CNT",
|
||||
|
||||
-- 발주수량 총계
|
||||
(SELECT SUM(COALESCE(NULLIF(POP.order_qty,''),'0')::numeric)
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POM.objid::text = POP.purchase_order_master_objid
|
||||
WHERE POM.bom_report_objid = PBR.objid::text
|
||||
AND POM.status = 'approvalComplete'
|
||||
AND POM.contract_mgmt_objid = CM.objid
|
||||
) AS "TOTAL_PO_QTY",
|
||||
|
||||
-- 발주수량 신규 (order_type_cd='0001407')
|
||||
(SELECT SUM(COALESCE(NULLIF(POP.order_qty,''),'0')::numeric)
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POM.objid::text = POP.purchase_order_master_objid
|
||||
WHERE POM.bom_report_objid = PBR.objid::text
|
||||
AND POM.order_type_cd = '0001407'
|
||||
AND POM.status = 'approvalComplete'
|
||||
AND POM.contract_mgmt_objid = CM.objid
|
||||
) AS "TOTAL_PO_NEW_QTY",
|
||||
|
||||
-- 발주수량 재발주 (order_type_cd='0001408')
|
||||
(SELECT SUM(COALESCE(NULLIF(POP.order_qty,''),'0')::numeric)
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POM.objid::text = POP.purchase_order_master_objid
|
||||
WHERE POM.bom_report_objid = PBR.objid::text
|
||||
AND POM.order_type_cd = '0001408'
|
||||
AND POM.status = 'approvalComplete'
|
||||
AND POM.contract_mgmt_objid = CM.objid
|
||||
) AS "TOTAL_PO_RE_QTY",
|
||||
|
||||
-- 입고수량 (inventory_mgmt_in 기준)
|
||||
(SELECT SUM(COALESCE(NULLIF(DH.receipt_qty,''),'0')::numeric)
|
||||
FROM inventory_mgmt_in DH
|
||||
JOIN purchase_order_master POM ON POM.objid::text = DH.purchase_order_sub_objid
|
||||
WHERE CM.objid::text = DH.contract_mgmt_objid
|
||||
AND POM.bom_report_objid = PBR.objid::text
|
||||
AND POM.status = 'approvalComplete'
|
||||
AND POM.contract_mgmt_objid = CM.objid
|
||||
) AS "TOTAL_DELIVERY_QTY",
|
||||
|
||||
-- 부적합 집계
|
||||
COALESCE(S1.total_defect_qty, 0) AS "TOTAL_DEFECT_QTY",
|
||||
COALESCE(S1.defect_qty_1, 0) AS "DEFECT_QTY_1",
|
||||
COALESCE(S1.defect_qty_2, 0) AS "DEFECT_QTY_2",
|
||||
COALESCE(S1.defect_qty_3, 0) AS "DEFECT_QTY_3",
|
||||
COALESCE(S1.defect_qty_4, 0) AS "DEFECT_QTY_4",
|
||||
COALESCE(S1.total_defect_price, 0) AS "TOTAL_DEFECT_PRICE"
|
||||
FROM sales_bom_report SBR,
|
||||
part_bom_report PBR
|
||||
LEFT OUTER JOIN (
|
||||
SELECT POM.bom_report_objid,
|
||||
SUM(COALESCE(NULLIF(DH.error_qty,''),'0')::numeric) AS total_defect_qty,
|
||||
SUM(CASE WHEN DH.error_reason = '0001114' THEN COALESCE(NULLIF(DH.error_qty,''),'0')::numeric ELSE NULL END) AS defect_qty_1,
|
||||
SUM(CASE WHEN DH.error_reason = '0001115' THEN COALESCE(NULLIF(DH.error_qty,''),'0')::numeric ELSE NULL END) AS defect_qty_2,
|
||||
SUM(CASE WHEN DH.error_reason = '0001116' THEN COALESCE(NULLIF(DH.error_qty,''),'0')::numeric ELSE NULL END) AS defect_qty_3,
|
||||
SUM(CASE WHEN DH.error_reason = '0001117' THEN COALESCE(NULLIF(DH.error_qty,''),'0')::numeric ELSE NULL END) AS defect_qty_4,
|
||||
SUM(COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric * COALESCE(NULLIF(DH.error_qty,''),'0')::numeric) AS total_defect_price
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POM.objid::text = POP.purchase_order_master_objid
|
||||
JOIN arrival_plan DH ON POM.objid::text = DH.parent_objid AND POP.part_objid = DH.part_objid
|
||||
WHERE POM.status = 'approvalComplete'
|
||||
GROUP BY POM.bom_report_objid
|
||||
) S1 ON PBR.objid::text = S1.bom_report_objid,
|
||||
project_mgmt CM
|
||||
WHERE ${where}
|
||||
ORDER BY "PROJECT_NO" DESC, "UNIT_PART_NAME"
|
||||
) T
|
||||
`;
|
||||
|
||||
try {
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
} catch (e) {
|
||||
console.error("delivery/status:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "조회 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne, queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId required" });
|
||||
|
||||
const info = await queryOne(
|
||||
`SELECT EM.expense_master_objid::text AS "OBJID", EM.expense_id AS "EXPENSE_ID",
|
||||
EM.project_mgmt_objid AS "PROJECT_MGMT_OBJID", EM.bns_start_date AS "BNS_START_DATE",
|
||||
EM.bns_end_date AS "BNS_END_DATE", EM.exp_status_cd AS "EXP_STATUS_CD",
|
||||
EM.exp_sort_cd AS "EXP_SORT_CD", EM.exp_area_cd AS "EXP_AREA_CD",
|
||||
EM.bus_title AS "BUS_TITLE", EM.bus_content AS "BUS_CONTENT",
|
||||
EM.vehicel_used AS "VEHICEL_USED", EM.amount_payment AS "AMOUNT_PAYMENT",
|
||||
EM.status AS "STATUS",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = EM.reg_user_id LIMIT 1), '') AS "REG_USER_NAME",
|
||||
CM.contract_no AS "PROJECT_NO", CM.project_name AS "PROJECT_NAME"
|
||||
FROM expense_master EM
|
||||
LEFT JOIN contract_mgmt CM ON CM.objid::text = EM.project_mgmt_objid
|
||||
WHERE EM.expense_master_objid::text = $1`, [objId]
|
||||
);
|
||||
if (!info) return NextResponse.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
|
||||
const details = await queryRows(
|
||||
`SELECT ED.expense_detail_objid::text AS "OBJID",
|
||||
ED.exp_sort_cd AS "EXP_SORT_CD", ED.exp_subm_cd AS "EXP_SUBM_CD",
|
||||
ED.exp_subd_cd AS "EXP_SUBD_CD",
|
||||
ED.card_used AS "CARD_USED", ED.cash_used AS "CASH_USED", ED.payment AS "PAYMENT"
|
||||
FROM expense_detail ED WHERE ED.expense_master_objid::text = $1`, [objId]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true, data: info, DETAILS: details });
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(EM.reg_date, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.writer_name) {
|
||||
conditions.push(`(SELECT user_name FROM user_info WHERE user_id = EM.reg_user_id LIMIT 1) LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.writer_name);
|
||||
} else if (body.reg_user_id) {
|
||||
conditions.push(`EM.reg_user_id = $${idx++}`);
|
||||
params.push(body.reg_user_id);
|
||||
}
|
||||
if (body.status_code || body.exp_status_cd) {
|
||||
conditions.push(`EM.exp_status_cd = $${idx++}`);
|
||||
params.push(body.status_code || body.exp_status_cd);
|
||||
}
|
||||
if (body.expense_date_from || body.bns_start_date) {
|
||||
conditions.push(`EM.bns_start_date >= $${idx++}`);
|
||||
params.push(body.expense_date_from || body.bns_start_date);
|
||||
}
|
||||
if (body.expense_date_to || body.bns_end_date) {
|
||||
conditions.push(`EM.bns_end_date <= $${idx++}`);
|
||||
params.push(body.expense_date_to || body.bns_end_date);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? "AND " + conditions.join(" AND ") : "";
|
||||
|
||||
const sql = `
|
||||
SELECT EM.expense_master_objid::text AS "OBJID", EM.expense_id AS "EXPENSE_ID",
|
||||
CM.contract_no AS "PROJECT_NO", CM.project_name AS "PROJECT_NAME",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = EM.reg_user_id LIMIT 1), '') AS "REG_USER_NAME",
|
||||
EM.bns_start_date AS "BNS_START_DATE",
|
||||
EM.bns_end_date AS "BNS_END_DATE",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = EM.exp_status_cd LIMIT 1), '') AS "EXP_STATUS_NAME",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = EM.exp_area_cd LIMIT 1), '') AS "EXP_AREA_NAME",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = EM.exp_sort_cd LIMIT 1), '') AS "EXP_SORT_NAME",
|
||||
EM.bus_title AS "BUS_TITLE", EM.bus_content AS "BUS_CONTENT",
|
||||
EM.vehicel_used AS "VEHICEL_USED",
|
||||
COALESCE(EM.amount_payment::numeric, 0) AS "AMOUNT_PAYMENT",
|
||||
EM.expense_id AS "EXPENSE_FORM_NO",
|
||||
EM.bns_start_date AS "EXPENSE_DATE",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = EM.exp_sort_cd LIMIT 1), '') AS "EXPENSE_TYPE_NAME",
|
||||
COALESCE(EM.amount_payment::numeric, 0) AS "EXPENSE_AMOUNT",
|
||||
EM.bus_title AS "EXPENSE_PLACE",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = EM.status LIMIT 1), EM.status) AS "STATUS_NAME",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = EM.reg_user_id LIMIT 1), '') AS "WRITER_NAME",
|
||||
EM.status AS "STATUS",
|
||||
EM.reg_date AS "REG_DATE",
|
||||
COALESCE((SELECT SUM(COALESCE(ED.payment,'0')::numeric) FROM expense_detail ED WHERE ED.expense_master_objid = EM.expense_master_objid), 0) AS "DETAIL_TOTAL"
|
||||
FROM expense_master EM
|
||||
LEFT JOIN contract_mgmt CM ON CM.objid::text = EM.project_mgmt_objid
|
||||
WHERE 1=1 ${whereClause}
|
||||
ORDER BY EM.reg_date DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 경비신청서 저장 (mergeExpenseMaster 대응)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const body = await request.json();
|
||||
const isNew = !body.objId || body.actionType === "regist";
|
||||
const objId = isNew ? createObjectId() : body.objId;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
// 신규시 expense_id 자동 생성: EC-YYYY-NNN
|
||||
let expenseId = body.expense_id || "";
|
||||
if (isNew && !expenseId) {
|
||||
const year = new Date().getFullYear();
|
||||
const r = await client.query(
|
||||
`SELECT COALESCE(MAX(SUBSTR(expense_id, 9)::numeric), 0)::int + 1 AS seq
|
||||
FROM expense_master WHERE expense_id LIKE $1`, [`EC-${year}-%`]
|
||||
);
|
||||
const seq = r.rows[0]?.seq || 1;
|
||||
expenseId = `EC-${year}-${String(seq).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO expense_master (
|
||||
expense_master_objid, expense_id, project_mgmt_objid,
|
||||
bns_start_date, bns_end_date, exp_status_cd, exp_sort_cd,
|
||||
exp_area_cd, bus_title, bus_content, vehicel_used, amount_payment,
|
||||
reg_user_id, reg_date, status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14)
|
||||
ON CONFLICT (expense_master_objid) DO UPDATE SET
|
||||
project_mgmt_objid=EXCLUDED.project_mgmt_objid,
|
||||
bns_start_date=EXCLUDED.bns_start_date, bns_end_date=EXCLUDED.bns_end_date,
|
||||
exp_status_cd=EXCLUDED.exp_status_cd, exp_sort_cd=EXCLUDED.exp_sort_cd,
|
||||
exp_area_cd=EXCLUDED.exp_area_cd, bus_title=EXCLUDED.bus_title,
|
||||
bus_content=EXCLUDED.bus_content, vehicel_used=EXCLUDED.vehicel_used,
|
||||
amount_payment=EXCLUDED.amount_payment, status=EXCLUDED.status`,
|
||||
[objId, expenseId, body.project_mgmt_objid || "",
|
||||
body.bns_start_date || null, body.bns_end_date || null,
|
||||
body.exp_status_cd || "", body.exp_sort_cd || "",
|
||||
body.exp_area_cd || "", body.bus_title || "", body.bus_content || "",
|
||||
body.vehicel_used || "", body.amount_payment || "0",
|
||||
user.userId, body.status || "created"]
|
||||
);
|
||||
return NextResponse.json({ success: true, objId, expenseId, message: isNew ? "등록되었습니다." : "수정되었습니다." });
|
||||
} catch (e) {
|
||||
console.error("Expense save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally { client.release(); }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId required" });
|
||||
|
||||
const info = await queryOne(
|
||||
`SELECT IM.objid::text AS "OBJID", IM.parent_objid AS "PARENT_OBJID",
|
||||
IM.price_sum AS "PRICE_SUM", IM.issuance_date AS "ISSUANCE_DATE",
|
||||
IM.status AS "STATUS", IM.group_seq AS "GROUP_SEQ",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = CM.customer_objid LIMIT 1), '') AS "CUSTOMER_NAME",
|
||||
CM.contract_no AS "PROJECT_NO", CM.project_name AS "PROJECT_NAME",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = IM.writer LIMIT 1), '') AS "WRITER_NAME"
|
||||
FROM invoice_mgmt IM
|
||||
LEFT JOIN contract_mgmt CM ON CM.objid::text = IM.parent_objid
|
||||
WHERE IM.objid::text = $1`, [objId]
|
||||
);
|
||||
if (!info) return NextResponse.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
return NextResponse.json({ success: true, data: info });
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(IM.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.customer_name) {
|
||||
conditions.push(`COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = CM.customer_objid LIMIT 1), '') LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.customer_name);
|
||||
}
|
||||
if (body.invoice_date_from) {
|
||||
conditions.push(`IM.issuance_date >= $${idx++}`);
|
||||
params.push(body.invoice_date_from);
|
||||
}
|
||||
if (body.invoice_date_to) {
|
||||
conditions.push(`IM.issuance_date <= $${idx++}`);
|
||||
params.push(body.invoice_date_to);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? "AND " + conditions.join(" AND ") : "";
|
||||
|
||||
const sql = `
|
||||
SELECT IM.objid::text AS "OBJID",
|
||||
IM.objid::text AS "INVOICE_NO",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = CM.customer_objid LIMIT 1), '') AS "CUSTOMER_NAME",
|
||||
CM.contract_no AS "PROJECT_NO", CM.project_name AS "PROJECT_NAME",
|
||||
IM.issuance_date AS "INVOICE_DATE",
|
||||
COALESCE(IM.price_sum::numeric, 0) AS "SUPPLY_AMOUNT",
|
||||
ROUND(COALESCE(IM.price_sum::numeric, 0) * 0.1) AS "TAX_AMOUNT",
|
||||
ROUND(COALESCE(IM.price_sum::numeric, 0) * 1.1) AS "TOTAL_AMOUNT",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = IM.status LIMIT 1), IM.status) AS "STATUS_NAME",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = IM.writer LIMIT 1), '') AS "WRITER_NAME"
|
||||
FROM invoice_mgmt IM
|
||||
LEFT JOIN contract_mgmt CM ON CM.objid::text = IM.parent_objid
|
||||
WHERE 1=1 ${whereClause}
|
||||
ORDER BY IM.regdate DESC
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 거래명세서 저장
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const body = await request.json();
|
||||
const isNew = !body.objId || body.actionType === "regist";
|
||||
const objId = isNew ? createObjectId() : body.objId;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query(
|
||||
`INSERT INTO invoice_mgmt (objid, parent_objid, group_seq, price_sum, issuance_date, status, regdate, writer)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, now(), $7)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
parent_objid=EXCLUDED.parent_objid,
|
||||
group_seq=EXCLUDED.group_seq,
|
||||
price_sum=EXCLUDED.price_sum,
|
||||
issuance_date=EXCLUDED.issuance_date,
|
||||
status=EXCLUDED.status`,
|
||||
[objId, body.parent_objid || "", body.group_seq || 1,
|
||||
body.price_sum || body.supply_amount || "0",
|
||||
body.issuance_date || null, body.status || "created", user.userId]
|
||||
);
|
||||
return NextResponse.json({ success: true, objId, message: isNew ? "등록되었습니다." : "수정되었습니다." });
|
||||
} catch (e) {
|
||||
console.error("Invoice save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally { client.release(); }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 자재관리 > 자금관리 (공급업체별 발주/입고 금액 집계)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["POM.status = 'approvalComplete'"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(POM.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.month) {
|
||||
conditions.push(`TO_CHAR(POM.regdate, 'MM') = $${idx++}`);
|
||||
params.push(body.month);
|
||||
}
|
||||
if (body.partner_objid) {
|
||||
conditions.push(`POM.partner_objid = $${idx++}`);
|
||||
params.push(body.partner_objid);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT POM.partner_objid AS "PARTNER_OBJID",
|
||||
COALESCE((SELECT supply_name FROM supply_mng WHERE objid::text = POM.partner_objid LIMIT 1), '') AS "PARTNER_NAME",
|
||||
-- 발주금액
|
||||
SUM(COALESCE(NULLIF(POM.total_supply_price,'')::numeric, 0)) AS "ORDER_AMOUNT",
|
||||
-- 입고금액 (입고된 파트의 공급가 합산)
|
||||
COALESCE(SUM((SELECT SUM(COALESCE(NULLIF(AP2.receipt_qty,'')::numeric, 0) * COALESCE(NULLIF(POP2.partner_price,'')::numeric, 0))
|
||||
FROM purchase_order_part POP2
|
||||
LEFT JOIN arrival_plan AP2 ON AP2.order_part_objid = POP2.objid::text
|
||||
WHERE POP2.purchase_order_master_objid = POM.objid)), 0) AS "DELIVERY_AMOUNT",
|
||||
-- 지급 관련 (placeholder — 별도 지급 테이블 확인 필요)
|
||||
0 AS "PAYMENT_PLAN_AMOUNT",
|
||||
0 AS "PAYMENT_DONE_AMOUNT",
|
||||
SUM(COALESCE(NULLIF(POM.total_supply_price,'')::numeric, 0)) AS "UNPAID_AMOUNT",
|
||||
POM.delivery_date AS "PAYMENT_PLAN_DATE",
|
||||
'' AS "PAYMENT_STATUS_NAME",
|
||||
'' AS "REMARK"
|
||||
FROM purchase_order_master POM
|
||||
WHERE ${where}
|
||||
GROUP BY POM.partner_objid, POM.delivery_date
|
||||
ORDER BY "PARTNER_NAME"
|
||||
`,
|
||||
params
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const body = await request.json();
|
||||
const objId = createObjectId();
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
// Insert input record
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_in (objid, parent_objid, receipt_qty, location, writer, regdate)
|
||||
VALUES ($1, $2, $3, $4, $5, now())`,
|
||||
[objId, body.parent_objid || "", body.receipt_qty || "", body.location || "", user.userId]
|
||||
);
|
||||
// Update parent inventory_mgmt input_qty and input_date
|
||||
await client.query(
|
||||
`UPDATE inventory_mgmt SET
|
||||
input_qty = COALESCE(input_qty, '0')::numeric + COALESCE($1, '0')::numeric || '',
|
||||
input_date = now()::text
|
||||
WHERE objid = $2`,
|
||||
[body.receipt_qty || "0", body.parent_objid || ""]
|
||||
);
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "입고 등록되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Inventory input save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." });
|
||||
} finally { client.release(); }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 자재 삭제 (deleteInventoryMng 대응)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objIds } = await request.json();
|
||||
if (!objIds?.length) return NextResponse.json({ success: false, message: "삭제할 항목을 선택하세요." });
|
||||
try {
|
||||
const ph = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
await execute(`DELETE FROM inventory_mgmt_out WHERE parent_objid IN (${ph})`, objIds);
|
||||
await execute(`DELETE FROM inventory_mgmt_in WHERE parent_objid IN (${ph})`, objIds);
|
||||
await execute(`DELETE FROM inventory_mgmt_history WHERE parent_objid IN (${ph})`, objIds);
|
||||
await execute(`DELETE FROM inventory_mgmt WHERE objid IN (${ph})`, objIds);
|
||||
return NextResponse.json({ success: true, message: `${objIds.length}건이 삭제되었습니다.` });
|
||||
} catch (error) {
|
||||
console.error("Inventory delete:", error);
|
||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 자재관리 > 자재리스트 (inventoryMng.getInventoryMngGridList 대응)
|
||||
// 원본: /Users/jhj/FITO/src/com/pms/mapper/inventoryMng.xml (line 790)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
// project_nos: comma-delimited (다중 선택)
|
||||
const projectNos = String(body.project_nos || "").trim();
|
||||
if (projectNos) {
|
||||
const arr = projectNos.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
if (arr.length) {
|
||||
const placeholders = arr.map(() => `$${idx++}`).join(",");
|
||||
conditions.push(`IM.contract_objid IN (${placeholders})`);
|
||||
params.push(...arr);
|
||||
}
|
||||
}
|
||||
if (body.unit_code) {
|
||||
conditions.push(`IM.unit = $${idx++}`);
|
||||
params.push(body.unit_code);
|
||||
}
|
||||
if (body.part_no) {
|
||||
conditions.push(`UPPER(P.part_no) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.part_no);
|
||||
}
|
||||
if (body.part_name) {
|
||||
conditions.push(`UPPER(P.part_name) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.part_name);
|
||||
}
|
||||
if (body.spec) {
|
||||
conditions.push(`UPPER(P.spec) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.spec);
|
||||
}
|
||||
if (body.part_type) {
|
||||
conditions.push(`P.part_type = $${idx++}`);
|
||||
params.push(body.part_type);
|
||||
}
|
||||
if (body.cls_cd) {
|
||||
conditions.push(`IM.cls_cd = $${idx++}`);
|
||||
params.push(body.cls_cd);
|
||||
}
|
||||
if (body.cau_cd) {
|
||||
conditions.push(`IM.cau_cd = $${idx++}`);
|
||||
params.push(body.cau_cd);
|
||||
}
|
||||
if (body.location) {
|
||||
conditions.push(`IM.location = $${idx++}`);
|
||||
params.push(body.location);
|
||||
}
|
||||
if (body.writer) {
|
||||
conditions.push(`IM.writer = $${idx++}`);
|
||||
params.push(body.writer);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
IM.OBJID::text AS "OBJID",
|
||||
IM.PART_OBJID AS "PART_OBJID",
|
||||
IM.CONTRACT_OBJID AS "CONTRACT_OBJID",
|
||||
IM.UNIT AS "UNIT",
|
||||
IM.CLS_CD AS "CLS_CD",
|
||||
IM.CAU_CD AS "CAU_CD",
|
||||
IM.QTY AS "QTY",
|
||||
IM.LOCATION AS "LOCATION",
|
||||
IM.REG_DATE AS "REG_DATE",
|
||||
IM.PRICE AS "PRICE",
|
||||
IM.WRITER AS "WRITER",
|
||||
IM.PART_NO AS "PART_NO",
|
||||
IM.PART_NAME AS "PART_NAME",
|
||||
IM.SPEC AS "SPEC",
|
||||
IM.MAKER AS "MAKER",
|
||||
IM.PART_TYPE_NAME AS "PART_TYPE_NAME",
|
||||
IM.PART_TYPE AS "PART_TYPE",
|
||||
IM.MATERIAL AS "MATERIAL",
|
||||
IM.WRITER_NAME AS "WRITER_NAME",
|
||||
(SELECT project_no FROM project_mgmt O WHERE O.objid = IM.CONTRACT_OBJID) AS "PROJECT_NO",
|
||||
(
|
||||
COALESCE((
|
||||
SELECT SUM(CASE WHEN (O.receipt_qty IS NULL OR O.receipt_qty = '')
|
||||
THEN 0
|
||||
ELSE (O.receipt_qty::numeric - COALESCE(O.move_qty::numeric, 0))
|
||||
END)
|
||||
FROM inventory_mgmt_in O WHERE O.parent_objid = IM.OBJID
|
||||
), 0)
|
||||
- COALESCE((
|
||||
SELECT SUM(CASE WHEN (O.request_qty IS NULL OR O.request_qty = '')
|
||||
THEN 0
|
||||
ELSE O.request_qty::numeric
|
||||
END)
|
||||
FROM inventory_mgmt_out O WHERE O.parent_objid = IM.OBJID
|
||||
), 0)
|
||||
) AS "USE_CNT",
|
||||
(
|
||||
COALESCE((
|
||||
SELECT SUM(CASE WHEN (O.receipt_qty IS NULL OR O.receipt_qty = '')
|
||||
THEN 0
|
||||
ELSE (O.receipt_qty::numeric - COALESCE(O.move_qty::numeric, 0))
|
||||
END)
|
||||
FROM inventory_mgmt_in O, inventory_mgmt S_IM
|
||||
WHERE S_IM.part_objid = IM.PART_OBJID
|
||||
AND O.parent_objid = S_IM.objid
|
||||
), 0)
|
||||
- COALESCE((
|
||||
SELECT SUM(CASE WHEN (O.request_qty IS NULL OR O.request_qty = '')
|
||||
THEN 0
|
||||
ELSE O.request_qty::numeric
|
||||
END)
|
||||
FROM inventory_mgmt_out O, inventory_mgmt S_IM
|
||||
WHERE S_IM.part_objid = IM.PART_OBJID
|
||||
AND O.parent_objid = S_IM.objid
|
||||
), 0)
|
||||
) AS "USE_CNT_ALL",
|
||||
COALESCE((
|
||||
SELECT SUM(CASE WHEN (O.out_qty IS NULL OR O.out_qty = '')
|
||||
THEN 0
|
||||
ELSE O.out_qty::numeric
|
||||
END)
|
||||
FROM inventory_mgmt_out O WHERE O.parent_objid = IM.OBJID
|
||||
), 0) AS "REQUEST_QTY",
|
||||
(SELECT O.unit_no || '-' || O.task_name FROM pms_wbs_task O WHERE O.objid = IM.UNIT) AS "UNIT_NAME",
|
||||
(
|
||||
SELECT ARRAY_TO_STRING(ARRAY_AGG(DISTINCT (SELECT code_name FROM comm_code CC WHERE CC.code_id = IMI.location)), ',')
|
||||
FROM inventory_mgmt_in IMI WHERE IMI.parent_objid = IM.OBJID
|
||||
) AS "LOCATION_NAME",
|
||||
(
|
||||
SELECT MAX(O.remark) FROM inventory_mgmt_out_master O
|
||||
WHERE O.objid IN (
|
||||
SELECT T1.inventory_request_master_objid FROM inventory_mgmt_out T1
|
||||
WHERE T1.parent_objid = IM.OBJID ORDER BY T1.regdate DESC LIMIT 1
|
||||
)
|
||||
) AS "REMARK",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = IM.CLS_CD) AS "CLS_CD_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = IM.CAU_CD) AS "CAU_CD_NAME"
|
||||
FROM (
|
||||
SELECT
|
||||
IM.objid AS OBJID,
|
||||
IM.part_objid AS PART_OBJID,
|
||||
P.part_no AS PART_NO,
|
||||
P.part_name AS PART_NAME,
|
||||
P.spec AS SPEC,
|
||||
P.maker AS MAKER,
|
||||
(SELECT code_name FROM comm_code WHERE code_id = P.part_type) AS PART_TYPE_NAME,
|
||||
P.part_type AS PART_TYPE,
|
||||
P.material AS MATERIAL,
|
||||
IM.cls_cd AS CLS_CD,
|
||||
IM.cau_cd AS CAU_CD,
|
||||
IM.qty AS QTY,
|
||||
IM.location AS LOCATION,
|
||||
IM.reg_date AS REG_DATE,
|
||||
IM.price AS PRICE,
|
||||
IM.writer AS WRITER,
|
||||
(SELECT user_name FROM user_info WHERE user_id = IM.writer) AS WRITER_NAME,
|
||||
IM.contract_objid AS CONTRACT_OBJID,
|
||||
IM.unit AS UNIT
|
||||
FROM inventory_mgmt IM
|
||||
INNER JOIN part_mng P ON P.objid::varchar = IM.part_objid
|
||||
WHERE ${where}
|
||||
) IM
|
||||
ORDER BY IM.REG_DATE DESC NULLS LAST
|
||||
`,
|
||||
params,
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId, getDate } from "@/lib/utils";
|
||||
|
||||
// 자재등록 (saveinventoryForm 대응)
|
||||
// 원본 쿼리: inventoryMng.xml line 595
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const objId = body.objId || body.objid || createObjectId();
|
||||
|
||||
try {
|
||||
// inventory_mgmt PK = (contract_objid, unit, part_objid) 복합키
|
||||
await execute(
|
||||
`INSERT INTO inventory_mgmt
|
||||
(objid, contract_objid, unit, part_objid, cls_cd, cau_cd, qty,
|
||||
location, sub_location, reg_date, price, writer)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
ON CONFLICT (contract_objid, unit, part_objid) DO UPDATE SET
|
||||
cls_cd = EXCLUDED.cls_cd,
|
||||
cau_cd = EXCLUDED.cau_cd,
|
||||
qty = EXCLUDED.qty,
|
||||
location = EXCLUDED.location,
|
||||
sub_location = EXCLUDED.sub_location,
|
||||
price = EXCLUDED.price,
|
||||
writer = EXCLUDED.writer`,
|
||||
[
|
||||
objId,
|
||||
body.contract_objid || body.project_no || "",
|
||||
body.unit || "",
|
||||
body.part_objid || body.part_no || "",
|
||||
body.cls_cd || "",
|
||||
body.cau_cd || "",
|
||||
body.qty || "0",
|
||||
body.location || "",
|
||||
body.sub_location || "",
|
||||
body.reg_date || getDate(),
|
||||
body.price || "0",
|
||||
user.userId,
|
||||
],
|
||||
);
|
||||
return NextResponse.json({ success: true, message: "저장되었습니다." });
|
||||
} catch (e) {
|
||||
console.error("inventory list save:", e);
|
||||
return NextResponse.json({ success: false, message: "오류가 발생하였습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 자재이동 저장 (saveInventoryMove 대응)
|
||||
// 원본: InventoryMngController.saveInventoryMove → mergeInventoryMoveInfo + saveInventoryMoveInInfo
|
||||
// 각 라인별로:
|
||||
// 1) 기존 inventory_mgmt_in 행의 move_objid=신규OBJID, move_qty=이동수량 업데이트
|
||||
// 2) 신규 inventory_mgmt_in 행 INSERT (이동된 재고, move_date/move_user 포함)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const lines = Array.isArray(body.lines) ? body.lines : [];
|
||||
if (lines.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "이동수량을 입력해주세요." });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
for (const line of lines) {
|
||||
const moveObjId = createObjectId();
|
||||
|
||||
// 1. 원본 inventory_mgmt_in 행의 move_objid/move_qty 갱신
|
||||
await client.query(
|
||||
`UPDATE inventory_mgmt_in
|
||||
SET move_objid = $1::varchar,
|
||||
move_qty = $2
|
||||
WHERE objid = $3`,
|
||||
[moveObjId, line.MOVE_QTY, line.IN_OBJID],
|
||||
);
|
||||
|
||||
// 2. 신규 inventory_mgmt_in 행 INSERT (같은 parent_objid 아래 이동처리된 행)
|
||||
// $1은 새 inventory_mgmt_in.objid + 원본 행 검색용 move_objid로 두 번 사용 → 명시 cast
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_in (
|
||||
objid, parent_objid, receipt_qty, location, sub_location,
|
||||
writer, regdate, purchase_order_master_objid, contract_mgmt_objid,
|
||||
purchase_order_sub_objid, move_date, move_user
|
||||
)
|
||||
SELECT $1::varchar, parent_objid, $2, $3, $4, $5, now(),
|
||||
purchase_order_master_objid, contract_mgmt_objid, purchase_order_sub_objid,
|
||||
$6, $7
|
||||
FROM inventory_mgmt_in
|
||||
WHERE move_objid = $8::varchar`,
|
||||
[
|
||||
moveObjId,
|
||||
line.MOVE_QTY,
|
||||
line.LOCATION || "",
|
||||
line.SUB_LOCATION || "",
|
||||
user.userId,
|
||||
line.MOVE_DATE || "",
|
||||
line.MOVE_USER || "",
|
||||
moveObjId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "저장되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("inventory move save:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "오류가 발생하였습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { getDate } from "@/lib/utils";
|
||||
|
||||
// 자재불출 완료 (acceptInventoryRequestInfo 대응)
|
||||
// 원본: InventoryMngController.acceptInventoryRequestInfo, inventoryMng.xml line 2191, 2200, 2073
|
||||
// 1) transfer(인계) 라인별 OUT_QTY/OUT_DATE/ACQ_USER 업데이트 (lines 파라미터 활용)
|
||||
// 2) mergeAcceptInventoryRequestInfo: master의 outstatus='complete', request_date 업데이트
|
||||
// 3) mergeAcceptInventoryRequestPartInfo: 모든 OUT 라인의 out_date 일괄 업데이트
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const masterObjId = String(body.INVENTORY_REQUEST_MASTER_OBJID || "");
|
||||
if (!masterObjId) {
|
||||
return NextResponse.json({ success: false, message: "마스터 OBJID 누락" }, { status: 400 });
|
||||
}
|
||||
const lines: Array<Record<string, unknown>> = Array.isArray(body.lines) ? body.lines : [];
|
||||
const requestDate = String(body.REQUEST_DATE || getDate());
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. 라인별 OUT_QTY/OUT_DATE/ACQ_USER 업데이트
|
||||
for (const line of lines) {
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_out
|
||||
(objid, parent_objid, request_qty, writer, regdate,
|
||||
inventory_request_master_objid)
|
||||
VALUES ($1, $2, $3, $4, NOW(), $5)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
out_qty = $6,
|
||||
out_date = $7,
|
||||
acq_user = $8,
|
||||
writer = $4`,
|
||||
[
|
||||
String(line.OBJID || ""),
|
||||
String(line.PARENT_OBJID || ""),
|
||||
String(line.REQUEST_QTY || "0"),
|
||||
user.userId,
|
||||
masterObjId,
|
||||
String(line.OUT_QTY || "0"),
|
||||
String(line.OUT_DATE || requestDate),
|
||||
String(line.ACQ_USER || ""),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 마스터 불출완료 상태
|
||||
await client.query(
|
||||
`UPDATE inventory_mgmt_out_master
|
||||
SET outstatus = $1,
|
||||
request_date = $2
|
||||
WHERE objid = $3`,
|
||||
["complete", requestDate, masterObjId],
|
||||
);
|
||||
|
||||
// 3. 상세 out_date 일괄
|
||||
await client.query(
|
||||
`UPDATE inventory_mgmt_out
|
||||
SET out_date = $1
|
||||
WHERE inventory_request_master_objid = $2`,
|
||||
[requestDate, masterObjId],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "불출완료되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("inventory request accept:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "오류가 발생하였습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 불출의뢰서 삭제 (inventory_mgmt_out_master + inventory_mgmt_out)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objIds } = await request.json();
|
||||
if (!objIds?.length) return NextResponse.json({ success: false, message: "삭제할 항목을 선택하세요." });
|
||||
try {
|
||||
const ph = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
await execute(`DELETE FROM inventory_mgmt_out WHERE inventory_request_master_objid IN (${ph})`, objIds);
|
||||
await execute(`DELETE FROM inventory_mgmt_out_master WHERE objid IN (${ph})`, objIds);
|
||||
return NextResponse.json({ success: true, message: `${objIds.length}건이 삭제되었습니다.` });
|
||||
} catch (error) {
|
||||
console.error("Inventory request delete:", error);
|
||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 설계수량 조회 (inventoryMng.getPartQTYInfoList 대응)
|
||||
// 원본: inventoryMng.xml line 1739
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const partObjId = String(body.part_objid || body.PART_OBJID || "");
|
||||
const projectObjId = String(body.project_objid || body.PROJECT_OBJID || "");
|
||||
const unitCode = String(body.unit_code || body.UNIT_CODE || "");
|
||||
if (!partObjId || !projectObjId || !unitCode) {
|
||||
return NextResponse.json({ RESULTLIST: [] });
|
||||
}
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
T.*,
|
||||
(COALESCE(NULLIF(T."DESIGN_QTY1", '')::numeric, 0) * COALESCE(T."QTY1", 0)
|
||||
* COALESCE(NULLIF(T."QTY3", '')::numeric, 1)) AS "DESIGN_QTY"
|
||||
FROM (
|
||||
SELECT
|
||||
BPQ.bom_report_objid AS "BOM_REPORT_OBJID",
|
||||
BPQ.qty AS "DESIGN_QTY1",
|
||||
(SELECT MAX(COALESCE(NULLIF(BPQ.qty, '')::numeric, 0))
|
||||
FROM bom_part_qty BPQ
|
||||
WHERE BPQ.bom_report_objid = PBR.objid AND COALESCE(BPQ.parent_part_no, '') = '') AS "QTY1",
|
||||
(SELECT SUM(COALESCE(NULLIF(BPQ3.qty, '')::numeric, 0))::text
|
||||
FROM bom_part_qty BPQ3
|
||||
WHERE BPQ3.bom_report_objid = PBR.objid AND BPQ3.child_objid = BPQ.parent_objid) AS "QTY3",
|
||||
PM.objid AS "PART_OBJID",
|
||||
PM.part_no AS "PART_NO",
|
||||
PM.part_name AS "PART_NAME",
|
||||
PBR.contract_objid AS "CONTRACT_OBJID",
|
||||
PBR.unit_code AS "UNIT_CODE",
|
||||
COALESCE((SELECT SUM(CASE WHEN (out_qty IS NULL OR out_qty = '') THEN 0
|
||||
ELSE out_qty::numeric END)
|
||||
FROM inventory_mgmt_out O, inventory_mgmt S_IM
|
||||
WHERE S_IM.part_objid = PM.objid
|
||||
AND O.parent_objid = S_IM.objid
|
||||
AND O.contract_mgmt_objid = PBR.contract_objid
|
||||
AND O.unit = PBR.unit_code), 0) AS "OUT_QTY"
|
||||
FROM bom_part_qty BPQ
|
||||
LEFT OUTER JOIN part_mng PM ON PM.objid = BPQ.part_no
|
||||
LEFT OUTER JOIN part_bom_report PBR ON PBR.objid = BPQ.bom_report_objid
|
||||
WHERE BPQ.last_part_objid = $1
|
||||
AND PBR.contract_objid = $2
|
||||
AND PBR.unit_code = $3
|
||||
AND BPQ.status = 'deploy'
|
||||
) T
|
||||
`,
|
||||
[partObjId, projectObjId, unitCode],
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows });
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 불출의뢰서 상세 (inventoryMng.getInventoryRequestMasterInfo + getInventoryMngRequestDetailList 대응)
|
||||
// 원본: inventoryMng.xml line 1946, 2110
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const masterObjId = String(body.INVENTORY_REQUEST_MASTER_OBJID || body.objId || "");
|
||||
if (!masterObjId) {
|
||||
return NextResponse.json({ success: false, message: "OBJID 누락" }, { status: 400 });
|
||||
}
|
||||
|
||||
const master = await queryOne(
|
||||
`SELECT
|
||||
IM.objid::text AS "OBJID",
|
||||
IM.parent_objid AS "PARENT_OBJID",
|
||||
IM.inventory_out_no AS "INVENTORY_OUT_NO",
|
||||
IM.request_date AS "REQUEST_DATE",
|
||||
IM.request_id AS "REQUEST_ID",
|
||||
IM.reception_status AS "RECEPTION_STATUS",
|
||||
IM.reception_id AS "RECEPTION_ID",
|
||||
IM.reception_date AS "RECEPTION_DATE",
|
||||
IM.outstatus AS "OUTSTATUS",
|
||||
IM.writer AS "WRITER",
|
||||
IM.regdate AS "REGDATE",
|
||||
IM.remark AS "REMARK",
|
||||
IM.contract_mgmt_objid AS "CONTRACT_MGMT_OBJID",
|
||||
IM.sign AS "SIGN",
|
||||
(SELECT project_no FROM project_mgmt P WHERE P.objid = IM.contract_mgmt_objid) AS "PROJECT_NO"
|
||||
FROM inventory_mgmt_out_master IM
|
||||
WHERE IM.objid = $1`,
|
||||
[masterObjId],
|
||||
);
|
||||
|
||||
const details = await queryRows(
|
||||
`SELECT
|
||||
IMO.objid::text AS "OBJID",
|
||||
IMO.parent_objid AS "PARENT_OBJID",
|
||||
IMO.request_qty AS "REQUEST_QTY",
|
||||
IMO.out_qty AS "OUT_QTY",
|
||||
IMO.out_date AS "OUT_DATE",
|
||||
IMO.writer AS "WRITER",
|
||||
IMO.acq_user AS "ACQ_USER",
|
||||
IMO.inventory_request_master_objid AS "INVENTORY_REQUEST_MASTER_OBJID",
|
||||
IMO.sign AS "SIGN_ROW",
|
||||
IMO.contract_mgmt_objid AS "CONTRACT_MGMT_OBJID",
|
||||
IMO.unit AS "UNIT",
|
||||
(SELECT project_no FROM project_mgmt PM WHERE PM.objid = IMO.contract_mgmt_objid) AS "PROJECT_NO",
|
||||
(SELECT O.unit_no || '-' || O.task_name FROM pms_wbs_task O WHERE O.objid = IMO.unit) AS "UNIT_NAME",
|
||||
(SELECT part_no FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "PART_NO",
|
||||
(SELECT part_name FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "PART_NAME",
|
||||
(SELECT spec FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "SPEC",
|
||||
(SELECT maker FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "MAKER",
|
||||
(SELECT (SELECT code_name FROM comm_code CC WHERE CC.code_id = part_type)
|
||||
FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "PART_TYPE_NAME",
|
||||
(SELECT material FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "MATERIAL",
|
||||
(SELECT code_name FROM comm_code O
|
||||
WHERE O.code_id = COALESCE(NULLIF(IMI.location, ''), IM.location)) AS "LOCATION_NAME",
|
||||
(SELECT code_name FROM comm_code O
|
||||
WHERE O.code_id = COALESCE(NULLIF(IMI.sub_location, ''), IM.sub_location)) AS "SUB_LOCATION_NAME",
|
||||
(SELECT sign FROM inventory_mgmt_out_master O WHERE O.objid = IMO.inventory_request_master_objid) AS "SIGN",
|
||||
(SELECT user_name FROM user_info UI
|
||||
WHERE UI.user_id = (
|
||||
SELECT writer FROM inventory_mgmt_in IMI2
|
||||
WHERE IMI2.parent_objid = IMO.parent_objid
|
||||
ORDER BY regdate DESC LIMIT 1
|
||||
)) AS "RECEIVER_NAME",
|
||||
(SELECT user_name FROM user_info UI WHERE UI.user_id = IMO.acq_user) AS "ACQ_USER_NAME",
|
||||
IMI.objid AS "IN_OBJID"
|
||||
FROM inventory_mgmt_out IMO
|
||||
LEFT OUTER JOIN inventory_mgmt IM ON IM.objid = IMO.parent_objid
|
||||
LEFT OUTER JOIN inventory_mgmt_in IMI
|
||||
ON IMO.objid = ANY(STRING_TO_ARRAY(IMI.out_objid, ','))
|
||||
WHERE IMO.inventory_request_master_objid = $1
|
||||
ORDER BY IMO.regdate`,
|
||||
[masterObjId],
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true, master, details });
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 입출고 History 팝업 (getInventoryRequestHistoryList 대응)
|
||||
// 원본: inventoryMng.xml line 2142
|
||||
// 입고(inventory_mgmt_in, move_date 없음) + 출고(inventory_mgmt_out) + 이동(inventory_mgmt_in, move_date 있음)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const objId = String(body.objId || body.OBJID || "");
|
||||
if (!objId) return NextResponse.json({ RESULTLIST: [] });
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
(SELECT MAX(parent_objid) FROM arrival_plan O
|
||||
WHERE O.part_objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "OBJID",
|
||||
(SELECT part_no FROM part_mng O
|
||||
WHERE O.objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "PART_NO",
|
||||
(SELECT part_name FROM part_mng O
|
||||
WHERE O.objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "PART_NAME",
|
||||
'입고' AS "GUBUN",
|
||||
T.receipt_qty AS "RECEIPT_QTY",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = T.location) AS "LOCATION_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = T.sub_location) AS "SUB_LOCATION_NAME",
|
||||
(SELECT project_no FROM project_mgmt PM WHERE PM.objid = T.contract_mgmt_objid) AS "PROJECT_NO"
|
||||
FROM inventory_mgmt_in T
|
||||
WHERE T.parent_objid = $1
|
||||
AND (T.move_date IS NULL OR T.move_date = '')
|
||||
UNION ALL
|
||||
SELECT
|
||||
T2.objid AS "OBJID",
|
||||
(SELECT part_no FROM part_mng O
|
||||
WHERE O.objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "PART_NO",
|
||||
(SELECT part_name FROM part_mng O
|
||||
WHERE O.objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "PART_NAME",
|
||||
'출고' AS "GUBUN",
|
||||
T.out_qty AS "RECEIPT_QTY",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = T1.location) AS "LOCATION_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = T1.sub_location) AS "SUB_LOCATION_NAME",
|
||||
(SELECT project_no FROM project_mgmt PM WHERE PM.objid = T.contract_mgmt_objid) AS "PROJECT_NO"
|
||||
FROM inventory_mgmt_out T
|
||||
LEFT JOIN inventory_mgmt T1 ON T.parent_objid = T1.objid
|
||||
LEFT JOIN inventory_mgmt_out_master T2 ON T.inventory_request_master_objid = T2.objid
|
||||
WHERE T.parent_objid = $1
|
||||
UNION ALL
|
||||
SELECT
|
||||
(SELECT MAX(parent_objid) FROM arrival_plan O
|
||||
WHERE O.part_objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "OBJID",
|
||||
(SELECT part_no FROM part_mng O
|
||||
WHERE O.objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "PART_NO",
|
||||
(SELECT part_name FROM part_mng O
|
||||
WHERE O.objid = (SELECT part_objid FROM inventory_mgmt O1 WHERE O1.objid = T.parent_objid)) AS "PART_NAME",
|
||||
'이동' AS "GUBUN",
|
||||
T.receipt_qty AS "RECEIPT_QTY",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = T.location) AS "LOCATION_NAME",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = T.sub_location) AS "SUB_LOCATION_NAME",
|
||||
(SELECT project_no FROM project_mgmt PM WHERE PM.objid = T.contract_mgmt_objid) AS "PROJECT_NO"
|
||||
FROM inventory_mgmt_in T
|
||||
WHERE T.parent_objid = $1
|
||||
AND T.move_date IS NOT NULL
|
||||
AND T.move_date != ''
|
||||
`,
|
||||
[objId],
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows });
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 불출의뢰/자재이동 팝업에서 선택된 inventory_mgmt OBJID들의 상세정보
|
||||
// 원본: inventoryMng.getInventoryMngRequestList (inventoryMng.xml line 1665)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const checkArrRaw = body.checkArr;
|
||||
const arr: string[] = Array.isArray(checkArrRaw)
|
||||
? checkArrRaw.map(String)
|
||||
: String(checkArrRaw || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (arr.length === 0) {
|
||||
return NextResponse.json({ RESULTLIST: [] });
|
||||
}
|
||||
|
||||
const placeholders = arr.map((_, i) => `$${i + 1}`).join(",");
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
TBL.*,
|
||||
(COALESCE(TBL."DESIGN_QTY1", 0) * COALESCE(TBL."QTY1", 0)
|
||||
* COALESCE(NULLIF(TBL."QTY3", '')::numeric, 1)) AS "DESIGN_QTY"
|
||||
FROM (
|
||||
SELECT
|
||||
IM.objid::text AS "OBJID",
|
||||
IM.part_objid AS "PART_OBJID",
|
||||
(SELECT project_no FROM project_mgmt O WHERE O.objid = IM.contract_objid) AS "PROJECT_NO",
|
||||
CASE WHEN IMI.receipt_qty IS NULL OR IMI.receipt_qty = '' THEN 0
|
||||
ELSE (IMI.receipt_qty::numeric - COALESCE(NULLIF(IMI.out_qty, '')::numeric, 0) - COALESCE(NULLIF(IMI.move_qty, '')::numeric, 0))
|
||||
END AS "USE_CNT",
|
||||
(SELECT SUM(CASE WHEN (receipt_qty IS NULL OR receipt_qty = '') THEN 0 ELSE receipt_qty::numeric END)
|
||||
FROM inventory_mgmt_in O WHERE O.parent_objid = IM.objid) AS "QTY",
|
||||
(SELECT O.unit_no || '-' || O.task_name FROM pms_wbs_task O WHERE O.objid = IM.unit) AS "UNIT_NAME",
|
||||
(SELECT part_no FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "PART_NO",
|
||||
(SELECT part_name FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "PART_NAME",
|
||||
(SELECT spec FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "SPEC",
|
||||
(SELECT maker FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "MAKER",
|
||||
(SELECT (SELECT code_name FROM comm_code CC WHERE CC.code_id = part_type)
|
||||
FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "PART_TYPE_NAME",
|
||||
(SELECT part_type FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "PART_TYPE",
|
||||
(SELECT code_name FROM comm_code O WHERE O.code_id = IM.cls_cd) AS "CLS_CD_NAME",
|
||||
IM.cls_cd AS "CLS_CD",
|
||||
(SELECT code_name FROM comm_code O WHERE O.code_id = IM.cau_cd) AS "CAU_CD_NAME",
|
||||
IM.cau_cd AS "CAU_CD",
|
||||
(SELECT material FROM part_mng O WHERE O.objid::varchar = IM.part_objid) AS "MATERIAL",
|
||||
IMI.location AS "LOCATION",
|
||||
(SELECT code_name FROM comm_code O WHERE O.code_id = IMI.location) AS "LOCATION_NAME",
|
||||
IMI.sub_location AS "SUB_LOCATION",
|
||||
(SELECT code_name FROM comm_code O WHERE O.code_id = IMI.sub_location) AS "SUB_LOCATION_NAME",
|
||||
IM.reg_date AS "REG_DATE",
|
||||
IM.price AS "PRICE",
|
||||
(SELECT user_name FROM user_info O WHERE O.user_id = IM.writer) AS "WRITER_NAME",
|
||||
IM.contract_objid AS "CONTRACT_OBJID",
|
||||
IM.unit AS "UNIT",
|
||||
IMI.objid AS "IN_OBJID",
|
||||
PBR.objid AS "BOM_REPORT_OBJID",
|
||||
(SELECT SUM(COALESCE(NULLIF(BPQ.qty, '')::numeric, 0))
|
||||
FROM bom_part_qty BPQ
|
||||
WHERE BPQ.bom_report_objid = PBR.objid
|
||||
AND BPQ.last_part_objid = IM.part_objid
|
||||
AND BPQ.status IN ('beforeEdit', 'editing', 'deleting', 'deploy')) AS "DESIGN_QTY1",
|
||||
(SELECT MAX(COALESCE(NULLIF(BPQ.qty, '')::numeric, 0))
|
||||
FROM bom_part_qty BPQ
|
||||
WHERE BPQ.bom_report_objid = PBR.objid
|
||||
AND COALESCE(BPQ.parent_part_no, '') = ''
|
||||
AND BPQ.status IN ('beforeEdit', 'editing', 'deleting', 'deploy')) AS "QTY1",
|
||||
(SELECT SUM(COALESCE(NULLIF(BPQ3.qty, '')::numeric, 0))::text
|
||||
FROM bom_part_qty BPQ3
|
||||
WHERE BPQ3.bom_report_objid = PBR.objid
|
||||
AND BPQ3.child_objid = (
|
||||
SELECT parent_objid FROM bom_part_qty BPQ
|
||||
WHERE BPQ.bom_report_objid = PBR.objid
|
||||
AND BPQ.last_part_objid = IM.part_objid
|
||||
AND BPQ.status IN ('beforeEdit', 'editing', 'deleting', 'deploy')
|
||||
LIMIT 1
|
||||
)) AS "QTY3",
|
||||
(SELECT SUM(COALESCE(NULLIF(pre_booking_qty, '')::numeric, 0))
|
||||
FROM sales_bom_report_part SBRP
|
||||
WHERE SBRP.parent_objid = PBR.objid AND SBRP.part_objid = IM.part_objid) AS "PRE_BOOKING_QTY",
|
||||
IMI.out_qty AS "OUT_QTY"
|
||||
FROM inventory_mgmt IM
|
||||
LEFT OUTER JOIN inventory_mgmt_in IMI ON IMI.parent_objid = IM.objid
|
||||
LEFT OUTER JOIN part_bom_report PBR
|
||||
ON PBR.contract_objid = IM.contract_objid AND PBR.unit_code = IM.unit
|
||||
WHERE IM.objid IN (${placeholders})
|
||||
) TBL
|
||||
WHERE COALESCE("USE_CNT", 0) != 0
|
||||
ORDER BY "PART_NO"
|
||||
`,
|
||||
arr,
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows });
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { getDate } from "@/lib/utils";
|
||||
|
||||
// 불출의뢰서 접수 (receiptInventoryRequestInfo / mergeReceiptInventoryRequestInfo 대응)
|
||||
// 원본: InventoryMngController.receiptInventoryRequestInfo, inventoryMng.xml line 2073
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const checkArr: string[] = Array.isArray(body.checkArr)
|
||||
? body.checkArr.map(String)
|
||||
: String(body.checkArr || "")
|
||||
.split(",")
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (checkArr.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "선택된 데이터가 없습니다." });
|
||||
}
|
||||
|
||||
try {
|
||||
for (const objId of checkArr) {
|
||||
await execute(
|
||||
`UPDATE inventory_mgmt_out_master
|
||||
SET reception_status = $1,
|
||||
reception_date = $2,
|
||||
reception_id = $3
|
||||
WHERE objid = $4`,
|
||||
["reception", getDate(), user.userId, objId],
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true, message: "접수되었습니다." });
|
||||
} catch (e) {
|
||||
console.error("inventory request receipt:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "오류가 발생하였습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 자재관리 > 불출의뢰서 (inventoryMng.materialRequesrGridtList 대응)
|
||||
// 원본: /Users/jhj/FITO/src/com/pms/mapper/inventoryMng.xml (line 1957)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// 내부 서브쿼리 조건 (INVENTORY_MGMT_OUT_MASTER 기준)
|
||||
const innerConds: string[] = ["1=1"];
|
||||
const innerParams: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.Year) {
|
||||
innerConds.push(`TO_CHAR(IM.regdate, 'YYYY') = $${idx++}`);
|
||||
innerParams.push(body.Year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
innerConds.push(`IM.contract_mgmt_objid = $${idx++}`);
|
||||
innerParams.push(body.project_no);
|
||||
}
|
||||
if (body.request_user) {
|
||||
innerConds.push(`IM.writer = $${idx++}`);
|
||||
innerParams.push(body.request_user);
|
||||
}
|
||||
if (body.reception_user) {
|
||||
innerConds.push(`IM.reception_id = $${idx++}`);
|
||||
innerParams.push(body.reception_user);
|
||||
}
|
||||
if (body.reception_status) {
|
||||
innerConds.push(`IM.reception_status = $${idx++}`);
|
||||
innerParams.push(body.reception_status);
|
||||
}
|
||||
if (body.out_status) {
|
||||
innerConds.push(
|
||||
`(CASE WHEN IM.outstatus = 'complete' THEN 'complete' ELSE 'NG' END) = $${idx++}`,
|
||||
);
|
||||
innerParams.push(body.out_status);
|
||||
}
|
||||
if (body.request_start_date) {
|
||||
innerConds.push(`TO_DATE(IM.request_date, 'YYYY-MM-DD') >= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
|
||||
innerParams.push(body.request_start_date);
|
||||
}
|
||||
if (body.request_end_date) {
|
||||
innerConds.push(`TO_DATE(IM.request_date, 'YYYY-MM-DD') <= TO_DATE($${idx++}, 'YYYY-MM-DD')`);
|
||||
innerParams.push(body.request_end_date);
|
||||
}
|
||||
if (body.reception_start_date) {
|
||||
innerConds.push(
|
||||
`TO_DATE(IM.reception_date, 'YYYY-MM-DD') >= TO_DATE($${idx++}, 'YYYY-MM-DD')`,
|
||||
);
|
||||
innerParams.push(body.reception_start_date);
|
||||
}
|
||||
if (body.reception_end_date) {
|
||||
innerConds.push(
|
||||
`TO_DATE(IM.reception_date, 'YYYY-MM-DD') <= TO_DATE($${idx++}, 'YYYY-MM-DD')`,
|
||||
);
|
||||
innerParams.push(body.reception_end_date);
|
||||
}
|
||||
|
||||
// 외부 조건 (PART_NO_ARR/PART_NAME_ARR — 서브쿼리 결과에 적용)
|
||||
const outerConds: string[] = ["1=1"];
|
||||
const outerParams: unknown[] = [];
|
||||
|
||||
if (body.part_no) {
|
||||
outerConds.push(`UPPER(PART_NO_ARR) LIKE '%' || TRIM(UPPER($${idx++})) || '%'`);
|
||||
outerParams.push(body.part_no);
|
||||
}
|
||||
if (body.part_name) {
|
||||
outerConds.push(`UPPER(PART_NAME_ARR) LIKE '%' || TRIM(UPPER($${idx++})) || '%'`);
|
||||
outerParams.push(body.part_name);
|
||||
}
|
||||
|
||||
const params = [...innerParams, ...outerParams];
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT IM.*
|
||||
FROM (
|
||||
SELECT
|
||||
IM.objid::text AS "OBJID",
|
||||
(SELECT STRING_AGG(part_no, ',') FROM part_mng O
|
||||
WHERE O.objid IN (
|
||||
SELECT part_objid FROM inventory_mgmt O1
|
||||
WHERE O1.objid IN (
|
||||
SELECT parent_objid FROM inventory_mgmt_out O2
|
||||
WHERE O2.inventory_request_master_objid = IM.objid
|
||||
)
|
||||
)) AS "PART_NO_ARR",
|
||||
(SELECT STRING_AGG(part_name, ',') FROM part_mng O
|
||||
WHERE O.objid IN (
|
||||
SELECT part_objid FROM inventory_mgmt O1
|
||||
WHERE O1.objid IN (
|
||||
SELECT parent_objid FROM inventory_mgmt_out O2
|
||||
WHERE O2.inventory_request_master_objid = IM.objid
|
||||
)
|
||||
)) AS "PART_NAME_ARR",
|
||||
IM.parent_objid AS "PARENT_OBJID",
|
||||
IM.inventory_out_no AS "INVENTORY_OUT_NO",
|
||||
IM.request_date AS "REQUEST_DATE",
|
||||
IM.request_id AS "REQUEST_ID",
|
||||
IM.reception_status AS "RECEPTION_STATUS",
|
||||
CASE WHEN IM.reception_status = 'reception' THEN '접수' ELSE '미접수' END
|
||||
AS "RECEPTION_STATUS_TITLE",
|
||||
IM.reception_id AS "RECEPTION_ID",
|
||||
(SELECT user_name FROM user_info O WHERE O.user_id = IM.reception_id)
|
||||
AS "RECEPTION_USER_NAME",
|
||||
IM.reception_date AS "RECEPTION_DATE",
|
||||
IM.outstatus AS "OUTSTATUS",
|
||||
CASE WHEN IM.outstatus = 'complete' THEN '완료' ELSE '미완료' END
|
||||
AS "OUTSTATUS_TITLE",
|
||||
IM.writer AS "WRITER",
|
||||
(SELECT user_name FROM user_info O WHERE O.user_id = IM.writer) AS "REQUEST_USER_NAME",
|
||||
IM.regdate AS "REGDATE",
|
||||
TO_CHAR(IM.regdate, 'YYYY-MM-DD') AS "REG_DATE",
|
||||
IMO.request_qty AS "REQUEST_QTY",
|
||||
IMO.out_qty AS "OUT_QTY",
|
||||
IMO.inventory_request_master_objid AS "INVENTORY_REQUEST_MASTER_OBJID"
|
||||
FROM inventory_mgmt_out_master IM
|
||||
LEFT OUTER JOIN (
|
||||
SELECT
|
||||
inventory_request_master_objid,
|
||||
SUM(COALESCE(NULLIF(request_qty, '')::numeric, 0)) AS request_qty,
|
||||
SUM(COALESCE(NULLIF(out_qty, '')::numeric, 0)) AS out_qty
|
||||
FROM inventory_mgmt_out
|
||||
GROUP BY inventory_request_master_objid
|
||||
) IMO ON IMO.inventory_request_master_objid = IM.objid
|
||||
WHERE ${innerConds.join(" AND ")}
|
||||
) IM
|
||||
WHERE ${outerConds.join(" AND ")}
|
||||
ORDER BY "REGDATE" DESC
|
||||
`,
|
||||
params,
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { getDate } from "@/lib/utils";
|
||||
|
||||
// 불출의뢰서 저장 (inventoryMng.saveInventoryRequest + mergeInventoryRequestMasterInfo 대응)
|
||||
// 원본: inventoryMng.xml line 1781, 1902
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const masterObjId = String(body.INVENTORY_REQUEST_MASTER_OBJID || "");
|
||||
if (!masterObjId) {
|
||||
return NextResponse.json({ success: false, message: "마스터 OBJID 누락" }, { status: 400 });
|
||||
}
|
||||
const remark = String(body.REMARK || "");
|
||||
const requestDate = String(body.REQUEST_DATE || getDate());
|
||||
const lines: Array<Record<string, unknown>> = Array.isArray(body.lines) ? body.lines : [];
|
||||
|
||||
// 첫 라인의 contract_mgmt_objid를 마스터 contract_mgmt_objid로 사용
|
||||
const firstContractObjId = String(lines[0]?.CONTRACT_MGMT_OBJID || "");
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. inventory_mgmt_out_master UPSERT
|
||||
// INVENTORY_OUT_NO 자동 생성: Rfw-YYYY-N (연도별 증가, 패딩 없음 — 원본 FITO 포맷)
|
||||
// 원본: request_id와 writer 모두 current user id. $5를 별도 파라미터로 전달해 타입 추론 충돌 회피
|
||||
const year = new Date().getFullYear();
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_out_master
|
||||
(objid, parent_objid, inventory_out_no, request_date, request_id,
|
||||
writer, regdate, remark, contract_mgmt_objid)
|
||||
VALUES (
|
||||
$1, $2,
|
||||
(SELECT 'Rfw-' || $3 || '-' ||
|
||||
(COALESCE(MAX(CASE WHEN SPLIT_PART(inventory_out_no, '-', 3) = ''
|
||||
OR inventory_out_no IS NULL
|
||||
THEN 0
|
||||
ELSE SPLIT_PART(inventory_out_no, '-', 3)::numeric
|
||||
END), 0)::integer + 1)::text
|
||||
FROM inventory_mgmt_out_master),
|
||||
$4, $5, $6, now(), $7, $8
|
||||
)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
remark = EXCLUDED.remark`,
|
||||
[
|
||||
masterObjId,
|
||||
String(body.PARENT_OBJID || ""),
|
||||
String(year),
|
||||
requestDate,
|
||||
user.userId,
|
||||
user.userId,
|
||||
remark,
|
||||
firstContractObjId,
|
||||
],
|
||||
);
|
||||
|
||||
// 2. 기존 파트 삭제 (initInventoryRequestPart)
|
||||
await client.query(
|
||||
`DELETE FROM inventory_mgmt_out WHERE inventory_request_master_objid = $1`,
|
||||
[masterObjId],
|
||||
);
|
||||
|
||||
// 3. 라인 INSERT
|
||||
for (const line of lines) {
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_out
|
||||
(objid, parent_objid, request_qty, writer, regdate,
|
||||
inventory_request_master_objid, contract_mgmt_objid, unit)
|
||||
VALUES ($1, $2, $3, $4, now(), $5, $6, $7)`,
|
||||
[
|
||||
String(line.OBJID || ""),
|
||||
String(line.PARENT_OBJID || ""),
|
||||
String(line.REQUEST_QTY || "0"),
|
||||
user.userId,
|
||||
masterObjId,
|
||||
String(line.CONTRACT_MGMT_OBJID || ""),
|
||||
String(line.UNIT || ""),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "저장되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("inventory request save:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "오류가 발생하였습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 사인 조회 (materialRequestDetailPopUpsign) / 저장 (savesignInventoryTransfer) / 삭제 (materialRequestDetailPopUpsigndelete)
|
||||
// 원본: inventoryMng.xml line 2082, 1883, 2093
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const action = String(body.action || "list");
|
||||
|
||||
if (action === "list") {
|
||||
// OBJID들의 사인 목록
|
||||
const checkArr: string[] = Array.isArray(body.checkArr)
|
||||
? body.checkArr.map(String)
|
||||
: String(body.checkArr || "")
|
||||
.split(",")
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (checkArr.length === 0) return NextResponse.json([]);
|
||||
const ph = checkArr.map((_, i) => `$${i + 1}`).join(",");
|
||||
const rows = await queryRows(
|
||||
`SELECT objid AS "OBJID", acq_user AS "ACQ_USER", sign AS "SIGN"
|
||||
FROM inventory_mgmt_out
|
||||
WHERE objid IN (${ph})`,
|
||||
checkArr,
|
||||
);
|
||||
return NextResponse.json(rows);
|
||||
}
|
||||
|
||||
if (action === "save") {
|
||||
// 단일 또는 다중 OBJID에 사인 저장
|
||||
const lines: Array<{ OBJID: string; ACQ_USER: string; SIGN: string }> = Array.isArray(body.lines)
|
||||
? body.lines
|
||||
: [];
|
||||
try {
|
||||
for (const line of lines) {
|
||||
await execute(
|
||||
`INSERT INTO inventory_mgmt_out (objid, acq_user, sign)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
acq_user = EXCLUDED.acq_user,
|
||||
sign = EXCLUDED.sign`,
|
||||
[line.OBJID, line.ACQ_USER || "", line.SIGN || ""],
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true, message: "사인이 등록 되었습니다." });
|
||||
} catch (e) {
|
||||
console.error("sign save:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "오류가 발생하였습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (action === "delete") {
|
||||
const objIds: string[] = Array.isArray(body.objIds) ? body.objIds.map(String) : [];
|
||||
try {
|
||||
for (const objId of objIds) {
|
||||
await execute(
|
||||
`INSERT INTO inventory_mgmt_out (objid, sign)
|
||||
VALUES ($1, '')
|
||||
ON CONFLICT (objid) DO UPDATE SET sign = ''`,
|
||||
[objId],
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true, message: "삭제되었습니다.", result: "true" });
|
||||
} catch (e) {
|
||||
console.error("sign delete:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "오류가 발생하였습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false, message: "지원하지 않는 action" }, { status: 400 });
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 불출의뢰서 인계 저장 (saveInventoryTransfer 대응)
|
||||
// 원본: InventoryMngController.saveInventoryTransfer, inventoryMng.xml line 1848
|
||||
// 각 라인의 OUT_QTY/OUT_DATE/ACQ_USER/WRITER 업데이트
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const lines: Array<Record<string, unknown>> = Array.isArray(body.lines) ? body.lines : [];
|
||||
if (lines.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "저장할 라인이 없습니다." });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
for (const line of lines) {
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_out
|
||||
(objid, parent_objid, request_qty, writer, regdate,
|
||||
inventory_request_master_objid, sign)
|
||||
VALUES ($1, $2, $3, $4, NOW(), $5, $6)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
out_qty = $7,
|
||||
out_date = $8,
|
||||
acq_user = $9,
|
||||
writer = $4`,
|
||||
[
|
||||
String(line.OBJID || ""),
|
||||
String(line.PARENT_OBJID || ""),
|
||||
String(line.REQUEST_QTY || "0"),
|
||||
user.userId,
|
||||
String(line.INVENTORY_REQUEST_MASTER_OBJID || ""),
|
||||
String(line.SIGN || ""),
|
||||
String(line.OUT_QTY || "0"),
|
||||
String(line.OUT_DATE || ""),
|
||||
String(line.ACQ_USER || ""),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "저장되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("inventory request transfer:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "오류가 발생하였습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// inventoryMng/inventoryMngInputGridList.do 대응
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`LEFT(IM.reg_date, 4) = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`CM.contract_no = $${idx++}`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.part_no) {
|
||||
conditions.push(`P.part_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.part_no);
|
||||
}
|
||||
if (body.part_name) {
|
||||
conditions.push(`P.part_name LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.part_name);
|
||||
}
|
||||
if (body.spec) {
|
||||
conditions.push(`P.spec LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.spec);
|
||||
}
|
||||
if (body.cls_cd) {
|
||||
conditions.push(`IM.cls_cd = $${idx++}`);
|
||||
params.push(body.cls_cd);
|
||||
}
|
||||
if (body.cau_cd) {
|
||||
conditions.push(`IM.cau_cd = $${idx++}`);
|
||||
params.push(body.cau_cd);
|
||||
}
|
||||
if (body.location) {
|
||||
conditions.push(`IM.location = $${idx++}`);
|
||||
params.push(body.location);
|
||||
}
|
||||
if (body.writer) {
|
||||
conditions.push(`IM.writer = $${idx++}`);
|
||||
params.push(body.writer);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT IM.objid::text AS "OBJID",
|
||||
CM.contract_no AS "PROJECT_NO",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = POM.unit_code LIMIT 1), IM.unit) AS "UNIT_NAME",
|
||||
P.part_no AS "PART_NO", P.part_name AS "PART_NAME", P.spec AS "SPEC", P.maker AS "MAKER",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = IM.cls_cd LIMIT 1), '') AS "CLS_CD_NAME",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = IM.cau_cd LIMIT 1), '') AS "CAU_CD_NAME",
|
||||
COALESCE(IM.qty::numeric, 0) AS "QTY",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = IM.location LIMIT 1), '') AS "LOCATION_NAME",
|
||||
COALESCE(IM.price::numeric, 0) AS "PRICE",
|
||||
IM.reg_date AS "REG_DATE",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = IM.writer LIMIT 1), '') AS "WRITER_NAME",
|
||||
-- 총입고수량 = SUM(inventory_mgmt_in.receipt_qty)
|
||||
COALESCE((SELECT SUM(COALESCE(NULLIF(IMI.receipt_qty,'')::numeric, 0))
|
||||
FROM inventory_mgmt_in IMI WHERE IMI.parent_objid = IM.objid), 0) AS "INPUT_QTY",
|
||||
-- 최종입고일
|
||||
COALESCE((SELECT MAX(TO_CHAR(IMI2.regdate, 'YYYY-MM-DD'))
|
||||
FROM inventory_mgmt_in IMI2 WHERE IMI2.parent_objid = IM.objid), '') AS "INPUT_DATE",
|
||||
-- 잔여수량 = qty - SUM(투입이력 input_qty)
|
||||
COALESCE(NULLIF(IM.qty,'')::numeric, 0)
|
||||
- COALESCE((SELECT SUM(COALESCE(IMH.input_qty::numeric, 0))
|
||||
FROM inventory_mgmt_history IMH WHERE IMH.parent_objid = IM.objid), 0) AS "REMAIN_QTY"
|
||||
FROM inventory_mgmt IM
|
||||
LEFT JOIN part_mng P ON P.objid = IM.part_objid
|
||||
LEFT JOIN contract_mgmt CM ON CM.objid::text = IM.contract_objid
|
||||
WHERE ${where}
|
||||
ORDER BY IM.reg_date DESC NULLS LAST
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 재고 입고 등록 (saveinventoryForm / insertInventoryIn 대응)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const body = await request.json();
|
||||
const objId = body.objId || createObjectId();
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. inventory_mgmt 마스터 UPSERT
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt (objid, contract_objid, unit, part_objid,
|
||||
cls_cd, cau_cd, qty, location, sub_location, price, writer, reg_date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now())
|
||||
ON CONFLICT (contract_objid, unit, part_objid) DO UPDATE SET
|
||||
cls_cd=EXCLUDED.cls_cd, cau_cd=EXCLUDED.cau_cd, qty=EXCLUDED.qty,
|
||||
location=EXCLUDED.location, sub_location=EXCLUDED.sub_location,
|
||||
price=EXCLUDED.price`,
|
||||
[objId, body.contract_objid||"", body.unit||"", body.part_objid||"",
|
||||
body.cls_cd||"", body.cau_cd||"", body.qty||"0",
|
||||
body.location||"", body.sub_location||"", body.price||"0", user.userId]
|
||||
);
|
||||
|
||||
// 2. inventory_mgmt_in 트랜잭션 생성 (입고수량 있을 때)
|
||||
if (body.receipt_qty && Number(body.receipt_qty) > 0) {
|
||||
await client.query(
|
||||
`INSERT INTO inventory_mgmt_in (
|
||||
objid, parent_objid, receipt_qty, location, sub_location,
|
||||
writer, regdate, contract_mgmt_objid
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, now(), $7)`,
|
||||
[createObjectId(), objId, String(body.receipt_qty),
|
||||
body.location||"", body.sub_location||"", user.userId,
|
||||
body.contract_mgmt_objid || body.contract_objid || ""]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, objId, message: "등록되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Inventory save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally { client.release(); }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 자재관리 > 현황 (inventoryMngDashList 대응)
|
||||
// 프로젝트별 입고/불출/잔여/이동 수량 집계
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`LEFT(IM.reg_date, 4) = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.project_no) {
|
||||
conditions.push(`CM.contract_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.project_no);
|
||||
}
|
||||
if (body.location_cd) {
|
||||
conditions.push(`EXISTS (SELECT 1 FROM inventory_mgmt_in I_F WHERE I_F.parent_objid = IM.objid AND I_F.location = $${idx++})`);
|
||||
params.push(body.location_cd);
|
||||
}
|
||||
if (body.part_type_cd) {
|
||||
conditions.push(`P.part_type = $${idx++}`);
|
||||
params.push(body.part_type_cd);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT CM.contract_no AS "PROJECT_NO",
|
||||
IM.unit AS "UNIT_NAME",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = P.part_type LIMIT 1), '') AS "PART_TYPE_NAME",
|
||||
COALESCE((SELECT ARRAY_TO_STRING(ARRAY_AGG(DISTINCT (SELECT code_name FROM comm_code WHERE code_id = I_L.location LIMIT 1)), ', ')
|
||||
FROM inventory_mgmt_in I_L WHERE I_L.parent_objid = IM.objid), '') AS "LOCATION_NAME",
|
||||
-- 총보유(입고) = SUM(receipt_qty - move_qty)
|
||||
COALESCE((SELECT SUM(CASE WHEN IMI.receipt_qty IS NULL OR IMI.receipt_qty = '' THEN 0
|
||||
ELSE IMI.receipt_qty::numeric - COALESCE(NULLIF(IMI.move_qty,'')::numeric, 0) END)
|
||||
FROM inventory_mgmt_in IMI WHERE IMI.parent_objid = IM.objid), 0) AS "TOTAL_QTY",
|
||||
-- 불출수량
|
||||
COALESCE((SELECT SUM(COALESCE(NULLIF(IMO.request_qty,'')::numeric, 0))
|
||||
FROM inventory_mgmt_out IMO WHERE IMO.parent_objid = IM.objid), 0) AS "REQUEST_QTY",
|
||||
-- 잔여 = 입고 - 불출
|
||||
(COALESCE((SELECT SUM(CASE WHEN IMI2.receipt_qty IS NULL OR IMI2.receipt_qty = '' THEN 0
|
||||
ELSE IMI2.receipt_qty::numeric - COALESCE(NULLIF(IMI2.move_qty,'')::numeric, 0) END)
|
||||
FROM inventory_mgmt_in IMI2 WHERE IMI2.parent_objid = IM.objid), 0)
|
||||
- COALESCE((SELECT SUM(COALESCE(NULLIF(IMO2.request_qty,'')::numeric, 0))
|
||||
FROM inventory_mgmt_out IMO2 WHERE IMO2.parent_objid = IM.objid), 0)
|
||||
) AS "REMAIN_QTY",
|
||||
-- 입고수량 (receipt_qty 합계)
|
||||
COALESCE((SELECT SUM(COALESCE(NULLIF(IMI3.receipt_qty,'')::numeric, 0))
|
||||
FROM inventory_mgmt_in IMI3 WHERE IMI3.parent_objid = IM.objid), 0) AS "DELIVERY_QTY",
|
||||
-- 이동수량
|
||||
COALESCE((SELECT SUM(COALESCE(NULLIF(IMI4.move_qty,'')::numeric, 0))
|
||||
FROM inventory_mgmt_in IMI4 WHERE IMI4.parent_objid = IM.objid), 0) AS "MOVE_QTY"
|
||||
FROM inventory_mgmt IM
|
||||
LEFT JOIN part_mng P ON P.objid = IM.part_objid
|
||||
LEFT JOIN contract_mgmt CM ON CM.objid::text = IM.contract_objid
|
||||
WHERE ${where}
|
||||
ORDER BY CM.contract_no, IM.unit
|
||||
`,
|
||||
params
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST() {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT objid AS "OBJID", email AS "EMAIL", company_name AS "COMPANY_NAME",
|
||||
ceo_name AS "CEO_NAME", phone AS "PHONE", biz_no AS "BIZ_NO",
|
||||
role AS "ROLE", status AS "STATUS",
|
||||
TO_CHAR(regdate, 'YYYY-MM-DD') AS "REGDATE"
|
||||
FROM momo_users WHERE COALESCE(is_del,'N') != 'Y'
|
||||
ORDER BY regdate DESC`
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
|
||||
const { objid, role, status } = await req.json();
|
||||
if (!objid) return NextResponse.json({ success: false, message: "objid 누락" }, { status: 400 });
|
||||
|
||||
const sets: string[] = [];
|
||||
const params: unknown[] = [objid];
|
||||
let i = 2;
|
||||
if (role) { sets.push(`role = $${i++}`); params.push(role); }
|
||||
if (status) { sets.push(`status = $${i++}`); params.push(status); }
|
||||
if (sets.length === 0) return NextResponse.json({ success: false, message: "변경 사항 없음" });
|
||||
|
||||
sets.push(`update_date = NOW()`);
|
||||
await execute(`UPDATE momo_users SET ${sets.join(", ")} WHERE objid = $1`, params);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: purchaseOrder.xml#purchaseOrderStatusAmountBySupply
|
||||
// 발주관리 > 업체별_입고요청월 금액현황
|
||||
// 입고요청일 기준 월별(M01~M12) 공급단가 합계를 업체(admin_supply_mng)별로 집계.
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const year = body.year ? String(body.year) : "";
|
||||
const projectNo = body.project_no ? String(body.project_no) : "";
|
||||
const salesMngUserId = body.sales_mng_user_id ? String(body.sales_mng_user_id) : "";
|
||||
const partnerObjId = body.partner_objid ? String(body.partner_objid) : "";
|
||||
|
||||
// $1=year, $2=project_no(LIKE), $3=sales_mng_user_id, $4=partner_objid
|
||||
const params: unknown[] = [
|
||||
year || null,
|
||||
projectNo || null,
|
||||
salesMngUserId || null,
|
||||
partnerObjId || null,
|
||||
];
|
||||
|
||||
const coreSql = `
|
||||
SELECT POM.partner_objid,
|
||||
SUM(COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric) AS total_supply_unit_price,
|
||||
${Array.from({ length: 12 }, (_, i) => {
|
||||
const mm = String(i + 1).padStart(2, "0");
|
||||
return `SUM(CASE WHEN TO_CHAR(TO_DATE(POM.delivery_date,'YYYY-MM-DD'),'MM') = '${mm}'
|
||||
THEN COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric END) AS m${mm}`;
|
||||
}).join(",\n ")}
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POM.objid = POP.purchase_order_master_objid
|
||||
JOIN project_mgmt CM ON POM.contract_mgmt_objid = CM.objid
|
||||
WHERE POM.status = 'approvalComplete'
|
||||
AND COALESCE(POM.delivery_date,'') <> ''
|
||||
AND ($1::text IS NULL OR TO_CHAR(TO_DATE(POM.delivery_date,'YYYY-MM-DD'),'YYYY') = $1)
|
||||
AND ($2::text IS NULL OR COALESCE(CM.project_no,'') ILIKE '%' || $2 || '%'
|
||||
OR COALESCE(CM.contract_no,'') ILIKE '%' || $2 || '%')
|
||||
AND ($3::text IS NULL OR POM.sales_mng_user_id = $3)
|
||||
GROUP BY POM.partner_objid
|
||||
`;
|
||||
|
||||
const listSql = `
|
||||
SELECT ASM.objid::text AS "OBJID",
|
||||
ASM.supply_name AS "SUPPLY_NAME",
|
||||
COALESCE(S1.total_supply_unit_price, 0) AS "TOTAL_SUPPLY_UNIT_PRICE",
|
||||
${Array.from({ length: 12 }, (_, i) => {
|
||||
const mm = String(i + 1).padStart(2, "0");
|
||||
return `COALESCE(S1.m${mm}, 0) AS "M${mm}"`;
|
||||
}).join(",\n ")}
|
||||
FROM admin_supply_mng ASM
|
||||
JOIN (${coreSql}) S1 ON ASM.objid::varchar = S1.partner_objid
|
||||
WHERE ($4::text IS NULL OR ASM.objid::text = $4)
|
||||
ORDER BY ASM.supply_name
|
||||
`;
|
||||
|
||||
const sumSql = `
|
||||
SELECT COALESCE(SUM(total_supply_unit_price), 0) AS "TOTAL_SUPPLY_UNIT_PRICE",
|
||||
${Array.from({ length: 12 }, (_, i) => {
|
||||
const mm = String(i + 1).padStart(2, "0");
|
||||
return `COALESCE(SUM(m${mm}), 0) AS "M${mm}"`;
|
||||
}).join(",\n ")}
|
||||
FROM (
|
||||
${coreSql}
|
||||
) S1
|
||||
JOIN admin_supply_mng ASM ON ASM.objid::varchar = S1.partner_objid
|
||||
WHERE ($4::text IS NULL OR ASM.objid::text = $4)
|
||||
`;
|
||||
|
||||
try {
|
||||
const rows = await queryRows(listSql, params);
|
||||
const sums = await queryOne(sumSql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length, SUMS: sums });
|
||||
} catch (e) {
|
||||
console.error("order/amount-status:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "조회 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: purchaseOrder.purchaseOrderSalesBomPartListByPartnerContract
|
||||
// 프로젝트 × 유닛 × 공급업체 기반으로 구매BOM(sales_bom_report_part)에서
|
||||
// 해당 공급업체가 등록된 부품을 발주서 작성 시 자동 투입용으로 조회.
|
||||
// 선택된 파트너가 supply_objid/1/2/3/4 중 어느 슬롯에 있든 그 슬롯의 단가만
|
||||
// 해당 컬럼(PARTNER_PRICE or PRICE1..4)에 복사, 나머지는 '0'.
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const contractObjId = String(body.contract_mgmt_objid || body.CONTRACT_MGMT_OBJID || "");
|
||||
const unitCode = String(body.unit_code || body.UNIT_CODE || "");
|
||||
const partnerObjId = String(body.partner_objid || body.PARTNER_OBJID || "");
|
||||
if (!contractObjId || !partnerObjId) {
|
||||
return NextResponse.json({ RESULTLIST: [] });
|
||||
}
|
||||
|
||||
const sql = `
|
||||
WITH RECURSIVE VIEW_BOM AS (
|
||||
SELECT A.bom_report_objid, A.objid, A.parent_objid, A.child_objid,
|
||||
A.parent_part_no, A.part_no, A.qty, A.seq,
|
||||
1 AS lev,
|
||||
ARRAY[A.child_objid::text] AS path,
|
||||
COALESCE(NULLIF(A.qty,''),'0')::numeric AS aggregate_qty
|
||||
FROM part_bom_report PBR
|
||||
JOIN bom_part_qty A ON PBR.objid = A.bom_report_objid
|
||||
WHERE PBR.contract_objid = $1
|
||||
AND ($2::text = '' OR PBR.unit_code = $2)
|
||||
AND (A.parent_objid IS NULL OR A.parent_objid = '')
|
||||
AND A.status = 'deploy'
|
||||
UNION ALL
|
||||
SELECT B.bom_report_objid, B.objid, B.parent_objid, B.child_objid,
|
||||
B.parent_part_no, B.part_no, B.qty, B.seq,
|
||||
V.lev + 1,
|
||||
V.path || B.child_objid::text,
|
||||
V.aggregate_qty * COALESCE(NULLIF(B.qty,''),'0')::numeric
|
||||
FROM bom_part_qty B
|
||||
JOIN VIEW_BOM V ON B.parent_objid = V.child_objid
|
||||
AND V.bom_report_objid = B.bom_report_objid
|
||||
WHERE B.status = 'deploy'
|
||||
AND NOT (B.child_objid = ANY(V.path))
|
||||
)
|
||||
SELECT base.*,
|
||||
-- 재고수량 = 입고수량 합 - 출고요청수량 합
|
||||
(COALESCE((SELECT SUM(CASE WHEN COALESCE(NULLIF(receipt_qty,''),'') = '' THEN 0
|
||||
ELSE receipt_qty::numeric END)
|
||||
FROM inventory_mgmt IM
|
||||
INNER JOIN inventory_mgmt_in O ON O.parent_objid = IM.objid
|
||||
WHERE IM.part_objid = base."PART_OBJID"), 0)
|
||||
- COALESCE((SELECT SUM(CASE WHEN COALESCE(NULLIF(request_qty,''),'') = '' THEN 0
|
||||
ELSE request_qty::numeric END)
|
||||
FROM inventory_mgmt IM
|
||||
INNER JOIN inventory_mgmt_out O ON O.parent_objid = IM.objid
|
||||
WHERE IM.part_objid = base."PART_OBJID"), 0)
|
||||
) AS "STOCK_QTY",
|
||||
-- 총발주수량 = 이 품번으로 approvalComplete 된 이전 발주 누적
|
||||
COALESCE((SELECT SUM(COALESCE(NULLIF(POP.order_qty,''),'0')::numeric)
|
||||
FROM purchase_order_part POP
|
||||
JOIN purchase_order_master POM
|
||||
ON POM.objid = POP.purchase_order_master_objid
|
||||
WHERE POP.part_objid = base."PART_OBJID"
|
||||
AND POM.status = 'approvalComplete'), 0) AS "TOTAL_ORDER_QTY"
|
||||
FROM (
|
||||
SELECT T1.parent_objid AS "BOM_REPORT_OBJID",
|
||||
PM.objid::text AS "PART_OBJID",
|
||||
T1.objid::text AS "SALES_BOM_REPORT_PART_OBJID",
|
||||
PM.part_name AS "PART_NAME",
|
||||
PM.part_no AS "PART_NO",
|
||||
PM.spec AS "SPEC",
|
||||
PM.maker AS "MAKER",
|
||||
PM.remark AS "REMARK",
|
||||
PM.unit AS "UNIT",
|
||||
(SELECT MAX(aggregate_qty) FROM VIEW_BOM V
|
||||
WHERE V.child_objid = BPQ.child_objid
|
||||
AND V.bom_report_objid = PBR.objid) AS "ORDER_QTY",
|
||||
BPQ.seq AS "SEQ_SORT",
|
||||
CASE WHEN T1.supply_objid = $3 THEN T1.price ELSE '0' END AS "PARTNER_PRICE",
|
||||
CASE WHEN T1.supply_objid1 = $3 THEN T1.price1 ELSE '0' END AS "PRICE1",
|
||||
CASE WHEN T1.supply_objid2 = $3 THEN T1.price2 ELSE '0' END AS "PRICE2",
|
||||
CASE WHEN T1.supply_objid3 = $3 THEN T1.price3 ELSE '0' END AS "PRICE3",
|
||||
CASE WHEN T1.supply_objid4 = $3 THEN T1.price4 ELSE '0' END AS "PRICE4"
|
||||
FROM sales_bom_report_part T1
|
||||
JOIN part_bom_report PBR ON T1.parent_objid = PBR.objid::text
|
||||
JOIN bom_part_qty BPQ ON PBR.objid = BPQ.bom_report_objid
|
||||
AND T1.bom_part_qty_objid = BPQ.child_objid
|
||||
JOIN part_mng PM ON BPQ.last_part_objid = PM.objid::varchar
|
||||
WHERE PBR.contract_objid = $1
|
||||
AND ($2::text = '' OR PBR.unit_code = $2)
|
||||
AND BPQ.status = 'deploy'
|
||||
AND COALESCE(PM.part_type,'') <> '0001788'
|
||||
AND (T1.supply_objid = $3
|
||||
OR T1.supply_objid1 = $3
|
||||
OR T1.supply_objid2 = $3
|
||||
OR T1.supply_objid3 = $3
|
||||
OR T1.supply_objid4 = $3)
|
||||
) base
|
||||
ORDER BY base."SEQ_SORT" NULLS LAST
|
||||
`;
|
||||
|
||||
try {
|
||||
const rows = await queryRows(sql, [contractObjId, unitCode, partnerObjId]);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
} catch (e) {
|
||||
console.error("order/bom-parts-by-partner:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "BOM 부품 조회 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne, queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: purchaseOrder.xml#getPurchaseOrderMasterInfo + getPurchaseOrderTargetPartList
|
||||
// 발주서 상세 — master 헤더 + parts 목록 (읽기전용 팝업용)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId required" });
|
||||
|
||||
const info = await queryOne(
|
||||
`SELECT POM.objid::text AS "OBJID",
|
||||
POM.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
POM.title AS "TITLE",
|
||||
POM.contract_mgmt_objid::text AS "CONTRACT_MGMT_OBJID",
|
||||
(SELECT project_no FROM project_mgmt CM WHERE CM.objid = POM.contract_mgmt_objid) AS "PROJECT_NO",
|
||||
(SELECT customer_project_name FROM project_mgmt CM WHERE CM.objid = POM.contract_mgmt_objid) AS "CUSTOMER_PROJECT_NAME",
|
||||
(SELECT contract_no FROM project_mgmt CM WHERE CM.objid = POM.contract_mgmt_objid) AS "CONTRACT_NO",
|
||||
(SELECT supply_name FROM supply_mng SM WHERE SM.objid::text =
|
||||
(SELECT customer_objid FROM project_mgmt CM WHERE CM.objid = POM.contract_mgmt_objid)::text LIMIT 1) AS "CUSTOMER_NAME",
|
||||
POM.type AS "TYPE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.type LIMIT 1) AS "TYPE_NAME",
|
||||
POM.order_type_cd AS "ORDER_TYPE_CD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.order_type_cd LIMIT 1) AS "ORDER_TYPE_CD_NAME",
|
||||
POM.unit_code AS "UNIT_CODE",
|
||||
(SELECT COALESCE(O.unit_no,'') || '-' || COALESCE(O.task_name,'') FROM pms_wbs_task O WHERE O.objid = POM.unit_code LIMIT 1) AS "UNIT_NAME",
|
||||
POM.delivery_date AS "DELIVERY_DATE",
|
||||
POM.delivery_place AS "DELIVERY_PLACE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.delivery_place LIMIT 1) AS "DELIVERY_PLACE_NAME",
|
||||
POM.effective_date AS "EFFECTIVE_DATE",
|
||||
POM.payment_terms AS "PAYMENT_TERMS",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.payment_terms LIMIT 1) AS "PAYMENT_TERMS_NAME",
|
||||
POM.inspect_method AS "INSPECT_METHOD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.inspect_method LIMIT 1) AS "INSPECT_METHOD_NAME",
|
||||
POM.vat_method AS "VAT_METHOD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.vat_method LIMIT 1) AS "VAT_METHOD_NAME",
|
||||
POM.partner_objid AS "PARTNER_OBJID",
|
||||
(SELECT supply_name FROM admin_supply_mng WHERE objid::text = POM.partner_objid LIMIT 1) AS "PARTNER_NAME",
|
||||
POM.po_client_id AS "PO_CLIENT_ID",
|
||||
SM_PC.supply_name AS "PO_CLIENT_NAME",
|
||||
SM_PC.bus_reg_no AS "PO_CLIENT_BUS_NO",
|
||||
SM_PC.charge_user_name AS "PO_CLIENT_CHARGER",
|
||||
SM_PC.supply_address AS "PO_CLIENT_ADDR",
|
||||
SM_PC.supply_tel_no AS "PO_CLIENT_TEL",
|
||||
SM_PC.supply_fax_no AS "PO_CLIENT_FAX",
|
||||
SM_PC.office_no AS "PO_CLIENT_HP",
|
||||
SM_PC.email AS "PO_CLIENT_EMAIL",
|
||||
POM.my_company_objid AS "MY_COMPANY_OBJID",
|
||||
POM.supply_bus_no AS "SUPPLY_BUS_NO",
|
||||
POM.supply_user_name AS "SUPPLY_USER_NAME",
|
||||
POM.supply_user_hp AS "SUPPLY_USER_HP",
|
||||
POM.supply_user_tel AS "SUPPLY_USER_TEL",
|
||||
POM.supply_user_fax AS "SUPPLY_USER_FAX",
|
||||
POM.supply_user_email AS "SUPPLY_USER_EMAIL",
|
||||
POM.supply_addr AS "SUPPLY_ADDR",
|
||||
SM_P.bus_reg_no AS "PARTNER_BUS_NO",
|
||||
SM_P.charge_user_name AS "PARTNER_CHARGER",
|
||||
SM_P.supply_address AS "PARTNER_ADDR",
|
||||
SM_P.supply_tel_no AS "PARTNER_TEL",
|
||||
SM_P.supply_fax_no AS "PARTNER_FAX",
|
||||
SM_P.office_no AS "PARTNER_HP",
|
||||
SM_P.email AS "PARTNER_EMAIL",
|
||||
COALESCE(POM.total_price,'0') AS "TOTAL_PRICE",
|
||||
COALESCE(POM.total_price_all,'0') AS "TOTAL_PRICE_ALL",
|
||||
COALESCE(POM.discount_price,'0') AS "DISCOUNT_PRICE",
|
||||
COALESCE(POM.discount_price_all,'0') AS "DISCOUNT_PRICE_ALL",
|
||||
COALESCE(POM.total_supply_price,'0') AS "TOTAL_SUPPLY_PRICE",
|
||||
COALESCE(POM.total_supply_unit_price,'0') AS "TOTAL_SUPPLY_UNIT_PRICE",
|
||||
COALESCE(POM.total_real_supply_price,'0') AS "TOTAL_REAL_SUPPLY_PRICE",
|
||||
POM.nego_rate AS "NEGO_RATE",
|
||||
POM.total_price_txt AS "TOTAL_PRICE_TXT",
|
||||
POM.total_price_txt_all AS "TOTAL_PRICE_TXT_ALL",
|
||||
POM.remark AS "REMARK",
|
||||
POM.writer AS "WRITER",
|
||||
(SELECT user_name FROM user_info WHERE user_id = POM.writer LIMIT 1) AS "WRITER_NAME",
|
||||
POM.sales_mng_user_id AS "SALES_MNG_USER_ID",
|
||||
(SELECT user_name FROM user_info WHERE user_id = POM.sales_mng_user_id LIMIT 1) AS "SALES_MNG_USER_NAME",
|
||||
POM.regdate AS "REGDATE",
|
||||
TO_CHAR(POM.regdate, 'YYYY-MM-DD') AS "REGDATE_TITLE",
|
||||
POM.status AS "STATUS",
|
||||
CASE POM.status
|
||||
WHEN 'create' THEN '등록'
|
||||
WHEN 'approvalRequest' THEN '결재중'
|
||||
WHEN 'approvalComplete' THEN '결재완료'
|
||||
WHEN 'reject' THEN '반려'
|
||||
WHEN 'cancel' THEN '취소'
|
||||
ELSE ''
|
||||
END AS "STATUS_TITLE",
|
||||
COALESCE(POM.multi_yn,'') AS "MULTI_YN",
|
||||
COALESCE(POM.multi_master_yn,'') AS "MULTI_MASTER_YN",
|
||||
POM.multi_master_objid AS "MULTI_MASTER_OBJID",
|
||||
(SELECT ARRAY_TO_STRING(ARRAY_AGG(S.objid), ',') FROM purchase_order_master S
|
||||
WHERE POM.objid = S.multi_master_objid) AS "MULTI_OBJIDS",
|
||||
POM.sales_request_objid AS "SALES_REQUEST_OBJID",
|
||||
POM.bom_report_objid AS "BOM_REPORT_OBJID",
|
||||
POM.purchase_order_no_org AS "PURCHASE_ORDER_NO_ORG",
|
||||
POM.sales_status AS "SALES_STATUS",
|
||||
POM.reception_status AS "RECEPTION_STATUS"
|
||||
FROM purchase_order_master POM
|
||||
LEFT OUTER JOIN admin_supply_mng SM_PC ON POM.po_client_id::text = SM_PC.objid::text
|
||||
LEFT OUTER JOIN admin_supply_mng SM_P ON POM.partner_objid::text = SM_P.objid::text
|
||||
WHERE POM.objid::text = $1`,
|
||||
[String(objId)],
|
||||
);
|
||||
if (!info) return NextResponse.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
|
||||
const parts = await queryRows(
|
||||
`SELECT POP.objid::text AS "OBJID",
|
||||
POP.purchase_order_master_objid::text AS "PURCHASE_ORDER_MASTER_OBJID",
|
||||
POP.part_objid AS "PART_OBJID",
|
||||
CASE WHEN POM.type IN ('0001070','0001069') THEN PM.part_no ELSE POP.part_no END AS "PART_NO",
|
||||
COALESCE(POP.part_name, PM.part_name) AS "PART_NAME",
|
||||
COALESCE(POP.spec, PM.spec) AS "SPEC",
|
||||
COALESCE(POP.maker, PM.maker) AS "MAKER",
|
||||
PM.material AS "MATERIAL",
|
||||
PM.revision AS "REVISION",
|
||||
POP.unit AS "UNIT",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POP.unit LIMIT 1) AS "UNIT_NAME",
|
||||
COALESCE(POP.order_qty,'0') AS "ORDER_QTY",
|
||||
COALESCE(POP.partner_price,'0') AS "PARTNER_PRICE",
|
||||
COALESCE(POP.price1,'0') AS "PRICE1",
|
||||
COALESCE(POP.price2,'0') AS "PRICE2",
|
||||
COALESCE(POP.price3,'0') AS "PRICE3",
|
||||
COALESCE(POP.price4,'0') AS "PRICE4",
|
||||
COALESCE(POP.supply_unit_price,'0') AS "SUPPLY_UNIT_PRICE",
|
||||
COALESCE(POP.supply_unit_vat_price,'0') AS "SUPPLY_UNIT_VAT_PRICE",
|
||||
COALESCE(POP.supply_unit_vat_sum_price,'0') AS "SUPPLY_UNIT_VAT_SUM_PRICE",
|
||||
COALESCE(POP.real_supply_price,'0') AS "REAL_SUPPLY_PRICE",
|
||||
POP.remark AS "REMARK",
|
||||
POP.ld_part_objid AS "LD_PART_OBJID",
|
||||
COALESCE(POP.total_order_qty,'0') AS "TOTAL_ORDER_QTY",
|
||||
COALESCE(POP.stock_qty,'0') AS "STOCK_QTY",
|
||||
COALESCE(POP.real_order_qty,'0') AS "REAL_ORDER_QTY",
|
||||
(SELECT SUM(COALESCE(DH.delivery_qty,'0')::numeric) FROM delivery_history DH WHERE DH.purchase_order_part_objid = POP.objid) AS "TOTAL_DELIVERY_QTY",
|
||||
(SELECT SUM(COALESCE(DH.defect_qty,'0')::numeric) FROM delivery_history DH WHERE DH.purchase_order_part_objid = POP.objid) AS "TOTAL_DEFECT_QTY"
|
||||
FROM purchase_order_part POP
|
||||
JOIN purchase_order_master POM ON POM.objid = POP.purchase_order_master_objid
|
||||
LEFT OUTER JOIN part_mng PM ON POP.part_objid = PM.objid::text
|
||||
WHERE POP.purchase_order_master_objid::text = $1
|
||||
ORDER BY PM.part_no NULLS LAST, POP.regdate DESC`,
|
||||
[String(objId)],
|
||||
);
|
||||
|
||||
// 동시발주 슬레이브 건들 (마스터의 경우만) — 같은 multi_master_objid를 가진 형제들
|
||||
const slaves =
|
||||
info.MULTI_MASTER_YN === "Y"
|
||||
? await queryRows(
|
||||
`SELECT S.objid::text AS "OBJID",
|
||||
S.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
S.partner_objid AS "PARTNER_OBJID",
|
||||
(SELECT supply_name FROM admin_supply_mng WHERE objid::text = S.partner_objid LIMIT 1) AS "PARTNER_NAME",
|
||||
S.contract_mgmt_objid::text AS "CONTRACT_MGMT_OBJID",
|
||||
(SELECT COALESCE(project_no, contract_no) FROM project_mgmt WHERE objid = S.contract_mgmt_objid LIMIT 1) AS "PROJECT_NO",
|
||||
S.unit_code AS "UNIT_CODE",
|
||||
S.delivery_plan_date AS "DELIVERY_PLAN_DATE",
|
||||
S.delivery_plan_qty AS "DELIVERY_PLAN_QTY",
|
||||
COALESCE(S.total_price,'0') AS "TOTAL_PRICE",
|
||||
S.status AS "STATUS"
|
||||
FROM purchase_order_master S
|
||||
WHERE S.multi_master_objid::text = $1
|
||||
AND S.objid::text != $1
|
||||
ORDER BY S.regdate`,
|
||||
[String(objId)],
|
||||
)
|
||||
: [];
|
||||
|
||||
return NextResponse.json({ success: true, data: info, PARTS: parts, SLAVES: slaves });
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: purchaseOrder.xml#purchaseOrderMasterList_new
|
||||
// 발주관리 > 발주서관리 목록. 동시발주는 마스터 행만 노출(MULTI_MASTER_YN='Y' 또는 동시발주 아님).
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const conditions: string[] = [
|
||||
"1=1",
|
||||
"(COALESCE(POM.multi_master_yn,'') = 'Y' OR (COALESCE(POM.multi_master_yn,'') != 'Y' AND COALESCE(POM.multi_yn,'') != 'Y'))",
|
||||
];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.year) {
|
||||
conditions.push(`TO_CHAR(POM.regdate, 'YYYY') = $${idx++}`);
|
||||
params.push(body.year);
|
||||
}
|
||||
if (body.customer_cd) {
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM project_mgmt SP WHERE POM.contract_mgmt_objid = SP.objid AND SP.customer_objid::text = $${idx++})`,
|
||||
);
|
||||
params.push(body.customer_cd);
|
||||
}
|
||||
if (body.project_no) {
|
||||
// project_no(LIKE) — project_mgmt.project_no 또는 contract_no 부분 일치
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM project_mgmt SP WHERE POM.contract_mgmt_objid = SP.objid
|
||||
AND (COALESCE(SP.project_no,'') LIKE '%' || $${idx} || '%'
|
||||
OR COALESCE(SP.contract_no,'') LIKE '%' || $${idx} || '%'))`,
|
||||
);
|
||||
params.push(body.project_no);
|
||||
idx++;
|
||||
}
|
||||
if (body.customer_project_name) {
|
||||
conditions.push(`CM.customer_project_name LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.customer_project_name);
|
||||
}
|
||||
if (body.unit_code) {
|
||||
conditions.push(`COALESCE(POM.unit_code,'') LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.unit_code);
|
||||
}
|
||||
if (body.purchase_order_no) {
|
||||
conditions.push(`POM.purchase_order_no LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.purchase_order_no);
|
||||
}
|
||||
if (body.type) {
|
||||
conditions.push(`POM.type = $${idx++}`);
|
||||
params.push(body.type);
|
||||
}
|
||||
if (body.order_type_cd) {
|
||||
conditions.push(`POM.order_type_cd = $${idx++}`);
|
||||
params.push(body.order_type_cd);
|
||||
}
|
||||
if (body.delivery_start_date) {
|
||||
conditions.push(
|
||||
`COALESCE(POM.delivery_date,'') <> '' AND TO_DATE(POM.delivery_date, 'YYYY-MM-DD') >= TO_DATE($${idx++}, 'YYYY-MM-DD')`,
|
||||
);
|
||||
params.push(body.delivery_start_date);
|
||||
}
|
||||
if (body.delivery_end_date) {
|
||||
conditions.push(
|
||||
`COALESCE(POM.delivery_date,'') <> '' AND TO_DATE(POM.delivery_date, 'YYYY-MM-DD') <= TO_DATE($${idx++}, 'YYYY-MM-DD')`,
|
||||
);
|
||||
params.push(body.delivery_end_date);
|
||||
}
|
||||
if (body.partner_objid) {
|
||||
conditions.push(`POM.partner_objid = $${idx++}`);
|
||||
params.push(body.partner_objid);
|
||||
}
|
||||
if (body.sales_mng_user_id) {
|
||||
conditions.push(`POM.sales_mng_user_id = $${idx++}`);
|
||||
params.push(body.sales_mng_user_id);
|
||||
}
|
||||
if (body.reg_start_date) {
|
||||
conditions.push(`TO_CHAR(POM.regdate, 'YYYY-MM-DD') >= $${idx++}`);
|
||||
params.push(body.reg_start_date);
|
||||
}
|
||||
if (body.reg_end_date) {
|
||||
conditions.push(`TO_CHAR(POM.regdate, 'YYYY-MM-DD') <= $${idx++}`);
|
||||
params.push(body.reg_end_date);
|
||||
}
|
||||
if (body.po_client_id) {
|
||||
conditions.push(`POM.po_client_id = $${idx++}`);
|
||||
params.push(body.po_client_id);
|
||||
}
|
||||
if (body.part_no) {
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM purchase_order_part POP WHERE POP.purchase_order_master_objid = POM.objid
|
||||
AND TRIM(UPPER(COALESCE(POP.part_no,''))) LIKE '%' || TRIM(UPPER($${idx++})) || '%')`,
|
||||
);
|
||||
params.push(body.part_no);
|
||||
}
|
||||
if (body.part_name) {
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM purchase_order_part POP WHERE POP.purchase_order_master_objid = POM.objid
|
||||
AND TRIM(UPPER(COALESCE(POP.part_name,''))) LIKE '%' || TRIM(UPPER($${idx++})) || '%')`,
|
||||
);
|
||||
params.push(body.part_name);
|
||||
}
|
||||
if (body.part_spec) {
|
||||
conditions.push(
|
||||
`EXISTS (SELECT 1 FROM purchase_order_part POP WHERE POP.purchase_order_master_objid = POM.objid
|
||||
AND TRIM(UPPER(COALESCE(POP.spec,''))) LIKE '%' || TRIM(UPPER($${idx++})) || '%')`,
|
||||
);
|
||||
params.push(body.part_spec);
|
||||
}
|
||||
if (body.appr_status) {
|
||||
const s = String(body.appr_status);
|
||||
if (s === "cancel") {
|
||||
conditions.push(`POM.status = 'cancel'`);
|
||||
} else if (s === "complete") {
|
||||
conditions.push(`POM.status = 'approvalComplete'`);
|
||||
} else if (s === "create") {
|
||||
conditions.push(
|
||||
`POM.status = 'create'
|
||||
AND NOT EXISTS (SELECT 1 FROM approval AT2 WHERE AT2.target_objid::text = POM.objid::text)
|
||||
AND NOT EXISTS (SELECT 1 FROM approval_target AT3 WHERE AT3.target_objid::text = POM.objid::text OR AT3.master_target_objid::text = POM.objid::text)`,
|
||||
);
|
||||
} else {
|
||||
// inProcess / reject 등은 approval.status 기준
|
||||
conditions.push(`A.appr_status = $${idx++}`);
|
||||
params.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT POM.objid::text AS "OBJID",
|
||||
(SELECT ARRAY_TO_STRING(ARRAY_AGG(S.objid), ',') FROM purchase_order_master S
|
||||
WHERE POM.objid = S.multi_master_objid) AS "MULTI_OBJIDS",
|
||||
TO_CHAR(CM.regdate, 'YYYY') AS "CM_YEAR",
|
||||
TO_CHAR(POM.regdate, 'YYYY') AS "PO_YEAR",
|
||||
(SELECT supply_name FROM supply_mng SM WHERE SM.objid::text = CM.customer_objid LIMIT 1) AS "CUSTOMER_NAME",
|
||||
CM.customer_project_name AS "CUSTOMER_PROJECT_NAME",
|
||||
COALESCE(CM.project_no, CM.contract_no) AS "PROJECT_NO",
|
||||
POM.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
(CASE WHEN COALESCE(POM.multi_yn,'') = 'Y' AND COALESCE(POM.multi_master_yn,'') != 'Y' THEN 'ㅡ' ELSE '' END)
|
||||
|| COALESCE(POM.title,'') AS "TITLE",
|
||||
POM.delivery_place AS "DELIVERY_PLACE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.delivery_place LIMIT 1) AS "DELIVERY_PLACE_NAME",
|
||||
POM.inspect_method AS "INSPECT_METHOD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.inspect_method LIMIT 1) AS "INSPECT_METHOD_NAME",
|
||||
POM.payment_terms AS "PAYMENT_TERMS",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.payment_terms LIMIT 1) AS "PAYMENT_TERMS_NAME",
|
||||
POM.delivery_date AS "DELIVERY_DATE",
|
||||
POM.type AS "TYPE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.type LIMIT 1) AS "TYPE_NAME",
|
||||
POM.partner_objid AS "PARTNER_OBJID",
|
||||
(SELECT supply_name FROM admin_supply_mng WHERE objid::text = POM.partner_objid LIMIT 1) AS "PARTNER_NAME",
|
||||
POM.sales_mng_user_id AS "SALES_MNG_USER_ID",
|
||||
(SELECT user_name FROM user_info WHERE user_id = POM.sales_mng_user_id LIMIT 1) AS "SALES_MNG_USER_NAME",
|
||||
TO_CHAR(POM.regdate, 'YYYY-MM-DD') AS "REGDATE",
|
||||
COALESCE(POM.total_price,'0') AS "TOTAL_PRICE",
|
||||
COALESCE(POM.total_price_all,'0') AS "TOTAL_PRICE_ALL",
|
||||
COALESCE(POM.discount_price,'0') AS "DISCOUNT_PRICE",
|
||||
COALESCE(POM.discount_price_all,'0') AS "DISCOUNT_PRICE_ALL",
|
||||
COALESCE(POM.total_supply_price,'0') AS "TOTAL_SUPPLY_PRICE",
|
||||
COALESCE(POM.total_supply_unit_price,'0') AS "TOTAL_SUPPLY_UNIT_PRICE",
|
||||
COALESCE(POM.total_real_supply_price,'0') AS "TOTAL_REAL_SUPPLY_PRICE",
|
||||
POM.nego_rate AS "NEGO_RATE",
|
||||
POM.order_type_cd AS "ORDER_TYPE_CD",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = POM.order_type_cd LIMIT 1) AS "ORDER_TYPE_CD_NAME",
|
||||
POM.unit_code AS "UNIT_CODE",
|
||||
(SELECT COALESCE(O.unit_no,'') || '-' || COALESCE(O.task_name,'') FROM pms_wbs_task O WHERE O.objid = POM.unit_code LIMIT 1) AS "UNIT_NAME",
|
||||
COALESCE(POM.multi_yn,'') AS "MULTI_YN",
|
||||
COALESCE(POM.multi_master_yn,'') AS "MULTI_MASTER_YN",
|
||||
POM.multi_master_objid AS "MULTI_MASTER_OBJID",
|
||||
CASE WHEN COALESCE(POM.multi_master_yn,'') = 'Y' THEN '' ELSE COALESCE(POM.multi_yn,'') END AS "MULTI_YN_MAKED",
|
||||
POM.status AS "STATUS",
|
||||
A.appr_status AS "APPR_STATUS",
|
||||
CASE WHEN POM.status = 'cancel' THEN '취소'
|
||||
ELSE COALESCE(A.appr_status_name, '작성중')
|
||||
END AS "APPR_STATUS_NAME",
|
||||
A.route_objid AS "ROUTE_OBJID",
|
||||
A.approval_objid AS "APPROVAL_OBJID",
|
||||
A.appr_date AS "APPR_DATE"
|
||||
FROM purchase_order_master POM
|
||||
LEFT OUTER JOIN (
|
||||
SELECT B.objid AS route_objid,
|
||||
B.status AS appr_status,
|
||||
CASE B.status
|
||||
WHEN 'inProcess' THEN '결재중'
|
||||
WHEN 'complete' THEN '결재완료'
|
||||
WHEN 'reject' THEN '반려'
|
||||
WHEN 'cancel' THEN '취소'
|
||||
ELSE ''
|
||||
END AS appr_status_name,
|
||||
AP.objid AS approval_objid,
|
||||
AP.target_objid,
|
||||
B.route_seq,
|
||||
TO_CHAR(B.regdate, 'YYYY-MM-DD') AS appr_date
|
||||
FROM approval AP
|
||||
JOIN (
|
||||
SELECT T1.*
|
||||
FROM (SELECT target_objid, MAX(route_seq) AS route_seq FROM route GROUP BY target_objid) T
|
||||
JOIN route T1 ON T.target_objid = T1.target_objid AND T.route_seq = T1.route_seq
|
||||
) B ON AP.objid = B.approval_objid
|
||||
WHERE AP.target_type IN ('PURCHASE_ORDER')
|
||||
) A ON POM.objid::text = A.target_objid::text
|
||||
LEFT OUTER JOIN project_mgmt CM ON POM.contract_mgmt_objid = CM.objid
|
||||
WHERE ${where}
|
||||
ORDER BY
|
||||
CASE WHEN POM.purchase_order_no ~ '^[^-]+-[^-]+-[0-9]+$'
|
||||
THEN SPLIT_PART(POM.purchase_order_no, '-', 3)::numeric
|
||||
ELSE 0 END DESC,
|
||||
POM.regdate DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
} catch (e) {
|
||||
console.error("order/list:", e);
|
||||
return NextResponse.json({ success: false, message: "조회 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: common.getProjectNameList (contract_objid 동일) + salesMng.salesBomReportInfo2(구매BOM 존재 여부)
|
||||
// 동시발주 추가 프로젝트 후보 조회:
|
||||
// 1) 마스터 프로젝트(contract_mgmt_objid)와 동일한 CONTRACT_OBJID(상위 계약) 소속
|
||||
// 2) 마스터 자신은 제외
|
||||
// 3) 해당 프로젝트에 구매BOM(sales_bom_report_part)이 등록되어 있어야 함
|
||||
// 4) (옵션) 선택된 공급처가 구매BOM의 supply_objid/1/2/3/4 중 하나로 등록된 프로젝트만
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const masterProjectObjId = String(body.contract_mgmt_objid || "");
|
||||
const partnerObjId = String(body.partner_objid || "");
|
||||
if (!masterProjectObjId) return NextResponse.json({ RESULTLIST: [] });
|
||||
|
||||
// partner_objid가 있으면 해당 공급처가 등록된 BOM만, 없으면 BOM 존재 여부만 필터
|
||||
const partnerFilter = partnerObjId
|
||||
? `AND EXISTS (
|
||||
SELECT 1
|
||||
FROM part_bom_report PBR
|
||||
JOIN sales_bom_report_part SBP ON SBP.parent_objid::text = PBR.objid::text
|
||||
WHERE PBR.contract_objid = P.objid
|
||||
AND (SBP.supply_objid = $2 OR SBP.supply_objid1 = $2
|
||||
OR SBP.supply_objid2 = $2 OR SBP.supply_objid3 = $2 OR SBP.supply_objid4 = $2)
|
||||
)`
|
||||
: `AND EXISTS (
|
||||
SELECT 1
|
||||
FROM part_bom_report PBR
|
||||
JOIN sales_bom_report_part SBP ON SBP.parent_objid::text = PBR.objid::text
|
||||
WHERE PBR.contract_objid = P.objid
|
||||
)`;
|
||||
|
||||
const sql = `
|
||||
SELECT P.objid::text AS "OBJID",
|
||||
P.project_no AS "PROJECT_NO",
|
||||
P.customer_project_name AS "CUSTOMER_PROJECT_NAME",
|
||||
COALESCE(P.project_no, '') || ' - ' || COALESCE(P.customer_project_name, '') AS "LABEL"
|
||||
FROM project_mgmt P
|
||||
WHERE P.contract_objid = (
|
||||
SELECT contract_objid FROM project_mgmt WHERE objid::text = $1 LIMIT 1
|
||||
)
|
||||
AND P.objid::text <> $1
|
||||
${partnerFilter}
|
||||
ORDER BY P.project_no
|
||||
`;
|
||||
|
||||
const params = partnerObjId ? [masterProjectObjId, partnerObjId] : [masterProjectObjId];
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows });
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 재발주 시 원본 발주서 선택용 — 같은 프로젝트의 approvalComplete 된 발주서 목록
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const contractObjId = String(body.contract_mgmt_objid || "");
|
||||
if (!contractObjId) return NextResponse.json({ RESULTLIST: [] });
|
||||
|
||||
// 재발주 원본 후보: 같은 프로젝트의 결재완료 건. 동시발주 슬레이브만 제외 (마스터/단일은 선택 가능).
|
||||
const rows = await queryRows(
|
||||
`SELECT POM.objid::text AS "OBJID",
|
||||
POM.purchase_order_no AS "PURCHASE_ORDER_NO",
|
||||
POM.title AS "TITLE",
|
||||
TO_CHAR(POM.regdate, 'YYYY-MM-DD') AS "REGDATE",
|
||||
(SELECT supply_name FROM admin_supply_mng WHERE objid::text = POM.partner_objid LIMIT 1) AS "PARTNER_NAME"
|
||||
FROM purchase_order_master POM
|
||||
WHERE POM.contract_mgmt_objid::text = $1
|
||||
AND POM.status = 'approvalComplete'
|
||||
AND NOT (
|
||||
COALESCE(POM.multi_yn,'') = 'Y'
|
||||
AND COALESCE(POM.multi_master_yn,'') != 'Y'
|
||||
)
|
||||
ORDER BY POM.regdate DESC
|
||||
`,
|
||||
[contractObjId],
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows });
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 원본: /purchaseOrder/purchaseOrderFormPopup_PriceSave.do — 단가만 저장
|
||||
// 품목의 PARTNER_PRICE, PRICE1~4, SUPPLY_UNIT_PRICE, VAT 관련 값만 업데이트.
|
||||
// 마스터의 수량/거래처/납품일 등은 건드리지 않음.
|
||||
// 동시발주 마스터인 경우 슬레이브 품목/합계도 동기화 (마스터 품목을 DELETE+INSERT로 복제).
|
||||
interface PriceRow {
|
||||
OBJID?: string;
|
||||
PARTNER_PRICE?: string | number;
|
||||
PRICE1?: string | number;
|
||||
PRICE2?: string | number;
|
||||
PRICE3?: string | number;
|
||||
PRICE4?: string | number;
|
||||
ORDER_QTY?: string | number;
|
||||
}
|
||||
|
||||
const toNum = (v: unknown) => {
|
||||
const s = String(v ?? "").replace(/,/g, "");
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
};
|
||||
const toStr = (v: unknown) => (v === undefined || v === null ? "" : String(v));
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const masterObjId = toStr(body.objId || body.MASTER_OBJID);
|
||||
const vatMethod = toStr(body.vat_method);
|
||||
const rows: PriceRow[] = Array.isArray(body.parts) ? body.parts : [];
|
||||
if (!masterObjId) {
|
||||
return NextResponse.json({ success: false, message: "objId가 필요합니다." });
|
||||
}
|
||||
const vatRate = vatMethod === "0000270" ? 0 : 0.1;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 결재중/결재완료/취소 상태는 단가저장 불가. 슬레이브도 차단 (마스터에서만).
|
||||
const check = await client.query(
|
||||
`SELECT COALESCE(status,'') AS status,
|
||||
COALESCE(multi_yn,'') AS multi_yn,
|
||||
COALESCE(multi_master_yn,'') AS multi_master_yn
|
||||
FROM purchase_order_master WHERE objid::text = $1`,
|
||||
[masterObjId],
|
||||
);
|
||||
const cur = check.rows[0];
|
||||
if (cur?.multi_yn === "Y" && cur?.multi_master_yn !== "Y") {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "동시발주 슬레이브건은 마스터 발주서에서 수정해주세요.",
|
||||
});
|
||||
}
|
||||
if (
|
||||
cur?.status === "approvalRequest" ||
|
||||
cur?.status === "approvalComplete" ||
|
||||
cur?.status === "cancel"
|
||||
) {
|
||||
await client.query("ROLLBACK");
|
||||
const label =
|
||||
cur.status === "approvalRequest"
|
||||
? "결재중"
|
||||
: cur.status === "approvalComplete"
|
||||
? "결재완료"
|
||||
: "취소";
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: `${label} 상태의 발주서는 단가저장할 수 없습니다.`,
|
||||
});
|
||||
}
|
||||
|
||||
let totalSupplyUnitPrice = 0;
|
||||
let totalSupplyPrice = 0;
|
||||
for (const r of rows) {
|
||||
if (!toStr(r.OBJID)) continue;
|
||||
const qty = toNum(r.ORDER_QTY);
|
||||
const unit = toNum(r.PARTNER_PRICE);
|
||||
const sum = qty * (unit + toNum(r.PRICE1) + toNum(r.PRICE2) + toNum(r.PRICE3) + toNum(r.PRICE4));
|
||||
totalSupplyUnitPrice += sum;
|
||||
totalSupplyPrice += qty * unit;
|
||||
await client.query(
|
||||
`UPDATE purchase_order_part SET
|
||||
partner_price = $2, price1 = $3, price2 = $4, price3 = $5, price4 = $6,
|
||||
supply_unit_price = $7,
|
||||
supply_unit_vat_price = $8,
|
||||
supply_unit_vat_sum_price = $9,
|
||||
real_supply_price = $9,
|
||||
update_date = now(), modifier = $10
|
||||
WHERE objid = $1`,
|
||||
[
|
||||
toStr(r.OBJID),
|
||||
String(unit),
|
||||
String(toNum(r.PRICE1)),
|
||||
String(toNum(r.PRICE2)),
|
||||
String(toNum(r.PRICE3)),
|
||||
String(toNum(r.PRICE4)),
|
||||
String(sum),
|
||||
String(Math.round(sum * vatRate)),
|
||||
String(Math.round(sum * (1 + vatRate))),
|
||||
user.userId,
|
||||
],
|
||||
);
|
||||
}
|
||||
// 동시발주 슬레이브 품목 동기화: 마스터 현재 품목을 슬레이브로 DELETE+INSERT 복제.
|
||||
// save/route.ts의 슬레이브 생성 로직과 동일 패턴 — 단가가 바뀌면 슬레이브 품목도 같이.
|
||||
const slavesRes = await client.query(
|
||||
`SELECT objid::text AS objid FROM purchase_order_master
|
||||
WHERE multi_master_objid::text = $1 AND objid::text <> $1`,
|
||||
[masterObjId],
|
||||
);
|
||||
if (slavesRes.rows.length > 0) {
|
||||
const masterPartsRes = await client.query(
|
||||
`SELECT part_objid, part_no, part_name, spec, maker, unit,
|
||||
order_qty, partner_price, price1, price2, price3, price4,
|
||||
supply_unit_price, supply_unit_vat_price, supply_unit_vat_sum_price,
|
||||
real_supply_price, real_order_qty, total_order_qty, stock_qty,
|
||||
remark, ld_part_objid
|
||||
FROM purchase_order_part
|
||||
WHERE purchase_order_master_objid::text = $1
|
||||
ORDER BY regdate ASC, objid ASC`,
|
||||
[masterObjId],
|
||||
);
|
||||
for (const slave of slavesRes.rows) {
|
||||
await client.query(
|
||||
`DELETE FROM purchase_order_part WHERE purchase_order_master_objid::text = $1`,
|
||||
[slave.objid],
|
||||
);
|
||||
for (const mp of masterPartsRes.rows) {
|
||||
await client.query(
|
||||
`INSERT INTO purchase_order_part (
|
||||
objid, purchase_order_master_objid, part_objid, part_no, part_name,
|
||||
spec, maker, unit, order_qty, partner_price,
|
||||
price1, price2, price3, price4,
|
||||
supply_unit_price, supply_unit_vat_price, supply_unit_vat_sum_price,
|
||||
real_supply_price, real_order_qty, total_order_qty, stock_qty,
|
||||
remark, writer, regdate, status, ld_part_objid
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21,
|
||||
$22, $23, now(), 'create', $24
|
||||
)`,
|
||||
[
|
||||
createObjectId(),
|
||||
slave.objid,
|
||||
toStr(mp.part_objid),
|
||||
toStr(mp.part_no),
|
||||
toStr(mp.part_name),
|
||||
toStr(mp.spec),
|
||||
toStr(mp.maker),
|
||||
toStr(mp.unit),
|
||||
toStr(mp.order_qty),
|
||||
toStr(mp.partner_price),
|
||||
toStr(mp.price1),
|
||||
toStr(mp.price2),
|
||||
toStr(mp.price3),
|
||||
toStr(mp.price4),
|
||||
toStr(mp.supply_unit_price),
|
||||
toStr(mp.supply_unit_vat_price),
|
||||
toStr(mp.supply_unit_vat_sum_price),
|
||||
toStr(mp.real_supply_price),
|
||||
toStr(mp.real_order_qty),
|
||||
toStr(mp.total_order_qty),
|
||||
toStr(mp.stock_qty),
|
||||
toStr(mp.remark),
|
||||
user.userId,
|
||||
toStr(mp.ld_part_objid),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마스터 + 슬레이브의 단건 합계 동기화 (discount_price_all은 건드리지 않음 — 원본 동작)
|
||||
const discount = toNum(body.discount_price);
|
||||
const totalPrice = totalSupplyUnitPrice - discount;
|
||||
const realSupply = Math.round(totalPrice * (1 + vatRate));
|
||||
const negoRate =
|
||||
totalSupplyPrice > 0 ? Math.round((discount / totalSupplyPrice) * 10000) / 100 : 0;
|
||||
await client.query(
|
||||
`UPDATE purchase_order_master SET
|
||||
total_supply_unit_price = $2, total_supply_price = $3,
|
||||
total_real_supply_price = $4, total_price = $5,
|
||||
discount_price = $6, nego_rate = $7
|
||||
WHERE objid::text = $1 OR multi_master_objid::text = $1`,
|
||||
[
|
||||
masterObjId,
|
||||
String(totalSupplyUnitPrice),
|
||||
String(totalSupplyPrice),
|
||||
String(realSupply),
|
||||
String(totalPrice),
|
||||
String(discount),
|
||||
String(negoRate),
|
||||
],
|
||||
);
|
||||
|
||||
// 원본 updatePurchaseOrderMasterPriceAll + updatePurchaseOrderPriceTxt
|
||||
// 마스터+슬레이브에 total_supply_unit_price_all, total_price_all, 한글 금액 세팅
|
||||
// (save/route.ts와 동일 패턴)
|
||||
const supplyUnitAll = totalSupplyUnitPrice * (1 + slavesRes.rows.length);
|
||||
await client.query(
|
||||
`UPDATE purchase_order_master SET
|
||||
total_supply_unit_price_all = $2,
|
||||
total_price_all = CASE
|
||||
WHEN COALESCE(discount_price, '0')::numeric != 0
|
||||
THEN (COALESCE(total_real_supply_price, '0')::numeric - COALESCE(discount_price, '0')::numeric)::text
|
||||
ELSE NULL END,
|
||||
total_price_txt = NUM_TO_KOR(COALESCE(total_supply_unit_price, '0'), '일금 ', ' 원정 (₩ ')
|
||||
|| TO_CHAR(COALESCE(total_supply_unit_price, '0')::numeric, '999,999,999,999') || ')',
|
||||
total_price_txt_all = NUM_TO_KOR(COALESCE(total_supply_unit_price_all, '0'), '일금 ', ' 원정 (₩ ')
|
||||
|| TO_CHAR(COALESCE(total_supply_unit_price_all, '0')::numeric, '999,999,999,999') || ')'
|
||||
WHERE objid::text = $1 OR multi_master_objid::text = $1`,
|
||||
[masterObjId, String(supplyUnitAll)],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "단가가 저장되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("order/price-save:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "단가 저장 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
import { COMPANY_INFO } from "@/lib/constants";
|
||||
|
||||
// 원본: purchaseOrder.mergePurchaseOrderMaster + purchaseOrder.initPurchaseOrderPart + mergePurchaseOrderPart
|
||||
// 발주서 UPSERT (단일발주 + 동시발주)
|
||||
// 동시발주: 하나의 공급처에 대해 여러 프로젝트를 묶어서 발주. 마스터 1건 +
|
||||
// 추가 프로젝트 수만큼 슬레이브 PO 생성. 슬레이브는 multi_yn='Y', multi_master_yn='N',
|
||||
// multi_master_objid=master, 품목은 마스터 것을 복사 (각 프로젝트별 contract_mgmt_objid만 다름).
|
||||
interface MultiProjectEntry {
|
||||
contract_mgmt_objid?: string;
|
||||
unit_code?: string;
|
||||
delivery_plan_date?: string;
|
||||
delivery_plan_qty?: string | number;
|
||||
objid?: string; // 기존 슬레이브 OBJID (편집 시)
|
||||
}
|
||||
|
||||
interface PartRow {
|
||||
OBJID?: string;
|
||||
PART_OBJID?: string;
|
||||
PART_NO?: string;
|
||||
PART_NAME?: string;
|
||||
SPEC?: string;
|
||||
MAKER?: string;
|
||||
UNIT?: string;
|
||||
ORDER_QTY?: string | number;
|
||||
PARTNER_PRICE?: string | number;
|
||||
PRICE1?: string | number;
|
||||
PRICE2?: string | number;
|
||||
PRICE3?: string | number;
|
||||
PRICE4?: string | number;
|
||||
SUPPLY_UNIT_PRICE?: string | number;
|
||||
SUPPLY_UNIT_VAT_PRICE?: string | number;
|
||||
SUPPLY_UNIT_VAT_SUM_PRICE?: string | number;
|
||||
REMARK?: string;
|
||||
LD_PART_OBJID?: string;
|
||||
TOTAL_ORDER_QTY?: string | number;
|
||||
STOCK_QTY?: string | number;
|
||||
REAL_ORDER_QTY?: string | number;
|
||||
}
|
||||
|
||||
const toStr = (v: unknown) => (v === undefined || v === null ? "" : String(v));
|
||||
const toNum = (v: unknown) => {
|
||||
const s = toStr(v).replace(/,/g, "");
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const inputObjId = toStr(body.objId || body.MASTER_OBJID);
|
||||
const isNew = !inputObjId;
|
||||
const masterObjId = isNew ? createObjectId() : inputObjId;
|
||||
const parts: PartRow[] = Array.isArray(body.parts) ? body.parts : [];
|
||||
const rawMultiProjects: MultiProjectEntry[] = Array.isArray(body.multi_projects)
|
||||
? body.multi_projects.filter((m: MultiProjectEntry) => toStr(m?.contract_mgmt_objid))
|
||||
: [];
|
||||
// 동시적용 중복 방어:
|
||||
// 1) 마스터와 같은 프로젝트는 거부 (프론트 우회 방어)
|
||||
// 2) 슬레이브끼리 같은 프로젝트 중복은 첫 건만 남기고 제거
|
||||
const masterProject = toStr(body.contract_mgmt_objid);
|
||||
if (rawMultiProjects.some((m) => toStr(m.contract_mgmt_objid) === masterProject)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "동시적용 프로젝트에 마스터 프로젝트를 포함할 수 없습니다.",
|
||||
});
|
||||
}
|
||||
const seenProjects = new Set<string>();
|
||||
const multiProjects: MultiProjectEntry[] = [];
|
||||
for (const m of rawMultiProjects) {
|
||||
const key = toStr(m.contract_mgmt_objid);
|
||||
if (seenProjects.has(key)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "동시적용 프로젝트가 중복되었습니다. 같은 프로젝트는 1회만 선택할 수 있습니다.",
|
||||
});
|
||||
}
|
||||
seenProjects.add(key);
|
||||
multiProjects.push(m);
|
||||
}
|
||||
const isMulti = multiProjects.length > 0;
|
||||
// 발주처는 FITO 고정 — 클라이언트 입력 무시
|
||||
const poClientId = COMPANY_INFO.PO_CLIENT_ID;
|
||||
|
||||
// 필수 검증
|
||||
if (!toStr(body.partner_objid)) {
|
||||
return NextResponse.json({ success: false, message: "공급처(PARTNER)를 선택하세요." });
|
||||
}
|
||||
if (!toStr(body.contract_mgmt_objid)) {
|
||||
return NextResponse.json({ success: false, message: "프로젝트를 선택하세요." });
|
||||
}
|
||||
if (!toStr(body.type)) {
|
||||
return NextResponse.json({ success: false, message: "발주부품을 선택하세요." });
|
||||
}
|
||||
|
||||
// 합계 계산 (서버사이드에서 재계산: 품목 공급가 = qty * (partner_price + price1..4))
|
||||
let totalSupplyUnitPrice = 0; // 공급단가 합계 (POP.supply_unit_price 합)
|
||||
let totalSupplyPrice = 0; // 공급가 합계 (price1..4 포함하지 않은 단가*수량 합)
|
||||
for (const p of parts) {
|
||||
const qty = toNum(p.ORDER_QTY);
|
||||
const unit = toNum(p.PARTNER_PRICE);
|
||||
const sum = qty * (unit + toNum(p.PRICE1) + toNum(p.PRICE2) + toNum(p.PRICE3) + toNum(p.PRICE4));
|
||||
totalSupplyUnitPrice += sum;
|
||||
totalSupplyPrice += qty * unit;
|
||||
}
|
||||
const discountPrice = toNum(body.discount_price);
|
||||
const totalPrice = totalSupplyUnitPrice - discountPrice;
|
||||
// VAT 10% 기본
|
||||
const vatRate = toStr(body.vat_method) === "0000270" ? 0 : 0.1; // 0000270 = 부가세 없음 (표준코드 추정)
|
||||
const totalRealSupplyPrice = Math.round(totalPrice * (1 + vatRate));
|
||||
const negoRate =
|
||||
totalSupplyPrice > 0 ? Math.round((discountPrice / totalSupplyPrice) * 10000) / 100 : 0;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// PO 번호 자동생성 (신규 때만). 형식: PO-YYMM-SEQ
|
||||
let poNo = toStr(body.purchase_order_no);
|
||||
if (isNew || !poNo) {
|
||||
const now = new Date();
|
||||
const yymm =
|
||||
String(now.getFullYear()).slice(2) + String(now.getMonth() + 1).padStart(2, "0");
|
||||
const r = await client.query(
|
||||
`SELECT COALESCE(MAX(
|
||||
CASE WHEN SPLIT_PART(purchase_order_no, '-', 3) ~ '^[0-9]+$'
|
||||
THEN SPLIT_PART(purchase_order_no, '-', 3)::int ELSE 0 END), 0) + 1 AS next_seq
|
||||
FROM purchase_order_master`,
|
||||
);
|
||||
const nextSeq = Number(r.rows[0]?.next_seq ?? 1);
|
||||
poNo = `PO-${yymm}-${nextSeq}`;
|
||||
}
|
||||
|
||||
// 공급처 정보 자동 채우기 (admin_supply_mng 조회)
|
||||
let supplyInfo: Record<string, string> = {};
|
||||
if (toStr(body.partner_objid)) {
|
||||
const r = await client.query(
|
||||
`SELECT bus_reg_no, supply_address, supply_tel_no, supply_fax_no, office_no, email, charge_user_name
|
||||
FROM admin_supply_mng WHERE objid::text = $1`,
|
||||
[toStr(body.partner_objid)],
|
||||
);
|
||||
if (r.rows[0]) {
|
||||
supplyInfo = {
|
||||
SUPPLY_BUS_NO: toStr(r.rows[0].bus_reg_no),
|
||||
SUPPLY_USER_NAME: toStr(r.rows[0].charge_user_name),
|
||||
SUPPLY_USER_HP: toStr(r.rows[0].office_no),
|
||||
SUPPLY_USER_TEL: toStr(r.rows[0].supply_tel_no),
|
||||
SUPPLY_USER_FAX: toStr(r.rows[0].supply_fax_no),
|
||||
SUPPLY_USER_EMAIL: toStr(r.rows[0].email),
|
||||
SUPPLY_ADDR: toStr(r.rows[0].supply_address),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
await client.query(
|
||||
`INSERT INTO purchase_order_master (
|
||||
objid, purchase_order_no, po_client_id, partner_objid, my_company_objid,
|
||||
contract_mgmt_objid, unit_code, bom_report_objid, sales_request_objid,
|
||||
type, order_type_cd, delivery_date, delivery_place, effective_date,
|
||||
payment_terms, inspect_method, vat_method, sales_mng_user_id,
|
||||
title, remark, status, writer, regdate, purchase_date,
|
||||
supply_bus_no, supply_user_name, supply_user_hp, supply_user_tel,
|
||||
supply_user_fax, supply_user_email, supply_addr,
|
||||
total_supply_unit_price, total_supply_price, total_real_supply_price,
|
||||
total_price, total_price_all, discount_price, discount_price_all,
|
||||
nego_rate, multi_yn, multi_master_yn, multi_master_objid, purchase_order_no_org
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6::numeric, $7, $8, $9,
|
||||
$10, $11, $12, $13, $14, $15, $16, $17, $18,
|
||||
$19, $20, 'create', $21, now(), TO_CHAR(NOW(), 'YYYY-MM-DD'),
|
||||
$22, $23, $24, $25, $26, $27, $28,
|
||||
$29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40
|
||||
)`,
|
||||
[
|
||||
masterObjId,
|
||||
poNo,
|
||||
poClientId,
|
||||
toStr(body.partner_objid),
|
||||
toStr(body.my_company_objid) || poClientId,
|
||||
toStr(body.contract_mgmt_objid),
|
||||
toStr(body.unit_code),
|
||||
toStr(body.bom_report_objid),
|
||||
toStr(body.sales_request_objid),
|
||||
toStr(body.type),
|
||||
toStr(body.order_type_cd),
|
||||
toStr(body.delivery_date),
|
||||
toStr(body.delivery_place),
|
||||
toStr(body.effective_date),
|
||||
toStr(body.payment_terms),
|
||||
toStr(body.inspect_method),
|
||||
toStr(body.vat_method),
|
||||
toStr(body.sales_mng_user_id) || user.userId,
|
||||
toStr(body.title),
|
||||
toStr(body.remark),
|
||||
user.userId,
|
||||
supplyInfo.SUPPLY_BUS_NO || "",
|
||||
supplyInfo.SUPPLY_USER_NAME || "",
|
||||
supplyInfo.SUPPLY_USER_HP || "",
|
||||
supplyInfo.SUPPLY_USER_TEL || "",
|
||||
supplyInfo.SUPPLY_USER_FAX || "",
|
||||
supplyInfo.SUPPLY_USER_EMAIL || "",
|
||||
supplyInfo.SUPPLY_ADDR || "",
|
||||
String(totalSupplyUnitPrice),
|
||||
String(totalSupplyPrice),
|
||||
String(totalRealSupplyPrice),
|
||||
String(totalPrice),
|
||||
String(totalRealSupplyPrice), // 단일발주는 전체=자신
|
||||
String(discountPrice),
|
||||
String(discountPrice),
|
||||
String(negoRate),
|
||||
isMulti ? "Y" : "",
|
||||
isMulti ? "Y" : "",
|
||||
"", // multi_master_objid (master 자기 자신이므로 비움)
|
||||
toStr(body.purchase_order_no_org),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// 결재중/결재완료/취소 상태는 수정 불가. 슬레이브도 차단 (마스터를 통해서만).
|
||||
const check = await client.query(
|
||||
`SELECT COALESCE(multi_yn,'') AS multi_yn,
|
||||
COALESCE(multi_master_yn,'') AS multi_master_yn,
|
||||
COALESCE(status,'') AS status
|
||||
FROM purchase_order_master WHERE objid::text = $1`,
|
||||
[masterObjId],
|
||||
);
|
||||
const cur = check.rows[0];
|
||||
if (cur?.multi_yn === "Y" && cur?.multi_master_yn !== "Y") {
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: "동시발주 슬레이브건은 마스터 발주서에서 수정해주세요.",
|
||||
});
|
||||
}
|
||||
if (
|
||||
cur?.status === "approvalRequest" ||
|
||||
cur?.status === "approvalComplete" ||
|
||||
cur?.status === "cancel"
|
||||
) {
|
||||
await client.query("ROLLBACK");
|
||||
const label =
|
||||
cur.status === "approvalRequest"
|
||||
? "결재중"
|
||||
: cur.status === "approvalComplete"
|
||||
? "결재완료"
|
||||
: "취소";
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: `${label} 상태의 발주서는 수정할 수 없습니다.`,
|
||||
});
|
||||
}
|
||||
await client.query(
|
||||
`UPDATE purchase_order_master SET
|
||||
po_client_id = $2, partner_objid = $3,
|
||||
contract_mgmt_objid = $4::numeric, unit_code = $5,
|
||||
type = $6, order_type_cd = $7, delivery_date = $8, delivery_place = $9,
|
||||
effective_date = $10, payment_terms = $11, inspect_method = $12,
|
||||
vat_method = $13, sales_mng_user_id = $14, title = $15, remark = $16,
|
||||
supply_bus_no = $17, supply_user_name = $18, supply_user_hp = $19,
|
||||
supply_user_tel = $20, supply_user_fax = $21, supply_user_email = $22,
|
||||
supply_addr = $23,
|
||||
total_supply_unit_price = $24, total_supply_price = $25,
|
||||
total_real_supply_price = $26, total_price = $27, total_price_all = $28,
|
||||
discount_price = $29, discount_price_all = $30, nego_rate = $31,
|
||||
multi_yn = $32, multi_master_yn = $33
|
||||
WHERE objid::text = $1`,
|
||||
[
|
||||
masterObjId,
|
||||
poClientId,
|
||||
toStr(body.partner_objid),
|
||||
toStr(body.contract_mgmt_objid),
|
||||
toStr(body.unit_code),
|
||||
toStr(body.type),
|
||||
toStr(body.order_type_cd),
|
||||
toStr(body.delivery_date),
|
||||
toStr(body.delivery_place),
|
||||
toStr(body.effective_date),
|
||||
toStr(body.payment_terms),
|
||||
toStr(body.inspect_method),
|
||||
toStr(body.vat_method),
|
||||
toStr(body.sales_mng_user_id) || user.userId,
|
||||
toStr(body.title),
|
||||
toStr(body.remark),
|
||||
supplyInfo.SUPPLY_BUS_NO || "",
|
||||
supplyInfo.SUPPLY_USER_NAME || "",
|
||||
supplyInfo.SUPPLY_USER_HP || "",
|
||||
supplyInfo.SUPPLY_USER_TEL || "",
|
||||
supplyInfo.SUPPLY_USER_FAX || "",
|
||||
supplyInfo.SUPPLY_USER_EMAIL || "",
|
||||
supplyInfo.SUPPLY_ADDR || "",
|
||||
String(totalSupplyUnitPrice),
|
||||
String(totalSupplyPrice),
|
||||
String(totalRealSupplyPrice),
|
||||
String(totalPrice),
|
||||
String(totalRealSupplyPrice),
|
||||
String(discountPrice),
|
||||
String(discountPrice),
|
||||
String(negoRate),
|
||||
isMulti ? "Y" : "",
|
||||
isMulti ? "Y" : "",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 품목 재등록: 기존 delete 후 재삽입
|
||||
await client.query(
|
||||
`DELETE FROM purchase_order_part WHERE purchase_order_master_objid = $1`,
|
||||
[masterObjId],
|
||||
);
|
||||
for (const p of parts) {
|
||||
if (!toStr(p.PART_OBJID) && !toStr(p.PART_NO)) continue;
|
||||
const qty = toNum(p.ORDER_QTY);
|
||||
const unit = toNum(p.PARTNER_PRICE);
|
||||
const sum = qty * (unit + toNum(p.PRICE1) + toNum(p.PRICE2) + toNum(p.PRICE3) + toNum(p.PRICE4));
|
||||
await client.query(
|
||||
`INSERT INTO purchase_order_part (
|
||||
objid, purchase_order_master_objid, part_objid, part_no, part_name,
|
||||
spec, maker, unit, order_qty, partner_price,
|
||||
price1, price2, price3, price4,
|
||||
supply_unit_price, supply_unit_vat_price, supply_unit_vat_sum_price,
|
||||
real_supply_price, real_order_qty, total_order_qty, stock_qty,
|
||||
remark, writer, regdate, status, ld_part_objid
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21,
|
||||
$22, $23, now(), 'create', $24
|
||||
)`,
|
||||
[
|
||||
createObjectId(),
|
||||
masterObjId,
|
||||
toStr(p.PART_OBJID),
|
||||
toStr(p.PART_NO),
|
||||
toStr(p.PART_NAME),
|
||||
toStr(p.SPEC),
|
||||
toStr(p.MAKER),
|
||||
toStr(p.UNIT),
|
||||
String(qty),
|
||||
String(unit),
|
||||
String(toNum(p.PRICE1)),
|
||||
String(toNum(p.PRICE2)),
|
||||
String(toNum(p.PRICE3)),
|
||||
String(toNum(p.PRICE4)),
|
||||
String(sum),
|
||||
String(Math.round(sum * vatRate)),
|
||||
String(Math.round(sum * (1 + vatRate))),
|
||||
String(Math.round(sum * (1 + vatRate))),
|
||||
String(toNum(p.REAL_ORDER_QTY) || qty),
|
||||
String(toNum(p.TOTAL_ORDER_QTY)),
|
||||
String(toNum(p.STOCK_QTY)),
|
||||
toStr(p.REMARK),
|
||||
user.userId,
|
||||
toStr(p.LD_PART_OBJID),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// === 동시발주 슬레이브 처리 ===
|
||||
// 기존 슬레이브 전부 삭제 후 새로 생성 (parts도 동일하게)
|
||||
await client.query(
|
||||
`DELETE FROM purchase_order_part
|
||||
WHERE purchase_order_master_objid IN (
|
||||
SELECT objid FROM purchase_order_master
|
||||
WHERE multi_master_objid::text = $1 AND objid::text <> $1
|
||||
)`,
|
||||
[masterObjId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM purchase_order_master
|
||||
WHERE multi_master_objid::text = $1 AND objid::text <> $1`,
|
||||
[masterObjId],
|
||||
);
|
||||
|
||||
// 슬레이브 unit_code 재조회용: 마스터 유닛의 task_name 한 번만 조회
|
||||
// 원본 common.getUnitCodeList — 동시 BOM으로 올라온 프로젝트는 같은 task_name의 유닛을 공유.
|
||||
let masterTaskName = "";
|
||||
if (isMulti && toStr(body.unit_code)) {
|
||||
const r = await client.query(
|
||||
`SELECT task_name FROM pms_wbs_task WHERE objid::text = $1`,
|
||||
[toStr(body.unit_code)],
|
||||
);
|
||||
masterTaskName = toStr(r.rows[0]?.task_name);
|
||||
}
|
||||
|
||||
for (const m of multiProjects) {
|
||||
const slaveObjId = createObjectId();
|
||||
const slaveContract = toStr(m.contract_mgmt_objid);
|
||||
// 슬레이브 프로젝트에서 같은 task_name의 유닛 찾아 unit_code 재조회 (원본 getUnitCodeList)
|
||||
const unitRes = await client.query(
|
||||
`SELECT objid::text AS code FROM pms_wbs_task
|
||||
WHERE contract_objid::text = $1
|
||||
AND (TRIM(UPPER(COALESCE(task_name, ''))) = TRIM(UPPER($2))
|
||||
OR TRIM(UPPER(COALESCE(unit_no, '') || '-' || COALESCE(task_name, ''))) = TRIM(UPPER($2)))
|
||||
ORDER BY unit_no ASC LIMIT 1`,
|
||||
[slaveContract, masterTaskName],
|
||||
);
|
||||
const slaveUnit = toStr(unitRes.rows[0]?.code);
|
||||
if (!slaveUnit) {
|
||||
const pr = await client.query(
|
||||
`SELECT project_no FROM project_mgmt WHERE objid::text = $1`,
|
||||
[slaveContract],
|
||||
);
|
||||
await client.query("ROLLBACK");
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: `${toStr(pr.rows[0]?.project_no) || slaveContract} 프로젝트에 '${masterTaskName}' 유닛이 없습니다. 동시 BOM 설정을 확인하세요.`,
|
||||
});
|
||||
}
|
||||
const slaveDelivDate = toStr(m.delivery_plan_date) || toStr(body.delivery_date);
|
||||
const slaveDelivQty = toStr(m.delivery_plan_qty);
|
||||
// 슬레이브 PO 번호 자동생성
|
||||
const seqRes = await client.query(
|
||||
`SELECT COALESCE(MAX(
|
||||
CASE WHEN SPLIT_PART(purchase_order_no, '-', 3) ~ '^[0-9]+$'
|
||||
THEN SPLIT_PART(purchase_order_no, '-', 3)::int ELSE 0 END), 0) + 1 AS next_seq
|
||||
FROM purchase_order_master`,
|
||||
);
|
||||
const slaveSeq = Number(seqRes.rows[0]?.next_seq ?? 1);
|
||||
const now = new Date();
|
||||
const yymm =
|
||||
String(now.getFullYear()).slice(2) + String(now.getMonth() + 1).padStart(2, "0");
|
||||
const slavePoNo = `PO-${yymm}-${slaveSeq}`;
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO purchase_order_master (
|
||||
objid, purchase_order_no, po_client_id, partner_objid, my_company_objid,
|
||||
contract_mgmt_objid, unit_code, bom_report_objid, sales_request_objid,
|
||||
type, order_type_cd, delivery_date, delivery_place, effective_date,
|
||||
payment_terms, inspect_method, vat_method, sales_mng_user_id,
|
||||
title, remark, status, writer, regdate, purchase_date,
|
||||
supply_bus_no, supply_user_name, supply_user_hp, supply_user_tel,
|
||||
supply_user_fax, supply_user_email, supply_addr,
|
||||
total_supply_unit_price, total_supply_price, total_real_supply_price,
|
||||
total_price, total_price_all, discount_price, discount_price_all,
|
||||
nego_rate, multi_yn, multi_master_yn, multi_master_objid,
|
||||
delivery_plan_date, delivery_plan_qty, purchase_order_no_org
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6::numeric, $7, $8, $9,
|
||||
$10, $11, $12, $13, $14, $15, $16, $17, $18,
|
||||
$19, $20, 'create', $21, now(), TO_CHAR(NOW(), 'YYYY-MM-DD'),
|
||||
$22, $23, $24, $25, $26, $27, $28,
|
||||
$29, $30, $31, $32, $33, $34, $35, $36, 'Y', 'N', $37,
|
||||
$38, $39, ''
|
||||
)`,
|
||||
[
|
||||
slaveObjId,
|
||||
slavePoNo,
|
||||
poClientId,
|
||||
toStr(body.partner_objid),
|
||||
toStr(body.my_company_objid) || poClientId,
|
||||
slaveContract,
|
||||
slaveUnit,
|
||||
"",
|
||||
"",
|
||||
toStr(body.type),
|
||||
toStr(body.order_type_cd),
|
||||
slaveDelivDate,
|
||||
toStr(body.delivery_place),
|
||||
toStr(body.effective_date),
|
||||
toStr(body.payment_terms),
|
||||
toStr(body.inspect_method),
|
||||
toStr(body.vat_method),
|
||||
toStr(body.sales_mng_user_id) || user.userId,
|
||||
toStr(body.title),
|
||||
toStr(body.remark),
|
||||
user.userId,
|
||||
supplyInfo.SUPPLY_BUS_NO || "",
|
||||
supplyInfo.SUPPLY_USER_NAME || "",
|
||||
supplyInfo.SUPPLY_USER_HP || "",
|
||||
supplyInfo.SUPPLY_USER_TEL || "",
|
||||
supplyInfo.SUPPLY_USER_FAX || "",
|
||||
supplyInfo.SUPPLY_USER_EMAIL || "",
|
||||
supplyInfo.SUPPLY_ADDR || "",
|
||||
String(totalSupplyUnitPrice),
|
||||
String(totalSupplyPrice),
|
||||
String(totalRealSupplyPrice),
|
||||
String(totalPrice),
|
||||
String(totalRealSupplyPrice),
|
||||
String(discountPrice),
|
||||
String(discountPrice),
|
||||
String(negoRate),
|
||||
masterObjId,
|
||||
slaveDelivDate,
|
||||
slaveDelivQty,
|
||||
],
|
||||
);
|
||||
// 슬레이브 품목도 마스터 것을 그대로 복사
|
||||
for (const p of parts) {
|
||||
if (!toStr(p.PART_OBJID) && !toStr(p.PART_NO)) continue;
|
||||
const qty = toNum(p.ORDER_QTY);
|
||||
const unit = toNum(p.PARTNER_PRICE);
|
||||
const sum =
|
||||
qty *
|
||||
(unit + toNum(p.PRICE1) + toNum(p.PRICE2) + toNum(p.PRICE3) + toNum(p.PRICE4));
|
||||
await client.query(
|
||||
`INSERT INTO purchase_order_part (
|
||||
objid, purchase_order_master_objid, part_objid, part_no, part_name,
|
||||
spec, maker, unit, order_qty, partner_price,
|
||||
price1, price2, price3, price4,
|
||||
supply_unit_price, supply_unit_vat_price, supply_unit_vat_sum_price,
|
||||
real_supply_price, real_order_qty, total_order_qty, stock_qty,
|
||||
remark, writer, regdate, status, ld_part_objid
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21,
|
||||
$22, $23, now(), 'create', $24
|
||||
)`,
|
||||
[
|
||||
createObjectId(),
|
||||
slaveObjId,
|
||||
toStr(p.PART_OBJID),
|
||||
toStr(p.PART_NO),
|
||||
toStr(p.PART_NAME),
|
||||
toStr(p.SPEC),
|
||||
toStr(p.MAKER),
|
||||
toStr(p.UNIT),
|
||||
String(qty),
|
||||
String(unit),
|
||||
String(toNum(p.PRICE1)),
|
||||
String(toNum(p.PRICE2)),
|
||||
String(toNum(p.PRICE3)),
|
||||
String(toNum(p.PRICE4)),
|
||||
String(sum),
|
||||
String(Math.round(sum * vatRate)),
|
||||
String(Math.round(sum * (1 + vatRate))),
|
||||
String(Math.round(sum * (1 + vatRate))),
|
||||
String(toNum(p.REAL_ORDER_QTY) || qty),
|
||||
String(toNum(p.TOTAL_ORDER_QTY)),
|
||||
String(toNum(p.STOCK_QTY)),
|
||||
toStr(p.REMARK),
|
||||
user.userId,
|
||||
toStr(p.LD_PART_OBJID),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 원본 updatePurchaseOrderMasterPriceAll + updatePurchaseOrderPriceTxt 를 1건으로 병합.
|
||||
// 마스터와 모든 슬레이브에 동일한 집계값을 세팅:
|
||||
// - total_supply_unit_price_all: 마스터 공급단가 × (1 + 슬레이브수)
|
||||
// - total_price_all: real - discount (할인 없으면 NULL)
|
||||
// - total_price_txt / _all: NUM_TO_KOR 한글 금액 (PG 함수)
|
||||
// discount_price_all은 저장 시점에 건드리지 않음 — 별도 네고 수정 화면에서 세팅하는 게 원본 동작.
|
||||
const supplyUnitAll = totalSupplyUnitPrice * (1 + multiProjects.length);
|
||||
await client.query(
|
||||
`UPDATE purchase_order_master SET
|
||||
total_supply_unit_price_all = $2,
|
||||
total_price_all = CASE
|
||||
WHEN COALESCE(discount_price, '0')::numeric != 0
|
||||
THEN (COALESCE(total_real_supply_price, '0')::numeric - COALESCE(discount_price, '0')::numeric)::text
|
||||
ELSE NULL END,
|
||||
total_price_txt = NUM_TO_KOR(COALESCE(total_supply_unit_price, '0'), '일금 ', ' 원정 (₩ ')
|
||||
|| TO_CHAR(COALESCE(total_supply_unit_price, '0')::numeric, '999,999,999,999') || ')',
|
||||
total_price_txt_all = NUM_TO_KOR(COALESCE(total_supply_unit_price_all, '0'), '일금 ', ' 원정 (₩ ')
|
||||
|| TO_CHAR(COALESCE(total_supply_unit_price_all, '0')::numeric, '999,999,999,999') || ')'
|
||||
WHERE objid::text = $1 OR multi_master_objid::text = $1`,
|
||||
[masterObjId, String(supplyUnitAll)],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
objId: masterObjId,
|
||||
purchaseOrderNo: poNo,
|
||||
message: isNew ? "등록되었습니다." : "수정되었습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("order/save:", e);
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "저장 중 오류가 발생했습니다." },
|
||||
{ status: 500 },
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 원본: purchaseOrder.xml#purchaseOrderStatusByProject
|
||||
// 발주관리 > 현황 — (프로젝트 × 유닛) 단위 발주율 / 재발주 / 턴키 / 총발주금액 집계
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// 필터 조건 (모든 서브쿼리에 공통 적용)
|
||||
const year = body.year ? String(body.year) : "";
|
||||
const projectNo = body.project_no ? String(body.project_no) : "";
|
||||
const unitCode = body.unit_code ? String(body.unit_code) : "";
|
||||
|
||||
// 공통 WHERE를 CTE별로 동일하게 쓰기 위해 파라미터 플레이스홀더를 정렬
|
||||
// $1=year(POM.regdate YYYY), $2=project_no(프로젝트 LIKE), $3=unit_code
|
||||
const params: unknown[] = [year || null, projectNo || null, unitCode || null];
|
||||
|
||||
const sql = `
|
||||
WITH base AS (
|
||||
-- 프로젝트 × 유닛(WBS) × (있다면)구매BOM 카르테시안
|
||||
SELECT CM.objid::text AS project_objid,
|
||||
COALESCE(CM.project_no, CM.contract_no) AS project_no,
|
||||
CM.customer_project_name,
|
||||
CM.project_name,
|
||||
TO_CHAR(CM.regdate, 'YYYY') AS cm_year,
|
||||
(SELECT supply_name FROM supply_mng O WHERE O.objid::text = CM.customer_objid LIMIT 1) AS customer_name,
|
||||
PWT.objid::text AS unit_code,
|
||||
COALESCE(PWT.unit_no,'') || '-' || COALESCE(PWT.task_name,'') AS unit_part_name,
|
||||
PBR.objid::text AS bom_report_objid,
|
||||
(SELECT COUNT(*) FROM bom_part_qty A WHERE A.bom_report_objid = PBR.objid) AS bom_cnt
|
||||
FROM project_mgmt CM
|
||||
LEFT JOIN pms_wbs_task PWT ON PWT.contract_objid = CM.objid
|
||||
LEFT JOIN part_bom_report PBR ON PBR.contract_objid = CM.objid AND PBR.unit_code = PWT.objid::varchar
|
||||
WHERE ($2::text IS NULL OR COALESCE(CM.project_no,'') ILIKE '%' || $2 || '%' OR COALESCE(CM.contract_no,'') ILIKE '%' || $2 || '%')
|
||||
AND ($3::text IS NULL OR PWT.objid::text = $3)
|
||||
),
|
||||
-- 구매BOM 부품 개수 (distinct part_no) — 발주율 분모
|
||||
bom_rollup AS (
|
||||
SELECT PBR.objid::text AS bom_report_objid,
|
||||
COUNT(DISTINCT BPQ.part_no) FILTER (
|
||||
WHERE COALESCE(PM.part_type,'') != ''
|
||||
AND BPQ.status IN ('beforeEdit','editing','deleting','deploy')
|
||||
) AS total_bom_part_cnt
|
||||
FROM part_bom_report PBR
|
||||
LEFT JOIN bom_part_qty BPQ ON BPQ.bom_report_objid = PBR.objid
|
||||
LEFT JOIN part_mng PM ON COALESCE(NULLIF(BPQ.last_part_objid,''), BPQ.part_no) = PM.objid::text
|
||||
GROUP BY PBR.objid
|
||||
),
|
||||
-- 발주 집계 (approvalComplete만) — 프로젝트 × 유닛 단위
|
||||
po_rollup AS (
|
||||
SELECT POM.contract_mgmt_objid::text AS project_objid,
|
||||
POM.unit_code,
|
||||
SUM(COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric) AS total_supply_unit_price,
|
||||
SUM(CASE WHEN PM.part_type IN ('0000063')
|
||||
THEN COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric END) AS price_pt_1,
|
||||
SUM(CASE WHEN PM.part_type IN ('0000064','0001540','0001398','0001397','0001396')
|
||||
THEN COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric END) AS price_pt_2,
|
||||
SUM(CASE WHEN PM.part_type IN ('0000065')
|
||||
THEN COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric END) AS price_pt_3,
|
||||
SUM(CASE WHEN PM.part_type NOT IN ('0000063','0000064','0001540','0001398','0001397','0001396','0000065') OR PM.part_type IS NULL
|
||||
THEN COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric END) AS price_pt_etc,
|
||||
COUNT(DISTINCT POP.part_objid) AS total_po_part_cnt
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POP.purchase_order_master_objid = POM.objid
|
||||
LEFT JOIN part_mng PM ON POP.part_objid = PM.objid::text
|
||||
WHERE POM.status = 'approvalComplete'
|
||||
AND COALESCE(POM.type,'') != '0001785'
|
||||
AND ($1::text IS NULL OR TO_CHAR(POM.regdate, 'YYYY') = $1)
|
||||
GROUP BY POM.contract_mgmt_objid, POM.unit_code
|
||||
),
|
||||
-- 재발주 집계
|
||||
re_po_rollup AS (
|
||||
SELECT POM.contract_mgmt_objid::text AS project_objid,
|
||||
POM.unit_code,
|
||||
COUNT(DISTINCT POM.objid) AS re_count,
|
||||
SUM(COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric) AS re_price
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POP.purchase_order_master_objid = POM.objid
|
||||
WHERE POM.status = 'approvalComplete'
|
||||
AND POM.order_type_cd = '0001408'
|
||||
AND COALESCE(POM.type,'') != '0001785'
|
||||
AND ($1::text IS NULL OR TO_CHAR(POM.regdate, 'YYYY') = $1)
|
||||
GROUP BY POM.contract_mgmt_objid, POM.unit_code
|
||||
),
|
||||
-- 턴키 집계
|
||||
turnkey_rollup AS (
|
||||
SELECT POM.contract_mgmt_objid::text AS project_objid,
|
||||
POM.unit_code,
|
||||
COUNT(DISTINCT POM.objid) AS turnkey_count,
|
||||
SUM(COALESCE(NULLIF(POP.supply_unit_price,''),'0')::numeric) AS turnkey_price
|
||||
FROM purchase_order_master POM
|
||||
JOIN purchase_order_part POP ON POP.purchase_order_master_objid = POM.objid
|
||||
WHERE POM.status = 'approvalComplete'
|
||||
AND POM.type = '0001785'
|
||||
AND ($1::text IS NULL OR TO_CHAR(POM.regdate, 'YYYY') = $1)
|
||||
GROUP BY POM.contract_mgmt_objid, POM.unit_code
|
||||
),
|
||||
-- 총 발주금액 (프로젝트 × 유닛) — total_price_all이 비어있으면 total_price로 폴백
|
||||
total_rollup AS (
|
||||
SELECT POM.contract_mgmt_objid::text AS project_objid,
|
||||
POM.unit_code,
|
||||
SUM(CASE WHEN COALESCE(NULLIF(POM.total_price_all,''),'') <> ''
|
||||
THEN POM.total_price_all::numeric
|
||||
ELSE COALESCE(NULLIF(POM.total_price,''),'0')::numeric END) AS total_price_all,
|
||||
SUM(COALESCE(NULLIF(POM.total_supply_price,''),'0')::numeric) AS total_supply_price,
|
||||
SUM(COALESCE(NULLIF(POM.discount_price,''),'0')::numeric) AS discount_price,
|
||||
SUM(COALESCE(NULLIF(POM.total_price,''),'0')::numeric) AS total_price
|
||||
FROM purchase_order_master POM
|
||||
WHERE POM.status = 'approvalComplete'
|
||||
AND (COALESCE(POM.multi_master_yn,'') = 'Y' OR (COALESCE(POM.multi_master_yn,'') != 'Y' AND COALESCE(POM.multi_yn,'') != 'Y'))
|
||||
AND ($1::text IS NULL OR TO_CHAR(POM.regdate, 'YYYY') = $1)
|
||||
GROUP BY POM.contract_mgmt_objid, POM.unit_code
|
||||
)
|
||||
SELECT B.project_objid AS "PROJECT_OBJID",
|
||||
B.cm_year AS "CM_YEAR",
|
||||
B.customer_name AS "CUSTOMER_NAME",
|
||||
COALESCE(B.project_name, B.customer_project_name) AS "PROJECT_NAME",
|
||||
B.customer_project_name AS "CUSTOMER_PROJECT_NAME",
|
||||
B.unit_part_name AS "UNIT_PART_NAME",
|
||||
B.project_no AS "PROJECT_NO",
|
||||
B.bom_report_objid AS "BOM_REPORT_OBJID",
|
||||
COALESCE(B.bom_cnt, 0) AS "BOM_CNT",
|
||||
COALESCE(BR.total_bom_part_cnt, 0) AS "TOTAL_BOM_PART_CNT",
|
||||
COALESCE(P.total_po_part_cnt, 0) AS "TOTAL_PO_PART_CNT",
|
||||
GREATEST(COALESCE(BR.total_bom_part_cnt, 0) - COALESCE(P.total_po_part_cnt, 0), 0) AS "NON_PO_PART_CNT",
|
||||
CASE WHEN COALESCE(BR.total_bom_part_cnt, 0) = 0 THEN 0
|
||||
ELSE ROUND(COALESCE(P.total_po_part_cnt, 0)::numeric / BR.total_bom_part_cnt * 100, 1)
|
||||
END AS "RATE_PO",
|
||||
COALESCE(P.price_pt_1, 0) AS "PRICE_PT_1",
|
||||
COALESCE(P.price_pt_2, 0) AS "PRICE_PT_2",
|
||||
COALESCE(P.price_pt_3, 0) AS "PRICE_PT_3",
|
||||
COALESCE(P.price_pt_etc, 0) AS "PRICE_PT_ETC",
|
||||
COALESCE(R.re_count, 0) AS "RE_COUNT",
|
||||
COALESCE(R.re_price, 0) AS "RE_PRICE",
|
||||
COALESCE(TK.turnkey_count, 0) AS "TURNKEY_COUNT",
|
||||
COALESCE(TK.turnkey_price, 0) AS "TURNKEY_PRICE",
|
||||
COALESCE(TR.total_price_all, 0) AS "TOTAL_PRICE_ALL",
|
||||
COALESCE(TR.total_price, 0) AS "TOTAL_PRICE"
|
||||
FROM base B
|
||||
LEFT JOIN bom_rollup BR ON BR.bom_report_objid = B.bom_report_objid
|
||||
LEFT JOIN po_rollup P ON P.project_objid = B.project_objid AND P.unit_code = B.unit_code
|
||||
LEFT JOIN re_po_rollup R ON R.project_objid = B.project_objid AND R.unit_code = B.unit_code
|
||||
LEFT JOIN turnkey_rollup TK ON TK.project_objid = B.project_objid AND TK.unit_code = B.unit_code
|
||||
LEFT JOIN total_rollup TR ON TR.project_objid = B.project_objid AND TR.unit_code = B.unit_code
|
||||
ORDER BY B.project_no DESC NULLS LAST, B.unit_part_name
|
||||
`;
|
||||
|
||||
try {
|
||||
const rows = await queryRows(sql, params);
|
||||
// 그랜드 토탈 — FITO 원본 purchaseOrderStatusByProjectSum 매퍼와 일치:
|
||||
// 총발주금액 = SUM(total_real_supply_price) (VAT 포함 실공급가 합계)
|
||||
// 단일발주금액 = SUM(total_supply_price) (공급가 합계)
|
||||
const totals = await queryOne(
|
||||
`SELECT SUM(COALESCE(NULLIF(POM.total_real_supply_price,''),'0')::numeric) AS "TOTAL_PRICE_ALL_SUM",
|
||||
SUM(COALESCE(NULLIF(POM.total_supply_price,''),'0')::numeric) AS "SINGLE_PRICE_SUM"
|
||||
FROM purchase_order_master POM
|
||||
WHERE POM.status = 'approvalComplete'
|
||||
AND ($1::text IS NULL OR TO_CHAR(POM.regdate, 'YYYY') = $1)`,
|
||||
[year || null],
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length, SUMS: totals });
|
||||
} catch (e) {
|
||||
console.error("order/status:", e);
|
||||
return NextResponse.json({ success: false, message: "조회 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// PartMngController / partmgmt.getPartMgntList 대응
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const { part_no, part_name, part_type, page = 1 } = body;
|
||||
const countPerPage = 20;
|
||||
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (part_no) { conditions.push(`P.PART_NO LIKE '%' || $${idx++} || '%'`); params.push(part_no); }
|
||||
if (part_name) { conditions.push(`P.PART_NAME LIKE '%' || $${idx++} || '%'`); params.push(part_name); }
|
||||
if (part_type) { conditions.push(`P.PART_TYPE = $${idx++}`); params.push(part_type); }
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
const offset = (Number(page) - 1) * countPerPage;
|
||||
|
||||
const list = await queryRows(
|
||||
`SELECT P.OBJID, P.PART_NO, P.PART_NAME,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE PARENT_CODE_ID = '0000062' AND CODE_ID = P.PART_TYPE LIMIT 1), '') AS PART_TYPE_NAME,
|
||||
P.SPEC, P.MATERIAL,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE PARENT_CODE_ID = '0005126' AND CODE_ID = P.MATERIAL LIMIT 1), '') AS MATERIAL_NAME,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE PARENT_CODE_ID = '0000059' AND CODE_ID = P.UNIT LIMIT 1), '') AS UNIT_NAME,
|
||||
P.WEIGHT, P.MAKER,
|
||||
(SELECT USER_NAME FROM USER_INFO WHERE USER_ID = P.WRITER LIMIT 1) AS WRITER_NAME,
|
||||
TO_CHAR(P.REGDATE::date, 'YYYY-MM-DD') AS REGDATE,
|
||||
COALESCE((SELECT COUNT(*) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = P.OBJID AND DOC_TYPE = 'PART_2D'), 0) AS DRAWING_2D_CNT,
|
||||
COALESCE((SELECT COUNT(*) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = P.OBJID AND DOC_TYPE = 'PART_3D'), 0) AS DRAWING_3D_CNT
|
||||
FROM PART_MGMT P
|
||||
WHERE ${where}
|
||||
ORDER BY P.REGDATE DESC
|
||||
LIMIT $${idx++} OFFSET $${idx++}`,
|
||||
[...params, countPerPage, offset]
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: list, TOTAL_CNT: list.length });
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// PART 등록 페이지 전용 조회 (part-register에서 호출)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const body = await request.json();
|
||||
|
||||
const conditions: string[] = ["P.status = 'release'"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
if (body.SEARCH_PART_NO) { conditions.push(`P.part_no LIKE '%' || $${idx++} || '%'`); params.push(body.SEARCH_PART_NO); }
|
||||
if (body.SEARCH_PART_NAME) { conditions.push(`P.part_name LIKE '%' || $${idx++} || '%'`); params.push(body.SEARCH_PART_NAME); }
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT P.objid::text AS "OBJID", P.part_no AS "PART_NO", P.part_name AS "PART_NAME",
|
||||
P.qty AS "BOM_QTY", P.material AS "MATERIAL", P.spec AS "SPEC",
|
||||
P.post_processing AS "POST_PROCESSING", P.maker AS "MAKER", P.revision AS "REVISION",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = P.part_type LIMIT 1), '') AS "PART_TYPE_TITLE",
|
||||
P.remark AS "REMARK",
|
||||
COALESCE((SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '3D_CAD'), 0) AS "CU01_CNT",
|
||||
COALESCE((SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '2D_DRAWING_CAD'), 0) AS "CU02_CNT",
|
||||
COALESCE((SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '2D_PDF_CAD'), 0) AS "CU03_CNT"
|
||||
FROM part_mng P
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY P.part_no, P.revision DESC
|
||||
`,
|
||||
params
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne, queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { STD_CATEGORIES } from "@/lib/procurement-std";
|
||||
|
||||
// 팝업 로드: 코드 단건 + 자재코드 단건 + 자재 팝업용 드롭다운 옵션 5종
|
||||
// body: { mode: "code"|"material"|"options", objId? }
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const mode = String(body.mode || "code");
|
||||
const objId = body.objId ? String(body.objId) : "";
|
||||
|
||||
if (mode === "options") {
|
||||
// 자재코드 팝업에서 쓰는 5개 카테고리 옵션 목록
|
||||
const result: Record<string, { value: string; label: string; code_id: string; code_name: string }[]> = {};
|
||||
for (const def of Object.values(STD_CATEGORIES)) {
|
||||
const rows = await queryRows(
|
||||
`SELECT objid::text AS "OBJID", code_id AS "CODE_ID", code_name AS "CODE_NAME"
|
||||
FROM procurement_standard
|
||||
WHERE category = $1
|
||||
AND COALESCE(status, 'active') = 'active'
|
||||
ORDER BY code_id`,
|
||||
[def.category]
|
||||
);
|
||||
result[def.alias] = rows.map((r) => ({
|
||||
value: String(r.OBJID),
|
||||
label: `${r.CODE_ID} - ${r.CODE_NAME}`,
|
||||
code_id: String(r.CODE_ID),
|
||||
code_name: String(r.CODE_NAME),
|
||||
}));
|
||||
}
|
||||
return NextResponse.json({ success: true, data: result });
|
||||
}
|
||||
|
||||
if (mode === "material") {
|
||||
if (!objId) return NextResponse.json({ success: true, data: null });
|
||||
const row = await queryOne(
|
||||
`SELECT T.objid::text AS "OBJID",
|
||||
T.part_no AS "PART_NO",
|
||||
T.major_category AS "MAJOR_CATEGORY",
|
||||
T.sub_category AS "SUB_CATEGORY",
|
||||
T.maker AS "MAKER",
|
||||
T.part_name AS "PART_NAME",
|
||||
T.spec AS "SPEC",
|
||||
T.code1 AS "CODE1",
|
||||
T.code2 AS "CODE2",
|
||||
T.code3 AS "CODE3",
|
||||
T.code4 AS "CODE4",
|
||||
T.code5 AS "CODE5",
|
||||
(SELECT objid::text FROM procurement_standard WHERE code_id = T.code1 AND category = '0001668' LIMIT 1) AS "CODE_OBJID1",
|
||||
(SELECT objid::text FROM procurement_standard WHERE code_id = T.code2 AND category = '0001669' LIMIT 1) AS "CODE_OBJID2",
|
||||
(SELECT objid::text FROM procurement_standard WHERE code_id = T.code3 AND category = '0001670' LIMIT 1) AS "CODE_OBJID3",
|
||||
(SELECT objid::text FROM procurement_standard WHERE code_id = T.code4 AND category = '0001671' LIMIT 1) AS "CODE_OBJID4",
|
||||
(SELECT objid::text FROM procurement_standard WHERE code_id = T.code5 AND category = '0001672' LIMIT 1) AS "CODE_OBJID5",
|
||||
T.status AS "STATUS"
|
||||
FROM part_mng T
|
||||
WHERE T.objid::text = $1`,
|
||||
[objId]
|
||||
);
|
||||
return NextResponse.json({ success: true, data: row || null });
|
||||
}
|
||||
|
||||
// code mode
|
||||
if (!objId) return NextResponse.json({ success: true, data: null });
|
||||
const row = await queryOne(
|
||||
`SELECT objid::text AS "OBJID",
|
||||
code_id AS "CODE_ID",
|
||||
code_name AS "CODE_NAME",
|
||||
detail AS "DETAIL",
|
||||
category AS "CATEGORY",
|
||||
COALESCE(status, 'active') AS "STATUS"
|
||||
FROM procurement_standard
|
||||
WHERE objid::text = $1`,
|
||||
[objId]
|
||||
);
|
||||
return NextResponse.json({ success: true, data: row || null });
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 자재코드 저장 (procurStandMgmt.mergeMaterialCode 대응)
|
||||
// body: { actionType, objId?, code_objid1..5 } — procurement_standard.objid 5개
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const isNew = !body.objId || body.actionType === "regist";
|
||||
const objId = isNew ? createObjectId() : String(body.objId);
|
||||
|
||||
const codeObjIds: string[] = [
|
||||
String(body.code_objid1 || ""),
|
||||
String(body.code_objid2 || ""),
|
||||
String(body.code_objid3 || ""),
|
||||
String(body.code_objid4 || ""),
|
||||
String(body.code_objid5 || ""),
|
||||
];
|
||||
|
||||
if (codeObjIds.some((v) => !v)) {
|
||||
return NextResponse.json({ success: false, message: "대분류/중분류/Maker/품명/규격을 모두 선택하세요." }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// 5개 카테고리 각각의 code_id, code_name 조회
|
||||
const categories = ["0001668", "0001669", "0001670", "0001671", "0001672"];
|
||||
const codes: { code_id: string; code_name: string }[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const row = await queryOne(
|
||||
`SELECT code_id AS "CODE_ID", code_name AS "CODE_NAME"
|
||||
FROM procurement_standard
|
||||
WHERE objid::text = $1 AND category = $2`,
|
||||
[codeObjIds[i], categories[i]]
|
||||
);
|
||||
if (!row) {
|
||||
return NextResponse.json({ success: false, message: `${["대분류","중분류","Maker","품명","규격"][i]} 선택값이 올바르지 않습니다.` }, { status: 400 });
|
||||
}
|
||||
codes.push({ code_id: String(row.CODE_ID), code_name: String(row.CODE_NAME) });
|
||||
}
|
||||
const materialCode = codes.map((c) => c.code_id).join("");
|
||||
|
||||
// 중복 체크
|
||||
const dup = await queryOne(
|
||||
`SELECT objid::text AS "OBJID" FROM part_mng
|
||||
WHERE objid::text != $1
|
||||
AND REPLACE(TRIM(UPPER(part_no)), ' ', '') = REPLACE(TRIM(UPPER($2)), ' ', '')`,
|
||||
[objId, materialCode]
|
||||
);
|
||||
if (dup) {
|
||||
return NextResponse.json({ success: false, message: "중복되는 자재코드가 존재합니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
// EO_NO 생성 (원본: 'EOB' + yy + '-' + 4자리 순번, 신규만)
|
||||
let eoNo: string | null = null;
|
||||
if (isNew) {
|
||||
const row = await queryOne<{ EO_NO: string }>(
|
||||
`SELECT 'EOB' || TO_CHAR(NOW(), 'YY') || '-' ||
|
||||
LPAD(
|
||||
(SELECT COALESCE(MAX(SUBSTR(eo_no, 7, 8)::INTEGER) + 1, 1)::TEXT
|
||||
FROM part_mng
|
||||
WHERE eo_no LIKE 'EOB%'), 4, '0'
|
||||
) AS "EO_NO"`,
|
||||
[]
|
||||
);
|
||||
eoNo = row ? String(row.EO_NO) : null;
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
await execute(
|
||||
`INSERT INTO part_mng (
|
||||
objid, part_no, part_name, spec, major_category, sub_category, maker,
|
||||
code1, code2, code3, code4, code5,
|
||||
part_type, revision, status, reg_date, writer, is_last, is_new, is_longd, eo_no, design_date
|
||||
) VALUES (
|
||||
$1::numeric, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12,
|
||||
'0001788', 'RE', 'release', NOW(), $13, '1', '1', '1', $14, TO_CHAR(NOW(), 'YYYY-MM-DD')
|
||||
)`,
|
||||
[
|
||||
objId, materialCode, codes[3].code_name, codes[4].code_name,
|
||||
codes[0].code_name, codes[1].code_name, codes[2].code_name,
|
||||
codes[0].code_id, codes[1].code_id, codes[2].code_id, codes[3].code_id, codes[4].code_id,
|
||||
user.userId, eoNo,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
await execute(
|
||||
`UPDATE part_mng SET
|
||||
part_no = $2,
|
||||
part_name = $3,
|
||||
spec = $4,
|
||||
major_category = $5,
|
||||
sub_category = $6,
|
||||
maker = $7,
|
||||
code1 = $8, code2 = $9, code3 = $10, code4 = $11, code5 = $12,
|
||||
edit_date = NOW(),
|
||||
writer = $13
|
||||
WHERE objid::text = $1`,
|
||||
[
|
||||
objId, materialCode, codes[3].code_name, codes[4].code_name,
|
||||
codes[0].code_name, codes[1].code_name, codes[2].code_name,
|
||||
codes[0].code_id, codes[1].code_id, codes[2].code_id, codes[3].code_id, codes[4].code_id,
|
||||
user.userId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, objId, materialCode, message: isNew ? "등록되었습니다." : "수정되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("Material code save:", error);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { getStdCategoryByAlias } from "@/lib/procurement-std";
|
||||
|
||||
// 구매품표준 중복체크 (procurStandMgmt.overlapStandardMng / overlapMaterialCode 대응)
|
||||
// body: { mode: "code"|"material", objId, code_group?, code_id?, code_name?, material_code? }
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const mode = String(body.mode || "code");
|
||||
const objId = String(body.objId || "0");
|
||||
|
||||
if (mode === "material") {
|
||||
const materialCode = String(body.material_code || "").trim().toUpperCase();
|
||||
if (!materialCode) return NextResponse.json({ duplicate: false });
|
||||
const rows = await queryRows(
|
||||
`SELECT objid::text AS "OBJID"
|
||||
FROM part_mng
|
||||
WHERE objid::text != $1
|
||||
AND REPLACE(TRIM(UPPER(part_no)), ' ', '') = REPLACE(TRIM(UPPER($2)), ' ', '')`,
|
||||
[objId, materialCode]
|
||||
);
|
||||
return NextResponse.json({ duplicate: rows.length > 0 });
|
||||
}
|
||||
|
||||
// code mode
|
||||
const alias = String(body.code_group || "");
|
||||
const def = getStdCategoryByAlias(alias);
|
||||
if (!def) return NextResponse.json({ duplicate: false });
|
||||
|
||||
const codeId = String(body.code_id || "").trim().toUpperCase();
|
||||
const codeName = String(body.code_name || "").trim();
|
||||
const rows = await queryRows(
|
||||
`SELECT objid::text AS "OBJID"
|
||||
FROM procurement_standard
|
||||
WHERE objid::text != $1
|
||||
AND category = $2
|
||||
AND (
|
||||
REPLACE(TRIM(UPPER(code_name)), ' ', '') = REPLACE(TRIM(UPPER($3)), ' ', '')
|
||||
OR REPLACE(TRIM(UPPER(code_id)), ' ', '') = REPLACE(TRIM(UPPER($4)), ' ', '')
|
||||
)`,
|
||||
[objId, def.category, codeName, codeId]
|
||||
);
|
||||
return NextResponse.json({ duplicate: rows.length > 0 });
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { getStdCategoryByAlias } from "@/lib/procurement-std";
|
||||
|
||||
// 구매품표준관리 목록 조회 (procurStandMgmt.mainCategoryGridList / materialCodeGridList 대응)
|
||||
// code_group = "CODE1"~"CODE5" → procurement_standard (카테고리별)
|
||||
// code_group = "MATERIAL_CODE" → part_mng
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const codeGroup = String(body.code_group || "");
|
||||
|
||||
// === 자재코드 모드 ===
|
||||
if (codeGroup === "MATERIAL_CODE") {
|
||||
const conditions: string[] = ["T.is_new = '1'"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.part_no) {
|
||||
conditions.push(`UPPER(T.part_no) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.part_no);
|
||||
}
|
||||
if (body.large_cd) {
|
||||
conditions.push(`UPPER(T.major_category) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.large_cd);
|
||||
}
|
||||
if (body.middle_cd) {
|
||||
conditions.push(`UPPER(T.sub_category) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.middle_cd);
|
||||
}
|
||||
if (body.maker) {
|
||||
conditions.push(`UPPER(T.maker) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.maker);
|
||||
}
|
||||
if (body.part_name) {
|
||||
conditions.push(`UPPER(T.part_name) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.part_name);
|
||||
}
|
||||
if (body.spec) {
|
||||
conditions.push(`UPPER(T.spec) LIKE UPPER('%' || $${idx++} || '%')`);
|
||||
params.push(body.spec);
|
||||
}
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT T.objid::text AS "OBJID",
|
||||
T.part_no AS "PART_NO",
|
||||
T.major_category AS "MAJOR_CATEGORY",
|
||||
T.sub_category AS "SUB_CATEGORY",
|
||||
T.maker AS "MAKER",
|
||||
T.part_name AS "PART_NAME",
|
||||
T.spec AS "SPEC",
|
||||
T.code1 AS "CODE1",
|
||||
T.code2 AS "CODE2",
|
||||
T.code3 AS "CODE3",
|
||||
T.code4 AS "CODE4",
|
||||
T.code5 AS "CODE5",
|
||||
T.revision AS "REVISION",
|
||||
T.eo_no AS "EO_NO",
|
||||
TO_CHAR(T.eo_date::date, 'YYYY-MM-DD') AS "EO_DATE"
|
||||
FROM part_mng T
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY T.reg_date DESC
|
||||
`,
|
||||
params
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
|
||||
// === 코드1~5 카테고리 모드 ===
|
||||
const def = getStdCategoryByAlias(codeGroup);
|
||||
const conditions: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (def) {
|
||||
conditions.push(`PS.category = $${idx++}`);
|
||||
params.push(def.category);
|
||||
}
|
||||
if (body.code_name) {
|
||||
conditions.push(`PS.code_name LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.code_name);
|
||||
}
|
||||
if (body.code_id) {
|
||||
conditions.push(`PS.code_id LIKE '%' || $${idx++} || '%'`);
|
||||
params.push(body.code_id);
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? conditions.join(" AND ") : "1=1";
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT PS.objid::text AS "OBJID",
|
||||
PS.code_id AS "CODE_ID",
|
||||
PS.code_name AS "CODE_NAME",
|
||||
PS.detail AS "DETAIL",
|
||||
PS.category AS "CATEGORY",
|
||||
COALESCE(PS.status, 'active') AS "STATUS",
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = PS.writer LIMIT 1), PS.writer) AS "WRITER_NAME",
|
||||
TO_CHAR(PS.regdate, 'YYYY-MM-DD') AS "REG_DATE"
|
||||
FROM procurement_standard PS
|
||||
WHERE ${where}
|
||||
ORDER BY PS.code_id`,
|
||||
params
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
|
||||
// 구매품표준관리 삭제
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const { objIds, mode } = body;
|
||||
|
||||
if (!objIds || !Array.isArray(objIds) || objIds.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "삭제할 항목을 선택하세요." });
|
||||
}
|
||||
|
||||
try {
|
||||
const placeholders = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
if (mode === "material") {
|
||||
await execute(`DELETE FROM part_mng WHERE objid IN (${placeholders})`, objIds);
|
||||
} else {
|
||||
await execute(`DELETE FROM procurement_standard WHERE objid IN (${placeholders})`, objIds);
|
||||
}
|
||||
return NextResponse.json({ success: true, message: `${objIds.length}건이 삭제되었습니다.` });
|
||||
} catch (error) {
|
||||
console.error("Procurement std delete:", error);
|
||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
import { getStdCategoryByAlias, validateStdCodeId } from "@/lib/procurement-std";
|
||||
|
||||
// 구매품표준코드 저장 (procurStandMgmt.saveCODE1Info 대응)
|
||||
// body: { actionType, objId?, code_group(alias), code_id, code_name, detail?, status? }
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const alias = String(body.code_group || "");
|
||||
const def = getStdCategoryByAlias(alias);
|
||||
if (!def) {
|
||||
return NextResponse.json({ success: false, message: "카테고리가 올바르지 않습니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
const codeId = String(body.code_id || "").toUpperCase();
|
||||
const codeName = String(body.code_name || "").trim();
|
||||
|
||||
if (!codeName) {
|
||||
return NextResponse.json({ success: false, message: "구분을 입력하세요." }, { status: 400 });
|
||||
}
|
||||
const v = validateStdCodeId(alias, codeId);
|
||||
if (!v.ok) {
|
||||
return NextResponse.json({ success: false, message: v.message }, { status: 400 });
|
||||
}
|
||||
|
||||
const isNew = !body.objId || body.actionType === "regist";
|
||||
const objId = isNew ? createObjectId() : String(body.objId);
|
||||
const detail = def.showDetail ? (body.detail || "") : "";
|
||||
const status = body.status || "active";
|
||||
|
||||
try {
|
||||
await execute(
|
||||
`INSERT INTO procurement_standard (objid, code_name, code_id, detail, category, regdate, writer, status)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW(), $6, $7)
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
code_name = EXCLUDED.code_name,
|
||||
code_id = EXCLUDED.code_id,
|
||||
detail = EXCLUDED.detail,
|
||||
category = EXCLUDED.category,
|
||||
status = EXCLUDED.status,
|
||||
editdate = NOW(),
|
||||
edit_user = EXCLUDED.writer`,
|
||||
[objId, codeName, codeId, detail, def.category, user.userId, status]
|
||||
);
|
||||
// 코드명 변경 시 part_mng 쪽 컬럼 동기화 (원본 updateCodeName 대응)
|
||||
// 해당 category에 소속된 code_id일 때만, part_mng.MAJOR_CATEGORY 등 컬럼 업데이트
|
||||
const colMap: Record<string, string> = {
|
||||
CODE1: "major_category",
|
||||
CODE2: "sub_category",
|
||||
CODE3: "maker",
|
||||
CODE4: "part_name",
|
||||
CODE5: "spec",
|
||||
};
|
||||
const partMngCol = colMap[alias];
|
||||
if (partMngCol) {
|
||||
await execute(
|
||||
`UPDATE part_mng SET ${partMngCol} = $1 WHERE ${alias.toLowerCase()} = $2`,
|
||||
[codeName, codeId]
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, objId, message: isNew ? "등록되었습니다." : "수정되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("Procurement std save:", error);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const { objId } = await request.json();
|
||||
if (!objId) return NextResponse.json({ success: false, message: "objId required" });
|
||||
|
||||
const info = await queryOne(
|
||||
`SELECT P.objid::text AS "OBJID", P.product_category AS "PRODUCT_CATEGORY",
|
||||
P.product_type AS "PRODUCT_TYPE", P.product_name AS "PRODUCT_NAME",
|
||||
P.product_code AS "PRODUCT_CODE", P.product_name_code AS "PRODUCT_NAME_CODE",
|
||||
P.product_grade AS "PRODUCT_GRADE", P.product_ton AS "PRODUCT_TON",
|
||||
P.product_boom AS "PRODUCT_BOOM", P.product_vehicle AS "PRODUCT_VEHICLE",
|
||||
P.production_flag AS "PRODUCTION_FLAG", P.note AS "NOTE",
|
||||
TO_CHAR(P.regdate, 'YYYY-MM-DD') AS "REGDATE"
|
||||
FROM product_mgmt P WHERE P.objid = $1`, [objId]
|
||||
);
|
||||
if (!info) return NextResponse.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
return NextResponse.json({ success: true, data: info });
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, queryOne, execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// ProductMgmtController.productmgmtList 대응
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const { year, product_category, page = 1, countPerPage = 20 } = body;
|
||||
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
if (year) {
|
||||
conditions.push(`EXTRACT(YEAR FROM P.REGDATE) = $${paramIdx++}`);
|
||||
params.push(year);
|
||||
}
|
||||
if (product_category) {
|
||||
conditions.push(`P.PRODUCT_CATEGORY = $${paramIdx++}`);
|
||||
params.push(product_category);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const countResult = await queryOne<{ CNT: string }>(
|
||||
`SELECT COUNT(*) AS CNT FROM PRODUCT_MGMT P WHERE ${where}`,
|
||||
params
|
||||
);
|
||||
const totalCount = Number(countResult?.CNT || 0);
|
||||
const maxPage = Math.max(1, Math.ceil(totalCount / countPerPage));
|
||||
const offset = (Number(page) - 1) * countPerPage;
|
||||
|
||||
const list = await queryRows(
|
||||
`SELECT P.OBJID::text AS "OBJID", P.PRODUCT_CATEGORY,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE PARENT_CODE_ID = '0000917' AND CODE_ID = P.PRODUCT_CATEGORY LIMIT 1), '') AS "PRODUCT_CATEGORY_NAME",
|
||||
P.PRODUCT_TYPE,
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE PARENT_CODE_ID = '0005116' AND CODE_ID = P.PRODUCT_TYPE LIMIT 1), '') AS "PRODUCT_TYPE_NAME",
|
||||
P.PRODUCT_NAME AS "PRODUCT_NAME",
|
||||
P.PRODUCT_CODE AS "PRODUCT_CODE",
|
||||
COALESCE((SELECT USER_NAME FROM USER_INFO WHERE USER_ID = P.WRITER LIMIT 1), P.WRITER) AS "WRITER_NAME",
|
||||
TO_CHAR(P.REGDATE, 'YYYY-MM-DD') AS "REGDATE",
|
||||
COALESCE((SELECT CODE_NAME FROM COMM_CODE WHERE PARENT_CODE_ID = '0000039' AND CODE_ID = P.PRODUCTION_FLAG LIMIT 1), '') AS "PRODUCTION_FLAG_NAME",
|
||||
COALESCE((SELECT COUNT(*) FROM ATTACH_FILE_INFO WHERE TARGET_OBJID = P.OBJID::text), 0) AS "FILE_CNT"
|
||||
FROM PRODUCT_MGMT P
|
||||
WHERE ${where}
|
||||
ORDER BY P.REGDATE DESC
|
||||
LIMIT $${paramIdx++} OFFSET $${paramIdx++}`,
|
||||
[...params, countPerPage, offset]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
RESULTLIST: list,
|
||||
TOTAL_CNT: totalCount,
|
||||
MAX_PAGE_SIZE: maxPage,
|
||||
COUNT_PER_PAGE: countPerPage,
|
||||
});
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const { objIds } = body;
|
||||
|
||||
if (!objIds || !Array.isArray(objIds) || objIds.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "선택한 항목이 없습니다." });
|
||||
}
|
||||
|
||||
try {
|
||||
const placeholders = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
await execute(`DELETE FROM PRODUCT_MGMT WHERE OBJID IN (${placeholders})`, objIds);
|
||||
return NextResponse.json({ success: true, message: "삭제되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("Product delete error:", error);
|
||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const isNew = body.actionType === "regist" || !body.objId;
|
||||
|
||||
try {
|
||||
if (isNew) {
|
||||
if (body.product_code) {
|
||||
const dup = await queryOne(`SELECT COUNT(*) AS cnt FROM product_mgmt WHERE product_code = $1`, [body.product_code]);
|
||||
if (Number(dup?.cnt || 0) > 0) {
|
||||
return NextResponse.json({ success: false, message: "이미 등록된 제품코드입니다." });
|
||||
}
|
||||
}
|
||||
|
||||
const objId = createObjectId();
|
||||
await execute(
|
||||
`INSERT INTO product_mgmt (objid, product_category, product_type, product_name,
|
||||
product_code, product_name_code, product_grade, product_ton, product_boom,
|
||||
product_vehicle, production_flag, note, writer, regdate)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now())`,
|
||||
[objId, body.product_category||"", body.product_type||"",
|
||||
body.product_name||"", body.product_code||"", body.product_name_code||"",
|
||||
body.product_grade||"", body.product_ton||"", body.product_boom||"",
|
||||
body.product_vehicle||"", body.production_flag||"", body.note||"",
|
||||
user.userId]
|
||||
);
|
||||
return NextResponse.json({ success: true, objId, message: "등록되었습니다." });
|
||||
} else {
|
||||
await execute(
|
||||
`UPDATE product_mgmt SET
|
||||
product_category=$1, product_type=$2, product_name=$3, product_code=$4,
|
||||
product_name_code=$5, product_grade=$6, product_ton=$7, product_boom=$8,
|
||||
product_vehicle=$9, production_flag=$10, note=$11
|
||||
WHERE objid=$12`,
|
||||
[body.product_category||"", body.product_type||"",
|
||||
body.product_name||"", body.product_code||"", body.product_name_code||"",
|
||||
body.product_grade||"", body.product_ton||"", body.product_boom||"",
|
||||
body.product_vehicle||"", body.production_flag||"", body.note||"",
|
||||
body.objId]
|
||||
);
|
||||
return NextResponse.json({ success: true, message: "수정되었습니다." });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Product save error:", error);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// partBomList.jsp UPG 매트릭스 데이터 (원본 getPartBomList.do 대응)
|
||||
// product_code 선택 시: productList(SPEC) + LIST(UPG별 VC 매트릭스)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const productCode = String(body.product_code || "");
|
||||
|
||||
// 상단 SPEC 헤더 리스트 (PRODUCT_MGMT_UPG_MASTER)
|
||||
let productList: Array<Record<string, unknown>> = [];
|
||||
if (productCode) {
|
||||
productList = await queryRows(
|
||||
`SELECT PMUM.objid::text AS "MASTER_OBJID",
|
||||
PMUM.target_objid::text AS "PRODUCT_MGMT_OBJID",
|
||||
PMUM.spec_name AS "SPEC_NAME"
|
||||
FROM product_mgmt_upg_master PMUM
|
||||
WHERE PMUM.target_objid = $1::integer
|
||||
ORDER BY PMUM.objid`,
|
||||
[productCode],
|
||||
);
|
||||
}
|
||||
|
||||
// UPG 코드 리스트 (unique UPG_CODE + UPG_NAME) — 각 코드에 대해 SPEC별 VC 값 매트릭스
|
||||
let upgList: Array<Record<string, unknown>> = [];
|
||||
if (productCode) {
|
||||
upgList = await queryRows(
|
||||
`SELECT DISTINCT
|
||||
PMUD.upg_code AS "UPG_CODE",
|
||||
PMUD.upg_name AS "UPG_NAME",
|
||||
PMUD.objid::text AS "CODE_ID"
|
||||
FROM product_mgmt_upg_detail PMUD
|
||||
WHERE PMUD.product_objid = $1::integer
|
||||
ORDER BY PMUD.upg_code`,
|
||||
[productCode],
|
||||
);
|
||||
|
||||
// 각 UPG_CODE에 대해 SPEC별 VC 값 채우기
|
||||
for (const u of upgList) {
|
||||
const upgCode = String(u.UPG_CODE);
|
||||
const matrix = await queryRows(
|
||||
`SELECT PMUD.target_objid::text AS "MASTER_OBJID",
|
||||
PMUD.vc AS "VC",
|
||||
PMUD.upg_code || '-' ||
|
||||
(SELECT product_code FROM product_mgmt PM WHERE PM.objid = PMUD.product_objid) ||
|
||||
'-' || PMUD.vc AS "UPG_NO"
|
||||
FROM product_mgmt_upg_detail PMUD
|
||||
WHERE PMUD.product_objid = $1::integer AND PMUD.upg_code = $2
|
||||
ORDER BY PMUD.target_objid`,
|
||||
[productCode, upgCode],
|
||||
);
|
||||
productList.forEach((p, i) => {
|
||||
const hit = matrix.find((m) => String(m.MASTER_OBJID) === String(p.MASTER_OBJID));
|
||||
u[`UPG_NO${i}`] = hit ? hit.UPG_NO : "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
productList,
|
||||
LIST: upgList,
|
||||
productListCnt: productList.length,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 제품관리_BOM 조회 (정전개) - 원본: partMng.structureAscendingList / getStructureAscendingList.do
|
||||
// PRODUCT_MGMT_SPEC (MASTER_OBJID) 또는 UPG_NO 기반으로 PART_BOM_REPORT 매칭,
|
||||
// BOM_PART_QTY 재귀 CTE로 트리 구조 조회
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const productCode = String(body.product_code || ""); // product_mgmt.objid 또는 PMUM.objid
|
||||
const productMgmtSpec = String(body.product_mgmt_spec || ""); // spec_name 문자열
|
||||
const upgNo = String(body.upg_no || "");
|
||||
const customerCd = String(body.customer_cd || "");
|
||||
const projectName = String(body.project_name || "");
|
||||
const unitCode = String(body.unit_code || "");
|
||||
const searchPartNo = String(body.search_partNo || "");
|
||||
const searchPartName = String(body.search_partName || "");
|
||||
const searchLevel = String(body.search_level || "");
|
||||
|
||||
// 매칭되는 bom_report_objid 리스트 확보
|
||||
const reportConds: string[] = ["1=1"];
|
||||
const reportParams: unknown[] = [];
|
||||
let pi = 1;
|
||||
if (productCode) {
|
||||
reportConds.push(`PBM.product_mgmt_objid::varchar = $${pi++}::varchar`);
|
||||
reportParams.push(productCode);
|
||||
}
|
||||
if (productMgmtSpec) {
|
||||
reportConds.push(`PBM.spec_name::varchar = $${pi++}::varchar`);
|
||||
reportParams.push(productMgmtSpec);
|
||||
}
|
||||
if (upgNo) {
|
||||
reportConds.push(`PBM.product_mgmt_upg = $${pi++}`);
|
||||
reportParams.push(upgNo);
|
||||
}
|
||||
if (customerCd) {
|
||||
reportConds.push(`PBM.customer_objid = $${pi++}`);
|
||||
reportParams.push(customerCd);
|
||||
}
|
||||
if (projectName) {
|
||||
reportConds.push(`PBM.contract_objid = $${pi++}`);
|
||||
reportParams.push(projectName);
|
||||
}
|
||||
if (unitCode) {
|
||||
reportConds.push(`PBM.unit_code = $${pi++}`);
|
||||
reportParams.push(unitCode);
|
||||
}
|
||||
// 품번만으로 조회 시 상위 200 report 내에 해당 품번이 없으면 빈 결과가 나오므로,
|
||||
// 품번 정확 일치 노드가 존재하는 bom_report 로 사전 필터링 (anchor 와 동일 기준)
|
||||
if (searchPartNo) {
|
||||
reportConds.push(`PBM.objid IN (
|
||||
SELECT DISTINCT BPQ.bom_report_objid
|
||||
FROM bom_part_qty BPQ
|
||||
JOIN part_mng PT ON PT.objid::text = BPQ.part_no
|
||||
WHERE UPPER(TRIM(PT.part_no)) = UPPER(TRIM($${pi++}))
|
||||
)`);
|
||||
reportParams.push(searchPartNo);
|
||||
}
|
||||
if (searchPartName) {
|
||||
reportConds.push(`PBM.objid IN (
|
||||
SELECT DISTINCT BPQ.bom_report_objid
|
||||
FROM bom_part_qty BPQ
|
||||
JOIN part_mng PT ON PT.objid::text = BPQ.part_no
|
||||
WHERE UPPER(PT.part_name) LIKE UPPER('%' || $${pi++} || '%')
|
||||
)`);
|
||||
reportParams.push(searchPartName);
|
||||
}
|
||||
|
||||
const reports = await queryRows<{ OBJID: string }>(
|
||||
`SELECT objid::text AS "OBJID" FROM part_bom_report PBM WHERE ${reportConds.join(" AND ")}`,
|
||||
reportParams,
|
||||
);
|
||||
if (reports.length === 0) {
|
||||
return NextResponse.json({ RESULTLIST: [], TOTAL_CNT: 0, MAX_LEVEL: 0 });
|
||||
}
|
||||
const reportIds = reports.map((r) => r.OBJID);
|
||||
|
||||
// 재귀 CTE로 BOM 트리 조회
|
||||
const placeholders = reportIds.map((_, i) => `$${i + 1}`).join(",");
|
||||
const idx = reportIds.length;
|
||||
|
||||
// 품번이 입력되면 재귀 anchor 를 "품번 정확 일치 노드"로 바꿔 그 subtree 만 전개.
|
||||
// (품번 포함 매칭이 아니라 정확 일치 기준. 대소문자/공백 무시)
|
||||
// 품명은 subtree 내 추가 필터로만 사용 (기존대로 LIKE).
|
||||
const extraParams: unknown[] = [];
|
||||
let ei = idx + 1;
|
||||
let anchorWhere: string;
|
||||
if (searchPartNo) {
|
||||
anchorWhere = `A.bom_report_objid IN (${placeholders})
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM part_mng PT
|
||||
WHERE PT.objid::text = A.part_no
|
||||
AND UPPER(TRIM(PT.part_no)) = UPPER(TRIM($${ei++}))
|
||||
)`;
|
||||
extraParams.push(searchPartNo);
|
||||
} else {
|
||||
anchorWhere = `(A.parent_objid IS NULL OR A.parent_objid = '')
|
||||
AND A.bom_report_objid IN (${placeholders})`;
|
||||
}
|
||||
|
||||
const extraConds: string[] = [];
|
||||
if (searchPartName) {
|
||||
extraConds.push(`UPPER(P.part_name) LIKE UPPER('%' || $${ei++} || '%')`);
|
||||
extraParams.push(searchPartName);
|
||||
}
|
||||
const extraWhere = extraConds.length > 0 ? " AND " + extraConds.join(" AND ") : "";
|
||||
|
||||
const sql = `
|
||||
WITH RECURSIVE VIEW_BOM AS (
|
||||
SELECT
|
||||
A.bom_report_objid, A.objid, A.parent_objid, A.child_objid,
|
||||
A.parent_part_no, A.part_no, A.qty, A.regdate, A.seq,
|
||||
1::int AS lev,
|
||||
ARRAY[A.child_objid::text] AS path,
|
||||
FALSE AS cycle,
|
||||
A.child_objid AS root_objid,
|
||||
A.child_objid AS sub_root_objid
|
||||
FROM bom_part_qty A
|
||||
WHERE ${anchorWhere}
|
||||
UNION ALL
|
||||
SELECT
|
||||
B.bom_report_objid, B.objid, B.parent_objid, B.child_objid,
|
||||
B.parent_part_no, B.part_no, B.qty, B.regdate, B.seq,
|
||||
V.lev + 1,
|
||||
V.path || B.child_objid::text,
|
||||
B.parent_objid = ANY(V.path),
|
||||
V.root_objid,
|
||||
CASE WHEN V.lev = 1 THEN B.child_objid ELSE V.sub_root_objid END
|
||||
FROM bom_part_qty B
|
||||
JOIN VIEW_BOM V
|
||||
ON B.parent_objid = V.child_objid
|
||||
AND V.bom_report_objid = B.bom_report_objid
|
||||
),
|
||||
MAX_LEV AS (
|
||||
SELECT MAX(lev) AS max_level FROM VIEW_BOM
|
||||
)
|
||||
SELECT
|
||||
V.bom_report_objid AS "BOM_REPORT_OBJID",
|
||||
V.objid AS "OBJID",
|
||||
V.parent_objid AS "PARENT_OBJID",
|
||||
V.child_objid AS "CHILD_OBJID",
|
||||
V.root_objid AS "ROOT_OBJID",
|
||||
V.sub_root_objid AS "SUB_ROOT_OBJID",
|
||||
V.part_no AS "PART_NO_RAW",
|
||||
V.qty AS "QTY",
|
||||
V.lev AS "LEV",
|
||||
V.lev AS "LEVEL",
|
||||
(SELECT max_level FROM MAX_LEV) AS "MAX_LEVEL",
|
||||
(CASE WHEN EXISTS (SELECT 1 FROM bom_part_qty C WHERE C.parent_objid = V.child_objid
|
||||
AND C.bom_report_objid = V.bom_report_objid) THEN 0 ELSE 1 END) AS "LEAF",
|
||||
P.objid::text AS "PART_OBJID",
|
||||
P.part_no AS "PART_NO",
|
||||
P.part_name AS "PART_NAME",
|
||||
P.revision AS "REVISION",
|
||||
P.material AS "MATERIAL",
|
||||
P.spec AS "SPEC",
|
||||
P.post_processing AS "POST_PROCESSING",
|
||||
P.maker AS "MAKER",
|
||||
P.eo_no AS "EO_NO",
|
||||
P.eo_date AS "EO_DATE",
|
||||
P.remark AS "REMARK",
|
||||
P.part_type AS "PART_TYPE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = P.part_type) AS "PART_TYPE_TITLE",
|
||||
(SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '3D_CAD') AS "FILE_3D_CNT",
|
||||
(SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '2D_DRAWING_CAD') AS "FILE_2D_CNT",
|
||||
(SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '2D_PDF_CAD') AS "FILE_PDF_CNT"
|
||||
FROM VIEW_BOM V
|
||||
LEFT JOIN part_mng P ON P.objid::text = V.part_no
|
||||
WHERE 1=1 ${extraWhere}
|
||||
ORDER BY V.bom_report_objid, V.root_objid, V.path, V.regdate
|
||||
`;
|
||||
|
||||
const rows = await queryRows(sql, [...reportIds, ...extraParams]);
|
||||
let maxLevel = rows.length > 0 ? Number(rows[0].MAX_LEVEL || 0) : 0;
|
||||
|
||||
// search_level 필터: 해당 레벨 이하만 남김
|
||||
let filtered = rows;
|
||||
if (searchLevel) {
|
||||
const n = Number(searchLevel);
|
||||
filtered = rows.filter((r) => Number(r.LEV || 0) <= n);
|
||||
if (filtered.length > 0) maxLevel = Math.max(...filtered.map((r) => Number(r.LEV || 0)));
|
||||
}
|
||||
|
||||
return NextResponse.json({ RESULTLIST: filtered, TOTAL_CNT: filtered.length, MAX_LEVEL: maxLevel });
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 1레벨 같은 Part No 중복 체크 (원본: partMng/checkSameTopPartNo.do)
|
||||
// 1레벨 (parent_objid IS NULL) 에 이미 등록된 part_mng.objid 가 rightCheckedArr 에 있으면 true
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ result: false });
|
||||
|
||||
const body = await request.json();
|
||||
const bomReportObjId = String(body.OBJID || body.BOM_REPORT_OBJID || "");
|
||||
const rightCheckedArr: string[] = Array.isArray(body.rightCheckedArr)
|
||||
? body.rightCheckedArr.map((s: unknown) => String(s))
|
||||
: [];
|
||||
|
||||
if (!bomReportObjId || rightCheckedArr.length === 0) {
|
||||
return NextResponse.json({ result: false });
|
||||
}
|
||||
|
||||
// 1레벨 기존 part_no (part_mng.objid) 목록
|
||||
const existing = await queryRows<{ part_no: string }>(
|
||||
`SELECT DISTINCT Q.part_no AS part_no
|
||||
FROM bom_part_qty Q
|
||||
WHERE Q.bom_report_objid = $1
|
||||
AND (Q.parent_objid IS NULL OR Q.parent_objid = '')`,
|
||||
[bomReportObjId],
|
||||
);
|
||||
const existingSet = new Set(existing.map((r) => String(r.part_no)));
|
||||
|
||||
// 기존 1레벨에 rightChecked 중 하나라도 있으면 중복
|
||||
// 단순히 part_no (objid) 매칭이 아니라, 실제 동일 PART_NO(문자열) 매칭도 고려해야 함
|
||||
const directHit = rightCheckedArr.some((id) => existingSet.has(id));
|
||||
if (directHit) return NextResponse.json({ result: true });
|
||||
|
||||
// objid가 달라도 part_no(문자열)가 같으면 중복으로 판정
|
||||
if (existing.length > 0 && rightCheckedArr.length > 0) {
|
||||
const rows = await queryRows<{ cnt: string }>(
|
||||
`SELECT COUNT(*)::text AS cnt FROM part_mng P1
|
||||
WHERE P1.objid::text = ANY($1::text[])
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM part_mng P2
|
||||
WHERE P2.objid::text = ANY($2::text[])
|
||||
AND P2.part_no = P1.part_no
|
||||
)`,
|
||||
[rightCheckedArr, existing.map((e) => String(e.part_no))],
|
||||
);
|
||||
if (Number(rows[0]?.cnt || 0) > 0) return NextResponse.json({ result: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ result: false });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// BOM 삭제
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const { objIds } = body;
|
||||
|
||||
if (!objIds || !Array.isArray(objIds) || objIds.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "삭제할 항목을 선택하세요." });
|
||||
}
|
||||
|
||||
try {
|
||||
const placeholders = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
await execute(`DELETE FROM bom_part_qty WHERE objid IN (${placeholders})`, objIds);
|
||||
return NextResponse.json({ success: true, message: `${objIds.length}건이 삭제되었습니다.` });
|
||||
} catch (error) {
|
||||
console.error("BOM delete error:", error);
|
||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// BOM 배포 - 배포사유 입력 후 (changeDesignNotePopUp.jsp → saveChangeDesignInfo + deployStructure)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const note = String(body.NOTE || "");
|
||||
const objIdsRaw = String(body.OBJID || "");
|
||||
const objIds = objIdsRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
if (objIds.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "배포할 항목이 없습니다." });
|
||||
}
|
||||
|
||||
try {
|
||||
const placeholders = objIds.map((_, i) => `$${i + 2}`).join(",");
|
||||
// saveChangeDesignInfo: NOTE 업데이트
|
||||
await execute(
|
||||
`UPDATE part_bom_report SET note = $1 WHERE objid IN (${placeholders})`,
|
||||
[note, ...objIds],
|
||||
);
|
||||
// deployStructure: status = deploy, deploy_date = now
|
||||
await execute(
|
||||
`UPDATE part_bom_report SET status = 'deploy', deploy_date = TO_CHAR(NOW(), 'YYYY-MM-DD')
|
||||
WHERE objid IN (${placeholders.replace(/\$(\d+)/g, (_, n) => `$${Number(n) - 1}`)})`,
|
||||
objIds,
|
||||
);
|
||||
return NextResponse.json({ success: true, message: "배포되었습니다." });
|
||||
} catch (error) {
|
||||
console.error("bom deploy:", error);
|
||||
return NextResponse.json({ success: false, message: "배포 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// BOM Excel 등록 (원본: partMng/partBomApplySave.do → savePartBomMaster)
|
||||
// 엑셀 파싱된 rows + 기준 정보(customer/project/unit/bom_report)를 받아
|
||||
// PART_BOM_REPORT + BOM_PART_QTY + PART_MNG(신규)를 저장한다.
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const rows: Array<Record<string, string>> = Array.isArray(body.rows) ? body.rows : [];
|
||||
if (rows.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "저장할 데이터가 없습니다." }, { status: 400 });
|
||||
}
|
||||
|
||||
const customerObjId = String(body.CUSTOMER_OBJID || "");
|
||||
const contractObjId = String(body.CONTRACT_OBJID || "");
|
||||
const unitCode = String(body.UNIT_CODE || "");
|
||||
let bomReportObjId = String(body.BOM_REPORT_OBJID || "");
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. BOM_REPORT 확보 (없으면 신규 생성)
|
||||
if (!bomReportObjId) {
|
||||
bomReportObjId = createObjectId();
|
||||
await client.query(
|
||||
`INSERT INTO part_bom_report (
|
||||
objid, customer_objid, contract_objid, unit_code,
|
||||
status, writer, regdate
|
||||
) VALUES ($1, $2, $3, $4, 'create', $5, NOW())`,
|
||||
[bomReportObjId, customerObjId, contractObjId, unitCode, user.userId],
|
||||
);
|
||||
}
|
||||
|
||||
// 2. PART_NO → part_mng.objid 매핑 확보 (신규면 생성)
|
||||
const partNoToObjId = new Map<string, string>();
|
||||
const allPartNos = new Set<string>();
|
||||
rows.forEach((r) => {
|
||||
if (r.PART_NO) allPartNos.add(r.PART_NO.trim());
|
||||
if (r.PARENT_PART_NO) allPartNos.add(r.PARENT_PART_NO.trim());
|
||||
});
|
||||
|
||||
for (const partNo of allPartNos) {
|
||||
const existing = await queryOne<{ objid: string }>(
|
||||
`SELECT objid::text AS objid FROM part_mng
|
||||
WHERE part_no = $1 AND status = 'release' AND COALESCE(is_last, '1') = '1'
|
||||
ORDER BY reg_date DESC LIMIT 1`,
|
||||
[partNo],
|
||||
);
|
||||
if (existing) {
|
||||
partNoToObjId.set(partNo, existing.objid);
|
||||
} else {
|
||||
// 해당 PART_NO의 row를 찾아 PART_MNG 신규 생성
|
||||
const src = rows.find((r) => r.PART_NO?.trim() === partNo);
|
||||
const newObjId = createObjectId();
|
||||
await client.query(
|
||||
`INSERT INTO part_mng (
|
||||
objid, part_no, part_name, qty, material, spec, post_processing, maker,
|
||||
part_type, remark, revision, status, is_last, writer, reg_date
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, '', 'release', '1', $11, NOW()
|
||||
)`,
|
||||
[
|
||||
newObjId, partNo,
|
||||
src?.PART_NAME || partNo,
|
||||
src?.QTY || "1",
|
||||
src?.MATERIAL || "",
|
||||
src?.SPEC || "",
|
||||
src?.POST_PROCESSING || "",
|
||||
src?.MAKER || "",
|
||||
src?.PART_TYPE || "",
|
||||
src?.REMARK || "",
|
||||
user.userId,
|
||||
],
|
||||
);
|
||||
partNoToObjId.set(partNo, newObjId);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 기존 bom_part_qty 삭제 후 재등록 (BOM_REPORT_OBJID 기준)
|
||||
await client.query(`DELETE FROM bom_part_qty WHERE bom_report_objid = $1`, [bomReportObjId]);
|
||||
|
||||
// 4. 각 row를 bom_part_qty 에 insert
|
||||
let seq = 1;
|
||||
for (const r of rows) {
|
||||
const partNo = (r.PART_NO || "").trim();
|
||||
const parentPartNo = (r.PARENT_PART_NO || "").trim();
|
||||
if (!partNo) continue;
|
||||
const childObjId = createObjectId();
|
||||
const partObjId = partNoToObjId.get(partNo) || "";
|
||||
const parentObjId = parentPartNo ? partNoToObjId.get(parentPartNo) || "" : "";
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO bom_part_qty (
|
||||
objid, bom_report_objid, parent_objid, child_objid,
|
||||
parent_part_no, part_no, last_part_objid,
|
||||
qty, qty_temp, status, seq, regdate
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $8, 'create', $9, NOW()
|
||||
)`,
|
||||
[
|
||||
childObjId, bomReportObjId,
|
||||
parentObjId || null,
|
||||
childObjId,
|
||||
parentObjId || null,
|
||||
partObjId,
|
||||
partObjId,
|
||||
r.QTY || "1",
|
||||
seq++,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, bomReportObjId, message: "등록되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("bom excel save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 제품관리_PART 및 구조등록 (structureList.jsp) 및 단순 BOM 조회 공용
|
||||
// - mode: "structure" → PART_BOM_REPORT 구조 리스트 (getBOMStandardStructureGridList 대응)
|
||||
// - 기본 → bom_part_qty 기반 단순 리스트
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const mode = String(body.mode || "structure");
|
||||
|
||||
if (mode === "structure" || mode === "register") {
|
||||
// getBOMStandardStructureGridList 대응
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.customer_cd) {
|
||||
conditions.push(`T.customer_objid = $${idx++}`);
|
||||
params.push(body.customer_cd);
|
||||
}
|
||||
if (body.project_name) {
|
||||
conditions.push(`T.contract_objid = $${idx++}`);
|
||||
params.push(body.project_name);
|
||||
}
|
||||
if (body.unit_code) {
|
||||
conditions.push(`T.unit_code = $${idx++}`);
|
||||
params.push(body.unit_code);
|
||||
}
|
||||
if (body.SEARCH_UNIT_NAME) {
|
||||
conditions.push(`EXISTS (SELECT 'E' FROM pms_wbs_task W WHERE W.objid = T.unit_code
|
||||
AND (W.task_name LIKE UPPER('%'||$${idx}||'%') OR W.unit_no LIKE UPPER('%'||$${idx}||'%')))`);
|
||||
params.push(body.SEARCH_UNIT_NAME);
|
||||
idx++;
|
||||
}
|
||||
if (body.SEARCH_WRITER) {
|
||||
conditions.push(`T.writer = $${idx++}`);
|
||||
params.push(body.SEARCH_WRITER);
|
||||
}
|
||||
if (body.SEARCH_DEPLOY_DATE_FROM) {
|
||||
conditions.push(`T.deploy_date IS NOT NULL AND TO_DATE(T.deploy_date,'YYYY-MM-DD') >= $${idx++}::timestamp`);
|
||||
params.push(body.SEARCH_DEPLOY_DATE_FROM);
|
||||
}
|
||||
if (body.SEARCH_DEPLOY_DATE_TO) {
|
||||
conditions.push(`T.deploy_date IS NOT NULL AND TO_DATE(T.deploy_date,'YYYY-MM-DD') <= $${idx++}::timestamp`);
|
||||
params.push(body.SEARCH_DEPLOY_DATE_TO);
|
||||
}
|
||||
if (body.status) {
|
||||
conditions.push(`T.status = $${idx++}`);
|
||||
params.push(body.status);
|
||||
}
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
|
||||
const sql = `
|
||||
SELECT ROW_NUMBER() OVER(ORDER BY T.regdate DESC) AS "NUM",
|
||||
T.objid::text AS "OBJID",
|
||||
T.customer_objid AS "CUSTOMER_OBJID",
|
||||
(SELECT supply_name FROM supply_mng O WHERE O.objid::varchar = T.customer_objid) AS "CUSTOMER_NAME",
|
||||
T.contract_objid AS "CONTRACT_OBJID",
|
||||
(SELECT customer_project_name FROM project_mgmt O WHERE O.objid = T.contract_objid) AS "CUSTOMER_PROJECT_NAME",
|
||||
(SELECT project_no FROM project_mgmt O WHERE O.objid = T.contract_objid) AS "PROJECT_NO",
|
||||
T.unit_code AS "UNIT_CODE",
|
||||
(SELECT O.unit_no || '-' || O.task_name FROM pms_wbs_task O WHERE O.objid = T.unit_code) AS "UNIT_NAME",
|
||||
T.status AS "STATUS",
|
||||
CASE UPPER(T.status)
|
||||
WHEN 'CREATE' THEN '등록중'
|
||||
WHEN 'CHANGEDESIGN' THEN '설계변경미배포'
|
||||
WHEN 'DEPLOY' THEN '배포완료'
|
||||
ELSE ''
|
||||
END AS "STATUS_TITLE",
|
||||
T.writer AS "WRITER",
|
||||
(SELECT dept_name FROM user_info WHERE user_id = T.writer) AS "DEPT_NAME",
|
||||
(SELECT user_name FROM user_info WHERE user_id = T.writer) AS "USER_NAME",
|
||||
COALESCE((SELECT dept_name FROM user_info WHERE user_id = T.writer), '') || '/' ||
|
||||
COALESCE((SELECT user_name FROM user_info WHERE user_id = T.writer), '') AS "DEPT_USER_NAME",
|
||||
TO_CHAR(T.regdate, 'YYYY-MM-DD') AS "REG_DATE",
|
||||
T.deploy_date AS "DEPLOY_DATE",
|
||||
T.revision AS "REVISION",
|
||||
(SELECT MAX(PM.eo_no) FROM bom_part_qty BP LEFT JOIN part_mng PM ON BP.part_no = PM.objid::varchar
|
||||
WHERE BP.bom_report_objid = T.objid) AS "EO_NO",
|
||||
(SELECT MAX(PM.eo_date) FROM bom_part_qty BP LEFT JOIN part_mng PM ON BP.part_no = PM.objid::varchar
|
||||
WHERE BP.bom_report_objid = T.objid) AS "EO_DATE",
|
||||
T.note AS "NOTE",
|
||||
T.multi_yn AS "MULTI_YN",
|
||||
T.multi_master_yn AS "MULTI_MASTER_YN",
|
||||
T.multi_break_yn AS "MULTI_BREAK_YN",
|
||||
T.multi_master_objid AS "MULTI_MASTER_OBJID",
|
||||
(SELECT COUNT(*) FROM bom_part_qty A WHERE A.bom_report_objid = T.objid) AS "BOM_CNT"
|
||||
FROM part_bom_report T
|
||||
WHERE ${where}
|
||||
ORDER BY T.regdate DESC
|
||||
`;
|
||||
const rows = await queryRows(sql, params);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
|
||||
// 단순 BOM 리스트 (기존 방식 유지)
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.part_name) { conditions.push(`P.part_name LIKE '%' || $${idx++} || '%'`); params.push(body.part_name); }
|
||||
if (body.part_no) { conditions.push(`P.part_no LIKE '%' || $${idx++} || '%'`); params.push(body.part_no); }
|
||||
if (body.project_no) { conditions.push(`B.project_no LIKE '%' || $${idx++} || '%'`); params.push(body.project_no); }
|
||||
|
||||
const where = conditions.join(" AND ");
|
||||
const rows = await queryRows(
|
||||
`SELECT B.objid AS "OBJID",
|
||||
P.objid::text AS "PART_OBJID",
|
||||
P.part_no AS "PART_NO", P.part_name AS "PART_NAME",
|
||||
P.spec AS "SPEC", P.material AS "MATERIAL", P.unit AS "UNIT",
|
||||
COALESCE(B.qty::text, '') AS "BOM_QTY",
|
||||
COALESCE(B.bom_level::text, '1') AS "BOM_LEVEL",
|
||||
P.maker AS "MAKER", P.remark AS "REMARK", P.revision AS "REVISION",
|
||||
COALESCE((SELECT code_name FROM comm_code WHERE code_id = P.part_type LIMIT 1), '') AS "PART_TYPE_TITLE"
|
||||
FROM bom_part_qty B
|
||||
JOIN part_mng P ON P.objid::text = B.part_no
|
||||
WHERE P.status = 'release' AND ${where}
|
||||
ORDER BY B.parent_part_no, P.part_no
|
||||
`, params,
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// BOM 저장
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
const body = await request.json();
|
||||
const isNew = !body.objId || body.actionType === "regist";
|
||||
const objId = isNew ? createObjectId() : body.objId;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query(
|
||||
`INSERT INTO bom_part_qty (objid, parent_part_no, part_no, qty, project_no, bom_level, writer, regdate)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
||||
ON CONFLICT (objid) DO UPDATE SET
|
||||
parent_part_no=EXCLUDED.parent_part_no, part_no=EXCLUDED.part_no,
|
||||
qty=EXCLUDED.qty, project_no=EXCLUDED.project_no, bom_level=EXCLUDED.bom_level`,
|
||||
[objId, body.parent_part_no || "", body.part_no || "",
|
||||
body.qty || "1", body.project_no || "", body.bom_level || "1", user.userId]
|
||||
);
|
||||
return NextResponse.json({ success: true, objId, message: isNew ? "등록되었습니다." : "수정되었습니다." });
|
||||
} catch (e) {
|
||||
console.error("BOM save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally { client.release(); }
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 구조 연결 변경 (원본: partMng/changeRelatePartInfo.do → partMngService.changeRelatePartInfo)
|
||||
// 1) UPDATE BOM_PART_QTY SET LAST_PART_OBJID = #{RIGHT_OBJID} WHERE BOM_REPORT_OBJID = ? AND OBJID = ?
|
||||
// 2) insertPartMngHistory (HIS_STATUS='CHANGE')
|
||||
// 3) changeStatusBomReport
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const bomReportObjId = String(body.BOM_REPORT_OBJID || body.OBJID || "");
|
||||
const bomObjId = String(body.B_OBJID || body.leftPartBomQtyObjId || ""); // bom_part_qty.objid
|
||||
const rightObjId = String(body.rightObjId || body.rightPartObjId || ""); // 새 part_mng.objid
|
||||
const leftPartObjid = String(body.leftPartObjid || ""); // 기존 part_mng.objid (PART_MNG의 P)
|
||||
const leftObjId = String(body.leftObjId || ""); // parent bom_part_qty.child_objid
|
||||
const leftPartNoQty = String(body.leftPartNoQty || "");
|
||||
const leftPartChildObjId = String(body.leftPartChildObjId || "");
|
||||
const rightPartNo = String(body.rightPartNo || "");
|
||||
const rightPartRev = String(body.rightPartRev || "");
|
||||
const changeType = String(body.CHANGE_TYPE || "");
|
||||
const changeOption = String(body.CHANGE_OPTION || "");
|
||||
|
||||
if (!bomReportObjId || !bomObjId || !rightObjId) {
|
||||
return NextResponse.json({ success: false, message: "필수 파라미터 누락" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1) bom_part_qty.last_part_objid 교체
|
||||
await client.query(
|
||||
`UPDATE bom_part_qty
|
||||
SET last_part_objid = $1
|
||||
WHERE bom_report_objid = $2 AND objid = $3`,
|
||||
[rightObjId, bomReportObjId, bomObjId],
|
||||
);
|
||||
|
||||
// 2) insertPartMngHistory (HIS_STATUS='CHANGE')
|
||||
const newChildObjId = createObjectId(); // 원본 서비스: 새 CHILD_OBJID로 세팅
|
||||
await client.query(
|
||||
`INSERT INTO part_mng_history (
|
||||
objid, product_mgmt_objid, upg_no, part_no, part_name, unit, qty,
|
||||
spec, material, weight, part_type, remark, es_spec, ms_spec,
|
||||
change_option, design_apply_point, management_flag, revision, status,
|
||||
reg_date, edit_date, writer, is_last, eo_no, eo_temp,
|
||||
excel_upload_seq, sourcing_code, sub_material, parent_part_no,
|
||||
design_date, eo_date, deploy_date,
|
||||
thickness, width, height, out_diameter, in_diameter, length, supply_code,
|
||||
change_type, contract_objid, maker, qty_temp,
|
||||
bom_report_objid, parent_part_objid, parent_qty_child_objid,
|
||||
bom_qty_status, his_reg_date, his_writer, his_status, qty_child_objid,
|
||||
bom_status, bom_deploy_date, chg_part_objid, chg_part_no, chg_part_rev
|
||||
)
|
||||
SELECT
|
||||
P.objid::numeric, P.product_mgmt_objid, P.upg_no, P.part_no, P.part_name, P.unit, Q.qty,
|
||||
P.spec, P.material, P.weight, P.part_type, P.remark, P.es_spec, P.ms_spec,
|
||||
COALESCE(NULLIF($1::varchar, ''), P.change_option),
|
||||
P.design_apply_point, P.management_flag, P.revision, P.status,
|
||||
P.reg_date, NOW(), $2::varchar, P.is_last, P.eo_no, P.eo_temp,
|
||||
P.excel_upload_seq, P.sourcing_code, P.sub_material,
|
||||
COALESCE(NULLIF($3::varchar, ''), Q.parent_part_no),
|
||||
P.design_date, P.eo_date, P.deploy_date,
|
||||
P.thickness, P.width, P.height, P.out_diameter, P.in_diameter, P.length, P.supply_code,
|
||||
COALESCE(NULLIF($4::varchar, ''), P.change_type),
|
||||
P.contract_objid, P.maker, Q.qty_temp,
|
||||
COALESCE(NULLIF($5::varchar, ''), Q.bom_report_objid),
|
||||
COALESCE(NULLIF($6::varchar, ''), Q.parent_part_no),
|
||||
COALESCE(NULLIF($7::varchar, ''), Q.parent_objid),
|
||||
Q.status, NOW(), $2::varchar, 'CHANGE', $8::varchar,
|
||||
'', NULL, $10::varchar, $11::varchar, $12::varchar
|
||||
FROM part_mng P
|
||||
LEFT OUTER JOIN bom_part_qty Q
|
||||
ON Q.child_objid = $8::varchar
|
||||
AND P.part_no IN (SELECT PM2.part_no FROM part_mng PM2 WHERE PM2.objid = Q.part_no)
|
||||
WHERE P.objid = $9::varchar`,
|
||||
[
|
||||
changeOption || "", // $1 CHANGE_OPTION
|
||||
user.userId, // $2 WRITER
|
||||
leftPartNoQty || "", // $3 PARENT_PART_NO
|
||||
changeType || "", // $4 CHANGE_TYPE
|
||||
bomReportObjId, // $5 BOM_REPORT_OBJID
|
||||
leftObjId || "", // $6 PARENT_PART_OBJID
|
||||
leftPartChildObjId || "", // $7 PARENT_QTY_CHILD_OBJID
|
||||
newChildObjId, // $8 CHILD_OBJID (JOIN + qty_child_objid)
|
||||
leftPartObjid || rightObjId, // $9 OBJID (원본 서비스: leftPartObjid)
|
||||
rightObjId, // $10 CHG_PART_OBJID
|
||||
rightPartNo, // $11 CHG_PART_NO
|
||||
rightPartRev, // $12 CHG_PART_REV
|
||||
],
|
||||
);
|
||||
|
||||
// 3) changeStatusBomReport
|
||||
await client.query(
|
||||
`UPDATE part_bom_report
|
||||
SET status = CASE WHEN status != 'create' THEN 'changeDesign' ELSE status END
|
||||
WHERE objid = $1`,
|
||||
[bomReportObjId],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "변경되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("bom change:", e);
|
||||
return NextResponse.json({ success: false, message: "변경 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
|
||||
// 구조 연결 (원본: partMng/relatePartInfo.do → partMngService.relatePartInfo)
|
||||
// leftObjId (부모 bom_part_qty.child_objid) 아래에 rightCheckedArr(part_mng.objid 배열)를 자식으로 추가.
|
||||
// 원본은 STATUS='adding' 고정 + seq는 nextval('seq_bom_qty').
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const bomReportObjId = String(body.BOM_REPORT_OBJID || body.OBJID || "");
|
||||
const leftObjId = String(body.leftObjId || ""); // 부모 bom_part_qty.child_objid
|
||||
const leftPartLastObjId = String(body.partObjId || body.leftPartLastObjId || "");
|
||||
const leftPartNoQty = String(body.leftPartNoQty || ""); // 부모 part_no (PARENT_PART_NO)
|
||||
const leftPartChildObjId = String(body.leftPartChildObjId || ""); // PARENT_QTY_CHILD_OBJID
|
||||
const rightCheckedArr: string[] = Array.isArray(body.rightCheckedArr) ? body.rightCheckedArr.map((s: unknown) => String(s)) : [];
|
||||
const changeType = String(body.CHANGE_TYPE || "");
|
||||
const changeOption = String(body.CHANGE_OPTION || "");
|
||||
|
||||
if (!bomReportObjId) {
|
||||
return NextResponse.json({ success: false, message: "BOM_REPORT_OBJID 누락" }, { status: 400 });
|
||||
}
|
||||
if (rightCheckedArr.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "추가할 Part가 없습니다." });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
for (const rightPartObjId of rightCheckedArr) {
|
||||
const newObjId = createObjectId();
|
||||
const newChildObjId = createObjectId();
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO bom_part_qty (
|
||||
objid, bom_report_objid, parent_objid, child_objid,
|
||||
parent_part_no, part_no, last_part_objid,
|
||||
qty, qty_temp, status, seq, regdate, writer
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $8, 'adding',
|
||||
nextval('seq_bom_qty'), NOW(), $9
|
||||
)`,
|
||||
[
|
||||
newObjId, bomReportObjId,
|
||||
leftObjId || null,
|
||||
newChildObjId,
|
||||
leftPartLastObjId || null,
|
||||
rightPartObjId,
|
||||
rightPartObjId,
|
||||
"1",
|
||||
user.userId,
|
||||
],
|
||||
);
|
||||
|
||||
// insertPartMngHistory (HIS_STATUS='ADD') — 원본 partMng.insertPartMngHistory
|
||||
await client.query(
|
||||
`INSERT INTO part_mng_history (
|
||||
objid, product_mgmt_objid, upg_no, part_no, part_name, unit, qty,
|
||||
spec, material, weight, part_type, remark, es_spec, ms_spec,
|
||||
change_option, design_apply_point, management_flag, revision, status,
|
||||
reg_date, edit_date, writer, is_last, eo_no, eo_temp,
|
||||
excel_upload_seq, sourcing_code, sub_material, parent_part_no,
|
||||
design_date, eo_date, deploy_date,
|
||||
thickness, width, height, out_diameter, in_diameter, length, supply_code,
|
||||
change_type, contract_objid, maker, qty_temp,
|
||||
bom_report_objid, parent_part_objid, parent_qty_child_objid,
|
||||
bom_qty_status, his_reg_date, his_writer, his_status, qty_child_objid,
|
||||
bom_status, bom_deploy_date, chg_part_objid, chg_part_no, chg_part_rev
|
||||
)
|
||||
SELECT
|
||||
P.objid::numeric, P.product_mgmt_objid, P.upg_no, P.part_no, P.part_name, P.unit, Q.qty,
|
||||
P.spec, P.material, P.weight, P.part_type, P.remark, P.es_spec, P.ms_spec,
|
||||
COALESCE(NULLIF($1::varchar, ''), P.change_option),
|
||||
P.design_apply_point, P.management_flag, P.revision, P.status,
|
||||
P.reg_date, NOW(), $2::varchar, P.is_last, P.eo_no, P.eo_temp,
|
||||
P.excel_upload_seq, P.sourcing_code, P.sub_material,
|
||||
COALESCE(NULLIF($3::varchar, ''), Q.parent_part_no),
|
||||
P.design_date, P.eo_date, P.deploy_date,
|
||||
P.thickness, P.width, P.height, P.out_diameter, P.in_diameter, P.length, P.supply_code,
|
||||
COALESCE(NULLIF($4::varchar, ''), P.change_type),
|
||||
P.contract_objid, P.maker, Q.qty_temp,
|
||||
COALESCE(NULLIF($5::varchar, ''), Q.bom_report_objid),
|
||||
COALESCE(NULLIF($6::varchar, ''), Q.parent_part_no),
|
||||
COALESCE(NULLIF($7::varchar, ''), Q.parent_objid),
|
||||
Q.status, NOW(), $2::varchar, 'ADD', $8::varchar,
|
||||
'', NULL, '', '', ''
|
||||
FROM part_mng P
|
||||
LEFT OUTER JOIN bom_part_qty Q
|
||||
ON Q.child_objid = $8::varchar
|
||||
AND P.part_no IN (SELECT PM2.part_no FROM part_mng PM2 WHERE PM2.objid = Q.part_no)
|
||||
WHERE P.objid = $9::varchar`,
|
||||
[
|
||||
changeOption || "", // $1 CHANGE_OPTION
|
||||
user.userId, // $2 WRITER
|
||||
leftPartNoQty || "", // $3 PARENT_PART_NO (원본 서비스: paramMap.leftPartNoQty)
|
||||
changeType || "", // $4 CHANGE_TYPE
|
||||
bomReportObjId, // $5 BOM_REPORT_OBJID
|
||||
leftObjId || "", // $6 PARENT_PART_OBJID (=leftObjId)
|
||||
leftPartChildObjId || "", // $7 PARENT_QTY_CHILD_OBJID
|
||||
newChildObjId, // $8 CHILD_OBJID
|
||||
rightPartObjId, // $9 OBJID (part_mng.objid)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE part_bom_report
|
||||
SET status = CASE WHEN status != 'create' THEN 'changeDesign' ELSE status END
|
||||
WHERE objid = $1`,
|
||||
[bomReportObjId],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "연결되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("bom connect:", e);
|
||||
return NextResponse.json({ success: false, message: "연결 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// PART_BOM_REPORT 삭제 (structureList.jsp fn_delete → /partMng/deleteStructure.do)
|
||||
// 배포완료/설계변경미배포 상태는 삭제 불가
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const { objIds } = body;
|
||||
if (!Array.isArray(objIds) || objIds.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "삭제할 항목을 선택하세요." });
|
||||
}
|
||||
|
||||
try {
|
||||
const placeholders = objIds.map((_: string, i: number) => `$${i + 1}`).join(",");
|
||||
await execute(
|
||||
`DELETE FROM bom_part_qty WHERE bom_report_objid IN (${placeholders})`,
|
||||
objIds,
|
||||
);
|
||||
await execute(
|
||||
`DELETE FROM part_bom_report WHERE objid IN (${placeholders}) AND status NOT IN ('deploy','changeDesign')`,
|
||||
objIds,
|
||||
);
|
||||
return NextResponse.json({ success: true, message: `${objIds.length}건이 삭제되었습니다.` });
|
||||
} catch (error) {
|
||||
console.error("structure delete:", error);
|
||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 구조 연결 해제 (원본: partMng/deleteStatusPartRelateInfo.do → partMngService.deleteStatusPartRelateInfo)
|
||||
// 1) insertPartMngHistoryWhenDelPartRelation (HIS_STATUS='DEL') — 재귀 DEL 이력
|
||||
// 2) deletePartRelateInfoHis — adding 상태의 하위 이력 제거
|
||||
// 3) deleteStatusPartRelateInfo — 재귀 논리 삭제 (status='deleting')
|
||||
// 4) changeStatusBomReport
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const bomReportObjId = String(body.BOM_REPORT_OBJID || body.OBJID || "");
|
||||
const leftObjId = String(body.leftObjId || ""); // bom_part_qty.child_objid
|
||||
const changeType = String(body.CHANGE_TYPE || "");
|
||||
const changeOption = String(body.CHANGE_OPTION || "");
|
||||
|
||||
if (!bomReportObjId || !leftObjId) {
|
||||
return NextResponse.json({ success: false, message: "필수 파라미터 누락" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1) DEL 이력 — 재귀 서브트리 전체를 part_mng_history에 기록
|
||||
await client.query(
|
||||
`WITH RECURSIVE VIEW_BOM AS (
|
||||
SELECT A.objid, A.child_objid, A.parent_objid, A.bom_report_objid
|
||||
FROM bom_part_qty A
|
||||
WHERE A.child_objid = $9::varchar AND A.bom_report_objid = $5::varchar
|
||||
UNION ALL
|
||||
SELECT B.objid, B.child_objid, B.parent_objid, B.bom_report_objid
|
||||
FROM bom_part_qty B
|
||||
JOIN VIEW_BOM V ON B.parent_objid = V.child_objid
|
||||
AND V.bom_report_objid = B.bom_report_objid
|
||||
AND B.bom_report_objid = $5::varchar
|
||||
AND B.status NOT IN ('deleting', 'deleted')
|
||||
)
|
||||
INSERT INTO part_mng_history (
|
||||
objid, product_mgmt_objid, upg_no, part_no, part_name, unit, qty,
|
||||
spec, material, weight, part_type, remark, es_spec, ms_spec,
|
||||
change_option, design_apply_point, management_flag, revision, status,
|
||||
reg_date, edit_date, writer, is_last, eo_no, eo_temp,
|
||||
excel_upload_seq, sourcing_code, sub_material, parent_part_no,
|
||||
design_date, eo_date, deploy_date,
|
||||
thickness, width, height, out_diameter, in_diameter, length, supply_code,
|
||||
change_type, contract_objid, maker, qty_temp,
|
||||
bom_report_objid, parent_part_objid, parent_qty_child_objid,
|
||||
bom_qty_status, his_reg_date, his_writer, his_status, qty_child_objid,
|
||||
bom_status
|
||||
)
|
||||
SELECT
|
||||
P.objid::numeric, P.product_mgmt_objid, P.upg_no, P.part_no, P.part_name, P.unit, Q.qty,
|
||||
P.spec, P.material, P.weight, P.part_type, P.remark, P.es_spec, P.ms_spec,
|
||||
COALESCE(NULLIF($1::varchar, ''), P.change_option),
|
||||
P.design_apply_point, P.management_flag, P.revision, P.status,
|
||||
P.reg_date, NOW(), $2::varchar, P.is_last, P.eo_no, P.eo_temp,
|
||||
P.excel_upload_seq, P.sourcing_code, P.sub_material,
|
||||
COALESCE(NULLIF($3::varchar, ''), Q.parent_part_no),
|
||||
P.design_date, P.eo_date, P.deploy_date,
|
||||
P.thickness, P.width, P.height, P.out_diameter, P.in_diameter, P.length, P.supply_code,
|
||||
COALESCE(NULLIF($4::varchar, ''), P.change_type),
|
||||
P.contract_objid, P.maker, Q.qty_temp,
|
||||
COALESCE(NULLIF($5::varchar, ''), Q.bom_report_objid),
|
||||
COALESCE(NULLIF($6::varchar, ''), Q.parent_part_no),
|
||||
COALESCE(NULLIF($7::varchar, ''), Q.parent_objid),
|
||||
Q.status, NOW(), $2::varchar, $8::varchar, Q.child_objid,
|
||||
''
|
||||
FROM bom_part_qty Q
|
||||
INNER JOIN part_mng P ON Q.last_part_objid = P.objid
|
||||
WHERE Q.objid IN (SELECT objid FROM VIEW_BOM)`,
|
||||
[
|
||||
changeOption || "", // $1 CHANGE_OPTION
|
||||
user.userId, // $2 WRITER
|
||||
"", // $3 PARENT_PART_NO (원본 서비스는 "")
|
||||
changeType || "", // $4 CHANGE_TYPE
|
||||
bomReportObjId, // $5 BOM_REPORT_OBJID
|
||||
"", // $6 PARENT_PART_OBJID (원본 서비스는 "")
|
||||
"", // $7 PARENT_QTY_CHILD_OBJID (원본 서비스는 "")
|
||||
"DEL", // $8 HIS_STATUS
|
||||
leftObjId, // $9 leftObjId (재귀 시작점)
|
||||
],
|
||||
);
|
||||
|
||||
// 2) deletePartRelateInfoHis — 하위 트리 중 'adding' 상태인 행의 이력만 삭제
|
||||
await client.query(
|
||||
`WITH RECURSIVE VIEW_BOM AS (
|
||||
SELECT A.child_objid, A.parent_objid, A.bom_report_objid, A.status
|
||||
FROM bom_part_qty A
|
||||
WHERE A.child_objid = $2 AND A.bom_report_objid = $1
|
||||
UNION ALL
|
||||
SELECT B.child_objid, B.parent_objid, B.bom_report_objid, B.status
|
||||
FROM bom_part_qty B
|
||||
JOIN VIEW_BOM V ON B.parent_objid = V.child_objid
|
||||
AND V.bom_report_objid = B.bom_report_objid
|
||||
AND B.bom_report_objid = $1
|
||||
)
|
||||
DELETE FROM part_mng_history
|
||||
WHERE qty_child_objid IN (
|
||||
SELECT child_objid FROM VIEW_BOM V WHERE V.status = 'adding'
|
||||
)`,
|
||||
[bomReportObjId, leftObjId],
|
||||
);
|
||||
|
||||
// 3) deleteStatusPartRelateInfo — 재귀 논리 삭제
|
||||
await client.query(
|
||||
`WITH RECURSIVE VIEW_BOM(objid, child_objid) AS (
|
||||
SELECT A.objid, A.child_objid FROM bom_part_qty A
|
||||
WHERE A.child_objid = $2 AND A.bom_report_objid = $1
|
||||
UNION ALL
|
||||
SELECT B.objid, B.child_objid FROM bom_part_qty B
|
||||
JOIN VIEW_BOM V ON B.parent_objid = V.child_objid
|
||||
WHERE B.bom_report_objid = $1
|
||||
AND B.status NOT IN ('deleting', 'deleted')
|
||||
)
|
||||
UPDATE bom_part_qty
|
||||
SET edit_date = NOW(), status = 'deleting'
|
||||
WHERE objid IN (SELECT objid FROM VIEW_BOM)`,
|
||||
[bomReportObjId, leftObjId],
|
||||
);
|
||||
|
||||
// 4) changeStatusBomReport
|
||||
await client.query(
|
||||
`UPDATE part_bom_report
|
||||
SET status = CASE WHEN status != 'create' THEN 'changeDesign' ELSE status END
|
||||
WHERE objid = $1`,
|
||||
[bomReportObjId],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return NextResponse.json({ success: true, message: "해제되었습니다." });
|
||||
} catch (e) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("bom disconnect:", e);
|
||||
return NextResponse.json({ success: false, message: "해제 중 오류가 발생했습니다." }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 구조 수량 저장 (원본: partMng/structureQtySave.do)
|
||||
// child_objid 기준으로 qty_temp 갱신
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const bomReportObjId = String(body.BOM_REPORT_OBJID || "");
|
||||
const childObjId = String(body.CHILD_OBJID || "");
|
||||
const qty = String(body.QTY_TEMP ?? body.QTY ?? "");
|
||||
|
||||
if (!childObjId) {
|
||||
return NextResponse.json({ success: false, message: "CHILD_OBJID 누락" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await execute(
|
||||
`UPDATE bom_part_qty SET qty_temp = $1
|
||||
WHERE bom_report_objid = $2 AND child_objid = $3`,
|
||||
[qty, bomReportObjId, childObjId],
|
||||
);
|
||||
return NextResponse.json({ success: true, result: true });
|
||||
} catch (e) {
|
||||
console.error("bom qty save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 실패" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 구조 순번 저장 (원본: partMng/structureSeqSave.do)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const childObjId = String(body.CHILD_OBJID || "");
|
||||
const seq = String(body.SEQ ?? "");
|
||||
|
||||
if (!childObjId) {
|
||||
return NextResponse.json({ success: false, message: "CHILD_OBJID 누락" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await execute(
|
||||
`UPDATE bom_part_qty SET seq = $1::numeric WHERE child_objid = $2`,
|
||||
[seq, childObjId],
|
||||
);
|
||||
return NextResponse.json({ success: true, result: true });
|
||||
} catch (e) {
|
||||
console.error("bom seq save:", e);
|
||||
return NextResponse.json({ success: false, message: "저장 실패" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// 구조등록 팝업 좌측 트리 (원본: partMng/structurePopupLeft.do)
|
||||
// bomReportObjId 기준 bom_part_qty 재귀 트리 + 파일카운트 + PART_MNG 정보
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const bomReportObjId = String(body.bomReportObjId || body.objId || "");
|
||||
if (!bomReportObjId) return NextResponse.json({ RESULTLIST: [], HEADER: {} });
|
||||
|
||||
const header = await queryOne(
|
||||
`SELECT T.objid::text AS "OBJID",
|
||||
T.status AS "STATUS",
|
||||
T.revision AS "REV",
|
||||
T.note AS "NOTE",
|
||||
T.deploy_date AS "DEPLOY_DATE",
|
||||
(SELECT supply_name FROM supply_mng O WHERE O.objid::varchar = T.customer_objid) AS "CUSTOMER_NAME",
|
||||
(SELECT project_no FROM project_mgmt O WHERE O.objid = T.contract_objid) AS "PROJECT_NO",
|
||||
(SELECT customer_project_name FROM project_mgmt O WHERE O.objid = T.contract_objid) AS "CUSTOMER_PROJECT_NAME",
|
||||
(SELECT O.unit_no || '-' || O.task_name FROM pms_wbs_task O WHERE O.objid = T.unit_code) AS "UNIT_NAME"
|
||||
FROM part_bom_report T WHERE T.objid = $1`,
|
||||
[bomReportObjId],
|
||||
);
|
||||
|
||||
const rows = await queryRows(
|
||||
`WITH RECURSIVE VIEW_BOM AS (
|
||||
SELECT
|
||||
A.bom_report_objid, A.objid, A.parent_objid, A.child_objid,
|
||||
A.parent_part_no, A.part_no, A.last_part_objid,
|
||||
A.qty, A.qty_temp, A.status, A.regdate, A.seq,
|
||||
1::int AS lev,
|
||||
ARRAY[A.child_objid::text] AS path,
|
||||
ARRAY[A.part_no::text] AS parent_parts_arr
|
||||
FROM bom_part_qty A
|
||||
WHERE (A.parent_objid IS NULL OR A.parent_objid = '')
|
||||
AND A.bom_report_objid = $1
|
||||
AND (A.status NOT IN ('deleting', 'deleted') OR A.status IS NULL)
|
||||
UNION ALL
|
||||
SELECT
|
||||
B.bom_report_objid, B.objid, B.parent_objid, B.child_objid,
|
||||
B.parent_part_no, B.part_no, B.last_part_objid,
|
||||
B.qty, B.qty_temp, B.status, B.regdate, B.seq,
|
||||
V.lev + 1,
|
||||
V.path || B.child_objid::text,
|
||||
V.parent_parts_arr || B.part_no::text
|
||||
FROM bom_part_qty B
|
||||
JOIN VIEW_BOM V ON B.parent_objid = V.child_objid
|
||||
AND V.bom_report_objid = B.bom_report_objid
|
||||
AND (B.status NOT IN ('deleting', 'deleted') OR B.status IS NULL)
|
||||
),
|
||||
MAX_LEV AS (SELECT MAX(lev) AS max_level FROM VIEW_BOM)
|
||||
SELECT
|
||||
V.objid::text AS "OBJID",
|
||||
V.bom_report_objid AS "BOM_REPORT_OBJID",
|
||||
V.parent_objid AS "PARENT_OBJID",
|
||||
V.child_objid AS "CHILD_OBJID",
|
||||
V.parent_part_no AS "PARENT_PART_NO",
|
||||
V.last_part_objid AS "LAST_PART_OBJID",
|
||||
V.last_part_objid AS "BOM_LAST_PART_OBJID",
|
||||
V.qty AS "QTY",
|
||||
V.qty_temp AS "QTY_TEMP",
|
||||
V.status AS "STATUS",
|
||||
V.seq AS "SEQ",
|
||||
V.lev AS "LEVEL",
|
||||
(SELECT max_level FROM MAX_LEV) AS "MAX_LEVEL",
|
||||
(CASE WHEN EXISTS (SELECT 1 FROM bom_part_qty C WHERE C.parent_objid = V.child_objid
|
||||
AND C.bom_report_objid = V.bom_report_objid) THEN 0 ELSE 1 END) AS "LEAF",
|
||||
array_to_string(V.parent_parts_arr, ',') AS "PARENT_PARTS",
|
||||
P.objid::text AS "PART_OBJID",
|
||||
P.part_no AS "PART_NO",
|
||||
P.part_name AS "PART_NAME",
|
||||
P.revision AS "REVISION",
|
||||
P.material AS "MATERIAL",
|
||||
P.spec AS "SPEC",
|
||||
P.maker AS "MAKER",
|
||||
P.eo_no AS "EO_NO",
|
||||
P.eo_date AS "EO_DATE",
|
||||
P.part_type AS "PART_TYPE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = P.part_type) AS "PART_TYPE_TITLE",
|
||||
(SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '3D_CAD') AS "CU01_CNT",
|
||||
(SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '2D_DRAWING_CAD') AS "CU02_CNT",
|
||||
(SELECT COUNT(*) FROM attach_file_info WHERE target_objid = P.objid::text AND doc_type = '2D_PDF_CAD') AS "CU03_CNT"
|
||||
FROM VIEW_BOM V
|
||||
LEFT JOIN part_mng P ON P.objid::text = V.last_part_objid
|
||||
ORDER BY V.path, V.seq NULLS LAST, V.regdate`,
|
||||
[bomReportObjId],
|
||||
);
|
||||
|
||||
return NextResponse.json({ RESULTLIST: rows, HEADER: header ?? {} });
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows, queryOne } from "@/lib/db";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
// BOM 구조 트리 조회 - bom_report_objid 기준 계층 + PART 정보
|
||||
// (setStructurePopupMainFS 및 bom-ascending 용)
|
||||
export async function POST(request: NextRequest) {
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const bomReportObjId = String(body.bomReportObjId || body.OBJID || "");
|
||||
|
||||
let info: Record<string, unknown> | null = null;
|
||||
if (bomReportObjId) {
|
||||
info = await queryOne(
|
||||
`SELECT T.objid::text AS "OBJID",
|
||||
T.status AS "STATUS",
|
||||
T.revision AS "REVISION",
|
||||
T.note AS "NOTE",
|
||||
T.deploy_date AS "DEPLOY_DATE",
|
||||
(SELECT supply_name FROM supply_mng O WHERE O.objid::varchar = T.customer_objid) AS "CUSTOMER_NAME",
|
||||
(SELECT project_no FROM project_mgmt O WHERE O.objid = T.contract_objid) AS "PROJECT_NO",
|
||||
(SELECT customer_project_name FROM project_mgmt O WHERE O.objid = T.contract_objid) AS "CUSTOMER_PROJECT_NAME",
|
||||
(SELECT O.unit_no || '-' || O.task_name FROM pms_wbs_task O WHERE O.objid = T.unit_code) AS "UNIT_NAME"
|
||||
FROM part_bom_report T WHERE T.objid = $1`,
|
||||
[bomReportObjId],
|
||||
);
|
||||
}
|
||||
|
||||
const parts = bomReportObjId
|
||||
? await queryRows(
|
||||
`SELECT Q.objid::text AS "OBJID",
|
||||
Q.parent_part_no AS "PARENT_PART_NO",
|
||||
Q.last_part_objid AS "PART_OBJID",
|
||||
Q.qty AS "QTY",
|
||||
Q.status AS "Q_STATUS",
|
||||
P.part_no AS "PART_NO",
|
||||
P.part_name AS "PART_NAME",
|
||||
P.material AS "MATERIAL",
|
||||
P.spec AS "SPEC",
|
||||
P.maker AS "MAKER",
|
||||
P.revision AS "REVISION",
|
||||
P.eo_no AS "EO_NO",
|
||||
P.eo_date AS "EO_DATE",
|
||||
P.part_type AS "PART_TYPE",
|
||||
(SELECT code_name FROM comm_code WHERE code_id = P.part_type) AS "PART_TYPE_TITLE",
|
||||
P.remark AS "REMARK",
|
||||
(SELECT part_no || ' ' || part_name FROM part_mng SP WHERE SP.objid = Q.parent_part_no) AS "PARENT_PART_INFO"
|
||||
FROM bom_part_qty Q
|
||||
LEFT JOIN part_mng P ON P.objid = Q.last_part_objid
|
||||
WHERE Q.bom_report_objid = $1
|
||||
ORDER BY Q.parent_part_no, P.part_no`,
|
||||
[bomReportObjId],
|
||||
)
|
||||
: [];
|
||||
|
||||
return NextResponse.json({ RESULTLIST: parts, HEADER: info ?? {} });
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user