From 8c29225043535214d95f2b1d648ade6543d0d508 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 20 Mar 2026 16:09:39 +0900 Subject: [PATCH] feat: enhance application with receiving management and global font size adjustments - Added new route for receiving management, allowing users to manage incoming goods effectively. - Updated global CSS to unify font sizes across the application, ensuring a consistent user experience. - Modified customer and sales order fetching logic to improve data retrieval and handling, including enhanced error logging for better debugging. These changes aim to streamline the receiving process and improve overall UI consistency within the application. --- backend-node/src/app.ts | 2 + .../src/controllers/receivingController.ts | 487 +++++++ backend-node/src/routes/receivingRoutes.ts | 40 + .../app/(main)/logistics/receiving/page.tsx | 1246 +++++++++++++++++ frontend/app/(main)/sales/claim/page.tsx | 36 +- .../app/(main)/sales/shipping-plan/page.tsx | 113 +- frontend/app/globals.css | 10 + frontend/lib/api/receiving.ts | 179 +++ 8 files changed, 2057 insertions(+), 56 deletions(-) create mode 100644 backend-node/src/controllers/receivingController.ts create mode 100644 backend-node/src/routes/receivingRoutes.ts create mode 100644 frontend/app/(main)/logistics/receiving/page.tsx create mode 100644 frontend/lib/api/receiving.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 9530259f..ae2424a0 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -150,6 +150,7 @@ import salesReportRoutes from "./routes/salesReportRoutes"; // 영업 리포트 import analyticsReportRoutes from "./routes/analyticsReportRoutes"; // 분석 리포트 (생산/재고/구매/품질/설비/금형) import designRoutes from "./routes/designRoutes"; // 설계 모듈 (DR/ECR/프로젝트/ECN) import materialStatusRoutes from "./routes/materialStatusRoutes"; // 자재현황 +import receivingRoutes from "./routes/receivingRoutes"; // 입고관리 import processInfoRoutes from "./routes/processInfoRoutes"; // 공정정보관리 import { BatchSchedulerService } from "./services/batchSchedulerService"; // import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석 @@ -351,6 +352,7 @@ app.use("/api/work-instruction", workInstructionRoutes); // 작업지시 관리 app.use("/api/sales-report", salesReportRoutes); // 영업 리포트 app.use("/api/report", analyticsReportRoutes); // 분석 리포트 (생산/재고/구매/품질/설비/금형) app.use("/api/design", designRoutes); // 설계 모듈 +app.use("/api/receiving", receivingRoutes); // 입고관리 app.use("/api", screenEmbeddingRoutes); // 화면 임베딩 및 데이터 전달 app.use("/api/ai/v1", aiAssistantProxy); // AI 어시스턴트 (동일 서비스 내 프록시 → AI 서비스 포트) app.use("/api/vehicle", vehicleTripRoutes); // 차량 운행 이력 관리 diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts new file mode 100644 index 00000000..132fcb3a --- /dev/null +++ b/backend-node/src/controllers/receivingController.ts @@ -0,0 +1,487 @@ +/** + * 입고관리 컨트롤러 + * + * 입고유형별 소스 테이블: + * - 구매입고 → purchase_order_mng (발주) + * - 반품입고 → shipment_instruction + shipment_instruction_detail (출하) + * - 기타입고 → item_info (품목) + */ + +import { Response } from "express"; +import { AuthenticatedRequest } from "../types/auth"; +import { getPool } from "../database/db"; +import { logger } from "../utils/logger"; + +// 입고 목록 조회 +export async function getList(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { + inbound_type, + inbound_status, + search_keyword, + date_from, + date_to, + } = req.query; + + const conditions: string[] = []; + const params: any[] = []; + let paramIdx = 1; + + if (companyCode === "*") { + // 최고 관리자: 전체 조회 + } else { + conditions.push(`im.company_code = $${paramIdx}`); + params.push(companyCode); + paramIdx++; + } + + if (inbound_type && inbound_type !== "all") { + conditions.push(`im.inbound_type = $${paramIdx}`); + params.push(inbound_type); + paramIdx++; + } + + if (inbound_status && inbound_status !== "all") { + conditions.push(`im.inbound_status = $${paramIdx}`); + params.push(inbound_status); + paramIdx++; + } + + if (search_keyword) { + conditions.push( + `(im.inbound_number ILIKE $${paramIdx} OR im.item_name ILIKE $${paramIdx} OR im.item_number ILIKE $${paramIdx} OR im.supplier_name ILIKE $${paramIdx} OR im.reference_number ILIKE $${paramIdx})` + ); + params.push(`%${search_keyword}%`); + paramIdx++; + } + + if (date_from) { + conditions.push(`im.inbound_date >= $${paramIdx}::date`); + params.push(date_from); + paramIdx++; + } + if (date_to) { + conditions.push(`im.inbound_date <= $${paramIdx}::date`); + params.push(date_to); + paramIdx++; + } + + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + + const query = ` + SELECT + im.*, + wh.warehouse_name + FROM inbound_mng im + LEFT JOIN warehouse_info wh + ON im.warehouse_code = wh.warehouse_code + AND im.company_code = wh.company_code + ${whereClause} + ORDER BY im.created_date DESC + `; + + const pool = getPool(); + const result = await pool.query(query, params); + + logger.info("입고 목록 조회", { + companyCode, + rowCount: result.rowCount, + }); + + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("입고 목록 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// 입고 등록 (다건) +export async function create(req: AuthenticatedRequest, res: Response) { + const pool = getPool(); + const client = await pool.connect(); + + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { items, inbound_number, inbound_date, warehouse_code, location_code, inspector, manager, memo } = req.body; + + if (!items || !Array.isArray(items) || items.length === 0) { + return res.status(400).json({ success: false, message: "입고 품목이 없습니다." }); + } + + await client.query("BEGIN"); + + const insertedRows: any[] = []; + + for (const item of items) { + const result = await client.query( + `INSERT INTO inbound_mng ( + company_code, inbound_number, inbound_type, inbound_date, + reference_number, supplier_code, supplier_name, + item_number, item_name, spec, material, unit, + inbound_qty, unit_price, total_amount, + lot_number, warehouse_code, location_code, + inbound_status, inspection_status, + inspector, manager, memo, + source_table, source_id, + created_date, created_by, writer, status + ) VALUES ( + $1, $2, $3, $4::date, + $5, $6, $7, + $8, $9, $10, $11, $12, + $13, $14, $15, + $16, $17, $18, + $19, $20, + $21, $22, $23, + $24, $25, + NOW(), $26, $26, '입고' + ) RETURNING *`, + [ + companyCode, + inbound_number || item.inbound_number, + item.inbound_type, + inbound_date || item.inbound_date, + item.reference_number || null, + item.supplier_code || null, + item.supplier_name || null, + item.item_number || null, + item.item_name || null, + item.spec || null, + item.material || null, + item.unit || "EA", + item.inbound_qty || 0, + item.unit_price || 0, + item.total_amount || 0, + item.lot_number || null, + warehouse_code || item.warehouse_code || null, + location_code || item.location_code || null, + item.inbound_status || "대기", + item.inspection_status || "대기", + inspector || item.inspector || null, + manager || item.manager || null, + memo || item.memo || null, + item.source_table || null, + item.source_id || null, + userId, + ] + ); + + insertedRows.push(result.rows[0]); + + // 구매입고인 경우 발주의 received_qty 업데이트 + if (item.inbound_type === "구매입고" && item.source_id && item.source_table === "purchase_order_mng") { + await client.query( + `UPDATE purchase_order_mng + SET received_qty = CAST( + COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1 AS text + ), + remain_qty = CAST( + GREATEST(COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) + - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) - $1, 0) AS text + ), + status = CASE + WHEN COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1 + >= COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) + THEN '입고완료' + ELSE '부분입고' + END, + updated_date = NOW() + WHERE id = $2 AND company_code = $3`, + [item.inbound_qty || 0, item.source_id, companyCode] + ); + } + } + + await client.query("COMMIT"); + + logger.info("입고 등록 완료", { + companyCode, + userId, + count: insertedRows.length, + inbound_number, + }); + + return res.json({ + success: true, + data: insertedRows, + message: `${insertedRows.length}건 입고 등록 완료`, + }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("입고 등록 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } +} + +// 입고 수정 +export async function update(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { id } = req.params; + const { + inbound_date, inbound_qty, unit_price, total_amount, + lot_number, warehouse_code, location_code, + inbound_status, inspection_status, + inspector, manager: mgr, memo, + } = req.body; + + const pool = getPool(); + const result = await pool.query( + `UPDATE inbound_mng SET + inbound_date = COALESCE($1::date, inbound_date), + inbound_qty = COALESCE($2, inbound_qty), + unit_price = COALESCE($3, unit_price), + total_amount = COALESCE($4, total_amount), + lot_number = COALESCE($5, lot_number), + warehouse_code = COALESCE($6, warehouse_code), + location_code = COALESCE($7, location_code), + inbound_status = COALESCE($8, inbound_status), + inspection_status = COALESCE($9, inspection_status), + inspector = COALESCE($10, inspector), + manager = COALESCE($11, manager), + memo = COALESCE($12, memo), + updated_date = NOW(), + updated_by = $13 + WHERE id = $14 AND company_code = $15 + RETURNING *`, + [ + inbound_date, inbound_qty, unit_price, total_amount, + lot_number, warehouse_code, location_code, + inbound_status, inspection_status, + inspector, mgr, memo, + userId, id, companyCode, + ] + ); + + if (result.rowCount === 0) { + return res.status(404).json({ success: false, message: "입고 데이터를 찾을 수 없습니다." }); + } + + logger.info("입고 수정", { companyCode, userId, id }); + + return res.json({ success: true, data: result.rows[0] }); + } catch (error: any) { + logger.error("입고 수정 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// 입고 삭제 +export async function deleteReceiving(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { id } = req.params; + const pool = getPool(); + + const result = await pool.query( + `DELETE FROM inbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`, + [id, companyCode] + ); + + if (result.rowCount === 0) { + return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." }); + } + + logger.info("입고 삭제", { companyCode, id }); + + return res.json({ success: true, message: "삭제 완료" }); + } catch (error: any) { + logger.error("입고 삭제 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// 구매입고용: 발주 데이터 조회 (미입고분) +export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { keyword } = req.query; + + const conditions: string[] = ["company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; + + // 잔량이 있는 것만 조회 + conditions.push( + `COALESCE(CAST(NULLIF(remain_qty, '') AS numeric), COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0)) > 0` + ); + conditions.push(`status NOT IN ('입고완료', '취소')`); + + if (keyword) { + conditions.push( + `(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})` + ); + params.push(`%${keyword}%`); + paramIdx++; + } + + const pool = getPool(); + const result = await pool.query( + `SELECT + id, purchase_no, order_date, supplier_code, supplier_name, + item_code, item_name, spec, material, + COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) AS order_qty, + COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) AS received_qty, + COALESCE(CAST(NULLIF(remain_qty, '') AS numeric), + COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) + - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + ) AS remain_qty, + COALESCE(CAST(NULLIF(unit_price, '') AS numeric), 0) AS unit_price, + status, due_date + FROM purchase_order_mng + WHERE ${conditions.join(" AND ")} + ORDER BY order_date DESC, purchase_no`, + params + ); + + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("발주 데이터 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// 반품입고용: 출하 데이터 조회 +export async function getShipments(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { keyword } = req.query; + + const conditions: string[] = ["si.company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; + + if (keyword) { + conditions.push( + `(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})` + ); + params.push(`%${keyword}%`); + paramIdx++; + } + + const pool = getPool(); + const result = await pool.query( + `SELECT + sid.id AS detail_id, + si.id AS instruction_id, + si.instruction_no, + si.instruction_date, + si.partner_id, + si.status AS instruction_status, + sid.item_code, + sid.item_name, + sid.spec, + sid.material, + COALESCE(sid.ship_qty, 0) AS ship_qty, + COALESCE(sid.order_qty, 0) AS order_qty, + sid.source_type + FROM shipment_instruction si + JOIN shipment_instruction_detail sid + ON si.id = sid.instruction_id + AND si.company_code = sid.company_code + WHERE ${conditions.join(" AND ")} + ORDER BY si.instruction_date DESC, si.instruction_no`, + params + ); + + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("출하 데이터 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// 기타입고용: 품목 데이터 조회 +export async function getItems(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { keyword } = req.query; + + const conditions: string[] = ["company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; + + if (keyword) { + conditions.push( + `(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})` + ); + params.push(`%${keyword}%`); + paramIdx++; + } + + const pool = getPool(); + const result = await pool.query( + `SELECT + id, item_number, item_name, size AS spec, material, unit, + COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price + FROM item_info + WHERE ${conditions.join(" AND ")} + ORDER BY item_name`, + params + ); + + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("품목 데이터 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// 입고번호 자동생성 +export async function generateNumber(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const pool = getPool(); + + const today = new Date(); + const yyyy = today.getFullYear(); + const prefix = `RCV-${yyyy}-`; + + const result = await pool.query( + `SELECT inbound_number FROM inbound_mng + WHERE company_code = $1 AND inbound_number LIKE $2 + ORDER BY inbound_number DESC LIMIT 1`, + [companyCode, `${prefix}%`] + ); + + let seq = 1; + if (result.rows.length > 0) { + const lastNo = result.rows[0].inbound_number; + const lastSeq = parseInt(lastNo.replace(prefix, ""), 10); + if (!isNaN(lastSeq)) seq = lastSeq + 1; + } + + const newNumber = `${prefix}${String(seq).padStart(4, "0")}`; + + return res.json({ success: true, data: newNumber }); + } catch (error: any) { + logger.error("입고번호 생성 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// 창고 목록 조회 +export async function getWarehouses(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const pool = getPool(); + + const result = await pool.query( + `SELECT warehouse_code, warehouse_name, warehouse_type + FROM warehouse_info + WHERE company_code = $1 AND status != '삭제' + ORDER BY warehouse_name`, + [companyCode] + ); + + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("창고 목록 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} diff --git a/backend-node/src/routes/receivingRoutes.ts b/backend-node/src/routes/receivingRoutes.ts new file mode 100644 index 00000000..0b5a5c13 --- /dev/null +++ b/backend-node/src/routes/receivingRoutes.ts @@ -0,0 +1,40 @@ +/** + * 입고관리 라우트 + */ + +import { Router } from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import * as receivingController from "../controllers/receivingController"; + +const router = Router(); + +router.use(authenticateToken); + +// 입고 목록 조회 +router.get("/list", receivingController.getList); + +// 입고번호 자동생성 +router.get("/generate-number", receivingController.generateNumber); + +// 창고 목록 조회 +router.get("/warehouses", receivingController.getWarehouses); + +// 소스 데이터: 발주 (구매입고) +router.get("/source/purchase-orders", receivingController.getPurchaseOrders); + +// 소스 데이터: 출하 (반품입고) +router.get("/source/shipments", receivingController.getShipments); + +// 소스 데이터: 품목 (기타입고) +router.get("/source/items", receivingController.getItems); + +// 입고 등록 +router.post("/", receivingController.create); + +// 입고 수정 +router.put("/:id", receivingController.update); + +// 입고 삭제 +router.delete("/:id", receivingController.deleteReceiving); + +export default router; diff --git a/frontend/app/(main)/logistics/receiving/page.tsx b/frontend/app/(main)/logistics/receiving/page.tsx new file mode 100644 index 00000000..eba618d5 --- /dev/null +++ b/frontend/app/(main)/logistics/receiving/page.tsx @@ -0,0 +1,1246 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { + Search, + Plus, + Trash2, + RotateCcw, + Loader2, + Package, + X, + Save, + ChevronRight, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + getReceivingList, + createReceiving, + deleteReceiving, + generateReceivingNumber, + getReceivingWarehouses, + getPurchaseOrderSources, + getShipmentSources, + getItemSources, + type InboundItem, + type PurchaseOrderSource, + type ShipmentSource, + type ItemSource, + type WarehouseOption, +} from "@/lib/api/receiving"; + +// 입고유형 옵션 +const INBOUND_TYPES = [ + { value: "구매입고", label: "구매입고", color: "bg-blue-100 text-blue-800" }, + { value: "반품입고", label: "반품입고", color: "bg-pink-100 text-pink-800" }, + { value: "기타입고", label: "기타입고", color: "bg-gray-100 text-gray-800" }, +]; + +const INBOUND_STATUS_OPTIONS = [ + { value: "대기", label: "대기", color: "bg-amber-100 text-amber-800" }, + { value: "입고완료", label: "입고완료", color: "bg-emerald-100 text-emerald-800" }, + { value: "부분입고", label: "부분입고", color: "bg-amber-100 text-amber-800" }, + { value: "입고취소", label: "입고취소", color: "bg-red-100 text-red-800" }, +]; + +const getTypeColor = (type: string) => INBOUND_TYPES.find((t) => t.value === type)?.color || "bg-gray-100 text-gray-800"; +const getStatusColor = (status: string) => INBOUND_STATUS_OPTIONS.find((s) => s.value === status)?.color || "bg-gray-100 text-gray-800"; + +// 소스 테이블 한글명 매핑 +const SOURCE_TABLE_LABEL: Record = { + purchase_order_mng: "발주", + shipment_instruction_detail: "출하", + item_info: "품목", +}; + +// 선택된 소스 아이템 (등록 모달에서 사용) +interface SelectedSourceItem { + key: string; + inbound_type: string; + reference_number: string; + supplier_code: string; + supplier_name: string; + item_number: string; + item_name: string; + spec: string; + material: string; + unit: string; + inbound_qty: number; + unit_price: number; + total_amount: number; + source_table: string; + source_id: string; +} + +export default function ReceivingPage() { + // 목록 데이터 + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [checkedIds, setCheckedIds] = useState([]); + + // 검색 필터 + const [searchType, setSearchType] = useState("all"); + const [searchStatus, setSearchStatus] = useState("all"); + const [searchKeyword, setSearchKeyword] = useState(""); + const [searchDateFrom, setSearchDateFrom] = useState(""); + const [searchDateTo, setSearchDateTo] = useState(""); + + // 등록 모달 + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalInboundType, setModalInboundType] = useState("구매입고"); + const [modalInboundNo, setModalInboundNo] = useState(""); + const [modalInboundDate, setModalInboundDate] = useState(""); + const [modalWarehouse, setModalWarehouse] = useState(""); + const [modalLocation, setModalLocation] = useState(""); + const [modalInspector, setModalInspector] = useState(""); + const [modalManager, setModalManager] = useState(""); + const [modalMemo, setModalMemo] = useState(""); + const [selectedItems, setSelectedItems] = useState([]); + const [saving, setSaving] = useState(false); + + // 소스 데이터 + const [sourceKeyword, setSourceKeyword] = useState(""); + const [sourceLoading, setSourceLoading] = useState(false); + const [purchaseOrders, setPurchaseOrders] = useState([]); + const [shipments, setShipments] = useState([]); + const [items, setItems] = useState([]); + const [warehouses, setWarehouses] = useState([]); + + // 날짜 초기화 + useEffect(() => { + const today = new Date(); + const threeMonthsAgo = new Date(today); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); + setSearchDateTo(today.toISOString().split("T")[0]); + }, []); + + // 목록 조회 + const fetchList = useCallback(async () => { + setLoading(true); + try { + const res = await getReceivingList({ + inbound_type: searchType !== "all" ? searchType : undefined, + inbound_status: searchStatus !== "all" ? searchStatus : undefined, + search_keyword: searchKeyword || undefined, + date_from: searchDateFrom || undefined, + date_to: searchDateTo || undefined, + }); + if (res.success) setData(res.data); + } catch { + // 에러 무시 + } finally { + setLoading(false); + } + }, [searchType, searchStatus, searchKeyword, searchDateFrom, searchDateTo]); + + useEffect(() => { + fetchList(); + }, [fetchList]); + + // 창고 목록 로드 + useEffect(() => { + (async () => { + try { + const res = await getReceivingWarehouses(); + if (res.success) setWarehouses(res.data); + } catch { + // ignore + } + })(); + }, []); + + // 검색 초기화 + const handleReset = () => { + setSearchType("all"); + setSearchStatus("all"); + setSearchKeyword(""); + const today = new Date(); + const threeMonthsAgo = new Date(today); + threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); + setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); + setSearchDateTo(today.toISOString().split("T")[0]); + }; + + // 체크박스 + const allChecked = data.length > 0 && checkedIds.length === data.length; + const toggleCheckAll = () => { + setCheckedIds(allChecked ? [] : data.map((d) => d.id)); + }; + const toggleCheck = (id: string) => { + setCheckedIds((prev) => + prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id] + ); + }; + + // 삭제 + const handleDelete = async () => { + if (checkedIds.length === 0) return; + if (!confirm(`선택한 ${checkedIds.length}건을 삭제하시겠습니까?`)) return; + for (const id of checkedIds) { + await deleteReceiving(id); + } + setCheckedIds([]); + fetchList(); + }; + + // --- 등록 모달 --- + + // 소스 데이터 로드 함수 + const loadSourceData = useCallback( + async (type: string, keyword?: string) => { + setSourceLoading(true); + try { + if (type === "구매입고") { + const res = await getPurchaseOrderSources(keyword || undefined); + if (res.success) setPurchaseOrders(res.data); + } else if (type === "반품입고") { + const res = await getShipmentSources(keyword || undefined); + if (res.success) setShipments(res.data); + } else { + const res = await getItemSources(keyword || undefined); + if (res.success) setItems(res.data); + } + } catch { + // ignore + } finally { + setSourceLoading(false); + } + }, + [] + ); + + const openRegisterModal = async () => { + const defaultType = "구매입고"; + setModalInboundType(defaultType); + setModalInboundDate(new Date().toISOString().split("T")[0]); + setModalWarehouse(""); + setModalLocation(""); + setModalInspector(""); + setModalManager(""); + setModalMemo(""); + setSelectedItems([]); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setIsModalOpen(true); + + // 입고번호 생성 + 발주 데이터 동시 로드 + try { + const [numRes] = await Promise.all([ + generateReceivingNumber(), + loadSourceData(defaultType), + ]); + if (numRes.success) setModalInboundNo(numRes.data); + } catch { + setModalInboundNo(""); + } + }; + + // 검색 버튼 클릭 시 + const searchSourceData = useCallback(async () => { + await loadSourceData(modalInboundType, sourceKeyword || undefined); + }, [modalInboundType, sourceKeyword, loadSourceData]); + + // 입고유형 변경 시 소스 데이터 자동 리로드 + const handleInboundTypeChange = useCallback( + (type: string) => { + setModalInboundType(type); + setSourceKeyword(""); + setPurchaseOrders([]); + setShipments([]); + setItems([]); + setSelectedItems([]); + loadSourceData(type); + }, + [loadSourceData] + ); + + // 발주 품목 추가 + const addPurchaseOrder = (po: PurchaseOrderSource) => { + const key = `po-${po.id}`; + if (selectedItems.some((s) => s.key === key)) return; + setSelectedItems((prev) => [ + ...prev, + { + key, + inbound_type: "구매입고", + reference_number: po.purchase_no, + supplier_code: po.supplier_code, + supplier_name: po.supplier_name, + item_number: po.item_code, + item_name: po.item_name, + spec: po.spec || "", + material: po.material || "", + unit: "EA", + inbound_qty: po.remain_qty, + unit_price: po.unit_price, + total_amount: po.remain_qty * po.unit_price, + source_table: "purchase_order_mng", + source_id: po.id, + }, + ]); + }; + + // 출하 품목 추가 + const addShipment = (sh: ShipmentSource) => { + const key = `sh-${sh.detail_id}`; + if (selectedItems.some((s) => s.key === key)) return; + setSelectedItems((prev) => [ + ...prev, + { + key, + inbound_type: "반품입고", + reference_number: sh.instruction_no, + supplier_code: "", + supplier_name: sh.partner_id, + item_number: sh.item_code, + item_name: sh.item_name, + spec: sh.spec || "", + material: sh.material || "", + unit: "EA", + inbound_qty: sh.ship_qty, + unit_price: 0, + total_amount: 0, + source_table: "shipment_instruction_detail", + source_id: String(sh.detail_id), + }, + ]); + }; + + // 품목 추가 + const addItem = (item: ItemSource) => { + const key = `item-${item.id}`; + if (selectedItems.some((s) => s.key === key)) return; + setSelectedItems((prev) => [ + ...prev, + { + key, + inbound_type: "기타입고", + reference_number: item.item_number, + supplier_code: "", + supplier_name: "", + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec || "", + material: item.material || "", + unit: item.unit || "EA", + inbound_qty: 0, + unit_price: item.standard_price, + total_amount: 0, + source_table: "item_info", + source_id: item.id, + }, + ]); + }; + + // 선택 품목 수량 변경 + const updateItemQty = (key: string, qty: number) => { + setSelectedItems((prev) => + prev.map((item) => + item.key === key + ? { ...item, inbound_qty: qty, total_amount: qty * item.unit_price } + : item + ) + ); + }; + + // 선택 품목 단가 변경 + const updateItemPrice = (key: string, price: number) => { + setSelectedItems((prev) => + prev.map((item) => + item.key === key + ? { ...item, unit_price: price, total_amount: item.inbound_qty * price } + : item + ) + ); + }; + + // 선택 품목 삭제 + const removeItem = (key: string) => { + setSelectedItems((prev) => prev.filter((item) => item.key !== key)); + }; + + // 저장 + const handleSave = async () => { + if (selectedItems.length === 0) { + alert("입고할 품목을 선택해주세요."); + return; + } + if (!modalInboundDate) { + alert("입고일을 입력해주세요."); + return; + } + + const zeroQtyItems = selectedItems.filter((i) => !i.inbound_qty || i.inbound_qty <= 0); + if (zeroQtyItems.length > 0) { + alert("입고수량이 0인 품목이 있습니다. 수량을 입력해주세요."); + return; + } + + setSaving(true); + try { + const res = await createReceiving({ + inbound_number: modalInboundNo, + inbound_date: modalInboundDate, + warehouse_code: modalWarehouse || undefined, + location_code: modalLocation || undefined, + inspector: modalInspector || undefined, + manager: modalManager || undefined, + memo: modalMemo || undefined, + items: selectedItems.map((item) => ({ + inbound_type: item.inbound_type, + reference_number: item.reference_number, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + item_number: item.item_number, + item_name: item.item_name, + spec: item.spec, + material: item.material, + unit: item.unit, + inbound_qty: item.inbound_qty, + unit_price: item.unit_price, + total_amount: item.total_amount, + source_table: item.source_table, + source_id: item.source_id, + inbound_status: "입고완료", + inspection_status: "대기", + })), + }); + + if (res.success) { + alert(res.message || "입고 등록 완료"); + setIsModalOpen(false); + fetchList(); + } + } catch { + alert("입고 등록 중 오류가 발생했습니다."); + } finally { + setSaving(false); + } + }; + + // 합계 계산 + const totalSummary = useMemo(() => { + return { + count: selectedItems.length, + qty: selectedItems.reduce((sum, i) => sum + (i.inbound_qty || 0), 0), + amount: selectedItems.reduce((sum, i) => sum + (i.total_amount || 0), 0), + }; + }, [selectedItems]); + + return ( +
+ {/* 검색 영역 */} +
+ + + + + setSearchKeyword(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && fetchList()} + className="h-9 w-[240px] text-xs" + /> + +
+ setSearchDateFrom(e.target.value)} + className="h-9 w-[140px] text-xs" + /> + ~ + setSearchDateTo(e.target.value)} + className="h-9 w-[140px] text-xs" + /> +
+ + + + +
+ + +
+
+ + {/* 입고 목록 테이블 */} +
+
+
+ +

