diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index 5b84008..0000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"c8c36d3d-1e9a-49fa-947f-2e00ae43f9b6","pid":20231,"procStart":"Tue May 12 08:01:45 2026","acquiredAt":1778576701825} \ No newline at end of file diff --git a/docs/~$모모유통 제조사 리스트(26.05.12).xlsx b/docs/~$2026_0410_ 금_라인업.xlsx similarity index 100% rename from docs/~$모모유통 제조사 리스트(26.05.12).xlsx rename to docs/~$2026_0410_ 금_라인업.xlsx diff --git a/src/app/(main)/approval/page.tsx b/src/app/(main)/approval/page.tsx deleted file mode 100644 index ed30618..0000000 --- a/src/app/(main)/approval/page.tsx +++ /dev/null @@ -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("PENDING"); - const [title, setTitle] = useState(""); - const [writerName, setWriterName] = useState(""); - const [fromDate, setFromDate] = useState(""); - const [toDate, setToDate] = useState(""); - const [data, setData] = useState[]>([]); - const [loading, setLoading] = useState(false); - - const openDetail = (row: Record) => { - 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) => , - }, - ]; - - 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).fn_search = fetchData; - return () => { delete (window as unknown as Record).fn_search; }; - }, [fetchData]); - - return ( -
-
-

결재관리

-
- -
-
- - {/* 탭 */} -
- {TABS.map((t) => ( - - ))} -
- - - - setTitle(e.target.value)} className="w-[240px]" /> - - - setWriterName(e.target.value)} className="w-[150px]" /> - - -
- setFromDate(e.target.value)} className="w-[140px]" /> - ~ - setToDate(e.target.value)} className="w-[140px]" /> -
-
-
- - -
- ); -} diff --git a/src/app/(main)/bom/page.tsx b/src/app/(main)/bom/page.tsx deleted file mode 100644 index fccb9bf..0000000 --- a/src/app/(main)/bom/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

BOM 관리

-
- -
-
- - - - setProductName(e.target.value)} className="w-[180px]" /> - - - setPartNo(e.target.value)} className="w-[130px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/cost-mgmt/page.tsx b/src/app/(main)/cost-mgmt/page.tsx deleted file mode 100644 index 08137d8..0000000 --- a/src/app/(main)/cost-mgmt/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

원가관리

-
- -
-
- - - - - - setProjectNo(e.target.value)} className="w-[130px]" /> - - - -
- ); -} diff --git a/src/app/(main)/cost/expense/page.tsx b/src/app/(main)/cost/expense/page.tsx deleted file mode 100644 index c895d80..0000000 --- a/src/app/(main)/cost/expense/page.tsx +++ /dev/null @@ -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[]>([]); - const [selected, setSelected] = useState[]>([]); - - 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 ( -
-
-

투입원가관리 - 경비관리

-
- - -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[140px]" /> - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/cost/labor/page.tsx b/src/app/(main)/cost/labor/page.tsx deleted file mode 100644 index 5a01e2d..0000000 --- a/src/app/(main)/cost/labor/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

투입원가관리 - 노무비관리

-
- -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[140px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/cost/material/page.tsx b/src/app/(main)/cost/material/page.tsx deleted file mode 100644 index 2968b70..0000000 --- a/src/app/(main)/cost/material/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

투입원가관리 - 재료비관리

-
- -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[140px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/cost/status/page.tsx b/src/app/(main)/cost/status/page.tsx deleted file mode 100644 index 96d61bc..0000000 --- a/src/app/(main)/cost/status/page.tsx +++ /dev/null @@ -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[]>([]); - const [selected, setSelected] = useState[]>([]); - - 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 ( -
-
-

투입원가관리 현황

-
- - -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[140px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/cs/chart/page.tsx b/src/app/(main)/cs/chart/page.tsx deleted file mode 100644 index a65c97a..0000000 --- a/src/app/(main)/cs/chart/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

CS 차트관리

-
- -
-
- - - - - - - - - - -
- {chartData.length > 0 ? ( -
- {/* TODO: Chart rendering - integrate with chart library */} -

차트 데이터 {chartData.length}건 로드됨

-

차트 라이브러리 연동 후 표시됩니다.

-
- ) : ( -
- 조회 버튼을 클릭하여 차트 데이터를 로드하세요. -
- )} -
-
- ); -} diff --git a/src/app/(main)/cs/manage/page.tsx b/src/app/(main)/cs/manage/page.tsx deleted file mode 100644 index e21f854..0000000 --- a/src/app/(main)/cs/manage/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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) => ({ - 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) => ({ - 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) => ( - openFileRegist(String(row.OBJID || ""))} - /> - ), - }, - { - title: "상태", field: "APPR_STATUS_NAME", width: 100, hozAlign: "center", - formatter: (_, row) => { - const apv = String(row.APPROVAL_OBJID || ""); - return ( - 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 ( -
-
-

CS관리_CS등록 및 조회

-
- - - - -
-
- - - - - - - - - - - - - - - -
- setRecStartDate(e.target.value)} className="w-[140px]" /> - ~ - setRecEndDate(e.target.value)} className="w-[140px]" /> -
-
- - - - -
- setActStartDate(e.target.value)} className="w-[140px]" /> - ~ - setActEndDate(e.target.value)} className="w-[140px]" /> -
-
- - - -
- - -
- ); -} diff --git a/src/app/(main)/cs/page.tsx b/src/app/(main)/cs/page.tsx deleted file mode 100644 index 95e7fa0..0000000 --- a/src/app/(main)/cs/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

CS 관리

