6af863199f
- 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
306 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|