"use client"; import React, { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { apiClient } from "@/lib/api/client"; import { PopShell } from "../PopShell"; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface Warehouse { id: string; warehouse_code: string; warehouse_name: string; } interface StockItem { id: string; item_code: string; item_name?: string; item_number?: string; warehouse_code: string; warehouse_name?: string; location_code?: string; location_name?: string; floor?: string; current_qty: string; unit?: string; spec?: string; } interface ProcessV2Item { id: string; wo_id: string; seq_no: string; process_name: string; equipment_code: string; good_qty: string; input_qty: string; status: string; target_warehouse_id: string | null; target_location_code: string | null; is_last_process: boolean; is_unstored: boolean; waiting_qty: number; next_input_total: number; item_code: string; item_name: string; work_instruction_no: string; } interface HistoryItem { id: string; item_code: string; warehouse_code: string; location_code?: string; transaction_type: string; transaction_date: string; quantity: string; balance_qty: string; remark: string; writer: string; manager_name?: string; created_date: string; } interface PendingMove { sourceType: "warehouse" | "process"; // warehouse source stock?: StockItem; // process source processItem?: ProcessV2Item; // common toWarehouse: string; toWarehouseName: string; moveQty: number; itemCode: string; itemName: string; } type TabType = "warehouse" | "process"; /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export function InventoryMove() { const router = useRouter(); const [activeTab, setActiveTab] = useState("warehouse"); // 창고 탭 상태 const [warehouses, setWarehouses] = useState([]); const [selectedWarehouse, setSelectedWarehouse] = useState("all"); const [searchKeyword, setSearchKeyword] = useState(""); const [stockItems, setStockItems] = useState([]); const [stockLoading, setStockLoading] = useState(true); // 공정 탭 상태 const [processNames, setProcessNames] = useState([]); const [equipments, setEquipments] = useState([]); const [selectedProcessName, setSelectedProcessName] = useState(""); const [selectedEquipment, setSelectedEquipment] = useState(""); const [processItems, setProcessItems] = useState([]); const [processLoading, setProcessLoading] = useState(false); const [showProcessModal, setShowProcessModal] = useState(false); const [showEquipmentModal, setShowEquipmentModal] = useState(false); // 이동 대기열 const [pendingItems, setPendingItems] = useState([]); // 이력 바텀시트 const [historyTarget, setHistoryTarget] = useState<{ item_code: string; item_name: string } | null>(null); const [historyItems, setHistoryItems] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); // 도착 창고 선택 모달 (warehouse source) const [moveTarget, setMoveTarget] = useState(null); // 도착 창고 선택 모달 (process source) const [processMoveTarget, setProcessMoveTarget] = useState(null); // 수량 입력 모달 const [qtyModalSource, setQtyModalSource] = useState<{ sourceType: "warehouse" | "process"; stock?: StockItem; processItem?: ProcessV2Item; maxQty: number; itemCode: string; itemName: string; } | null>(null); const [qtyModalToWh, setQtyModalToWh] = useState(null); const [qtyInput, setQtyInput] = useState(""); // 토스트 const [toastMsg, setToastMsg] = useState<{ text: string; type: "success" | "error" } | null>(null); const showToast = (text: string, type: "success" | "error" = "success") => { setToastMsg({ text, type }); setTimeout(() => setToastMsg(null), 2500); }; const [submitting, setSubmitting] = useState(false); /* ---- 데이터 조회 ---- */ const fetchWarehouses = useCallback(async () => { try { const res = await apiClient.get("/outbound/warehouses"); setWarehouses(res.data?.data || []); } catch { /* */ } }, []); const fetchStock = useCallback(async () => { setStockLoading(true); try { const params: Record = {}; if (selectedWarehouse !== "all") { params.warehouse_code = selectedWarehouse; } if (searchKeyword) { params.keyword = searchKeyword; } const res = await apiClient.get("/pop/inventory/stock-detail", { params }); const data = res.data?.data ?? []; setStockItems(Array.isArray(data) ? data : []); } catch { setStockItems([]); } finally { setStockLoading(false); } }, [selectedWarehouse, searchKeyword]); const fetchProcessStockV2 = useCallback(async (procName?: string, equipCode?: string) => { setProcessLoading(true); try { const params: Record = {}; if (procName) params.process_name = procName; if (equipCode) params.equipment_code = equipCode; const res = await apiClient.get("/pop/inventory/process-stock-v2", { params }); const data = res.data?.data; if (data?.processNames) setProcessNames(data.processNames); if (data?.equipments) setEquipments(data.equipments); if (data?.processes) setProcessItems(data.processes); else setProcessItems([]); } catch { setProcessItems([]); } finally { setProcessLoading(false); } }, []); const fetchItemHistory = useCallback(async (itemCode: string, itemName: string) => { setHistoryTarget({ item_code: itemCode, item_name: itemName }); setHistoryLoading(true); try { const res = await apiClient.get("/pop/inventory/item-history", { params: { item_code: itemCode }, }); setHistoryItems(res.data?.data ?? []); } catch { setHistoryItems([]); } finally { setHistoryLoading(false); } }, []); useEffect(() => { fetchWarehouses(); }, [fetchWarehouses]); useEffect(() => { fetchStock(); }, [fetchStock]); useEffect(() => { if (activeTab === "process") fetchProcessStockV2(selectedProcessName || undefined, selectedEquipment || undefined); }, [activeTab, selectedProcessName, selectedEquipment, fetchProcessStockV2]); /* ---- 창고 탭 검색 필터 ---- */ const filtered = stockItems.filter((item) => { if (!searchKeyword) return true; const kw = searchKeyword.toLowerCase(); return ( (item.item_code || "").toLowerCase().includes(kw) || (item.item_name || "").toLowerCase().includes(kw) || (item.item_number || "").toLowerCase().includes(kw) ); }); /* ---- 이동 플로우 (창고) ---- */ const handleMoveStart = (stock: StockItem) => { setMoveTarget(stock); setProcessMoveTarget(null); }; /* ---- 이동 플로우 (공정) ---- */ const handleProcessMoveStart = (proc: ProcessV2Item) => { setProcessMoveTarget(proc); setMoveTarget(null); }; /* ---- 도착 창고 선택 ---- */ const handleSelectToWarehouse = (wh: Warehouse) => { if (moveTarget) { // 창고 소스 const maxQty = parseFloat(moveTarget.current_qty || "0"); setQtyModalSource({ sourceType: "warehouse", stock: moveTarget, maxQty, itemCode: moveTarget.item_code, itemName: moveTarget.item_name || moveTarget.item_code, }); setQtyModalToWh(wh); setQtyInput(String(maxQty)); setMoveTarget(null); } else if (processMoveTarget) { // 공정 소스 const goodQty = parseFloat(processMoveTarget.good_qty || "0"); setQtyModalSource({ sourceType: "process", processItem: processMoveTarget, maxQty: goodQty, itemCode: processMoveTarget.item_code, itemName: processMoveTarget.item_name || processMoveTarget.item_code, }); setQtyModalToWh(wh); setQtyInput(String(goodQty)); setProcessMoveTarget(null); } }; // 수량 입력 (숫자키패드) const handleNumpadPress = (key: string) => { if (key === "C") { setQtyInput(""); } else if (key === "BS") { setQtyInput((prev) => prev.slice(0, -1)); } else if (key === ".") { if (!qtyInput.includes(".")) { setQtyInput((prev) => prev + "."); } } else { setQtyInput((prev) => prev + key); } }; // 대기열에 추가 const handleAddToQueue = () => { if (!qtyModalSource || !qtyModalToWh) return; const qty = parseFloat(qtyInput); if (!qty || qty <= 0) { showToast("수량을 입력하세요", "error"); return; } if (qty > qtyModalSource.maxQty) { showToast(`최대 수량은 ${qtyModalSource.maxQty.toLocaleString()}입니다`, "error"); return; } // 중복 체크 if (qtyModalSource.sourceType === "warehouse" && qtyModalSource.stock) { const dup = pendingItems.find( (p) => p.sourceType === "warehouse" && p.stock?.id === qtyModalSource.stock?.id && p.toWarehouse === qtyModalToWh.warehouse_code ); if (dup) { showToast("이미 대기열에 있는 항목입니다", "error"); setQtyModalSource(null); setQtyModalToWh(null); return; } } if (qtyModalSource.sourceType === "process" && qtyModalSource.processItem) { const dup = pendingItems.find( (p) => p.sourceType === "process" && p.processItem?.id === qtyModalSource.processItem?.id ); if (dup) { showToast("이미 대기열에 있는 항목입니다", "error"); setQtyModalSource(null); setQtyModalToWh(null); return; } } setPendingItems((prev) => [ ...prev, { sourceType: qtyModalSource.sourceType, stock: qtyModalSource.stock, processItem: qtyModalSource.processItem, toWarehouse: qtyModalToWh.warehouse_code, toWarehouseName: qtyModalToWh.warehouse_name, moveQty: qty, itemCode: qtyModalSource.itemCode, itemName: qtyModalSource.itemName, }, ]); setQtyModalSource(null); setQtyModalToWh(null); showToast("대기열에 추가됨"); }; // 대기열에서 제거 const removePending = (index: number) => { setPendingItems((prev) => prev.filter((_, i) => i !== index)); }; /* ---- 이동 확정 ---- */ const handleConfirmMove = async () => { if (pendingItems.length === 0) return; setSubmitting(true); try { const res = await apiClient.post("/pop/inventory/move-batch", { items: pendingItems.map((p) => { if (p.sourceType === "process" && p.processItem) { return { source_type: "process", work_order_process_id: p.processItem.id, item_code: p.itemCode, from_warehouse: "", to_warehouse: p.toWarehouse, to_location: "", quantity: p.moveQty, }; } return { source_type: "warehouse", item_code: p.itemCode, from_warehouse: p.stock?.warehouse_code || "", from_location: p.stock?.location_code || "", to_warehouse: p.toWarehouse, to_location: "", quantity: p.moveQty, stock_id: p.stock?.id || "", }; }), }); if (res.data?.success) { showToast(res.data.message || "이동 완료"); setPendingItems([]); fetchStock(); if (activeTab === "process") fetchProcessStockV2(selectedProcessName || undefined, selectedEquipment || undefined); } else { showToast(res.data?.message || "이동 실패", "error"); } } catch (err: unknown) { const e = err as { response?: { data?: { message?: string } }; message?: string }; showToast(e?.response?.data?.message || e?.message || "오류 발생", "error"); } finally { setSubmitting(false); } }; /* ---- 이력 트랜잭션 타입 아이콘/색상 ---- */ const txTypeStyle = (type: string) => { if (type?.includes("입고") || type?.includes("공정입고")) return { bg: "bg-blue-100", text: "text-blue-700", icon: "+" }; if (type?.includes("출고")) return { bg: "bg-green-100", text: "text-green-700", icon: "-" }; if (type?.includes("조정")) return { bg: "bg-amber-100", text: "text-amber-700", icon: "~" }; if (type?.includes("공정") || type?.includes("이동")) return { bg: "bg-purple-100", text: "text-purple-700", icon: "M" }; return { bg: "bg-gray-100", text: "text-gray-700", icon: "?" }; }; // 현재 활성 모달의 소스 창고 (도착 선택 시 필터용) const activeMoveSourceWarehouse = moveTarget?.warehouse_code || ""; /* ------------------------------------------------------------------ */ /* Render */ /* ------------------------------------------------------------------ */ return (
{/* ===== 왼쪽: 재고 현황 ===== */}
{/* 탭: 창고 / 공정 */}
{/* 탭 내용 */}
{activeTab === "warehouse" ? ( /* ---- 창고 탭 ---- */ <> {/* 창고 탭 버튼 + 검색 (InventoryTransfer 패턴) */}
{warehouses.map((wh) => ( ))}
setSearchKeyword(e.target.value)} className="flex-1 px-4 py-3.5 rounded-xl border border-gray-200 text-lg focus:outline-none focus:border-blue-400 bg-white" />
{/* 품목 리스트 (flat divide-y) */}
{stockLoading ? (
) : filtered.length === 0 ? (

재고 데이터가 없습니다

) : (
{filtered.map((item) => { const qty = parseFloat(item.current_qty || "0"); const isPending = pendingItems.some( (p) => p.sourceType === "warehouse" && p.stock?.id === item.id ); return (
{/* 품목 정보 - 터치하면 이력 */} {/* 수량 */}

{qty.toLocaleString()}

{item.unit || "EA"}

{/* 이동 버튼 */}
); })}
)}
) : ( /* ---- 공정 탭 ---- */ <> {/* 공정/설비 필터 */}
{/* 공정 선택 버튼 */} {/* 설비 선택 버튼 */}
{/* 품목 기준 리스트 */}
{processLoading ? (
) : processItems.length === 0 ? (

{processNames.length === 0 ? "양품이 있는 공정이 없습니다" : "해당 조건의 공정 데이터가 없습니다"}

) : (
{processItems.map((proc) => { const goodQty = parseFloat(proc.good_qty || "0"); const isPending = pendingItems.some( (p) => p.sourceType === "process" && p.processItem?.id === proc.id ); const isStored = !!proc.target_warehouse_id; return (
{/* 품목 정보 */} {/* 양품 수량 */}

{goodQty.toLocaleString()}

양품

{/* 이동 버튼 */} {proc.is_unstored && !isPending && !isStored ? ( ) : isPending ? ( 대기중 ) : isStored ? (
) : null}
); })}
)}
)}
{/* ===== 오른쪽: 이동 대기열 ===== */}
{/* 헤더 */}

이동 대기

{pendingItems.length}건
{/* 대기열 리스트 */}
{pendingItems.length === 0 ? (

이동 대기열이 비었습니다

왼쪽에서 품목의 화살표를 터치하세요

) : (
{pendingItems.map((p, idx) => { const isProcess = p.sourceType === "process"; return (

{p.itemName}

{isProcess ? `공정 (${p.processItem?.process_name || ""})` : (p.stock?.warehouse_name || p.stock?.warehouse_code || "")} {p.toWarehouseName}

{p.moveQty.toLocaleString()} EA

); })}
)}
{/* 하단 고정 버튼 */}
{/* ===== 도착 창고 선택 모달 ===== */} {(moveTarget || processMoveTarget) && (
{ setMoveTarget(null); setProcessMoveTarget(null); }} >
e.stopPropagation()} > {/* 헤더 */}

도착 창고 선택

{moveTarget ? `${moveTarget.item_name || moveTarget.item_code} (${parseFloat(moveTarget.current_qty || "0").toLocaleString()} ${moveTarget.unit || "EA"})` : processMoveTarget ? `${processMoveTarget.item_name || processMoveTarget.item_code} (양품 ${parseFloat(processMoveTarget.good_qty || "0").toLocaleString()})` : "" }

{/* 창고 카드 리스트 */}
{warehouses .filter((wh) => wh.warehouse_code !== activeMoveSourceWarehouse) .map((wh) => ( ))}
)} {/* ===== 수량 입력 모달 (숫자키패드) ===== */} {qtyModalSource && qtyModalToWh && (
{ setQtyModalSource(null); setQtyModalToWh(null); }} >
e.stopPropagation()} > {/* 헤더 */}