입고 목록

+ + 총 {data.length}건 + +
+
+ +
+ + + + + + + 입고번호 + 입고유형 + 입고일 + 참조번호 + 데이터출처 + 공급처 + 품목코드 + 품목명 + 규격 + 입고수량 + 단가 + 금액 + 창고 + 입고상태 + 비고 + + + + {loading ? ( + + + + + + ) : data.length === 0 ? ( + + +
+ +

등록된 입고 내역이 없습니다

+

+ '입고 등록' 버튼을 클릭하여 입고를 추가하세요 +

+
+
+
+ ) : ( + data.map((row) => ( + toggleCheck(row.id)} + > + e.stopPropagation()} + > + toggleCheck(row.id)} + /> + + + {row.inbound_number} + + + + {row.inbound_type || "-"} + + + + {row.inbound_date + ? new Date(row.inbound_date).toLocaleDateString("ko-KR") + : "-"} + + + {row.reference_number || "-"} + + + {row.source_table + ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table + : "-"} + + + {row.supplier_name || "-"} + + + {row.item_number || "-"} + + {row.item_name || "-"} + {row.spec || "-"} + + {Number(row.inbound_qty || 0).toLocaleString()} + + + {Number(row.unit_price || 0).toLocaleString()} + + + {Number(row.total_amount || 0).toLocaleString()} + + + {row.warehouse_name || row.warehouse_code || "-"} + + + + {row.inbound_status || "-"} + + + + {row.memo || "-"} + + + )) + )} +
+
+
+
+ + {/* 입고 등록 모달 */} + + + + 입고 등록 + + 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가하세요. + + + + {/* 입고유형 선택 */} +
+ + + + {modalInboundType === "구매입고" + ? "발주 데이터에서 입고 처리합니다." + : modalInboundType === "반품입고" + ? "출하 데이터에서 반품 입고 처리합니다." + : "품목 데이터를 직접 선택하여 입고 처리합니다."} + +
+ + {/* 메인 콘텐츠: 좌측 소스 데이터 / 우측 선택 품목 */} +
+ + {/* 좌측: 근거 데이터 검색 */} + +
+ {/* 소스 검색 바 */} +
+ setSourceKeyword(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && searchSourceData()} + className="h-8 flex-1 text-xs" + /> + +
+ + {/* 소스 데이터 테이블 */} +
+

