Files
distribution_erp/src/app/(main)/product/bom-register/page.tsx
T
chpark 6af863199f feat: 모모유통 유통관리 ERP (Next.js 16) — MOMO 브랜딩 + distribution DB + momo.junggomoa.com
- fito-nextjs 기반으로 재구성
- 로그인: MOMO 로고 + 모모유통 + 유통관리 ERP, 하단에 본사/지사 주소 표시
- 사이드바 상단: MOMO 아이콘 + 모모유통 + 유통관리 ERP
- 파비콘: /src/app/icon.svg (MOMO 그린 배지)
- layout.tsx title: 모모유통 | 유통관리 ERP
- DB: 183.99.177.40:5432/distribution (fito 스키마 import 완료)
- Traefik: Host(momo.junggomoa.com), 컨테이너 momo-erp
2026-04-25 02:44:40 +09:00

306 lines
13 KiB
TypeScript

"use client";
import { useState, useCallback, useEffect } from "react";
import { DataGrid, type GridColumn } from "@/components/grid/data-grid";
import { SearchForm, SearchField } from "@/components/layout/search-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { FolderCell } from "@/components/ui/folder-cell";
import * as XLSX from "xlsx";
import Swal from "sweetalert2";
// 제품관리_PART 및 구조등록 (원본: partMng/structureList.jsp)
export default function BomRegisterPage() {
const [customerCd, setCustomerCd] = useState("");
const [projectName, setProjectName] = useState("");
const [unitCode, setUnitCode] = useState("");
const [searchUnitName, setSearchUnitName] = useState("");
const [searchWriter, setSearchWriter] = useState("");
const [deployFrom, setDeployFrom] = useState("");
const [deployTo, setDeployTo] = useState("");
const [status, setStatus] = useState("");
const [data, setData] = useState<Record<string, unknown>[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
const [projects, setProjects] = useState<{ value: string; label: string }[]>([]);
const [units, setUnits] = useState<{ value: string; label: string }[]>([]);
const [users, setUsers] = useState<{ value: string; label: string }[]>([]);
// 고객사 목록 (supply_mng)
useEffect(() => {
fetch("/api/common/supply-list", {
method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
}).then((r) => r.json())
.then((j) => setCustomers((j.RESULTLIST || []).map((r: Record<string, unknown>) => ({
value: String(r.OBJID), label: String(r.SUPPLY_NAME || r.OBJID),
}))))
.catch(() => {});
}, []);
useEffect(() => {
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
.then((r) => r.json())
.then((j) => setUsers((j.RESULTLIST || []).map((r: Record<string, unknown>) => ({
value: String(r.USER_ID), label: String(r.USER_NAME || ""),
}))))
.catch(() => {});
}, []);
// 고객사 변경 시 프로젝트 로드
useEffect(() => {
setProjectName(""); setUnitCode("");
fetch("/api/common/project-list", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ customer_cd: customerCd }),
}).then((r) => r.json())
.then((j) => setProjects((j.RESULTLIST || []).map((r: Record<string, unknown>) => ({
value: String(r.OBJID), label: String(r.LABEL || r.PROJECT_NO || r.OBJID),
}))))
.catch(() => {});
}, [customerCd]);
// 프로젝트 변경 시 유닛 로드
useEffect(() => {
setUnitCode("");
if (!projectName) { setUnits([]); return; }
fetch("/api/common/unit-list", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ contract_objid: projectName }),
}).then((r) => r.json())
.then((j) => setUnits((j.RESULTLIST || []).map((r: Record<string, unknown>) => ({
value: String(r.OBJID), label: String(r.UNIT_NAME || r.TASK_NAME),
}))))
.catch(() => {});
}, [projectName]);
const openChangeDesignNote = (objIds: string) => {
const w = 1000, h = 200;
const left = (window.screen.width - w) / 2;
const top = (window.screen.height - h) / 2;
window.open(
`/product/popup/change-design-note?objId=${encodeURIComponent(objIds)}`,
"changeDesignNotePopUp",
`width=${w},height=${h},left=${left},top=${top}`,
);
};
const openStructureDetail = (objId: string) => {
window.open(
`/product/popup/set-structure?objId=${encodeURIComponent(objId)}`,
"setStructurePopup",
"width=1880,height=900,resizable=yes,scrollbars=yes",
);
};
const columns: GridColumn[] = [
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
{ title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" },
{ title: "고객사프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 200, hozAlign: "left" },
{ title: "유닛명", field: "UNIT_NAME", width: 270, hozAlign: "left" },
{
title: "E-BOM", field: "BOM_CNT", width: 80, hozAlign: "center",
formatter: (cell) => <FolderCell count={cell} />,
cellClick: (row) => openStructureDetail(String(row.OBJID)),
},
{ title: "등록자", field: "DEPT_USER_NAME", width: 120, hozAlign: "center" },
{ title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" },
{ title: "배포일", field: "DEPLOY_DATE", width: 100, hozAlign: "center" },
{ title: "Version", field: "REVISION", width: 85, hozAlign: "center" },
{ title: "배포사유", field: "NOTE", hozAlign: "left" },
{ title: "상태", field: "STATUS_TITLE", width: 100, hozAlign: "center" },
];
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await fetch("/api/product/bom", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
mode: "structure",
customer_cd: customerCd,
project_name: projectName,
unit_code: unitCode,
SEARCH_UNIT_NAME: searchUnitName,
SEARCH_WRITER: searchWriter,
SEARCH_DEPLOY_DATE_FROM: deployFrom,
SEARCH_DEPLOY_DATE_TO: deployTo,
status,
}),
});
if (res.ok) {
const j = await res.json();
setData(j.RESULTLIST || []);
}
} finally {
setLoading(false);
}
}, [customerCd, projectName, unitCode, searchUnitName, searchWriter, deployFrom, deployTo, status]);
useEffect(() => { fetchData(); }, [fetchData]);
const handleDeploy = async () => {
if (selectedRows.length === 0) { Swal.fire("알림", "선택된 내용이 없습니다.", "warning"); return; }
const isDeployed = selectedRows.some((r) => r.STATUS === "deploy");
if (isDeployed) { Swal.fire("알림", "배포완료건은 배포 할 수 없습니다.", "warning"); return; }
// 동시배포 체크 (MULTI_MASTER_OBJID 동일해야 동시배포 가능)
if (selectedRows.length > 1) {
let prevMaster = "";
let onlyMulti = true;
for (const r of selectedRows) {
const master = String(r.MULTI_MASTER_OBJID || r.OBJID);
if (!prevMaster || master === prevMaster) prevMaster = master;
else { onlyMulti = false; break; }
}
if (!onlyMulti) {
Swal.fire("알림", "한번에 한개의 배포만 가능합니다.(동시 프로젝트 등록중인 건만 동시배포 가능합니다.)", "warning");
return;
}
}
const c = await Swal.fire({
title: "선택된 내용을 배포하시겠습니까?", icon: "warning",
showCancelButton: true, confirmButtonText: "확인", cancelButtonText: "취소",
});
if (!c.isConfirmed) return;
openChangeDesignNote(selectedRows.map((r) => String(r.OBJID)).join(","));
};
const handleDelete = async () => {
if (selectedRows.length === 0) { Swal.fire("알림", "선택된 내용이 없습니다.", "warning"); return; }
const blocked = selectedRows.some((r) => r.STATUS === "deploy" || r.STATUS === "changeDesign");
if (blocked) { Swal.fire("알림", "배포완료/설계변경미배포 건은 삭제 할 수 없습니다.", "warning"); return; }
const c = await Swal.fire({
title: "선택한 정보를 삭제하시겠습니까?", icon: "warning",
showCancelButton: true, confirmButtonText: "확인", cancelButtonText: "취소",
});
if (!c.isConfirmed) return;
const res = await fetch("/api/product/bom/structure-delete", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }),
});
const j = await res.json();
if (j.success) {
await Swal.fire({ icon: "success", title: j.message, timer: 1200, showConfirmButton: false });
fetchData();
} else {
Swal.fire("오류", j.message, "error");
}
};
const handleExcelDownload = () => {
if (data.length === 0) {
Swal.fire("알림", "다운로드할 데이터가 없습니다.", "warning");
return;
}
const header = [
"프로젝트번호", "고객사", "고객사프로젝트명", "유닛명", "E-BOM",
"등록자", "등록일", "배포일", "Version", "배포사유", "상태",
];
const body = data.map((r) => [
r.PROJECT_NO ?? "", r.CUSTOMER_NAME ?? "", r.CUSTOMER_PROJECT_NAME ?? "",
r.UNIT_NAME ?? "", Number(r.BOM_CNT ?? 0), r.DEPT_USER_NAME ?? "",
r.REG_DATE ?? "", r.DEPLOY_DATE ?? "", r.REVISION ?? "",
r.NOTE ?? "", r.STATUS_TITLE ?? "",
]);
const ws = XLSX.utils.aoa_to_sheet([header, ...body]);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "PART 및 구조등록");
const today = new Date().toISOString().slice(0, 10);
XLSX.writeFile(wb, `PART_및_구조등록_${today}.xlsx`);
};
const handleSaveExcel = () => {
if (selectedRows.length > 1) { Swal.fire("알림", "단건만 등록 가능합니다.", "warning"); return; }
if (selectedRows.length === 1) {
const r = selectedRows[0];
if (String(r.STATUS || "") !== "create") {
Swal.fire("알림", "등록중인 건만 등록/추가 할 수 있습니다.", "warning");
return;
}
const qs = new URLSearchParams({
customer_cd: String(r.CUSTOMER_OBJID || ""),
project_name: String(r.CONTRACT_OBJID || ""),
unit_code: String(r.UNIT_CODE || ""),
BOM_REPORT_OBJID: String(r.OBJID || ""),
}).toString();
window.open(`/product/popup/bom-excel-import?${qs}`, "openBomReportExcelImportPopUp",
"width=1920,height=860,resizable=yes,scrollbars=yes");
return;
}
if (!customerCd) { Swal.fire("알림", "고객사를 선택해 주세요.", "warning"); return; }
if (!projectName) { Swal.fire("알림", "프로젝트를 선택해 주세요.", "warning"); return; }
if (!unitCode) { Swal.fire("알림", "유닛명을 선택해 주세요.", "warning"); return; }
const qs = new URLSearchParams({
customer_cd: customerCd, project_name: projectName, unit_code: unitCode,
}).toString();
window.open(`/product/popup/bom-excel-import?${qs}`, "openBomReportExcelImportPopUp",
"width=1920,height=860,resizable=yes,scrollbars=yes");
};
return (
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-gray-800">_PART </h2>
<div className="flex gap-2">
<Button size="sm" onClick={handleDeploy}></Button>
<Button size="sm" variant="danger" onClick={handleDelete}></Button>
<Button size="sm" onClick={handleSaveExcel}></Button>
<Button size="sm" onClick={fetchData}></Button>
<Button size="sm" variant="secondary" onClick={() => {
setCustomerCd(""); setProjectName(""); setUnitCode("");
setSearchUnitName(""); setSearchWriter("");
setDeployFrom(""); setDeployTo(""); setStatus("");
}}></Button>
<Button size="sm" variant="secondary" onClick={handleExcelDownload}>Excel Download</Button>
</div>
</div>
<SearchForm onSearch={fetchData}>
<SearchField label="고객사">
<SearchableSelect options={customers} value={customerCd} onChange={setCustomerCd} className="w-[200px]" />
</SearchField>
<SearchField label="프로젝트번호">
<SearchableSelect options={projects} value={projectName} onChange={setProjectName} className="w-[230px]" />
</SearchField>
<SearchField label="유닛명">
<SearchableSelect options={units} value={unitCode} onChange={setUnitCode} className="w-[300px]" />
</SearchField>
<SearchField label="공통유닛명">
<Input value={searchUnitName} onChange={(e) => setSearchUnitName(e.target.value)} className="w-[140px]" />
</SearchField>
<SearchField label="등록자">
<SearchableSelect options={users} value={searchWriter} onChange={setSearchWriter} className="w-[190px]" />
</SearchField>
<SearchField label="배포일">
<div className="flex items-center gap-1">
<Input type="date" value={deployFrom} onChange={(e) => setDeployFrom(e.target.value)} className="w-[140px]" />
<span>~</span>
<Input type="date" value={deployTo} onChange={(e) => setDeployTo(e.target.value)} className="w-[140px]" />
</div>
</SearchField>
<SearchField label="상태">
<select value={status} onChange={(e) => setStatus(e.target.value)}
className="h-9 w-[170px] rounded border border-gray-300 bg-white px-2 text-sm">
<option value=""></option>
<option value="create"></option>
<option value="changeDesign"></option>
<option value="deploy"></option>
</select>
</SearchField>
</SearchForm>
<DataGrid
columns={columns} data={data} showCheckbox loading={loading}
onSelectionChange={setSelectedRows}
/>
</div>
);
}