-
- - -
-
- - - - - - setCustomerName(e.target.value)} className="w-[150px]" /> - - - -
- ); -} diff --git a/src/app/(main)/cs/status/page.tsx b/src/app/(main)/cs/status/page.tsx deleted file mode 100644 index 26a7e48..0000000 --- a/src/app/(main)/cs/status/page.tsx +++ /dev/null @@ -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[]>([]); - const [dynColumns, setDynColumns] = useState([]); - 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) => ({ - 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) => ({ - 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(); - 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 ( -
-
-

CS관리_현황

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx deleted file mode 100644 index 245f471..0000000 --- a/src/app/(main)/dashboard/page.tsx +++ /dev/null @@ -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[]; - 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(null); - const [year, setYear] = useState(new Date().getFullYear().toString()); - const [tab, setTab] = useState("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 ( -
- {/* 헤더 */} -
-

- Dashboard - - {user?.userName}님, 환영합니다. - -

-
- {/* 탭 */} -
- setTab("sales")} icon={TrendingUp} label="영업" /> - setTab("project")} icon={Briefcase} label="프로젝트" /> -
- -
-
- - {/* 탭 내용 — 남은 공간 가득 채움 */} -
- {tab === "sales" ? ( - - ) : ( - - )} -
-
- ); -} - -function TabButton({ active, onClick, icon: Icon, label }: { - active: boolean; onClick: () => void; icon: React.ElementType; label: string; -}) { - return ( - - ); -} - -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 ( -
- {/* 상단: 영업현황 표 (고정 높이) */} - - - {/* 하단: 3분할 — 제품별 pie / 고객사별 pie / 년도별 combo (남은 공간 가득) */} -
- - - -
-
- ); -} - -function YearGoalTable({ info, onOpenGoal }: { info: YearGoalRow[]; onOpenGoal: () => void }) { - return ( -
-
-

- - 영업현황 -

- -
- - - - - - - - - - - - - - - - - {info.length === 0 ? ( - - ) : info.map((row, idx) => ( - - - - - - - - - - ))} - -
년도수주현황(건수)수주율(%)예상매출(억원)영업목표(억원)달성율(%)
국내해외
데이터가 없습니다.
{row.YEAR}{row.CONTRACT_CNT_YEAR_IN ?? 0}{row.CONTRACT_CNT_YEAR_OUT ?? 0}{row.CONTRACT_CNT_YEAR_RATE ?? 0}{numberWithCommas(Number(row.CONTRACT_COST_YEAR ?? 0))}{numberWithCommas(Number(row.PRICE ?? 0))}{row.GOAL_RATE ?? 0}
-
- ); -} - -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 ( -
-

{title}

- {total === 0 ? ( -
데이터가 없습니다.
- ) : ( -
- - - - percent != null && percent >= 0.05 ? `${Math.round(percent * 100)}%` : "" - } - labelLine={false} - > - {chartData.map((entry, index) => ( - - ))} - - `${v}건`} /> - - - -
- )} -
- ); -} - -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 ( -
-

■ 년도별 영업현황

-
- - - - - - - - name === "달성율" ? `${v}%` : `${numberWithCommas(Number(v))}억` - } - /> - - - - - - -
-
- ); -} - -type StatusFilter = "all" | "noplan" | "ing" | "delay" | "end"; - -const FILTER_LABELS: Record = { - all: "전체", - noplan: "계획미수립", - ing: "진행중", - delay: "지연", - end: "종료", -}; - -function ProjectTab({ data, year }: { data: DashboardData | null; year: string }) { - const stats = data?.projectStats || {}; - const allProjects = (data?.projectList || []) as Record[]; - const [statusFilter, setStatusFilter] = useState("all"); - const [projectFilter, setProjectFilter] = useState(""); - 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 ( -
- {/* 상단 프로젝트현황 카드 — 원본 스타일 (5개 숫자 가로 + 컨트롤) */} -
-
-

- - 프로젝트현황 -

- -
-
- {/* 좌측: 년도/프로젝트번호 셀렉트 */} -
-
- -
{year}
-
-
- - -
-
- {/* 우측: 5개 숫자 가로 배치 */} -
- setStatusFilter("all")} /> - toggleFilter("noplan")} /> - toggleFilter("ing")} /> - toggleFilter("delay")} /> - toggleFilter("end")} /> -
-
-
- - {/* 프로젝트 리스트 — 전체 너비, 원본 10컬럼 구조 */} -
-

- - 프로젝트 리스트 {statusFilter !== "all" && ( - [{FILTER_LABELS[statusFilter]}] - )} · 총 {projectList.length}건 - {statusFilter !== "all" && ( - - )} -

-
- - - - - - - - - - - - - - - - - {projectList.length === 0 ? ( - - ) : 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 ( - setSelectedIdx(idx)}> - - - - - - - - - - - - ); - })} - -
선택고객사제품구분프로젝트번호납기일셋업지제작공장진척율(%)셋업완료일상태
데이터가 없습니다.
- setSelectedIdx(idx)} className="pointer-events-none" /> - {String(pjt.CUSTOMER_NAME || "")}{String(pjt.PRODUCT_NAME || "")} - {String(pjt.PROJECT_NO || "")} - {String(pjt.CONTRACT_DEL_DATE || "-")}{String(pjt.SETUP || "")}{String(pjt.MANUFACTURE_PLANT_NAME || "")}{Number(pjt.SETUP_RATE || 0).toFixed(1)}{String(pjt.SETUP_DONE_DATE || "")}{statusTitle}
-
-
- - {/* 선택된 프로젝트 상세 (이슈 + 투입원가) */} -
- -
-
- ); -} - -function ProjectDetailPanel({ project }: { project: Record | undefined }) { - if (!project) { - return ( -
-
프로젝트를 선택하세요
-
- ); - } - - 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분할 (가로 배치) */} -
- {/* 이슈 (Quality) */} -
-

- - 이슈 (Quality) -

-
- - - - -
-
- - {/* 투입원가현황 — 원본 dashboard.jsp 5행 테이블 */} -
-

- - 투입원가현황 -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
수주가(원)항목목표원가(원)투입원가(원)투입율(%)
{numberWithCommas(contractPrice)}재료비{numberWithCommas(materialGoal)}{numberWithCommas(materialActual)}{materialRate}
노무비{numberWithCommas(laborGoal)}{numberWithCommas(laborActual)}{laborRate}
경비{numberWithCommas(expenseGoal)}{numberWithCommas(expenseActual)}{expenseRate}
관리비{numberWithCommas(mgmtGoal)}{numberWithCommas(mgmtActual)}{mgmtRate}
{numberWithCommas(totalGoal)}{numberWithCommas(totalActual)}{totalRateCost}
-
-
-
- - ); -} - -function MiniStat({ label, value, color }: { label: string; value: number | string; color: string }) { - return ( -
-
{label}
-
{value}
-
- ); -} - -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 ( - - ); -} - diff --git a/src/app/(main)/delivery/acceptance/page.tsx b/src/app/(main)/delivery/acceptance/page.tsx deleted file mode 100644 index 2bfaf03..0000000 --- a/src/app/(main)/delivery/acceptance/page.tsx +++ /dev/null @@ -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([]); - const [userOptions, setUserOptions] = useState([]); - - const [data, setData] = useState[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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) => ({ - 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) => ({ - 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 ( -
-
-

입고관리_입고결과등록

-
- - - -
-
- - - - - - - setCustomerProjectName(e.target.value)} className="w-[150px]" /> - - - setProjectNo(e.target.value)} className="w-[140px]" placeholder="프로젝트 OBJID" /> - - - setUnitCode(e.target.value)} className="w-[130px]" placeholder="유닛 OBJID" /> - - - setPurchaseOrderNo(e.target.value)} className="w-[120px]" /> - - - - - - setSearchPartSpec(e.target.value)} className="w-[120px]" /> - - - - - - - - - -
- setDeliveryStartDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> - ~ - setDeliveryEndDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> -
-
- -
- setRegStartDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> - ~ - setRegEndDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> -
-
- - - - - setSearchPartName(e.target.value)} className="w-[130px]" /> - - - setSearchPartNo(e.target.value)} className="w-[130px]" /> - - - - - -
- - -
- ); -} diff --git a/src/app/(main)/delivery/defect/page.tsx b/src/app/(main)/delivery/defect/page.tsx deleted file mode 100644 index d3e4329..0000000 --- a/src/app/(main)/delivery/defect/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

부적합리스트

-
- - -
-
- - - - - - - - - - setProjectNo(e.target.value)} placeholder="프로젝트번호" className="w-[150px]" /> - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/delivery/price/page.tsx b/src/app/(main)/delivery/price/page.tsx deleted file mode 100644 index 7a461a3..0000000 --- a/src/app/(main)/delivery/price/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

단가관리

-
- - - - -
-
- - - - - - - setPartNo(e.target.value)} placeholder="Part No" className="w-[150px]" /> - - - setPartName(e.target.value)} placeholder="품명" className="w-[150px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/delivery/status/page.tsx b/src/app/(main)/delivery/status/page.tsx deleted file mode 100644 index 0d2a0a8..0000000 --- a/src/app/(main)/delivery/status/page.tsx +++ /dev/null @@ -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[]>([]); - - const [customerOptions, setCustomerOptions] = useState([]); - const [projectOptions, setProjectOptions] = useState([]); - - 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) => ({ - 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) => ({ - 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 ( -
-
-

입고관리_현황

-
- -
-
- - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/fund/expense-form/page.tsx b/src/app/(main)/fund/expense-form/page.tsx deleted file mode 100644 index 05c0f05..0000000 --- a/src/app/(main)/fund/expense-form/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

경비신청서관리