+ {modalInboundType === "구매입고" + ? "미입고 발주 목록" + : modalInboundType === "반품입고" + ? "출하 목록" + : "품목 목록"} +

+ + {sourceLoading ? ( +
+ +
+ ) : modalInboundType === "구매입고" ? ( + s.key)} + /> + ) : modalInboundType === "반품입고" ? ( + s.key)} + /> + ) : ( + s.key)} + /> + )} +
+
+
+ + + + {/* 우측: 입고 정보 + 선택 품목 */} + +
+ {/* 입고 정보 입력 */} +
+

입고 정보

+
+
+ + +
+
+ + setModalInboundDate(e.target.value)} + className="h-8 text-xs" + /> +
+
+ + +
+
+ + setModalLocation(e.target.value)} + placeholder="위치 입력" + className="h-8 text-xs" + /> +
+
+ + setModalInspector(e.target.value)} + placeholder="검수자" + className="h-8 text-xs" + /> +
+
+ + setModalManager(e.target.value)} + placeholder="담당자" + className="h-8 text-xs" + /> +
+
+ + setModalMemo(e.target.value)} + placeholder="메모" + className="h-8 text-xs" + /> +
+
+
+ + {/* 선택된 품목 테이블 */} +
+

+ 입고 처리 품목 ({selectedItems.length}건) +

