menu_info 에 등록된 활성 URL prefix 는 m/* 와 admin/* 뿐 — 그 외 폴더는 옛 FITO/우성 레거시. 사용자 토글 시 진입되던 /dashboard 더미 페이지 포함 전부 정리. 삭제 폴더 (총 23개): - dashboard (영업현황/제품별현황 더미) - approval/bom/cost/cost-mgmt/cs/delivery/fund/inventory - order/part/part-mgmt/procurement-std/product/product-mgmt - production/project/purchase/purchase-order/quality/sales/scm/work 유지: m/, admin/, profile/. /api/admin/* 와 /api/approval 등 API 라우트는 admin-panel 의 일부 inline 컴포넌트가 참조하므로 그대로 둠. tsc 타입 체크 통과.
This commit is contained in:
@@ -1 +0,0 @@
|
||||
{"sessionId":"c8c36d3d-1e9a-49fa-947f-2e00ae43f9b6","pid":20231,"procStart":"Tue May 12 08:01:45 2026","acquiredAt":1778576701825}
|
||||
@@ -1,138 +0,0 @@
|
||||
"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 { ApprovalStatusBadge } from "@/components/approval/ApprovalStatusBadge";
|
||||
import { TARGET_NAME_MAP } from "@/components/approval/TargetLinkMap";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// approvalList.jsp 대응 - 결재함 (미결재/승인/반려/전체 탭)
|
||||
type Tab = "PENDING" | "APPROVED" | "REJECTED" | "";
|
||||
|
||||
const TABS: { key: Tab; label: string }[] = [
|
||||
{ key: "PENDING", label: "미결재" },
|
||||
{ key: "APPROVED", label: "승인" },
|
||||
{ key: "REJECTED", label: "반려" },
|
||||
{ key: "", label: "전체" },
|
||||
];
|
||||
|
||||
export default function ApprovalPage() {
|
||||
const [tab, setTab] = useState<Tab>("PENDING");
|
||||
const [title, setTitle] = useState("");
|
||||
const [writerName, setWriterName] = useState("");
|
||||
const [fromDate, setFromDate] = useState("");
|
||||
const [toDate, setToDate] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const openDetail = (row: Record<string, unknown>) => {
|
||||
const w = 900, h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(`/approval/form?objId=${row.APPROVAL_OBJID || row.OBJID}`,
|
||||
"approvalDetail",
|
||||
`width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "결재번호", field: "APPROVAL_NO", width: 110, hozAlign: "center" },
|
||||
{
|
||||
title: "대상구분", field: "TYPE_NAME", width: 120, hozAlign: "left",
|
||||
formatter: (_, row) => (TARGET_NAME_MAP[String(row.TARGET_TYPE)] || String(row.TYPE_NAME ?? row.TARGET_TYPE ?? "-")),
|
||||
},
|
||||
{
|
||||
title: "제목", field: "TITLE", width: 300, hozAlign: "left",
|
||||
cellClick: openDetail,
|
||||
},
|
||||
{ title: "상신일", field: "REGDATE", width: 110, hozAlign: "center" },
|
||||
{
|
||||
title: "상신자", field: "WRITER_NAME", width: 180, hozAlign: "left",
|
||||
formatter: (_, row) => {
|
||||
const dept = row.DEPT_NAME ? String(row.DEPT_NAME) + " / " : "";
|
||||
return dept + String(row.WRITER_NAME ?? row.WRITER ?? "");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "상태", field: "STATUS_NAME", width: 100, hozAlign: "center",
|
||||
formatter: (_, row) => <ApprovalStatusBadge status={row.STATUS_NAME || row.APPROVAL_STATUS} />,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/approval", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
status: tab,
|
||||
title,
|
||||
writer_name: writerName,
|
||||
from_date: fromDate,
|
||||
to_date: toDate,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} finally { setLoading(false); }
|
||||
}, [tab, title, writerName, fromDate, toDate]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
// 파일 업로드 팝업 등에서 refresh 콜백으로 쓸 수 있게 전역 등록
|
||||
useEffect(() => {
|
||||
(window as unknown as Record<string, unknown>).fn_search = fetchData;
|
||||
return () => { delete (window as unknown as Record<string, unknown>).fn_search; };
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">결재관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="flex items-center border-b border-gray-200 mb-4">
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm border-b-2 -mb-px transition-colors",
|
||||
tab === t.key
|
||||
? "border-primary text-primary font-semibold"
|
||||
: "border-transparent text-gray-500 hover:text-gray-800"
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="제목">
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} className="w-[240px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상신자">
|
||||
<Input value={writerName} onChange={(e) => setWriterName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상신일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={fromDate} onChange={(e) => setFromDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-gray-400">~</span>
|
||||
<Input type="date" value={toDate} onChange={(e) => setToDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
"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";
|
||||
|
||||
// bom/bomList.jsp 대응 - BOM 관리
|
||||
export default function BomPage() {
|
||||
const [productName, setProductName] = useState("");
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/product/part-register/form?objId=${row.OBJID}`, "bomDetail", "width=1200,height=900") },
|
||||
{ title: "제품코드", field: "PRODUCT_CODE", width: 120, hozAlign: "left" },
|
||||
{ title: "Level", field: "BOM_LEVEL", width: 60, hozAlign: "center" },
|
||||
{ title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left" },
|
||||
{ title: "파트명", field: "PART_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "규격", field: "SPEC", width: 120, hozAlign: "left" },
|
||||
{ title: "수량", field: "QTY", width: 70, hozAlign: "right", formatter: "money" },
|
||||
{ title: "단위", field: "UNIT_NAME", width: 60, hozAlign: "center" },
|
||||
{ title: "재질", field: "MATERIAL_NAME", width: 100, hozAlign: "left" },
|
||||
{ title: "비고", field: "REMARK", hozAlign: "left" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/bom", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ product_name: productName, part_no: partNo }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [productName, partNo]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">BOM 관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="제품명">
|
||||
<Input value={productName} onChange={(e) => setProductName(e.target.value)} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="파트번호">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
"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";
|
||||
|
||||
// costMgmt/costMgmtList.jsp 대응 - 원가관리
|
||||
export default function CostMgmtPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120 },
|
||||
{ title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "목표원가", field: "TARGET_COST", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "실적원가", field: "ACTUAL_COST", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "차이", field: "DIFF_COST", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "달성율", field: "ACHIEVE_RATE", width: 80, hozAlign: "center",
|
||||
formatter: (_cell, row) => `${row.ACHIEVE_RATE || 0}%` },
|
||||
{ title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/cost-mgmt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">원가관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// costMgmt/expenseDashBoard.jsp 대응 - 경비관리
|
||||
export default function CostExpensePage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selected, setSelected] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true },
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "경비현황",
|
||||
columns: [
|
||||
{ title: "목표가", field: "EXPENSE_COST_GOAL", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발생경비", field: "TOTAL_SETTLE_AMOUNT", width: 130, hozAlign: "right", formatter: "money" },
|
||||
{ title: "투입율(%)", field: "INPUT_RATE", width: 90, hozAlign: "right" },
|
||||
{ title: "조립", field: "SETTLE_AMOUNT_ASSEMBLE", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "셋업", field: "SETTLE_AMOUNT_SETUP", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "외주(Turn-key)", field: "SETTLE_AMOUNT_CS", width: 130, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/cost/expense", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year, project_no: projectNo, customer_objid: customerObjid, pm_user_id: pmUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
(window as unknown as { fn_search?: () => void }).fn_search = fetchData;
|
||||
return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; };
|
||||
}, [fetchData]);
|
||||
|
||||
const openExpenseApply = () => {
|
||||
const contractObjid = selected.length === 1 ? String(selected[0].OBJID || "") : "";
|
||||
if (selected.length > 1) {
|
||||
Swal.fire({ icon: "warning", title: "한번에 1개의 프로젝트만 선택 가능합니다." });
|
||||
return;
|
||||
}
|
||||
const w = 900, h = 600;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/cost/expense/apply${contractObjid ? `?contractObjid=${encodeURIComponent(contractObjid)}` : ""}`,
|
||||
"expenseApply",
|
||||
`width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">투입원가관리 - 경비관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={openExpenseApply}>경비신청</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// costMgmt/laborCostMgmtList.jsp 대응 - 노무비관리
|
||||
export default function CostLaborPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true },
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "고객납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "노무비현황",
|
||||
columns: [
|
||||
{ title: "목표가", field: "LABOR_COST_GOAL", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "실투입노무비", field: "LABOR_COST_ACTUAL", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "투입율(%)", field: "LABOR_INPUT_RATE", width: 90, hozAlign: "right" },
|
||||
{ title: "투입공수(H)", field: "LABOR_HOURS", width: 100, hozAlign: "right" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/cost/labor", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year, project_no: projectNo, customer_objid: customerObjid,
|
||||
product, pm_user_id: pmUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">투입원가관리 - 노무비관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// costMgmt/materialCostTotaltList.jsp 대응 - 재료비관리
|
||||
export default function CostMaterialPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true },
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "고객납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "재료비현황",
|
||||
columns: [
|
||||
{ title: "목표가", field: "MATERIAL_COST_GOAL", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "실투입재료비", field: "ALL_TOTAL_PRICE", width: 130, hozAlign: "right", formatter: "money" },
|
||||
{ title: "투입율(%)", field: "MATERIAL_COST_GOAL_RATE", width: 90, hozAlign: "right" },
|
||||
{ title: "발주금액", field: "NEW_TOTAL_PRICE", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "재발주금액", field: "ALL_TOTAL_PRICE_RE", width: 120, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/cost/material", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year, project_no: projectNo, customer_objid: customerObjid,
|
||||
product, pm_user_id: pmUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">투입원가관리 - 재료비관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// costMgmt/costTotaltList.jsp 대응 - 투입원가관리 현황
|
||||
export default function CostStatusPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selected, setSelected] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true },
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "투입원가현황",
|
||||
columns: [
|
||||
{ title: "수주가", field: "CONTRACT_PRICE", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "목표가", field: "TOTAL_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "실투입원가", field: "TOTAL_COST_ACTUAL", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "투입율(%)", field: "TOTAL_INPUT_RATE", width: 80, hozAlign: "right" },
|
||||
{ title: "MC율(%)", field: "MC_RATE", width: 80, hozAlign: "right" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "재료비현황",
|
||||
columns: [
|
||||
{ title: "목표가", field: "MATERIAL_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발생재료비", field: "ACCRUAL_MATERIAL_COST", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "투입율(%)", field: "MATERIAL_COST_GOAL_RATE", width: 90, hozAlign: "right" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "노무비현황",
|
||||
columns: [
|
||||
{ title: "목표가", field: "LABOR_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발생노무비", field: "LABOR_COST_ACTUAL", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "투입율(%)", field: "LABOR_INPUT_RATE", width: 90, hozAlign: "right" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "경비현황",
|
||||
columns: [
|
||||
{ title: "목표가", field: "EXPENSE_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발생경비", field: "ACCRUAL_EXPENSE", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "투입율(%)", field: "EXPENSE_RATE", width: 90, hozAlign: "right" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/cost/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year, project_no: projectNo, customer_objid: customerObjid,
|
||||
product, pm_user_id: pmUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
// 팝업 저장 후 새로고침
|
||||
useEffect(() => {
|
||||
(window as unknown as { fn_search?: () => void }).fn_search = fetchData;
|
||||
return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; };
|
||||
}, [fetchData]);
|
||||
|
||||
const openGoalPopup = () => {
|
||||
if (selected.length === 0) {
|
||||
Swal.fire({ icon: "warning", title: "선택된 내용이 없습니다." });
|
||||
return;
|
||||
}
|
||||
if (selected.length > 1) {
|
||||
Swal.fire({ icon: "warning", title: "한번에 1개의 내용만 등록 가능합니다." });
|
||||
return;
|
||||
}
|
||||
const contractObjid = String(selected[0].OBJID || "");
|
||||
const w = 500;
|
||||
const h = 350;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/cost/goal/form?contractObjid=${encodeURIComponent(contractObjid)}`,
|
||||
"costGoalForm",
|
||||
`width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">투입원가관리 현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={openGoalPopup}>목표가 등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// csMgmt/csChartList.jsp 대응 - CS 차트관리
|
||||
export default function CsChartPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [productCode, setProductCode] = useState("");
|
||||
const [chartData, setChartData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/cs/chart", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
product_code: productCode,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setChartData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, productCode]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">CS 차트관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={productCode} onChange={setProductCode} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-6" style={{ height: "calc(100vh - 350px)" }}>
|
||||
{chartData.length > 0 ? (
|
||||
<div className="text-center text-gray-500">
|
||||
{/* TODO: Chart rendering - integrate with chart library */}
|
||||
<p className="text-sm">차트 데이터 {chartData.length}건 로드됨</p>
|
||||
<p className="text-xs text-gray-400 mt-2">차트 라이브러리 연동 후 표시됩니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">
|
||||
조회 버튼을 클릭하여 차트 데이터를 로드하세요.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
import { ApprovalButton } from "@/components/approval/ApprovalButton";
|
||||
import { ApprovalStatusBadge } from "@/components/approval/ApprovalStatusBadge";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// asMngList_CS.jsp 대응 - CS등록 및 조치
|
||||
export default function CsManagePage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [productCd, setProductCd] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [warranty, setWarranty] = useState("");
|
||||
const [recStartDate, setRecStartDate] = useState("");
|
||||
const [recEndDate, setRecEndDate] = useState("");
|
||||
const [managerId, setManagerId] = useState("");
|
||||
const [actStartDate, setActStartDate] = useState("");
|
||||
const [actEndDate, setActEndDate] = useState("");
|
||||
const [apprStatus, setApprStatus] = useState("");
|
||||
|
||||
const [projectOpts, setProjectOpts] = useState<{ value: string; label: string }[]>([]);
|
||||
const [userOpts, setUserOpts] = useState<{ value: string; label: string }[]>([]);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((j) =>
|
||||
setProjectOpts(
|
||||
(j.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.LABEL || r.PROJECT_NO || r.OBJID),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => setProjectOpts([]));
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((j) =>
|
||||
setUserOpts(
|
||||
(j.RESULTLIST || []).map((u: Record<string, unknown>) => ({
|
||||
value: String(u.USER_ID),
|
||||
label: `${u.USER_NAME || u.USER_ID}${u.DEPT_NAME ? ` (${u.DEPT_NAME})` : ""}`,
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => setUserOpts([]));
|
||||
}, []);
|
||||
|
||||
const openFormPopup = (objId = "") => {
|
||||
const w = 1400, h = 930;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/cs/manage/form${objId ? `?objId=${objId}` : ""}`,
|
||||
"asMngFormPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const openFileRegist = (objId: string) => {
|
||||
const w = 800, h = 500;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/common/files?targetObjId=${encodeURIComponent(objId)}&docType=AS_DOC_01&docTypeName=${encodeURIComponent("CS 조치내역 첨부파일")}`,
|
||||
"fileAS_DOC_01",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const openApprovalDetail = (approvalObjId: string) => {
|
||||
if (!approvalObjId) return;
|
||||
const w = 900, h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/approval/form?objId=${approvalObjId}`,
|
||||
"approvalDetailPopup",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "접수 No.", field: "SERVICE_NO", width: 110, hozAlign: "left",
|
||||
cellClick: (row) => openFormPopup(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "제품구분(기계형식)", field: "PRODUCT_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 130, hozAlign: "left" },
|
||||
{ title: "출고일자", field: "RELEASE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 120, hozAlign: "left" },
|
||||
{ title: "유무상", field: "WARRANTY_NAME", width: 80, hozAlign: "left" },
|
||||
{ title: "CS구분", field: "CATEGORY_NAME", width: 100, hozAlign: "left" },
|
||||
{ title: "유형", field: "CATEGORY_H_NAME", width: 100, hozAlign: "left" },
|
||||
{ title: "제목", field: "TITLE", width: 200, hozAlign: "left" },
|
||||
{ title: "접수일", field: "REC_DT", width: 100, hozAlign: "center" },
|
||||
{ title: "예상발생비용", field: "PLAN_COST", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "등록자", field: "MANAGER_NAME", width: 90, hozAlign: "left" },
|
||||
{ title: "조치완료일", field: "ACT_DATE", width: 110, hozAlign: "center" },
|
||||
{
|
||||
title: "첨부파일", field: "CU03_CNT", width: 80, hozAlign: "center",
|
||||
formatter: (cell, row) => (
|
||||
<FolderCell
|
||||
count={Number(cell) || 0}
|
||||
onClick={() => openFileRegist(String(row.OBJID || ""))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "상태", field: "APPR_STATUS_NAME", width: 100, hozAlign: "center",
|
||||
formatter: (_, row) => {
|
||||
const apv = String(row.APPROVAL_OBJID || "");
|
||||
return (
|
||||
<ApprovalStatusBadge
|
||||
status={row.APPR_STATUS_NAME || row.APPR_STATUS}
|
||||
onClick={apv ? () => openApprovalDetail(apv) : undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/cs/manage", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
product_cd: productCd,
|
||||
project_no: projectNo,
|
||||
warranty,
|
||||
rec_start_date: recStartDate,
|
||||
rec_end_date: recEndDate,
|
||||
manager_id: managerId,
|
||||
act_start_date: actStartDate,
|
||||
act_end_date: actEndDate,
|
||||
appr_status: apprStatus,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [year, productCd, projectNo, warranty, recStartDate, recEndDate, managerId, actStartDate, actEndDate, apprStatus]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
const result = await Swal.fire({
|
||||
title: "삭제 확인",
|
||||
text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`,
|
||||
icon: "question", showCancelButton: true,
|
||||
confirmButtonText: "삭제", cancelButtonText: "취소",
|
||||
});
|
||||
if (!result.isConfirmed) return;
|
||||
const res = await fetch("/api/cs/manage/delete", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: json.message || "삭제되었습니다.", timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message || "삭제 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// 단건만 결재 허용 + 이미 진행/완료건 필터
|
||||
const approvalRow = selectedRows.length === 1 ? selectedRows[0] : null;
|
||||
const approvalStatus = String(approvalRow?.APPR_STATUS_NAME || "");
|
||||
const canRequestApproval =
|
||||
!!approvalRow && approvalStatus !== "결재중" && approvalStatus !== "결재완료";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">CS관리_CS등록 및 조회</h2>
|
||||
<div className="flex gap-2">
|
||||
<ApprovalButton
|
||||
objIds={approvalRow ? [String(approvalRow.OBJID)] : []}
|
||||
targetType="CSM"
|
||||
title={approvalRow ? `CS조치내역서 상신 - ${approvalRow.TITLE || approvalRow.SERVICE_NO || ""}` : ""}
|
||||
description={approvalRow ? String(approvalRow.TITLE || "") : ""}
|
||||
onSuccess={fetchData}
|
||||
disabled={!canRequestApproval}
|
||||
/>
|
||||
<Button size="sm" onClick={() => openFormPopup("")}>등록</Button>
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[110px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={productCd} onChange={setProductCd} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<SearchableSelect options={projectOpts} value={projectNo} onChange={setProjectNo} className="w-[220px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유/무상">
|
||||
<SearchableCodeSelect codeId="0000156" value={warranty} onChange={setWarranty} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="접수일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={recStartDate} onChange={(e) => setRecStartDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-gray-400">~</span>
|
||||
<Input type="date" value={recEndDate} onChange={(e) => setRecEndDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="조치담당자">
|
||||
<SearchableSelect options={userOpts} value={managerId} onChange={setManagerId} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="조치완료일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={actStartDate} onChange={(e) => setActStartDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-gray-400">~</span>
|
||||
<Input type="date" value={actEndDate} onChange={(e) => setActEndDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<select
|
||||
value={apprStatus}
|
||||
onChange={(e) => setApprStatus(e.target.value)}
|
||||
className="h-9 w-[130px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
<option value="inProcess">결재중</option>
|
||||
<option value="reject">반려</option>
|
||||
<option value="complete">결재완료</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
"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";
|
||||
|
||||
// customerMng/customerServiceList.jsp 대응 - CS관리
|
||||
export default function CsPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [customerName, setCustomerName] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "접수번호", field: "CS_NO", width: 120,
|
||||
cellClick: (row) => window.open(`/cs/manage/form?objId=${row.OBJID}`, "csDetail", "width=900,height=700") },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "제품명", field: "PRODUCT_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "접수일", field: "RECEIPT_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "유형", field: "CS_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "내용", field: "DESCRIPTION", hozAlign: "left" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "담당자", field: "CHARGER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "완료일", field: "COMPLETE_DATE", width: 100, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/cs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, customer_name: customerName }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, customerName]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">CS 관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => window.open("/cs/manage/form", "csForm", "width=900,height=700")}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<Input value={customerName} onChange={(e) => setCustomerName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { DataGrid, type GridColumn } from "@/components/grid/data-grid";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
|
||||
interface DynColumn {
|
||||
GROUP_SEQ: number;
|
||||
GROUP_CNT: number;
|
||||
GROUP_NAME: string;
|
||||
PARENT_CODE_ID: string;
|
||||
CODE_ID: string;
|
||||
NAME: string;
|
||||
COL_NAME: string;
|
||||
}
|
||||
|
||||
// asList_CS.jsp 대응 - CS관리_현황 (제품×프로젝트 대시보드)
|
||||
export default function CsStatusPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [productCd, setProductCd] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [warranty, setWarranty] = useState("");
|
||||
const [csCategory, setCsCategory] = useState("");
|
||||
const [categoryH, setCategoryH] = useState("");
|
||||
|
||||
const [projectOpts, setProjectOpts] = useState<{ value: string; label: string }[]>([]);
|
||||
const [categoryHOpts, setCategoryHOpts] = useState<{ value: string; label: string }[]>([]);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [dynColumns, setDynColumns] = useState<DynColumn[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((j) =>
|
||||
setProjectOpts(
|
||||
(j.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.LABEL || r.PROJECT_NO || r.OBJID),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => setProjectOpts([]));
|
||||
}, []);
|
||||
|
||||
// cs_category(0000970) 선택 시 유형(category_h) 옵션 로드
|
||||
useEffect(() => {
|
||||
if (!csCategory) {
|
||||
setCategoryHOpts([]);
|
||||
setCategoryH("");
|
||||
return;
|
||||
}
|
||||
fetch("/api/common/code-list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ codeId: csCategory }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((j) =>
|
||||
setCategoryHOpts(
|
||||
(j.data || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.code_id || r.CODE_ID),
|
||||
label: String(r.code_name || r.CODE_NAME),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => setCategoryHOpts([]));
|
||||
}, [csCategory]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/cs/status", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
product_cd: productCd,
|
||||
project_no: projectNo,
|
||||
warranty,
|
||||
cs_category: csCategory,
|
||||
category_h: categoryH,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
setDynColumns(json.COLUMN_LIST || []);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [year, productCd, projectNo, warranty, csCategory, categoryH]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const columns: GridColumn[] = useMemo(() => {
|
||||
// 동적 컬럼 → GROUP_NAME(2nd-level 부모코드명) 기준으로 묶고, leaf는 NAME 사용
|
||||
const groupMap = new Map<string, DynColumn[]>();
|
||||
const groupOrder: string[] = [];
|
||||
dynColumns.forEach((c) => {
|
||||
const key = `${c.PARENT_CODE_ID}::${c.GROUP_NAME}`;
|
||||
if (!groupMap.has(key)) {
|
||||
groupMap.set(key, []);
|
||||
groupOrder.push(key);
|
||||
}
|
||||
groupMap.get(key)!.push(c);
|
||||
});
|
||||
|
||||
const dynGroupColumns: GridColumn[] = groupOrder.map((key) => {
|
||||
const group = groupMap.get(key)!;
|
||||
return {
|
||||
title: group[0].GROUP_NAME || "유형",
|
||||
columns: group.map((c) => ({
|
||||
title: c.NAME,
|
||||
field: c.COL_NAME,
|
||||
width: 90,
|
||||
hozAlign: "right",
|
||||
headerHozAlign: "center",
|
||||
formatter: "money",
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// 유형 최상위 묶음 (있을 때만)
|
||||
const categoryWrapper: GridColumn[] = dynGroupColumns.length
|
||||
? [{ title: "유형", columns: dynGroupColumns }]
|
||||
: [];
|
||||
|
||||
return [
|
||||
{ title: "제품구분", field: "PRODUCT_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" },
|
||||
{
|
||||
title: "유상",
|
||||
columns: [
|
||||
{ title: "건수", field: "WARRANTY1", width: 80, hozAlign: "right", headerHozAlign: "center", formatter: "money" },
|
||||
{ title: "발생비용", field: "COST1", width: 110, hozAlign: "right", headerHozAlign: "center", formatter: "money" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "무상",
|
||||
columns: [
|
||||
{ title: "건수", field: "WARRANTY2", width: 80, hozAlign: "right", headerHozAlign: "center", formatter: "money" },
|
||||
{ title: "발생비용", field: "COST2", width: 110, hozAlign: "right", headerHozAlign: "center", formatter: "money" },
|
||||
],
|
||||
},
|
||||
...categoryWrapper,
|
||||
];
|
||||
}, [dynColumns]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">CS관리_현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[110px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={productCd} onChange={setProductCd} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<SearchableSelect options={projectOpts} value={projectNo} onChange={setProjectNo} className="w-[220px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유/무상">
|
||||
<SearchableCodeSelect codeId="0000156" value={warranty} onChange={setWarranty} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="CS구분">
|
||||
<SearchableCodeSelect codeId="0000970" value={csCategory} onChange={setCsCategory} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유형">
|
||||
<SearchableSelect options={categoryHOpts} value={categoryH} onChange={setCategoryH} className="w-[150px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,576 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
PieChart, Pie, Cell, Tooltip, ResponsiveContainer, Legend,
|
||||
ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid,
|
||||
} from "recharts";
|
||||
import { useAuthStore } from "@/store/auth-store";
|
||||
import { useMenuStore } from "@/store/menu-store";
|
||||
import { numberWithCommas } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
FolderKanban, AlertCircle,
|
||||
TrendingUp, BarChart3, Briefcase,
|
||||
} from "lucide-react";
|
||||
|
||||
interface YearGoalRow {
|
||||
YEAR: string;
|
||||
CONTRACT_CNT_YEAR_IN?: number;
|
||||
CONTRACT_CNT_YEAR_OUT?: number;
|
||||
CONTRACT_CNT_YEAR_RATE?: number;
|
||||
CONTRACT_COST_YEAR?: string | number;
|
||||
PRICE?: string | number;
|
||||
GOAL_RATE?: number;
|
||||
YEAR_GOAL_OBJID?: string;
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
projectStats: {
|
||||
CNT_TOTAL?: number; CNT_NOPLAN?: number; CNT_ING?: number;
|
||||
CNT_DELAY?: number; CNT_END?: number;
|
||||
ISSUE_TOTAL?: number; ISSUE_MISS?: number;
|
||||
};
|
||||
productDist: { CODE: string; NAME: string; CNT: number }[];
|
||||
supplyDist: { CODE: string; NAME: string; CNT: number }[];
|
||||
monthlyContract: { MONTH: number; AMOUNT: string }[];
|
||||
projectList: Record<string, unknown>[];
|
||||
yearGoalInfo: YearGoalRow[];
|
||||
}
|
||||
|
||||
type Tab = "sales" | "project";
|
||||
|
||||
const PIE_COLORS = ["#3b82f6", "#22c55e", "#a855f7", "#f97316", "#ef4444", "#14b8a6", "#eab308"];
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAuthStore();
|
||||
const { topMenus, fetchSideMenus } = useMenuStore();
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [tab, setTab] = useState<Tab>("sales");
|
||||
|
||||
useEffect(() => {
|
||||
if (topMenus.length > 0) {
|
||||
const userMenu = topMenus.find((m) => m.MENU_NAME_KOR !== "관리자") || topMenus[0];
|
||||
fetchSideMenus(userMenu.OBJID);
|
||||
}
|
||||
}, [topMenus, fetchSideMenus]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/dashboard", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year }),
|
||||
})
|
||||
.then((r) => r.ok ? r.json() : null)
|
||||
.then((d) => setData(d))
|
||||
.catch(() => {});
|
||||
}, [year]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-120px)]">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between mb-6 shrink-0">
|
||||
<h1 className="text-xl font-bold text-gray-800">
|
||||
Dashboard
|
||||
<span className="text-sm font-normal text-gray-400 ml-2">
|
||||
{user?.userName}님, 환영합니다.
|
||||
</span>
|
||||
</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 탭 */}
|
||||
<div className="inline-flex bg-gray-100 rounded-lg p-1">
|
||||
<TabButton active={tab === "sales"} onClick={() => setTab("sales")} icon={TrendingUp} label="영업" />
|
||||
<TabButton active={tab === "project"} onClick={() => setTab("project")} icon={Briefcase} label="프로젝트" />
|
||||
</div>
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}년</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 내용 — 남은 공간 가득 채움 */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{tab === "sales" ? (
|
||||
<SalesTab data={data} year={year} />
|
||||
) : (
|
||||
<ProjectTab data={data} year={year} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButton({ active, onClick, icon: Icon, label }: {
|
||||
active: boolean; onClick: () => void; icon: React.ElementType; label: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-2 px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
active ? "bg-white text-primary shadow-sm" : "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
<Icon size={15} />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SalesTab({ data, year }: { data: DashboardData | null; year: string }) {
|
||||
const openGoalPopup = () => {
|
||||
const w = 700, h = 500;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(`/sales/year-goal?year=${year}`, "yearGoalPopup",
|
||||
`width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 h-full min-h-0">
|
||||
{/* 상단: 영업현황 표 (고정 높이) */}
|
||||
<YearGoalTable info={data?.yearGoalInfo || []} onOpenGoal={openGoalPopup} />
|
||||
|
||||
{/* 하단: 3분할 — 제품별 pie / 고객사별 pie / 년도별 combo (남은 공간 가득) */}
|
||||
<div className="grid grid-cols-3 gap-4 flex-1 min-h-0">
|
||||
<PieCard title="■ 제품별현황" data={data?.productDist || []} />
|
||||
<PieCard title="■ 고객사별현황" data={data?.supplyDist || []} />
|
||||
<YearSalesComboChart info={data?.yearGoalInfo || []} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function YearGoalTable({ info, onOpenGoal }: { info: YearGoalRow[]; onOpenGoal: () => void }) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4 shadow-sm shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<TrendingUp size={16} className="text-primary" />
|
||||
영업현황
|
||||
</h3>
|
||||
<Button size="sm" onClick={onOpenGoal}>영업목표 등록</Button>
|
||||
</div>
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-600 text-white">
|
||||
<th rowSpan={2} className="border border-gray-300 px-2 py-1 font-semibold">년도</th>
|
||||
<th colSpan={2} className="border border-gray-300 px-2 py-1 font-semibold">수주현황(건수)</th>
|
||||
<th rowSpan={2} className="border border-gray-300 px-2 py-1 font-semibold">수주율(%)</th>
|
||||
<th rowSpan={2} className="border border-gray-300 px-2 py-1 font-semibold">예상매출(억원)</th>
|
||||
<th rowSpan={2} className="border border-gray-300 px-2 py-1 font-semibold">영업목표(억원)</th>
|
||||
<th rowSpan={2} className="border border-gray-300 px-2 py-1 font-semibold">달성율(%)</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-600 text-white">
|
||||
<th className="border border-gray-300 px-2 py-1 font-semibold">국내</th>
|
||||
<th className="border border-gray-300 px-2 py-1 font-semibold">해외</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{info.length === 0 ? (
|
||||
<tr><td colSpan={7} className="text-center py-4 text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : info.map((row, idx) => (
|
||||
<tr key={idx} className="hover:bg-gray-50">
|
||||
<td className="border border-gray-200 px-2 py-1.5 text-center font-medium">{row.YEAR}</td>
|
||||
<td className="border border-gray-200 px-2 py-1.5 text-center">{row.CONTRACT_CNT_YEAR_IN ?? 0}</td>
|
||||
<td className="border border-gray-200 px-2 py-1.5 text-center">{row.CONTRACT_CNT_YEAR_OUT ?? 0}</td>
|
||||
<td className="border border-gray-200 px-2 py-1.5 text-center">{row.CONTRACT_CNT_YEAR_RATE ?? 0}</td>
|
||||
<td className="border border-gray-200 px-2 py-1.5 text-center">{numberWithCommas(Number(row.CONTRACT_COST_YEAR ?? 0))}</td>
|
||||
<td className="border border-gray-200 px-2 py-1.5 text-center">{numberWithCommas(Number(row.PRICE ?? 0))}</td>
|
||||
<td className="border border-gray-200 px-2 py-1.5 text-center">{row.GOAL_RATE ?? 0}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PieCard({ title, data }: { title: string; data: { CODE: string; NAME: string; CNT: number }[] }) {
|
||||
const chartData = data.map((d, i) => ({
|
||||
name: d.NAME || `코드 ${d.CODE}`,
|
||||
value: d.CNT,
|
||||
color: PIE_COLORS[i % PIE_COLORS.length],
|
||||
}));
|
||||
const total = chartData.reduce((s, d) => s + d.value, 0);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm flex flex-col min-h-0">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3 shrink-0">{title}</h3>
|
||||
{total === 0 ? (
|
||||
<div className="flex-1 flex items-center justify-center text-sm text-gray-400">데이터가 없습니다.</div>
|
||||
) : (
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius="75%"
|
||||
label={({ percent }: { percent?: number }) =>
|
||||
percent != null && percent >= 0.05 ? `${Math.round(percent * 100)}%` : ""
|
||||
}
|
||||
labelLine={false}
|
||||
>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(v) => `${v}건`} />
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
align="center"
|
||||
iconSize={10}
|
||||
wrapperStyle={{ fontSize: "11px" }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function YearSalesComboChart({ info }: { info: YearGoalRow[] }) {
|
||||
// 과거→현재 순서
|
||||
const chartData = [...info]
|
||||
.sort((a, b) => Number(a.YEAR) - Number(b.YEAR))
|
||||
.map((r) => ({
|
||||
YEAR: r.YEAR,
|
||||
영업목표: Number(r.PRICE || 0),
|
||||
수주금액: Number(r.CONTRACT_COST_YEAR || 0),
|
||||
달성율: Number(r.GOAL_RATE || 0),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm flex flex-col min-h-0">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3 shrink-0">■ 년도별 영업현황</h3>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f1f1" />
|
||||
<XAxis dataKey="YEAR" tick={{ fontSize: 11 }} />
|
||||
<YAxis yAxisId="left" tick={{ fontSize: 10 }} />
|
||||
<YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} unit="%" />
|
||||
<Tooltip
|
||||
formatter={(v, name) =>
|
||||
name === "달성율" ? `${v}%` : `${numberWithCommas(Number(v))}억`
|
||||
}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: "11px" }} iconSize={10} />
|
||||
<Bar yAxisId="left" dataKey="영업목표" fill="#3b82f6" barSize={20} />
|
||||
<Bar yAxisId="left" dataKey="수주금액" fill="#ef4444" barSize={20} />
|
||||
<Line yAxisId="right" type="monotone" dataKey="달성율" stroke="#f97316" strokeWidth={2}
|
||||
dot={{ r: 5, fill: "#f97316", stroke: "#fff", strokeWidth: 2 }} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type StatusFilter = "all" | "noplan" | "ing" | "delay" | "end";
|
||||
|
||||
const FILTER_LABELS: Record<StatusFilter, string> = {
|
||||
all: "전체",
|
||||
noplan: "계획미수립",
|
||||
ing: "진행중",
|
||||
delay: "지연",
|
||||
end: "종료",
|
||||
};
|
||||
|
||||
function ProjectTab({ data, year }: { data: DashboardData | null; year: string }) {
|
||||
const stats = data?.projectStats || {};
|
||||
const allProjects = (data?.projectList || []) as Record<string, unknown>[];
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
const [projectFilter, setProjectFilter] = useState<string>("");
|
||||
const [selectedIdx, setSelectedIdx] = useState(0);
|
||||
|
||||
const projectList = allProjects.filter((p) => {
|
||||
const s = String(p.STATUS_TITLE || "");
|
||||
if (statusFilter !== "all") {
|
||||
if (statusFilter === "noplan" && s !== "계획미수립") return false;
|
||||
if (statusFilter === "ing" && s !== "진행중") return false;
|
||||
if (statusFilter === "delay" && s !== "지연") return false;
|
||||
if (statusFilter === "end" && s !== "종료") return false;
|
||||
}
|
||||
if (projectFilter && String(p.OBJID) !== projectFilter) return false;
|
||||
return true;
|
||||
});
|
||||
const selected = projectList[selectedIdx] || projectList[0];
|
||||
|
||||
const toggleFilter = (f: StatusFilter) => {
|
||||
setSelectedIdx(0);
|
||||
setStatusFilter((cur) => (cur === f ? "all" : f));
|
||||
};
|
||||
|
||||
const openProjectSchedule = () => {
|
||||
// 프로젝트 일정 전체 보기 → 프로젝트 관리 > 종합현황 페이지로 이동
|
||||
window.location.href = `/project/total?year=${year}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full">
|
||||
{/* 상단 프로젝트현황 카드 — 원본 스타일 (5개 숫자 가로 + 컨트롤) */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 flex items-center gap-2">
|
||||
<FolderKanban size={16} className="text-primary" />
|
||||
프로젝트현황
|
||||
</h3>
|
||||
<Button size="sm" onClick={openProjectSchedule}>프로젝트 일정 전체 보기</Button>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* 좌측: 년도/프로젝트번호 셀렉트 */}
|
||||
<div className="flex flex-col gap-2 min-w-[240px] pr-4 border-r border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-gray-500 w-20">년도</label>
|
||||
<div className="flex-1 text-sm font-medium">{year}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-gray-500 w-20">프로젝트번호</label>
|
||||
<select value={projectFilter} onChange={(e) => setProjectFilter(e.target.value)}
|
||||
className="flex-1 h-8 rounded border border-gray-300 bg-white px-2 text-xs">
|
||||
<option value="">선택</option>
|
||||
{allProjects.map((p) => (
|
||||
<option key={String(p.OBJID)} value={String(p.OBJID)}>{String(p.PROJECT_NO || "")}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 우측: 5개 숫자 가로 배치 */}
|
||||
<div className="flex-1 grid grid-cols-5 gap-2">
|
||||
<CountBadge label="전체" value={Number(stats.CNT_TOTAL || 0)} color="blue"
|
||||
active={statusFilter === "all"} onClick={() => setStatusFilter("all")} />
|
||||
<CountBadge label="계획미수립" value={Number(stats.CNT_NOPLAN || 0)} color="blue"
|
||||
active={statusFilter === "noplan"} onClick={() => toggleFilter("noplan")} />
|
||||
<CountBadge label="진행중" value={Number(stats.CNT_ING || 0)} color="blue"
|
||||
active={statusFilter === "ing"} onClick={() => toggleFilter("ing")} />
|
||||
<CountBadge label="지연" value={Number(stats.CNT_DELAY || 0)} color="red"
|
||||
active={statusFilter === "delay"} onClick={() => toggleFilter("delay")} />
|
||||
<CountBadge label="종료" value={Number(stats.CNT_END || 0)} color="blue"
|
||||
active={statusFilter === "end"} onClick={() => toggleFilter("end")} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 프로젝트 리스트 — 전체 너비, 원본 10컬럼 구조 */}
|
||||
<div className="flex-1 bg-white rounded-xl border border-gray-200 p-4 flex flex-col min-h-0 shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3 shrink-0 flex items-center gap-2">
|
||||
<FolderKanban size={16} className="text-primary" />
|
||||
프로젝트 리스트 {statusFilter !== "all" && (
|
||||
<span className="text-xs font-normal text-primary">[{FILTER_LABELS[statusFilter]}]</span>
|
||||
)} · 총 {projectList.length}건
|
||||
{statusFilter !== "all" && (
|
||||
<button onClick={() => setStatusFilter("all")}
|
||||
className="ml-auto text-[10px] text-gray-400 hover:text-gray-700">
|
||||
필터 해제 ×
|
||||
</button>
|
||||
)}
|
||||
</h3>
|
||||
<div className="flex-1 overflow-auto min-h-0 border border-gray-100 rounded">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead className="sticky top-0 bg-gray-600 text-white z-10">
|
||||
<tr>
|
||||
<th className="px-2 py-2 font-semibold text-xs w-12">선택</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">고객사</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">제품구분</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">프로젝트번호</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">납기일</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">셋업지</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">제작공장</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">진척율(%)</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">셋업완료일</th>
|
||||
<th className="px-2 py-2 font-semibold text-xs">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projectList.length === 0 ? (
|
||||
<tr><td colSpan={10} className="text-center py-10 text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : projectList.map((pjt, idx) => {
|
||||
const statusTitle = String(pjt.STATUS_TITLE || "");
|
||||
const statusColor =
|
||||
statusTitle === "종료" ? "text-green-600" :
|
||||
statusTitle === "지연" ? "text-red-500" :
|
||||
statusTitle === "계획미수립" ? "text-gray-500" :
|
||||
"text-blue-600";
|
||||
const isSelected = idx === selectedIdx;
|
||||
return (
|
||||
<tr key={idx}
|
||||
className={`border-b border-gray-100 cursor-pointer ${isSelected ? "bg-primary/10" : "hover:bg-gray-50"}`}
|
||||
onClick={() => setSelectedIdx(idx)}>
|
||||
<td className="px-2 py-2 text-center">
|
||||
<input type="radio" checked={isSelected} onChange={() => setSelectedIdx(idx)} className="pointer-events-none" />
|
||||
</td>
|
||||
<td className="px-2 py-2 text-center text-xs">{String(pjt.CUSTOMER_NAME || "")}</td>
|
||||
<td className="px-2 py-2 text-center text-xs">{String(pjt.PRODUCT_NAME || "")}</td>
|
||||
<td className="px-2 py-2 text-center">
|
||||
<span className="inline-block px-2 py-0.5 bg-gray-700 text-white rounded text-[11px]">{String(pjt.PROJECT_NO || "")}</span>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-center text-xs">{String(pjt.CONTRACT_DEL_DATE || "-")}</td>
|
||||
<td className="px-2 py-2 text-center text-xs">{String(pjt.SETUP || "")}</td>
|
||||
<td className="px-2 py-2 text-center text-xs">{String(pjt.MANUFACTURE_PLANT_NAME || "")}</td>
|
||||
<td className="px-2 py-2 text-right text-xs">{Number(pjt.SETUP_RATE || 0).toFixed(1)}</td>
|
||||
<td className="px-2 py-2 text-center text-xs">{String(pjt.SETUP_DONE_DATE || "")}</td>
|
||||
<td className={`px-2 py-2 text-center text-xs font-semibold ${statusColor}`}>{statusTitle}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 선택된 프로젝트 상세 (이슈 + 투입원가) */}
|
||||
<div className="shrink-0">
|
||||
<ProjectDetailPanel project={selected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectDetailPanel({ project }: { project: Record<string, unknown> | undefined }) {
|
||||
if (!project) {
|
||||
return (
|
||||
<div className="flex-1 bg-white rounded-xl border border-gray-200 p-10 flex items-center justify-center shadow-sm">
|
||||
<div className="text-sm text-gray-400">프로젝트를 선택하세요</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const issueTotal = Number(project.ISSUE_CNT || 0);
|
||||
const issueDone = Number(project.ISSUE_DONE_CNT || 0);
|
||||
const issueMiss = Number(project.ISSUE_MISS_CNT || 0);
|
||||
const issueRate = issueTotal > 0 ? Math.round((issueDone / issueTotal) * 100) : 0;
|
||||
|
||||
// 투입원가 항목별 (원본 dashboard.jsp fn_getProjectCostStatusList 이식)
|
||||
const contractPrice = Number(project.CONTRACT_PRICE || 0);
|
||||
const materialGoal = Number(project.MATERIAL_COST_GOAL || 0);
|
||||
const materialActual = Number(project.ACCRUAL_MATERIAL_COST || 0);
|
||||
const laborGoal = Number(project.LABOR_COST_GOAL || 0);
|
||||
const laborActual = Number(project.LABOR_COST_ACTUAL || 0);
|
||||
const expenseGoal = Number(project.EXPENSE_COST_GOAL || 0);
|
||||
const expenseActual = Number(project.ACCRUAL_EXPENSE || 0);
|
||||
const totalGoalBase = materialGoal + laborGoal + expenseGoal;
|
||||
const totalActualBase = materialActual + laborActual + expenseActual;
|
||||
// 관리비 = 전체의 10%
|
||||
const mgmtGoal = Math.round(totalGoalBase * 0.1);
|
||||
const mgmtActual = Math.round(totalActualBase * 0.1);
|
||||
const totalGoal = totalGoalBase + mgmtGoal;
|
||||
const totalActual = totalActualBase + mgmtActual;
|
||||
// 각 항목 투입율(%) — 재료비는 수주가 기준, 나머지는 목표 기준 (원본 로직)
|
||||
const materialRate = contractPrice > 0 ? Math.round((materialActual / contractPrice) * 1000) / 10 : 0;
|
||||
const laborRate = laborGoal > 0 ? Math.round((laborActual / laborGoal) * 1000) / 10 : 0;
|
||||
const expenseRate = expenseGoal > 0 ? Math.round((expenseActual / expenseGoal) * 1000) / 10 : 0;
|
||||
const mgmtRate = mgmtGoal > 0 ? Math.round((mgmtActual / mgmtGoal) * 1000) / 10 : 0;
|
||||
const totalRateCost = totalGoal > 0 ? Math.round((totalActual / totalGoal) * 1000) / 10 : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 이슈 + 투입원가 2분할 (가로 배치) */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 이슈 (Quality) */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 flex flex-col shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-4 flex items-center gap-2 shrink-0">
|
||||
<AlertCircle size={16} className="text-red-500" />
|
||||
이슈 (Quality)
|
||||
</h3>
|
||||
<div className="flex-1 grid grid-cols-2 gap-3 content-center">
|
||||
<MiniStat label="발생" value={issueTotal} color="text-gray-800" />
|
||||
<MiniStat label="조치" value={issueDone} color="text-green-600" />
|
||||
<MiniStat label="미결" value={issueMiss} color="text-red-500" />
|
||||
<MiniStat label="조치율" value={`${issueRate}%`} color="text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 투입원가현황 — 원본 dashboard.jsp 5행 테이블 */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 flex flex-col shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-4 flex items-center gap-2 shrink-0">
|
||||
<BarChart3 size={16} className="text-primary" />
|
||||
투입원가현황
|
||||
</h3>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-600 text-white">
|
||||
<th className="border border-gray-300 px-2 py-1.5 font-semibold">수주가(원)</th>
|
||||
<th className="border border-gray-300 px-2 py-1.5 font-semibold">항목</th>
|
||||
<th className="border border-gray-300 px-2 py-1.5 font-semibold">목표원가(원)</th>
|
||||
<th className="border border-gray-300 px-2 py-1.5 font-semibold">투입원가(원)</th>
|
||||
<th className="border border-gray-300 px-2 py-1.5 font-semibold">투입율(%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-700">
|
||||
<tr>
|
||||
<td rowSpan={5} className="border border-gray-300 px-2 py-2 text-right align-middle">{numberWithCommas(contractPrice)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-center">재료비</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(materialGoal)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(materialActual)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{materialRate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-center">노무비</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(laborGoal)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(laborActual)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{laborRate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-center">경비</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(expenseGoal)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(expenseActual)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{expenseRate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-center">관리비</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(mgmtGoal)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{numberWithCommas(mgmtActual)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right">{mgmtRate}</td>
|
||||
</tr>
|
||||
<tr style={{ backgroundColor: "#efb3b3" }}>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-center font-semibold">계</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right font-semibold">{numberWithCommas(totalGoal)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right font-semibold">{numberWithCommas(totalActual)}</td>
|
||||
<td className="border border-gray-300 px-2 py-1.5 text-right font-semibold">{totalRateCost}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniStat({ label, value, color }: { label: string; value: number | string; color: string }) {
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-lg p-4 text-center">
|
||||
<div className="text-xs text-gray-500 mb-1">{label}</div>
|
||||
<div className={`text-2xl font-bold ${color}`}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CountBadge({ label, value, color, active, onClick }: {
|
||||
label: string; value: number; color: "blue" | "red";
|
||||
active?: boolean; onClick?: () => void;
|
||||
}) {
|
||||
const numColor = value > 0 ? (color === "red" ? "text-red-500" : "text-blue-600") : "text-gray-300";
|
||||
const bg = active ? (color === "red" ? "bg-red-50" : "bg-blue-50") : "hover:bg-gray-50";
|
||||
return (
|
||||
<button type="button" onClick={onClick}
|
||||
className={`flex flex-col items-center justify-center rounded-lg py-2 transition-colors ${bg}`}>
|
||||
<div className={`text-3xl font-bold ${numColor}`}>{numberWithCommas(value)}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">({label})</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: purchaseOrder/deliveryMngAcceptanceList.jsp
|
||||
// 입고관리 > 입고결과등록
|
||||
export default function AcceptancePage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState("");
|
||||
const [customerProjectName, setCustomerProjectName] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [purchaseOrderNo, setPurchaseOrderNo] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
const [searchPartSpec, setSearchPartSpec] = useState("");
|
||||
const [partnerObjid, setPartnerObjid] = useState("");
|
||||
const [salesMngUserId, setSalesMngUserId] = useState("");
|
||||
const [deliveryStartDate, setDeliveryStartDate] = useState("");
|
||||
const [deliveryEndDate, setDeliveryEndDate] = useState("");
|
||||
const [regStartDate, setRegStartDate] = useState("");
|
||||
const [regEndDate, setRegEndDate] = useState("");
|
||||
const [deliveryStatus, setDeliveryStatus] = useState("");
|
||||
const [searchPartName, setSearchPartName] = useState("");
|
||||
const [searchPartNo, setSearchPartNo] = useState("");
|
||||
const [poClientId, setPoClientId] = useState("");
|
||||
|
||||
const [supplyOptions, setSupplyOptions] = useState<Option[]>([]);
|
||||
const [userOptions, setUserOptions] = useState<Option[]>([]);
|
||||
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/supply", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setSupplyOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.SUPPLY_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
fetch("/api/admin/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setUserOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID),
|
||||
label: String(r.USER_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// 발주서 상세 팝업 열기 (발주번호 셀 클릭)
|
||||
const openOrderForm = (objId: string) => {
|
||||
const w = 1460;
|
||||
const h = 1050;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/order/list/form?objId=${objId}&action=view`,
|
||||
`orderForm_${objId}`,
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
// 입고결과 팝업 (view) — 입고결과 셀 클릭
|
||||
const openAcceptanceViewPopup = (objId: string, status: string) => {
|
||||
const w = 1560;
|
||||
const h = 1050;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/delivery/acceptance/form?objId=${objId}&delivery_status=${encodeURIComponent(status)}`,
|
||||
"deliveryAcceptancePopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 110, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "center" },
|
||||
{
|
||||
title: "발주번호",
|
||||
field: "PURCHASE_ORDER_NO",
|
||||
width: 100,
|
||||
hozAlign: "center",
|
||||
cellClick: (row) => openOrderForm(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "동시", field: "MULTI_YN_MAKED", width: 50, hozAlign: "center" },
|
||||
{ title: "발주서_제목", field: "TITLE", width: 150, hozAlign: "left" },
|
||||
{ title: "입고요청일", field: "DELIVERY_DATE", width: 85, hozAlign: "center" },
|
||||
{ title: "구매/제작업체명", field: "PARTNER_NAME", width: 120, hozAlign: "left" },
|
||||
{ title: "구매담당", field: "SALES_MNG_USER_NAME", width: 78, hozAlign: "center" },
|
||||
{ title: "발주일", field: "REGDATE", width: 78, hozAlign: "center" },
|
||||
{ title: "발주수량", field: "TOTAL_PO_QTY", width: 78, hozAlign: "right", formatter: "money" },
|
||||
{ title: "입고일", field: "CUR_DELIVERY_DATE", width: 78, hozAlign: "center" },
|
||||
{ title: "입고자", field: "CUR_RECEIVER_NAME", width: 70, hozAlign: "center" },
|
||||
{ title: "입고수량", field: "TOTAL_DELIVERY_QTY", width: 75, hozAlign: "right", formatter: "money" },
|
||||
{ title: "미입고수량", field: "NON_DELIVERY_QTY", width: 85, hozAlign: "right", formatter: "money" },
|
||||
{
|
||||
title: "입고결과",
|
||||
field: "DELIVERY_STATUS",
|
||||
width: 75,
|
||||
hozAlign: "center",
|
||||
cellClick: (row) =>
|
||||
openAcceptanceViewPopup(String(row.OBJID || ""), String(row.DELIVERY_STATUS || "")),
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/delivery/acceptance", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
customer_project_name: customerProjectName,
|
||||
project_nos: projectNo ? [projectNo] : [],
|
||||
unit_code: unitCode,
|
||||
purchase_order_no: purchaseOrderNo,
|
||||
type,
|
||||
SEARCH_PART_SPEC: searchPartSpec,
|
||||
partner_objid: partnerObjid,
|
||||
sales_mng_user_ids: salesMngUserId ? [salesMngUserId] : [],
|
||||
delivery_start_date: deliveryStartDate,
|
||||
delivery_end_date: deliveryEndDate,
|
||||
reg_start_date: regStartDate,
|
||||
reg_end_date: regEndDate,
|
||||
delivery_status: deliveryStatus,
|
||||
SEARCH_PART_NAME: searchPartName,
|
||||
SEARCH_PART_NO: searchPartNo,
|
||||
po_client_id: poClientId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
}, [
|
||||
year, customerProjectName, projectNo, unitCode, purchaseOrderNo, type,
|
||||
searchPartSpec, partnerObjid, salesMngUserId, deliveryStartDate, deliveryEndDate,
|
||||
regStartDate, regEndDate, deliveryStatus, searchPartName, searchPartNo, poClientId,
|
||||
]);
|
||||
|
||||
// 입고결과등록: 원본 가드 로직 동일
|
||||
// - 미선택: "선택된 데이터가 없습니다."
|
||||
// - 2건이상: "한건씩 등록 가능합니다."
|
||||
// - MULTI_YN='Y' AND MULTI_MASTER_YN='N': "동시발주건은 마스터건으로 수입검사해야 합니다."
|
||||
const handleAcceptanceRegister = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택된 데이터가 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
if (selectedRows.length > 1) {
|
||||
Swal.fire("알림", "한건씩 등록 가능합니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
const multiYn = String(row.MULTI_YN || "");
|
||||
const multiMasterYn = String(row.MULTI_MASTER_YN || "");
|
||||
if (multiYn === "Y" && multiMasterYn === "N") {
|
||||
Swal.fire("알림", "동시발주건은 마스터건으로 수입검사해야 합니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const w = 1560;
|
||||
const h = 1050;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/delivery/acceptance/form?objId=${row.OBJID}&action=regist`,
|
||||
"deliveryAcceptancePopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
// 부적합등록: 원본 가드 로직 동일
|
||||
// - 미선택/복수선택 체크 + MULTI_MASTER_YN='N': "동시발주건은 마스터건으로 부적합 등록해야 합니다."
|
||||
const handleDefectRegister = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택된 데이터가 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
if (selectedRows.length > 1) {
|
||||
Swal.fire("알림", "한건씩 등록 가능합니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
const multiMasterYn = String(row.MULTI_MASTER_YN || "");
|
||||
if (multiMasterYn === "N") {
|
||||
Swal.fire("알림", "동시발주건은 마스터건으로 부적합 등록해야 합니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const w = 1260;
|
||||
const h = 1050;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/delivery/defect/form?objId=${row.OBJID}`,
|
||||
"InvalidFormPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// 최초 1회만
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">입고관리_입고결과등록</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" onClick={handleAcceptanceRegister}>입고결과등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleDefectRegister}>부적합등록</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 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={customerProjectName} onChange={(e) => setCustomerProjectName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" placeholder="프로젝트 OBJID" />
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<Input value={unitCode} onChange={(e) => setUnitCode(e.target.value)} className="w-[130px]" placeholder="유닛 OBJID" />
|
||||
</SearchField>
|
||||
<SearchField label="발주No.">
|
||||
<Input value={purchaseOrderNo} onChange={(e) => setPurchaseOrderNo(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="부품구분">
|
||||
<SearchableCodeSelect codeId="0001068" value={type} onChange={setType} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="규격">
|
||||
<Input value={searchPartSpec} onChange={(e) => setSearchPartSpec(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="공급업체">
|
||||
<SearchableSelect options={supplyOptions} value={partnerObjid} onChange={setPartnerObjid} className="w-[170px]" />
|
||||
</SearchField>
|
||||
<SearchField label="구매담당자">
|
||||
<SearchableSelect options={userOptions} value={salesMngUserId} onChange={setSalesMngUserId} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="입고요청일">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
value={deliveryStartDate}
|
||||
onChange={(e) => setDeliveryStartDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={deliveryEndDate}
|
||||
onChange={(e) => setDeliveryEndDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="발주일">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
value={regStartDate}
|
||||
onChange={(e) => setRegStartDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={regEndDate}
|
||||
onChange={(e) => setRegEndDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="입고결과">
|
||||
<select
|
||||
value={deliveryStatus}
|
||||
onChange={(e) => setDeliveryStatus(e.target.value)}
|
||||
className="h-9 w-[110px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
<option value="입고중">입고중</option>
|
||||
<option value="입고완료">입고완료</option>
|
||||
<option value="지연">지연</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input value={searchPartName} onChange={(e) => setSearchPartName(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품번">
|
||||
<Input value={searchPartNo} onChange={(e) => setSearchPartNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="발주처">
|
||||
<SearchableSelect options={supplyOptions} value={poClientId} onChange={setPoClientId} className="w-[170px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 입고관리 > 부적합리스트
|
||||
export default function DefectPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [customerCd, setCustomerCd] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [partnerObjid, setPartnerObjid] = useState("");
|
||||
const [defectType, setDefectType] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "부적합번호", field: "DEFECT_NO", width: 130, hozAlign: "left",
|
||||
cellClick: (row) => {
|
||||
window.open(`/delivery/defect/form?objId=${row.OBJID}`, "defectForm", "width=1000,height=800");
|
||||
},
|
||||
},
|
||||
{ title: "발주No", field: "PURCHASE_ORDER_NO", width: 140, hozAlign: "left" },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" },
|
||||
{ title: "공급업체", field: "PARTNER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "Part No", field: "PART_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "품명", field: "PART_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "부적합유형", field: "DEFECT_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "부적합수량", field: "DEFECT_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "부적합내용", field: "DEFECT_CONTENT", width: 250, hozAlign: "left" },
|
||||
{ title: "처리상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "등록자", field: "REG_USER_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/delivery/defect", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
customer_cd: customerCd,
|
||||
project_no: projectNo,
|
||||
partner_objid: partnerObjid,
|
||||
defect_type: defectType,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, customerCd, projectNo, partnerObjid, defectType]);
|
||||
|
||||
const handleExcelDownload = () => {
|
||||
Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">부적합리스트</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcelDownload}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerCd} onChange={setCustomerCd} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} placeholder="프로젝트번호" className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<CodeSelect codeId="PARTNER" value={partnerObjid} onChange={setPartnerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="부적합유형">
|
||||
<CodeSelect codeId="DEFECT_TYPE" value={defectType} onChange={setDefectType} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 입고관리 > 단가관리
|
||||
export default function PricePage() {
|
||||
const [partnerObjid, setPartnerObjid] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "공급업체", field: "PARTNER_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "Part No", field: "PART_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "품명", field: "PART_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "규격", field: "SPEC", width: 150, hozAlign: "left" },
|
||||
{ title: "재질", field: "MATERIAL", width: 100, hozAlign: "left" },
|
||||
{ title: "단위", field: "UNIT_NAME", width: 60, hozAlign: "center" },
|
||||
{ title: "단가", field: "UNIT_PRICE", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "적용시작일", field: "START_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "적용종료일", field: "END_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "비고", field: "REMARK", width: 200, hozAlign: "left" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/delivery/price", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
partner_objid: partnerObjid,
|
||||
part_name: partName,
|
||||
part_no: partNo,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [partnerObjid, partName, partNo]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
const result = await Swal.fire({
|
||||
title: "삭제 확인",
|
||||
text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`,
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "삭제",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (result.isConfirmed) {
|
||||
Swal.fire("완료", "삭제되었습니다.", "success");
|
||||
fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleExcelDownload = () => {
|
||||
Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">단가관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => {/* TODO: open register form */}}>등록</Button>
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcelDownload}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="공급업체">
|
||||
<CodeSelect codeId="PARTNER" value={partnerObjid} onChange={setPartnerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="Part No">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} placeholder="Part No" className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input value={partName} onChange={(e) => setPartName(e.target.value)} placeholder="품명" className="w-[150px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
"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 { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: purchaseOrder/deliveryMngStatus.jsp
|
||||
// 입고관리 > 현황 — 프로젝트 BOM별 발주/입고/부적합 집계 리포트
|
||||
export default function DeliveryStatusPage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [projectNo, setProjectNo] = useState(""); // 단일 project objid
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const [customerOptions, setCustomerOptions] = useState<Option[]>([]);
|
||||
const [projectOptions, setProjectOptions] = useState<Option[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/common/supply-list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setCustomerOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.SUPPLY_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
fetch("/api/common/project-list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setProjectOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.LABEL || r.PROJECT_NO || r.CUSTOMER_PROJECT_NAME || r.OBJID),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// 구매BOM 팝업
|
||||
const openBomPopup = (bomReportObjId: string) => {
|
||||
if (!bomReportObjId) return;
|
||||
const w = 1600;
|
||||
const h = 900;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/bom/form?parentObjId=${bomReportObjId}&actType=view`,
|
||||
`bomReport_${bomReportObjId}`,
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
headerHozAlign: "center",
|
||||
frozen: true,
|
||||
columns: [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 110, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_PART_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "발주내역",
|
||||
headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "구매BOM",
|
||||
field: "TOTAL_BOM_PART_CNT",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
cellClick: (row) => openBomPopup(String(row.BOM_REPORT_OBJID || "")),
|
||||
formatter: (cell) => (Number(cell) > 0 ? "조회" : "-"),
|
||||
},
|
||||
{ title: "전체품목수", field: "TOTAL_BOM_PART_CNT", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발주품목수", field: "TOTAL_PO_PART_CNT", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발주율(%)", field: "PO_RATE", width: 90, hozAlign: "right" },
|
||||
{ title: "미발주품수", field: "NON_PO_PART_CNT", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "총수량", field: "TOTAL_BOM_PART_QTY_SUM", width: 90, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "입고현황",
|
||||
headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "발주수량(신)", field: "TOTAL_PO_NEW_QTY", width: 95, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발주수량(재)", field: "TOTAL_PO_RE_QTY", width: 95, hozAlign: "right", formatter: "money" },
|
||||
{ title: "입고수량", field: "DELIVERY_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "미입고수량", field: "NON_DELIVERY_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "수입검사결과(불량현황)",
|
||||
headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "부적합수량", field: "TOTAL_DEFECT_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "불량률(%)", field: "DELIVERY_RATE", width: 90, hozAlign: "right" },
|
||||
{ title: "설계오류", field: "DEFECT_QTY_1", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "제작불량", field: "DEFECT_QTY_2", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "구매오류", field: "DEFECT_QTY_3", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "오품반입", field: "DEFECT_QTY_4", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "손실비용", field: "TOTAL_DEFECT_PRICE", width: 90, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/delivery/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
customer_objid: customerObjid,
|
||||
project_nos: projectNo ? [projectNo] : [],
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
}, [year, customerObjid, projectNo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// 최초 1회만
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">입고관리_현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 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="고객사">
|
||||
<SearchableSelect
|
||||
options={customerOptions}
|
||||
value={customerObjid}
|
||||
onChange={setCustomerObjid}
|
||||
className="w-[180px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<SearchableSelect
|
||||
options={projectOptions}
|
||||
value={projectNo}
|
||||
onChange={setProjectNo}
|
||||
className="w-[240px]"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import { ApprovalButton } from "@/components/approval/ApprovalButton";
|
||||
|
||||
// fundMgmt/fundExpenseFormList.jsp 대응 - 경비신청서관리
|
||||
export default function FundExpenseFormPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [writerName, setWriterName] = useState("");
|
||||
const [statusCode, setStatusCode] = useState("");
|
||||
const [expenseDateFrom, setExpenseDateFrom] = useState("");
|
||||
const [expenseDateTo, setExpenseDateTo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "신청번호", field: "EXPENSE_FORM_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/fund/expense-form/form?objId=${row.OBJID}`, "expenseFormDetail", "width=1000,height=700") },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "신청자", field: "WRITER_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "신청일", field: "EXPENSE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "경비구분", field: "EXPENSE_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "신청금액", field: "EXPENSE_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "사용처", field: "EXPENSE_PLACE", width: 150, hozAlign: "left" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/fund/expense-form", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
writer_name: writerName,
|
||||
status_code: statusCode,
|
||||
expense_date_from: expenseDateFrom,
|
||||
expense_date_to: expenseDateTo,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, writerName, statusCode, expenseDateFrom, expenseDateTo]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">경비신청서관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => window.open("/fund/expense-form/form", "expenseFormNew", "width=1000,height=700")}>등록</Button>
|
||||
<ApprovalButton
|
||||
objIds={selectedRows.map((r) => String(r.OBJID))}
|
||||
targetType="EXPENSE_APPLY"
|
||||
title={`경비 결재 요청 (${selectedRows.length}건)`}
|
||||
description={selectedRows.map((r) => `${r.EXPENSE_ID} - ${r.BUS_TITLE}`).join("\n")}
|
||||
onSuccess={fetchData}
|
||||
disabled={selectedRows.length === 0}
|
||||
/>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="신청자">
|
||||
<Input value={writerName} onChange={(e) => setWriterName(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<CodeSelect codeId="EXPENSE_STATUS" value={statusCode} onChange={setStatusCode} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="신청일(From)">
|
||||
<Input type="date" value={expenseDateFrom} onChange={(e) => setExpenseDateFrom(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="신청일(To)">
|
||||
<Input type="date" value={expenseDateTo} onChange={(e) => setExpenseDateTo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} showCheckbox
|
||||
onSelectionChange={setSelectedRows} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// fundMgmt/fundInvoiceList.jsp 대응 - 거래명세서관리
|
||||
export default function FundInvoicePage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [customerName, setCustomerName] = useState("");
|
||||
const [invoiceDateFrom, setInvoiceDateFrom] = useState("");
|
||||
const [invoiceDateTo, setInvoiceDateTo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "거래명세서번호", field: "INVOICE_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/fund/invoice/form?objId=${row.OBJID}`, "invoiceDetail", "width=1000,height=700") },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "발행일", field: "INVOICE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "공급가액", field: "SUPPLY_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "세액", field: "TAX_AMOUNT", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "합계", field: "TOTAL_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/fund/invoice", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
customer_name: customerName,
|
||||
invoice_date_from: invoiceDateFrom,
|
||||
invoice_date_to: invoiceDateTo,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, customerName, invoiceDateFrom, invoiceDateTo]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">거래명세서관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => window.open("/fund/invoice/form", "invoiceForm", "width=1000,height=700")}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<Input value={customerName} onChange={(e) => setCustomerName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발행일(From)">
|
||||
<Input type="date" value={invoiceDateFrom} onChange={(e) => setInvoiceDateFrom(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발행일(To)">
|
||||
<Input type="date" value={invoiceDateTo} onChange={(e) => setInvoiceDateTo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 자재관리 > 자금관리
|
||||
export default function FundPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [month, setMonth] = useState("");
|
||||
const [partnerObjid, setPartnerObjid] = useState("");
|
||||
const [paymentStatus, setPaymentStatus] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "공급업체", field: "PARTNER_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "발주금액", field: "ORDER_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "입고금액", field: "DELIVERY_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "지급예정금액", field: "PAYMENT_PLAN_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "지급완료금액", field: "PAYMENT_DONE_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "미지급금액", field: "UNPAID_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "지급예정일", field: "PAYMENT_PLAN_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "지급상태", field: "PAYMENT_STATUS_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "비고", field: "REMARK", width: 200, hozAlign: "left" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/inventory/fund", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
month,
|
||||
partner_objid: partnerObjid,
|
||||
payment_status: paymentStatus,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, month, partnerObjid, paymentStatus]);
|
||||
|
||||
const handleExcelDownload = () => {
|
||||
Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">자금관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcelDownload}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="월">
|
||||
<select value={month} onChange={(e) => setMonth(e.target.value)}
|
||||
className="h-9 w-[80px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 12 }, (_, i) => i + 1).map((m) => (
|
||||
<option key={m} value={String(m).padStart(2, "0")}>{m}월</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<CodeSelect codeId="PARTNER" value={partnerObjid} onChange={setPartnerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="지급상태">
|
||||
<CodeSelect codeId="PAYMENT_STATUS" value={paymentStatus} onChange={setPaymentStatus} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { DataGrid, type GridColumn } from "@/components/grid/data-grid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// 재고 입출고 이력 팝업
|
||||
export default function InventoryHistoryPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const objId = searchParams.get("objId") || "";
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!objId) return;
|
||||
const res = await fetch("/api/inventory/history", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ parent_objid: objId }),
|
||||
});
|
||||
if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); }
|
||||
}, [objId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "유형", field: "TYPE", width: 80, hozAlign: "center" },
|
||||
{ title: "수량", field: "QTY", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "일자", field: "HIST_DATE", width: 110, hozAlign: "center" },
|
||||
{ title: "위치", field: "LOCATION_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "비고", field: "REMARK", hozAlign: "left" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-3">재고 입출고 이력</h2>
|
||||
<DataGrid columns={columns} data={data} />
|
||||
<div className="flex justify-end mt-3">
|
||||
<Button variant="secondary" onClick={() => window.close()}>닫기</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } 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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 자재관리 > 자재리스트 (원본 /inventoryMng/inventoryMngNewList.do)
|
||||
export default function InventoryListPage() {
|
||||
const [projectNos, setProjectNos] = useState<string[]>([]);
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [partType, setPartType] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
|
||||
const [projectOptions, setProjectOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
const [unitOptions, setUnitOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 프로젝트 옵션 로드
|
||||
useEffect(() => {
|
||||
fetch("/api/common/project-list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
const list = (j.RESULTLIST || []) as Array<Record<string, unknown>>;
|
||||
setProjectOptions(
|
||||
list.map((r) => ({
|
||||
value: String(r.OBJID || ""),
|
||||
label: String(r.LABEL || r.PROJECT_NO || ""),
|
||||
})),
|
||||
);
|
||||
})
|
||||
.catch(() => setProjectOptions([]));
|
||||
}, []);
|
||||
|
||||
// 프로젝트 선택 변경 시 유닛 로드 (단일/다중 모두 대응 — 첫 번째 프로젝트 기준)
|
||||
useEffect(() => {
|
||||
setUnitCode("");
|
||||
if (projectNos.length === 0) {
|
||||
setUnitOptions([]);
|
||||
return;
|
||||
}
|
||||
const first = projectNos[0];
|
||||
fetch("/api/common/unit-list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ contract_objid: first }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
const list = (j.RESULTLIST || []) as Array<Record<string, unknown>>;
|
||||
setUnitOptions(
|
||||
list.map((r) => ({
|
||||
value: String(r.OBJID || ""),
|
||||
label: String(r.UNIT_NAME || ""),
|
||||
})),
|
||||
);
|
||||
})
|
||||
.catch(() => setUnitOptions([]));
|
||||
}, [projectNos]);
|
||||
|
||||
const openHistoryPopup = useCallback((objId: string) => {
|
||||
const w = 730;
|
||||
const h = 400;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/inventory/request/history?objId=${encodeURIComponent(objId)}`,
|
||||
"inventoryRequestHistoryPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const columns: GridColumn[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "자재목록",
|
||||
headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 200, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "품번", field: "PART_NO", width: 180, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "품명", field: "PART_NAME", width: 180, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "재질", field: "MATERIAL", width: 180, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "사양(규격)", field: "SPEC", width: 200, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "PART구분", field: "PART_TYPE_NAME", width: 120, hozAlign: "center", headerHozAlign: "center" },
|
||||
{ title: "보유수량", field: "USE_CNT", width: 100, hozAlign: "center", headerHozAlign: "center", formatter: "money" },
|
||||
{ title: "보유수량(전체)", field: "USE_CNT_ALL", width: 120, hozAlign: "center", headerHozAlign: "center", formatter: "money" },
|
||||
{ title: "Location", field: "LOCATION_NAME", width: 120, hozAlign: "left", headerHozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "불출이력",
|
||||
headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "불출이력",
|
||||
field: "REQUEST_QTY",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
headerHozAlign: "center",
|
||||
formatter: (cell, row) => {
|
||||
const v = Number(cell || 0);
|
||||
if (v === 0) return "0";
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openHistoryPopup(String(row.OBJID || ""));
|
||||
}}
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
{v.toLocaleString()}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ title: "비고", field: "REMARK", width: 200, hozAlign: "left", headerHozAlign: "center" },
|
||||
],
|
||||
},
|
||||
],
|
||||
[openHistoryPopup],
|
||||
);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/inventory/list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
project_nos: projectNos.join(","),
|
||||
unit_code: unitCode,
|
||||
part_no: partNo,
|
||||
part_name: partName,
|
||||
part_type: partType,
|
||||
location,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectNos, unitCode, partNo, partName, partType, location]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
// 등록 팝업 (inventoryFormPopUp)
|
||||
const handleRegister = () => {
|
||||
const w = 850;
|
||||
const h = 500;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
"/inventory/list/form",
|
||||
"inventoryFormPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
// 자재이동 (materialMoveFormPopUp)
|
||||
const handleMove = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택된 데이터가 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
for (const r of selectedRows) {
|
||||
if (Number(r.USE_CNT || 0) === 0) {
|
||||
alert("보유수량이 0일 경우 이동이 불가능합니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const checkArr = selectedRows.map((r) => String(r.OBJID)).join(",");
|
||||
const w = 1600;
|
||||
const h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/inventory/move/form?checkArr=${encodeURIComponent(checkArr)}`,
|
||||
"materialMoveFormPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
// 불출의뢰 (materialRequestFormPopUp)
|
||||
const handleRequest = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택된 데이터가 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
for (const r of selectedRows) {
|
||||
if (Number(r.USE_CNT || 0) === 0) {
|
||||
alert("보유수량이 0일 경우 불출의뢰가 불가능합니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const checkArr = selectedRows.map((r) => String(r.OBJID)).join(",");
|
||||
const w = 1800;
|
||||
const h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/inventory/request/form?checkArr=${encodeURIComponent(checkArr)}`,
|
||||
"inventoryRequestPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">자재관리_자재리스트</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleRegister}>
|
||||
등록
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleMove}>
|
||||
자재이동
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleRequest}>
|
||||
불출의뢰
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="프로젝트번호">
|
||||
<MultiSelect
|
||||
options={projectOptions}
|
||||
value={projectNos}
|
||||
onChange={setProjectNos}
|
||||
placeholder="선택"
|
||||
className="w-[300px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<SearchableSelect
|
||||
options={unitOptions}
|
||||
value={unitCode}
|
||||
onChange={setUnitCode}
|
||||
placeholder="선택"
|
||||
className="w-[230px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="품번">
|
||||
<Input
|
||||
value={partNo}
|
||||
onChange={(e) => setPartNo(e.target.value)}
|
||||
placeholder="품번"
|
||||
className="w-[150px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input
|
||||
value={partName}
|
||||
onChange={(e) => setPartName(e.target.value)}
|
||||
placeholder="품명"
|
||||
className="w-[170px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="PART 구분">
|
||||
<SearchableCodeSelect
|
||||
codeId="0000062"
|
||||
value={partType}
|
||||
onChange={setPartType}
|
||||
className="w-[120px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="Location">
|
||||
<SearchableCodeSelect
|
||||
codeId="0000262"
|
||||
value={location}
|
||||
onChange={setLocation}
|
||||
className="w-[230px]"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
loading={loading}
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 간단한 다중 선택 — 선택된 라벨을 태그로 표시 + 드롭다운
|
||||
function MultiSelect({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
}: {
|
||||
options: { value: string; label: string }[];
|
||||
value: string[];
|
||||
onChange: (v: string[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = options.filter((o) => {
|
||||
if (!search) return true;
|
||||
const s = search.toLowerCase();
|
||||
return o.label.toLowerCase().includes(s) || o.value.toLowerCase().includes(s);
|
||||
});
|
||||
|
||||
const toggle = (v: string) => {
|
||||
if (value.includes(v)) onChange(value.filter((x) => x !== v));
|
||||
else onChange([...value, v]);
|
||||
};
|
||||
|
||||
const selectedLabels = value
|
||||
.map((v) => options.find((o) => o.value === v)?.label || v)
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<div className={`relative ${className || ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((p) => !p)}
|
||||
className="h-9 w-full text-left rounded border border-gray-300 bg-white px-2 text-sm truncate pr-6"
|
||||
title={selectedLabels}
|
||||
>
|
||||
{selectedLabels || <span className="text-gray-400">{placeholder || "선택"}</span>}
|
||||
</button>
|
||||
{value.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange([])}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 text-sm"
|
||||
tabIndex={-1}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
{open && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
||||
<div className="absolute z-50 mt-1 w-full bg-white border border-gray-300 rounded shadow max-h-64 overflow-y-auto">
|
||||
<div className="sticky top-0 bg-white border-b p-1">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="검색"
|
||||
className="w-full px-2 py-1 text-sm border rounded"
|
||||
/>
|
||||
</div>
|
||||
{filtered.length === 0 ? (
|
||||
<div className="p-2 text-xs text-gray-400 text-center">결과 없음</div>
|
||||
) : (
|
||||
filtered.map((o) => {
|
||||
const selected = value.includes(o.value);
|
||||
return (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => toggle(o.value)}
|
||||
className={`w-full text-left px-2 py-1.5 text-sm hover:bg-blue-50 flex items-center gap-2 ${
|
||||
selected ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
readOnly
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span className="truncate">{o.label}</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// inventoryMngInputList.jsp 대응 - 재고관리(입고)
|
||||
export default function InventoryPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [spec, setSpec] = useState("");
|
||||
const [clsCd, setClsCd] = useState("");
|
||||
const [cauCd, setCauCd] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// inventoryMngInputList.jsp columns 대응
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", hozAlign: "left" },
|
||||
{ title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left" },
|
||||
{ title: "파트명", field: "PART_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "규격", field: "SPEC", width: 110, hozAlign: "left" },
|
||||
{ title: "업체", field: "MAKER", width: 100, hozAlign: "left" },
|
||||
{ title: "재고구분", field: "CLS_CD_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "발생사유", field: "CAU_CD_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "발생수량", field: "QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "위치", field: "LOCATION_NAME", width: 100, hozAlign: "left" },
|
||||
{ title: "금액(원)", field: "PRICE", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "등록일", field: "REG_DATE", width: 90, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "총입고수량", field: "INPUT_QTY", width: 90, hozAlign: "center",
|
||||
cellClick: (row) => openHistoryPopup(String(row.OBJID)) },
|
||||
{ title: "최종입고일", field: "INPUT_DATE", width: 90, hozAlign: "center" },
|
||||
{ title: "잔여수량", field: "REMAIN_QTY", width: 90, hozAlign: "center",
|
||||
cellClick: (row) => openHistoryPopup(String(row.OBJID)) },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/inventory", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year, project_no: projectNo, part_no: partNo, part_name: partName,
|
||||
spec, cls_cd: clsCd, cau_cd: cauCd, location,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
setTotalCount(json.TOTAL_CNT || 0);
|
||||
}
|
||||
}, [year, projectNo, partNo, partName, spec, clsCd, cauCd, location]);
|
||||
|
||||
const openHistoryPopup = (objId: string) => {
|
||||
window.open(`/inventory/history?objId=${objId}`, "inventoryHistory", "width=600,height=500");
|
||||
};
|
||||
|
||||
const openInputPopup = () => {
|
||||
window.open("/inventory/input-form", "inventoryInput", "width=850,height=330");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">재고관리 (입고)</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={openInputPopup}>입고등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="파트번호">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="파트명">
|
||||
<Input value={partName} onChange={(e) => setPartName(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="규격">
|
||||
<Input value={spec} onChange={(e) => setSpec(e.target.value)} className="w-[100px]" />
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="재고구분">
|
||||
<CodeSelect codeId="0001576" value={clsCd} onChange={setClsCd} placeholder="전체" className="w-[120px]" />
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="위치">
|
||||
<CodeSelect codeId="0000262" value={location} onChange={setLocation} placeholder="전체" className="w-[120px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { numberWithCommas } from "@/lib/utils";
|
||||
|
||||
// 입출고 History 팝업 (원본 /inventoryMng/inventoryRequestHistoryPopUp.do)
|
||||
function HistoryPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const objId = searchParams.get("objId") || searchParams.get("partId") || "";
|
||||
const [rows, setRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!objId) return;
|
||||
const res = await fetch("/api/inventory/request/history", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setRows(json.RESULTLIST || []);
|
||||
}
|
||||
}, [objId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const openTarget = (id: string, gubun: string) => {
|
||||
if (gubun === "출고") {
|
||||
const w = 1500;
|
||||
const h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
const params = new URLSearchParams({
|
||||
INVENTORY_REQUEST_MASTER_OBJID: id,
|
||||
action: "view",
|
||||
});
|
||||
window.open(
|
||||
`/inventory/request/detail?${params.toString()}`,
|
||||
"inventoryRequestPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
} else if (gubun === "입고") {
|
||||
const w = 1260;
|
||||
const h = 1050;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/delivery/acceptance/form?objId=${encodeURIComponent(id)}&actionType=view`,
|
||||
"deliveryAcceptancePopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-white min-h-screen">
|
||||
<div className="mb-2">
|
||||
<h2 className="text-base font-bold">입출고 이력</h2>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-300 overflow-x-auto max-h-[calc(100vh-120px)]">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-100 text-center sticky top-0">
|
||||
<tr>
|
||||
<th className="border px-2 py-1.5">프로젝트번호</th>
|
||||
<th className="border px-2 py-1.5">품번</th>
|
||||
<th className="border px-2 py-1.5">품명</th>
|
||||
<th className="border px-2 py-1.5">구분</th>
|
||||
<th className="border px-2 py-1.5">입출고수량</th>
|
||||
<th className="border px-2 py-1.5">Location</th>
|
||||
<th className="border px-2 py-1.5">Sub_Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center py-4 text-gray-400">
|
||||
조회된 데이터가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
rows.map((r, i) => {
|
||||
const gubun = String(r.GUBUN || "");
|
||||
const rowObjId = String(r.OBJID || "");
|
||||
return (
|
||||
<tr key={i} className="border-b">
|
||||
<td className="border px-2 py-1">{String(r.PROJECT_NO || "")}</td>
|
||||
<td className="border px-2 py-1">{String(r.PART_NO || "")}</td>
|
||||
<td className="border px-2 py-1">{String(r.PART_NAME || "")}</td>
|
||||
<td className="border px-2 py-1 text-center">
|
||||
{gubun === "입고" || gubun === "출고" ? (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openTarget(rowObjId, gubun);
|
||||
}}
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
{gubun}
|
||||
</a>
|
||||
) : (
|
||||
<span>{gubun}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="border px-2 py-1 text-right">
|
||||
{numberWithCommas(String(r.RECEIPT_QTY || ""))}
|
||||
</td>
|
||||
<td className="border px-2 py-1">{String(r.LOCATION_NAME || "")}</td>
|
||||
<td className="border px-2 py-1">{String(r.SUB_LOCATION_NAME || "")}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-3 pt-2 border-t">
|
||||
<Button variant="secondary" onClick={() => window.close()}>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<div className="p-8 text-center text-gray-400">로딩 중...</div>}>
|
||||
<HistoryPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } 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 { ExcelDownloadButton } from "@/components/ui/excel-download-button";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 자재관리 > 불출의뢰서 (원본 /inventoryMng/materialRequestList.do)
|
||||
export default function InventoryRequestPage() {
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [requestStartDate, setRequestStartDate] = useState("");
|
||||
const [requestEndDate, setRequestEndDate] = useState("");
|
||||
const [requestUser, setRequestUser] = useState("");
|
||||
const [receptionStatus, setReceptionStatus] = useState("");
|
||||
const [receptionUser, setReceptionUser] = useState("");
|
||||
const [receptionStartDate, setReceptionStartDate] = useState("");
|
||||
const [receptionEndDate, setReceptionEndDate] = useState("");
|
||||
const [outStatus, setOutStatus] = useState("");
|
||||
|
||||
const [userOptions, setUserOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
const list = (j.RESULTLIST || []) as Array<Record<string, unknown>>;
|
||||
setUserOptions(
|
||||
list.map((r) => ({
|
||||
value: String(r.USER_ID || ""),
|
||||
label: String(r.USER_NAME || r.USER_ID || ""),
|
||||
})),
|
||||
);
|
||||
})
|
||||
.catch(() => setUserOptions([]));
|
||||
}, []);
|
||||
|
||||
const openDetailPopup = useCallback(
|
||||
(objId: string, outStatusTitle: string, receptionStatusTitle: string) => {
|
||||
const w = 1800;
|
||||
const h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
const params = new URLSearchParams({
|
||||
INVENTORY_REQUEST_MASTER_OBJID: objId,
|
||||
action: "view",
|
||||
OUTSTATUS: outStatusTitle,
|
||||
RECEPTION_STATUS: receptionStatusTitle,
|
||||
});
|
||||
window.open(
|
||||
`/inventory/request/detail?${params.toString()}`,
|
||||
"inventoryRequestPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const columns: GridColumn[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "자재불출번호",
|
||||
field: "INVENTORY_OUT_NO",
|
||||
width: 140,
|
||||
hozAlign: "left",
|
||||
headerHozAlign: "center",
|
||||
formatter: (cell, row) => (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openDetailPopup(
|
||||
String(row.OBJID || ""),
|
||||
String(row.OUTSTATUS_TITLE || ""),
|
||||
String(row.RECEPTION_STATUS_TITLE || ""),
|
||||
);
|
||||
}}
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
{String(cell || "")}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{ title: "품번", field: "PART_NO_ARR", width: 480, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "품명", field: "PART_NAME_ARR", width: 480, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "불출의뢰일", field: "REQUEST_DATE", width: 120, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "의뢰자", field: "REQUEST_USER_NAME", width: 100, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "상태", field: "RECEPTION_STATUS_TITLE", width: 100, hozAlign: "left", headerHozAlign: "center" },
|
||||
{ title: "접수자", field: "RECEPTION_USER_NAME", width: 100, hozAlign: "center", headerHozAlign: "center" },
|
||||
{ title: "접수일", field: "RECEPTION_DATE", width: 100, hozAlign: "center", headerHozAlign: "center" },
|
||||
{ title: "불출상태", field: "OUTSTATUS_TITLE", width: 100, hozAlign: "center", headerHozAlign: "center" },
|
||||
],
|
||||
[openDetailPopup],
|
||||
);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/inventory/request", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
part_no: partNo,
|
||||
part_name: partName,
|
||||
request_start_date: requestStartDate,
|
||||
request_end_date: requestEndDate,
|
||||
request_user: requestUser,
|
||||
reception_status: receptionStatus,
|
||||
reception_user: receptionUser,
|
||||
reception_start_date: receptionStartDate,
|
||||
reception_end_date: receptionEndDate,
|
||||
out_status: outStatus,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
partNo,
|
||||
partName,
|
||||
requestStartDate,
|
||||
requestEndDate,
|
||||
requestUser,
|
||||
receptionStatus,
|
||||
receptionUser,
|
||||
receptionStartDate,
|
||||
receptionEndDate,
|
||||
outStatus,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
// 접수 (미접수만 가능)
|
||||
const handleReceipt = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("선택된 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
const checkedArr = selectedRows
|
||||
.filter((r) => String(r.RECEPTION_STATUS_TITLE) === "미접수")
|
||||
.map((r) => String(r.OBJID));
|
||||
if (checkedArr.length === 0) {
|
||||
Swal.fire("선택된 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
const confirmed = await Swal.fire({
|
||||
title: "선택된 데이터를 접수하시겠습니까?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "확인",
|
||||
cancelButtonText: "취소",
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
});
|
||||
if (!confirmed.isConfirmed) return;
|
||||
|
||||
const res = await fetch("/api/inventory/request/receipt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ checkArr: checkedArr.join(",") }),
|
||||
});
|
||||
const data = await res.json();
|
||||
Swal.fire(data.message || "접수되었습니다.");
|
||||
if (data.success) fetchData();
|
||||
};
|
||||
|
||||
// 자재불출 (단일 선택, 접수 상태만)
|
||||
const handleAccept = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("선택된 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (selectedRows.length > 1) {
|
||||
Swal.fire("한번에 1개의 내용만 불출가능합니다.");
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
const receptionStatusTitle = String(row.RECEPTION_STATUS_TITLE || "");
|
||||
const outStatusTitle = String(row.OUTSTATUS_TITLE || "");
|
||||
|
||||
if (receptionStatusTitle !== "접수") {
|
||||
Swal.fire("접수한 데이터만 불출가능합니다.");
|
||||
return;
|
||||
}
|
||||
if (outStatusTitle === "완료") {
|
||||
Swal.fire("불출완료된 데이터는 불출가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const w = 1800;
|
||||
const h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
const params = new URLSearchParams({
|
||||
INVENTORY_REQUEST_MASTER_OBJID: String(row.OBJID || ""),
|
||||
OUTSTATUS: outStatusTitle,
|
||||
RECEPTION_STATUS: receptionStatusTitle,
|
||||
});
|
||||
window.open(
|
||||
`/inventory/request/detail?${params.toString()}`,
|
||||
"inventoryRequestPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">자재관리_불출의뢰서</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleAccept}>
|
||||
자재불출
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleReceipt}>
|
||||
접수
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>
|
||||
조회
|
||||
</Button>
|
||||
<ExcelDownloadButton data={data} columns={columns} filename="자재관리_불출의뢰서" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="품번">
|
||||
<Input
|
||||
value={partNo}
|
||||
onChange={(e) => setPartNo(e.target.value)}
|
||||
placeholder="품번"
|
||||
className="w-[150px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input
|
||||
value={partName}
|
||||
onChange={(e) => setPartName(e.target.value)}
|
||||
placeholder="품명"
|
||||
className="w-[150px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="불출의뢰일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="date"
|
||||
value={requestStartDate}
|
||||
onChange={(e) => setRequestStartDate(e.target.value)}
|
||||
className="w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<Input
|
||||
type="date"
|
||||
value={requestEndDate}
|
||||
onChange={(e) => setRequestEndDate(e.target.value)}
|
||||
className="w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="의뢰자">
|
||||
<SearchableSelect
|
||||
options={userOptions}
|
||||
value={requestUser}
|
||||
onChange={setRequestUser}
|
||||
className="w-[130px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="접수상태">
|
||||
<select
|
||||
value={receptionStatus}
|
||||
onChange={(e) => setReceptionStatus(e.target.value)}
|
||||
className="h-9 w-[130px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
<option value="reception">접수</option>
|
||||
<option value="AA">미접수</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="접수자">
|
||||
<SearchableSelect
|
||||
options={userOptions}
|
||||
value={receptionUser}
|
||||
onChange={setReceptionUser}
|
||||
className="w-[130px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="접수일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="date"
|
||||
value={receptionStartDate}
|
||||
onChange={(e) => setReceptionStartDate(e.target.value)}
|
||||
className="w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<Input
|
||||
type="date"
|
||||
value={receptionEndDate}
|
||||
onChange={(e) => setReceptionEndDate(e.target.value)}
|
||||
className="w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="불출상태">
|
||||
<select
|
||||
value={outStatus}
|
||||
onChange={(e) => setOutStatus(e.target.value)}
|
||||
className="h-9 w-[130px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
<option value="complete">완료</option>
|
||||
<option value="NG">미완료</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
loading={loading}
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 자재관리 > 현황
|
||||
export default function InventoryStatusPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [locationCd, setLocationCd] = useState("");
|
||||
const [partTypeCd, setPartTypeCd] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 120, hozAlign: "center" },
|
||||
{ title: "부품구분", field: "PART_TYPE_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "위치", field: "LOCATION_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "총보유수량", field: "TOTAL_QTY", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "불출수량", field: "REQUEST_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "잔여수량", field: "REMAIN_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "입고수량", field: "DELIVERY_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "이동수량", field: "MOVE_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/inventory/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
project_no: projectNo,
|
||||
location_cd: locationCd,
|
||||
part_type_cd: partTypeCd,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, locationCd, partTypeCd]);
|
||||
|
||||
const handleExcelDownload = () => {
|
||||
Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">자재현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcelDownload}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} placeholder="프로젝트번호" className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="위치">
|
||||
<CodeSelect codeId="LOCATION" value={locationCd} onChange={setLocationCd} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="부품구분">
|
||||
<CodeSelect codeId="PART_TYPE" value={partTypeCd} onChange={setPartTypeCd} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
"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 { numberWithCommas } from "@/lib/utils";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: /purchaseOrder/purchaseOrderStatusAmountBySupply.do
|
||||
// 발주관리 > 업체별_입고요청월 금액현황
|
||||
export default function AmountStatusPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [partnerObjid, setPartnerObjid] = useState("");
|
||||
const [salesMngUserId, setSalesMngUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [sums, setSums] = useState<Record<string, unknown>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [supplyOptions, setSupplyOptions] = useState<Option[]>([]);
|
||||
const [userOptions, setUserOptions] = useState<Option[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setSupplyOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.SUPPLY_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setUserOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID),
|
||||
label: String(r.USER_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "공급업체", field: "SUPPLY_NAME", width: 180, hozAlign: "left", frozen: true },
|
||||
{ title: "1월", field: "M01", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "2월", field: "M02", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "3월", field: "M03", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "4월", field: "M04", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "5월", field: "M05", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "6월", field: "M06", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "7월", field: "M07", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "8월", field: "M08", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "9월", field: "M09", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "10월", field: "M10", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "11월", field: "M11", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "12월", field: "M12", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "합계", field: "TOTAL_SUPPLY_UNIT_PRICE", width: 140, hozAlign: "right", formatter: "money" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/order/amount-status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
project_no: projectNo,
|
||||
partner_objid: partnerObjid,
|
||||
sales_mng_user_id: salesMngUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
setSums(json.SUMS || {});
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [year, projectNo, partnerObjid, salesMngUserId]);
|
||||
|
||||
const handleReset = () => {
|
||||
setYear(new Date().getFullYear().toString());
|
||||
setProjectNo("");
|
||||
setPartnerObjid("");
|
||||
setSalesMngUserId("");
|
||||
};
|
||||
|
||||
const handleExcelDownload = () => {
|
||||
Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">업체별 입고요청월 금액현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleReset}>초기화</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcelDownload}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 6 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input
|
||||
value={projectNo}
|
||||
onChange={(e) => setProjectNo(e.target.value)}
|
||||
placeholder="프로젝트번호 부분 일치"
|
||||
className="w-[180px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<SearchableSelect
|
||||
options={supplyOptions}
|
||||
value={partnerObjid}
|
||||
onChange={setPartnerObjid}
|
||||
className="w-[180px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="구매담당">
|
||||
<SearchableSelect
|
||||
options={userOptions}
|
||||
value={salesMngUserId}
|
||||
onChange={setSalesMngUserId}
|
||||
className="w-[140px]"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="flex items-center gap-6 mb-3 text-sm font-bold text-red-600">
|
||||
<span>총 합계(원) : {numberWithCommas(Number(sums.TOTAL_SUPPLY_UNIT_PRICE ?? 0))}</span>
|
||||
</div>
|
||||
|
||||
<DataGrid columns={columns} data={data} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
import { ApprovalButton } from "@/components/approval/ApprovalButton";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: /purchaseOrder/purchaseOrderList_new.do
|
||||
export default function OrderListPage() {
|
||||
const [year, setYear] = useState("");
|
||||
const [customerCd, setCustomerCd] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [purchaseOrderNo, setPurchaseOrderNo] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
const [orderTypeCd, setOrderTypeCd] = useState("");
|
||||
const [deliveryStartDate, setDeliveryStartDate] = useState("");
|
||||
const [deliveryEndDate, setDeliveryEndDate] = useState("");
|
||||
const [partnerObjid, setPartnerObjid] = useState("");
|
||||
const [salesMngUserId, setSalesMngUserId] = useState("");
|
||||
const [regStartDate, setRegStartDate] = useState("");
|
||||
const [regEndDate, setRegEndDate] = useState("");
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [poClientId, setPoClientId] = useState("");
|
||||
const [apprStatus, setApprStatus] = useState("");
|
||||
const [partSpec, setPartSpec] = useState("");
|
||||
|
||||
const [customerOptions, setCustomerOptions] = useState<Option[]>([]);
|
||||
const [supplyOptions, setSupplyOptions] = useState<Option[]>([]);
|
||||
const [userOptions, setUserOptions] = useState<Option[]>([]);
|
||||
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/common/supply-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setCustomerOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.SUPPLY_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setSupplyOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.SUPPLY_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setUserOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID),
|
||||
label: String(r.USER_NAME),
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const openOrderForm = (objId?: string) => {
|
||||
const url = objId ? `/order/list/form?objId=${objId}&action=view` : "/order/list/form";
|
||||
const w = 1460;
|
||||
const h = 1050;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(url, `orderForm_${objId || "new"}`, `width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "발주No",
|
||||
field: "PURCHASE_ORDER_NO",
|
||||
width: 130,
|
||||
hozAlign: "left",
|
||||
frozen: true,
|
||||
cellClick: (row) => openOrderForm(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "년도", field: "CM_YEAR", width: 60, hozAlign: "center" },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 130, hozAlign: "left" },
|
||||
{ title: "고객사프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "당사프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" },
|
||||
{ title: "부품", field: "TYPE_NAME", width: 70, hozAlign: "center" },
|
||||
{ title: "구분", field: "ORDER_TYPE_CD_NAME", width: 70, hozAlign: "center" },
|
||||
{ title: "동시", field: "MULTI_YN_MAKED", width: 50, hozAlign: "center" },
|
||||
{ title: "발주서제목", field: "TITLE", width: 220, hozAlign: "left" },
|
||||
{ title: "납품장소", field: "DELIVERY_PLACE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "검수방법", field: "INSPECT_METHOD_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "결재조건", field: "PAYMENT_TERMS_NAME", width: 110, hozAlign: "center" },
|
||||
{ title: "입고요청일", field: "DELIVERY_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "공급업체", field: "PARTNER_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "구매담당", field: "SALES_MNG_USER_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "발주일", field: "REGDATE", width: 100, hozAlign: "center" },
|
||||
{ title: "발주금액", field: "TOTAL_PRICE_ALL", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "상태", field: "APPR_STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/order/list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
customer_cd: customerCd,
|
||||
project_no: projectNo,
|
||||
unit_code: unitCode,
|
||||
purchase_order_no: purchaseOrderNo,
|
||||
type,
|
||||
order_type_cd: orderTypeCd,
|
||||
delivery_start_date: deliveryStartDate,
|
||||
delivery_end_date: deliveryEndDate,
|
||||
partner_objid: partnerObjid,
|
||||
sales_mng_user_id: salesMngUserId,
|
||||
reg_start_date: regStartDate,
|
||||
reg_end_date: regEndDate,
|
||||
part_no: partNo,
|
||||
part_name: partName,
|
||||
po_client_id: poClientId,
|
||||
appr_status: apprStatus,
|
||||
part_spec: partSpec,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
}, [
|
||||
year, customerCd, projectNo, unitCode, purchaseOrderNo, type, orderTypeCd,
|
||||
deliveryStartDate, deliveryEndDate, partnerObjid, salesMngUserId,
|
||||
regStartDate, regEndDate, partNo, partName, poClientId, apprStatus, partSpec,
|
||||
]);
|
||||
|
||||
const masterSelectedIds = selectedRows
|
||||
.filter((r) => r.MULTI_YN !== "Y" || r.MULTI_MASTER_YN === "Y")
|
||||
.map((r) => String(r.OBJID));
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// 최초 1회만
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">발주서관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => openOrderForm()}>발주서작성</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<ApprovalButton
|
||||
objIds={masterSelectedIds}
|
||||
targetType="PURCHASE_ORDER"
|
||||
title={`발주 결재 요청 (${masterSelectedIds.length}건)`}
|
||||
description={selectedRows.map((r) => `${r.PURCHASE_ORDER_NO} - ${r.TITLE}`).join("\n")}
|
||||
onSuccess={fetchData}
|
||||
disabled={masterSelectedIds.length === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[90px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 6 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<SearchableSelect options={customerOptions} value={customerCd} onChange={setCustomerCd} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유닛">
|
||||
<Input value={unitCode} onChange={(e) => setUnitCode(e.target.value)} className="w-[130px]" placeholder="유닛코드" />
|
||||
</SearchField>
|
||||
<SearchField label="발주No">
|
||||
<Input value={purchaseOrderNo} onChange={(e) => setPurchaseOrderNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발주부품">
|
||||
<SearchableCodeSelect codeId="0001068" value={type} onChange={setType} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발주구분">
|
||||
<SearchableCodeSelect codeId="0001406" value={orderTypeCd} onChange={setOrderTypeCd} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="입고요청일">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
value={deliveryStartDate}
|
||||
onChange={(e) => setDeliveryStartDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={deliveryEndDate}
|
||||
onChange={(e) => setDeliveryEndDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<SearchableSelect options={supplyOptions} value={partnerObjid} onChange={setPartnerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="구매담당">
|
||||
<SearchableSelect options={userOptions} value={salesMngUserId} onChange={setSalesMngUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발주일">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
value={regStartDate}
|
||||
onChange={(e) => setRegStartDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={regEndDate}
|
||||
onChange={(e) => setRegEndDate(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="품번">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input value={partName} onChange={(e) => setPartName(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발주처">
|
||||
<SearchableSelect options={supplyOptions} value={poClientId} onChange={setPoClientId} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<select
|
||||
value={apprStatus}
|
||||
onChange={(e) => setApprStatus(e.target.value)}
|
||||
className="h-9 w-[120px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
<option value="create">작성중</option>
|
||||
<option value="inProcess">결재중</option>
|
||||
<option value="reject">반려</option>
|
||||
<option value="complete">결재완료</option>
|
||||
<option value="cancel">취소</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="규격">
|
||||
<Input value={partSpec} onChange={(e) => setPartSpec(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
"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 { FolderCell } from "@/components/ui/folder-cell";
|
||||
import { numberWithCommas } from "@/lib/utils";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
const openBomPopup = (objId: string) => {
|
||||
if (!objId) return;
|
||||
const w = 1800, h = 800;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/bom/structure?objId=${objId}&readonly=1`,
|
||||
`bomStructure_${objId}`,
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
// 원본: /purchaseOrder/purchaseOrderStatusByProject.do (발주관리_현황)
|
||||
export default function OrderStatusPage() {
|
||||
const [year, setYear] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [sums, setSums] = useState<Record<string, unknown>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 110, hozAlign: "left", frozen: true },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "left", frozen: true },
|
||||
{ title: "유닛명", field: "UNIT_PART_NAME", width: 220, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 130, hozAlign: "left" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "발주현황",
|
||||
columns: [
|
||||
{
|
||||
title: "구매BOM",
|
||||
field: "BOM_CNT",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
formatter: (cell, row) => (
|
||||
<FolderCell
|
||||
count={cell}
|
||||
onClick={() => openBomPopup(String(row.BOM_REPORT_OBJID || ""))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ title: "전체수량", field: "TOTAL_BOM_PART_CNT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "발주품수", field: "TOTAL_PO_PART_CNT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "미발주품수", field: "NON_PO_PART_CNT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{
|
||||
title: "발주율",
|
||||
field: "RATE_PO",
|
||||
width: 80,
|
||||
hozAlign: "right",
|
||||
formatter: (cell) => `${Number(cell) || 0}%`,
|
||||
},
|
||||
{ title: "구매품", field: "PRICE_PT_1", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "제작품", field: "PRICE_PT_2", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "사급품", field: "PRICE_PT_3", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "기타", field: "PRICE_PT_ETC", width: 100, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "재발주현황",
|
||||
columns: [
|
||||
{ title: "건수", field: "RE_COUNT", width: 70, hozAlign: "right", formatter: "money" },
|
||||
{ title: "금액(원)", field: "RE_PRICE", width: 120, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "턴키현황",
|
||||
columns: [
|
||||
{ title: "건수", field: "TURNKEY_COUNT", width: 70, hozAlign: "right", formatter: "money" },
|
||||
{ title: "금액(원)", field: "TURNKEY_PRICE", width: 120, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
{ title: "총발주금액(원)", field: "TOTAL_PRICE_ALL", width: 140, hozAlign: "right", formatter: "money" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/order/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
setSums(json.SUMS || {});
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [year, projectNo]);
|
||||
|
||||
const handleReset = () => {
|
||||
setYear("");
|
||||
setProjectNo("");
|
||||
};
|
||||
|
||||
const handleExcelDownload = () => {
|
||||
Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">발주관리_현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleReset}>초기화</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcelDownload}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 6 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input
|
||||
value={projectNo}
|
||||
onChange={(e) => setProjectNo(e.target.value)}
|
||||
placeholder="프로젝트번호 부분 일치"
|
||||
className="w-[200px]"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="flex items-center gap-6 mb-3 text-sm font-bold text-red-600">
|
||||
<span>총발주금액(원) : {numberWithCommas(Number(sums.TOTAL_PRICE_ALL_SUM ?? 0))}</span>
|
||||
<span>단일발주금액(원) : {numberWithCommas(Number(sums.SINGLE_PRICE_SUM ?? 0))}</span>
|
||||
</div>
|
||||
|
||||
<DataGrid columns={columns} data={data} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// partmgmt/partmgmtList.jsp 대응 - 부품관리(PART)
|
||||
export default function PartMgmtPage() {
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [partType, setPartType] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "파트번호", field: "PART_NO", width: 150, hozAlign: "left",
|
||||
cellClick: (row) => openPartDetail(String(row.OBJID)) },
|
||||
{ title: "파트명", field: "PART_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "파트유형", field: "PART_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "규격", field: "SPEC", width: 130, hozAlign: "left" },
|
||||
{ title: "재질", field: "MATERIAL_NAME", width: 100, hozAlign: "left" },
|
||||
{ title: "UNIT", field: "UNIT_NAME", width: 70, hozAlign: "center" },
|
||||
{ title: "중량", field: "WEIGHT", width: 80, hozAlign: "right" },
|
||||
{ title: "업체", field: "MAKER", width: 120, hozAlign: "left" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" },
|
||||
{ title: "2D", field: "DRAWING_2D_CNT", width: 40, hozAlign: "center" },
|
||||
{ title: "3D", field: "DRAWING_3D_CNT", width: 40, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/part-mgmt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ part_no: partNo, part_name: partName, part_type: partType }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [partNo, partName, partType]);
|
||||
|
||||
const openPartDetail = (objId: string) => {
|
||||
window.open(`/product-mgmt/form?objId=${objId}`, "partDetail", "width=1100,height=800");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
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={() => window.open("/product-mgmt/form", "partForm", "width=1100,height=800")}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="파트번호">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="파트명">
|
||||
<Input value={partName} onChange={(e) => setPartName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="파트유형">
|
||||
<CodeSelect codeId="0000062" value={partType} onChange={setPartType} placeholder="전체" className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} showCheckbox />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// partMgmt/partRegisterList.jsp 대응 - Part 등록
|
||||
export default function PartRegisterPage() {
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [spec, setSpec] = useState("");
|
||||
const [maker, setMaker] = useState("");
|
||||
const [categoryCode, setCategoryCode] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "파트번호", field: "PART_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/product/part-register/form?objId=${row.OBJID}`, "partDetail", "width=900,height=600") },
|
||||
{ title: "파트명", field: "PART_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "규격", field: "SPEC", width: 150, hozAlign: "left" },
|
||||
{ title: "단위", field: "UNIT_NAME", width: 60, hozAlign: "center" },
|
||||
{ title: "카테고리", field: "CATEGORY_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "제조사", field: "MAKER", width: 120, hozAlign: "left" },
|
||||
{ title: "단가", field: "UNIT_PRICE", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/part/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
part_no: partNo,
|
||||
part_name: partName,
|
||||
spec,
|
||||
maker,
|
||||
category_code: categoryCode,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [partNo, partName, spec, maker, categoryCode]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
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={() => window.open("/product/part-register/form", "partForm", "width=900,height=600")}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="파트번호">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="파트명">
|
||||
<Input value={partName} onChange={(e) => setPartName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="규격">
|
||||
<Input value={spec} onChange={(e) => setSpec(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제조사">
|
||||
<Input value={maker} onChange={(e) => setMaker(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="카테고리">
|
||||
<CodeSelect codeId="PART_CATEGORY" value={categoryCode} onChange={setCategoryCode} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import StandardCodePage from "@/components/procurement-std/StandardCodePage";
|
||||
|
||||
// 구매품표준관리 - 코드1
|
||||
export default function Code1Page() {
|
||||
return (
|
||||
<StandardCodePage
|
||||
title="구매품표준관리 - 대분류(CODE1)"
|
||||
codeGroupId="CODE1"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import StandardCodePage from "@/components/procurement-std/StandardCodePage";
|
||||
|
||||
// 구매품표준관리 - 코드2
|
||||
export default function Code2Page() {
|
||||
return (
|
||||
<StandardCodePage
|
||||
title="구매품표준관리 - 중분류(CODE2)"
|
||||
codeGroupId="CODE2"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import StandardCodePage from "@/components/procurement-std/StandardCodePage";
|
||||
|
||||
// 구매품표준관리 - 코드3
|
||||
export default function Code3Page() {
|
||||
return (
|
||||
<StandardCodePage
|
||||
title="구매품표준관리 - Maker(CODE3)"
|
||||
codeGroupId="CODE3"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import StandardCodePage from "@/components/procurement-std/StandardCodePage";
|
||||
|
||||
// 구매품표준관리 - 코드4
|
||||
export default function Code4Page() {
|
||||
return (
|
||||
<StandardCodePage
|
||||
title="구매품표준관리 - 품명(CODE4)"
|
||||
codeGroupId="CODE4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import StandardCodePage from "@/components/procurement-std/StandardCodePage";
|
||||
|
||||
// 구매품표준관리 - 코드5
|
||||
export default function Code5Page() {
|
||||
return (
|
||||
<StandardCodePage
|
||||
title="구매품표준관리 - 규격(CODE5)"
|
||||
codeGroupId="CODE5"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import StandardCodePage from "@/components/procurement-std/StandardCodePage";
|
||||
|
||||
// 구매품표준관리 - 자재코드
|
||||
export default function MaterialCodePage() {
|
||||
return (
|
||||
<StandardCodePage
|
||||
title="구매품표준관리 - 자재코드"
|
||||
codeGroupId="MATERIAL_CODE"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Pagination } from "@/components/ui/pagination";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// productmgmtList.jsp 대응 - 제품마스터
|
||||
export default function ProductMgmtPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [productCategory, setProductCategory] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
const countPerPage = 20;
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "사업부", field: "PRODUCT_CATEGORY_NAME", width: 120,
|
||||
cellClick: (row) => openProductFormPopup(String(row.OBJID)) },
|
||||
{ title: "제품군", field: "PRODUCT_TYPE_NAME", width: 100, hozAlign: "left" },
|
||||
{ title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "제품코드", field: "PRODUCT_CODE", width: 130, hozAlign: "left" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" },
|
||||
{ title: "생산여부", field: "PRODUCTION_FLAG_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "첨부", field: "FILE_CNT", width: 60, hozAlign: "center",
|
||||
formatter: (_cell, row) => Number(row.FILE_CNT || 0) > 0 ? "📎" : "" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async (page = 1) => {
|
||||
const res = await fetch("/api/product-mgmt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, product_category: productCategory, page, countPerPage }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
setTotalCount(json.TOTAL_CNT || 0);
|
||||
setCurrentPage(page);
|
||||
}
|
||||
}, [year, productCategory]);
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchData(1);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("선택한 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: "삭제 확인",
|
||||
text: `${selectedRows.length}건을 삭제하시겠습니까?`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "삭제",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
const res = await fetch("/api/product-mgmt", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objIds: selectedRows.map((r) => r.OBJID) }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: "삭제되었습니다.", timer: 1500, showConfirmButton: false });
|
||||
fetchData(currentPage);
|
||||
} else {
|
||||
Swal.fire({ icon: "error", title: json.message || "삭제 실패" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openProductFormPopup = (objId?: string) => {
|
||||
// TODO: 모달 다이얼로그로 대체 (기존 window.open popup 대응)
|
||||
const url = objId
|
||||
? `/product-mgmt/form?objId=${objId}`
|
||||
: `/product-mgmt/form?actionType=regist`;
|
||||
window.open(url, "productForm", "width=850,height=480");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">제품마스터</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => openProductFormPopup()}>등록</Button>
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색 영역 (productmgmtList.jsp #plmSearchZon 대응) */}
|
||||
<SearchForm onSearch={handleSearch}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
|
||||
<SearchField label="사업부">
|
||||
<CodeSelect
|
||||
codeId="0000917"
|
||||
value={productCategory}
|
||||
onChange={setProductCategory}
|
||||
placeholder="전체"
|
||||
className="w-[150px]"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
{/* 그리드 (Tabulator/plm_table 대응) */}
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
onSelectionChange={setSelectedRows}
|
||||
|
||||
/>
|
||||
|
||||
{/* 페이징 */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
maxPage={Math.ceil(totalCount / countPerPage)}
|
||||
totalCount={totalCount}
|
||||
onPageChange={(page) => fetchData(page)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } 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";
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
// 제품관리_BOM 조회 (원본: partMng/structureAscendingList.jsp)
|
||||
// 고객사 → 프로젝트번호 → 유닛명 캐스케이드 + 품번/LEVEL 필터로 BOM 정전개 트리 조회
|
||||
const LEVEL_COLORS = [
|
||||
"", "#fde9d9", "#daeef3", "#e4dfec", "#ebf1de", "#f2f2f2",
|
||||
"#f2dcdb", "#eeece1", "#dce6f1", "#FFFFEB", "#ffffff",
|
||||
];
|
||||
|
||||
export default function BomListPage() {
|
||||
const [customerCd, setCustomerCd] = useState("");
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [searchPartNo, setSearchPartNo] = useState("");
|
||||
const [searchLevel, setSearchLevel] = useState("");
|
||||
|
||||
const [data, setData] = useState<Row[]>([]);
|
||||
const [maxLevel, setMaxLevel] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// 접힌 노드: CHILD_OBJID 를 키로 사용 (해당 노드의 모든 하위 행을 숨김)
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
||||
|
||||
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
|
||||
const [projects, setProjects] = useState<{ value: string; label: string }[]>([]);
|
||||
const [units, setUnits] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
// 고객사 로드
|
||||
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: Row) => ({
|
||||
value: String(r.OBJID), label: String(r.SUPPLY_NAME || r.OBJID),
|
||||
}))))
|
||||
.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: Row) => ({
|
||||
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: Row) => ({
|
||||
value: String(r.OBJID), label: String(r.UNIT_NAME || r.TASK_NAME),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
}, [projectName]);
|
||||
|
||||
const openPartDetail = (partObjId: string) => {
|
||||
const w = 600, h = 700;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(`/product/popup/part-form?objId=${encodeURIComponent(partObjId)}&readOnly=true`,
|
||||
"partMngDetail", `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`);
|
||||
};
|
||||
|
||||
const openFilePopup = (objId: string, docType: string, docTypeName: string) => {
|
||||
const w = 800, h = 335;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`,
|
||||
"fileRegistPopUp", `width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
// 조상 체인 계산용 인덱스 (CHILD_OBJID → data index)
|
||||
const indexByChildObjid = useMemo(() => {
|
||||
const m = new Map<string, number>();
|
||||
data.forEach((r, i) => m.set(String(r.CHILD_OBJID), i));
|
||||
return m;
|
||||
}, [data]);
|
||||
|
||||
// 각 row 의 조상 CHILD_OBJID 리스트 (루트→자신 바로 위)
|
||||
const ancestorsOf = useCallback((row: Row): string[] => {
|
||||
const chain: string[] = [];
|
||||
let cur: Row | undefined = row;
|
||||
while (cur && cur.PARENT_OBJID) {
|
||||
const p = String(cur.PARENT_OBJID);
|
||||
chain.push(p);
|
||||
const idx = indexByChildObjid.get(p);
|
||||
cur = idx !== undefined ? data[idx] : undefined;
|
||||
}
|
||||
return chain;
|
||||
}, [data, indexByChildObjid]);
|
||||
|
||||
// 접힌 조상이 하나라도 있으면 숨김
|
||||
const visibleData = useMemo(
|
||||
() => (collapsed.size === 0
|
||||
? data
|
||||
: data.filter((r) => !ancestorsOf(r).some((a) => collapsed.has(a)))),
|
||||
[data, collapsed, ancestorsOf],
|
||||
);
|
||||
|
||||
const toggleNode = useCallback((childObjid: string) => {
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(childObjid)) next.delete(childObjid);
|
||||
else next.add(childObjid);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const expandAll = () => setCollapsed(new Set());
|
||||
const collapseAllLevel1 = () => {
|
||||
// LEV 1 중 자식이 있는 노드만 접기 (원본 JSP 기본 동작 상응)
|
||||
const ids = new Set<string>();
|
||||
data.forEach((r) => {
|
||||
if (Number(r.LEV ?? 0) === 1 && Number(r.LEAF ?? 1) === 0) {
|
||||
ids.add(String(r.CHILD_OBJID));
|
||||
}
|
||||
});
|
||||
setCollapsed(ids);
|
||||
};
|
||||
|
||||
// 레벨 컬럼 동적 생성 (1, 2, 3 ... MAX_LEVEL)
|
||||
const levelColumns: GridColumn[] = Array.from({ length: Math.max(maxLevel, 1) }, (_, i) => ({
|
||||
title: String(i + 1), field: `_LEV_${i + 1}`, width: 22, hozAlign: "center",
|
||||
formatter: (_cell, row) => Number(row.LEV ?? 0) === i + 1 ? "*" : "",
|
||||
}));
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "", field: "_TREE", width: 28, hozAlign: "center",
|
||||
formatter: (_cell, row) => {
|
||||
if (Number(row.LEAF ?? 1) !== 0) return "";
|
||||
const id = String(row.CHILD_OBJID);
|
||||
const isCollapsed = collapsed.has(id);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); toggleNode(id); }}
|
||||
className="inline-flex h-4 w-4 items-center justify-center border border-gray-400 text-[11px] leading-none font-bold text-gray-700 hover:bg-gray-100"
|
||||
aria-label={isCollapsed ? "펴기" : "접기"}
|
||||
>
|
||||
{isCollapsed ? "+" : "−"}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
...levelColumns,
|
||||
{
|
||||
title: "품번", field: "PART_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => openPartDetail(String(row.PART_OBJID)),
|
||||
},
|
||||
{ title: "품명", field: "PART_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "수량", field: "QTY", width: 55, hozAlign: "center" },
|
||||
{
|
||||
title: "3D", field: "FILE_3D_CNT", width: 40, hozAlign: "center",
|
||||
formatter: (cell) => <FolderCell count={cell} />,
|
||||
cellClick: (row) => openFilePopup(String(row.PART_OBJID), "3D_CAD", "3D CAD 첨부파일"),
|
||||
},
|
||||
{
|
||||
title: "2D", field: "FILE_2D_CNT", width: 40, hozAlign: "center",
|
||||
formatter: (cell) => <FolderCell count={cell} />,
|
||||
cellClick: (row) => openFilePopup(String(row.PART_OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"),
|
||||
},
|
||||
{
|
||||
title: "PDF", field: "FILE_PDF_CNT", width: 40, hozAlign: "center",
|
||||
formatter: (cell) => <FolderCell count={cell} />,
|
||||
cellClick: (row) => openFilePopup(String(row.PART_OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일"),
|
||||
},
|
||||
{ title: "재질", field: "MATERIAL", width: 100, hozAlign: "left" },
|
||||
{ title: "사양(규격)", field: "SPEC", width: 130, hozAlign: "left" },
|
||||
{ title: "후처리", field: "POST_PROCESSING", width: 120, hozAlign: "left" },
|
||||
{ title: "MAKER", field: "MAKER", width: 90, hozAlign: "left" },
|
||||
{ title: "Revision", field: "REVISION", width: 70, hozAlign: "center" },
|
||||
{ title: "EO No", field: "EO_NO", width: 85, hozAlign: "center" },
|
||||
{ title: "EO Date", field: "EO_DATE", width: 90, hozAlign: "center" },
|
||||
{ title: "PART구분", field: "PART_TYPE_TITLE", width: 90, hozAlign: "center" },
|
||||
{ title: "비고", field: "REMARK", width: 200, hozAlign: "left" },
|
||||
];
|
||||
|
||||
const validate = () => {
|
||||
// 품번만 입력해도 검색 허용 — 품번이 있으면 고객사/프로젝트/유닛 미선택 가능
|
||||
if (searchPartNo) return true;
|
||||
if (!customerCd) { Swal.fire("알림", "고객사를 선택하거나 품번을 입력해 주세요.", "warning"); return false; }
|
||||
if (!projectName) { Swal.fire("알림", "프로젝트번호를 선택해 주세요.", "warning"); return false; }
|
||||
if (!unitCode) { Swal.fire("알림", "유닛명을 선택해 주세요.", "warning"); return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async (skipValidate = false) => {
|
||||
if (!skipValidate && !validate()) return;
|
||||
if (!searchPartNo && (!customerCd || !projectName || !unitCode)) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/product/bom/ascending", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
customer_cd: customerCd,
|
||||
project_name: projectName,
|
||||
unit_code: unitCode,
|
||||
search_partNo: searchPartNo,
|
||||
search_level: searchLevel,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const j = await res.json();
|
||||
setData(j.RESULTLIST || []);
|
||||
setMaxLevel(Number(j.MAX_LEVEL || 0));
|
||||
setCollapsed(new Set());
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [customerCd, projectName, unitCode, searchPartNo, searchLevel]);
|
||||
|
||||
const handleReset = () => {
|
||||
setCustomerCd(""); setProjectName(""); setUnitCode("");
|
||||
setSearchPartNo(""); setSearchLevel("");
|
||||
setData([]); setMaxLevel(0);
|
||||
setCollapsed(new Set());
|
||||
};
|
||||
|
||||
const handleExcel = () => {
|
||||
if (data.length === 0) { Swal.fire("알림", "다운로드할 데이터가 없습니다.", "warning"); return; }
|
||||
const levelHeaders = Array.from({ length: Math.max(maxLevel, 1) }, (_, i) => String(i + 1));
|
||||
const header = [
|
||||
...levelHeaders, "품번", "품명", "수량", "3D", "2D", "PDF",
|
||||
"재질", "사양(규격)", "후처리", "MAKER", "Revision",
|
||||
"EO No", "EO Date", "PART구분", "비고",
|
||||
];
|
||||
const body = data.map((r) => {
|
||||
const lev = Number(r.LEV ?? 0);
|
||||
const markers = Array.from({ length: Math.max(maxLevel, 1) }, (_, i) => (lev === i + 1 ? "*" : ""));
|
||||
return [
|
||||
...markers, r.PART_NO ?? "", r.PART_NAME ?? "", r.QTY ?? "",
|
||||
Number(r.FILE_3D_CNT ?? 0), Number(r.FILE_2D_CNT ?? 0), Number(r.FILE_PDF_CNT ?? 0),
|
||||
r.MATERIAL ?? "", r.SPEC ?? "", r.POST_PROCESSING ?? "",
|
||||
r.MAKER ?? "", r.REVISION ?? "", r.EO_NO ?? "",
|
||||
r.EO_DATE ?? "", r.PART_TYPE_TITLE ?? "", r.REMARK ?? "",
|
||||
];
|
||||
});
|
||||
const ws = XLSX.utils.aoa_to_sheet([header, ...body]);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "BOM_REPORT_정전개");
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
XLSX.writeFile(wb, `BOM_REPORT_정전개_${today}.xlsx`);
|
||||
};
|
||||
|
||||
// 행별 배경색(레벨별)
|
||||
const rowClass = (row: Row): string => {
|
||||
const lev = Number(row.LEV ?? 0);
|
||||
const bg = LEVEL_COLORS[Math.min(lev, LEVEL_COLORS.length - 1)];
|
||||
return bg ? "" : "";
|
||||
void bg; // 배경색은 인라인 스타일 쓰지 않고 기본 유지 (선택적)
|
||||
};
|
||||
void rowClass;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">제품관리_BOM 조회</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={expandAll} disabled={data.length === 0}>전체 펴기</Button>
|
||||
<Button size="sm" variant="secondary" onClick={collapseAllLevel1} disabled={data.length === 0}>전체 접기</Button>
|
||||
<Button size="sm" onClick={() => fetchData()}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleReset}>초기화</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcel}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={() => fetchData()}>
|
||||
<SearchField label="고객사 *">
|
||||
<SearchableSelect options={customers} value={customerCd} onChange={setCustomerCd} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호 *">
|
||||
<SearchableSelect options={projects} value={projectName} onChange={setProjectName} className="w-[220px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유닛명 *">
|
||||
<SearchableSelect options={units} value={unitCode} onChange={setUnitCode} className="w-[300px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품번">
|
||||
<Input value={searchPartNo} onChange={(e) => setSearchPartNo(e.target.value)} className="w-[200px]" />
|
||||
</SearchField>
|
||||
<SearchField label="LEVEL">
|
||||
<select value={searchLevel} onChange={(e) => setSearchLevel(e.target.value)}
|
||||
className="h-9 w-[80px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
<option value="">전체</option>
|
||||
{[1, 2, 3, 4, 5, 6].map((n) => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={visibleData} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { ExcelDownloadButton } from "@/components/ui/excel-download-button";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 제품관리_설계변경 리스트 (원본: partMng/partMngHisList.jsp)
|
||||
export default function DesignChangePage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState("");
|
||||
const [contractObjid, setContractObjid] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [changeOption, setChangeOption] = useState("");
|
||||
const [eoStart, setEoStart] = useState("");
|
||||
const [eoEnd, setEoEnd] = useState("");
|
||||
const [changeType, setChangeType] = useState("");
|
||||
const [partType, setPartType] = useState("");
|
||||
const [writerId, setWriterId] = useState("");
|
||||
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [projects, setProjects] = useState<{ value: string; label: string }[]>([]);
|
||||
const [units, setUnits] = useState<{ value: string; label: string }[]>([]);
|
||||
const [users, setUsers] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
// 프로젝트 목록 로드
|
||||
useEffect(() => {
|
||||
fetch("/api/common/project-list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
|
||||
}).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(() => {});
|
||||
}, []);
|
||||
|
||||
// 프로젝트 변경 시 유닛 목록 로드
|
||||
useEffect(() => {
|
||||
if (!contractObjid) { setUnits([]); return; }
|
||||
fetch("/api/common/unit-list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ contract_objid: contractObjid }),
|
||||
}).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(() => {});
|
||||
setUnitCode("");
|
||||
}, [contractObjid]);
|
||||
|
||||
// 사용자 목록 로드
|
||||
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(() => {});
|
||||
}, []);
|
||||
|
||||
const openHisDetail = (objId: string) => {
|
||||
const w = 800, h = 550;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/product/popup/part-his-detail?objId=${encodeURIComponent(objId)}`,
|
||||
"partMngHisDetailPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "EO No", field: "EO_NO", width: 85, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "모품번", field: "PARENT_PART_INFO", width: 150, hozAlign: "left" },
|
||||
{
|
||||
title: "품번", field: "PART_NO", width: 150, hozAlign: "left",
|
||||
cellClick: (row) => openHisDetail(String(row.OBJID)),
|
||||
},
|
||||
{ title: "품명", field: "PART_NAME", hozAlign: "left" },
|
||||
{ title: "수량", field: "QTY", width: 55, hozAlign: "center" },
|
||||
{ title: "변경수량", field: "QTY_TEMP", width: 75, hozAlign: "center" },
|
||||
{ title: "EO구분", field: "CHANGE_TYPE_NAME", width: 75, hozAlign: "center" },
|
||||
{ title: "EO사유", field: "CHANGE_OPTION_NAME", width: 75, hozAlign: "center" },
|
||||
{ title: "Revision", field: "REVISION", width: 85, hozAlign: "center" },
|
||||
{ title: "EO Date", field: "EO_DATE", width: 85, hozAlign: "center" },
|
||||
{ title: "PART구분", field: "PART_TYPE_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "담당자", field: "WRITER_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "실행일", field: "HIS_REG_DATE_TITLE", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/product/design-change", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
contract_objid: contractObjid,
|
||||
unit_code: unitCode,
|
||||
part_no: partNo,
|
||||
part_name: partName,
|
||||
change_option: changeOption,
|
||||
eo_start_date: eoStart,
|
||||
eo_end_date: eoEnd,
|
||||
change_type: changeType,
|
||||
part_type: partType,
|
||||
writer_id: writerId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} catch {
|
||||
Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [year, contractObjid, unitCode, partNo, partName, changeOption, eoStart, eoEnd, changeType, partType, writerId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">제품관리_설계변경 리스트</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={fetchData}>조회</Button>
|
||||
<ExcelDownloadButton data={data} columns={columns} filename="제품관리_설계변경리스트" />
|
||||
<Button size="sm" variant="secondary" onClick={() => {
|
||||
setYear(""); setContractObjid(""); setUnitCode("");
|
||||
setPartNo(""); setPartName(""); setChangeOption("");
|
||||
setEoStart(""); setEoEnd(""); setChangeType(""); setPartType(""); setWriterId("");
|
||||
}}>초기화</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] 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="프로젝트번호">
|
||||
<SearchableSelect options={projects} value={contractObjid} onChange={setContractObjid} className="w-[200px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<SearchableSelect options={units} value={unitCode} onChange={setUnitCode} className="w-[220px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품번">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input value={partName} onChange={(e) => setPartName(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="EO사유">
|
||||
<SearchableCodeSelect codeId="0000318" value={changeOption} onChange={setChangeOption} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="EO Date">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={eoStart} onChange={(e) => setEoStart(e.target.value)} className="w-[140px]" />
|
||||
<span>~</span>
|
||||
<Input type="date" value={eoEnd} onChange={(e) => setEoEnd(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="EO구분">
|
||||
<SearchableCodeSelect codeId="0001054" value={changeType} onChange={setChangeType} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PART 구분">
|
||||
<SearchableCodeSelect codeId="0000062" value={partType} onChange={setPartType} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="담당자">
|
||||
<SearchableSelect options={users} value={writerId} onChange={setWriterId} className="w-[140px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
import { ExcelDownloadButton } from "@/components/ui/excel-download-button";
|
||||
import type { ExcelColumn } from "@/lib/excel-export";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
type CodeOpt = { CODE_ID: string; CODE_NAME: string };
|
||||
|
||||
// 제품관리_설변대상 PART조회 (원본: partMng/partMngChangeList.jsp)
|
||||
export default function PartChangePage() {
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [searchPartObjid, setSearchPartObjid] = useState("");
|
||||
|
||||
const [data, setData] = useState<Row[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [projects, setProjects] = useState<{ value: string; label: string }[]>([]);
|
||||
const [units, setUnits] = useState<{ value: string; label: string }[]>([]);
|
||||
const [parts, setParts] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
const [partTypes, setPartTypes] = useState<CodeOpt[]>([]);
|
||||
const [changeTypes, setChangeTypes] = useState<CodeOpt[]>([]);
|
||||
const [changeOptions, setChangeOptions] = useState<CodeOpt[]>([]);
|
||||
|
||||
// 공통코드 로드 (API 응답이 소문자 키라 대문자로 정규화)
|
||||
useEffect(() => {
|
||||
const loadCode = async (codeId: string): Promise<CodeOpt[]> => {
|
||||
const res = await fetch("/api/common/code-list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ codeId }),
|
||||
});
|
||||
const j = await res.json();
|
||||
return (j.data || []).map((r: Record<string, unknown>) => ({
|
||||
CODE_ID: String(r.code_id || r.CODE_ID || ""),
|
||||
CODE_NAME: String(r.code_name || r.CODE_NAME || ""),
|
||||
}));
|
||||
};
|
||||
loadCode("0000062").then(setPartTypes);
|
||||
loadCode("0001054").then(setChangeTypes); // PART_CHANGE_TYPE_CODE
|
||||
loadCode("0000318").then(setChangeOptions); // PART_CHANGE_OPTION_CODE
|
||||
}, []);
|
||||
|
||||
// 프로젝트 목록
|
||||
useEffect(() => {
|
||||
fetch("/api/common/project-list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
|
||||
}).then((r) => r.json())
|
||||
.then((j) => setProjects((j.RESULTLIST || []).map((r: Row) => ({
|
||||
value: String(r.OBJID), label: String(r.LABEL || r.PROJECT_NO || r.OBJID),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// 프로젝트 변경 시 유닛
|
||||
useEffect(() => {
|
||||
setUnitCode("");
|
||||
setSearchPartObjid("");
|
||||
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: Row) => ({
|
||||
value: String(r.OBJID), label: String(r.UNIT_NAME || r.TASK_NAME),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
}, [projectName]);
|
||||
|
||||
// 유닛 변경 시 PART 목록
|
||||
useEffect(() => {
|
||||
setSearchPartObjid("");
|
||||
if (!projectName || !unitCode) { setParts([]); return; }
|
||||
fetch("/api/product/part-change", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "list", project_name: projectName, unit_code: unitCode }),
|
||||
}).then((r) => r.json())
|
||||
.then((j) => {
|
||||
const uniq = new Map<string, string>();
|
||||
(j.RESULTLIST || []).forEach((r: Row) => {
|
||||
const id = String(r.OBJID);
|
||||
uniq.set(id, `${r.PART_NO ?? ""} ${r.PART_NAME ?? ""}`);
|
||||
});
|
||||
setParts([...uniq.entries()].map(([value, label]) => ({ value, label })));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [projectName, unitCode]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!projectName) {
|
||||
Swal.fire("알림", "프로젝트번호를 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
if (!unitCode) {
|
||||
Swal.fire("알림", "유닛명을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
if (!searchPartObjid) {
|
||||
Swal.fire("알림", "품번을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/product/part-change", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "list",
|
||||
project_name: projectName,
|
||||
unit_code: unitCode,
|
||||
SEARCH_PART_OBJID: searchPartObjid,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const j = await res.json();
|
||||
setData(j.RESULTLIST || []);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectName, unitCode, searchPartObjid]);
|
||||
|
||||
const updateCell = (idx: number, field: string, value: string) => {
|
||||
setData((prev) => {
|
||||
const next = [...prev];
|
||||
next[idx] = { ...next[idx], [field]: value };
|
||||
// 같은 품번끼리 동기화 (원본 cellEdited 동작)
|
||||
const partNo = next[idx].PART_NO;
|
||||
for (let i = 0; i < next.length; i++) {
|
||||
if (i !== idx && next[i].PART_NO === partNo) {
|
||||
next[i] = { ...next[i], [field]: value };
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 같은 PART_NO가 여러 행(동시 적용 BOM)으로 올 때 첫 번째만 편집 가능.
|
||||
// updateCell 이 같은 PART_NO 나머지 행도 동기화하므로 하나만 활성화해도 충분.
|
||||
const editableIdxSet = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
const s = new Set<number>();
|
||||
data.forEach((row, idx) => {
|
||||
if (String(row.Q_STATUS || "") !== "deploy") return;
|
||||
const partNo = String(row.PART_NO ?? "");
|
||||
if (!seen.has(partNo)) {
|
||||
seen.add(partNo);
|
||||
s.add(idx);
|
||||
}
|
||||
});
|
||||
return s;
|
||||
}, [data]);
|
||||
|
||||
const isEditable = (idx: number) => editableIdxSet.has(idx);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (data.length === 0) {
|
||||
Swal.fire("알림", "저장할 데이터가 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const allDeploy = data.every((r) => String(r.Q_STATUS || "") === "deploy");
|
||||
if (!allDeploy) {
|
||||
Swal.fire("알림", "설변중인 파트입니다. 배포후에 다시 설계변경 할 수 있습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const allFilled = data.every((r) =>
|
||||
String(r.CHANGE_TYPE || "") !== "" && String(r.CHANGE_OPTION || "") !== ""
|
||||
);
|
||||
if (!allFilled) {
|
||||
Swal.fire("알림", "EO구분, EO사유는 필수 입력입니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const c = await Swal.fire({
|
||||
title: "저장하시겠습니까?", icon: "question", showCancelButton: true,
|
||||
confirmButtonText: "확인", cancelButtonText: "취소",
|
||||
});
|
||||
if (!c.isConfirmed) return;
|
||||
const res = await fetch("/api/product/part-change", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "save", rows: data }),
|
||||
});
|
||||
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 openPartForm = (objId: string) => {
|
||||
const w = 600, h = 500;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(`/product/popup/part-form?objId=${encodeURIComponent(objId)}`, "partMngPopUp",
|
||||
`width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const openFilePopup = (objId: string, docType: string, docTypeName: string) => {
|
||||
const w = 800, h = 335;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`,
|
||||
"fileRegistPopUp", `width=${w},height=${h},left=${left},top=${top}`,
|
||||
);
|
||||
};
|
||||
|
||||
const yellowHeader = "px-2 py-1 text-[#FFBB00] border border-gray-200 font-semibold text-center whitespace-nowrap";
|
||||
const normalHeader = "px-2 py-1 border border-gray-200 font-semibold text-center whitespace-nowrap";
|
||||
// frozen 셀/헤더 공통 스타일 — 인라인 스타일로 sticky/bg/zIndex 강제 적용 (border-collapse:separate 와 조합)
|
||||
const freezeHeader = (left: number): React.CSSProperties => ({
|
||||
position: "sticky", left, top: 0, zIndex: 30, backgroundColor: "#F9FAFB",
|
||||
});
|
||||
const freezeCell = (left: number): React.CSSProperties => ({
|
||||
position: "sticky", left, zIndex: 20, backgroundColor: "#FFFFFF",
|
||||
});
|
||||
|
||||
// Excel 컬럼 정의 (원본 partMngChangeList 헤더 순서 그대로)
|
||||
const excelColumns: ExcelColumn[] = useMemo(() => {
|
||||
const codeName = (list: CodeOpt[], id: string) =>
|
||||
list.find((c) => c.CODE_ID === id)?.CODE_NAME ?? id;
|
||||
return [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO" },
|
||||
{ title: "유닛명", field: "UNIT_NAME" },
|
||||
{ title: "모품번", field: "PARENT_PART_INFO" },
|
||||
{ title: "품번", field: "PART_NO" },
|
||||
{ title: "품명", field: "PART_NAME" },
|
||||
{ title: "수량", field: "Q_QTY" },
|
||||
{ title: "3D", excelFormatter: (r) => Number(r.CU01_CNT ?? 0) },
|
||||
{ title: "2D", excelFormatter: (r) => Number(r.CU02_CNT ?? 0) },
|
||||
{ title: "PDF", excelFormatter: (r) => Number(r.CU03_CNT ?? 0) },
|
||||
{ title: "EO구분", excelFormatter: (r) => codeName(changeTypes, String(r.CHANGE_TYPE ?? "")) },
|
||||
{ title: "EO사유", excelFormatter: (r) => codeName(changeOptions, String(r.CHANGE_OPTION ?? "")) },
|
||||
{ title: "PART구분", excelFormatter: (r) => codeName(partTypes, String(r.PART_TYPE ?? "")) },
|
||||
{ title: "재질", field: "MATERIAL" },
|
||||
{ title: "사양(규격)", field: "SPEC" },
|
||||
{ title: "후처리", field: "POST_PROCESSING" },
|
||||
{ title: "MAKER", field: "MAKER" },
|
||||
{ title: "대분류", field: "MAJOR_CATEGORY" },
|
||||
{ title: "중분류", field: "SUB_CATEGORY" },
|
||||
{ title: "Revision", field: "REVISION" },
|
||||
{ title: "EO No", field: "EO_NO" },
|
||||
{ title: "EO Date", field: "EO_DATE" },
|
||||
{ title: "비고", field: "REMARK" },
|
||||
];
|
||||
}, [changeTypes, changeOptions, partTypes]);
|
||||
|
||||
const codeSel = useMemo(() => (opts: CodeOpt[], val: string, onChange: (v: string) => void, disabled: boolean) => (
|
||||
<select value={val || ""} onChange={(e) => onChange(e.target.value)} disabled={disabled}
|
||||
className="w-full h-7 text-xs border border-gray-300 rounded px-1 disabled:bg-gray-100">
|
||||
<option value="">선택</option>
|
||||
{opts.map((o) => <option key={o.CODE_ID} value={o.CODE_ID}>{o.CODE_NAME}</option>)}
|
||||
</select>
|
||||
), []);
|
||||
|
||||
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={handleSave}>설계변경저장</Button>
|
||||
<Button size="sm" onClick={fetchData}>조회</Button>
|
||||
<ExcelDownloadButton
|
||||
data={data}
|
||||
columns={excelColumns}
|
||||
filename="제품관리_설변대상PART조회"
|
||||
sheetName="설변대상PART"
|
||||
/>
|
||||
<Button size="sm" variant="secondary" onClick={() => {
|
||||
setProjectName(""); setUnitCode(""); setSearchPartObjid(""); setData([]);
|
||||
}}>초기화</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="프로젝트번호">
|
||||
<SearchableSelect options={projects} value={projectName} onChange={setProjectName} className="w-[230px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<SearchableSelect options={units} value={unitCode} onChange={setUnitCode} className="w-[260px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품번">
|
||||
<SearchableSelect options={parts} value={searchPartObjid} onChange={setSearchPartObjid} className="w-[230px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="relative border border-gray-200 rounded bg-white overflow-auto" style={{ maxHeight: "calc(100vh - 310px)" }}>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-[1px]">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-12 h-12 border-4 border-gray-300 border-t-primary rounded-full animate-spin" />
|
||||
<span className="text-sm font-semibold text-gray-700">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<table className="text-xs table-fixed border-collapse" style={{ width: 2295 }}>
|
||||
<colgroup>
|
||||
<col style={{ width: 100 }} />
|
||||
<col style={{ width: 200 }} />
|
||||
<col style={{ width: 125 }} />
|
||||
<col style={{ width: 125 }} />
|
||||
<col style={{ width: 270 }} />
|
||||
<col style={{ width: 60 }} />
|
||||
<col style={{ width: 45 }} />
|
||||
<col style={{ width: 45 }} />
|
||||
<col style={{ width: 55 }} />
|
||||
<col style={{ width: 110 }} />
|
||||
<col style={{ width: 110 }} />
|
||||
<col style={{ width: 110 }} />
|
||||
<col style={{ width: 100 }} />
|
||||
<col style={{ width: 100 }} />
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 80 }} />
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 90 }} />
|
||||
<col style={{ width: 120 }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={normalHeader} style={freezeHeader(0)}>프로젝트번호</th>
|
||||
<th className={normalHeader} style={freezeHeader(100)}>유닛명</th>
|
||||
<th className={normalHeader} style={freezeHeader(300)}>모품번</th>
|
||||
<th className={normalHeader} style={freezeHeader(425)}>품번</th>
|
||||
<th className={normalHeader} style={freezeHeader(550)}>품명</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>수량</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>3D</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>2D</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>PDF</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>EO구분</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>EO사유</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>PART구분</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>재질</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>사양(규격)</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>후처리</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>MAKER</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>대분류</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>중분류</th>
|
||||
<th className={normalHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>Revision</th>
|
||||
<th className={normalHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>EO No</th>
|
||||
<th className={normalHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>EO Date</th>
|
||||
<th className={yellowHeader} style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#F9FAFB" }}>비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr><td colSpan={22} className="text-center py-10 text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : data.map((row, idx) => {
|
||||
const editable = isEditable(idx);
|
||||
const frozenCell = "px-2 py-1 border border-gray-200";
|
||||
return (
|
||||
<tr key={idx}>
|
||||
<td className={frozenCell} style={freezeCell(0)}>{String(row.PROJECT_NO ?? "")}</td>
|
||||
<td className={frozenCell} style={freezeCell(100)}>{String(row.UNIT_NAME ?? "")}</td>
|
||||
<td className={frozenCell} style={freezeCell(300)}>{String(row.PARENT_PART_INFO ?? "")}</td>
|
||||
<td className={frozenCell} style={freezeCell(425)}>
|
||||
<a className="text-primary hover:underline cursor-pointer" onClick={() => openPartForm(String(row.OBJID))}>
|
||||
{String(row.PART_NO ?? "")}
|
||||
</a>
|
||||
</td>
|
||||
<td className={frozenCell} style={freezeCell(550)}>{String(row.PART_NAME ?? "")}</td>
|
||||
<td className="px-2 py-1 border border-gray-200 text-center">
|
||||
{editable ? (
|
||||
<input type="number" value={String(row.Q_QTY ?? "")} onChange={(e) => updateCell(idx, "Q_QTY", e.target.value)}
|
||||
className="w-full h-6 text-xs border border-gray-300 rounded px-1 text-right" />
|
||||
) : String(row.Q_QTY ?? "")}
|
||||
</td>
|
||||
<td className="px-2 py-1 border border-gray-200 text-center">
|
||||
<FolderCell count={row.CU01_CNT} onClick={() => openFilePopup(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일")} />
|
||||
</td>
|
||||
<td className="px-2 py-1 border border-gray-200 text-center">
|
||||
<FolderCell count={row.CU02_CNT} onClick={() => openFilePopup(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일")} />
|
||||
</td>
|
||||
<td className="px-2 py-1 border border-gray-200 text-center">
|
||||
<FolderCell count={row.CU03_CNT} onClick={() => openFilePopup(String(row.OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일")} />
|
||||
</td>
|
||||
<td className="px-1 py-1 border border-gray-200">{codeSel(changeTypes, String(row.CHANGE_TYPE ?? ""), (v) => updateCell(idx, "CHANGE_TYPE", v), !editable)}</td>
|
||||
<td className="px-1 py-1 border border-gray-200">{codeSel(changeOptions, String(row.CHANGE_OPTION ?? ""), (v) => updateCell(idx, "CHANGE_OPTION", v), !editable)}</td>
|
||||
<td className="px-1 py-1 border border-gray-200">{codeSel(partTypes, String(row.PART_TYPE ?? ""), (v) => updateCell(idx, "PART_TYPE", v), !editable)}</td>
|
||||
<td className="px-1 py-1 border border-gray-200">
|
||||
<Input value={String(row.MATERIAL ?? "")} onChange={(e) => updateCell(idx, "MATERIAL", e.target.value)} disabled={!editable} className="h-6 text-xs" />
|
||||
</td>
|
||||
<td className="px-1 py-1 border border-gray-200">
|
||||
<Input value={String(row.SPEC ?? "")} onChange={(e) => updateCell(idx, "SPEC", e.target.value)} disabled={!editable} className="h-6 text-xs" />
|
||||
</td>
|
||||
<td className="px-1 py-1 border border-gray-200">
|
||||
<Input value={String(row.POST_PROCESSING ?? "")} onChange={(e) => updateCell(idx, "POST_PROCESSING", e.target.value)} disabled={!editable} className="h-6 text-xs" />
|
||||
</td>
|
||||
<td className="px-1 py-1 border border-gray-200">
|
||||
<Input value={String(row.MAKER ?? "")} onChange={(e) => updateCell(idx, "MAKER", e.target.value)} disabled={!editable} className="h-6 text-xs" />
|
||||
</td>
|
||||
<td className="px-1 py-1 border border-gray-200">
|
||||
<Input value={String(row.MAJOR_CATEGORY ?? "")} onChange={(e) => updateCell(idx, "MAJOR_CATEGORY", e.target.value)} disabled={!editable} className="h-6 text-xs" />
|
||||
</td>
|
||||
<td className="px-1 py-1 border border-gray-200">
|
||||
<Input value={String(row.SUB_CATEGORY ?? "")} onChange={(e) => updateCell(idx, "SUB_CATEGORY", e.target.value)} disabled={!editable} className="h-6 text-xs" />
|
||||
</td>
|
||||
<td className="px-2 py-1 border border-gray-200 text-center">{String(row.REVISION ?? "")}</td>
|
||||
<td className="px-2 py-1 border border-gray-200 text-center">{String(row.EO_NO ?? "")}</td>
|
||||
<td className="px-2 py-1 border border-gray-200 text-center">{String(row.EO_DATE ?? "")}</td>
|
||||
<td className="px-1 py-1 border border-gray-200">
|
||||
<Input value={String(row.REMARK ?? "")} onChange={(e) => updateCell(idx, "REMARK", e.target.value)} disabled={!editable} className="h-6 text-xs" />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
import * as XLSX from "xlsx";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 제품관리_PART 조회 (원본: partMng/partMngList.jsp)
|
||||
export default function PartListPage() {
|
||||
const [searchPartNo, setSearchPartNo] = useState("");
|
||||
const [searchPartName, setSearchPartName] = useState("");
|
||||
const [searchRevision, setSearchRevision] = useState("0"); // 원본 JSP 기본값 = all
|
||||
const [searchMaterial, setSearchMaterial] = useState("");
|
||||
const [searchSpec, setSearchSpec] = useState("");
|
||||
const [searchPartType, setSearchPartType] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
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 openPartDetail = (objId: string) => {
|
||||
centerPopup(`/product/popup/part-form?objId=${encodeURIComponent(objId)}`, "partMngPopUp", 900, 600);
|
||||
};
|
||||
|
||||
const openFilePopup = (objId: string, docType: string, docTypeName: string) => {
|
||||
centerPopup(
|
||||
`/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`,
|
||||
"fileRegistPopUp",
|
||||
800,
|
||||
335,
|
||||
);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "품번", field: "PART_NO", width: 125, hozAlign: "left",
|
||||
cellClick: (row) => openPartDetail(String(row.OBJID)),
|
||||
},
|
||||
{
|
||||
title: "품명", field: "PART_NAME", hozAlign: "left",
|
||||
cellClick: (row) => openPartDetail(String(row.OBJID)),
|
||||
},
|
||||
{ title: "수량", field: "BOM_QTY", width: 50, hozAlign: "center" },
|
||||
{
|
||||
title: "3D", field: "CU01_CNT", width: 45, hozAlign: "center",
|
||||
formatter: (cell) => <FolderCell count={cell} />,
|
||||
cellClick: (row) => openFilePopup(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일"),
|
||||
},
|
||||
{
|
||||
title: "2D", field: "CU02_CNT", width: 45, hozAlign: "center",
|
||||
formatter: (cell) => <FolderCell count={cell} />,
|
||||
cellClick: (row) => openFilePopup(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"),
|
||||
},
|
||||
{
|
||||
title: "PDF", field: "CU03_CNT", width: 55, hozAlign: "center",
|
||||
formatter: (cell) => <FolderCell count={cell} />,
|
||||
cellClick: (row) => openFilePopup(String(row.OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일"),
|
||||
},
|
||||
{ title: "재질", field: "MATERIAL", width: 90, hozAlign: "left" },
|
||||
{ title: "사양(규격)", field: "SPEC", width: 90, hozAlign: "left" },
|
||||
{ title: "후처리", field: "POST_PROCESSING", width: 80, hozAlign: "left" },
|
||||
{ title: "MAKER", field: "MAKER", width: 80, hozAlign: "left" },
|
||||
{ title: "대분류", field: "MAJOR_CATEGORY", width: 80, hozAlign: "left" },
|
||||
{ title: "중분류", field: "SUB_CATEGORY", width: 80, 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: 80, hozAlign: "center" },
|
||||
{ title: "PART구분", field: "PART_TYPE_TITLE", width: 88, hozAlign: "center" },
|
||||
{ title: "비고", field: "REMARK", width: 80, 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: "list",
|
||||
SEARCH_PART_NO: searchPartNo,
|
||||
SEARCH_PART_NAME: searchPartName,
|
||||
SEARCH_REVISION_RELEASE: searchRevision,
|
||||
SEARCH_MATERIAL: searchMaterial,
|
||||
SEARCH_SPEC: searchSpec,
|
||||
SEARCH_PART_TYPE: searchPartType,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} catch {
|
||||
Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchPartNo, searchPartName, searchRevision, searchMaterial, searchSpec, searchPartType]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const handleExcel = () => {
|
||||
if (data.length === 0) {
|
||||
Swal.fire("알림", "다운로드할 데이터가 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const header = [
|
||||
"품번", "품명", "수량", "3D", "2D", "PDF", "재질", "사양(규격)",
|
||||
"후처리", "MAKER", "대분류", "중분류", "Revision", "EO No",
|
||||
"EO Date", "PART구분", "비고",
|
||||
];
|
||||
const body = data.map((r) => [
|
||||
r.PART_NO ?? "", r.PART_NAME ?? "", r.BOM_QTY ?? "",
|
||||
r.CU01_CNT ?? 0, r.CU02_CNT ?? 0, r.CU03_CNT ?? 0,
|
||||
r.MATERIAL ?? "", r.SPEC ?? "", r.POST_PROCESSING ?? "",
|
||||
r.MAKER ?? "", r.MAJOR_CATEGORY ?? "", r.SUB_CATEGORY ?? "",
|
||||
r.REVISION ?? "", r.EO_NO ?? "", r.EO_DATE ?? "",
|
||||
r.PART_TYPE_TITLE ?? "", r.REMARK ?? "",
|
||||
]);
|
||||
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 handleReset = () => {
|
||||
setSearchPartNo(""); setSearchPartName(""); setSearchRevision("0");
|
||||
setSearchMaterial(""); setSearchSpec(""); setSearchPartType("");
|
||||
};
|
||||
|
||||
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={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleReset}>초기화</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleExcel}>Excel Download</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="품번">
|
||||
<Input value={searchPartNo} onChange={(e) => setSearchPartNo(e.target.value)} className="w-[194px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input value={searchPartName} onChange={(e) => setSearchPartName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="Revision">
|
||||
<select
|
||||
value={searchRevision}
|
||||
onChange={(e) => setSearchRevision(e.target.value)}
|
||||
className="h-9 w-[150px] rounded border border-gray-300 bg-white px-2 text-sm"
|
||||
>
|
||||
<option value="1">current</option>
|
||||
<option value="0">all</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="재질">
|
||||
<Input value={searchMaterial} onChange={(e) => setSearchMaterial(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="사양(규격)">
|
||||
<Input value={searchSpec} onChange={(e) => setSearchSpec(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="부품 유형">
|
||||
<SearchableCodeSelect codeId="0000062" value={searchPartType} onChange={setSearchPartType} className="w-[150px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
"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 Swal from "sweetalert2";
|
||||
|
||||
// 사양관리
|
||||
export default function ProductSpecPage() {
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [specName, setSpecName] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "center",
|
||||
cellClick: (row) => {
|
||||
window.open(
|
||||
`/product/spec/detail?objId=${row.OBJID}`,
|
||||
"specDetail",
|
||||
"width=1000,height=700"
|
||||
);
|
||||
},
|
||||
},
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "사양명", field: "SPEC_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "사양값", field: "SPEC_VALUE", width: 150, hozAlign: "left" },
|
||||
{ title: "단위", field: "UNIT", width: 80, hozAlign: "center" },
|
||||
{ title: "비고", field: "REMARK", width: 200, hozAlign: "left" },
|
||||
{ title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "등록자", field: "REG_USER_NAME", width: 100, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/product/spec", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ project_no: projectNo, spec_name: specName }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} catch {
|
||||
Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error");
|
||||
}
|
||||
}, [projectNo, specName]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
const { value: form } = await Swal.fire({
|
||||
title: "사양 등록",
|
||||
html: `
|
||||
<div style="text-align:left;font-size:13px">
|
||||
<label style="display:block;margin-bottom:4px">프로젝트번호</label>
|
||||
<input id="swal-pn" class="swal2-input" style="width:100%;margin:0 0 8px" />
|
||||
<label style="display:block;margin-bottom:4px">사양명 <span style="color:red">*</span></label>
|
||||
<input id="swal-sn" class="swal2-input" style="width:100%;margin:0 0 8px" />
|
||||
<label style="display:block;margin-bottom:4px">사양값</label>
|
||||
<input id="swal-sv" class="swal2-input" style="width:100%;margin:0 0 8px" />
|
||||
<label style="display:block;margin-bottom:4px">단위</label>
|
||||
<input id="swal-un" class="swal2-input" style="width:100%;margin:0" />
|
||||
</div>`,
|
||||
showCancelButton: true, confirmButtonText: "저장", cancelButtonText: "취소",
|
||||
preConfirm: () => ({
|
||||
project_no: (document.getElementById("swal-pn") as HTMLInputElement)?.value,
|
||||
spec_name: (document.getElementById("swal-sn") as HTMLInputElement)?.value,
|
||||
spec_value: (document.getElementById("swal-sv") as HTMLInputElement)?.value,
|
||||
unit: (document.getElementById("swal-un") as HTMLInputElement)?.value,
|
||||
}),
|
||||
});
|
||||
if (!form || !form.spec_name) return;
|
||||
const res = await fetch("/api/product/spec/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ actionType: "regist", ...form }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else Swal.fire("오류", json.message || "저장 실패", "error");
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
Swal.fire("알림", "항목을 선택한 뒤 다시 시도하세요 (리스트 삭제는 별도 구현 예정)", "info");
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">사양관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleAdd}>등록</Button>
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="사양명">
|
||||
<Input value={specName} onChange={(e) => setSpecName(e.target.value)} className="w-[200px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} showCheckbox />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
|
||||
// productionplanning/inspectionMgmtList.jsp 대응 - 생산관리_검사관리
|
||||
function openPopup(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},scrollbars=yes,resizable=yes`);
|
||||
}
|
||||
|
||||
export default function ProductionInspectionPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selected, setSelected] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openProjectForm = (objId: string) =>
|
||||
openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650);
|
||||
const openInspection = (objId: string) =>
|
||||
openPopup(`/production/inspection-popup?objId=${encodeURIComponent(objId)}`, "inspection", 1300, 700);
|
||||
const openFiles = (targetObjId: string, docType: string, docTypeName: string) =>
|
||||
openPopup(
|
||||
`/common/files?targetObjId=${encodeURIComponent(targetObjId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`,
|
||||
"filePopup", 800, 500
|
||||
);
|
||||
|
||||
const handleRegister = () => {
|
||||
if (selected.length === 0) {
|
||||
Swal.fire({ icon: "warning", title: "선택된 내용이 없습니다." });
|
||||
return;
|
||||
}
|
||||
if (selected.length > 1) {
|
||||
Swal.fire({ icon: "warning", title: "한번에 1개의 내용만 등록 가능합니다." });
|
||||
return;
|
||||
}
|
||||
openInspection(String(selected[0].OBJID));
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true,
|
||||
cellClick: (row) => openProjectForm(String(row.OBJID)) },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 130, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 110, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "검사결과",
|
||||
columns: [
|
||||
{ title: "체크리스트", field: "INSPECTION_CNT", width: 110, hozAlign: "center",
|
||||
formatter: (v) => <FolderCell count={v} />,
|
||||
cellClick: (row) => openInspection(String(row.OBJID)) },
|
||||
{ title: "입회검사", field: "ADMISSION_INSPECTION_CNT", width: 110, hozAlign: "center",
|
||||
formatter: (v) => <FolderCell count={v} />,
|
||||
cellClick: (row) => openFiles(String(row.OBJID), "ADMISSION_INSPECTION_FILE", "입회검사") },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production/inspection", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
(window as unknown as { fn_search?: () => void }).fn_search = fetchData;
|
||||
return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; };
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리_검사관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleRegister}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// productionplanning/issuemgmtList.jsp 대응 - 생산관리_이슈관리
|
||||
function openPopup(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},scrollbars=yes,resizable=yes`);
|
||||
}
|
||||
|
||||
export default function ProductionIssuePage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [issueCategory, setIssueCategory] = useState("");
|
||||
const [issueType, setIssueType] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selected, setSelected] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openProjectForm = (objId: string) =>
|
||||
openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650);
|
||||
const openIssueForm = (objId: string) =>
|
||||
openPopup(`/production/issue/form?objId=${encodeURIComponent(objId)}`, "issueForm", 1100, 800);
|
||||
|
||||
const handleRegister = () => openIssueForm("");
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production/issue", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year, project_no: projectNo, unit_code: unitCode,
|
||||
issue_category: issueCategory, issue_type: issueType,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, unitCode, issueCategory, issueType]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
(window as unknown as { fn_search?: () => void }).fn_search = fetchData;
|
||||
return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; };
|
||||
}, [fetchData]);
|
||||
|
||||
const handleAction = async (action: "delete" | "release") => {
|
||||
if (selected.length === 0) {
|
||||
Swal.fire({ icon: "warning", title: "선택된 대상이 없습니다." });
|
||||
return;
|
||||
}
|
||||
const targets = selected.filter((r) => String(r.STATUS || "") === "write");
|
||||
if (targets.length === 0) {
|
||||
Swal.fire({ icon: "warning", title: "등록중인 데이터만 처리 가능합니다." });
|
||||
return;
|
||||
}
|
||||
const title = action === "delete" ? "선택된 데이터를 삭제하시겠습니까?" : "선택된 데이터를 배포하시겠습니까?";
|
||||
const r = await Swal.fire({
|
||||
title, icon: "warning",
|
||||
showCancelButton: true, confirmButtonText: "확인", cancelButtonText: "취소",
|
||||
});
|
||||
if (!r.isConfirmed) return;
|
||||
|
||||
const objIds = targets.map((t) => String(t.OBJID));
|
||||
const res = await fetch("/api/production/issue/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ actionType: action, objIds }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) {
|
||||
await Swal.fire({ icon: "success", title: j.message || "완료", timer: 1000, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", j.message || "처리 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "이슈번호", field: "ISSUE_NO", width: 110, hozAlign: "center",
|
||||
cellClick: (row) => openIssueForm(String(row.OBJID)) },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "center",
|
||||
cellClick: (row) => openProjectForm(String(row.PROJECT_OBJID)) },
|
||||
{ title: "유닛명", field: "UNIT_CODE_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "품번", field: "PART_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "품명", field: "PART_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "이슈구분", field: "ISSUE_CATEGORY_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "이슈유형", field: "ISSUE_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "이슈내용", field: "CONTENT", width: 220, hozAlign: "left" },
|
||||
{ title: "등록일", field: "REG_DATE_TEXT", width: 100, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "설계담당자", field: "DESIGN_USERID_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "구매담당자", field: "PURCHASE_USERID_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "품질담당자", field: "QUALITY_USERID_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "생산담당자", field: "PRODUCTION_USERID_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "조치결과", field: "DESIGN_RESULT_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "조치일", field: "DESIGN_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리_이슈관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="danger" onClick={() => handleAction("delete")}>삭제</Button>
|
||||
<Button size="sm" onClick={() => handleAction("release")}>배포</Button>
|
||||
<Button size="sm" onClick={handleRegister}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<Input value={unitCode} onChange={(e) => setUnitCode(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="이슈구분">
|
||||
<CodeSelect codeId="ISSUE_CATEGORY" value={issueCategory} onChange={setIssueCategory} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="이슈유형">
|
||||
<CodeSelect codeId={issueCategory || "ISSUE_TYPE"} value={issueType} onChange={setIssueType} className="w-[140px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
"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";
|
||||
|
||||
// ProductionMng/ProdMgmList.jsp 대응 - 생산관리
|
||||
export default function ProductionPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120 },
|
||||
{ title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/sales/contract/form?objId=${row.OBJID}`, "prodDetail", "width=1200,height=800") },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "생산수량", field: "PROD_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "완료수량", field: "COMPLETE_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "진행율", field: "PROGRESS_RATE", width: 80, hozAlign: "center",
|
||||
formatter: (_cell, row) => `${row.PROGRESS_RATE || 0}%` },
|
||||
{ title: "시작일", field: "START_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "종료일", field: "END_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
|
||||
// productionplanning/planningList.jsp 대응 - 생산관리_생산계획수립
|
||||
function openPopup(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},scrollbars=yes,resizable=yes`);
|
||||
}
|
||||
|
||||
export default function ProductionPlanningPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openProjectForm = (objId: string) =>
|
||||
openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650);
|
||||
const openAssemblyWbs = (objId: string) =>
|
||||
openPopup(`/project/wbs-task?objId=${encodeURIComponent(objId)}`, "wbsTask", 900, 800);
|
||||
const openSetupWbs = (objId: string) =>
|
||||
openPopup(`/project/wbs-setup?objId=${encodeURIComponent(objId)}`, "wbsSetup", 1100, 750);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true,
|
||||
cellClick: (row) => openProjectForm(String(row.OBJID)) },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 110, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 110, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "조립(▤)",
|
||||
columns: [
|
||||
{ title: "WBS", field: "WBS_CNT", width: 70, hozAlign: "center",
|
||||
formatter: (v) => <FolderCell count={v} />,
|
||||
cellClick: (row) => openAssemblyWbs(String(row.OBJID)) },
|
||||
{
|
||||
title: "계획",
|
||||
columns: [
|
||||
{ title: "시작일", field: "PRODUCE_PLAN_START", width: 100, hozAlign: "center" },
|
||||
{ title: "종료일", field: "PRODUCE_PLAN_END", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "실적",
|
||||
columns: [
|
||||
{ title: "시작일", field: "PRODUCE_ACT_START", width: 100, hozAlign: "center" },
|
||||
{ title: "종료일", field: "PRODUCE_ACT_END", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "셋업(▤)",
|
||||
columns: [
|
||||
{ title: "WBS", field: "SETUP_WBS_CNT", width: 70, hozAlign: "center",
|
||||
formatter: (v) => <FolderCell count={v} />,
|
||||
cellClick: (row) => openSetupWbs(String(row.OBJID)) },
|
||||
{
|
||||
title: "계획",
|
||||
columns: [
|
||||
{ title: "시작일", field: "SETUP_PLAN_START", width: 100, hozAlign: "center" },
|
||||
{ title: "종료일", field: "SETUP_PLAN_END", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "실적",
|
||||
columns: [
|
||||
{ title: "시작일", field: "SETUP_ACT_START", width: 100, hozAlign: "center" },
|
||||
{ title: "종료일", field: "SETUP_ACT_END", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production/planning", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리_생산계획수립</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// productionplanning/processperformanceList.jsp 대응 - 생산관리_공정실적관리
|
||||
function openPopup(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},scrollbars=yes,resizable=yes`);
|
||||
}
|
||||
|
||||
export default function ProductionProcessPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openProjectForm = (objId: string) =>
|
||||
openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650);
|
||||
const openAssemblyPopup = (row: Record<string, unknown>) =>
|
||||
openPopup(
|
||||
`/production/assembly-popup?objId=${encodeURIComponent(String(row.OBJID || ""))}`,
|
||||
"assemblyList", 1300, 800
|
||||
);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true,
|
||||
cellClick: (row) => openProjectForm(String(row.CONTRACT_OBJID)) },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "DUE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 110, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "조립(제작)",
|
||||
columns: [
|
||||
{
|
||||
title: "E-BOM/구매 BOM",
|
||||
columns: [
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 270, hozAlign: "left" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "생산BOM",
|
||||
columns: [
|
||||
{ title: "조립총수", field: "BOM_CNT", width: 90, hozAlign: "right", formatter: "money",
|
||||
cellClick: (row) => openAssemblyPopup(row) },
|
||||
{ title: "조립품수", field: "ASSING_CNT", width: 90, hozAlign: "right", formatter: "money",
|
||||
cellClick: (row) => openAssemblyPopup(row) },
|
||||
{ title: "공정율(%)", field: "AS_RATE", width: 90, hozAlign: "right" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "작업공수(H)",
|
||||
columns: [
|
||||
{ title: "투입공수", field: "TOTAL_SUM", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "자사", field: "INSOURCING_SUM", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "외주", field: "OUTSOURCING_SUM", width: 80, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production/process", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리_공정실적관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
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 { CodeSelect } from "@/components/ui/code-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
|
||||
// productionplanning/releaseMgmtList.jsp 대응 - 생산관리_출고관리
|
||||
function openPopup(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},scrollbars=yes,resizable=yes`);
|
||||
}
|
||||
|
||||
export default function ProductionReleasePage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selected, setSelected] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openProjectForm = (objId: string) =>
|
||||
openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650);
|
||||
const openReleaseForm = (row: Record<string, unknown>) => {
|
||||
const rObj = row.RELEASE_OBJID ? String(row.RELEASE_OBJID) : "";
|
||||
const qs = new URLSearchParams({
|
||||
parentObjId: String(row.OBJID || ""),
|
||||
product: String(row.PRODUCT || ""),
|
||||
productGroup: String(row.PRODUCT_GROUP || ""),
|
||||
...(rObj ? { objId: rObj } : {}),
|
||||
}).toString();
|
||||
openPopup(`/production/release/form?${qs}`, "releaseForm", 900, 500);
|
||||
};
|
||||
const openInspection = (objId: string) =>
|
||||
openPopup(`/production/inspection-popup?objId=${encodeURIComponent(objId)}`, "inspection", 1300, 700);
|
||||
const openFiles = (targetObjId: string, docType: string, docTypeName: string) =>
|
||||
openPopup(
|
||||
`/common/files?targetObjId=${encodeURIComponent(targetObjId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`,
|
||||
"filePopup", 800, 500
|
||||
);
|
||||
|
||||
const handleRegister = () => {
|
||||
if (selected.length === 0) {
|
||||
Swal.fire({ icon: "warning", title: "선택된 내용이 없습니다." });
|
||||
return;
|
||||
}
|
||||
if (selected.length > 1) {
|
||||
Swal.fire({ icon: "warning", title: "한번에 1개의 내용만 등록 가능합니다." });
|
||||
return;
|
||||
}
|
||||
openReleaseForm(selected[0]);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true,
|
||||
cellClick: (row) => openProjectForm(String(row.OBJID)) },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 230, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 130, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "출고관리",
|
||||
columns: [
|
||||
{
|
||||
title: "검사결과",
|
||||
columns: [
|
||||
{ title: "체크리스트", field: "INSPECTION_CNT", width: 100, hozAlign: "center",
|
||||
formatter: (v) => <FolderCell count={v} />,
|
||||
cellClick: (row) => openInspection(String(row.OBJID)) },
|
||||
{ title: "입회검사", field: "ADMISSION_INSPECTION_CNT", width: 100, hozAlign: "center",
|
||||
formatter: (v) => <FolderCell count={v} />,
|
||||
cellClick: (row) => openFiles(String(row.OBJID), "ADMISSION_INSPECTION_FILE", "입회검사") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "출고내역",
|
||||
columns: [
|
||||
{ title: "출하지시서", field: "RELEASE_ORDER_CNT", width: 100, hozAlign: "center",
|
||||
formatter: (v) => <FolderCell count={v} />,
|
||||
cellClick: (row) => {
|
||||
const rObj = row.RELEASE_OBJID ? String(row.RELEASE_OBJID) : "";
|
||||
if (!rObj) {
|
||||
Swal.fire({ icon: "info", title: "출고 등록 후 첨부 가능합니다." });
|
||||
return;
|
||||
}
|
||||
openFiles(rObj, "RELEASE_ORDER", "출하지시서");
|
||||
} },
|
||||
{ title: "출고일", field: "RELEASE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "담당자", field: "RELEASE_WRITER", width: 100, hozAlign: "center" },
|
||||
{ title: "상태", field: "RELEASE_STATUS_TITLE", width: 90, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production/release", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
(window as unknown as { fn_search?: () => void }).fn_search = fetchData;
|
||||
return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; };
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리_출고관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleRegister}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// productionplanning/setupmgmtList.jsp 대응 - 생산관리_셋업관리
|
||||
function openPopup(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},scrollbars=yes,resizable=yes`);
|
||||
}
|
||||
|
||||
export default function ProductionSetupPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openProjectForm = (objId: string) =>
|
||||
openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650);
|
||||
const openSetupWbs = (objId: string) =>
|
||||
openPopup(`/project/wbs-setup?objId=${encodeURIComponent(objId)}`, "wbsSetup", 1200, 800);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true,
|
||||
cellClick: (row) => openProjectForm(String(row.OBJID)) },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 240, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 110, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "셋업",
|
||||
columns: [
|
||||
{
|
||||
title: "셋업WBS",
|
||||
columns: [
|
||||
{ title: "TASK총수", field: "TASK_CNT", width: 90, hozAlign: "right", formatter: "money",
|
||||
cellClick: (row) => openSetupWbs(String(row.OBJID)) },
|
||||
{ title: "완료TASK", field: "COMPLETE_CNT", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "공정율(%)", field: "SETUP_RATE", width: 90, hozAlign: "right" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "투입인원(명)",
|
||||
columns: [
|
||||
{ title: "자사", field: "EMPLOYEES_IN", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "외주", field: "EMPLOYEES_OUT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "(계)", field: "EMPLOYEES_TOTAL", width: 80, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production/setup", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리_셋업관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// productionplanning/planningdashboard.jsp 대응 - 생산관리_현황
|
||||
function openPopup(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},scrollbars=yes,resizable=yes`);
|
||||
}
|
||||
|
||||
export default function ProductionStatusPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openProjectForm = (objId: string) =>
|
||||
openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650);
|
||||
const openAssemblyWbs = (objId: string) =>
|
||||
openPopup(`/project/wbs-task?objId=${encodeURIComponent(objId)}`, "wbsTask", 900, 800);
|
||||
const openSetupWbs = (objId: string) =>
|
||||
openPopup(`/project/wbs-setup?objId=${encodeURIComponent(objId)}`, "wbsSetup", 1100, 750);
|
||||
const openIssueList = (objId: string, status: string) =>
|
||||
openPopup(
|
||||
`/production/issue-popup?status=${encodeURIComponent(status)}&project_no=${encodeURIComponent(objId)}`,
|
||||
"issueList", 1720, 900
|
||||
);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트정보",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true,
|
||||
cellClick: (row) => openProjectForm(String(row.OBJID)) },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 130, hozAlign: "left" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 260, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 130, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 90, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "진척관리",
|
||||
columns: [
|
||||
{
|
||||
title: "조립(제작)",
|
||||
columns: [
|
||||
{ title: "공정율(%)", field: "ASSEMBLY_RATE", width: 90, hozAlign: "center",
|
||||
cellClick: (row) => openAssemblyWbs(String(row.OBJID)) },
|
||||
{ title: "종료일", field: "ASSEMBLY_DATE_END", width: 100, hozAlign: "center" },
|
||||
{ title: "투입공수(H)", field: "ASSEMBLY_EMPLOYEES_TOTAL", width: 100, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "셋업",
|
||||
columns: [
|
||||
{ title: "공정율(%)", field: "SETUP_RATE", width: 90, hozAlign: "center",
|
||||
cellClick: (row) => openSetupWbs(String(row.OBJID)) },
|
||||
{ title: "종료일", field: "SETUP_ACT_END", width: 100, hozAlign: "center" },
|
||||
{ title: "투입공수(H)", field: "EMPLOYEES_TOTAL", width: 100, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "이슈관리",
|
||||
columns: [
|
||||
{ title: "조치율(%)", field: "ISSUE_RATE", width: 90, hozAlign: "center" },
|
||||
{ title: "발생", field: "ISSUE_CNT", width: 80, hozAlign: "center",
|
||||
cellClick: (row) => openIssueList(String(row.OBJID), "all") },
|
||||
{ title: "조치", field: "COMP_CNT", width: 80, hozAlign: "center",
|
||||
cellClick: (row) => openIssueList(String(row.OBJID), "complete") },
|
||||
{ title: "미결", field: "MISS_CNT", width: 80, hozAlign: "center",
|
||||
cellClick: (row) => openIssueList(String(row.OBJID), "late") },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/production/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, customerObjid, product, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">생산관리_현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<CodeSelect codeId="CUSTOMER" value={customerObjid} onChange={setCustomerObjid} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<CodeSelect codeId="PRODUCT_TYPE" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<CodeSelect codeId="PM_USER" value={pmUserId} onChange={setPmUserId} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
"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";
|
||||
|
||||
// project/projectList.jsp 대응 - 프로젝트관리
|
||||
export default function ProjectPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 130,
|
||||
cellClick: (row) => window.open(`/sales/contract/form?objId=${row.OBJID}`, "projectDetail", "width=1200,height=900") },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "제품명", field: "PRODUCT_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "제조공장", field: "MANUFACTURE_PLANT_NAME", width: 120, hozAlign: "center" },
|
||||
{ title: "상태", field: "STATUS", width: 80, hozAlign: "center" },
|
||||
{ title: "진행율", field: "TOTAL_RATE", width: 80, hozAlign: "center",
|
||||
formatter: (_cell, row) => `${row.TOTAL_RATE || 0}%` },
|
||||
{ title: "PM", field: "PM_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "시작일", field: "START_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "종료일", field: "END_DATE", width: 100, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/project", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, project_no: projectNo, project_name: projectName }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, projectNo, projectName]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">프로젝트관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트명">
|
||||
<Input value={projectName} onChange={(e) => setProjectName(e.target.value)} className="w-[200px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 프로젝트관리_일정관리(WBS) (원본: project/projectMgmtWbsList.jsp)
|
||||
export default function ProjectProgressPage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState(String(currentYear));
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [categoryCd, setCategoryCd] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [contractStartDate, setContractStartDate] = useState("");
|
||||
const [contractEndDate, setContractEndDate] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [setup, setSetup] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const [projectNoOpts, setProjectNoOpts] = useState<{ value: string; label: string }[]>([]);
|
||||
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
|
||||
const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/sales/contract/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setProjectNoOpts((d.rows || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.CODE ?? r.code ?? ""),
|
||||
label: String(r.NAME ?? r.name ?? ""),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setCustomers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setPmUsers((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 openProjectModify = (objId: string) =>
|
||||
centerPopup(`/project/modify?objId=${objId}&actionType=edit`, "projectModifyPopUp", 500, 400);
|
||||
const openWbsPopUp = (objId: string, categoryCd: string) =>
|
||||
centerPopup(`/project/wbs-task?OBJID=${objId}&CATEGORY_CD=${categoryCd}`, "wbsTaskPopUp", 1700, 800);
|
||||
const openSetupWbsPopUp = (objId: string) =>
|
||||
centerPopup(`/project/wbs-setup?OBJID=${objId}`, "setupWbsPopUp", 1100, 750);
|
||||
|
||||
// 카테고리별 클릭 팝업 — 설계/구매/제작은 wbs, 셋업은 setup_wbs
|
||||
const wbsClick = (row: Record<string, unknown>) =>
|
||||
openWbsPopUp(String(row.OBJID), String(row.CATEGORY_CD ?? ""));
|
||||
const setupClick = (row: Record<string, unknown>) => openSetupWbsPopUp(String(row.OBJID));
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트번호", field: "PROJECT_NO", width: 100, frozen: true, hozAlign: "left",
|
||||
cellClick: (row) => openProjectModify(String(row.OBJID)),
|
||||
},
|
||||
{
|
||||
title: "프로젝트정보", headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "계약구분", field: "CATEGORY_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "center" },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 120, hozAlign: "left" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 160, hozAlign: "left" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 90, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "설계 및 구매", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "설계관리", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "공정율(%)", field: "DESIGN_RATETOTAL", width: 90, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.DESIGN_RATETOTAL ?? 0}%`,
|
||||
cellClick: wbsClick,
|
||||
},
|
||||
{ title: "🟢 완료", field: "DESIGN_COMP_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🟡 지연완료", field: "DESIGN_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🔵 진행중", field: "DESIGN_ING_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🔴 지연", field: "DESIGN_LATE_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "구매관리", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "공정율(%)", field: "PURCHASE_RATETOTAL", width: 90, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.PURCHASE_RATETOTAL ?? 0}%`,
|
||||
cellClick: wbsClick,
|
||||
},
|
||||
{ title: "🟢 완료", field: "PURCHASE_COMP_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🟡 지연완료", field: "PURCHASE_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🔵 진행중", field: "PURCHASE_ING_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🔴 지연", field: "PURCHASE_LATE_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "설비조립 및 셋업", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "조립(제작)관리", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "공정율(%)", field: "PRODUCE_RATETOTAL", width: 90, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.PRODUCE_RATETOTAL ?? 0}%`,
|
||||
cellClick: wbsClick,
|
||||
},
|
||||
{ title: "🟢 완료", field: "PRODUCE_COMP_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🟡 지연완료", field: "PRODUCE_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🔵 진행중", field: "PRODUCE_ING_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
{ title: "🔴 지연", field: "PRODUCE_LATE_CNT", width: 80, hozAlign: "center", cellClick: wbsClick },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "셋업관리", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "공정율(%)", field: "SETUP_RATETOTAL", width: 90, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.SETUP_RATETOTAL ?? 0}%`,
|
||||
cellClick: setupClick,
|
||||
},
|
||||
{ title: "완료", field: "SETUP_COMP_CNT", width: 80, hozAlign: "center", cellClick: setupClick },
|
||||
{ title: "지연완료", field: "SETUP_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: setupClick },
|
||||
{ title: "진행중", field: "SETUP_ING_CNT", width: 80, hozAlign: "center", cellClick: setupClick },
|
||||
{ title: "지연", field: "SETUP_LATE_CNT", width: 80, hozAlign: "center", cellClick: setupClick },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/project/progress", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
project_no: projectNo,
|
||||
category_cd: categoryCd,
|
||||
customer_objid: customerObjid,
|
||||
product,
|
||||
contract_start_date: contractStartDate,
|
||||
contract_end_date: contractEndDate,
|
||||
location,
|
||||
setup,
|
||||
pm_user_id: pmUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} catch {
|
||||
Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error");
|
||||
}
|
||||
}, [year, projectNo, categoryCd, customerObjid, product, contractStartDate, contractEndDate, location, setup, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">프로젝트관리_일정관리(WBS)</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] 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="프로젝트번호">
|
||||
<SearchableSelect options={projectNoOpts} value={projectNo} onChange={setProjectNo} className="w-[240px]" />
|
||||
</SearchField>
|
||||
<SearchField label="계약구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={categoryCd} onChange={setCategoryCd} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<SearchableSelect options={customers} value={customerObjid} onChange={setCustomerObjid} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="예상납기일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={contractStartDate} onChange={(e) => setContractStartDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-xs text-gray-500">~</span>
|
||||
<Input type="date" value={contractEndDate} onChange={(e) => setContractEndDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="입고지">
|
||||
<Input value={location} onChange={(e) => setLocation(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="셋업지">
|
||||
<Input value={setup} onChange={(e) => setSetup(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<SearchableSelect options={pmUsers} value={pmUserId} onChange={setPmUserId} className="w-[160px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 프로젝트관리_진행관리 (원본: project/projectMgmtList.jsp)
|
||||
export default function ProjectStatusPage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState(String(currentYear));
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [categoryCd, setCategoryCd] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [contractStartDate, setContractStartDate] = useState("");
|
||||
const [contractEndDate, setContractEndDate] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [setup, setSetup] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const [projectNoOpts, setProjectNoOpts] = useState<{ value: string; label: string }[]>([]);
|
||||
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
|
||||
const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/sales/contract/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setProjectNoOpts((d.rows || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.CODE ?? r.code ?? ""),
|
||||
label: String(r.NAME ?? r.name ?? ""),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setCustomers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setPmUsers((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`);
|
||||
};
|
||||
|
||||
// 원본: openProjectFormPopUp → /project/projectmodifyPopUp.do?OBJID=xxx (420x350)
|
||||
const openProjectModify = (objId: string) => centerPopup(`/project/modify?objId=${objId}`, "projectModifyPopUp", 500, 400);
|
||||
// 원본: openIssueFormPopUp(status,projectobjid) → /productionplanning/issuemgmtList.do (1720x900)
|
||||
const openIssuePopUp = (status: string, objId: string) =>
|
||||
centerPopup(`/production/issue-popup?status=${status}&project_no=${objId}`, "issueListPopUp", 1720, 900);
|
||||
// 원본: fn_openInvestmentCostPricePopUp
|
||||
const openInvestCost = (projectObjId: string, productObjId: string, milestoneObjId: string) =>
|
||||
centerPopup(
|
||||
`/project/invest-cost?projectObjId=${projectObjId}&productObjId=${productObjId}&milestoneObjId=${milestoneObjId}`,
|
||||
"investCostPopUp", 1200, 800,
|
||||
);
|
||||
// 원본: wbs_popup
|
||||
const openWbsPopUp = (objId: string, categoryCd: string) =>
|
||||
centerPopup(`/project/wbs-task?OBJID=${objId}&CATEGORY_CD=${categoryCd}&actionType=view`, "wbsTaskPopUp", 1700, 800);
|
||||
// 원본: setup_wbs_popup
|
||||
const openSetupWbsPopUp = (objId: string) =>
|
||||
centerPopup(`/project/wbs-setup?OBJID=${objId}`, "setupWbsPopUp", 1100, 750);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트번호", field: "PROJECT_NO", width: 100, frozen: true, hozAlign: "left",
|
||||
cellClick: (row) => openProjectModify(String(row.OBJID)),
|
||||
},
|
||||
{
|
||||
title: "프로젝트정보", headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "계약구분", field: "CATEGORY_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "center" },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "고객납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "셋업지", field: "SETUP", width: 100, hozAlign: "center" },
|
||||
{ title: "설비방향", field: "FACILITY_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "예상납기일", field: "CONTRACT_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "현황", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "이슈(건수)", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "발생", field: "ISSUE_CNT", width: 60, hozAlign: "center",
|
||||
cellClick: (row) => openIssuePopUp("all", String(row.OBJID)),
|
||||
},
|
||||
{
|
||||
title: "조치", field: "COMP_CNT", width: 60, hozAlign: "center",
|
||||
cellClick: (row) => openIssuePopUp("complete", String(row.OBJID)),
|
||||
},
|
||||
{
|
||||
title: "미결", field: "MISS_CNT", width: 60, hozAlign: "center",
|
||||
cellClick: (row) => openIssuePopUp("late", String(row.OBJID)),
|
||||
},
|
||||
{
|
||||
title: "조치율", field: "ISSUE_RATE", width: 70, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.ISSUE_RATE ?? 0}%`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "투입원가", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "목표가", field: "TOTAL_COST_GOAL", width: 110, hozAlign: "right", formatter: "money",
|
||||
cellClick: (row) => openInvestCost(
|
||||
String(row.OBJID), String(row.PROD_REL_OBJID ?? ""), String(row.MILESTONE_OBJID ?? ""),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "투입금액", field: "TOTAL_COST_ACTUAL", width: 110, hozAlign: "right", formatter: "money",
|
||||
cellClick: (row) => openInvestCost(
|
||||
String(row.OBJID), String(row.PROD_REL_OBJID ?? ""), String(row.MILESTONE_OBJID ?? ""),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "투입율(%)", field: "TOTAL_INPUT_RATE", width: 80, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.TOTAL_INPUT_RATE ?? 0}%`,
|
||||
cellClick: (row) => openInvestCost(
|
||||
String(row.OBJID), String(row.PROD_REL_OBJID ?? ""), String(row.MILESTONE_OBJID ?? ""),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "진척율(%)", headerHozAlign: "center",
|
||||
columns: [
|
||||
{
|
||||
title: "전체공정", field: "TOTAL_RATE", width: 80, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.TOTAL_RATE ?? 0}%`,
|
||||
cellClick: (row) => openWbsPopUp(String(row.OBJID), String(row.CATEGORY_CD ?? "")),
|
||||
},
|
||||
{
|
||||
title: "셋업", field: "SETUP_RATE", width: 70, hozAlign: "center",
|
||||
formatter: (_c, row) => `${row.SETUP_RATE ?? 0}%`,
|
||||
cellClick: (row) => openSetupWbsPopUp(String(row.OBJID)),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "출고", headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "출고여부", field: "RELEASE_STATUS_TITLE", width: 80, hozAlign: "center" },
|
||||
{ title: "출고일", field: "RELEASE_DATE", width: 100, hozAlign: "center" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/project/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
project_no: projectNo,
|
||||
category_cd: categoryCd,
|
||||
customer_objid: customerObjid,
|
||||
product,
|
||||
contract_start_date: contractStartDate,
|
||||
contract_end_date: contractEndDate,
|
||||
location,
|
||||
setup,
|
||||
pm_user_id: pmUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
} catch {
|
||||
Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error");
|
||||
}
|
||||
}, [year, projectNo, categoryCd, customerObjid, product, contractStartDate, contractEndDate, location, setup, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">프로젝트관리_진행관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] 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="프로젝트번호">
|
||||
<SearchableSelect options={projectNoOpts} value={projectNo} onChange={setProjectNo} className="w-[240px]" />
|
||||
</SearchField>
|
||||
<SearchField label="계약구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={categoryCd} onChange={setCategoryCd} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<SearchableSelect options={customers} value={customerObjid} onChange={setCustomerObjid} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="예상납기일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={contractStartDate} onChange={(e) => setContractStartDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-xs text-gray-500">~</span>
|
||||
<Input type="date" value={contractEndDate} onChange={(e) => setContractEndDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="입고지">
|
||||
<Input value={location} onChange={(e) => setLocation(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="셋업지">
|
||||
<Input value={setup} onChange={(e) => setSetup(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<SearchableSelect options={pmUsers} value={pmUserId} onChange={setPmUserId} className="w-[160px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 프로젝트종합 (원본: project/projectMgmtTotalList.jsp)
|
||||
export default function ProjectTotalPage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState(String(currentYear));
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [categoryCd, setCategoryCd] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [contractStartDate, setContractStartDate] = useState("");
|
||||
const [contractEndDate, setContractEndDate] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [setup, setSetup] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [rows, setRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedObjId, setSelectedObjId] = useState<string>("");
|
||||
|
||||
const [projectNoOpts, setProjectNoOpts] = useState<{ value: string; label: string }[]>([]);
|
||||
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
|
||||
const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
// Gantt 윈도우: 현재년도 기준 3년(이전/당해/다음) × 12개월
|
||||
const ganttYear = parseInt(year, 10) || currentYear;
|
||||
const ganttStart = new Date(ganttYear - 1, 0, 1);
|
||||
const ganttEnd = new Date(ganttYear + 1, 11, 31);
|
||||
const ganttMonths = useMemo(() => {
|
||||
const list: { y: number; m: number }[] = [];
|
||||
for (let y = ganttYear - 1; y <= ganttYear + 1; y++) {
|
||||
for (let m = 0; m < 12; m++) list.push({ y, m });
|
||||
}
|
||||
return list;
|
||||
}, [ganttYear]);
|
||||
const ganttTotalDays = (ganttEnd.getTime() - ganttStart.getTime()) / 86400000 + 1;
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/sales/contract/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setProjectNoOpts((d.rows || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.CODE ?? r.code ?? ""),
|
||||
label: String(r.NAME ?? r.name ?? ""),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setCustomers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""),
|
||||
}))))
|
||||
.catch(() => {});
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => setPmUsers((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 openProjectModify = (objId: string) => centerPopup(`/project/modify?objId=${objId}`, "projectModifyPopUp", 500, 400);
|
||||
const openWbsPopUp = (objId: string, cat: string) =>
|
||||
centerPopup(`/project/wbs-task?OBJID=${objId}&CATEGORY_CD=${cat}`, "wbsTaskPopUp", 1700, 800);
|
||||
const openGanttPopUp = (objId: string) =>
|
||||
centerPopup(`/project/wbs-gantt?OBJID=${objId}`, "ganttPopUp", 1800, 800);
|
||||
|
||||
const selectedRow = useMemo(
|
||||
() => rows.find((r) => String(r.OBJID) === selectedObjId),
|
||||
[rows, selectedObjId]
|
||||
);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/project/total", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
project_no: projectNo,
|
||||
category_cd: categoryCd,
|
||||
customer_objid: customerObjid,
|
||||
product,
|
||||
contract_start_date: contractStartDate,
|
||||
contract_end_date: contractEndDate,
|
||||
location,
|
||||
setup,
|
||||
pm_user_id: pmUserId,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
const list = (json.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setRows(list);
|
||||
setSelectedObjId(list.length > 0 ? String(list[0].OBJID) : "");
|
||||
}
|
||||
} catch {
|
||||
Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error");
|
||||
}
|
||||
}, [year, projectNo, categoryCd, customerObjid, product, contractStartDate, contractEndDate, location, setup, pmUserId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
// YYYY-MM-DD 문자열 → 윈도우 내 % (left, width)
|
||||
const toPct = (start?: unknown, end?: unknown) => {
|
||||
const s = String(start ?? "").slice(0, 10);
|
||||
const e = String(end ?? "").slice(0, 10);
|
||||
if (!s && !e) return null;
|
||||
const sDate = s ? new Date(s) : (e ? new Date(e) : null);
|
||||
const eDate = e ? new Date(e) : (s ? new Date(s) : null);
|
||||
if (!sDate || !eDate || isNaN(sDate.getTime()) || isNaN(eDate.getTime())) return null;
|
||||
if (eDate < ganttStart || sDate > ganttEnd) return null;
|
||||
const sClamp = sDate < ganttStart ? ganttStart : sDate;
|
||||
const eClamp = eDate > ganttEnd ? ganttEnd : eDate;
|
||||
const left = ((sClamp.getTime() - ganttStart.getTime()) / 86400000) / ganttTotalDays * 100;
|
||||
const width = Math.max(0.3, ((eClamp.getTime() - sClamp.getTime()) / 86400000 + 1) / ganttTotalDays * 100);
|
||||
return { left, width };
|
||||
};
|
||||
|
||||
const todayPct = (() => {
|
||||
const today = new Date();
|
||||
if (today < ganttStart || today > ganttEnd) return null;
|
||||
return ((today.getTime() - ganttStart.getTime()) / 86400000) / ganttTotalDays * 100;
|
||||
})();
|
||||
|
||||
// 진척율 포맷
|
||||
const fmtRate = (v: unknown) => (v == null || v === "" ? 0 : Number(v));
|
||||
|
||||
// 상태 계산: 지연 건수 합 > 0 이면 빨강
|
||||
const isLate = (r: Record<string, unknown>) =>
|
||||
[r.DESIGN_LATE_CNT, r.PURCHASE_LATE_CNT, r.PRODUCE_LATE_CNT, r.SETUP_LATE_CNT]
|
||||
.some((v) => Number(v ?? 0) > 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-4">프로젝트종합</h2>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] 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="프로젝트번호">
|
||||
<SearchableSelect options={projectNoOpts} value={projectNo} onChange={setProjectNo} className="w-[240px]" />
|
||||
</SearchField>
|
||||
<SearchField label="계약구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={categoryCd} onChange={setCategoryCd} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<SearchableSelect options={customers} value={customerObjid} onChange={setCustomerObjid} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="예상납기일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={contractStartDate} onChange={(e) => setContractStartDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-xs text-gray-500">~</span>
|
||||
<Input type="date" value={contractEndDate} onChange={(e) => setContractEndDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="입고지">
|
||||
<Input value={location} onChange={(e) => setLocation(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="셋업지">
|
||||
<Input value={setup} onChange={(e) => setSetup(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<SearchableSelect options={pmUsers} value={pmUserId} onChange={setPmUserId} className="w-[160px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<span className="font-semibold text-gray-600">범례:</span>
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: "#3b82f6" }} />설계</span>
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: "#22c55e" }} />구매</span>
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: "#1f2937" }} />조립</span>
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-3 h-3 rotate-45" style={{ backgroundColor: "#a855f7" }} />출고</span>
|
||||
<span className="flex items-center gap-1"><span className="inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: "#ef4444" }} />셋업</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" disabled={!selectedRow} onClick={() => selectedRow && openWbsPopUp(String(selectedRow.OBJID), String(selectedRow.CATEGORY_CD ?? ""))}>
|
||||
프로젝트 일정
|
||||
</Button>
|
||||
<Button size="sm" disabled={!selectedRow} onClick={() => selectedRow && openGanttPopUp(String(selectedRow.OBJID))}>
|
||||
G-Chart
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded bg-white overflow-hidden">
|
||||
<div className="overflow-auto" style={{ maxHeight: "calc(100vh - 430px)" }}>
|
||||
<table className="text-xs border-collapse" style={{ minWidth: "1600px", width: "100%" }}>
|
||||
<thead className="sticky top-0 z-10 bg-gray-50">
|
||||
<tr>
|
||||
<th rowSpan={2} className="w-10 border border-gray-200 px-1 py-1 bg-gray-100">선택</th>
|
||||
<th colSpan={5} className="border border-gray-200 px-2 py-1 bg-gray-100 text-center">프로젝트 정보</th>
|
||||
<th colSpan={6} className="border border-gray-200 px-2 py-1 bg-gray-100 text-center">진척율(%)</th>
|
||||
<th colSpan={12} className="border border-gray-200 px-2 py-1 text-center" style={{ backgroundColor: "#fef3c7" }}>{ganttYear - 1}년</th>
|
||||
<th colSpan={12} className="border border-gray-200 px-2 py-1 text-center" style={{ backgroundColor: "#dbeafe" }}>{ganttYear}년</th>
|
||||
<th colSpan={12} className="border border-gray-200 px-2 py-1 text-center" style={{ backgroundColor: "#dcfce7" }}>{ganttYear + 1}년</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 100 }}>고객사</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 70 }}>제품구분</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 90 }}>프로젝트번호</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 160 }}>프로젝트명</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 70 }}>제작공장</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 80 }}>요청납기일</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 50 }}>설계</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 50 }}>구매</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 50 }}>조립</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 50 }}>셋업</th>
|
||||
<th className="border border-gray-200 px-1 py-1 bg-gray-50" style={{ width: 50 }}>상태</th>
|
||||
{ganttMonths.map((mm, i) => (
|
||||
<th key={i} className="border border-gray-200 px-1 py-1 bg-gray-50 text-[10px]" style={{ width: 30 }}>
|
||||
{mm.m + 1}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={48} className="text-center py-12 text-gray-400">조회된 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
rows.map((row) => {
|
||||
const objId = String(row.OBJID);
|
||||
const isSelected = objId === selectedObjId;
|
||||
const design = toPct(row.DESIGN_ACT_START, row.DESIGN_ACT_END);
|
||||
const purchase = toPct(row.PURCHASE_ACT_START, row.PURCHASE_ACT_END);
|
||||
const produce = toPct(row.PRODUCE_ACT_START, row.PRODUCE_ACT_END);
|
||||
const ship = toPct(row.REQ_DEL_DATE, row.REQ_DEL_DATE);
|
||||
const setupBar = toPct(row.SETUP_ACT_START, row.SETUP_ACT_END);
|
||||
const late = isLate(row);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={objId}
|
||||
className={`cursor-pointer ${isSelected ? "bg-blue-50" : "hover:bg-gray-50"}`}
|
||||
onClick={() => setSelectedObjId(objId)}
|
||||
>
|
||||
<td className="border border-gray-200 text-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="selectProject"
|
||||
checked={isSelected}
|
||||
onChange={() => setSelectedObjId(objId)}
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-gray-200 px-2 py-1 truncate" title={String(row.CUSTOMER_NAME ?? "")}>{String(row.CUSTOMER_NAME ?? "")}</td>
|
||||
<td className="border border-gray-200 px-2 py-1 truncate">{String(row.PRODUCT_NAME ?? "")}</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-primary hover:underline" onClick={(e) => { e.stopPropagation(); openProjectModify(objId); }}>
|
||||
{String(row.PROJECT_NO ?? "")}
|
||||
</td>
|
||||
<td className="border border-gray-200 px-2 py-1 truncate">{String(row.PROJECT_NAME ?? "")}</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center">{String(row.MANUFACTURE_PLANT_NAME ?? "")}</td>
|
||||
<td className="border border-gray-200 px-2 py-1 text-center">{String(row.REQ_DEL_DATE ?? "")}</td>
|
||||
<td className="border border-gray-200 px-1 py-1 text-center">{fmtRate(row.DESIGN_RATETOTAL)}</td>
|
||||
<td className="border border-gray-200 px-1 py-1 text-center">{fmtRate(row.PURCHASE_RATETOTAL)}</td>
|
||||
<td className="border border-gray-200 px-1 py-1 text-center">{fmtRate(row.PRODUCE_RATETOTAL)}</td>
|
||||
<td className="border border-gray-200 px-1 py-1 text-center">{fmtRate(row.SETUP_RATETOTAL)}</td>
|
||||
<td className="border border-gray-200 px-1 py-1 text-center">
|
||||
<span className={`inline-block w-3 h-3 rounded-full ${late ? "bg-red-500" : "bg-green-500"}`} />
|
||||
</td>
|
||||
<td colSpan={36} className="border border-gray-200 relative p-0" style={{ height: 48 }}>
|
||||
{todayPct != null && (
|
||||
<div className="absolute top-0 bottom-0 border-l-2 border-dotted" style={{ left: `${todayPct}%`, borderColor: "#f59e0b" }} title="오늘" />
|
||||
)}
|
||||
{design && (
|
||||
<div className="absolute rounded-sm" title={`설계: ${row.DESIGN_ACT_START} ~ ${row.DESIGN_ACT_END}`}
|
||||
style={{ top: 4, left: `${design.left}%`, width: `${design.width}%`, height: 4, backgroundColor: "#3b82f6" }} />
|
||||
)}
|
||||
{purchase && (
|
||||
<div className="absolute rounded-sm" title={`구매: ${row.PURCHASE_ACT_START} ~ ${row.PURCHASE_ACT_END}`}
|
||||
style={{ top: 12, left: `${purchase.left}%`, width: `${purchase.width}%`, height: 4, backgroundColor: "#22c55e" }} />
|
||||
)}
|
||||
{produce && (
|
||||
<div className="absolute rounded-sm" title={`조립: ${row.PRODUCE_ACT_START} ~ ${row.PRODUCE_ACT_END}`}
|
||||
style={{ top: 20, left: `${produce.left}%`, width: `${produce.width}%`, height: 4, backgroundColor: "#1f2937" }} />
|
||||
)}
|
||||
{ship && (
|
||||
<div className="absolute rotate-45" title={`출고: ${row.REQ_DEL_DATE}`}
|
||||
style={{ top: 28, left: `calc(${ship.left}% - 4px)`, width: 8, height: 8, backgroundColor: "#a855f7" }} />
|
||||
)}
|
||||
{setupBar && (
|
||||
<div className="absolute rounded-sm" title={`셋업: ${row.SETUP_ACT_START} ~ ${row.SETUP_ACT_END}`}
|
||||
style={{ top: 38, left: `${setupBar.left}%`, width: `${setupBar.width}%`, height: 4, backgroundColor: "#ef4444" }} />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
import { DataGrid, type GridColumn } from "@/components/grid/data-grid";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
|
||||
// 프로젝트관리 > 제품구분_UNIT관리 (project/wbsTemplateMngList.do 대응)
|
||||
export default function WbsTemplatePage() {
|
||||
const [product, setProduct] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openCenterPopup = (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 columns: GridColumn[] = [
|
||||
{ title: "제품구분", field: "PRODUCT_NAME", width: 250, hozAlign: "left" },
|
||||
{ title: "기계형식", field: "TITLE", width: 250, hozAlign: "left" },
|
||||
{
|
||||
title: "고객사_장비목적",
|
||||
field: "CUSTOMER_PRODUCT",
|
||||
hozAlign: "left",
|
||||
cellClick: (row) => {
|
||||
openCenterPopup(
|
||||
`/project/wbs-template/master-form?objId=${row.OBJID}`,
|
||||
"openTemplateMasterPopUp",
|
||||
1000,
|
||||
200,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "UNIT",
|
||||
field: "WBS_TASK_CNT",
|
||||
width: 250,
|
||||
hozAlign: "center",
|
||||
formatter: (cell) => `📁 ${cell ?? 0}`,
|
||||
cellClick: (row) => {
|
||||
openCenterPopup(
|
||||
`/project/wbs-template/task-list?objId=${row.OBJID}`,
|
||||
"openWBSTaskListPopUp",
|
||||
800,
|
||||
700,
|
||||
);
|
||||
},
|
||||
},
|
||||
{ title: "등록자", field: "WRITER_TITLE", width: 250, hozAlign: "left" },
|
||||
{ title: "등록일", field: "REG_DATE_TITLE", width: 250, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/project/wbs-template", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ product }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [product]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleRegister = () => {
|
||||
if (!product) {
|
||||
Swal.fire("알림", "제품은 필수값입니다. 제품을 선택해 주세요.", "warning");
|
||||
return;
|
||||
}
|
||||
openCenterPopup(
|
||||
`/project/wbs-template/excel-import?product=${encodeURIComponent(product)}`,
|
||||
"openWBSExcelImportPopUp",
|
||||
1340,
|
||||
700,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택된 대상이 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const r = await Swal.fire({
|
||||
title: "삭제하시겠습니까?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "확인",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (!r.isConfirmed) return;
|
||||
const res = await fetch("/api/project/wbs-template/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) {
|
||||
Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">프로젝트관리_제품구분_UNIT관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" onClick={handleRegister}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect
|
||||
codeId="0000001"
|
||||
value={product}
|
||||
onChange={setProduct}
|
||||
className="w-[200px]"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
"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 Swal from "sweetalert2";
|
||||
|
||||
// purchaseOrder/purchaseOrderList.jsp 대응 - 발주관리
|
||||
export default function PurchaseOrderPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [poNo, setPoNo] = useState("");
|
||||
const [supplierName, setSupplierName] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "발주번호", field: "PO_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => {
|
||||
const w = 1150, h = 850;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(`/purchase-order/form?objId=${row.OBJID}`, "poForm", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
} },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center" },
|
||||
{ title: "협력사", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "발주명", field: "PO_NAME", width: 200, hozAlign: "left" },
|
||||
{ title: "발주일", field: "PO_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "납기일", field: "DELIVERY_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "발주금액", field: "PO_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/purchase-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, po_no: poNo, supplier_name: supplierName }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, poNo, supplierName]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">발주관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => {
|
||||
const w = 1150, h = 850;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open("/purchase-order/form", "poForm", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
}}>등록</Button>
|
||||
<Button size="sm" variant="danger" onClick={async () => {
|
||||
if (selectedRows.length === 0) { Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning"); return; }
|
||||
const r = await Swal.fire({ title: "삭제 확인", text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`, icon: "warning", showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소" });
|
||||
if (!r.isConfirmed) return;
|
||||
const res = await fetch("/api/purchase-order/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }) });
|
||||
const json = await res.json();
|
||||
if (json.success) { Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); fetchData(); }
|
||||
else Swal.fire("오류", json.message, "error");
|
||||
}}>삭제</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="발주번호">
|
||||
<Input value={poNo} onChange={(e) => setPoNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="협력사">
|
||||
<Input value={supplierName} onChange={(e) => setSupplierName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} showCheckbox
|
||||
onSelectionChange={setSelectedRows} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
"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";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: /salesMng/salesBomReportList.do
|
||||
export default function PurchaseBomPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [customerCd, setCustomerCd] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [unitName, setUnitName] = useState("");
|
||||
const [writer2Id, setWriter2Id] = useState("");
|
||||
const [customerOptions, setCustomerOptions] = useState<Option[]>([]);
|
||||
const [userOptions, setUserOptions] = useState<Option[]>([]);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setCustomerOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.SUPPLY_NAME),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => {});
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setUserOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID),
|
||||
label: String(r.USER_NAME),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const openStructurePopup = (objId: string) => {
|
||||
if (!objId) return;
|
||||
const w = 1800, h = 800;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/bom/structure?objId=${objId}&readonly=1`,
|
||||
"bomStructure",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const openSalesBomReport = (objId: string, sbrObjId: string) => {
|
||||
if (!objId) return;
|
||||
const w = 1900, h = 900;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/bom/form?parentObjId=${objId}&objId=${sbrObjId || ""}`,
|
||||
"salesMngReport",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "left" },
|
||||
{ title: "요청납기", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "수주회사", field: "CONTRACT_COMPANY_NAME", width: 110, hozAlign: "left" },
|
||||
{ title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 220, hozAlign: "left" },
|
||||
{
|
||||
title: "E-BOM",
|
||||
field: "BOM_CNT",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
formatter: (cell, row) => (
|
||||
<FolderCell count={cell} onClick={() => openStructurePopup(String(row.OBJID || ""))} />
|
||||
),
|
||||
},
|
||||
{ title: "배포일", field: "DEPLOY_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "설계담당자", field: "WRITER1_NAME", width: 100, hozAlign: "center" },
|
||||
{
|
||||
title: "구매BOM",
|
||||
field: "SALES_PART_CNT",
|
||||
width: 90,
|
||||
hozAlign: "center",
|
||||
formatter: (cell, row) => (
|
||||
<FolderCell
|
||||
count={cell}
|
||||
onClick={() => openSalesBomReport(String(row.OBJID || ""), String(row.SBR_OBJID || ""))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ title: "작성일", field: "REGDATE2", width: 100, hozAlign: "center" },
|
||||
{ title: "구매담당자", field: "WRITER2_NAME", width: 100, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/purchase/bom", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
customer_cd: customerCd,
|
||||
project_no: projectNo,
|
||||
unit_name: unitName,
|
||||
writer2_id: writer2Id,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
}, [year, customerCd, projectNo, unitName, writer2Id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">구매BOM관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>
|
||||
{y}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<SearchableSelect
|
||||
options={customerOptions}
|
||||
value={customerCd}
|
||||
onChange={setCustomerCd}
|
||||
className="w-[180px]"
|
||||
placeholder="전체"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input
|
||||
value={projectNo}
|
||||
onChange={(e) => setProjectNo(e.target.value)}
|
||||
placeholder="프로젝트번호"
|
||||
className="w-[160px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<Input
|
||||
value={unitName}
|
||||
onChange={(e) => setUnitName(e.target.value)}
|
||||
placeholder="유닛명"
|
||||
className="w-[160px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="구매담당자">
|
||||
<SearchableSelect
|
||||
options={userOptions}
|
||||
value={writer2Id}
|
||||
onChange={setWriter2Id}
|
||||
className="w-[160px]"
|
||||
placeholder="전체"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: /salesMng/salesPartChgList.do
|
||||
export default function DesignChangePage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [partName, setPartName] = useState("");
|
||||
const [revision, setRevision] = useState("");
|
||||
const [changeType, setChangeType] = useState("");
|
||||
const [changeOption, setChangeOption] = useState("");
|
||||
const [partType, setPartType] = useState("");
|
||||
const [partWriter, setPartWriter] = useState("");
|
||||
const [salesWriter, setSalesWriter] = useState("");
|
||||
const [actCd, setActCd] = useState("");
|
||||
const [actStatus, setActStatus] = useState("");
|
||||
const [eoDateStart, setEoDateStart] = useState("");
|
||||
const [eoDateEnd, setEoDateEnd] = useState("");
|
||||
const [confirmDateStart, setConfirmDateStart] = useState("");
|
||||
const [confirmDateEnd, setConfirmDateEnd] = useState("");
|
||||
const [userOptions, setUserOptions] = useState<Option[]>([]);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setUserOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID),
|
||||
label: String(r.USER_NAME),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const openPartHistoryPopup = (objId: string) => {
|
||||
if (!objId) return;
|
||||
const w = 550, h = 250;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/design-change/part-history?objId=${objId}`,
|
||||
"partHistory",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const openOrderPopup = (pomObjId: string) => {
|
||||
if (!pomObjId) return;
|
||||
const w = 950, h = 765;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/order/list/form?objId=${pomObjId}`,
|
||||
"purchaseOrderForm",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const openActPopup = (row: Record<string, unknown>) => {
|
||||
const w = 600, h = 350;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
const q = new URLSearchParams({
|
||||
objId: String(row.SPC_OBJID || ""),
|
||||
partObjId: String(row.OBJID || ""),
|
||||
eoNo: String(row.EO_NO || ""),
|
||||
partNo: String(row.PART_NO || ""),
|
||||
projectNo: String(row.PROJECT_NO || ""),
|
||||
}).toString();
|
||||
window.open(`/purchase/design-change/form?${q}`, "actRegist", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "EO NO", field: "EO_NO", width: 90, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 160, hozAlign: "left" },
|
||||
{ title: "모품번", field: "PARENT_PART_INFO", width: 130, hozAlign: "left" },
|
||||
{
|
||||
title: "품번",
|
||||
field: "PART_NO",
|
||||
width: 120,
|
||||
hozAlign: "left",
|
||||
cellClick: (row) => openPartHistoryPopup(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "품명", field: "PART_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "수량", field: "QTY", width: 60, hozAlign: "right" },
|
||||
{ title: "변경수량", field: "QTY_TEMP", width: 80, hozAlign: "right" },
|
||||
{ title: "EO구분", field: "CHANGE_TYPE_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "EO사유", field: "CHANGE_OPTION_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "Revision", field: "REVISION", width: 80, hozAlign: "center" },
|
||||
{ title: "EO Date", field: "EO_DATE", width: 95, hozAlign: "center" },
|
||||
{ title: "PART구분", field: "PART_TYPE_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "설계담당자", field: "WRITER_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "실행일", field: "HIS_REG_DATE_TITLE", width: 90, hozAlign: "center" },
|
||||
{ title: "구매확인", field: "CONFIRM_DATE", width: 95, hozAlign: "center" },
|
||||
{ title: "구매담당자", field: "WRITER1_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "조치내역", field: "ACT_NAME", width: 90, hozAlign: "center" },
|
||||
{
|
||||
title: "발주서NO",
|
||||
field: "PURCHASE_ORDER_NO",
|
||||
width: 110,
|
||||
hozAlign: "left",
|
||||
cellClick: (row) => openOrderPopup(String(row.PURCHASE_ORDER_MASTER_OBJID || "")),
|
||||
},
|
||||
{ title: "조치결과", field: "ACT_STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/purchase/design-change", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
project_no: projectNo,
|
||||
part_no: partNo,
|
||||
part_name: partName,
|
||||
revision,
|
||||
change_type: changeType,
|
||||
change_option: changeOption,
|
||||
part_type: partType,
|
||||
part_writer: partWriter,
|
||||
sales_writer: salesWriter,
|
||||
act_cd: actCd,
|
||||
act_status: actStatus,
|
||||
eo_date_start: eoDateStart,
|
||||
eo_date_end: eoDateEnd,
|
||||
confirm_date_start: confirmDateStart,
|
||||
confirm_date_end: confirmDateEnd,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
}, [
|
||||
year, projectNo, partNo, partName, revision, changeType, changeOption,
|
||||
partType, partWriter, salesWriter, actCd, actStatus,
|
||||
eoDateStart, eoDateEnd, confirmDateStart, confirmDateEnd,
|
||||
]);
|
||||
|
||||
const handleReceive = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "접수할 항목을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
const confirm = await Swal.fire({
|
||||
title: "접수 확인",
|
||||
text: `선택한 ${selectedRows.length}건을 접수 처리하시겠습니까?`,
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "접수",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (!confirm.isConfirmed) return;
|
||||
const res = await fetch("/api/purchase/design-change/receipt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rows: selectedRows }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message || "접수 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleActRegist = () => {
|
||||
if (selectedRows.length !== 1) {
|
||||
Swal.fire("알림", "조치내역을 등록할 항목 1건을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
openActPopup(selectedRows[0]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">설계변경리스트</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleReceive}>
|
||||
접수
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleActRegist}>
|
||||
조치내역등록
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[90px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>
|
||||
{y}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품번">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="품명">
|
||||
<Input value={partName} onChange={(e) => setPartName(e.target.value)} className="w-[120px]" />
|
||||
</SearchField>
|
||||
<SearchField label="Revision">
|
||||
<Input value={revision} onChange={(e) => setRevision(e.target.value)} className="w-[80px]" />
|
||||
</SearchField>
|
||||
<SearchField label="EO구분">
|
||||
<SearchableCodeSelect codeId="0001054" value={changeType} onChange={setChangeType} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="EO사유">
|
||||
<SearchableCodeSelect codeId="0000318" value={changeOption} onChange={setChangeOption} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PART구분">
|
||||
<SearchableCodeSelect codeId="0000062" value={partType} onChange={setPartType} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="설계담당자">
|
||||
<SearchableSelect options={userOptions} value={partWriter} onChange={setPartWriter} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="구매담당자">
|
||||
<SearchableSelect options={userOptions} value={salesWriter} onChange={setSalesWriter} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="조치내역">
|
||||
<SearchableCodeSelect codeId="0001059" value={actCd} onChange={setActCd} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="조치결과">
|
||||
<SearchableCodeSelect codeId="0001062" value={actStatus} onChange={setActStatus} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="EO Date">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
value={eoDateStart}
|
||||
onChange={(e) => setEoDateStart(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={eoDateEnd}
|
||||
onChange={(e) => setEoDateEnd(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="구매확인일자">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
value={confirmDateStart}
|
||||
onChange={(e) => setConfirmDateStart(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={confirmDateEnd}
|
||||
onChange={(e) => setConfirmDateEnd(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: /salesMng/salesRequestMngRegList.do
|
||||
export default function PurchaseRequestPage() {
|
||||
const [year, setYear] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [requestCd, setRequestCd] = useState("");
|
||||
const [receiptWriter, setReceiptWriter] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [receiptDateStart, setReceiptDateStart] = useState("");
|
||||
const [receiptDateEnd, setReceiptDateEnd] = useState("");
|
||||
const [userOptions, setUserOptions] = useState<Option[]>([]);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setUserOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID),
|
||||
label: String(r.USER_NAME),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const openRequestForm = (objId?: string) => {
|
||||
const url = objId ? `/purchase/request/form?objId=${objId}` : "/purchase/request/form";
|
||||
const w = 1100, h = 630;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(url, "requestForm", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "요청번호",
|
||||
field: "REQUEST_MNG_NO",
|
||||
width: 110,
|
||||
hozAlign: "left",
|
||||
cellClick: (row) => openRequestForm(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "요청구분", field: "REQUEST_CD_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NUMBER", width: 110, hozAlign: "left" },
|
||||
{ title: "프로젝트명", field: "PROJECT_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_CODE_NAME", width: 170, hozAlign: "left" },
|
||||
{ title: "구매요청품 수", field: "ITEMS_QTY", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "총 수량", field: "TOTAL_QTY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "요청사유", field: "REQUEST_REASONS_NAME", width: 100, hozAlign: "left" },
|
||||
{ title: "요청인", field: "REQUEST_USER_NAME", width: 110, hozAlign: "center" },
|
||||
{ title: "입고요청일", field: "DELIVERY_REQUEST_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "상태", field: "STATUS_TITLE", width: 90, hozAlign: "center" },
|
||||
{ title: "접수자", field: "RECEIPT_USER_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "접수일", field: "RECEIPT_DATE", width: 90, hozAlign: "center" },
|
||||
{ title: "발주서NO", field: "PURCHASE_ORDER_NO_ARR", width: 180, hozAlign: "left" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/purchase/request", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
project_no: projectNo,
|
||||
request_cd: requestCd,
|
||||
receipt_writer: receiptWriter,
|
||||
status,
|
||||
receipt_date_start: receiptDateStart,
|
||||
receipt_date_end: receiptDateEnd,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
}, [year, projectNo, requestCd, receiptWriter, status, receiptDateStart, receiptDateEnd]);
|
||||
|
||||
const handleReceive = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "접수할 항목을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
const confirm = await Swal.fire({
|
||||
title: "접수 확인",
|
||||
text: `선택한 ${selectedRows.length}건을 접수 처리하시겠습니까?`,
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "접수",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (!confirm.isConfirmed) return;
|
||||
const res = await fetch("/api/purchase/request/receipt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message || "접수 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleOrderWrite = () => {
|
||||
Swal.fire("알림", "발주서작성 기능은 준비 중입니다.", "info");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">구매요청서관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => openRequestForm()}>
|
||||
구매요청서작성
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleOrderWrite}>
|
||||
발주서작성
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleReceive}>
|
||||
접수
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[90px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>
|
||||
{y}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="요청구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={requestCd} onChange={setRequestCd} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
className="h-9 w-[130px] rounded border border-gray-300 bg-white px-3 text-sm"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
<option value="create">등록</option>
|
||||
<option value="release">제출 완료</option>
|
||||
<option value="reception">접수</option>
|
||||
<option value="approvalRequest">결재중</option>
|
||||
<option value="approvalComplete">결재완료</option>
|
||||
<option value="reject">반려</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="접수자">
|
||||
<SearchableSelect
|
||||
options={userOptions}
|
||||
value={receiptWriter}
|
||||
onChange={setReceiptWriter}
|
||||
className="w-[140px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="접수일">
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
value={receiptDateStart}
|
||||
onChange={(e) => setReceiptDateStart(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
<span>~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={receiptDateEnd}
|
||||
onChange={(e) => setReceiptDateEnd(e.target.value)}
|
||||
className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]"
|
||||
/>
|
||||
</div>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
// 원본: /salesMng/salesLongDeliveryList.do
|
||||
export default function StockPage() {
|
||||
const [ldPartName, setLdPartName] = useState("");
|
||||
const [spec, setSpec] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [maker, setMaker] = useState("");
|
||||
const [materialCode, setMaterialCode] = useState("");
|
||||
const [adminSupply, setAdminSupply] = useState("");
|
||||
const [supplyOptions, setSupplyOptions] = useState<Option[]>([]);
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) =>
|
||||
setSupplyOptions(
|
||||
(d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID),
|
||||
label: String(r.SUPPLY_NAME),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const openStandardPopup = (objId?: string) => {
|
||||
const w = 600, h = 300;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/stock/form?actionType=STANDARD${objId ? `&objId=${objId}` : ""}`,
|
||||
"stockStandard",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const openInputPopup = (objId: string) => {
|
||||
if (!objId) return;
|
||||
const w = 600, h = 590;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/stock/form?actionType=INPUT&objId=${objId}`,
|
||||
"stockInput",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const openPredictPopup = (objId: string) => {
|
||||
if (!objId) return;
|
||||
const w = 900, h = 590;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(
|
||||
`/purchase/stock/form?actionType=PREDICT&objId=${objId}`,
|
||||
"stockPredict",
|
||||
`width=${w},height=${h},left=${left},top=${top}`
|
||||
);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "품명",
|
||||
field: "LD_PART_NAME",
|
||||
width: 180,
|
||||
hozAlign: "left",
|
||||
cellClick: (row) => openStandardPopup(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "사양(규격)", field: "SPEC", width: 300, hozAlign: "left" },
|
||||
{ title: "Location", field: "LOCATION_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "내자/외자", field: "FORM_NO", width: 100, hozAlign: "center" },
|
||||
{ title: "메이커", field: "MAKER", width: 100, hozAlign: "left" },
|
||||
{ title: "자재코드", field: "MATERIAL_CODE", width: 160, hozAlign: "left" },
|
||||
{ title: "공급업체", field: "SUPPLY_NAME", width: 140, hozAlign: "left" },
|
||||
{ title: "재고수량", field: "INPUT_QTY", width: 85, hozAlign: "right", formatter: "money" },
|
||||
{
|
||||
title: "자재투입이력",
|
||||
field: "INPUT_CNT",
|
||||
width: 100,
|
||||
hozAlign: "right",
|
||||
formatter: "money",
|
||||
cellClick: (row) => openInputPopup(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "단가", field: "PRICE", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "보유금액", field: "PRICE_SUM", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "합계", field: "M_TOTAL", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "1월", field: "M01", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "2월", field: "M02", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "3월", field: "M03", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "4월", field: "M04", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "5월", field: "M05", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "6월", field: "M06", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "7월", field: "M07", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "8월", field: "M08", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "9월", field: "M09", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "10월", field: "M10", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "11월", field: "M11", width: 60, hozAlign: "right", formatter: "money" },
|
||||
{ title: "12월", field: "M12", width: 60, hozAlign: "right", formatter: "money" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/purchase/stock", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
ld_part_name: ldPartName,
|
||||
spec,
|
||||
Location: location,
|
||||
maker,
|
||||
material_code: materialCode,
|
||||
admin_supply: adminSupply,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
} else {
|
||||
const j = await res.json().catch(() => ({}));
|
||||
Swal.fire("오류", j.message || "조회 실패", "error");
|
||||
}
|
||||
}, [ldPartName, spec, location, maker, materialCode, adminSupply]);
|
||||
|
||||
const totalCost = data.reduce((sum, r) => sum + Number(r.PRICE_SUM || 0), 0);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
const confirm = await Swal.fire({
|
||||
title: "삭제 확인",
|
||||
text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`,
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "삭제",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (!confirm.isConfirmed) return;
|
||||
const res = await fetch("/api/purchase/stock/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message || "삭제 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputClick = () => {
|
||||
if (selectedRows.length !== 1) {
|
||||
Swal.fire("알림", "자재투입할 항목 1건을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
openInputPopup(String(selectedRows[0].OBJID || ""));
|
||||
};
|
||||
const handlePredictClick = () => {
|
||||
if (selectedRows.length !== 1) {
|
||||
Swal.fire("알림", "예측수량을 등록할 항목 1건을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
openPredictPopup(String(selectedRows[0].OBJID || ""));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">재고리스트</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" onClick={() => openStandardPopup()}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleInputClick}>자재투입</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handlePredictClick}>예측수량등록</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="품명">
|
||||
<Input value={ldPartName} onChange={(e) => setLdPartName(e.target.value)} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="사양(규격)">
|
||||
<Input value={spec} onChange={(e) => setSpec(e.target.value)} className="w-[160px]" />
|
||||
</SearchField>
|
||||
<SearchField label="Location">
|
||||
<SearchableCodeSelect codeId="0000262" value={location} onChange={setLocation} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="메이커">
|
||||
<Input value={maker} onChange={(e) => setMaker(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="자재코드">
|
||||
<Input value={materialCode} onChange={(e) => setMaterialCode(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<SearchableSelect
|
||||
options={supplyOptions}
|
||||
value={adminSupply}
|
||||
onChange={setAdminSupply}
|
||||
className="w-[180px]"
|
||||
placeholder="전체"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="flex items-center gap-6 mb-3 text-sm font-bold text-red-600">
|
||||
<span>장납기품 비용(원) : {totalCost.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 구매관리 > 공급업체관리 — 원본: /admin/supplyMngPagingList.do (admin_supply_mng)
|
||||
export default function SupplierPage() {
|
||||
const [supplyCode, setSupplyCode] = useState("");
|
||||
const [supplyName, setSupplyName] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openSupplierForm = (objId?: string) => {
|
||||
const url = objId ? `/purchase/supplier/form?objid=${objId}` : "/purchase/supplier/form";
|
||||
const w = 1300;
|
||||
const h = 620;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
window.open(url, "supplierForm", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "NO", field: "RNUM", width: 50, hozAlign: "center" },
|
||||
{ title: "구분", field: "SUPPLY_CODE_NAME", width: 90, hozAlign: "center" },
|
||||
{
|
||||
title: "고객명",
|
||||
field: "SUPPLY_NAME",
|
||||
width: 190,
|
||||
hozAlign: "left",
|
||||
cellClick: (row) => openSupplierForm(row.OBJID as string),
|
||||
},
|
||||
{ title: "지역", field: "AREA_CD_NAME", width: 110, hozAlign: "center" },
|
||||
{ title: "대표자명", field: "CHARGE_USER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "업종", field: "SUPPLY_STOCKNAME", width: 280, hozAlign: "left" },
|
||||
{ title: "업태", field: "SUPPLY_BUSNAME", width: 180, hozAlign: "left" },
|
||||
{ title: "주소", field: "SUPPLY_ADDRESS", width: 250, hozAlign: "left" },
|
||||
{ title: "핸드폰", field: "SUPPLY_TEL_NO", width: 150, hozAlign: "center" },
|
||||
{ title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/admin/supply", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ supply_code: supplyCode, supply_name: supplyName }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [supplyCode, supplyName]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
const result = await Swal.fire({
|
||||
title: "삭제 확인",
|
||||
text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`,
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "삭제",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const res = await fetch("/api/admin/supply/delete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message || "삭제 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">공급업체관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>
|
||||
조회
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => openSupplierForm()}>
|
||||
공급업체 등록
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="구분">
|
||||
<SearchableCodeSelect
|
||||
codeId="0000167"
|
||||
value={supplyCode}
|
||||
onChange={setSupplyCode}
|
||||
placeholder="전체"
|
||||
className="w-[160px]"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="고객명">
|
||||
<Input
|
||||
value={supplyName}
|
||||
onChange={(e) => setSupplyName(e.target.value)}
|
||||
placeholder="고객명"
|
||||
className="w-[180px]"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
"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";
|
||||
|
||||
// quality/qualityList.jsp 대응 - 품질관리
|
||||
export default function QualityPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [partNo, setPartNo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120 },
|
||||
{ title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/production/inspection/form?objId=${row.OBJID}`, "qualityDetail", "width=1000,height=700") },
|
||||
{ title: "파트명", field: "PART_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "검사유형", field: "TEST_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "검사결과", field: "TEST_RESULT_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "검사일", field: "TEST_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "검사자", field: "TESTER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "판정", field: "JUDGMENT", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/quality", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, part_no: partNo }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, partNo]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">품질관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="파트번호">
|
||||
<Input value={partNo} onChange={(e) => setPartNo(e.target.value)} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { numberWithCommas } from "@/lib/utils";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface Product { CODE: string; NAME: string }
|
||||
interface MonthRow {
|
||||
MM: string;
|
||||
CONTRACT_CNT_YEAR: number;
|
||||
CONTRACT_COST_YEAR_ORG: number;
|
||||
CONTRACT_COST_YEAR: number;
|
||||
RELEASE_CNT_YEAR: number;
|
||||
[key: string]: number | string;
|
||||
}
|
||||
interface YearGoal {
|
||||
YEAR: string;
|
||||
PRICE: number;
|
||||
YEAR_GOAL_OBJID?: string;
|
||||
CONTRACT_CNT_YEAR: number;
|
||||
CONTRACT_COST_YEAR: number;
|
||||
GOAL_RATE: number;
|
||||
}
|
||||
|
||||
const PIE_COLORS = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00"];
|
||||
|
||||
// 간단한 SVG 파이 차트
|
||||
function PieChart({ data, size = 220 }: { data: { label: string; value: number }[]; size?: number }) {
|
||||
const total = data.reduce((s, d) => s + d.value, 0);
|
||||
if (total === 0) {
|
||||
return <div className="text-center text-xs text-gray-400 py-10">데이터 없음</div>;
|
||||
}
|
||||
const cx = size / 2, cy = size / 2, r = size / 2 - 4;
|
||||
let acc = 0;
|
||||
const slices = data.map((d, i) => {
|
||||
const start = (acc / total) * Math.PI * 2 - Math.PI / 2;
|
||||
acc += d.value;
|
||||
const end = (acc / total) * Math.PI * 2 - Math.PI / 2;
|
||||
const large = end - start > Math.PI ? 1 : 0;
|
||||
const x1 = cx + r * Math.cos(start);
|
||||
const y1 = cy + r * Math.sin(start);
|
||||
const x2 = cx + r * Math.cos(end);
|
||||
const y2 = cy + r * Math.sin(end);
|
||||
const mid = (start + end) / 2;
|
||||
const lx = cx + r * 0.65 * Math.cos(mid);
|
||||
const ly = cy + r * 0.65 * Math.sin(mid);
|
||||
const pct = ((d.value / total) * 100);
|
||||
return {
|
||||
path: `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`,
|
||||
color: PIE_COLORS[i % PIE_COLORS.length],
|
||||
label: d.label,
|
||||
value: d.value,
|
||||
pct,
|
||||
lx, ly,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
||||
{slices.map((s, i) => (
|
||||
<g key={i}>
|
||||
<path d={s.path} fill={s.color} stroke="#fff" strokeWidth="1" />
|
||||
{s.pct >= 5 && (
|
||||
<text x={s.lx} y={s.ly} fontSize="10" fill="#fff" textAnchor="middle" dominantBaseline="middle">
|
||||
{s.pct.toFixed(0)}%
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
<div className="text-[11px] flex flex-col gap-0.5 pt-2 min-w-0">
|
||||
{slices.map((s, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<span className="inline-block w-3 h-3 shrink-0 rounded-sm" style={{ backgroundColor: s.color }} />
|
||||
<span className="truncate">{s.label}</span>
|
||||
<span className="text-gray-500 ml-auto pl-1 shrink-0">{s.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 년도별 영업현황 — 막대 2개(목표/수주) + 꺾은선(달성율)
|
||||
function GoalComboChart({ years }: { years: YearGoal[] }) {
|
||||
const W = 340, H = 230, PAD_L = 36, PAD_R = 36, PAD_T = 16, PAD_B = 34;
|
||||
const maxPrice = Math.max(1, ...years.flatMap((y) => [y.PRICE, y.CONTRACT_COST_YEAR]));
|
||||
const maxRate = Math.max(100, ...years.map((y) => y.GOAL_RATE));
|
||||
const chartW = W - PAD_L - PAD_R;
|
||||
const chartH = H - PAD_T - PAD_B;
|
||||
const xStep = chartW / years.length;
|
||||
const barW = Math.min(18, xStep / 3);
|
||||
|
||||
return (
|
||||
<svg width="100%" height={H} viewBox={`0 0 ${W} ${H}`}>
|
||||
{/* Y축 가이드 */}
|
||||
<line x1={PAD_L} y1={PAD_T} x2={PAD_L} y2={H - PAD_B} stroke="#ddd" />
|
||||
<line x1={PAD_L} y1={H - PAD_B} x2={W - PAD_R} y2={H - PAD_B} stroke="#ddd" />
|
||||
|
||||
{years.map((y, i) => {
|
||||
const cx = PAD_L + xStep * (i + 0.5);
|
||||
const hGoal = (y.PRICE / maxPrice) * chartH;
|
||||
const hActual = (y.CONTRACT_COST_YEAR / maxPrice) * chartH;
|
||||
const rateY = H - PAD_B - (y.GOAL_RATE / maxRate) * chartH;
|
||||
return (
|
||||
<g key={i}>
|
||||
<rect x={cx - barW - 1} y={H - PAD_B - hGoal} width={barW} height={hGoal} fill="#3366cc" />
|
||||
<rect x={cx + 1} y={H - PAD_B - hActual} width={barW} height={hActual} fill="#dc3912" />
|
||||
<text x={cx} y={H - PAD_B + 14} fontSize="11" textAnchor="middle" fill="#444">{y.YEAR}</text>
|
||||
<circle cx={cx} cy={rateY} r={3} fill="#ff9900" stroke="#fff" strokeWidth="1" />
|
||||
<text x={cx} y={rateY - 6} fontSize="9" textAnchor="middle" fill="#b87a00">{y.GOAL_RATE.toFixed(0)}%</text>
|
||||
{i > 0 && (() => {
|
||||
const prev = years[i - 1];
|
||||
const pcx = PAD_L + xStep * (i - 1 + 0.5);
|
||||
const prateY = H - PAD_B - (prev.GOAL_RATE / maxRate) * chartH;
|
||||
return <line x1={pcx} y1={prateY} x2={cx} y2={rateY} stroke="#ff9900" strokeWidth="1.5" />;
|
||||
})()}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 범례 */}
|
||||
<g transform={`translate(${PAD_L},${PAD_T - 8})`}>
|
||||
<rect x="0" y="0" width="10" height="10" fill="#3366cc" />
|
||||
<text x="14" y="9" fontSize="10" fill="#333">영업목표(억)</text>
|
||||
<rect x="85" y="0" width="10" height="10" fill="#dc3912" />
|
||||
<text x="99" y="9" fontSize="10" fill="#333">수주(억)</text>
|
||||
<line x1="165" y1="5" x2="180" y2="5" stroke="#ff9900" strokeWidth="2" />
|
||||
<circle cx="172" cy="5" r="3" fill="#ff9900" />
|
||||
<text x="184" y="9" fontSize="10" fill="#333">달성율(%)</text>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ContractDashboardPage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState(String(currentYear));
|
||||
const [categoryCd, setCategoryCd] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [total, setTotal] = useState<MonthRow | null>(null);
|
||||
const [months, setMonths] = useState<MonthRow[]>([]);
|
||||
const [years, setYears] = useState<YearGoal[]>([]);
|
||||
const [customerStats, setCustomerStats] = useState<{ OBJID: string; SUPPLY_NAME: string; TOTAL_SUPPLY_UNIT_CNT: number }[]>([]);
|
||||
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json()).then((d) => setCustomers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID || ""), label: String(r.SUPPLY_NAME || ""),
|
||||
})))).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const body = { Year: year, category_cd: categoryCd, customer_objid: customerObjid, product };
|
||||
const [dashR, goalR, custR] = await Promise.all([
|
||||
fetch("/api/sales/contract-dashboard", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }),
|
||||
fetch("/api/sales/contract-dashboard/year-goal", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Year: year }) }),
|
||||
fetch("/api/sales/contract-dashboard/customer-stats", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Year: year }) }),
|
||||
]);
|
||||
if (dashR.ok) {
|
||||
const d = await dashR.json();
|
||||
setProducts(d.products || []);
|
||||
setTotal(d.total || null);
|
||||
setMonths(d.months || []);
|
||||
}
|
||||
if (goalR.ok) {
|
||||
const g = await goalR.json();
|
||||
const ys = (g.years || []) as Record<string, unknown>[];
|
||||
setYears(ys.map((y) => ({
|
||||
YEAR: String(y.YEAR || ""),
|
||||
PRICE: Number(y.PRICE || 0),
|
||||
YEAR_GOAL_OBJID: String(y.YEAR_GOAL_OBJID || ""),
|
||||
CONTRACT_CNT_YEAR: Number(y.CONTRACT_CNT_YEAR || 0),
|
||||
CONTRACT_COST_YEAR: Number(y.CONTRACT_COST_YEAR || 0),
|
||||
GOAL_RATE: Number(y.GOAL_RATE || 0),
|
||||
})));
|
||||
}
|
||||
if (custR.ok) {
|
||||
const c = await custR.json();
|
||||
setCustomerStats(((c.rows || []) as Record<string, unknown>[]).map((r) => ({
|
||||
OBJID: String(r.OBJID || ""),
|
||||
SUPPLY_NAME: String(r.SUPPLY_NAME || ""),
|
||||
TOTAL_SUPPLY_UNIT_CNT: Number(r.TOTAL_SUPPLY_UNIT_CNT || 0),
|
||||
})));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [year, categoryCd, customerObjid, product]);
|
||||
|
||||
useEffect(() => { fetchAll(); }, [fetchAll]);
|
||||
|
||||
// 제품별 연 합계 (파이 데이터)
|
||||
const productPieData = useMemo(() => {
|
||||
if (!total) return [];
|
||||
return products.map((p) => ({
|
||||
label: p.NAME,
|
||||
value: Number(total[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0),
|
||||
})).filter((d) => d.value > 0);
|
||||
}, [products, total]);
|
||||
|
||||
const customerPieData = useMemo(() => customerStats.map((c) => ({
|
||||
label: c.SUPPLY_NAME, value: c.TOTAL_SUPPLY_UNIT_CNT,
|
||||
})), [customerStats]);
|
||||
|
||||
const handleRegistGoal = async () => {
|
||||
const current = years.find((y) => y.YEAR === year);
|
||||
const { value: priceStr } = await Swal.fire<string>({
|
||||
title: `${year}년 영업목표`,
|
||||
input: "number",
|
||||
inputLabel: "영업목표 (억원)",
|
||||
inputValue: String(current?.PRICE || ""),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "저장",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (!priceStr) return;
|
||||
const res = await fetch("/api/sales/contract-dashboard/year-goal/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ Year: year, PRICE: priceStr, YEAR_GOAL_OBJID: current?.YEAR_GOAL_OBJID || "" }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.success) {
|
||||
Swal.fire({ icon: "success", title: "저장되었습니다.", timer: 1200, showConfirmButton: false });
|
||||
fetchAll();
|
||||
} else {
|
||||
Swal.fire("오류", j.message || "저장 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">영업관리_계약현황</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleRegistGoal}>영업목표등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchAll}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchAll}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => currentYear - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={categoryCd} onChange={setCategoryCd} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<select value={customerObjid} onChange={(e) => setCustomerObjid(e.target.value)}
|
||||
className="h-9 w-[180px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
<option value="">전체</option>
|
||||
{customers.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
{loading && <div className="text-xs text-gray-400 mb-2">조회 중...</div>}
|
||||
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
{/* 좌: 계약현황 테이블 */}
|
||||
<section className="col-span-12 lg:col-span-5 bg-white border rounded p-2">
|
||||
<div className="text-sm font-bold text-gray-700 mb-2 pb-1 border-b">■ 계약현황 ({year})</div>
|
||||
<div className="overflow-auto" style={{ maxHeight: "calc(100vh - 330px)" }}>
|
||||
<table className="w-full text-[11px] border-collapse">
|
||||
<thead className="bg-gray-50 sticky top-0">
|
||||
<tr className="border-b">
|
||||
<th className="p-1 border" rowSpan={2}></th>
|
||||
<th className="p-1 border text-center" colSpan={products.length}>수주확정 건수</th>
|
||||
<th className="p-1 border text-center" rowSpan={2}>매출액<br />(억원)</th>
|
||||
<th className="p-1 border text-center" rowSpan={2}>출고</th>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
{products.map((p) => <th key={p.CODE} className="p-1 border text-center">{p.NAME}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{total && (
|
||||
<tr className="bg-orange-50 font-semibold">
|
||||
<td className="p-1 border text-center">계</td>
|
||||
{products.map((p) => (
|
||||
<td key={p.CODE} className="p-1 border text-right">
|
||||
{numberWithCommas(Number(total[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0))}
|
||||
</td>
|
||||
))}
|
||||
<td className="p-1 border text-right">{(total.CONTRACT_COST_YEAR || 0).toFixed(2)}</td>
|
||||
<td className="p-1 border text-right">{numberWithCommas(total.RELEASE_CNT_YEAR || 0)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{months.map((m) => (
|
||||
<tr key={m.MM} className="hover:bg-gray-50">
|
||||
<td className="p-1 border text-center">{parseInt(m.MM, 10)}월</td>
|
||||
{products.map((p) => (
|
||||
<td key={p.CODE} className="p-1 border text-right">
|
||||
{Number(m[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0) > 0
|
||||
? numberWithCommas(Number(m[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0))
|
||||
: ""}
|
||||
</td>
|
||||
))}
|
||||
<td className="p-1 border text-right">{m.CONTRACT_COST_YEAR ? m.CONTRACT_COST_YEAR.toFixed(2) : ""}</td>
|
||||
<td className="p-1 border text-right">{m.RELEASE_CNT_YEAR > 0 ? numberWithCommas(m.RELEASE_CNT_YEAR) : ""}</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!months || months.length === 0) && (
|
||||
<tr><td colSpan={products.length + 3} className="p-4 text-center text-gray-400">데이터 없음</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 중: 영업목표 테이블 + 제품별현황 파이 */}
|
||||
<section className="col-span-12 lg:col-span-3 flex flex-col gap-3">
|
||||
<div className="bg-white border rounded p-2">
|
||||
<div className="text-sm font-bold text-gray-700 mb-2 pb-1 border-b">■ 영업목표</div>
|
||||
<table className="w-full text-[11px] border-collapse">
|
||||
<thead className="bg-gray-50">
|
||||
<tr className="border-b">
|
||||
<th className="p-1 border" rowSpan={2}>년도</th>
|
||||
<th className="p-1 border" rowSpan={2}>영업목표<br />(억원)</th>
|
||||
<th className="p-1 border" colSpan={3}>현황</th>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<th className="p-1 border">계약건수</th>
|
||||
<th className="p-1 border">계약금액</th>
|
||||
<th className="p-1 border">달성율</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{years.length === 0 ? (
|
||||
<tr><td colSpan={5} className="p-3 text-center text-gray-400">데이터 없음</td></tr>
|
||||
) : (
|
||||
years.slice().reverse().map((y) => (
|
||||
<tr key={y.YEAR} className="hover:bg-gray-50">
|
||||
<td className="p-1 border text-center">{y.YEAR}</td>
|
||||
<td className="p-1 border text-right">{numberWithCommas(y.PRICE)}</td>
|
||||
<td className="p-1 border text-right">{numberWithCommas(y.CONTRACT_CNT_YEAR)}</td>
|
||||
<td className="p-1 border text-right">{(y.CONTRACT_COST_YEAR || 0).toFixed(2)}</td>
|
||||
<td className="p-1 border text-right">{(y.GOAL_RATE || 0).toFixed(1)}%</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded p-2 flex-1">
|
||||
<div className="text-sm font-bold text-gray-700 mb-2 pb-1 border-b">■ 제품별현황</div>
|
||||
<PieChart data={productPieData} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 우: 년도별 영업현황 + 고객사별현황 */}
|
||||
<section className="col-span-12 lg:col-span-4 flex flex-col gap-3">
|
||||
<div className="bg-white border rounded p-2">
|
||||
<div className="text-sm font-bold text-gray-700 mb-2 pb-1 border-b">■ 년도별 영업현황</div>
|
||||
<GoalComboChart years={years} />
|
||||
</div>
|
||||
|
||||
<div className="bg-white border rounded p-2 flex-1">
|
||||
<div className="text-sm font-bold text-gray-700 mb-2 pb-1 border-b">■ 고객사별현황</div>
|
||||
<PieChart data={customerPieData} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 영업관리 > 계약관리 (원본: contractMgmt/contractList.jsp)
|
||||
export default function ContractPage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState(String(currentYear));
|
||||
const [categoryCd, setCategoryCd] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [contractResult, setContractResult] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [contractStartDate, setContractStartDate] = useState("");
|
||||
const [contractEndDate, setContractEndDate] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
|
||||
const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/sales/customer", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
|
||||
}).then((r) => r.json()).then((d) => {
|
||||
setCustomers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""),
|
||||
})));
|
||||
}).catch(() => {});
|
||||
|
||||
fetch("/api/admin/users", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
|
||||
}).then((r) => r.json()).then((d) => {
|
||||
setPmUsers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID), label: String(r.USER_NAME || ""),
|
||||
})));
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const openContractForm = (objId?: string) => {
|
||||
const url = objId ? `/sales/contract/form?objId=${objId}` : "/sales/contract/form?actionType=regist";
|
||||
const w = 1280, h = 780;
|
||||
const left = Math.max(0, (window.screen.width - w) / 2);
|
||||
const top = Math.max(0, (window.screen.availHeight - h) / 2);
|
||||
window.open(url, "contractForm", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
// 그리드 컬럼 — 원본 3개 섹션(영업정보/진행사항/수주정보) 순서대로 플랫하게 배치
|
||||
const columns: GridColumn[] = [
|
||||
// ----- 영업번호 (frozen) -----
|
||||
{
|
||||
title: "영업번호", field: "CONTRACT_NO", width: 100, frozen: true,
|
||||
cellClick: (row) => openContractForm(String(row.OBJID || "")),
|
||||
},
|
||||
// ----- 영업정보(상세) -----
|
||||
{ title: "계약구분", field: "CATEGORY_NAME", width: 80 },
|
||||
{ title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "right" },
|
||||
{ title: "국내/해외", field: "AREA_NAME", width: 80 },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 160 },
|
||||
{ title: "제품구분", field: "PRODUCT_NAME", width: 90 },
|
||||
{ title: "기계형식", field: "MECHANICAL_TYPE", width: 110 },
|
||||
{ title: "고객사 프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 200 },
|
||||
{ title: "예상납기일", field: "DUE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "입고지", field: "LOCATION", width: 100 },
|
||||
{ title: "셋업지", field: "SETUP", width: 100 },
|
||||
{ title: "설비방향", field: "FACILITY_NAME", width: 90 },
|
||||
{ title: "설비대수", field: "FACILITY_QTY", width: 75, formatter: "money", hozAlign: "right" },
|
||||
{ title: "설비타입", field: "FACILITY_TYPE", width: 100 },
|
||||
{ title: "설비길이", field: "FACILITY_DEPTH", width: 90 },
|
||||
{ title: "담당자", field: "WRITER_NAME", width: 80 },
|
||||
{ title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" },
|
||||
{
|
||||
title: "첨부", field: "CU01_CNT", width: 60, hozAlign: "center",
|
||||
formatter: (_c, row) => (
|
||||
<FolderCell
|
||||
count={row.CU01_CNT}
|
||||
onClick={
|
||||
Number(row.CU01_CNT) > 0
|
||||
? () => window.open(`/common/files?objId=${row.OBJID}&docType=contractMgmt01`, "files", "width=800,height=500")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// ----- 진행사항 -----
|
||||
{
|
||||
title: "검토", field: "CU03_CNT", width: 60, hozAlign: "right",
|
||||
formatter: (_c, row) => Number(row.CU03_CNT || 0) > 0 ? String(row.CU03_CNT) : "",
|
||||
},
|
||||
{ title: "상태", field: "CONTRACT_RESULT_NAME", width: 80 },
|
||||
// ----- 수주정보 -----
|
||||
{ title: "수주일", field: "CONTRACT_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "PO계약 No", field: "PO_NO", width: 110 },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 80 },
|
||||
{ title: "통화", field: "CONTRACT_CURRENCY_NAME", width: 70 },
|
||||
{ title: "수주가", field: "CONTRACT_PRICE_CURRENCY", width: 110, formatter: "money", hozAlign: "right" },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 180 },
|
||||
{ title: "계약납기일", field: "CONTRACT_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "수주회사", field: "CONTRACT_COMPANY_NAME", width: 90 },
|
||||
{ title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 90 },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/sales/contract", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
category_cd: categoryCd,
|
||||
customer_objid: customerObjid,
|
||||
product,
|
||||
contract_result: contractResult,
|
||||
pm_user_id: pmUserId,
|
||||
contract_start_date: contractStartDate,
|
||||
contract_end_date: contractEndDate,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, categoryCd, customerObjid, product, contractResult, pmUserId, contractStartDate, contractEndDate]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택한 항목이 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const r = await Swal.fire({
|
||||
title: "선택한 계약을 삭제하시겠습니까?",
|
||||
icon: "warning", showCancelButton: true,
|
||||
confirmButtonText: "확인", cancelButtonText: "취소",
|
||||
});
|
||||
if (!r.isConfirmed) return;
|
||||
const res = await fetch("/api/sales/contract/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) {
|
||||
Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message || "삭제 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">영업관리_계약관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" onClick={() => openContractForm()}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] 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="계약구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={categoryCd} onChange={setCategoryCd} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<select value={customerObjid} onChange={(e) => setCustomerObjid(e.target.value)}
|
||||
className="h-9 w-[160px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
<option value="">전체</option>
|
||||
{customers.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="계약일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={contractStartDate} onChange={(e) => setContractStartDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-xs text-gray-500">~</span>
|
||||
<Input type="date" value={contractEndDate} onChange={(e) => setContractEndDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="수주결과">
|
||||
<SearchableCodeSelect codeId="0000963" value={contractResult} onChange={setContractResult} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<select value={pmUserId} onChange={(e) => setPmUserId(e.target.value)}
|
||||
className="h-9 w-[140px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
<option value="">전체</option>
|
||||
{pmUsers.map((u) => <option key={u.value} value={u.value}>{u.label}</option>)}
|
||||
</select>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="text-xs text-gray-500 mb-2">총 {data.length}건</div>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 영업관리 > 고객관리 (원본: contractMgmt/supplyMngList.jsp)
|
||||
export default function CustomerPage() {
|
||||
const [supplyName, setSupplyName] = useState("");
|
||||
const [supplyCode, setSupplyCode] = useState("");
|
||||
const [areaCd, setAreaCd] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const openPopup = (objid: string = "") => {
|
||||
const q = objid ? `?objid=${encodeURIComponent(objid)}` : "";
|
||||
window.open(`/sales/customer/form${q}`, "customerForm", "width=900,height=560");
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "고객번호", field: "CUS_NO", width: 110,
|
||||
cellClick: (row) => openPopup(String(row.OBJID || "")),
|
||||
},
|
||||
{ title: "고객구분", field: "SUPPLY_CODE_NAME", width: 100 },
|
||||
{ title: "지역", field: "AREA_CD_NAME", width: 110 },
|
||||
{ title: "고객사", field: "SUPPLY_NAME", width: 220 },
|
||||
{ title: "대표자명", field: "CHARGE_USER_NAME", width: 110 },
|
||||
{ title: "사업자등록번호", field: "BUS_REG_NO", width: 140 },
|
||||
{ title: "주소", field: "SUPPLY_ADDRESS", width: 280 },
|
||||
{ title: "연락처", field: "SUPPLY_TEL_NO", width: 130 },
|
||||
{ title: "E-MAIL", field: "EMAIL", width: 220 },
|
||||
{ title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/sales/customer", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
supply_name: supplyName,
|
||||
supply_code: supplyCode,
|
||||
area_cd: areaCd,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [supplyName, supplyCode, areaCd]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택한 항목이 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const r = await Swal.fire({
|
||||
title: "선택한 고객정보를 삭제하시겠습니까?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "확인",
|
||||
cancelButtonText: "취소",
|
||||
});
|
||||
if (!r.isConfirmed) return;
|
||||
const res = await fetch("/api/sales/customer/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) {
|
||||
Swal.fire({ icon: "success", title: "삭제되었습니다.", timer: 1200, showConfirmButton: false });
|
||||
fetchData();
|
||||
} else {
|
||||
Swal.fire("오류", json.message || "삭제 실패", "error");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">영업관리_고객관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
<Button size="sm" onClick={() => openPopup()}>고객등록</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="고객사명">
|
||||
<Input value={supplyName} onChange={(e) => setSupplyName(e.target.value)} className="w-[200px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={supplyCode} onChange={setSupplyCode} className="w-[180px]" />
|
||||
</SearchField>
|
||||
<SearchField label="지역">
|
||||
<SearchableCodeSelect codeId="0000172" value={areaCd} onChange={setAreaCd} className="w-[180px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="text-xs text-gray-500 mb-2">총 {data.length}건</div>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// salesMng/salesMngBOMList.jsp 대응 - 영업관리
|
||||
export default function SalesPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [customerName, setCustomerName] = useState("");
|
||||
const [productCode, setProductCode] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/sales/contract/form?objId=${row.OBJID}`, "salesDetail", "width=1100,height=800") },
|
||||
{ title: "제품코드", field: "PRODUCT_CODE", width: 120, hozAlign: "left" },
|
||||
{ title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center" },
|
||||
{ title: "수량", field: "QTY", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "단가", field: "UNIT_PRICE", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "금액", field: "AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/sales", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, customer_name: customerName, product_code: productCode }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, customerName, productCode]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">영업관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<Input value={customerName} onChange={(e) => setCustomerName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="제품코드">
|
||||
<CodeSelect codeId="0005116" value={productCode} onChange={setProductCode} placeholder="전체" className="w-[140px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} showCheckbox />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
"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 { SearchableCodeSelect } from "@/components/ui/searchable-code-select";
|
||||
import { FolderCell } from "@/components/ui/folder-cell";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
// 영업관리 > 출고관리 (원본: releaseMgmt/releaseMgmtList.jsp)
|
||||
export default function ReleasePage() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [year, setYear] = useState(String(currentYear));
|
||||
const [categoryCd, setCategoryCd] = useState("");
|
||||
const [customerObjid, setCustomerObjid] = useState("");
|
||||
const [product, setProduct] = useState("");
|
||||
const [pmUserId, setPmUserId] = useState("");
|
||||
const [releaseStartDate, setReleaseStartDate] = useState("");
|
||||
const [releaseEndDate, setReleaseEndDate] = useState("");
|
||||
const [installResult, setInstallResult] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]);
|
||||
const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json()).then((d) => setCustomers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.OBJID || ""), label: String(r.SUPPLY_NAME || ""),
|
||||
})))).catch(() => {});
|
||||
fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json()).then((d) => setPmUsers((d.RESULTLIST || []).map((r: Record<string, unknown>) => ({
|
||||
value: String(r.USER_ID || ""), label: String(r.USER_NAME || ""),
|
||||
})))).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const openForm = (row: Record<string, unknown>) => {
|
||||
const projectObjId = String(row.OBJID || "");
|
||||
if (!projectObjId) return;
|
||||
const w = 900, h = 760;
|
||||
const left = Math.max(0, (window.screen.width - w) / 2);
|
||||
const top = Math.max(0, (window.screen.availHeight - h) / 2);
|
||||
window.open(`/sales/release/form?projectObjId=${projectObjId}`, "releaseForm", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트번호", field: "PROJECT_NO", width: 120, frozen: true,
|
||||
cellClick: (row) => openForm(row),
|
||||
},
|
||||
// 프로젝트정보
|
||||
{ title: "계약구분", field: "CATEGORY_NAME", width: 80 },
|
||||
{ title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "right" },
|
||||
{ title: "국내/해외", field: "AREA_NAME", width: 80 },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 160 },
|
||||
{ title: "제품구분", field: "PRODUCT_NAME", width: 90 },
|
||||
{ title: "기계형식", field: "MECHANICAL_TYPE", width: 100 },
|
||||
{ title: "당사프로젝트명", field: "PROJECT_NAME", width: 180 },
|
||||
{ title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "입고지", field: "LOCATION", width: 90 },
|
||||
{ title: "셋업지", field: "SETUP", width: 90 },
|
||||
{ title: "설비방향", field: "FACILITY_NAME", width: 80 },
|
||||
{ title: "설비대수", field: "FACILITY_QTY", width: 70, formatter: "money", hozAlign: "right" },
|
||||
{ title: "설비타입", field: "FACILITY_TYPE", width: 90 },
|
||||
{ title: "설비길이", field: "FACILITY_DEPTH", width: 80 },
|
||||
{ title: "PM", field: "PM_USER_NAME", width: 80 },
|
||||
{ title: "계약납기일", field: "CONTRACT_DEL_DATE", width: 100, hozAlign: "center" },
|
||||
// 출고정보
|
||||
{
|
||||
title: "출고검사", field: "RELEASE_CHECK_CNT", width: 75, hozAlign: "center",
|
||||
formatter: (_c, row) => (
|
||||
<FolderCell
|
||||
count={row.RELEASE_CHECK_CNT}
|
||||
onClick={
|
||||
Number(row.RELEASE_CHECK_CNT) > 0
|
||||
? () => window.open(`/common/files?objId=${row.OBJID}&docType=RELEASE_CHECK`, "files", "width=800,height=500")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "출하지시", field: "RELEASE_ORDER_CNT", width: 75, hozAlign: "center",
|
||||
formatter: (_c, row) => (
|
||||
<FolderCell
|
||||
count={row.RELEASE_ORDER_CNT}
|
||||
onClick={
|
||||
Number(row.RELEASE_ORDER_CNT) > 0
|
||||
? () => window.open(`/common/files?objId=${row.OBJID}&docType=RELEASE_ORDER`, "files", "width=800,height=500")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ title: "출고일", field: "RELEASE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "출고결과", field: "RELEASE_STATUS_TITLE", width: 80 },
|
||||
// 설치&시운전
|
||||
{ title: "설치완료일", field: "INSTALL_COMPLETE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "설치결과", field: "INSTALL_RESULT", width: 80 },
|
||||
{
|
||||
title: "인수인계", field: "RELEASE_TAKING_OVER_CNT", width: 80, hozAlign: "center",
|
||||
formatter: (_c, row) => (
|
||||
<FolderCell
|
||||
count={row.RELEASE_TAKING_OVER_CNT}
|
||||
onClick={
|
||||
Number(row.RELEASE_TAKING_OVER_CNT) > 0
|
||||
? () => window.open(`/common/files?objId=${row.OBJID}&docType=RELEASE_TAKING_OVER`, "files", "width=800,height=500")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/sales/release", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Year: year,
|
||||
category_cd: categoryCd,
|
||||
customer_objid: customerObjid,
|
||||
product,
|
||||
pm_user_id: pmUserId,
|
||||
release_start_date: releaseStartDate,
|
||||
release_end_date: releaseEndDate,
|
||||
install_result: installResult,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, categoryCd, customerObjid, product, pmUserId, releaseStartDate, releaseEndDate, installResult]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const handleRegister = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "선택된 내용이 없습니다.", "warning");
|
||||
return;
|
||||
}
|
||||
if (selectedRows.length > 1) {
|
||||
Swal.fire("알림", "한번에 1개의 내용만 등록 가능합니다.", "warning");
|
||||
return;
|
||||
}
|
||||
openForm(selectedRows[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">영업관리_출고관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleRegister}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] 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="계약구분">
|
||||
<SearchableCodeSelect codeId="0000167" value={categoryCd} onChange={setCategoryCd} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<select value={customerObjid} onChange={(e) => setCustomerObjid(e.target.value)}
|
||||
className="h-9 w-[180px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
<option value="">전체</option>
|
||||
{customers.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="제품구분">
|
||||
<SearchableCodeSelect codeId="0000001" value={product} onChange={setProduct} className="w-[130px]" />
|
||||
</SearchField>
|
||||
<SearchField label="출고일">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input type="date" value={releaseStartDate} onChange={(e) => setReleaseStartDate(e.target.value)} className="w-[140px]" />
|
||||
<span className="text-xs text-gray-500">~</span>
|
||||
<Input type="date" value={releaseEndDate} onChange={(e) => setReleaseEndDate(e.target.value)} className="w-[140px]" />
|
||||
</div>
|
||||
</SearchField>
|
||||
<SearchField label="PM">
|
||||
<select value={pmUserId} onChange={(e) => setPmUserId(e.target.value)}
|
||||
className="h-9 w-[130px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
<option value="">전체</option>
|
||||
{pmUsers.map((u) => <option key={u.value} value={u.value}>{u.label}</option>)}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="설치결과">
|
||||
<select value={installResult} onChange={(e) => setInstallResult(e.target.value)}
|
||||
className="h-9 w-[110px] rounded border border-gray-300 bg-white px-2 text-sm">
|
||||
<option value="">전체</option>
|
||||
<option value="OPEN">OPEN</option>
|
||||
<option value="CLOSED">CLOSED</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="text-xs text-gray-500 mb-2">총 {data.length}건 (수주 완료된 계약만 표시)</div>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// scm/scmDefectList.jsp 대응 - 부적합품관리
|
||||
export default function ScmDefectPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [supplierName, setSupplierName] = useState("");
|
||||
const [statusCode, setStatusCode] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "부적합번호", field: "DEFECT_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/scm/defect/form?objId=${row.OBJID}`, "defectDetail", "width=900,height=600") },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left" },
|
||||
{ title: "파트명", field: "PART_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "부적합유형", field: "DEFECT_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "발생일", field: "DEFECT_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "수량", field: "DEFECT_QTY", width: 70, hozAlign: "right" },
|
||||
{ title: "조치내용", field: "ACTION_CONTENT", width: 200, hozAlign: "left" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/scm/defect", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
supplier_name: supplierName,
|
||||
status_code: statusCode,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, supplierName, statusCode]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">부적합품관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => window.open("/scm/defect/form", "defectForm", "width=900,height=600")}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<Input value={supplierName} onChange={(e) => setSupplierName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<CodeSelect codeId="DEFECT_STATUS" value={statusCode} onChange={setStatusCode} className="w-[120px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
"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";
|
||||
|
||||
// scm/scmInvoiceList.jsp 대응 - SCM 거래명세서관리
|
||||
export default function ScmInvoicePage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [supplierName, setSupplierName] = useState("");
|
||||
const [invoiceDateFrom, setInvoiceDateFrom] = useState("");
|
||||
const [invoiceDateTo, setInvoiceDateTo] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "거래명세서번호", field: "INVOICE_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/scm/invoice/form?objId=${row.OBJID}`, "scmInvoiceDetail", "width=1000,height=700") },
|
||||
{ title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "발행일", field: "INVOICE_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "공급가액", field: "SUPPLY_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "세액", field: "TAX_AMOUNT", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "합계", field: "TOTAL_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/scm/invoice", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
supplier_name: supplierName,
|
||||
invoice_date_from: invoiceDateFrom,
|
||||
invoice_date_to: invoiceDateTo,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, supplierName, invoiceDateFrom, invoiceDateTo]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">SCM 거래명세서관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<Input value={supplierName} onChange={(e) => setSupplierName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발행일(From)">
|
||||
<Input type="date" value={invoiceDateFrom} onChange={(e) => setInvoiceDateFrom(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="발행일(To)">
|
||||
<Input type="date" value={invoiceDateTo} onChange={(e) => setInvoiceDateTo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface PartRow {
|
||||
PART_OBJID: string; PART_NO: string; PART_NAME: string;
|
||||
ORDER_QTY: string; ARRIVAL_OBJID?: string;
|
||||
ARRIVAL_QTY: string; ARRIVAL_PLAN_DATE: string;
|
||||
}
|
||||
|
||||
export default function ArrivalPlanPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const objId = searchParams.get("objId") || "";
|
||||
const [parts, setParts] = useState<PartRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!objId) return;
|
||||
const res = await fetch("/api/delivery/acceptance/detail", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objId }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
setParts((json.PARTS || []).map((p: Record<string, unknown>) => ({
|
||||
PART_OBJID: String(p.PART_OBJID || ""),
|
||||
PART_NO: String(p.PART_NO || ""), PART_NAME: String(p.PART_NAME || ""),
|
||||
ORDER_QTY: String(p.ORDER_QTY || "0"),
|
||||
ARRIVAL_OBJID: String(p.ARRIVAL_OBJID || ""),
|
||||
ARRIVAL_QTY: String(p.ARRIVAL_QTY || p.ORDER_QTY || "0"),
|
||||
ARRIVAL_PLAN_DATE: String(p.ARRIVAL_PLAN_DATE || ""),
|
||||
})));
|
||||
}
|
||||
}, [objId]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const updatePart = (i: number, k: keyof PartRow, v: string) =>
|
||||
setParts((prev) => { const n = [...prev]; n[i] = { ...n[i], [k]: v }; return n; });
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const items = parts.map((p) => ({
|
||||
objId: p.ARRIVAL_OBJID || undefined,
|
||||
parent_objid: objId, order_part_objid: p.PART_OBJID, part_objid: p.PART_OBJID,
|
||||
arrival_qty: p.ARRIVAL_QTY, arrival_plan_date: p.ARRIVAL_PLAN_DATE,
|
||||
receipt_qty: "0",
|
||||
}));
|
||||
const res = await fetch("/api/delivery/acceptance/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ icon: "success", title: "입고계획이 등록되었습니다.", timer: 1500, showConfirmButton: false });
|
||||
if (window.opener) { try { window.opener.location.reload(); } catch {} }
|
||||
} else Swal.fire("오류", json.message, "error");
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-4">입고계획 등록</h2>
|
||||
<div className="border rounded overflow-auto" style={{ maxHeight: "calc(100vh - 200px)" }}>
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th className="p-2 text-left">PART NO</th>
|
||||
<th className="p-2 text-left">PART NAME</th>
|
||||
<th className="p-2 w-[70px]">발주수량</th>
|
||||
<th className="p-2 w-[80px]">계획수량</th>
|
||||
<th className="p-2 w-[110px]">입고계획일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parts.map((p, i) => (
|
||||
<tr key={i} className="border-t">
|
||||
<td className="p-1.5">{p.PART_NO}</td>
|
||||
<td className="p-1.5">{p.PART_NAME}</td>
|
||||
<td className="p-1.5 text-right">{p.ORDER_QTY}</td>
|
||||
<td className="p-1"><Input type="number" value={p.ARRIVAL_QTY} onChange={(e) => updatePart(i, "ARRIVAL_QTY", e.target.value)} className="h-7 text-xs text-right" /></td>
|
||||
<td className="p-1"><Input type="date" value={p.ARRIVAL_PLAN_DATE} onChange={(e) => updatePart(i, "ARRIVAL_PLAN_DATE", e.target.value)} className="h-7 text-xs" /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-3 mt-4">
|
||||
<Button onClick={handleSave} disabled={loading}>{loading ? "저장 중..." : "저장"}</Button>
|
||||
<Button variant="secondary" onClick={() => window.close()}>닫기</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// scm/scmOrderList.jsp 대응 - SCM 발주관리
|
||||
export default function ScmOrderPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [purchaseOrderNo, setPurchaseOrderNo] = useState("");
|
||||
const [customerName, setCustomerName] = useState("");
|
||||
const [projectNo, setProjectNo] = useState("");
|
||||
const [statusCode, setStatusCode] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "발주No", field: "PURCHASE_ORDER_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/scm/order/form?objId=${row.OBJID}`, "scmOrderDetail", "width=1100,height=700") },
|
||||
{ title: "복합마스터", field: "MULTI_MASTER_YN", width: 90, hozAlign: "center" },
|
||||
{ title: "영업담당", field: "SALES_MNG_USER_NAME", width: 90, hozAlign: "center" },
|
||||
{ title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" },
|
||||
{ title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 120, hozAlign: "left" },
|
||||
{ title: "발주명", field: "TITLE", width: 200, hozAlign: "left" },
|
||||
{ title: "납기일", field: "DELIVERY_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "납품장소", field: "DELIVERY_PLACE_NAME", width: 120, hozAlign: "left" },
|
||||
{ title: "파트수", field: "PART_CNT", width: 70, hozAlign: "right" },
|
||||
{ title: "실발주수", field: "REAL_ORDER_CNT", width: 80, hozAlign: "right" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "입고수", field: "ARRIVAL_CNT", width: 70, hozAlign: "right" },
|
||||
{ title: "영업상태", field: "SALES_STATUS", width: 80, hozAlign: "center" },
|
||||
{ title: "입고수량", field: "RECEIPT_QTY", width: 80, hozAlign: "right" },
|
||||
{ title: "미납수량", field: "NON_DELIVERY_QTY", width: 80, hozAlign: "right" },
|
||||
{ title: "불량수량", field: "ERROR_QTY", width: 80, hozAlign: "right" },
|
||||
{ title: "발행일", field: "ISSUANCE_DATE", width: 100, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/scm/order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
purchase_order_no: purchaseOrderNo,
|
||||
customer_name: customerName,
|
||||
project_no: projectNo,
|
||||
status_code: statusCode,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, purchaseOrderNo, customerName, projectNo, statusCode]);
|
||||
|
||||
const handleReceipt = async () => {
|
||||
if (selectedRows.length === 0) {
|
||||
alert("접수할 항목을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
// 선택 건들 일괄 접수 처리 (reception_status='reception')
|
||||
const objIds = selectedRows.map((r) => String(r.OBJID));
|
||||
const res = await fetch("/api/scm/order/receipt", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objIds }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
alert(json.message || "접수 완료");
|
||||
fetchData();
|
||||
} else {
|
||||
alert(json.message || "접수 실패");
|
||||
}
|
||||
};
|
||||
|
||||
const handleArrivalPlan = () => {
|
||||
if (selectedRows.length === 0) {
|
||||
alert("입고계획을 등록할 항목을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
const w = 900, h = 600;
|
||||
const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2;
|
||||
window.open(`/scm/order/arrival-plan?objId=${row.OBJID}`, "arrivalPlan",
|
||||
`width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">SCM 발주관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={handleReceipt}>접수</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleArrivalPlan}>입고계획등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="발주번호">
|
||||
<Input value={purchaseOrderNo} onChange={(e) => setPurchaseOrderNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="고객사">
|
||||
<Input value={customerName} onChange={(e) => setCustomerName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<Input value={projectNo} onChange={(e) => setProjectNo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<CodeSelect codeId="SCM_ORDER_STATUS" value={statusCode} onChange={setStatusCode} className="w-[120px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// scm/scmPaymentList.jsp 대응 - 자금지급관리
|
||||
export default function ScmPaymentPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [supplierName, setSupplierName] = useState("");
|
||||
const [paymentDateFrom, setPaymentDateFrom] = useState("");
|
||||
const [paymentDateTo, setPaymentDateTo] = useState("");
|
||||
const [statusCode, setStatusCode] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "지급번호", field: "PAYMENT_NO", width: 140, hozAlign: "left",
|
||||
cellClick: (row) => window.open(`/scm/payment/form?objId=${row.OBJID}`, "paymentDetail", "width=1000,height=700") },
|
||||
{ title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" },
|
||||
{ title: "지급일", field: "PAYMENT_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "지급금액", field: "PAYMENT_AMOUNT", width: 120, hozAlign: "right", formatter: "money" },
|
||||
{ title: "지급구분", field: "PAYMENT_TYPE_NAME", width: 100, hozAlign: "center" },
|
||||
{ title: "계좌번호", field: "ACCOUNT_NO", width: 150, hozAlign: "left" },
|
||||
{ title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" },
|
||||
{ title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/scm/payment", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
supplier_name: supplierName,
|
||||
payment_date_from: paymentDateFrom,
|
||||
payment_date_to: paymentDateTo,
|
||||
status_code: statusCode,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, supplierName, paymentDateFrom, paymentDateTo, statusCode]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">자금지급관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onClick={() => window.open("/scm/payment/form", "paymentForm", "width=1000,height=700")}>등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<Input value={supplierName} onChange={(e) => setSupplierName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="지급일(From)">
|
||||
<Input type="date" value={paymentDateFrom} onChange={(e) => setPaymentDateFrom(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="지급일(To)">
|
||||
<Input type="date" value={paymentDateTo} onChange={(e) => setPaymentDateTo(e.target.value)} className="w-[140px]" />
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<CodeSelect codeId="PAYMENT_STATUS" value={statusCode} onChange={setStatusCode} className="w-[120px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
"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 { CodeSelect } from "@/components/ui/code-select";
|
||||
|
||||
// scm/scmQualityList.jsp 대응 - 공급업체품질관리
|
||||
export default function ScmQualityPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [supplierName, setSupplierName] = useState("");
|
||||
const [evaluationPeriod, setEvaluationPeriod] = useState("");
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" },
|
||||
{ title: "평가기간", field: "EVALUATION_PERIOD", width: 100, hozAlign: "center" },
|
||||
{ title: "납기준수율%", field: "DELIVERY_RATE", width: 100, hozAlign: "right" },
|
||||
{ title: "품질점수", field: "QUALITY_SCORE", width: 80, hozAlign: "right" },
|
||||
{ title: "불량율%", field: "DEFECT_RATE", width: 80, hozAlign: "right" },
|
||||
{ title: "납품건수", field: "DELIVERY_CNT", width: 80, hozAlign: "right" },
|
||||
{ title: "불량건수", field: "DEFECT_CNT", width: 80, hozAlign: "right" },
|
||||
{ title: "종합등급", field: "TOTAL_GRADE", width: 80, hozAlign: "center" },
|
||||
{ title: "평가일", field: "EVALUATION_DATE", width: 100, hozAlign: "center" },
|
||||
{ title: "평가자", field: "EVALUATOR_NAME", width: 80, hozAlign: "center" },
|
||||
];
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/scm/quality", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
supplier_name: supplierName,
|
||||
evaluation_period: evaluationPeriod,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, supplierName, evaluationPeriod]);
|
||||
|
||||
// 페이지 진입 시 자동 로드
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">공급업체품질관리</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[100px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="공급업체">
|
||||
<Input value={supplierName} onChange={(e) => setSupplierName(e.target.value)} className="w-[150px]" />
|
||||
</SearchField>
|
||||
<SearchField label="평가기간">
|
||||
<CodeSelect codeId="EVALUATION_PERIOD" value={evaluationPeriod} onChange={setEvaluationPeriod} className="w-[120px]" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid columns={columns} data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
import { DataGrid, type GridColumn } from "@/components/grid/data-grid";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
|
||||
// productionplanning/workDiaryList.jsp 대응 - 작업일지 목록
|
||||
interface Option { value: string; label: string }
|
||||
|
||||
export default function WorkDiaryPage() {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [searchDivision, setSearchDivision] = useState("");
|
||||
const [projectObjId, setProjectObjId] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [deptCode, setDeptCode] = useState("");
|
||||
const [worker, setWorker] = useState("");
|
||||
const [searchStatus, setSearchStatus] = useState("");
|
||||
|
||||
const [projectOptions, setProjectOptions] = useState<Option[]>([]);
|
||||
const [unitOptions, setUnitOptions] = useState<Option[]>([]);
|
||||
const [deptOptions, setDeptOptions] = useState<Option[]>([]);
|
||||
const [workerOptions, setWorkerOptions] = useState<Option[]>([]);
|
||||
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
// 공용 드롭다운
|
||||
useEffect(() => {
|
||||
fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const rows = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setProjectOptions(rows.map((r) => ({
|
||||
value: String(r.OBJID ?? ""),
|
||||
label: String(r.LABEL ?? r.PROJECT_NO ?? ""),
|
||||
})));
|
||||
})
|
||||
.catch(() => setProjectOptions([]));
|
||||
|
||||
fetch("/api/common/dept-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const rows = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setDeptOptions(rows.map((r) => ({ value: String(r.DEPT_CODE ?? ""), label: String(r.DEPT_NAME ?? "") })));
|
||||
})
|
||||
.catch(() => setDeptOptions([]));
|
||||
|
||||
fetch("/api/common/user-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const rows = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setWorkerOptions(rows.map((r) => ({
|
||||
value: String(r.USER_ID ?? ""),
|
||||
label: `${r.DEPT_NAME ?? ""} / ${r.USER_NAME ?? ""}`,
|
||||
})));
|
||||
})
|
||||
.catch(() => setWorkerOptions([]));
|
||||
}, []);
|
||||
|
||||
const handleProjectChange = useCallback((next: string) => {
|
||||
setProjectObjId(next);
|
||||
setUnitCode("");
|
||||
if (!next) { setUnitOptions([]); return; }
|
||||
fetch("/api/common/unit-list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ contract_objid: next }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const list = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setUnitOptions(list.map((r) => ({ value: String(r.OBJID ?? ""), label: String(r.UNIT_NAME ?? "") })));
|
||||
})
|
||||
.catch(() => setUnitOptions([]));
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/work/diary", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
search_division: searchDivision,
|
||||
project_nos: projectObjId ? [projectObjId] : [],
|
||||
unit_code: unitCode,
|
||||
busUsersDeptId: deptCode,
|
||||
worker,
|
||||
search_status: searchStatus,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, searchDivision, projectObjId, unitCode, deptCode, worker, searchStatus]);
|
||||
|
||||
useEffect(() => { fetchData(); }, [fetchData]);
|
||||
|
||||
const openFormPopup = (objId?: string) => {
|
||||
const w = 1200; const h = 600;
|
||||
const left = (window.screen.width - w) / 2;
|
||||
const top = (window.screen.height - h) / 2;
|
||||
const url = objId ? `/work/diary/form?objId=${encodeURIComponent(objId)}` : "/work/diary/form";
|
||||
window.open(url, "diaryForm", `width=${w},height=${h},left=${left},top=${top}`);
|
||||
};
|
||||
|
||||
const collectWriteIds = () => selectedRows
|
||||
.filter((r) => r.STATUS === "write")
|
||||
.map((r) => String(r.OBJID));
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (selectedRows.length === 0) { Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); return; }
|
||||
const ids = collectWriteIds();
|
||||
if (ids.length === 0) { Swal.fire("알림", "작성중인 데이터만 삭제 가능합니다.", "warning"); return; }
|
||||
|
||||
const confirm = await Swal.fire({
|
||||
title: "선택된 데이터를 삭제 하시겠습니까?",
|
||||
icon: "warning", showCancelButton: true,
|
||||
confirmButtonText: "확인", cancelButtonText: "취소",
|
||||
});
|
||||
if (!confirm.isConfirmed) return;
|
||||
|
||||
const res = await fetch("/api/work/diary/delete", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ checkArr: ids }),
|
||||
});
|
||||
const json = await res.json();
|
||||
await Swal.fire(json.msg || (json.success ? "삭제되었습니다." : "삭제 실패"), "", json.success ? "success" : "error");
|
||||
if (json.success) fetchData();
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (selectedRows.length === 0) { Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); return; }
|
||||
const ids = collectWriteIds();
|
||||
if (ids.length === 0) { Swal.fire("알림", "등록중인 데이터만 배포 가능합니다.", "warning"); return; }
|
||||
|
||||
const confirm = await Swal.fire({
|
||||
title: "선택된 데이터를 확정 하시겠습니까?",
|
||||
icon: "warning", showCancelButton: true,
|
||||
confirmButtonText: "확인", cancelButtonText: "취소",
|
||||
});
|
||||
if (!confirm.isConfirmed) return;
|
||||
|
||||
const res = await fetch("/api/work/diary/confirm", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ checkArr: ids }),
|
||||
});
|
||||
const json = await res.json();
|
||||
await Swal.fire(json.msg || (json.success ? "확정되었습니다." : "확정 실패"), "", json.success ? "success" : "error");
|
||||
if (json.success) fetchData();
|
||||
};
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "구분", field: "DIVISION", width: 110, hozAlign: "center",
|
||||
formatter: (v) => v === "project" ? "프로젝트" : v === "non_project" ? "비프로젝트" : String(v ?? ""),
|
||||
cellClick: (row) => openFormPopup(String(row.OBJID)),
|
||||
},
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_CODE_NAME", width: 260, hozAlign: "left" },
|
||||
{ title: "TASK명", field: "TASK_NAME", width: 320, hozAlign: "left" },
|
||||
{ title: "팀명", field: "DEPT_NAME", width: 110, hozAlign: "center" },
|
||||
{ title: "작업자", field: "WORKER_NAME", width: 110, hozAlign: "center" },
|
||||
{ title: "작업시작일", field: "WORK_START_DATE", width: 110, hozAlign: "center" },
|
||||
{ title: "작업종료일", field: "WORK_END_DATE", width: 110, hozAlign: "center" },
|
||||
{ title: "작업시간", field: "WORK_HOUR", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "상태", field: "STATUS_TITLE", width: 90, hozAlign: "center" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold text-gray-800">작업일지</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="danger" onClick={handleDelete}>삭제</Button>
|
||||
<Button size="sm" onClick={() => openFormPopup()}>작업일지등록</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleConfirm}>팀장확인</Button>
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[110px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="구분">
|
||||
<select value={searchDivision} onChange={(e) => setSearchDivision(e.target.value)}
|
||||
className="h-9 w-[130px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
<option value="project">프로젝트</option>
|
||||
<option value="non_project">비프로젝트</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<SearchableSelect options={projectOptions} value={projectObjId} onChange={handleProjectChange}
|
||||
className="w-[240px]" placeholder="선택" />
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<SearchableSelect options={unitOptions} value={unitCode} onChange={setUnitCode}
|
||||
className="w-[200px]" placeholder="선택" disabled={!projectObjId} />
|
||||
</SearchField>
|
||||
<SearchField label="팀명">
|
||||
<SearchableSelect options={deptOptions} value={deptCode} onChange={setDeptCode}
|
||||
className="w-[160px]" placeholder="선택" />
|
||||
</SearchField>
|
||||
<SearchField label="작업자">
|
||||
<SearchableSelect options={workerOptions} value={worker} onChange={setWorker}
|
||||
className="w-[200px]" placeholder="선택" />
|
||||
</SearchField>
|
||||
<SearchField label="상태">
|
||||
<select value={searchStatus} onChange={(e) => setSearchStatus(e.target.value)}
|
||||
className="h-9 w-[130px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
<option value="">선택</option>
|
||||
<option value="write">작성중</option>
|
||||
<option value="complete">확인완료</option>
|
||||
</select>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
showCheckbox
|
||||
onSelectionChange={setSelectedRows}
|
||||
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Swal from "sweetalert2";
|
||||
import { DataGrid, type GridColumn } from "@/components/grid/data-grid";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { numberWithCommas } from "@/lib/utils";
|
||||
|
||||
// productionplanning/workStatusByProjectList.jsp + workStatusByImployeeList.jsp 대응
|
||||
// 탭: 프로젝트 작업현황 / 담당자별 작업현황
|
||||
type Tab = "project" | "employee";
|
||||
|
||||
interface Option { value: string; label: string }
|
||||
|
||||
interface ProjectSumMap {
|
||||
SUM_DESIGN_INPUT?: number;
|
||||
SUM_PURCHASE_INPUT?: number;
|
||||
SUM_SALES_INPUT?: number;
|
||||
SUM_PRODUCTION_MGMT_INPUT?: number;
|
||||
SUM_PRODUCTION_INPUT?: number;
|
||||
SUM_MGMT_INPUT?: number;
|
||||
SUM_OUTSOURCING?: number;
|
||||
SUM_WORK_HOUR?: number;
|
||||
SUM_MAN_DAY?: number;
|
||||
SUM_MAN_MONTH?: number;
|
||||
}
|
||||
|
||||
interface EmployeeSumMap {
|
||||
SUM_WORK_HOUR?: number;
|
||||
SUM_MAN_DAY?: number;
|
||||
SUM_MAN_MONTH?: number;
|
||||
}
|
||||
|
||||
export default function WorkStatusPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="p-8 text-center text-gray-400">로딩 중...</div>}>
|
||||
<WorkStatusContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkStatusContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const tabParam = searchParams.get("tab");
|
||||
const [tab, setTab] = useState<Tab>(tabParam === "employee" ? "employee" : "project");
|
||||
|
||||
// 메뉴 재클릭 등으로 ?tab= 변경될 때 탭 동기화
|
||||
useEffect(() => {
|
||||
if (tabParam === "employee" || tabParam === "project") setTab(tabParam);
|
||||
}, [tabParam]);
|
||||
|
||||
// 공용 드롭다운 옵션
|
||||
const [projectOptions, setProjectOptions] = useState<Option[]>([]);
|
||||
const [deptOptions, setDeptOptions] = useState<Option[]>([]);
|
||||
const [workerOptions, setWorkerOptions] = useState<Option[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const rows = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setProjectOptions(rows.map((r) => ({
|
||||
value: String(r.OBJID ?? ""),
|
||||
label: String(r.LABEL ?? r.PROJECT_NO ?? ""),
|
||||
})));
|
||||
})
|
||||
.catch(() => setProjectOptions([]));
|
||||
|
||||
fetch("/api/common/dept-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const rows = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setDeptOptions(rows.map((r) => ({
|
||||
value: String(r.DEPT_CODE ?? ""),
|
||||
label: String(r.DEPT_NAME ?? ""),
|
||||
})));
|
||||
})
|
||||
.catch(() => setDeptOptions([]));
|
||||
|
||||
fetch("/api/common/user-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" })
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const rows = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setWorkerOptions(rows.map((r) => ({
|
||||
value: String(r.USER_ID ?? ""),
|
||||
label: `${r.DEPT_NAME ?? ""} / ${r.USER_NAME ?? ""}`,
|
||||
})));
|
||||
})
|
||||
.catch(() => setWorkerOptions([]));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-4">작업관리 현황</h2>
|
||||
|
||||
<div className="inline-flex bg-gray-100 rounded-lg p-1 mb-4">
|
||||
<TabButton active={tab === "project"} onClick={() => setTab("project")} label="프로젝트 작업현황" />
|
||||
<TabButton active={tab === "employee"} onClick={() => setTab("employee")} label="담당자별 작업현황" />
|
||||
</div>
|
||||
|
||||
{tab === "project" ? (
|
||||
<ProjectTab projectOptions={projectOptions} />
|
||||
) : (
|
||||
<EmployeeTab projectOptions={projectOptions} deptOptions={deptOptions} workerOptions={workerOptions} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButton({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
active ? "bg-white text-primary shadow-sm" : "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 프로젝트 작업현황 ────────────────────────────────────────
|
||||
function ProjectTab({ projectOptions }: { projectOptions: Option[] }) {
|
||||
const [projectObjId, setProjectObjId] = useState("");
|
||||
const [unitCode, setUnitCode] = useState("");
|
||||
const [unitOptions, setUnitOptions] = useState<Option[]>([]);
|
||||
const [rows, setRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [sumMap, setSumMap] = useState<ProjectSumMap>({});
|
||||
|
||||
const handleProjectChange = useCallback((next: string) => {
|
||||
setProjectObjId(next);
|
||||
setUnitCode("");
|
||||
if (!next) { setUnitOptions([]); return; }
|
||||
fetch("/api/common/unit-list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ contract_objid: next }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const list = (d.RESULTLIST || []) as Record<string, unknown>[];
|
||||
setUnitOptions(list.map((r) => ({
|
||||
value: String(r.OBJID ?? ""),
|
||||
label: String(r.UNIT_NAME ?? ""),
|
||||
})));
|
||||
})
|
||||
.catch(() => setUnitOptions([]));
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!projectObjId) {
|
||||
Swal.fire("알림", "프로젝트번호는 필수값입니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const res = await fetch("/api/work/status/project", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ project_nos: [projectObjId], unit_code: unitCode }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setRows(json.RESULTLIST || []);
|
||||
setSumMap(json.SUM_PRICE_MAP || {});
|
||||
}
|
||||
}, [projectObjId, unitCode]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{
|
||||
title: "프로젝트", headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 140, hozAlign: "left" },
|
||||
{ title: "유닛명", field: "UNIT_NAME", width: 200, hozAlign: "left" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "투입공수", headerHozAlign: "center",
|
||||
columns: [
|
||||
{ title: "영업", field: "SALES_INPUT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "관리", field: "MGMT_INPUT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "설계", field: "DESIGN_INPUT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "구매", field: "PURCHASE_INPUT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "생관", field: "PRODUCTION_MGMT_INPUT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "생산", field: "PRODUCTION_INPUT", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "외주", field: "OUTSOURCING", width: 80, hozAlign: "right", formatter: "money" },
|
||||
{ title: "작업시간(h)", field: "WORK_HOUR", width: 110, hozAlign: "right", formatter: "money" },
|
||||
{ title: "Day/Man", field: "MAN_DAY", width: 90, hozAlign: "right", formatter: "money" },
|
||||
{ title: "Month/Man", field: "MAN_MONTH", width: 100, hozAlign: "right", formatter: "money" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="프로젝트번호">
|
||||
<SearchableSelect
|
||||
options={projectOptions}
|
||||
value={projectObjId}
|
||||
onChange={handleProjectChange}
|
||||
className="w-[280px]"
|
||||
placeholder="선택"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="유닛명">
|
||||
<SearchableSelect
|
||||
options={unitOptions}
|
||||
value={unitCode}
|
||||
onChange={setUnitCode}
|
||||
className="w-[240px]"
|
||||
placeholder="선택"
|
||||
disabled={!projectObjId}
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="flex justify-end gap-2 mb-3">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
|
||||
<DataGrid columns={columns} data={rows} />
|
||||
|
||||
{/* 합계 */}
|
||||
{rows.length > 0 && (
|
||||
<div className="mt-2 border border-gray-200 rounded bg-gray-50 px-3 py-2 text-xs text-gray-700">
|
||||
<span className="font-semibold mr-4">(계)</span>
|
||||
<SumCell label="영업" value={sumMap.SUM_SALES_INPUT} />
|
||||
<SumCell label="관리" value={sumMap.SUM_MGMT_INPUT} />
|
||||
<SumCell label="설계" value={sumMap.SUM_DESIGN_INPUT} />
|
||||
<SumCell label="구매" value={sumMap.SUM_PURCHASE_INPUT} />
|
||||
<SumCell label="생관" value={sumMap.SUM_PRODUCTION_MGMT_INPUT} />
|
||||
<SumCell label="생산" value={sumMap.SUM_PRODUCTION_INPUT} />
|
||||
<SumCell label="외주" value={sumMap.SUM_OUTSOURCING} />
|
||||
<SumCell label="작업시간" value={sumMap.SUM_WORK_HOUR} />
|
||||
<SumCell label="Day/Man" value={sumMap.SUM_MAN_DAY} />
|
||||
<SumCell label="Month/Man" value={sumMap.SUM_MAN_MONTH} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 담당자별 작업현황 ────────────────────────────────────────
|
||||
function EmployeeTab({ projectOptions, deptOptions, workerOptions }: {
|
||||
projectOptions: Option[]; deptOptions: Option[]; workerOptions: Option[];
|
||||
}) {
|
||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||
const [projectObjId, setProjectObjId] = useState("");
|
||||
const [deptCode, setDeptCode] = useState("");
|
||||
const [worker, setWorker] = useState("");
|
||||
const [list, setList] = useState<Record<string, unknown>[]>([]);
|
||||
const [npList, setNpList] = useState<Record<string, unknown>[]>([]);
|
||||
const [sumMap, setSumMap] = useState<EmployeeSumMap>({});
|
||||
|
||||
// 부서 선택 시 작업자 목록 필터링
|
||||
const filteredWorkers = useMemo(() => {
|
||||
if (!deptCode) return workerOptions;
|
||||
// user-list 옵션 label을 '부서명 / 작업자'로 구성하므로 간단 필터링 불가 → 전체 재요청 생략
|
||||
return workerOptions;
|
||||
}, [deptCode, workerOptions]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!deptCode && !worker) {
|
||||
Swal.fire("알림", "팀명 또는 작업자는 필수값입니다.", "warning");
|
||||
return;
|
||||
}
|
||||
const res = await fetch("/api/work/status/employee", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
year,
|
||||
project_nos: projectObjId ? [projectObjId] : [],
|
||||
busUsersDeptId: deptCode,
|
||||
worker,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setList(json.LIST || []);
|
||||
setNpList(json.NP_LIST || []);
|
||||
setSumMap(json.SUM_PRICE_MAP || {});
|
||||
}
|
||||
}, [year, projectObjId, deptCode, worker]);
|
||||
|
||||
// 합쳐서 그리드에 표시: 프로젝트 행 + 합계 구분행 + 비프로젝트 행
|
||||
const combined = useMemo(() => {
|
||||
const projectRows = list.map((r) => ({ ...r, _DIVISION: "프로젝트" }));
|
||||
const npRows = npList.map((r) => ({ ...r, _DIVISION: "비프로젝트", PROJECT_NO: "" }));
|
||||
return [...projectRows, ...npRows];
|
||||
}, [list, npList]);
|
||||
|
||||
const columns: GridColumn[] = [
|
||||
{ title: "구분", field: "_DIVISION", width: 100, hozAlign: "left" },
|
||||
{ title: "프로젝트번호", field: "PROJECT_NO", width: 140, hozAlign: "left" },
|
||||
{ title: "팀명", field: "WORKER_DEPT_NAME", width: 120, hozAlign: "center" },
|
||||
{ title: "작업자", field: "WORKER_USER_NAME", width: 120, hozAlign: "center" },
|
||||
{ title: "작업시간", field: "WORK_HOUR", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "Day/Man", field: "MAN_DAY", width: 100, hozAlign: "right", formatter: "money" },
|
||||
{ title: "Month/Man", field: "MAN_MONTH", width: 110, hozAlign: "right", formatter: "money" },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="년도">
|
||||
<select value={year} onChange={(e) => setYear(e.target.value)}
|
||||
className="h-9 w-[110px] rounded border border-gray-300 bg-white px-3 text-sm">
|
||||
{Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i).map((y) => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</SearchField>
|
||||
<SearchField label="프로젝트번호">
|
||||
<SearchableSelect
|
||||
options={projectOptions}
|
||||
value={projectObjId}
|
||||
onChange={setProjectObjId}
|
||||
className="w-[260px]"
|
||||
placeholder="선택"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="팀명">
|
||||
<SearchableSelect
|
||||
options={deptOptions}
|
||||
value={deptCode}
|
||||
onChange={setDeptCode}
|
||||
className="w-[180px]"
|
||||
placeholder="선택"
|
||||
/>
|
||||
</SearchField>
|
||||
<SearchField label="작업자">
|
||||
<SearchableSelect
|
||||
options={filteredWorkers}
|
||||
value={worker}
|
||||
onChange={setWorker}
|
||||
className="w-[220px]"
|
||||
placeholder="선택"
|
||||
/>
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
|
||||
<div className="flex justify-end gap-2 mb-3">
|
||||
<Button size="sm" variant="secondary" onClick={fetchData}>조회</Button>
|
||||
</div>
|
||||
|
||||
<DataGrid columns={columns} data={combined} />
|
||||
|
||||
{/* 프로젝트 합계 */}
|
||||
{list.length > 0 && (
|
||||
<div className="mt-2 border border-gray-200 rounded bg-gray-50 px-3 py-2 text-xs text-gray-700">
|
||||
<span className="font-semibold mr-4">프로젝트 합계</span>
|
||||
<SumCell label="작업시간" value={sumMap.SUM_WORK_HOUR} />
|
||||
<SumCell label="Day/Man" value={sumMap.SUM_MAN_DAY} />
|
||||
<SumCell label="Month/Man" value={sumMap.SUM_MAN_MONTH} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SumCell({ label, value }: { label: string; value?: number }) {
|
||||
return (
|
||||
<span className="inline-block mr-5">
|
||||
<span className="text-gray-500">{label}:</span>{" "}
|
||||
<span className="font-semibold">{numberWithCommas(Number(value) || 0)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user