feat(frontend/logistics): render remark via category label map

This commit is contained in:
kmh
2026-04-24 15:00:42 +09:00
parent 6656a525a2
commit de660679ca
15 changed files with 216 additions and 14 deletions

View File

@@ -23,6 +23,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
const HISTORY_TABLE = "inventory_history";
@@ -65,6 +66,13 @@ const parseRemark = (remark: string | null | undefined): string => {
export default function InboundOutboundPage() {
const { user } = useAuth();
// remark의 value_code → value_label 변환용 카테고리 맵 (상위 parseRemark 를 shadow)
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);

View File

@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -62,6 +62,7 @@ import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { toast } from "sonner";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
import { exportToExcel } from "@/lib/utils/excelExport";
const STOCK_TABLE = "inventory_stock";
@@ -118,6 +119,13 @@ export default function InventoryStatusPage() {
const { user } = useAuth();
const ts = useTableSettings("c16-inventory", STOCK_TABLE, STOCK_COLUMNS);
// remark의 value_code → value_label 변환 파서
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
// 좌측: 재고 목록
const [stockItems, setStockItems] = useState<any[]>([]);
const [stockLoading, setStockLoading] = useState(false);
@@ -750,7 +758,7 @@ export default function InventoryStatusPage() {
{h.reference_number || h.reference_no || ""}
</TableCell>
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
{parseRemark(h.remark) || h.reason || ""}
</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>

View File

@@ -23,6 +23,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
const HISTORY_TABLE = "inventory_history";
@@ -65,6 +66,13 @@ const parseRemark = (remark: string | null | undefined): string => {
export default function InboundOutboundPage() {
const { user } = useAuth();
// remark의 value_code → value_label 변환용 카테고리 맵 (상위 parseRemark 를 shadow)
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);

View File

@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -62,6 +62,7 @@ import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { toast } from "sonner";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
import { exportToExcel } from "@/lib/utils/excelExport";
const STOCK_TABLE = "inventory_stock";
@@ -118,6 +119,13 @@ export default function InventoryStatusPage() {
const { user } = useAuth();
const ts = useTableSettings("c16-inventory", STOCK_TABLE, STOCK_COLUMNS);
// remark의 value_code → value_label 변환 파서
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
// 좌측: 재고 목록
const [stockItems, setStockItems] = useState<any[]>([]);
const [stockLoading, setStockLoading] = useState(false);
@@ -753,7 +761,7 @@ export default function InventoryStatusPage() {
{h.reference_number || h.reference_no || ""}
</TableCell>
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
{parseRemark(h.remark) || h.reason || ""}
</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>

View File

@@ -23,6 +23,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
const HISTORY_TABLE = "inventory_history";
@@ -65,6 +66,13 @@ const parseRemark = (remark: string | null | undefined): string => {
export default function InboundOutboundPage() {
const { user } = useAuth();
// remark의 value_code → value_label 변환용 카테고리 맵 (상위 parseRemark 를 shadow)
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);

View File

@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -62,6 +62,7 @@ import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { toast } from "sonner";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
import { exportToExcel } from "@/lib/utils/excelExport";
const STOCK_TABLE = "inventory_stock";
@@ -118,6 +119,13 @@ export default function InventoryStatusPage() {
const { user } = useAuth();
const ts = useTableSettings("c16-inventory", STOCK_TABLE, STOCK_COLUMNS);
// remark의 value_code → value_label 변환 파서
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
// 좌측: 재고 목록
const [stockItems, setStockItems] = useState<any[]>([]);
const [stockLoading, setStockLoading] = useState(false);
@@ -750,7 +758,7 @@ export default function InventoryStatusPage() {
{h.reference_number || h.reference_no || ""}
</TableCell>
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
{parseRemark(h.remark) || h.reason || ""}
</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>

View File

@@ -23,6 +23,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
const HISTORY_TABLE = "inventory_history";
@@ -65,6 +66,13 @@ const parseRemark = (remark: string | null | undefined): string => {
export default function InboundOutboundPage() {
const { user } = useAuth();
// remark의 value_code → value_label 변환용 카테고리 맵 (상위 parseRemark 를 shadow)
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);

View File

@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -62,6 +62,7 @@ import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { toast } from "sonner";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
import { exportToExcel } from "@/lib/utils/excelExport";
const STOCK_TABLE = "inventory_stock";
@@ -121,6 +122,13 @@ export default function InventoryStatusPage() {
const { user } = useAuth();
const ts = useTableSettings("c16-inventory", STOCK_TABLE, STOCK_COLUMNS);
// remark의 value_code → value_label 변환 파서
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
// 좌측: 재고 목록
const [stockItems, setStockItems] = useState<any[]>([]);
const [stockLoading, setStockLoading] = useState(false);
@@ -759,7 +767,7 @@ export default function InventoryStatusPage() {
{h.reference_number || h.reference_no || ""}
</TableCell>
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
{parseRemark(h.remark) || h.reason || ""}
</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>

View File

@@ -23,6 +23,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
const HISTORY_TABLE = "inventory_history";
@@ -65,6 +66,13 @@ const parseRemark = (remark: string | null | undefined): string => {
export default function InboundOutboundPage() {
const { user } = useAuth();
// remark의 value_code → value_label 변환용 카테고리 맵 (컴포넌트 내부에서 상위 parseRemark 를 shadow)
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);

View File

@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -62,6 +62,7 @@ import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { toast } from "sonner";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
import { exportToExcel } from "@/lib/utils/excelExport";
const STOCK_TABLE = "inventory_stock";
@@ -118,6 +119,13 @@ export default function InventoryStatusPage() {
const { user } = useAuth();
const ts = useTableSettings("c16-inventory", STOCK_TABLE, STOCK_COLUMNS);
// remark의 value_code → value_label 변환 파서
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
// 좌측: 재고 목록
const [stockItems, setStockItems] = useState<any[]>([]);
const [stockLoading, setStockLoading] = useState(false);
@@ -753,7 +761,7 @@ export default function InventoryStatusPage() {
{h.reference_number || h.reference_no || ""}
</TableCell>
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
{parseRemark(h.remark) || h.reason || ""}
</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>

View File

@@ -23,6 +23,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
const HISTORY_TABLE = "inventory_history";
@@ -65,6 +66,13 @@ const parseRemark = (remark: string | null | undefined): string => {
export default function InboundOutboundPage() {
const { user } = useAuth();
// remark의 value_code → value_label 변환용 카테고리 맵 (상위 parseRemark 를 shadow)
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);

View File

@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -62,6 +62,7 @@ import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { toast } from "sonner";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
import { exportToExcel } from "@/lib/utils/excelExport";
const STOCK_TABLE = "inventory_stock";
@@ -118,6 +119,13 @@ export default function InventoryStatusPage() {
const { user } = useAuth();
const ts = useTableSettings("c16-inventory", STOCK_TABLE, STOCK_COLUMNS);
// remark의 value_code → value_label 변환 파서
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
// 좌측: 재고 목록
const [stockItems, setStockItems] = useState<any[]>([]);
const [stockLoading, setStockLoading] = useState(false);
@@ -752,7 +760,7 @@ export default function InventoryStatusPage() {
{h.reference_number || h.reference_no || ""}
</TableCell>
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
{parseRemark(h.remark) || h.reason || ""}
</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>

View File

@@ -23,6 +23,7 @@ import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { exportToExcel } from "@/lib/utils/excelExport";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
const HISTORY_TABLE = "inventory_history";
@@ -65,6 +66,13 @@ const parseRemark = (remark: string | null | undefined): string => {
export default function InboundOutboundPage() {
const { user } = useAuth();
// remark의 value_code → value_label 변환용 카테고리 맵 (상위 parseRemark 를 shadow)
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);

View File

@@ -9,7 +9,7 @@
* ★ 재고 직접 등록/삭제 불가 — 입출고를 통해서만 변동, 조정만 가능
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -62,6 +62,7 @@ import { TableSettingsModal } from "@/components/common/TableSettingsModal";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable";
import { toast } from "sonner";
import { useCategoryLabelMap, makeParseRemark } from "@/lib/categoryLabel";
import { exportToExcel } from "@/lib/utils/excelExport";
const STOCK_TABLE = "inventory_stock";
@@ -118,6 +119,13 @@ export default function InventoryStatusPage() {
const { user } = useAuth();
const ts = useTableSettings("c16-inventory", STOCK_TABLE, STOCK_COLUMNS);
// remark의 value_code → value_label 변환 파서
const codeLabelMap = useCategoryLabelMap([
{ table: "inbound_mng", column: "inbound_type" },
{ table: "outbound_mng", column: "outbound_type" },
]);
const parseRemark = useMemo(() => makeParseRemark(codeLabelMap), [codeLabelMap]);
// 좌측: 재고 목록
const [stockItems, setStockItems] = useState<any[]>([]);
const [stockLoading, setStockLoading] = useState(false);
@@ -750,7 +758,7 @@ export default function InventoryStatusPage() {
{h.reference_number || h.reference_no || ""}
</TableCell>
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
{parseRemark(h.remark) || h.reason || ""}
</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>

View File

@@ -0,0 +1,90 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { apiClient } from "@/lib/api/client";
export type CategoryLabelMap = Record<string, string>; // value_code -> value_label
export interface CategoryTarget {
table: string;
column: string;
}
/**
* 여러 (table, column) 쌍의 카테고리 값을 한 번에 조회해서
* value_code -> value_label 통합 맵을 반환한다.
*/
export function useCategoryLabelMap(targets: CategoryTarget[]): CategoryLabelMap {
const [map, setMap] = useState<CategoryLabelMap>({});
const key = useMemo(() => JSON.stringify(targets), [targets]);
useEffect(() => {
let cancelled = false;
(async () => {
const merged: CategoryLabelMap = {};
for (const { table, column } of targets) {
try {
const res = await apiClient.get(
`/table-categories/${table}/${column}/values`,
);
if (res.data?.success && Array.isArray(res.data.data)) {
const flatten = (vals: any[]) => {
for (const v of vals) {
if (v.valueCode && v.valueLabel) {
merged[v.valueCode] = v.valueLabel;
}
if (v.children?.length) flatten(v.children);
}
};
flatten(res.data.data);
}
} catch {
/* skip */
}
}
if (!cancelled) setMap(merged);
})();
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key]);
return map;
}
/**
* remark 원문을 화면 표시용 문자열로 변환하는 파서 팩토리.
* - JSON remark → 사람 읽을 수 있는 한글 (창고이동/재고조정/재고확인/공정입고)
* - value_code → codeLabelMap 에서 찾은 value_label
* - 매칭 없음 → 원문 그대로 (과거 한글 레코드 호환)
*/
export function makeParseRemark(codeLabelMap: CategoryLabelMap) {
return (remark: string | null | undefined): string => {
if (!remark) return "";
const trimmed = remark.trim();
if (trimmed.startsWith("{")) {
try {
const d = JSON.parse(trimmed);
switch (d.type) {
case "move":
return `창고이동 (${d.from_warehouse}${d.to_warehouse})`;
case "adjust":
return `재고조정 (${d.reason || "사유 없음"}, ${d.system_qty}${d.actual_qty}, 차이:${d.diff >= 0 ? "+" : ""}${d.diff})`;
case "confirm":
return `재고확인 (${d.reason || "이상없음"})`;
case "process_inbound":
return "공정입고";
default:
return d.reason || d.memo || trimmed;
}
} catch {
return trimmed;
}
}
if (codeLabelMap[trimmed]) return codeLabelMap[trimmed];
return trimmed;
};
}