e8dc97a32f
Deploy momo-erp / deploy (push) Successful in 46s
- 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 에서 추가/수정/삭제 가능
239 lines
9.2 KiB
TypeScript
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>
|
|
);
|
|
}
|