+ + {selectedItems.length === 0 ? ( +
+ + 좌측에서 품목을 선택하여 추가해주세요 +
+ ) : ( + + + + No + 품목명 + 참조번호 + + 수량 + + + 단가 + + + 금액 + + + + + + {selectedItems.map((item, idx) => ( + + + {idx + 1} + + +
+ + {item.item_name} + + + {item.item_number} + {item.spec ? ` | ${item.spec}` : ""} + +
+
+ + {item.reference_number} + + + + updateItemQty( + item.key, + Number(e.target.value) || 0 + ) + } + className="h-7 w-[70px] text-right text-xs" + min={0} + /> + + + + updateItemPrice( + item.key, + Number(e.target.value) || 0 + ) + } + className="h-7 w-[70px] text-right text-xs" + min={0} + /> + + + {item.total_amount.toLocaleString()} + + + + +
+ ))} +
+
+ )} +
+
+
+
+
+ + {/* 푸터 */} + +
+ {selectedItems.length > 0 ? ( + <> + {totalSummary.count}건 | 수량 합계:{" "} + {totalSummary.qty.toLocaleString()} | 금액 합계:{" "} + {totalSummary.amount.toLocaleString()}원 + + ) : ( + "품목을 추가해주세요" + )} +
+
+ + +
+
+
+
+
+ ); +} + +// --- 소스 데이터 테이블 컴포넌트들 --- + +function SourcePurchaseOrderTable({ + data, + onAdd, + selectedKeys, +}: { + data: PurchaseOrderSource[]; + onAdd: (po: PurchaseOrderSource) => void; + selectedKeys: string[]; +}) { + if (data.length === 0) { + return ( +
+ 검색 버튼을 눌러 발주 데이터를 조회하세요 +
+ ); + } + + return ( + + + + + 발주번호 + 공급처 + 품목 + 발주수량 + 입고수량 + 미입고 + + + + {data.map((po) => { + const isSelected = selectedKeys.includes(`po-${po.id}`); + return ( + !isSelected && onAdd(po)} + > + + {isSelected ? ( + + 추가됨 + + ) : ( + + )} + + {po.purchase_no} + {po.supplier_name} + +
+ {po.item_name} + + {po.item_code} + {po.spec ? ` | ${po.spec}` : ""} + +
+
+ + {Number(po.order_qty).toLocaleString()} + + + {Number(po.received_qty).toLocaleString()} + + + {Number(po.remain_qty).toLocaleString()} + +
+ ); + })} +
+
+ ); +} + +function SourceShipmentTable({ + data, + onAdd, + selectedKeys, +}: { + data: ShipmentSource[]; + onAdd: (sh: ShipmentSource) => void; + selectedKeys: string[]; +}) { + if (data.length === 0) { + return ( +
+ 검색 버튼을 눌러 출하 데이터를 조회하세요 +
+ ); + } + + return ( + + + + + 출하번호 + 출하일 + 거래처 + 품목 + 출하수량 + + + + {data.map((sh) => { + const isSelected = selectedKeys.includes(`sh-${sh.detail_id}`); + return ( + !isSelected && onAdd(sh)} + > + + {isSelected ? ( + + 추가됨 + + ) : ( + + )} + + {sh.instruction_no} + + {sh.instruction_date + ? new Date(sh.instruction_date).toLocaleDateString("ko-KR") + : "-"} + + {sh.partner_id} + +
+ {sh.item_name} + + {sh.item_code} + {sh.spec ? ` | ${sh.spec}` : ""} + +
+
+ + {Number(sh.ship_qty).toLocaleString()} + +
+ ); + })} +
+
+ ); +} + +function SourceItemTable({ + data, + onAdd, + selectedKeys, +}: { + data: ItemSource[]; + onAdd: (item: ItemSource) => void; + selectedKeys: string[]; +}) { + if (data.length === 0) { + return ( +
+ 검색 버튼을 눌러 품목 데이터를 조회하세요 +
+ ); + } + + return ( + + + + + 품목 + 규격 + 재질 + 단위 + 기준가 + + + + {data.map((item) => { + const isSelected = selectedKeys.includes(`item-${item.id}`); + return ( + !isSelected && onAdd(item)} + > + + {isSelected ? ( + + 추가됨 + + ) : ( + + )} + + +
+ {item.item_name} + + {item.item_number} + +
+
+ {item.spec || "-"} + {item.material || "-"} + {item.unit || "-"} + + {Number(item.standard_price).toLocaleString()} + +
+ ); + })} +
+
+ ); +} diff --git a/frontend/app/(main)/sales/claim/page.tsx b/frontend/app/(main)/sales/claim/page.tsx index 25fc3b4c..12d37472 100644 --- a/frontend/app/(main)/sales/claim/page.tsx +++ b/frontend/app/(main)/sales/claim/page.tsx @@ -212,21 +212,24 @@ export default function ClaimManagementPage() { }, []); // 거래처 목록 조회 - const fetchCustomers = useCallback(async () => { - if (customers.length > 0) return; + const fetchCustomers = useCallback(async (force = false) => { + if (!force && customers.length > 0) return; setCustomersLoading(true); try { const res = await apiClient.post("/table-management/tables/customer_mng/data", { page: 1, size: 9999, - autoFilter: true, + autoFilter: { enabled: true }, }); - if (res.data?.success && res.data?.data?.rows) { - const list: CustomerOption[] = res.data.data.rows.map((row: any) => ({ - customerCode: row.customer_code || "", + const rows = res.data?.data?.data || res.data?.data?.rows; + if (res.data?.success && Array.isArray(rows)) { + const list: CustomerOption[] = rows.map((row: any) => ({ + customerCode: row.customer_code || row.id || "", customerName: row.customer_name || "", })); setCustomers(list); + } else { + console.warn("거래처 응답 구조 확인:", JSON.stringify(res.data, null, 2)); } } catch (e) { console.error("거래처 목록 조회 실패:", e); @@ -236,19 +239,20 @@ export default function ClaimManagementPage() { }, [customers.length]); // 수주 목록 조회 - const fetchSalesOrders = useCallback(async () => { - if (salesOrders.length > 0) return; + const fetchSalesOrders = useCallback(async (force = false) => { + if (!force && salesOrders.length > 0) return; setOrdersLoading(true); try { const res = await apiClient.post("/table-management/tables/sales_order_mng/data", { page: 1, size: 9999, - autoFilter: true, + autoFilter: { enabled: true }, }); - if (res.data?.success && res.data?.data?.rows) { + const rows = res.data?.data?.data || res.data?.data?.rows; + if (res.data?.success && Array.isArray(rows)) { const seen = new Set(); const list: SalesOrderOption[] = []; - for (const row of res.data.data.rows) { + for (const row of rows) { const orderNo = row.order_no || ""; if (!orderNo || seen.has(orderNo)) continue; seen.add(orderNo); @@ -259,6 +263,8 @@ export default function ClaimManagementPage() { }); } setSalesOrders(list); + } else { + console.warn("수주 응답 구조 확인:", JSON.stringify(res.data, null, 2)); } } catch (e) { console.error("수주 목록 조회 실패:", e); @@ -343,6 +349,8 @@ export default function ClaimManagementPage() { processContent: "", }); setIsModalOpen(true); + fetchCustomers(true); + fetchSalesOrders(true); }; const openEditModal = (claimNo: string) => { @@ -351,6 +359,8 @@ export default function ClaimManagementPage() { setIsEditMode(true); setFormData({ ...claim }); setIsModalOpen(true); + fetchCustomers(true); + fetchSalesOrders(true); }; const handleFormChange = (field: keyof Claim, value: string) => { @@ -893,7 +903,7 @@ export default function ClaimManagementPage() { role="combobox" aria-expanded={customerOpen} className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal" - onClick={() => fetchCustomers()} + onClick={() => fetchCustomers(false)} > {formData.customerName || "거래처 선택"} @@ -985,7 +995,7 @@ export default function ClaimManagementPage() { role="combobox" aria-expanded={orderOpen} className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm font-normal" - onClick={() => fetchSalesOrders()} + onClick={() => fetchSalesOrders(false)} > {formData.orderNo || "수주번호 선택"} diff --git a/frontend/app/(main)/sales/shipping-plan/page.tsx b/frontend/app/(main)/sales/shipping-plan/page.tsx index d027b3fc..f04e6908 100644 --- a/frontend/app/(main)/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/sales/shipping-plan/page.tsx @@ -124,6 +124,23 @@ export default function ShippingPlanPage() { const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); + const groupedData = useMemo(() => { + const orderMap = new Map(); + const orderKeys: string[] = []; + data.forEach(plan => { + const key = plan.order_no || `_no_order_${plan.id}`; + if (!orderMap.has(key)) { + orderMap.set(key, []); + orderKeys.push(key); + } + orderMap.get(key)!.push(plan); + }); + return orderKeys.map(key => ({ + orderNo: key, + plans: orderMap.get(key)!, + })); + }, [data]); + const handleRowClick = (plan: ShipmentPlanListItem) => { if (isDetailChanged && selectedId !== plan.id) { if (!confirm("변경사항이 있습니다. 저장하지 않고 이동하시겠습니까?")) return; @@ -298,59 +315,73 @@ export default function ShippingPlanPage() { onCheckedChange={handleCheckAll} /> - No - 상태 - 수주번호 + 수주번호 + 납기일 거래처 품목코드 품목명 수주수량 계획수량 출하계획일 - 납기일 + 상태 - {data.length === 0 ? ( + {groupedData.length === 0 ? ( - + 출하계획이 없습니다. ) : ( - data.map((plan, idx) => ( - handleRowClick(plan)} - > - e.stopPropagation()}> - handleCheck(plan.id, c as boolean)} - disabled={plan.status === "CANCELLED"} - /> - - {idx + 1} - - - {getStatusLabel(plan.status)} - - - {plan.order_no || "-"} - {plan.customer_name || "-"} - {plan.part_code || "-"} - {plan.part_name || "-"} - {formatNumber(plan.order_qty)} - {formatNumber(plan.plan_qty)} - {formatDate(plan.plan_date)} - {formatDate(plan.due_date)} - - )) + groupedData.map((group) => + group.plans.map((plan, planIdx) => ( + handleRowClick(plan)} + > + e.stopPropagation()}> + {planIdx === 0 && ( + checkedIds.includes(p.id))} + onCheckedChange={(c) => { + if (c) { + setCheckedIds(prev => [...new Set([...prev, ...group.plans.filter(p => p.status !== "CANCELLED").map(p => p.id)])]); + } else { + setCheckedIds(prev => prev.filter(id => !group.plans.some(p => p.id === id))); + } + }} + /> + )} + + + {planIdx === 0 ? (plan.order_no || "-") : ""} + + + {planIdx === 0 ? formatDate(plan.due_date) : ""} + + + {planIdx === 0 ? (plan.customer_name || "-") : ""} + + {plan.part_code || "-"} + {plan.part_name || "-"} + {formatNumber(plan.order_qty)} + {formatNumber(plan.plan_qty)} + {formatDate(plan.plan_date)} + + + {getStatusLabel(plan.status)} + + + + )) + ) )} @@ -390,10 +421,6 @@ export default function ShippingPlanPage() {