-
- - 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} - /> - -
-
- - - - - - - setWriterName(e.target.value)} className="w-[120px]" /> - - - - - - setExpenseDateFrom(e.target.value)} className="w-[140px]" /> - - - setExpenseDateTo(e.target.value)} className="w-[140px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/fund/invoice/page.tsx b/src/app/(main)/fund/invoice/page.tsx deleted file mode 100644 index 0bab97b..0000000 --- a/src/app/(main)/fund/invoice/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

거래명세서관리

-
- - -
-
- - - - - - - setCustomerName(e.target.value)} className="w-[150px]" /> - - - setInvoiceDateFrom(e.target.value)} className="w-[140px]" /> - - - setInvoiceDateTo(e.target.value)} className="w-[140px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/inventory/fund/page.tsx b/src/app/(main)/inventory/fund/page.tsx deleted file mode 100644 index 78c810b..0000000 --- a/src/app/(main)/inventory/fund/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

자금관리

-
- - -
-
- - - - - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/inventory/history/page.tsx b/src/app/(main)/inventory/history/page.tsx deleted file mode 100644 index 3a2c0a2..0000000 --- a/src/app/(main)/inventory/history/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-

재고 입출고 이력

- -
- -
-
- ); -} diff --git a/src/app/(main)/inventory/list/page.tsx b/src/app/(main)/inventory/list/page.tsx deleted file mode 100644 index 764662c..0000000 --- a/src/app/(main)/inventory/list/page.tsx +++ /dev/null @@ -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([]); - 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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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>; - 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>; - 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 ( - { - e.preventDefault(); - openHistoryPopup(String(row.OBJID || "")); - }} - className="text-blue-600 underline" - > - {v.toLocaleString()} - - ); - }, - }, - { 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 ( -
-
-

자재관리_자재리스트

-
- - - - -
-
- - - - - - - - - - setPartNo(e.target.value)} - placeholder="품번" - className="w-[150px]" - /> - - - setPartName(e.target.value)} - placeholder="품명" - className="w-[170px]" - /> - - - - - - - - - - -
- ); -} - -// 간단한 다중 선택 — 선택된 라벨을 태그로 표시 + 드롭다운 -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 ( -
- - {value.length > 0 && ( - - )} - {open && ( - <> -
setOpen(false)} /> -
-
- setSearch(e.target.value)} - placeholder="검색" - className="w-full px-2 py-1 text-sm border rounded" - /> -
- {filtered.length === 0 ? ( -
결과 없음
- ) : ( - filtered.map((o) => { - const selected = value.includes(o.value); - return ( - - ); - }) - )} -
- - )} -
- ); -} diff --git a/src/app/(main)/inventory/page.tsx b/src/app/(main)/inventory/page.tsx deleted file mode 100644 index a7f401f..0000000 --- a/src/app/(main)/inventory/page.tsx +++ /dev/null @@ -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[]>([]); - 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 ( -
-
-

재고관리 (입고)

-
- - -
-
- - - - - - - - setPartNo(e.target.value)} className="w-[120px]" /> - - - - setPartName(e.target.value)} className="w-[120px]" /> - - - - setSpec(e.target.value)} className="w-[100px]" /> - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/inventory/request/history/page.tsx b/src/app/(main)/inventory/request/history/page.tsx deleted file mode 100644 index 3055012..0000000 --- a/src/app/(main)/inventory/request/history/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

입출고 이력

-
- -
- - - - - - - - - - - - - - {rows.length === 0 ? ( - - - - ) : ( - rows.map((r, i) => { - const gubun = String(r.GUBUN || ""); - const rowObjId = String(r.OBJID || ""); - return ( - - - - - - - - - - ); - }) - )} - -
프로젝트번호품번품명구분입출고수량LocationSub_Location
- 조회된 데이터가 없습니다. -
{String(r.PROJECT_NO || "")}{String(r.PART_NO || "")}{String(r.PART_NAME || "")} - {gubun === "입고" || gubun === "출고" ? ( - { - e.preventDefault(); - openTarget(rowObjId, gubun); - }} - className="text-blue-600 underline" - > - {gubun} - - ) : ( - {gubun} - )} - - {numberWithCommas(String(r.RECEIPT_QTY || ""))} - {String(r.LOCATION_NAME || "")}{String(r.SUB_LOCATION_NAME || "")}
-
- -
- -
-
- ); -} - -export default function Page() { - return ( - 로딩 중...
}> - - - ); -} diff --git a/src/app/(main)/inventory/request/page.tsx b/src/app/(main)/inventory/request/page.tsx deleted file mode 100644 index 237fa6b..0000000 --- a/src/app/(main)/inventory/request/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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>; - 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) => ( - { - e.preventDefault(); - openDetailPopup( - String(row.OBJID || ""), - String(row.OUTSTATUS_TITLE || ""), - String(row.RECEPTION_STATUS_TITLE || ""), - ); - }} - className="text-blue-600 underline" - > - {String(cell || "")} - - ), - }, - { 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 ( -
-
-

자재관리_불출의뢰서

-
- - - - -
-
- - - - setPartNo(e.target.value)} - placeholder="품번" - className="w-[150px]" - /> - - - setPartName(e.target.value)} - placeholder="품명" - className="w-[150px]" - /> - - -
- setRequestStartDate(e.target.value)} - className="w-[130px]" - /> - ~ - setRequestEndDate(e.target.value)} - className="w-[130px]" - /> -
-
- - - - - - - - - - -
- setReceptionStartDate(e.target.value)} - className="w-[130px]" - /> - ~ - setReceptionEndDate(e.target.value)} - className="w-[130px]" - /> -
-
- - - -
- - -
- ); -} diff --git a/src/app/(main)/inventory/status/page.tsx b/src/app/(main)/inventory/status/page.tsx deleted file mode 100644 index 4e3fc68..0000000 --- a/src/app/(main)/inventory/status/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

자재현황

