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[] }; +}