Files
distribution_erp/src/app/(main)/product/part-register/page.tsx
T
chpark e8dc97a32f
Deploy momo-erp / deploy (push) Successful in 46s
feat: FITO admin-panel 복원 + menu_info 에 모모유통 메뉴 19개 등록
- src/app/(main), admin, admin-panel, common, api/{admin,common,menu} 복원
- /api/auth/login: FITO 인증 다시 활성화 (plm_admin 등 FITO 사용자 로그인 가능)
- 미들웨어: 옛 경로 강제 리다이렉트 제거
- /m/layout.tsx: FITO 슈퍼관리자(isAdmin)도 ADMIN 으로 받아 모모 페이지 진입 허용
- DB 005: menu_info 에 모모유통 루트(9000000) + 자식 19개(/m/* URL 직접 연결)
  → plm_admin 로그인 후 사이드바 [모모유통] 그룹에서 클릭 시 동작
  → 메뉴 관리 UI 에서 추가/수정/삭제 가능
2026-04-25 23:47:13 +09:00

239 lines
9.2 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 Swal from "sweetalert2";
// 제품관리_PART 등록 (원본: partMng/partMngTempList.jsp)
export default function PartRegisterPage() {
const currentYear = new Date().getFullYear();
const [searchYear, setSearchYear] = useState("");
const [searchPartNo, setSearchPartNo] = useState("");
const [searchPartName, setSearchPartName] = useState("");
const [writer, setWriter] = useState("");
const [data, setData] = useState<Record<string, unknown>[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
const [users, setUsers] = useState<{ value: string; label: string }[]>([]);
useEffect(() => {
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
.then((r) => r.json())
.then((d) => setUsers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
value: String(r.USER_ID), label: String(r.USER_NAME || ""),
}))))
.catch(() => {});
}, []);
const centerPopup = (url: string, name: string, w: number, h: number) => {
const left = (window.screen.width - w) / 2;
const top = (window.screen.height - h) / 2;
window.open(url, name, `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`);
};
const openPartForm = (objId?: string) => {
const url = objId
? `/product/popup/part-form?objId=${encodeURIComponent(objId)}`
: `/product/popup/part-form`;
centerPopup(url, "partMngPopUp", 900, 600);
};
const openFileRegist = (objId: string, docType: string, docTypeName: string) => {
centerPopup(
`/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`,
"fileRegistPopUp",
800,
300,
);
};
const columns: GridColumn[] = [
{ title: "순", field: "RNUM", width: 50, hozAlign: "center" },
{ title: "품명", field: "PART_NAME", width: 240, hozAlign: "left", frozen: true },
{ title: "모품번", field: "PARENT_PART_INFO", width: 120, hozAlign: "left", frozen: true },
{
title: "품번", field: "PART_NO", width: 160, hozAlign: "left", frozen: true,
cellClick: (row) => openPartForm(String(row.OBJID)),
},
{ title: "수량", field: "Q_QTY", width: 70, hozAlign: "right" },
{
title: "3D", field: "CU01_CNT", width: 60, hozAlign: "center",
formatter: (cell) => <FolderCell count={cell} />,
cellClick: (row) => openFileRegist(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일"),
},
{
title: "2D", field: "CU02_CNT", width: 60, hozAlign: "center",
formatter: (cell) => <FolderCell count={cell} />,
cellClick: (row) => openFileRegist(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"),
},
{
title: "PDF", field: "CU03_CNT", width: 60, hozAlign: "center",
formatter: (cell) => <FolderCell count={cell} />,
cellClick: (row) => openFileRegist(String(row.OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일"),
},
{ title: "재질", field: "MATERIAL", width: 100, hozAlign: "left" },
{ title: "사양(규격)", field: "SPEC", width: 180, hozAlign: "left" },
{ title: "후처리", field: "POST_PROCESSING", width: 90, hozAlign: "left" },
{ title: "MAKER", field: "MAKER", width: 90, hozAlign: "left" },
{ title: "대분류", field: "MAJOR_CATEGORY", width: 110, hozAlign: "left" },
{ title: "중분류", field: "SUB_CATEGORY", width: 110, hozAlign: "left" },
{ title: "Revision", field: "REVISION", width: 80, hozAlign: "center" },
{ title: "EO No", field: "EO_NO", width: 90, hozAlign: "center" },
{ title: "EO Date", field: "EO_DATE", width: 100, hozAlign: "center" },
{ title: "PART 구분", field: "PART_TYPE_TITLE", width: 100, hozAlign: "center" },
{ title: "비고", field: "REMARK", width: 120, hozAlign: "left" },
];
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await fetch("/api/product/part", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
mode: "register",
SEARCH_YEAR: searchYear,
SEARCH_PART_NO: searchPartNo,
SEARCH_PART_NAME: searchPartName,
WRITER: writer,
}),
});
if (res.ok) {
const json = await res.json();
setData(json.RESULTLIST || []);
setSelectedRows([]);
}
} catch {
Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error");
} finally {
setLoading(false);
}
}, [searchYear, searchPartNo, searchPartName, writer]);
useEffect(() => { fetchData(); }, [fetchData]);
const handleDeploy = async () => {
if (selectedRows.length === 0) {
Swal.fire("알림", "선택된 Part가 없습니다.", "warning");
return;
}
const r = await Swal.fire({
title: "같은 Part는 동시 확정됩니다. 선택된 Part를 확정하시겠습니까?",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "확인",
cancelButtonText: "취소",
});
if (!r.isConfirmed) return;
const res = await fetch("/api/product/part/deploy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: selectedRows }),
});
const json = await res.json();
if (json.success) {
await Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
fetchData();
} else {
Swal.fire("오류", json.message, "error");
}
};
const handleDelete = async () => {
if (selectedRows.length === 0) {
Swal.fire("알림", "선택된 Part가 없습니다.", "warning");
return;
}
const r = await Swal.fire({
title: "선택된 Part를 삭제하시겠습니까?",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "확인",
cancelButtonText: "취소",
});
if (!r.isConfirmed) return;
const res = await fetch("/api/product/part/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ objIds: selectedRows.map((row) => String(row.OBJID)) }),
});
const json = await res.json();
if (json.success) {
await Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
fetchData();
} else {
Swal.fire("오류", json.message, "error");
}
};
const handleExcelPopup = () => {
centerPopup(
"/product/popup/part-excel-import",
"openPartExcelImportPopUp",
1520,
860,
);
};
const handleReset = () => {
setSearchYear(""); setSearchPartNo(""); setSearchPartName(""); setWriter("");
};
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={() => openPartForm()}></Button>
<Button size="sm" variant="secondary" onClick={handleExcelPopup}>(Excel Upload)</Button>
<Button size="sm" onClick={fetchData}></Button>
<Button size="sm" variant="secondary" onClick={handleReset}></Button>
</div>
</div>
<SearchForm onSearch={fetchData}>
<SearchField label="년도">
<select value={searchYear} onChange={(e) => setSearchYear(e.target.value)}
className="h-9 w-[120px] rounded border border-gray-300 bg-white px-2 text-sm">
<option value=""></option>
{Array.from({ length: 5 }, (_, i) => currentYear - i).map((y) => (
<option key={y} value={y}>{y}</option>
))}
</select>
</SearchField>
<SearchField label="품번">
<Input value={searchPartNo} onChange={(e) => setSearchPartNo(e.target.value)} className="w-[180px]" />
</SearchField>
<SearchField label="품명">
<Input value={searchPartName} onChange={(e) => setSearchPartName(e.target.value)} className="w-[160px]" />
</SearchField>
<SearchField label="등록자">
<SearchableSelect options={users} value={writer} onChange={setWriter} className="w-[160px]" />
</SearchField>
</SearchForm>
<DataGrid
columns={columns}
data={data}
showCheckbox
loading={loading}
onSelectionChange={setSelectedRows}
/>
</div>
);
}