-
- - -
-
- - - - - - - setProjectNo(e.target.value)} placeholder="프로젝트번호" className="w-[150px]" /> - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/order/amount-status/page.tsx b/src/app/(main)/order/amount-status/page.tsx deleted file mode 100644 index cf1ce71..0000000 --- a/src/app/(main)/order/amount-status/page.tsx +++ /dev/null @@ -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[]>([]); - const [sums, setSums] = useState>({}); - const [loading, setLoading] = useState(false); - - const [supplyOptions, setSupplyOptions] = useState([]); - const [userOptions, setUserOptions] = useState([]); - - 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) => ({ - 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) => ({ - 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 ( -
-
-

업체별 입고요청월 금액현황

-
- - - -
-
- - - - - - - setProjectNo(e.target.value)} - placeholder="프로젝트번호 부분 일치" - className="w-[180px]" - /> - - - - - - - - - -
- 총 합계(원) : {numberWithCommas(Number(sums.TOTAL_SUPPLY_UNIT_PRICE ?? 0))} -
- - -
- ); -} diff --git a/src/app/(main)/order/list/page.tsx b/src/app/(main)/order/list/page.tsx deleted file mode 100644 index e30b5d5..0000000 --- a/src/app/(main)/order/list/page.tsx +++ /dev/null @@ -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([]); - const [supplyOptions, setSupplyOptions] = useState([]); - const [userOptions, setUserOptions] = useState([]); - - const [data, setData] = useState[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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) => ({ - 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) => ({ - 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) => ({ - 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 ( -
-
-

발주서관리

-
- - - `${r.PURCHASE_ORDER_NO} - ${r.TITLE}`).join("\n")} - onSuccess={fetchData} - disabled={masterSelectedIds.length === 0} - /> -
-
- - - - - - - - - - setProjectNo(e.target.value)} className="w-[150px]" /> - - - setUnitCode(e.target.value)} className="w-[130px]" placeholder="유닛코드" /> - - - setPurchaseOrderNo(e.target.value)} className="w-[140px]" /> - - - - - - - - -
- setDeliveryStartDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> - ~ - setDeliveryEndDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> -
-
- - - - - - - -
- setRegStartDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> - ~ - setRegEndDate(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> -
-
- - setPartNo(e.target.value)} className="w-[130px]" /> - - - setPartName(e.target.value)} className="w-[130px]" /> - - - - - - - - - setPartSpec(e.target.value)} className="w-[130px]" /> - -
- - -
- ); -} diff --git a/src/app/(main)/order/status/page.tsx b/src/app/(main)/order/status/page.tsx deleted file mode 100644 index ac0b01a..0000000 --- a/src/app/(main)/order/status/page.tsx +++ /dev/null @@ -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[]>([]); - const [sums, setSums] = useState>({}); - 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) => ( - 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 ( -
-
-

발주관리_현황

-
- - - -
-
- - - - - - - setProjectNo(e.target.value)} - placeholder="프로젝트번호 부분 일치" - className="w-[200px]" - /> - - - -
- 총발주금액(원) : {numberWithCommas(Number(sums.TOTAL_PRICE_ALL_SUM ?? 0))} - 단일발주금액(원) : {numberWithCommas(Number(sums.SINGLE_PRICE_SUM ?? 0))} -
- - -
- ); -} diff --git a/src/app/(main)/part-mgmt/page.tsx b/src/app/(main)/part-mgmt/page.tsx deleted file mode 100644 index 5349e1c..0000000 --- a/src/app/(main)/part-mgmt/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

부품관리 (PART)

-
- - -
-
- - - - setPartNo(e.target.value)} className="w-[150px]" /> - - - setPartName(e.target.value)} className="w-[150px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/part/register/page.tsx b/src/app/(main)/part/register/page.tsx deleted file mode 100644 index 4bb34c3..0000000 --- a/src/app/(main)/part/register/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

Part 등록

-
- - -
-
- - - - setPartNo(e.target.value)} className="w-[140px]" /> - - - setPartName(e.target.value)} className="w-[150px]" /> - - - setSpec(e.target.value)} className="w-[120px]" /> - - - setMaker(e.target.value)} className="w-[120px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/procurement-std/code1/page.tsx b/src/app/(main)/procurement-std/code1/page.tsx deleted file mode 100644 index bc8672b..0000000 --- a/src/app/(main)/procurement-std/code1/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import StandardCodePage from "@/components/procurement-std/StandardCodePage"; - -// 구매품표준관리 - 코드1 -export default function Code1Page() { - return ( - - ); -} diff --git a/src/app/(main)/procurement-std/code2/page.tsx b/src/app/(main)/procurement-std/code2/page.tsx deleted file mode 100644 index 2156308..0000000 --- a/src/app/(main)/procurement-std/code2/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import StandardCodePage from "@/components/procurement-std/StandardCodePage"; - -// 구매품표준관리 - 코드2 -export default function Code2Page() { - return ( - - ); -} diff --git a/src/app/(main)/procurement-std/code3/page.tsx b/src/app/(main)/procurement-std/code3/page.tsx deleted file mode 100644 index 4181a4e..0000000 --- a/src/app/(main)/procurement-std/code3/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import StandardCodePage from "@/components/procurement-std/StandardCodePage"; - -// 구매품표준관리 - 코드3 -export default function Code3Page() { - return ( - - ); -} diff --git a/src/app/(main)/procurement-std/code4/page.tsx b/src/app/(main)/procurement-std/code4/page.tsx deleted file mode 100644 index ae866d7..0000000 --- a/src/app/(main)/procurement-std/code4/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import StandardCodePage from "@/components/procurement-std/StandardCodePage"; - -// 구매품표준관리 - 코드4 -export default function Code4Page() { - return ( - - ); -} diff --git a/src/app/(main)/procurement-std/code5/page.tsx b/src/app/(main)/procurement-std/code5/page.tsx deleted file mode 100644 index cf6160b..0000000 --- a/src/app/(main)/procurement-std/code5/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import StandardCodePage from "@/components/procurement-std/StandardCodePage"; - -// 구매품표준관리 - 코드5 -export default function Code5Page() { - return ( - - ); -} diff --git a/src/app/(main)/procurement-std/material-code/page.tsx b/src/app/(main)/procurement-std/material-code/page.tsx deleted file mode 100644 index 94b2508..0000000 --- a/src/app/(main)/procurement-std/material-code/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import StandardCodePage from "@/components/procurement-std/StandardCodePage"; - -// 구매품표준관리 - 자재코드 -export default function MaterialCodePage() { - return ( - - ); -} diff --git a/src/app/(main)/product-mgmt/page.tsx b/src/app/(main)/product-mgmt/page.tsx deleted file mode 100644 index 931a7ab..0000000 --- a/src/app/(main)/product-mgmt/page.tsx +++ /dev/null @@ -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[]>([]); - const [totalCount, setTotalCount] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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 ( -
-
-

제품마스터

-
- - - -
-
- - {/* 검색 영역 (productmgmtList.jsp #plmSearchZon 대응) */} - - - - - - - - - - - {/* 그리드 (Tabulator/plm_table 대응) */} - - - {/* 페이징 */} - fetchData(page)} - /> -
- ); -} diff --git a/src/app/(main)/product/bom-list/page.tsx b/src/app/(main)/product/bom-list/page.tsx deleted file mode 100644 index 42dc440..0000000 --- a/src/app/(main)/product/bom-list/page.tsx +++ /dev/null @@ -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; - -// 제품관리_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([]); - const [maxLevel, setMaxLevel] = useState(0); - const [loading, setLoading] = useState(false); - // 접힌 노드: CHILD_OBJID 를 키로 사용 (해당 노드의 모든 하위 행을 숨김) - const [collapsed, setCollapsed] = useState>(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(); - 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(); - 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 ( - - ); - }, - }, - ...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) => , - cellClick: (row) => openFilePopup(String(row.PART_OBJID), "3D_CAD", "3D CAD 첨부파일"), - }, - { - title: "2D", field: "FILE_2D_CNT", width: 40, hozAlign: "center", - formatter: (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) => , - 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 ( -
-
-

제품관리_BOM 조회

-
- - - - - -
-
- - fetchData()}> - - - - - - - - - - - setSearchPartNo(e.target.value)} className="w-[200px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/product/bom-register/page.tsx b/src/app/(main)/product/bom-register/page.tsx deleted file mode 100644 index f8f4c9f..0000000 --- a/src/app/(main)/product/bom-register/page.tsx +++ /dev/null @@ -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[]>([]); - const [loading, setLoading] = useState(false); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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) => ({ - 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) => ({ - 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) => ({ - 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) => ({ - 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) => , - 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 ( -
-
-

제품관리_PART 및 구조등록