{qtyModalSource.sourceType === "process" ? "입고 수량 입력" : "이동 수량 입력"}

{qtyModalSource.itemName}

{qtyModalSource.sourceType === "process" ? `공정` : (qtyModalSource.stock?.warehouse_name || qtyModalSource.stock?.warehouse_code || "") } {qtyModalToWh.warehouse_name}
{/* 수량 표시 */}

최대: {qtyModalSource.maxQty.toLocaleString()} EA

{qtyInput || "0"}

{/* 숫자 키패드 */}
{["1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "BS"].map((key) => ( ))}
{/* 하단 버튼 */}
)} {/* ===== 이력 바텀시트 ===== */} {historyTarget && (
setHistoryTarget(null)} >
e.stopPropagation()} style={{ maxHeight: "70vh" }} > {/* 헤더 */}

재고 이력

{historyTarget.item_name} ({historyTarget.item_code})

{/* 이력 리스트 */}
{historyLoading ? (
) : historyItems.length === 0 ? (

이력이 없습니다

) : (
{historyItems.map((h) => { const style = txTypeStyle(h.transaction_type); const qty = parseFloat(h.quantity || "0"); return (
{style.icon}
{h.transaction_type || "-"} {h.warehouse_code}{h.location_code ? ` / ${h.location_code}` : ""}

{h.transaction_date ? new Date(h.transaction_date).toLocaleDateString("ko-KR") : ""} {h.manager_name ? ` / ${h.manager_name}` : ""}

= 0 ? "text-blue-600" : "text-red-600"}`}> {qty >= 0 ? "+" : ""}{qty.toLocaleString()}

{h.balance_qty && (

잔량 {parseFloat(h.balance_qty || "0").toLocaleString()}

)}
); })}
)}
)}
{/* ===== 공정 선택 바텀시트 ===== */} {showProcessModal && (
setShowProcessModal(false)} >
e.stopPropagation()} >

공정 선택

{processNames.map((name) => ( ))}
)} {/* ===== 설비 선택 바텀시트 ===== */} {showEquipmentModal && (
setShowEquipmentModal(false)} >
e.stopPropagation()} >

설비 선택

{equipments.map((eq) => ( ))} {equipments.length === 0 && (

등록된 설비가 없습니다

)}
)} {/* 토스트 메시지 */} {toastMsg && (
{toastMsg.text}
)} ); }