기본 정보

-
- 출하계획번호 - {selectedPlan.shipment_plan_no || "-"} -
상태 diff --git a/frontend/app/globals.css b/frontend/app/globals.css index f1e6383a..65f7dcac 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -508,6 +508,16 @@ select { font-family: "Gaegu", cursive; } +/* ===== 전체 폰트 사이즈 16px 통일 (버튼 제외) ===== */ +body *:not(button, [role="button"]) { + font-size: 16px !important; +} + +body button *, +body [role="button"] * { + font-size: inherit !important; +} + /* ===== Component-Specific Overrides ===== */ /* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */ /* 예: Calendar, Table 등의 미세 조정 */ diff --git a/frontend/lib/api/receiving.ts b/frontend/lib/api/receiving.ts new file mode 100644 index 00000000..8a27849a --- /dev/null +++ b/frontend/lib/api/receiving.ts @@ -0,0 +1,179 @@ +import { apiClient } from "./client"; + +// --- 타입 정의 --- + +export interface InboundItem { + id: string; + company_code: string; + inbound_number: string; + inbound_type: string; + inbound_date: string; + reference_number: string | null; + supplier_code: string | null; + supplier_name: string | null; + item_number: string | null; + item_name: string | null; + spec: string | null; + material: string | null; + unit: string | null; + inbound_qty: number; + unit_price: number; + total_amount: number; + lot_number: string | null; + warehouse_code: string | null; + warehouse_name?: string | null; + location_code: string | null; + inbound_status: string; + inspection_status: string | null; + inspector: string | null; + manager: string | null; + memo: string | null; + source_table: string | null; + source_id: string | null; + created_date: string; + created_by: string | null; +} + +export interface PurchaseOrderSource { + id: string; + purchase_no: string; + order_date: string; + supplier_code: string; + supplier_name: string; + item_code: string; + item_name: string; + spec: string | null; + material: string | null; + order_qty: number; + received_qty: number; + remain_qty: number; + unit_price: number; + status: string; + due_date: string | null; +} + +export interface ShipmentSource { + detail_id: number; + instruction_id: number; + instruction_no: string; + instruction_date: string; + partner_id: string; + instruction_status: string; + item_code: string; + item_name: string; + spec: string | null; + material: string | null; + ship_qty: number; + order_qty: number; + source_type: string | null; +} + +export interface ItemSource { + id: string; + item_number: string; + item_name: string; + spec: string | null; + material: string | null; + unit: string | null; + standard_price: number; +} + +export interface WarehouseOption { + warehouse_code: string; + warehouse_name: string; + warehouse_type: string; +} + +export interface CreateReceivingPayload { + inbound_number: string; + inbound_date: string; + warehouse_code?: string; + location_code?: string; + inspector?: string; + manager?: string; + memo?: string; + items: Array<{ + inbound_type: string; + reference_number?: string; + supplier_code?: string; + supplier_name?: string; + item_number?: string; + item_name?: string; + spec?: string; + material?: string; + unit?: string; + inbound_qty: number; + unit_price?: number; + total_amount?: number; + lot_number?: string; + warehouse_code?: string; + location_code?: string; + inbound_status?: string; + inspection_status?: string; + inspector?: string; + manager?: string; + memo?: string; + source_table: string; + source_id: string; + }>; +} + +// --- API 호출 --- + +export async function getReceivingList(params?: { + inbound_type?: string; + inbound_status?: string; + search_keyword?: string; + date_from?: string; + date_to?: string; +}) { + const res = await apiClient.get("/receiving/list", { params }); + return res.data as { success: boolean; data: InboundItem[] }; +} + +export async function createReceiving(payload: CreateReceivingPayload) { + const res = await apiClient.post("/receiving", payload); + return res.data as { success: boolean; data: InboundItem[]; message?: string }; +} + +export async function updateReceiving(id: string, payload: Partial) { + const res = await apiClient.put(`/receiving/${id}`, payload); + return res.data as { success: boolean; data: InboundItem }; +} + +export async function deleteReceiving(id: string) { + const res = await apiClient.delete(`/receiving/${id}`); + return res.data as { success: boolean; message?: string }; +} + +export async function generateReceivingNumber() { + const res = await apiClient.get("/receiving/generate-number"); + return res.data as { success: boolean; data: string }; +} + +export async function getReceivingWarehouses() { + const res = await apiClient.get("/receiving/warehouses"); + return res.data as { success: boolean; data: WarehouseOption[] }; +} + +// 소스 데이터 조회 +export async function getPurchaseOrderSources(keyword?: string) { + const res = await apiClient.get("/receiving/source/purchase-orders", { + params: keyword ? { keyword } : {}, + }); + return res.data as { success: boolean; data: PurchaseOrderSource[] }; +} + +export async function getShipmentSources(keyword?: string) { + const res = await apiClient.get("/receiving/source/shipments", { + params: keyword ? { keyword } : {}, + }); + return res.data as { success: boolean; data: ShipmentSource[] }; +} + +export async function getItemSources(keyword?: string) { + const res = await apiClient.get("/receiving/source/items", { + params: keyword ? { keyword } : {}, + }); + return res.data as { success: boolean; data: ItemSource[] }; +}