-
- - - - - - -
-
- - - - - - - - - - - - - setSearchUnitName(e.target.value)} className="w-[140px]" /> - - - - - -
- setDeployFrom(e.target.value)} className="w-[140px]" /> - ~ - setDeployTo(e.target.value)} className="w-[140px]" /> -
-
- - - -
- - -
- ); -} diff --git a/src/app/(main)/product/design-change/page.tsx b/src/app/(main)/product/design-change/page.tsx deleted file mode 100644 index ef8758e..0000000 --- a/src/app/(main)/product/design-change/page.tsx +++ /dev/null @@ -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[]>([]); - 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) => ({ - 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) => ({ - 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) => ({ - 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 ( -
-
-

제품관리_설계변경 리스트

-
- - - -
-
- - - - - - - - - - - - - setPartNo(e.target.value)} className="w-[140px]" /> - - - setPartName(e.target.value)} className="w-[140px]" /> - - - - - -
- setEoStart(e.target.value)} className="w-[140px]" /> - ~ - setEoEnd(e.target.value)} className="w-[140px]" /> -
-
- - - - - - - - - -
- - -
- ); -} diff --git a/src/app/(main)/product/part-change/page.tsx b/src/app/(main)/product/part-change/page.tsx deleted file mode 100644 index 1a15de6..0000000 --- a/src/app/(main)/product/part-change/page.tsx +++ /dev/null @@ -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; -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([]); - 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([]); - const [changeTypes, setChangeTypes] = useState([]); - const [changeOptions, setChangeOptions] = useState([]); - - // 공통코드 로드 (API 응답이 소문자 키라 대문자로 정규화) - useEffect(() => { - const loadCode = async (codeId: string): Promise => { - 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) => ({ - 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(); - (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(); - const s = new Set(); - 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) => ( - - ), []); - - return ( -
-
-

제품관리_설변대상 PART조회

