From f54d763373c8eb48678e202766f3008f32d77aab Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 13 Apr 2026 18:31:35 +0900 Subject: [PATCH] refactor: Update packaging page to enhance data handling and user experience - Changed column key for maximum weight from `max_weight` to `max_load_kg` for clarity. - Added a new status label `UNREGISTERED` to improve item status tracking. - Enhanced data loading logic to merge item information with packaging units, ensuring accurate representation of available items. - Removed the registration button, allowing users to click on unregistered items to open the registration modal directly. - These changes aim to streamline the packaging management process and improve usability across multiple company implementations. --- .../COMPANY_10/logistics/packaging/page.tsx | 235 ++++++++++-------- .../COMPANY_16/logistics/packaging/page.tsx | 235 ++++++++++-------- .../COMPANY_29/logistics/packaging/page.tsx | 235 ++++++++++-------- .../COMPANY_30/logistics/packaging/page.tsx | 235 ++++++++++-------- .../COMPANY_8/logistics/packaging/page.tsx | 235 ++++++++++-------- .../COMPANY_9/logistics/packaging/page.tsx | 235 ++++++++++-------- 6 files changed, 822 insertions(+), 588 deletions(-) diff --git a/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx index 6ae340aa..942002e3 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx @@ -37,7 +37,7 @@ const GRID_COLUMNS = [ { key: "pkg_name", label: "포장명" }, { key: "pkg_type", label: "유형" }, { key: "size", label: "크기(mm)" }, - { key: "max_weight", label: "최대중량" }, + { key: "max_load_kg", label: "최대중량" }, { key: "status", label: "상태" }, ]; @@ -51,7 +51,7 @@ const LOADING_TYPE_LABEL: Record = { ALU_PALLET: "알루미늄파렛트", CONTAINER: "컨테이너", STEEL_BOX: "철재함", CAGE: "케이지", ETC: "기타", }; -const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용" }; +const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용", UNREGISTERED: "미등록" }; const getStatusColor = (s: string) => s === "ACTIVE" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"; const fmtSize = (w: any, l: any, h: any) => { @@ -118,20 +118,80 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); - // --- 데이터 로드 --- + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); try { - const res = await getPkgUnits(); - if (res.success) setPkgUnits(res.data); + const [itemRes, pkgRes] = await Promise.all([ + getItemsByDivision("포장재"), + getPkgUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = pkgRes.success ? pkgRes.data : []; + // item_info 기준으로 병합: item_number 매칭되는 pkg_unit이 있으면 그 데이터 사용, 없으면 미등록 placeholder + const merged: PkgUnit[] = items.map((item) => { + const match = existing.find((p) => p.item_number === item.item_number || p.pkg_code === item.item_number); + if (match) return { ...match, pkg_code: match.pkg_code || item.item_number, pkg_name: match.pkg_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + pkg_code: item.item_number, + pkg_name: item.item_name, + pkg_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + volume_l: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as PkgUnit; + }); + // 품목정보에 없지만 pkg_unit에만 있는 항목 (고아) 추가 + const orphans = existing.filter((p) => !items.some((i) => i.item_number === p.item_number || i.item_number === p.pkg_code)); + setPkgUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setPkgLoading(false); } }, []); const fetchLoadingUnits = useCallback(async () => { setLoadingLoading(true); try { - const res = await getLoadingUnits(); - if (res.success) setLoadingUnits(res.data); + const [itemRes, luRes] = await Promise.all([ + getItemsByDivision("적재함"), + getLoadingUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = luRes.success ? luRes.data : []; + const merged: LoadingUnit[] = items.map((item) => { + const match = existing.find((l) => l.item_number === item.item_number || l.loading_code === item.item_number); + if (match) return { ...match, loading_code: match.loading_code || item.item_number, loading_name: match.loading_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + loading_code: item.item_number, + loading_name: item.item_name, + loading_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + max_stack: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as LoadingUnit; + }); + const orphans = existing.filter((l) => !items.some((i) => i.item_number === l.item_number || i.item_number === l.loading_code)); + setLoadingUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setLoadingLoading(false); } }, []); @@ -177,12 +237,21 @@ export default function PackagingPage() { }); // --- 포장재 등록/수정 모달 --- - const openPkgModal = async (mode: "create" | "edit") => { - setPkgModalMode(mode); - if (mode === "edit" && selectedPkg) { - setPkgForm({ ...selectedPkg }); + // row: 리스트에서 클릭한 행. id가 빈 문자열이면 미등록 상태(item_info만 있음) → 등록 모달 + const openPkgModal = async (mode: "create" | "edit", row?: PkgUnit) => { + const target = row || selectedPkg; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setPkgModalMode(actualMode); + if (target) { + // 기존 데이터 or 품목정보 기반 초기값 세팅 + setPkgForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.pkg_code || "", + }); } else { - setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "" }); + setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "", item_number: "" }); } setPkgItemPopoverOpen(false); try { @@ -232,12 +301,19 @@ export default function PackagingPage() { }; // --- 적재함 등록/수정 모달 --- - const openLoadModal = async (mode: "create" | "edit") => { - setLoadModalMode(mode); - if (mode === "edit" && selectedLoading) { - setLoadForm({ ...selectedLoading }); + const openLoadModal = async (mode: "create" | "edit", row?: LoadingUnit) => { + const target = row || selectedLoading; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setLoadModalMode(actualMode); + if (target) { + setLoadForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.loading_code || "", + }); } else { - setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "" }); + setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "", item_number: "" }); } setLoadItemPopoverOpen(false); try { @@ -442,12 +518,8 @@ export default function PackagingPage() {
)} - + {/* 메인 리스트가 품목정보 기반으로 자동 구성되므로 등록 버튼 제거. + 미등록 상태의 품목을 직접 클릭하면 등록 모달이 열림. */} @@ -466,22 +538,30 @@ export default function PackagingPage() { size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - status: { width: "w-[60px]", align: "center", render: (v: any) => ( - - {STATUS_LABEL[v] || v} - - )}, + status: { width: "w-[60px]", align: "center", render: (v: any, row: any) => { + const isUnreg = !row.id; + const cls = isUnreg ? "bg-warning/10 text-warning" : getStatusColor(v); + const label = isUnreg ? "미등록" : (STATUS_LABEL[v] || v); + return {label}; + }}, }; return { key: col.key, label: col.label, ...renderMap[col.key] }; })} data={ts.groupData(filteredPkgUnits)} - rowKey={(row) => String(row.id)} + rowKey={(row) => row.id ? `reg-${row.id}` : `new-${row.pkg_code || row.item_number}`} loading={pkgLoading} - emptyMessage="등록된 포장재가 없어요" - selectedId={selectedPkg ? String(selectedPkg.id) : null} + emptyMessage="포장재 품목이 없어요. 품목정보에서 관리품목을 '포장재'로 등록해주세요." + selectedId={selectedPkg ? (selectedPkg.id ? `reg-${selectedPkg.id}` : `new-${selectedPkg.pkg_code || selectedPkg.item_number}`) : null} onSelect={(id) => { - const pkg = filteredPkgUnits.find((p) => String(p.id) === id); - if (pkg) selectPkg(pkg); + const pkg = filteredPkgUnits.find((p) => (p.id ? `reg-${p.id}` : `new-${p.pkg_code || p.item_number}`) === id); + if (pkg) { + if (!pkg.id) { + // 미등록 → 바로 등록 모달 + openPkgModal("create", pkg); + } else { + selectPkg(pkg); + } + } }} showPagination={false} draggableColumns @@ -592,14 +672,24 @@ export default function PackagingPage() {
- ) : filteredLoadingUnits.map((l) => ( + ) : filteredLoadingUnits.map((l) => { + const isUnreg = !l.id; + const rowKey = l.id ? `reg-${l.id}` : `new-${l.loading_code || l.item_number}`; + const selKey = selectedLoading ? (selectedLoading.id ? `reg-${selectedLoading.id}` : `new-${selectedLoading.loading_code || selectedLoading.item_number}`) : null; + return ( selectLoading(l)} + onClick={() => { + if (isUnreg) { + openLoadModal("create", l); + } else { + selectLoading(l); + } + }} > {l.loading_code} {l.loading_name} @@ -607,12 +697,13 @@ export default function PackagingPage() { {fmtSize(l.width_mm, l.length_mm, l.height_mm)} {Number(l.max_load_kg || 0) > 0 ? `${l.max_load_kg}kg` : "-"} - - {STATUS_LABEL[l.status] || l.status} + + {isUnreg ? "미등록" : (STATUS_LABEL[l.status] || l.status)} - ))} + ); + })} @@ -699,35 +790,9 @@ export default function PackagingPage() { {pkgModalMode === "create" && (
- - - - - - { - const item = pkgItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {pkgItemOptions.map((item) => ( - onPkgItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {pkgForm.pkg_code ? `${pkgForm.pkg_name} (${pkgForm.pkg_code})` : "-"} +
)}
@@ -781,35 +846,9 @@ export default function PackagingPage() { {loadModalMode === "create" && (
- - - - - - { - const item = loadItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {loadItemOptions.map((item) => ( - onLoadItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {loadForm.loading_code ? `${loadForm.loading_name} (${loadForm.loading_code})` : "-"} +
)}
diff --git a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx index 6ae340aa..942002e3 100644 --- a/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_16/logistics/packaging/page.tsx @@ -37,7 +37,7 @@ const GRID_COLUMNS = [ { key: "pkg_name", label: "포장명" }, { key: "pkg_type", label: "유형" }, { key: "size", label: "크기(mm)" }, - { key: "max_weight", label: "최대중량" }, + { key: "max_load_kg", label: "최대중량" }, { key: "status", label: "상태" }, ]; @@ -51,7 +51,7 @@ const LOADING_TYPE_LABEL: Record = { ALU_PALLET: "알루미늄파렛트", CONTAINER: "컨테이너", STEEL_BOX: "철재함", CAGE: "케이지", ETC: "기타", }; -const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용" }; +const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용", UNREGISTERED: "미등록" }; const getStatusColor = (s: string) => s === "ACTIVE" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"; const fmtSize = (w: any, l: any, h: any) => { @@ -118,20 +118,80 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); - // --- 데이터 로드 --- + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); try { - const res = await getPkgUnits(); - if (res.success) setPkgUnits(res.data); + const [itemRes, pkgRes] = await Promise.all([ + getItemsByDivision("포장재"), + getPkgUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = pkgRes.success ? pkgRes.data : []; + // item_info 기준으로 병합: item_number 매칭되는 pkg_unit이 있으면 그 데이터 사용, 없으면 미등록 placeholder + const merged: PkgUnit[] = items.map((item) => { + const match = existing.find((p) => p.item_number === item.item_number || p.pkg_code === item.item_number); + if (match) return { ...match, pkg_code: match.pkg_code || item.item_number, pkg_name: match.pkg_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + pkg_code: item.item_number, + pkg_name: item.item_name, + pkg_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + volume_l: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as PkgUnit; + }); + // 품목정보에 없지만 pkg_unit에만 있는 항목 (고아) 추가 + const orphans = existing.filter((p) => !items.some((i) => i.item_number === p.item_number || i.item_number === p.pkg_code)); + setPkgUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setPkgLoading(false); } }, []); const fetchLoadingUnits = useCallback(async () => { setLoadingLoading(true); try { - const res = await getLoadingUnits(); - if (res.success) setLoadingUnits(res.data); + const [itemRes, luRes] = await Promise.all([ + getItemsByDivision("적재함"), + getLoadingUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = luRes.success ? luRes.data : []; + const merged: LoadingUnit[] = items.map((item) => { + const match = existing.find((l) => l.item_number === item.item_number || l.loading_code === item.item_number); + if (match) return { ...match, loading_code: match.loading_code || item.item_number, loading_name: match.loading_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + loading_code: item.item_number, + loading_name: item.item_name, + loading_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + max_stack: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as LoadingUnit; + }); + const orphans = existing.filter((l) => !items.some((i) => i.item_number === l.item_number || i.item_number === l.loading_code)); + setLoadingUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setLoadingLoading(false); } }, []); @@ -177,12 +237,21 @@ export default function PackagingPage() { }); // --- 포장재 등록/수정 모달 --- - const openPkgModal = async (mode: "create" | "edit") => { - setPkgModalMode(mode); - if (mode === "edit" && selectedPkg) { - setPkgForm({ ...selectedPkg }); + // row: 리스트에서 클릭한 행. id가 빈 문자열이면 미등록 상태(item_info만 있음) → 등록 모달 + const openPkgModal = async (mode: "create" | "edit", row?: PkgUnit) => { + const target = row || selectedPkg; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setPkgModalMode(actualMode); + if (target) { + // 기존 데이터 or 품목정보 기반 초기값 세팅 + setPkgForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.pkg_code || "", + }); } else { - setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "" }); + setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "", item_number: "" }); } setPkgItemPopoverOpen(false); try { @@ -232,12 +301,19 @@ export default function PackagingPage() { }; // --- 적재함 등록/수정 모달 --- - const openLoadModal = async (mode: "create" | "edit") => { - setLoadModalMode(mode); - if (mode === "edit" && selectedLoading) { - setLoadForm({ ...selectedLoading }); + const openLoadModal = async (mode: "create" | "edit", row?: LoadingUnit) => { + const target = row || selectedLoading; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setLoadModalMode(actualMode); + if (target) { + setLoadForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.loading_code || "", + }); } else { - setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "" }); + setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "", item_number: "" }); } setLoadItemPopoverOpen(false); try { @@ -442,12 +518,8 @@ export default function PackagingPage() {
)} - + {/* 메인 리스트가 품목정보 기반으로 자동 구성되므로 등록 버튼 제거. + 미등록 상태의 품목을 직접 클릭하면 등록 모달이 열림. */} @@ -466,22 +538,30 @@ export default function PackagingPage() { size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - status: { width: "w-[60px]", align: "center", render: (v: any) => ( - - {STATUS_LABEL[v] || v} - - )}, + status: { width: "w-[60px]", align: "center", render: (v: any, row: any) => { + const isUnreg = !row.id; + const cls = isUnreg ? "bg-warning/10 text-warning" : getStatusColor(v); + const label = isUnreg ? "미등록" : (STATUS_LABEL[v] || v); + return {label}; + }}, }; return { key: col.key, label: col.label, ...renderMap[col.key] }; })} data={ts.groupData(filteredPkgUnits)} - rowKey={(row) => String(row.id)} + rowKey={(row) => row.id ? `reg-${row.id}` : `new-${row.pkg_code || row.item_number}`} loading={pkgLoading} - emptyMessage="등록된 포장재가 없어요" - selectedId={selectedPkg ? String(selectedPkg.id) : null} + emptyMessage="포장재 품목이 없어요. 품목정보에서 관리품목을 '포장재'로 등록해주세요." + selectedId={selectedPkg ? (selectedPkg.id ? `reg-${selectedPkg.id}` : `new-${selectedPkg.pkg_code || selectedPkg.item_number}`) : null} onSelect={(id) => { - const pkg = filteredPkgUnits.find((p) => String(p.id) === id); - if (pkg) selectPkg(pkg); + const pkg = filteredPkgUnits.find((p) => (p.id ? `reg-${p.id}` : `new-${p.pkg_code || p.item_number}`) === id); + if (pkg) { + if (!pkg.id) { + // 미등록 → 바로 등록 모달 + openPkgModal("create", pkg); + } else { + selectPkg(pkg); + } + } }} showPagination={false} draggableColumns @@ -592,14 +672,24 @@ export default function PackagingPage() {
- ) : filteredLoadingUnits.map((l) => ( + ) : filteredLoadingUnits.map((l) => { + const isUnreg = !l.id; + const rowKey = l.id ? `reg-${l.id}` : `new-${l.loading_code || l.item_number}`; + const selKey = selectedLoading ? (selectedLoading.id ? `reg-${selectedLoading.id}` : `new-${selectedLoading.loading_code || selectedLoading.item_number}`) : null; + return ( selectLoading(l)} + onClick={() => { + if (isUnreg) { + openLoadModal("create", l); + } else { + selectLoading(l); + } + }} > {l.loading_code} {l.loading_name} @@ -607,12 +697,13 @@ export default function PackagingPage() { {fmtSize(l.width_mm, l.length_mm, l.height_mm)} {Number(l.max_load_kg || 0) > 0 ? `${l.max_load_kg}kg` : "-"} - - {STATUS_LABEL[l.status] || l.status} + + {isUnreg ? "미등록" : (STATUS_LABEL[l.status] || l.status)} - ))} + ); + })}
@@ -699,35 +790,9 @@ export default function PackagingPage() { {pkgModalMode === "create" && (
- - - - - - { - const item = pkgItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {pkgItemOptions.map((item) => ( - onPkgItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {pkgForm.pkg_code ? `${pkgForm.pkg_name} (${pkgForm.pkg_code})` : "-"} +
)}
@@ -781,35 +846,9 @@ export default function PackagingPage() { {loadModalMode === "create" && (
- - - - - - { - const item = loadItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {loadItemOptions.map((item) => ( - onLoadItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {loadForm.loading_code ? `${loadForm.loading_name} (${loadForm.loading_code})` : "-"} +
)}
diff --git a/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx index 6ae340aa..942002e3 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/packaging/page.tsx @@ -37,7 +37,7 @@ const GRID_COLUMNS = [ { key: "pkg_name", label: "포장명" }, { key: "pkg_type", label: "유형" }, { key: "size", label: "크기(mm)" }, - { key: "max_weight", label: "최대중량" }, + { key: "max_load_kg", label: "최대중량" }, { key: "status", label: "상태" }, ]; @@ -51,7 +51,7 @@ const LOADING_TYPE_LABEL: Record = { ALU_PALLET: "알루미늄파렛트", CONTAINER: "컨테이너", STEEL_BOX: "철재함", CAGE: "케이지", ETC: "기타", }; -const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용" }; +const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용", UNREGISTERED: "미등록" }; const getStatusColor = (s: string) => s === "ACTIVE" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"; const fmtSize = (w: any, l: any, h: any) => { @@ -118,20 +118,80 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); - // --- 데이터 로드 --- + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); try { - const res = await getPkgUnits(); - if (res.success) setPkgUnits(res.data); + const [itemRes, pkgRes] = await Promise.all([ + getItemsByDivision("포장재"), + getPkgUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = pkgRes.success ? pkgRes.data : []; + // item_info 기준으로 병합: item_number 매칭되는 pkg_unit이 있으면 그 데이터 사용, 없으면 미등록 placeholder + const merged: PkgUnit[] = items.map((item) => { + const match = existing.find((p) => p.item_number === item.item_number || p.pkg_code === item.item_number); + if (match) return { ...match, pkg_code: match.pkg_code || item.item_number, pkg_name: match.pkg_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + pkg_code: item.item_number, + pkg_name: item.item_name, + pkg_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + volume_l: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as PkgUnit; + }); + // 품목정보에 없지만 pkg_unit에만 있는 항목 (고아) 추가 + const orphans = existing.filter((p) => !items.some((i) => i.item_number === p.item_number || i.item_number === p.pkg_code)); + setPkgUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setPkgLoading(false); } }, []); const fetchLoadingUnits = useCallback(async () => { setLoadingLoading(true); try { - const res = await getLoadingUnits(); - if (res.success) setLoadingUnits(res.data); + const [itemRes, luRes] = await Promise.all([ + getItemsByDivision("적재함"), + getLoadingUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = luRes.success ? luRes.data : []; + const merged: LoadingUnit[] = items.map((item) => { + const match = existing.find((l) => l.item_number === item.item_number || l.loading_code === item.item_number); + if (match) return { ...match, loading_code: match.loading_code || item.item_number, loading_name: match.loading_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + loading_code: item.item_number, + loading_name: item.item_name, + loading_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + max_stack: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as LoadingUnit; + }); + const orphans = existing.filter((l) => !items.some((i) => i.item_number === l.item_number || i.item_number === l.loading_code)); + setLoadingUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setLoadingLoading(false); } }, []); @@ -177,12 +237,21 @@ export default function PackagingPage() { }); // --- 포장재 등록/수정 모달 --- - const openPkgModal = async (mode: "create" | "edit") => { - setPkgModalMode(mode); - if (mode === "edit" && selectedPkg) { - setPkgForm({ ...selectedPkg }); + // row: 리스트에서 클릭한 행. id가 빈 문자열이면 미등록 상태(item_info만 있음) → 등록 모달 + const openPkgModal = async (mode: "create" | "edit", row?: PkgUnit) => { + const target = row || selectedPkg; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setPkgModalMode(actualMode); + if (target) { + // 기존 데이터 or 품목정보 기반 초기값 세팅 + setPkgForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.pkg_code || "", + }); } else { - setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "" }); + setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "", item_number: "" }); } setPkgItemPopoverOpen(false); try { @@ -232,12 +301,19 @@ export default function PackagingPage() { }; // --- 적재함 등록/수정 모달 --- - const openLoadModal = async (mode: "create" | "edit") => { - setLoadModalMode(mode); - if (mode === "edit" && selectedLoading) { - setLoadForm({ ...selectedLoading }); + const openLoadModal = async (mode: "create" | "edit", row?: LoadingUnit) => { + const target = row || selectedLoading; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setLoadModalMode(actualMode); + if (target) { + setLoadForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.loading_code || "", + }); } else { - setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "" }); + setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "", item_number: "" }); } setLoadItemPopoverOpen(false); try { @@ -442,12 +518,8 @@ export default function PackagingPage() {
)} - + {/* 메인 리스트가 품목정보 기반으로 자동 구성되므로 등록 버튼 제거. + 미등록 상태의 품목을 직접 클릭하면 등록 모달이 열림. */} @@ -466,22 +538,30 @@ export default function PackagingPage() { size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - status: { width: "w-[60px]", align: "center", render: (v: any) => ( - - {STATUS_LABEL[v] || v} - - )}, + status: { width: "w-[60px]", align: "center", render: (v: any, row: any) => { + const isUnreg = !row.id; + const cls = isUnreg ? "bg-warning/10 text-warning" : getStatusColor(v); + const label = isUnreg ? "미등록" : (STATUS_LABEL[v] || v); + return {label}; + }}, }; return { key: col.key, label: col.label, ...renderMap[col.key] }; })} data={ts.groupData(filteredPkgUnits)} - rowKey={(row) => String(row.id)} + rowKey={(row) => row.id ? `reg-${row.id}` : `new-${row.pkg_code || row.item_number}`} loading={pkgLoading} - emptyMessage="등록된 포장재가 없어요" - selectedId={selectedPkg ? String(selectedPkg.id) : null} + emptyMessage="포장재 품목이 없어요. 품목정보에서 관리품목을 '포장재'로 등록해주세요." + selectedId={selectedPkg ? (selectedPkg.id ? `reg-${selectedPkg.id}` : `new-${selectedPkg.pkg_code || selectedPkg.item_number}`) : null} onSelect={(id) => { - const pkg = filteredPkgUnits.find((p) => String(p.id) === id); - if (pkg) selectPkg(pkg); + const pkg = filteredPkgUnits.find((p) => (p.id ? `reg-${p.id}` : `new-${p.pkg_code || p.item_number}`) === id); + if (pkg) { + if (!pkg.id) { + // 미등록 → 바로 등록 모달 + openPkgModal("create", pkg); + } else { + selectPkg(pkg); + } + } }} showPagination={false} draggableColumns @@ -592,14 +672,24 @@ export default function PackagingPage() {
- ) : filteredLoadingUnits.map((l) => ( + ) : filteredLoadingUnits.map((l) => { + const isUnreg = !l.id; + const rowKey = l.id ? `reg-${l.id}` : `new-${l.loading_code || l.item_number}`; + const selKey = selectedLoading ? (selectedLoading.id ? `reg-${selectedLoading.id}` : `new-${selectedLoading.loading_code || selectedLoading.item_number}`) : null; + return ( selectLoading(l)} + onClick={() => { + if (isUnreg) { + openLoadModal("create", l); + } else { + selectLoading(l); + } + }} > {l.loading_code} {l.loading_name} @@ -607,12 +697,13 @@ export default function PackagingPage() { {fmtSize(l.width_mm, l.length_mm, l.height_mm)} {Number(l.max_load_kg || 0) > 0 ? `${l.max_load_kg}kg` : "-"} - - {STATUS_LABEL[l.status] || l.status} + + {isUnreg ? "미등록" : (STATUS_LABEL[l.status] || l.status)} - ))} + ); + })}
@@ -699,35 +790,9 @@ export default function PackagingPage() { {pkgModalMode === "create" && (
- - - - - - { - const item = pkgItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {pkgItemOptions.map((item) => ( - onPkgItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {pkgForm.pkg_code ? `${pkgForm.pkg_name} (${pkgForm.pkg_code})` : "-"} +
)}
@@ -781,35 +846,9 @@ export default function PackagingPage() { {loadModalMode === "create" && (
- - - - - - { - const item = loadItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {loadItemOptions.map((item) => ( - onLoadItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {loadForm.loading_code ? `${loadForm.loading_name} (${loadForm.loading_code})` : "-"} +
)}
diff --git a/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx index 6ae340aa..942002e3 100644 --- a/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_30/logistics/packaging/page.tsx @@ -37,7 +37,7 @@ const GRID_COLUMNS = [ { key: "pkg_name", label: "포장명" }, { key: "pkg_type", label: "유형" }, { key: "size", label: "크기(mm)" }, - { key: "max_weight", label: "최대중량" }, + { key: "max_load_kg", label: "최대중량" }, { key: "status", label: "상태" }, ]; @@ -51,7 +51,7 @@ const LOADING_TYPE_LABEL: Record = { ALU_PALLET: "알루미늄파렛트", CONTAINER: "컨테이너", STEEL_BOX: "철재함", CAGE: "케이지", ETC: "기타", }; -const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용" }; +const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용", UNREGISTERED: "미등록" }; const getStatusColor = (s: string) => s === "ACTIVE" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"; const fmtSize = (w: any, l: any, h: any) => { @@ -118,20 +118,80 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); - // --- 데이터 로드 --- + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); try { - const res = await getPkgUnits(); - if (res.success) setPkgUnits(res.data); + const [itemRes, pkgRes] = await Promise.all([ + getItemsByDivision("포장재"), + getPkgUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = pkgRes.success ? pkgRes.data : []; + // item_info 기준으로 병합: item_number 매칭되는 pkg_unit이 있으면 그 데이터 사용, 없으면 미등록 placeholder + const merged: PkgUnit[] = items.map((item) => { + const match = existing.find((p) => p.item_number === item.item_number || p.pkg_code === item.item_number); + if (match) return { ...match, pkg_code: match.pkg_code || item.item_number, pkg_name: match.pkg_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + pkg_code: item.item_number, + pkg_name: item.item_name, + pkg_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + volume_l: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as PkgUnit; + }); + // 품목정보에 없지만 pkg_unit에만 있는 항목 (고아) 추가 + const orphans = existing.filter((p) => !items.some((i) => i.item_number === p.item_number || i.item_number === p.pkg_code)); + setPkgUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setPkgLoading(false); } }, []); const fetchLoadingUnits = useCallback(async () => { setLoadingLoading(true); try { - const res = await getLoadingUnits(); - if (res.success) setLoadingUnits(res.data); + const [itemRes, luRes] = await Promise.all([ + getItemsByDivision("적재함"), + getLoadingUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = luRes.success ? luRes.data : []; + const merged: LoadingUnit[] = items.map((item) => { + const match = existing.find((l) => l.item_number === item.item_number || l.loading_code === item.item_number); + if (match) return { ...match, loading_code: match.loading_code || item.item_number, loading_name: match.loading_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + loading_code: item.item_number, + loading_name: item.item_name, + loading_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + max_stack: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as LoadingUnit; + }); + const orphans = existing.filter((l) => !items.some((i) => i.item_number === l.item_number || i.item_number === l.loading_code)); + setLoadingUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setLoadingLoading(false); } }, []); @@ -177,12 +237,21 @@ export default function PackagingPage() { }); // --- 포장재 등록/수정 모달 --- - const openPkgModal = async (mode: "create" | "edit") => { - setPkgModalMode(mode); - if (mode === "edit" && selectedPkg) { - setPkgForm({ ...selectedPkg }); + // row: 리스트에서 클릭한 행. id가 빈 문자열이면 미등록 상태(item_info만 있음) → 등록 모달 + const openPkgModal = async (mode: "create" | "edit", row?: PkgUnit) => { + const target = row || selectedPkg; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setPkgModalMode(actualMode); + if (target) { + // 기존 데이터 or 품목정보 기반 초기값 세팅 + setPkgForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.pkg_code || "", + }); } else { - setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "" }); + setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "", item_number: "" }); } setPkgItemPopoverOpen(false); try { @@ -232,12 +301,19 @@ export default function PackagingPage() { }; // --- 적재함 등록/수정 모달 --- - const openLoadModal = async (mode: "create" | "edit") => { - setLoadModalMode(mode); - if (mode === "edit" && selectedLoading) { - setLoadForm({ ...selectedLoading }); + const openLoadModal = async (mode: "create" | "edit", row?: LoadingUnit) => { + const target = row || selectedLoading; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setLoadModalMode(actualMode); + if (target) { + setLoadForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.loading_code || "", + }); } else { - setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "" }); + setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "", item_number: "" }); } setLoadItemPopoverOpen(false); try { @@ -442,12 +518,8 @@ export default function PackagingPage() {
)} - + {/* 메인 리스트가 품목정보 기반으로 자동 구성되므로 등록 버튼 제거. + 미등록 상태의 품목을 직접 클릭하면 등록 모달이 열림. */} @@ -466,22 +538,30 @@ export default function PackagingPage() { size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - status: { width: "w-[60px]", align: "center", render: (v: any) => ( - - {STATUS_LABEL[v] || v} - - )}, + status: { width: "w-[60px]", align: "center", render: (v: any, row: any) => { + const isUnreg = !row.id; + const cls = isUnreg ? "bg-warning/10 text-warning" : getStatusColor(v); + const label = isUnreg ? "미등록" : (STATUS_LABEL[v] || v); + return {label}; + }}, }; return { key: col.key, label: col.label, ...renderMap[col.key] }; })} data={ts.groupData(filteredPkgUnits)} - rowKey={(row) => String(row.id)} + rowKey={(row) => row.id ? `reg-${row.id}` : `new-${row.pkg_code || row.item_number}`} loading={pkgLoading} - emptyMessage="등록된 포장재가 없어요" - selectedId={selectedPkg ? String(selectedPkg.id) : null} + emptyMessage="포장재 품목이 없어요. 품목정보에서 관리품목을 '포장재'로 등록해주세요." + selectedId={selectedPkg ? (selectedPkg.id ? `reg-${selectedPkg.id}` : `new-${selectedPkg.pkg_code || selectedPkg.item_number}`) : null} onSelect={(id) => { - const pkg = filteredPkgUnits.find((p) => String(p.id) === id); - if (pkg) selectPkg(pkg); + const pkg = filteredPkgUnits.find((p) => (p.id ? `reg-${p.id}` : `new-${p.pkg_code || p.item_number}`) === id); + if (pkg) { + if (!pkg.id) { + // 미등록 → 바로 등록 모달 + openPkgModal("create", pkg); + } else { + selectPkg(pkg); + } + } }} showPagination={false} draggableColumns @@ -592,14 +672,24 @@ export default function PackagingPage() {
- ) : filteredLoadingUnits.map((l) => ( + ) : filteredLoadingUnits.map((l) => { + const isUnreg = !l.id; + const rowKey = l.id ? `reg-${l.id}` : `new-${l.loading_code || l.item_number}`; + const selKey = selectedLoading ? (selectedLoading.id ? `reg-${selectedLoading.id}` : `new-${selectedLoading.loading_code || selectedLoading.item_number}`) : null; + return ( selectLoading(l)} + onClick={() => { + if (isUnreg) { + openLoadModal("create", l); + } else { + selectLoading(l); + } + }} > {l.loading_code} {l.loading_name} @@ -607,12 +697,13 @@ export default function PackagingPage() { {fmtSize(l.width_mm, l.length_mm, l.height_mm)} {Number(l.max_load_kg || 0) > 0 ? `${l.max_load_kg}kg` : "-"} - - {STATUS_LABEL[l.status] || l.status} + + {isUnreg ? "미등록" : (STATUS_LABEL[l.status] || l.status)} - ))} + ); + })}
@@ -699,35 +790,9 @@ export default function PackagingPage() { {pkgModalMode === "create" && (
- - - - - - { - const item = pkgItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {pkgItemOptions.map((item) => ( - onPkgItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {pkgForm.pkg_code ? `${pkgForm.pkg_name} (${pkgForm.pkg_code})` : "-"} +
)}
@@ -781,35 +846,9 @@ export default function PackagingPage() { {loadModalMode === "create" && (
- - - - - - { - const item = loadItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {loadItemOptions.map((item) => ( - onLoadItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {loadForm.loading_code ? `${loadForm.loading_name} (${loadForm.loading_code})` : "-"} +
)}
diff --git a/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx index 6ae340aa..942002e3 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx @@ -37,7 +37,7 @@ const GRID_COLUMNS = [ { key: "pkg_name", label: "포장명" }, { key: "pkg_type", label: "유형" }, { key: "size", label: "크기(mm)" }, - { key: "max_weight", label: "최대중량" }, + { key: "max_load_kg", label: "최대중량" }, { key: "status", label: "상태" }, ]; @@ -51,7 +51,7 @@ const LOADING_TYPE_LABEL: Record = { ALU_PALLET: "알루미늄파렛트", CONTAINER: "컨테이너", STEEL_BOX: "철재함", CAGE: "케이지", ETC: "기타", }; -const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용" }; +const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용", UNREGISTERED: "미등록" }; const getStatusColor = (s: string) => s === "ACTIVE" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"; const fmtSize = (w: any, l: any, h: any) => { @@ -118,20 +118,80 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); - // --- 데이터 로드 --- + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); try { - const res = await getPkgUnits(); - if (res.success) setPkgUnits(res.data); + const [itemRes, pkgRes] = await Promise.all([ + getItemsByDivision("포장재"), + getPkgUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = pkgRes.success ? pkgRes.data : []; + // item_info 기준으로 병합: item_number 매칭되는 pkg_unit이 있으면 그 데이터 사용, 없으면 미등록 placeholder + const merged: PkgUnit[] = items.map((item) => { + const match = existing.find((p) => p.item_number === item.item_number || p.pkg_code === item.item_number); + if (match) return { ...match, pkg_code: match.pkg_code || item.item_number, pkg_name: match.pkg_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + pkg_code: item.item_number, + pkg_name: item.item_name, + pkg_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + volume_l: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as PkgUnit; + }); + // 품목정보에 없지만 pkg_unit에만 있는 항목 (고아) 추가 + const orphans = existing.filter((p) => !items.some((i) => i.item_number === p.item_number || i.item_number === p.pkg_code)); + setPkgUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setPkgLoading(false); } }, []); const fetchLoadingUnits = useCallback(async () => { setLoadingLoading(true); try { - const res = await getLoadingUnits(); - if (res.success) setLoadingUnits(res.data); + const [itemRes, luRes] = await Promise.all([ + getItemsByDivision("적재함"), + getLoadingUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = luRes.success ? luRes.data : []; + const merged: LoadingUnit[] = items.map((item) => { + const match = existing.find((l) => l.item_number === item.item_number || l.loading_code === item.item_number); + if (match) return { ...match, loading_code: match.loading_code || item.item_number, loading_name: match.loading_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + loading_code: item.item_number, + loading_name: item.item_name, + loading_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + max_stack: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as LoadingUnit; + }); + const orphans = existing.filter((l) => !items.some((i) => i.item_number === l.item_number || i.item_number === l.loading_code)); + setLoadingUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setLoadingLoading(false); } }, []); @@ -177,12 +237,21 @@ export default function PackagingPage() { }); // --- 포장재 등록/수정 모달 --- - const openPkgModal = async (mode: "create" | "edit") => { - setPkgModalMode(mode); - if (mode === "edit" && selectedPkg) { - setPkgForm({ ...selectedPkg }); + // row: 리스트에서 클릭한 행. id가 빈 문자열이면 미등록 상태(item_info만 있음) → 등록 모달 + const openPkgModal = async (mode: "create" | "edit", row?: PkgUnit) => { + const target = row || selectedPkg; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setPkgModalMode(actualMode); + if (target) { + // 기존 데이터 or 품목정보 기반 초기값 세팅 + setPkgForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.pkg_code || "", + }); } else { - setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "" }); + setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "", item_number: "" }); } setPkgItemPopoverOpen(false); try { @@ -232,12 +301,19 @@ export default function PackagingPage() { }; // --- 적재함 등록/수정 모달 --- - const openLoadModal = async (mode: "create" | "edit") => { - setLoadModalMode(mode); - if (mode === "edit" && selectedLoading) { - setLoadForm({ ...selectedLoading }); + const openLoadModal = async (mode: "create" | "edit", row?: LoadingUnit) => { + const target = row || selectedLoading; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setLoadModalMode(actualMode); + if (target) { + setLoadForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.loading_code || "", + }); } else { - setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "" }); + setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "", item_number: "" }); } setLoadItemPopoverOpen(false); try { @@ -442,12 +518,8 @@ export default function PackagingPage() {
)} - + {/* 메인 리스트가 품목정보 기반으로 자동 구성되므로 등록 버튼 제거. + 미등록 상태의 품목을 직접 클릭하면 등록 모달이 열림. */} @@ -466,22 +538,30 @@ export default function PackagingPage() { size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - status: { width: "w-[60px]", align: "center", render: (v: any) => ( - - {STATUS_LABEL[v] || v} - - )}, + status: { width: "w-[60px]", align: "center", render: (v: any, row: any) => { + const isUnreg = !row.id; + const cls = isUnreg ? "bg-warning/10 text-warning" : getStatusColor(v); + const label = isUnreg ? "미등록" : (STATUS_LABEL[v] || v); + return {label}; + }}, }; return { key: col.key, label: col.label, ...renderMap[col.key] }; })} data={ts.groupData(filteredPkgUnits)} - rowKey={(row) => String(row.id)} + rowKey={(row) => row.id ? `reg-${row.id}` : `new-${row.pkg_code || row.item_number}`} loading={pkgLoading} - emptyMessage="등록된 포장재가 없어요" - selectedId={selectedPkg ? String(selectedPkg.id) : null} + emptyMessage="포장재 품목이 없어요. 품목정보에서 관리품목을 '포장재'로 등록해주세요." + selectedId={selectedPkg ? (selectedPkg.id ? `reg-${selectedPkg.id}` : `new-${selectedPkg.pkg_code || selectedPkg.item_number}`) : null} onSelect={(id) => { - const pkg = filteredPkgUnits.find((p) => String(p.id) === id); - if (pkg) selectPkg(pkg); + const pkg = filteredPkgUnits.find((p) => (p.id ? `reg-${p.id}` : `new-${p.pkg_code || p.item_number}`) === id); + if (pkg) { + if (!pkg.id) { + // 미등록 → 바로 등록 모달 + openPkgModal("create", pkg); + } else { + selectPkg(pkg); + } + } }} showPagination={false} draggableColumns @@ -592,14 +672,24 @@ export default function PackagingPage() {
- ) : filteredLoadingUnits.map((l) => ( + ) : filteredLoadingUnits.map((l) => { + const isUnreg = !l.id; + const rowKey = l.id ? `reg-${l.id}` : `new-${l.loading_code || l.item_number}`; + const selKey = selectedLoading ? (selectedLoading.id ? `reg-${selectedLoading.id}` : `new-${selectedLoading.loading_code || selectedLoading.item_number}`) : null; + return ( selectLoading(l)} + onClick={() => { + if (isUnreg) { + openLoadModal("create", l); + } else { + selectLoading(l); + } + }} > {l.loading_code} {l.loading_name} @@ -607,12 +697,13 @@ export default function PackagingPage() { {fmtSize(l.width_mm, l.length_mm, l.height_mm)} {Number(l.max_load_kg || 0) > 0 ? `${l.max_load_kg}kg` : "-"} - - {STATUS_LABEL[l.status] || l.status} + + {isUnreg ? "미등록" : (STATUS_LABEL[l.status] || l.status)} - ))} + ); + })}
@@ -699,35 +790,9 @@ export default function PackagingPage() { {pkgModalMode === "create" && (
- - - - - - { - const item = pkgItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {pkgItemOptions.map((item) => ( - onPkgItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {pkgForm.pkg_code ? `${pkgForm.pkg_name} (${pkgForm.pkg_code})` : "-"} +
)}
@@ -781,35 +846,9 @@ export default function PackagingPage() { {loadModalMode === "create" && (
- - - - - - { - const item = loadItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {loadItemOptions.map((item) => ( - onLoadItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {loadForm.loading_code ? `${loadForm.loading_name} (${loadForm.loading_code})` : "-"} +
)}
diff --git a/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx index 6ae340aa..942002e3 100644 --- a/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_9/logistics/packaging/page.tsx @@ -37,7 +37,7 @@ const GRID_COLUMNS = [ { key: "pkg_name", label: "포장명" }, { key: "pkg_type", label: "유형" }, { key: "size", label: "크기(mm)" }, - { key: "max_weight", label: "최대중량" }, + { key: "max_load_kg", label: "최대중량" }, { key: "status", label: "상태" }, ]; @@ -51,7 +51,7 @@ const LOADING_TYPE_LABEL: Record = { ALU_PALLET: "알루미늄파렛트", CONTAINER: "컨테이너", STEEL_BOX: "철재함", CAGE: "케이지", ETC: "기타", }; -const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용" }; +const STATUS_LABEL: Record = { ACTIVE: "사용", INACTIVE: "미사용", UNREGISTERED: "미등록" }; const getStatusColor = (s: string) => s === "ACTIVE" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"; const fmtSize = (w: any, l: any, h: any) => { @@ -118,20 +118,80 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); - // --- 데이터 로드 --- + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); try { - const res = await getPkgUnits(); - if (res.success) setPkgUnits(res.data); + const [itemRes, pkgRes] = await Promise.all([ + getItemsByDivision("포장재"), + getPkgUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = pkgRes.success ? pkgRes.data : []; + // item_info 기준으로 병합: item_number 매칭되는 pkg_unit이 있으면 그 데이터 사용, 없으면 미등록 placeholder + const merged: PkgUnit[] = items.map((item) => { + const match = existing.find((p) => p.item_number === item.item_number || p.pkg_code === item.item_number); + if (match) return { ...match, pkg_code: match.pkg_code || item.item_number, pkg_name: match.pkg_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + pkg_code: item.item_number, + pkg_name: item.item_name, + pkg_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + volume_l: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as PkgUnit; + }); + // 품목정보에 없지만 pkg_unit에만 있는 항목 (고아) 추가 + const orphans = existing.filter((p) => !items.some((i) => i.item_number === p.item_number || i.item_number === p.pkg_code)); + setPkgUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setPkgLoading(false); } }, []); const fetchLoadingUnits = useCallback(async () => { setLoadingLoading(true); try { - const res = await getLoadingUnits(); - if (res.success) setLoadingUnits(res.data); + const [itemRes, luRes] = await Promise.all([ + getItemsByDivision("적재함"), + getLoadingUnits(), + ]); + const items = itemRes.success ? itemRes.data : []; + const existing = luRes.success ? luRes.data : []; + const merged: LoadingUnit[] = items.map((item) => { + const match = existing.find((l) => l.item_number === item.item_number || l.loading_code === item.item_number); + if (match) return { ...match, loading_code: match.loading_code || item.item_number, loading_name: match.loading_name || item.item_name }; + const dims = parseSpecDimensions(item.size); + return { + id: "", + company_code: "", + loading_code: item.item_number, + loading_name: item.item_name, + loading_type: "", + status: "INACTIVE", + width_mm: dims.w || null, + length_mm: dims.l || null, + height_mm: dims.h || null, + self_weight_kg: null, + max_load_kg: null, + max_stack: null, + remarks: null, + created_date: "", + writer: null, + item_number: item.item_number, + } as LoadingUnit; + }); + const orphans = existing.filter((l) => !items.some((i) => i.item_number === l.item_number || i.item_number === l.loading_code)); + setLoadingUnits([...merged, ...orphans]); } catch { /* ignore */ } finally { setLoadingLoading(false); } }, []); @@ -177,12 +237,21 @@ export default function PackagingPage() { }); // --- 포장재 등록/수정 모달 --- - const openPkgModal = async (mode: "create" | "edit") => { - setPkgModalMode(mode); - if (mode === "edit" && selectedPkg) { - setPkgForm({ ...selectedPkg }); + // row: 리스트에서 클릭한 행. id가 빈 문자열이면 미등록 상태(item_info만 있음) → 등록 모달 + const openPkgModal = async (mode: "create" | "edit", row?: PkgUnit) => { + const target = row || selectedPkg; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setPkgModalMode(actualMode); + if (target) { + // 기존 데이터 or 품목정보 기반 초기값 세팅 + setPkgForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.pkg_code || "", + }); } else { - setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "" }); + setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "", item_number: "" }); } setPkgItemPopoverOpen(false); try { @@ -232,12 +301,19 @@ export default function PackagingPage() { }; // --- 적재함 등록/수정 모달 --- - const openLoadModal = async (mode: "create" | "edit") => { - setLoadModalMode(mode); - if (mode === "edit" && selectedLoading) { - setLoadForm({ ...selectedLoading }); + const openLoadModal = async (mode: "create" | "edit", row?: LoadingUnit) => { + const target = row || selectedLoading; + const isRegistered = !!(target && target.id); + const actualMode: "create" | "edit" = isRegistered ? "edit" : "create"; + setLoadModalMode(actualMode); + if (target) { + setLoadForm({ + ...target, + status: target.status || "ACTIVE", + item_number: target.item_number || target.loading_code || "", + }); } else { - setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "" }); + setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "", item_number: "" }); } setLoadItemPopoverOpen(false); try { @@ -442,12 +518,8 @@ export default function PackagingPage() {
)} - + {/* 메인 리스트가 품목정보 기반으로 자동 구성되므로 등록 버튼 제거. + 미등록 상태의 품목을 직접 클릭하면 등록 모달이 열림. */} @@ -466,22 +538,30 @@ export default function PackagingPage() { size: { width: "w-[100px]", render: (_v: any, row: any) => fmtSize(row.width_mm, row.length_mm, row.height_mm) }, max_weight: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, max_load_kg: { width: "w-[80px]", align: "right", render: (v: any) => Number(v || 0) > 0 ? `${v}kg` : "-" }, - status: { width: "w-[60px]", align: "center", render: (v: any) => ( - - {STATUS_LABEL[v] || v} - - )}, + status: { width: "w-[60px]", align: "center", render: (v: any, row: any) => { + const isUnreg = !row.id; + const cls = isUnreg ? "bg-warning/10 text-warning" : getStatusColor(v); + const label = isUnreg ? "미등록" : (STATUS_LABEL[v] || v); + return {label}; + }}, }; return { key: col.key, label: col.label, ...renderMap[col.key] }; })} data={ts.groupData(filteredPkgUnits)} - rowKey={(row) => String(row.id)} + rowKey={(row) => row.id ? `reg-${row.id}` : `new-${row.pkg_code || row.item_number}`} loading={pkgLoading} - emptyMessage="등록된 포장재가 없어요" - selectedId={selectedPkg ? String(selectedPkg.id) : null} + emptyMessage="포장재 품목이 없어요. 품목정보에서 관리품목을 '포장재'로 등록해주세요." + selectedId={selectedPkg ? (selectedPkg.id ? `reg-${selectedPkg.id}` : `new-${selectedPkg.pkg_code || selectedPkg.item_number}`) : null} onSelect={(id) => { - const pkg = filteredPkgUnits.find((p) => String(p.id) === id); - if (pkg) selectPkg(pkg); + const pkg = filteredPkgUnits.find((p) => (p.id ? `reg-${p.id}` : `new-${p.pkg_code || p.item_number}`) === id); + if (pkg) { + if (!pkg.id) { + // 미등록 → 바로 등록 모달 + openPkgModal("create", pkg); + } else { + selectPkg(pkg); + } + } }} showPagination={false} draggableColumns @@ -592,14 +672,24 @@ export default function PackagingPage() {
- ) : filteredLoadingUnits.map((l) => ( + ) : filteredLoadingUnits.map((l) => { + const isUnreg = !l.id; + const rowKey = l.id ? `reg-${l.id}` : `new-${l.loading_code || l.item_number}`; + const selKey = selectedLoading ? (selectedLoading.id ? `reg-${selectedLoading.id}` : `new-${selectedLoading.loading_code || selectedLoading.item_number}`) : null; + return ( selectLoading(l)} + onClick={() => { + if (isUnreg) { + openLoadModal("create", l); + } else { + selectLoading(l); + } + }} > {l.loading_code} {l.loading_name} @@ -607,12 +697,13 @@ export default function PackagingPage() { {fmtSize(l.width_mm, l.length_mm, l.height_mm)} {Number(l.max_load_kg || 0) > 0 ? `${l.max_load_kg}kg` : "-"} - - {STATUS_LABEL[l.status] || l.status} + + {isUnreg ? "미등록" : (STATUS_LABEL[l.status] || l.status)} - ))} + ); + })}
@@ -699,35 +790,9 @@ export default function PackagingPage() { {pkgModalMode === "create" && (
- - - - - - { - const item = pkgItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {pkgItemOptions.map((item) => ( - onPkgItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {pkgForm.pkg_code ? `${pkgForm.pkg_name} (${pkgForm.pkg_code})` : "-"} +
)}
@@ -781,35 +846,9 @@ export default function PackagingPage() { {loadModalMode === "create" && (
- - - - - - { - const item = loadItemOptions.find((i) => i.id === value); - if (!item) return 0; - const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase(); - return target.includes(search.toLowerCase()) ? 1 : 0; - }}> - - - 검색 결과가 없습니다 - {loadItemOptions.map((item) => ( - onLoadItemSelect(item)} className="text-xs"> - - {item.item_name} - {item.item_number} - {item.size && {item.size}} - - ))} - - - - +
+ {loadForm.loading_code ? `${loadForm.loading_name} (${loadForm.loading_code})` : "-"} +
)}