-
- - - - -
-
- - - - - - - - - - - - - -
- {loading && ( -
-
-
- Loading... -
-
- )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {data.length === 0 ? ( - - ) : data.map((row, idx) => { - const editable = isEditable(idx); - const frozenCell = "px-2 py-1 border border-gray-200"; - return ( - - - - - - - - - - - - - - - - - - - - - - - - - ); - })} - -
프로젝트번호유닛명모품번품번품명수량3D2DPDFEO구분EO사유PART구분재질사양(규격)후처리MAKER대분류중분류RevisionEO NoEO Date비고
데이터가 없습니다.
{String(row.PROJECT_NO ?? "")}{String(row.UNIT_NAME ?? "")}{String(row.PARENT_PART_INFO ?? "")} - openPartForm(String(row.OBJID))}> - {String(row.PART_NO ?? "")} - - {String(row.PART_NAME ?? "")} - {editable ? ( - 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 ?? "")} - - openFilePopup(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일")} /> - - openFilePopup(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일")} /> - - openFilePopup(String(row.OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일")} /> - {codeSel(changeTypes, String(row.CHANGE_TYPE ?? ""), (v) => updateCell(idx, "CHANGE_TYPE", v), !editable)}{codeSel(changeOptions, String(row.CHANGE_OPTION ?? ""), (v) => updateCell(idx, "CHANGE_OPTION", v), !editable)}{codeSel(partTypes, String(row.PART_TYPE ?? ""), (v) => updateCell(idx, "PART_TYPE", v), !editable)} - updateCell(idx, "MATERIAL", e.target.value)} disabled={!editable} className="h-6 text-xs" /> - - updateCell(idx, "SPEC", e.target.value)} disabled={!editable} className="h-6 text-xs" /> - - updateCell(idx, "POST_PROCESSING", e.target.value)} disabled={!editable} className="h-6 text-xs" /> - - updateCell(idx, "MAKER", e.target.value)} disabled={!editable} className="h-6 text-xs" /> - - updateCell(idx, "MAJOR_CATEGORY", e.target.value)} disabled={!editable} className="h-6 text-xs" /> - - updateCell(idx, "SUB_CATEGORY", e.target.value)} disabled={!editable} className="h-6 text-xs" /> - {String(row.REVISION ?? "")}{String(row.EO_NO ?? "")}{String(row.EO_DATE ?? "")} - updateCell(idx, "REMARK", e.target.value)} disabled={!editable} className="h-6 text-xs" /> -
-
-
- ); -} diff --git a/src/app/(main)/product/part-list/page.tsx b/src/app/(main)/product/part-list/page.tsx deleted file mode 100644 index 4bd8038..0000000 --- a/src/app/(main)/product/part-list/page.tsx +++ /dev/null @@ -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[]>([]); - 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) => , - cellClick: (row) => openFilePopup(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일"), - }, - { - title: "2D", field: "CU02_CNT", width: 45, hozAlign: "center", - formatter: (cell) => , - cellClick: (row) => openFilePopup(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"), - }, - { - title: "PDF", field: "CU03_CNT", width: 55, hozAlign: "center", - formatter: (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 ( -
-
-

제품관리_PART 조회

-
- - - -
-
- - - - setSearchPartNo(e.target.value)} className="w-[194px]" /> - - - setSearchPartName(e.target.value)} className="w-[150px]" /> - - - - - - setSearchMaterial(e.target.value)} className="w-[150px]" /> - - - setSearchSpec(e.target.value)} className="w-[150px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/product/part-register/page.tsx b/src/app/(main)/product/part-register/page.tsx deleted file mode 100644 index 7bb9934..0000000 --- a/src/app/(main)/product/part-register/page.tsx +++ /dev/null @@ -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[]>([]); - const [loading, setLoading] = useState(false); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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) => ({ - 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) => , - cellClick: (row) => openFileRegist(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일"), - }, - { - title: "2D", field: "CU02_CNT", width: 60, hozAlign: "center", - formatter: (cell) => , - cellClick: (row) => openFileRegist(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"), - }, - { - title: "PDF", field: "CU03_CNT", width: 60, hozAlign: "center", - formatter: (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 ( -
-
-

제품관리_PART 등록

-
- - - - - - -
-
- - - - - - - setSearchPartNo(e.target.value)} className="w-[180px]" /> - - - setSearchPartName(e.target.value)} className="w-[160px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/product/spec/page.tsx b/src/app/(main)/product/spec/page.tsx deleted file mode 100644 index 2b6b0d8..0000000 --- a/src/app/(main)/product/spec/page.tsx +++ /dev/null @@ -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[]>([]); - - 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: ` -
- - - - - - - - -
`, - 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 ( -
-
-

사양관리

-
- - - -
-
- - - - setProjectNo(e.target.value)} className="w-[130px]" /> - - - setSpecName(e.target.value)} className="w-[200px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/production/inspection/page.tsx b/src/app/(main)/production/inspection/page.tsx deleted file mode 100644 index 8c57cc8..0000000 --- a/src/app/(main)/production/inspection/page.tsx +++ /dev/null @@ -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[]>([]); - const [selected, setSelected] = useState[]>([]); - - 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) => , - cellClick: (row) => openInspection(String(row.OBJID)) }, - { title: "입회검사", field: "ADMISSION_INSPECTION_CNT", width: 110, hozAlign: "center", - formatter: (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 ( -
-
-

생산관리_검사관리

-
- - -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[180px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/production/issue/page.tsx b/src/app/(main)/production/issue/page.tsx deleted file mode 100644 index 56bde61..0000000 --- a/src/app/(main)/production/issue/page.tsx +++ /dev/null @@ -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[]>([]); - const [selected, setSelected] = useState[]>([]); - - 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 ( -
-
-

생산관리_이슈관리

-
- - - - -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[160px]" /> - - - setUnitCode(e.target.value)} className="w-[140px]" /> - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/production/page.tsx b/src/app/(main)/production/page.tsx deleted file mode 100644 index 5bda54f..0000000 --- a/src/app/(main)/production/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

생산관리

-
- -
-
- - - - - - setProjectNo(e.target.value)} className="w-[130px]" /> - - - -
- ); -} diff --git a/src/app/(main)/production/planning/page.tsx b/src/app/(main)/production/planning/page.tsx deleted file mode 100644 index 2947a32..0000000 --- a/src/app/(main)/production/planning/page.tsx +++ /dev/null @@ -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[]>([]); - - 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) => , - 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) => , - 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 ( -
-
-

생산관리_생산계획수립

-
- -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[180px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/production/process/page.tsx b/src/app/(main)/production/process/page.tsx deleted file mode 100644 index 7647d07..0000000 --- a/src/app/(main)/production/process/page.tsx +++ /dev/null @@ -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[]>([]); - - const openProjectForm = (objId: string) => - openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); - const openAssemblyPopup = (row: Record) => - 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 ( -
-
-

생산관리_공정실적관리

-
- -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[180px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/production/release/page.tsx b/src/app/(main)/production/release/page.tsx deleted file mode 100644 index bf1f268..0000000 --- a/src/app/(main)/production/release/page.tsx +++ /dev/null @@ -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[]>([]); - const [selected, setSelected] = useState[]>([]); - - const openProjectForm = (objId: string) => - openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); - const openReleaseForm = (row: Record) => { - 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) => , - cellClick: (row) => openInspection(String(row.OBJID)) }, - { title: "입회검사", field: "ADMISSION_INSPECTION_CNT", width: 100, hozAlign: "center", - formatter: (v) => , - cellClick: (row) => openFiles(String(row.OBJID), "ADMISSION_INSPECTION_FILE", "입회검사") }, - ], - }, - { - title: "출고내역", - columns: [ - { title: "출하지시서", field: "RELEASE_ORDER_CNT", width: 100, hozAlign: "center", - formatter: (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 ( -
-
-

생산관리_출고관리

-
- - -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[180px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/production/setup/page.tsx b/src/app/(main)/production/setup/page.tsx deleted file mode 100644 index 3a75080..0000000 --- a/src/app/(main)/production/setup/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

생산관리_셋업관리

-
- -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[180px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/production/status/page.tsx b/src/app/(main)/production/status/page.tsx deleted file mode 100644 index a7bd3bd..0000000 --- a/src/app/(main)/production/status/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

생산관리_현황

-
- -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[180px]" /> - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/project/page.tsx b/src/app/(main)/project/page.tsx deleted file mode 100644 index 1be13c6..0000000 --- a/src/app/(main)/project/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

프로젝트관리

-
- -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[130px]" /> - - - setProjectName(e.target.value)} className="w-[200px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/project/progress/page.tsx b/src/app/(main)/project/progress/page.tsx deleted file mode 100644 index c595a0a..0000000 --- a/src/app/(main)/project/progress/page.tsx +++ /dev/null @@ -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[]>([]); - - 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) => ({ - 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) => ({ - 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) => ({ - 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) => - openWbsPopUp(String(row.OBJID), String(row.CATEGORY_CD ?? "")); - const setupClick = (row: Record) => 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 ( -
-
-

프로젝트관리_일정관리(WBS)

-
- -
-
- - - - - - - - - - - - - - - - - - -
- setContractStartDate(e.target.value)} className="w-[140px]" /> - ~ - setContractEndDate(e.target.value)} className="w-[140px]" /> -
-
- - setLocation(e.target.value)} className="w-[130px]" /> - - - setSetup(e.target.value)} className="w-[130px]" /> - - - - -
- - -
- ); -} diff --git a/src/app/(main)/project/status/page.tsx b/src/app/(main)/project/status/page.tsx deleted file mode 100644 index 85d6ce9..0000000 --- a/src/app/(main)/project/status/page.tsx +++ /dev/null @@ -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[]>([]); - - 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) => ({ - 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) => ({ - 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) => ({ - 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 ( -
-
-

프로젝트관리_진행관리

-
- -
-
- - - - - - - - - - - - - - - - - - -
- setContractStartDate(e.target.value)} className="w-[140px]" /> - ~ - setContractEndDate(e.target.value)} className="w-[140px]" /> -
-
- - setLocation(e.target.value)} className="w-[130px]" /> - - - setSetup(e.target.value)} className="w-[130px]" /> - - - - -
- - -
- ); -} diff --git a/src/app/(main)/project/total/page.tsx b/src/app/(main)/project/total/page.tsx deleted file mode 100644 index 76486a0..0000000 --- a/src/app/(main)/project/total/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedObjId, setSelectedObjId] = useState(""); - - 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) => ({ - 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) => ({ - 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) => ({ - 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[]; - 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) => - [r.DESIGN_LATE_CNT, r.PURCHASE_LATE_CNT, r.PRODUCE_LATE_CNT, r.SETUP_LATE_CNT] - .some((v) => Number(v ?? 0) > 0); - - return ( -
-

프로젝트종합

- - - - - - - - - - - - - - - - - - -
- setContractStartDate(e.target.value)} className="w-[140px]" /> - ~ - setContractEndDate(e.target.value)} className="w-[140px]" /> -
-
- - setLocation(e.target.value)} className="w-[130px]" /> - - - setSetup(e.target.value)} className="w-[130px]" /> - - - - -
- -
-
- 범례: - 설계 - 구매 - 조립 - 출고 - 셋업 -
-
- - - -
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - {ganttMonths.map((mm, i) => ( - - ))} - - - - {rows.length === 0 ? ( - - - - ) : ( - 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 ( - setSelectedObjId(objId)} - > - - - - - - - - - - - - - - - ); - }) - )} - -
선택프로젝트 정보진척율(%){ganttYear - 1}년{ganttYear}년{ganttYear + 1}년
고객사제품구분프로젝트번호프로젝트명제작공장요청납기일설계구매조립셋업상태 - {mm.m + 1} -
조회된 데이터가 없습니다.
- setSelectedObjId(objId)} - /> - {String(row.CUSTOMER_NAME ?? "")}{String(row.PRODUCT_NAME ?? "")} { e.stopPropagation(); openProjectModify(objId); }}> - {String(row.PROJECT_NO ?? "")} - {String(row.PROJECT_NAME ?? "")}{String(row.MANUFACTURE_PLANT_NAME ?? "")}{String(row.REQ_DEL_DATE ?? "")}{fmtRate(row.DESIGN_RATETOTAL)}{fmtRate(row.PURCHASE_RATETOTAL)}{fmtRate(row.PRODUCE_RATETOTAL)}{fmtRate(row.SETUP_RATETOTAL)} - - - {todayPct != null && ( -
- )} - {design && ( -
- )} - {purchase && ( -
- )} - {produce && ( -
- )} - {ship && ( -
- )} - {setupBar && ( -
- )} -
-
-
-
- ); -} diff --git a/src/app/(main)/project/wbs-template/page.tsx b/src/app/(main)/project/wbs-template/page.tsx deleted file mode 100644 index e08e8a5..0000000 --- a/src/app/(main)/project/wbs-template/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

프로젝트관리_제품구분_UNIT관리

-
- - - -
-
- - - - - - - - -
- ); -} diff --git a/src/app/(main)/purchase-order/page.tsx b/src/app/(main)/purchase-order/page.tsx deleted file mode 100644 index 35766f6..0000000 --- a/src/app/(main)/purchase-order/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

발주관리

-
- - - -
-
- - - - - - - setPoNo(e.target.value)} className="w-[140px]" /> - - - setSupplierName(e.target.value)} className="w-[150px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/purchase/bom/page.tsx b/src/app/(main)/purchase/bom/page.tsx deleted file mode 100644 index 4c7c73a..0000000 --- a/src/app/(main)/purchase/bom/page.tsx +++ /dev/null @@ -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([]); - const [userOptions, setUserOptions] = useState([]); - const [data, setData] = useState[]>([]); - - 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) => ({ - 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) => ({ - 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) => ( - 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) => ( - 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 ( -
-
-

구매BOM관리

-
- -
-
- - - - - - - - - - setProjectNo(e.target.value)} - placeholder="프로젝트번호" - className="w-[160px]" - /> - - - setUnitName(e.target.value)} - placeholder="유닛명" - className="w-[160px]" - /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/purchase/design-change/page.tsx b/src/app/(main)/purchase/design-change/page.tsx deleted file mode 100644 index b65bcdb..0000000 --- a/src/app/(main)/purchase/design-change/page.tsx +++ /dev/null @@ -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([]); - const [data, setData] = useState[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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) => ({ - 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) => { - 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 ( -
-
-

설계변경리스트

-
- - - -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[140px]" /> - - - setPartNo(e.target.value)} className="w-[120px]" /> - - - setPartName(e.target.value)} className="w-[120px]" /> - - - setRevision(e.target.value)} className="w-[80px]" /> - - - - - - - - - - - - - - - - - - - - - - - -
- setEoDateStart(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> - ~ - setEoDateEnd(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> -
-
- -
- setConfirmDateStart(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> - ~ - setConfirmDateEnd(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> -
-
-
- - -
- ); -} diff --git a/src/app/(main)/purchase/request/page.tsx b/src/app/(main)/purchase/request/page.tsx deleted file mode 100644 index fee8727..0000000 --- a/src/app/(main)/purchase/request/page.tsx +++ /dev/null @@ -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([]); - const [data, setData] = useState[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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) => ({ - 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 ( -
-
-

구매요청서관리

-
- - - - -
-
- - - - - - - setProjectNo(e.target.value)} className="w-[140px]" /> - - - - - - - - - - - -
- setReceiptDateStart(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> - ~ - setReceiptDateEnd(e.target.value)} - className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" - /> -
-
-
- - -
- ); -} diff --git a/src/app/(main)/purchase/stock/page.tsx b/src/app/(main)/purchase/stock/page.tsx deleted file mode 100644 index 9327f7e..0000000 --- a/src/app/(main)/purchase/stock/page.tsx +++ /dev/null @@ -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([]); - const [data, setData] = useState[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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) => ({ - 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 ( -
-
-

재고리스트

-
- - - - - -
-
- - - - setLdPartName(e.target.value)} className="w-[160px]" /> - - - setSpec(e.target.value)} className="w-[160px]" /> - - - - - - setMaker(e.target.value)} className="w-[140px]" /> - - - setMaterialCode(e.target.value)} className="w-[140px]" /> - - - - - - -
- 장납기품 비용(원) : {totalCost.toLocaleString()} -
- - -
- ); -} diff --git a/src/app/(main)/purchase/supplier/page.tsx b/src/app/(main)/purchase/supplier/page.tsx deleted file mode 100644 index 0f4d29c..0000000 --- a/src/app/(main)/purchase/supplier/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

공급업체관리

-
- - - -
-
- - - - - - - setSupplyName(e.target.value)} - placeholder="고객명" - className="w-[180px]" - /> - - - - -
- ); -} diff --git a/src/app/(main)/quality/page.tsx b/src/app/(main)/quality/page.tsx deleted file mode 100644 index dfbe686..0000000 --- a/src/app/(main)/quality/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

품질관리

-
- -
-
- - - - - - setPartNo(e.target.value)} className="w-[130px]" /> - - - -
- ); -} diff --git a/src/app/(main)/sales/contract-dashboard/page.tsx b/src/app/(main)/sales/contract-dashboard/page.tsx deleted file mode 100644 index 471e904..0000000 --- a/src/app/(main)/sales/contract-dashboard/page.tsx +++ /dev/null @@ -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
데이터 없음
; - } - 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 ( -
- - {slices.map((s, i) => ( - - - {s.pct >= 5 && ( - - {s.pct.toFixed(0)}% - - )} - - ))} - -
- {slices.map((s, i) => ( -
- - {s.label} - {s.value} -
- ))} -
-
- ); -} - -// 년도별 영업현황 — 막대 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 ( - - {/* Y축 가이드 */} - - - - {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 ( - - - - {y.YEAR} - - {y.GOAL_RATE.toFixed(0)}% - {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 ; - })()} - - ); - })} - - {/* 범례 */} - - - 영업목표(억) - - 수주(억) - - - 달성율(%) - - - ); -} - -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([]); - const [total, setTotal] = useState(null); - const [months, setMonths] = useState([]); - const [years, setYears] = useState([]); - 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) => ({ - 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[]; - 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[]).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({ - 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 ( -
-
-

영업관리_계약현황

-
- - -
-
- - - - - - - - - - - - - - - - - {loading &&
조회 중...
} - -
- {/* 좌: 계약현황 테이블 */} -
-
■ 계약현황 ({year})
-
- - - - - - - - - - {products.map((p) => )} - - - - {total && ( - - - {products.map((p) => ( - - ))} - - - - )} - {months.map((m) => ( - - - {products.map((p) => ( - - ))} - - - - ))} - {(!months || months.length === 0) && ( - - )} - -
수주확정 건수매출액
(억원)
출고
{p.NAME}
- {numberWithCommas(Number(total[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0))} - {(total.CONTRACT_COST_YEAR || 0).toFixed(2)}{numberWithCommas(total.RELEASE_CNT_YEAR || 0)}
{parseInt(m.MM, 10)}월 - {Number(m[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0) > 0 - ? numberWithCommas(Number(m[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0)) - : ""} - {m.CONTRACT_COST_YEAR ? m.CONTRACT_COST_YEAR.toFixed(2) : ""}{m.RELEASE_CNT_YEAR > 0 ? numberWithCommas(m.RELEASE_CNT_YEAR) : ""}
데이터 없음
-
-
- - {/* 중: 영업목표 테이블 + 제품별현황 파이 */} -
-
-
■ 영업목표
- - - - - - - - - - - - - - - {years.length === 0 ? ( - - ) : ( - years.slice().reverse().map((y) => ( - - - - - - - - )) - )} - -
년도영업목표
(억원)
현황
계약건수계약금액달성율
데이터 없음
{y.YEAR}{numberWithCommas(y.PRICE)}{numberWithCommas(y.CONTRACT_CNT_YEAR)}{(y.CONTRACT_COST_YEAR || 0).toFixed(2)}{(y.GOAL_RATE || 0).toFixed(1)}%
-
- -
-
■ 제품별현황
- -
-
- - {/* 우: 년도별 영업현황 + 고객사별현황 */} -
-
-
■ 년도별 영업현황
- -
- -
-
■ 고객사별현황
- -
-
-
-
- ); -} diff --git a/src/app/(main)/sales/contract/page.tsx b/src/app/(main)/sales/contract/page.tsx deleted file mode 100644 index 96d6f49..0000000 --- a/src/app/(main)/sales/contract/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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) => ({ - 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) => ({ - 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) => ( - 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 ( -
-
-

영업관리_계약관리

-
- - - -
-
- - - - - - - - - - - - - - - -
- setContractStartDate(e.target.value)} className="w-[140px]" /> - ~ - setContractEndDate(e.target.value)} className="w-[140px]" /> -
-
- - - - - - -
- -
총 {data.length}건
- - -
- ); -} diff --git a/src/app/(main)/sales/customer/page.tsx b/src/app/(main)/sales/customer/page.tsx deleted file mode 100644 index d9ced65..0000000 --- a/src/app/(main)/sales/customer/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

영업관리_고객관리

-
- - - -
-
- - - - setSupplyName(e.target.value)} className="w-[200px]" /> - - - - - - - - - -
총 {data.length}건
- - -
- ); -} diff --git a/src/app/(main)/sales/page.tsx b/src/app/(main)/sales/page.tsx deleted file mode 100644 index 9b9c0e0..0000000 --- a/src/app/(main)/sales/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

영업관리

-
- -
-
- - - - - - - setCustomerName(e.target.value)} className="w-[150px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/sales/release/page.tsx b/src/app/(main)/sales/release/page.tsx deleted file mode 100644 index 429900b..0000000 --- a/src/app/(main)/sales/release/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - 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) => ({ - 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) => ({ - value: String(r.USER_ID || ""), label: String(r.USER_NAME || ""), - })))).catch(() => {}); - }, []); - - const openForm = (row: Record) => { - 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) => ( - 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) => ( - 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) => ( - 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 ( -
-
-

영업관리_출고관리

-
- - -
-
- - - - - - - - - - - - - - - -
- setReleaseStartDate(e.target.value)} className="w-[140px]" /> - ~ - setReleaseEndDate(e.target.value)} className="w-[140px]" /> -
-
- - - - - - -
- -
총 {data.length}건 (수주 완료된 계약만 표시)
- - -
- ); -} diff --git a/src/app/(main)/scm/defect/page.tsx b/src/app/(main)/scm/defect/page.tsx deleted file mode 100644 index 5b7fd29..0000000 --- a/src/app/(main)/scm/defect/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

부적합품관리

-
- - -
-
- - - - - - - setSupplierName(e.target.value)} className="w-[150px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/scm/invoice/page.tsx b/src/app/(main)/scm/invoice/page.tsx deleted file mode 100644 index f7aac10..0000000 --- a/src/app/(main)/scm/invoice/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

SCM 거래명세서관리

-
- -
-
- - - - - - - setSupplierName(e.target.value)} className="w-[150px]" /> - - - setInvoiceDateFrom(e.target.value)} className="w-[140px]" /> - - - setInvoiceDateTo(e.target.value)} className="w-[140px]" /> - - - - -
- ); -} diff --git a/src/app/(main)/scm/order/arrival-plan/page.tsx b/src/app/(main)/scm/order/arrival-plan/page.tsx deleted file mode 100644 index bb148e0..0000000 --- a/src/app/(main)/scm/order/arrival-plan/page.tsx +++ /dev/null @@ -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([]); - 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) => ({ - 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 ( -
-

입고계획 등록

-
- - - - - - - - - - - - {parts.map((p, i) => ( - - - - - - - - ))} - -
PART NOPART NAME발주수량계획수량입고계획일
{p.PART_NO}{p.PART_NAME}{p.ORDER_QTY} updatePart(i, "ARRIVAL_QTY", e.target.value)} className="h-7 text-xs text-right" /> updatePart(i, "ARRIVAL_PLAN_DATE", e.target.value)} className="h-7 text-xs" />
-
-
- - -
-
- ); -} diff --git a/src/app/(main)/scm/order/page.tsx b/src/app/(main)/scm/order/page.tsx deleted file mode 100644 index 239541f..0000000 --- a/src/app/(main)/scm/order/page.tsx +++ /dev/null @@ -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[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - 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 ( -
-
-

SCM 발주관리

-
- - - -
-
- - - - - - - setPurchaseOrderNo(e.target.value)} className="w-[140px]" /> - - - setCustomerName(e.target.value)} className="w-[150px]" /> - - - setProjectNo(e.target.value)} className="w-[140px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/scm/payment/page.tsx b/src/app/(main)/scm/payment/page.tsx deleted file mode 100644 index fdffbdb..0000000 --- a/src/app/(main)/scm/payment/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

자금지급관리

-
- - -
-
- - - - - - - setSupplierName(e.target.value)} className="w-[150px]" /> - - - setPaymentDateFrom(e.target.value)} className="w-[140px]" /> - - - setPaymentDateTo(e.target.value)} className="w-[140px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/scm/quality/page.tsx b/src/app/(main)/scm/quality/page.tsx deleted file mode 100644 index c17ad1a..0000000 --- a/src/app/(main)/scm/quality/page.tsx +++ /dev/null @@ -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[]>([]); - - 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 ( -
-
-

공급업체품질관리

-
- -
-
- - - - - - - setSupplierName(e.target.value)} className="w-[150px]" /> - - - - - - - -
- ); -} diff --git a/src/app/(main)/work/diary/page.tsx b/src/app/(main)/work/diary/page.tsx deleted file mode 100644 index 29b358d..0000000 --- a/src/app/(main)/work/diary/page.tsx +++ /dev/null @@ -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([]); - const [unitOptions, setUnitOptions] = useState([]); - const [deptOptions, setDeptOptions] = useState([]); - const [workerOptions, setWorkerOptions] = useState([]); - - const [data, setData] = useState[]>([]); - const [selectedRows, setSelectedRows] = useState[]>([]); - - // 공용 드롭다운 - 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[]; - 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[]; - 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[]; - 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[]; - 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 ( -
-
-

작업일지

-
- - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/work/status/page.tsx b/src/app/(main)/work/status/page.tsx deleted file mode 100644 index 098ce4c..0000000 --- a/src/app/(main)/work/status/page.tsx +++ /dev/null @@ -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 ( - 로딩 중...
}> - - - ); -} - -function WorkStatusContent() { - const searchParams = useSearchParams(); - const tabParam = searchParams.get("tab"); - const [tab, setTab] = useState(tabParam === "employee" ? "employee" : "project"); - - // 메뉴 재클릭 등으로 ?tab= 변경될 때 탭 동기화 - useEffect(() => { - if (tabParam === "employee" || tabParam === "project") setTab(tabParam); - }, [tabParam]); - - // 공용 드롭다운 옵션 - const [projectOptions, setProjectOptions] = useState([]); - const [deptOptions, setDeptOptions] = useState([]); - const [workerOptions, setWorkerOptions] = useState([]); - - 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[]; - 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[]; - 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[]; - setWorkerOptions(rows.map((r) => ({ - value: String(r.USER_ID ?? ""), - label: `${r.DEPT_NAME ?? ""} / ${r.USER_NAME ?? ""}`, - }))); - }) - .catch(() => setWorkerOptions([])); - }, []); - - return ( -
-

작업관리 현황

- -
- setTab("project")} label="프로젝트 작업현황" /> - setTab("employee")} label="담당자별 작업현황" /> -
- - {tab === "project" ? ( - - ) : ( - - )} -
- ); -} - -function TabButton({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) { - return ( - - ); -} - -// ── 프로젝트 작업현황 ──────────────────────────────────────── -function ProjectTab({ projectOptions }: { projectOptions: Option[] }) { - const [projectObjId, setProjectObjId] = useState(""); - const [unitCode, setUnitCode] = useState(""); - const [unitOptions, setUnitOptions] = useState([]); - const [rows, setRows] = useState[]>([]); - const [sumMap, setSumMap] = useState({}); - - 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[]; - 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 ( - <> - - - - - - - - - -
- -
- - - - {/* 합계 */} - {rows.length > 0 && ( -
- (계) - - - - - - - - - - -
- )} - - ); -} - -// ── 담당자별 작업현황 ──────────────────────────────────────── -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[]>([]); - const [npList, setNpList] = useState[]>([]); - const [sumMap, setSumMap] = useState({}); - - // 부서 선택 시 작업자 목록 필터링 - 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 ( - <> - - - - - - - - - - - - - - - -
- -
- - - - {/* 프로젝트 합계 */} - {list.length > 0 && ( -
- 프로젝트 합계 - - - -
- )} - - ); -} - -function SumCell({ label, value }: { label: string; value?: number }) { - return ( - - {label}:{" "} - {numberWithCommas(Number(value) || 0)} - - ); -}