feat: Enhance inventory and outbound pages with category mapping and user information

- Implemented user mapping to display user names instead of IDs in the inventory and receiving pages.
- Added category mapping for materials and units in the outbound page, improving data representation.
- Updated API calls to fetch user and category data, ensuring accurate and user-friendly displays.
- These enhancements aim to improve the overall user experience by providing clearer information and better data management across multiple company implementations.
This commit is contained in:
kjs
2026-04-12 19:34:45 +09:00
parent 3a63cafea1
commit 31bdbe1331
21 changed files with 679 additions and 119 deletions
@@ -140,31 +140,47 @@ export default function InventoryStatusPage() {
Record<string, { code: string; label: string }[]>
>({});
// 카테고리 로드
// 사용자 맵 (writer → 이름)
const [userMap, setUserMap] = useState<Record<string, string>>({});
// 카테고리 + 사용자 로드
useEffect(() => {
const load = async () => {
const optMap: Record<string, { code: string; label: string }[]> = {};
const flatten = (vals: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of vals) {
result.push({ code: v.valueCode, label: v.valueLabel });
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
for (const col of ["status", "unit"]) {
// inventory_stock 카테고리
for (const col of ["status"]) {
try {
const res = await apiClient.get(
`/table-categories/${STOCK_TABLE}/${col}/values`
);
const res = await apiClient.get(`/table-categories/${STOCK_TABLE}/${col}/values`);
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch {
/* skip */
}
} catch { /* skip */ }
}
// item_info 단위 카테고리
try {
const res = await apiClient.get("/table-categories/item_info/unit/values");
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
} catch { /* skip */ }
setCategoryOptions(optMap);
};
load();
// 사용자 목록 로드
apiClient.get("/admin/users").then((res) => {
const users = res.data?.data || res.data || [];
const map: Record<string, string> = {};
for (const u of users) {
const id = u.user_id || u.id;
const name = u.user_name || u.name || id;
if (id) map[id] = name;
}
setUserMap(map);
}).catch(() => {});
}, []);
// 재고 목록 조회
@@ -193,10 +209,12 @@ export default function InventoryStatusPage() {
};
const data = raw.map((r: any) => {
const itemInfo = itemMap.get(r.item_code) as any;
const rawUnit = itemInfo?.unit || r.unit || "";
return {
...r,
item_name: itemInfo?.name || "",
unit: itemInfo?.unit || resolve("unit", r.unit),
unit: resolve("item_unit", rawUnit) || rawUnit,
warehouse_code: whMap.get(r.warehouse_code) || r.warehouse_code || "",
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
status: resolve("status", r.status),
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
@@ -613,7 +631,7 @@ export default function InventoryStatusPage() {
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
</TableCell>
<TableCell>{h.writer || h.created_by || ""}</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>
))}
</TableBody>
@@ -311,6 +311,34 @@ export default function OutboundPage() {
const [editMode, setEditMode] = useState(false);
const [editItemIds, setEditItemIds] = useState<string[]>([]);
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 소스 데이터
const [sourceKeyword, setSourceKeyword] = useState("");
const [sourceLoading, setSourceLoading] = useState(false);
@@ -1337,6 +1365,7 @@ export default function OutboundPage() {
data={pagedItems}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1772,10 +1801,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1826,8 +1857,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -353,17 +353,45 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
// 구매관리 division 코드 로드
// 구매관리 division 코드 + 재질/단위 카테고리 로드
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리");
const found = vals.find((v: any) => (v.valueLabel || v.value_label || v.label) === "구매관리");
if (found) setPurchaseDivisionCode(found.value_code || found.code);
}).catch(() => {});
// 재질, 단위 카테고리
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
// 카테고리 코드→라벨 변환
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
@@ -1287,6 +1315,7 @@ export default function ReceivingPage() {
data={items}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1742,10 +1771,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1798,8 +1829,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -140,31 +140,47 @@ export default function InventoryStatusPage() {
Record<string, { code: string; label: string }[]>
>({});
// 카테고리 로드
// 사용자 맵 (writer → 이름)
const [userMap, setUserMap] = useState<Record<string, string>>({});
// 카테고리 + 사용자 로드
useEffect(() => {
const load = async () => {
const optMap: Record<string, { code: string; label: string }[]> = {};
const flatten = (vals: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of vals) {
result.push({ code: v.valueCode, label: v.valueLabel });
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
for (const col of ["status", "unit"]) {
// inventory_stock 카테고리
for (const col of ["status"]) {
try {
const res = await apiClient.get(
`/table-categories/${STOCK_TABLE}/${col}/values`
);
const res = await apiClient.get(`/table-categories/${STOCK_TABLE}/${col}/values`);
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch {
/* skip */
}
} catch { /* skip */ }
}
// item_info 단위 카테고리
try {
const res = await apiClient.get("/table-categories/item_info/unit/values");
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
} catch { /* skip */ }
setCategoryOptions(optMap);
};
load();
// 사용자 목록 로드
apiClient.get("/admin/users").then((res) => {
const users = res.data?.data || res.data || [];
const map: Record<string, string> = {};
for (const u of users) {
const id = u.user_id || u.id;
const name = u.user_name || u.name || id;
if (id) map[id] = name;
}
setUserMap(map);
}).catch(() => {});
}, []);
// 재고 목록 조회
@@ -193,10 +209,12 @@ export default function InventoryStatusPage() {
};
const data = raw.map((r: any) => {
const itemInfo = itemMap.get(r.item_code) as any;
const rawUnit = itemInfo?.unit || r.unit || "";
return {
...r,
item_name: itemInfo?.name || "",
unit: itemInfo?.unit || resolve("unit", r.unit),
unit: resolve("item_unit", rawUnit) || rawUnit,
warehouse_code: whMap.get(r.warehouse_code) || r.warehouse_code || "",
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
status: resolve("status", r.status),
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
@@ -613,7 +631,7 @@ export default function InventoryStatusPage() {
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
</TableCell>
<TableCell>{h.writer || h.created_by || ""}</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>
))}
</TableBody>
@@ -311,6 +311,34 @@ export default function OutboundPage() {
const [editMode, setEditMode] = useState(false);
const [editItemIds, setEditItemIds] = useState<string[]>([]);
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 소스 데이터
const [sourceKeyword, setSourceKeyword] = useState("");
const [sourceLoading, setSourceLoading] = useState(false);
@@ -1337,6 +1365,7 @@ export default function OutboundPage() {
data={pagedItems}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1772,10 +1801,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1826,8 +1857,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -353,17 +353,45 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
// 구매관리 division 코드 로드
// 구매관리 division 코드 + 재질/단위 카테고리 로드
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리");
const found = vals.find((v: any) => (v.valueLabel || v.value_label || v.label) === "구매관리");
if (found) setPurchaseDivisionCode(found.value_code || found.code);
}).catch(() => {});
// 재질, 단위 카테고리
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
// 카테고리 코드→라벨 변환
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
@@ -1287,6 +1315,7 @@ export default function ReceivingPage() {
data={items}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1742,10 +1771,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1798,8 +1829,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -140,31 +140,47 @@ export default function InventoryStatusPage() {
Record<string, { code: string; label: string }[]>
>({});
// 카테고리 로드
// 사용자 맵 (writer → 이름)
const [userMap, setUserMap] = useState<Record<string, string>>({});
// 카테고리 + 사용자 로드
useEffect(() => {
const load = async () => {
const optMap: Record<string, { code: string; label: string }[]> = {};
const flatten = (vals: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of vals) {
result.push({ code: v.valueCode, label: v.valueLabel });
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
for (const col of ["status", "unit"]) {
// inventory_stock 카테고리
for (const col of ["status"]) {
try {
const res = await apiClient.get(
`/table-categories/${STOCK_TABLE}/${col}/values`
);
const res = await apiClient.get(`/table-categories/${STOCK_TABLE}/${col}/values`);
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch {
/* skip */
}
} catch { /* skip */ }
}
// item_info 단위 카테고리
try {
const res = await apiClient.get("/table-categories/item_info/unit/values");
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
} catch { /* skip */ }
setCategoryOptions(optMap);
};
load();
// 사용자 목록 로드
apiClient.get("/admin/users").then((res) => {
const users = res.data?.data || res.data || [];
const map: Record<string, string> = {};
for (const u of users) {
const id = u.user_id || u.id;
const name = u.user_name || u.name || id;
if (id) map[id] = name;
}
setUserMap(map);
}).catch(() => {});
}, []);
// 재고 목록 조회
@@ -193,10 +209,12 @@ export default function InventoryStatusPage() {
};
const data = raw.map((r: any) => {
const itemInfo = itemMap.get(r.item_code) as any;
const rawUnit = itemInfo?.unit || r.unit || "";
return {
...r,
item_name: itemInfo?.name || "",
unit: itemInfo?.unit || resolve("unit", r.unit),
unit: resolve("item_unit", rawUnit) || rawUnit,
warehouse_code: whMap.get(r.warehouse_code) || r.warehouse_code || "",
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
status: resolve("status", r.status),
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
@@ -613,7 +631,7 @@ export default function InventoryStatusPage() {
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
</TableCell>
<TableCell>{h.writer || h.created_by || ""}</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>
))}
</TableBody>
@@ -311,6 +311,34 @@ export default function OutboundPage() {
const [editMode, setEditMode] = useState(false);
const [editItemIds, setEditItemIds] = useState<string[]>([]);
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 소스 데이터
const [sourceKeyword, setSourceKeyword] = useState("");
const [sourceLoading, setSourceLoading] = useState(false);
@@ -1337,6 +1365,7 @@ export default function OutboundPage() {
data={pagedItems}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1772,10 +1801,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1826,8 +1857,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -353,17 +353,45 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
// 구매관리 division 코드 로드
// 구매관리 division 코드 + 재질/단위 카테고리 로드
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리");
const found = vals.find((v: any) => (v.valueLabel || v.value_label || v.label) === "구매관리");
if (found) setPurchaseDivisionCode(found.value_code || found.code);
}).catch(() => {});
// 재질, 단위 카테고리
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
// 카테고리 코드→라벨 변환
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
@@ -1287,6 +1315,7 @@ export default function ReceivingPage() {
data={items}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1742,10 +1771,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1798,8 +1829,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -140,31 +140,47 @@ export default function InventoryStatusPage() {
Record<string, { code: string; label: string }[]>
>({});
// 카테고리 로드
// 사용자 맵 (writer → 이름)
const [userMap, setUserMap] = useState<Record<string, string>>({});
// 카테고리 + 사용자 로드
useEffect(() => {
const load = async () => {
const optMap: Record<string, { code: string; label: string }[]> = {};
const flatten = (vals: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of vals) {
result.push({ code: v.valueCode, label: v.valueLabel });
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
for (const col of ["status", "unit"]) {
// inventory_stock 카테고리
for (const col of ["status"]) {
try {
const res = await apiClient.get(
`/table-categories/${STOCK_TABLE}/${col}/values`
);
const res = await apiClient.get(`/table-categories/${STOCK_TABLE}/${col}/values`);
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch {
/* skip */
}
} catch { /* skip */ }
}
// item_info 단위 카테고리
try {
const res = await apiClient.get("/table-categories/item_info/unit/values");
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
} catch { /* skip */ }
setCategoryOptions(optMap);
};
load();
// 사용자 목록 로드
apiClient.get("/admin/users").then((res) => {
const users = res.data?.data || res.data || [];
const map: Record<string, string> = {};
for (const u of users) {
const id = u.user_id || u.id;
const name = u.user_name || u.name || id;
if (id) map[id] = name;
}
setUserMap(map);
}).catch(() => {});
}, []);
// 재고 목록 조회
@@ -193,10 +209,12 @@ export default function InventoryStatusPage() {
};
const data = raw.map((r: any) => {
const itemInfo = itemMap.get(r.item_code) as any;
const rawUnit = itemInfo?.unit || r.unit || "";
return {
...r,
item_name: itemInfo?.name || "",
unit: itemInfo?.unit || resolve("unit", r.unit),
unit: resolve("item_unit", rawUnit) || rawUnit,
warehouse_code: whMap.get(r.warehouse_code) || r.warehouse_code || "",
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
status: resolve("status", r.status),
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
@@ -613,7 +631,7 @@ export default function InventoryStatusPage() {
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
</TableCell>
<TableCell>{h.writer || h.created_by || ""}</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>
))}
</TableBody>
@@ -311,6 +311,34 @@ export default function OutboundPage() {
const [editMode, setEditMode] = useState(false);
const [editItemIds, setEditItemIds] = useState<string[]>([]);
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 소스 데이터
const [sourceKeyword, setSourceKeyword] = useState("");
const [sourceLoading, setSourceLoading] = useState(false);
@@ -1337,6 +1365,7 @@ export default function OutboundPage() {
data={pagedItems}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1772,10 +1801,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1826,8 +1857,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -353,17 +353,45 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
// 구매관리 division 코드 로드
// 구매관리 division 코드 + 재질/단위 카테고리 로드
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리");
const found = vals.find((v: any) => (v.valueLabel || v.value_label || v.label) === "구매관리");
if (found) setPurchaseDivisionCode(found.value_code || found.code);
}).catch(() => {});
// 재질, 단위 카테고리
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
// 카테고리 코드→라벨 변환
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
@@ -1287,6 +1315,7 @@ export default function ReceivingPage() {
data={items}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1742,10 +1771,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1798,8 +1829,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -140,31 +140,47 @@ export default function InventoryStatusPage() {
Record<string, { code: string; label: string }[]>
>({});
// 카테고리 로드
// 사용자 맵 (writer → 이름)
const [userMap, setUserMap] = useState<Record<string, string>>({});
// 카테고리 + 사용자 로드
useEffect(() => {
const load = async () => {
const optMap: Record<string, { code: string; label: string }[]> = {};
const flatten = (vals: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of vals) {
result.push({ code: v.valueCode, label: v.valueLabel });
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
for (const col of ["status", "unit"]) {
// inventory_stock 카테고리
for (const col of ["status"]) {
try {
const res = await apiClient.get(
`/table-categories/${STOCK_TABLE}/${col}/values`
);
const res = await apiClient.get(`/table-categories/${STOCK_TABLE}/${col}/values`);
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch {
/* skip */
}
} catch { /* skip */ }
}
// item_info 단위 카테고리
try {
const res = await apiClient.get("/table-categories/item_info/unit/values");
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
} catch { /* skip */ }
setCategoryOptions(optMap);
};
load();
// 사용자 목록 로드
apiClient.get("/admin/users").then((res) => {
const users = res.data?.data || res.data || [];
const map: Record<string, string> = {};
for (const u of users) {
const id = u.user_id || u.id;
const name = u.user_name || u.name || id;
if (id) map[id] = name;
}
setUserMap(map);
}).catch(() => {});
}, []);
// 재고 목록 조회
@@ -193,10 +209,12 @@ export default function InventoryStatusPage() {
};
const data = raw.map((r: any) => {
const itemInfo = itemMap.get(r.item_code) as any;
const rawUnit = itemInfo?.unit || r.unit || "";
return {
...r,
item_name: itemInfo?.name || "",
unit: itemInfo?.unit || resolve("unit", r.unit),
unit: resolve("item_unit", rawUnit) || rawUnit,
warehouse_code: whMap.get(r.warehouse_code) || r.warehouse_code || "",
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
status: resolve("status", r.status),
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
@@ -613,7 +631,7 @@ export default function InventoryStatusPage() {
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
</TableCell>
<TableCell>{h.writer || h.created_by || ""}</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>
))}
</TableBody>
@@ -311,6 +311,34 @@ export default function OutboundPage() {
const [editMode, setEditMode] = useState(false);
const [editItemIds, setEditItemIds] = useState<string[]>([]);
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 소스 데이터
const [sourceKeyword, setSourceKeyword] = useState("");
const [sourceLoading, setSourceLoading] = useState(false);
@@ -1337,6 +1365,7 @@ export default function OutboundPage() {
data={pagedItems}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1772,10 +1801,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1826,8 +1857,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -353,17 +353,45 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
// 구매관리 division 코드 로드
// 구매관리 division 코드 + 재질/단위 카테고리 로드
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리");
const found = vals.find((v: any) => (v.valueLabel || v.value_label || v.label) === "구매관리");
if (found) setPurchaseDivisionCode(found.value_code || found.code);
}).catch(() => {});
// 재질, 단위 카테고리
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
// 카테고리 코드→라벨 변환
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
@@ -1287,6 +1315,7 @@ export default function ReceivingPage() {
data={items}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1742,10 +1771,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1798,8 +1829,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -140,31 +140,47 @@ export default function InventoryStatusPage() {
Record<string, { code: string; label: string }[]>
>({});
// 카테고리 로드
// 사용자 맵 (writer → 이름)
const [userMap, setUserMap] = useState<Record<string, string>>({});
// 카테고리 + 사용자 로드
useEffect(() => {
const load = async () => {
const optMap: Record<string, { code: string; label: string }[]> = {};
const flatten = (vals: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of vals) {
result.push({ code: v.valueCode, label: v.valueLabel });
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
for (const col of ["status", "unit"]) {
// inventory_stock 카테고리
for (const col of ["status"]) {
try {
const res = await apiClient.get(
`/table-categories/${STOCK_TABLE}/${col}/values`
);
const res = await apiClient.get(`/table-categories/${STOCK_TABLE}/${col}/values`);
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch {
/* skip */
}
} catch { /* skip */ }
}
// item_info 단위 카테고리
try {
const res = await apiClient.get("/table-categories/item_info/unit/values");
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
} catch { /* skip */ }
setCategoryOptions(optMap);
};
load();
// 사용자 목록 로드
apiClient.get("/admin/users").then((res) => {
const users = res.data?.data || res.data || [];
const map: Record<string, string> = {};
for (const u of users) {
const id = u.user_id || u.id;
const name = u.user_name || u.name || id;
if (id) map[id] = name;
}
setUserMap(map);
}).catch(() => {});
}, []);
// 재고 목록 조회
@@ -193,10 +209,12 @@ export default function InventoryStatusPage() {
};
const data = raw.map((r: any) => {
const itemInfo = itemMap.get(r.item_code) as any;
const rawUnit = itemInfo?.unit || r.unit || "";
return {
...r,
item_name: itemInfo?.name || "",
unit: itemInfo?.unit || resolve("unit", r.unit),
unit: resolve("item_unit", rawUnit) || rawUnit,
warehouse_code: whMap.get(r.warehouse_code) || r.warehouse_code || "",
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
status: resolve("status", r.status),
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
@@ -613,7 +631,7 @@ export default function InventoryStatusPage() {
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
</TableCell>
<TableCell>{h.writer || h.created_by || ""}</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>
))}
</TableBody>
@@ -311,6 +311,34 @@ export default function OutboundPage() {
const [editMode, setEditMode] = useState(false);
const [editItemIds, setEditItemIds] = useState<string[]>([]);
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 소스 데이터
const [sourceKeyword, setSourceKeyword] = useState("");
const [sourceLoading, setSourceLoading] = useState(false);
@@ -1337,6 +1365,7 @@ export default function OutboundPage() {
data={pagedItems}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1772,10 +1801,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1826,8 +1857,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -353,17 +353,45 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
// 구매관리 division 코드 로드
// 구매관리 division 코드 + 재질/단위 카테고리 로드
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리");
const found = vals.find((v: any) => (v.valueLabel || v.value_label || v.label) === "구매관리");
if (found) setPurchaseDivisionCode(found.value_code || found.code);
}).catch(() => {});
// 재질, 단위 카테고리
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
// 카테고리 코드→라벨 변환
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
@@ -1287,6 +1315,7 @@ export default function ReceivingPage() {
data={items}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1742,10 +1771,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1798,8 +1829,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -140,31 +140,47 @@ export default function InventoryStatusPage() {
Record<string, { code: string; label: string }[]>
>({});
// 카테고리 로드
// 사용자 맵 (writer → 이름)
const [userMap, setUserMap] = useState<Record<string, string>>({});
// 카테고리 + 사용자 로드
useEffect(() => {
const load = async () => {
const optMap: Record<string, { code: string; label: string }[]> = {};
const flatten = (vals: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of vals) {
result.push({ code: v.valueCode, label: v.valueLabel });
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
for (const col of ["status", "unit"]) {
// inventory_stock 카테고리
for (const col of ["status"]) {
try {
const res = await apiClient.get(
`/table-categories/${STOCK_TABLE}/${col}/values`
);
const res = await apiClient.get(`/table-categories/${STOCK_TABLE}/${col}/values`);
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch {
/* skip */
}
} catch { /* skip */ }
}
// item_info 단위 카테고리
try {
const res = await apiClient.get("/table-categories/item_info/unit/values");
if (res.data?.success) optMap["item_unit"] = flatten(res.data.data || []);
} catch { /* skip */ }
setCategoryOptions(optMap);
};
load();
// 사용자 목록 로드
apiClient.get("/admin/users").then((res) => {
const users = res.data?.data || res.data || [];
const map: Record<string, string> = {};
for (const u of users) {
const id = u.user_id || u.id;
const name = u.user_name || u.name || id;
if (id) map[id] = name;
}
setUserMap(map);
}).catch(() => {});
}, []);
// 재고 목록 조회
@@ -193,10 +209,12 @@ export default function InventoryStatusPage() {
};
const data = raw.map((r: any) => {
const itemInfo = itemMap.get(r.item_code) as any;
const rawUnit = itemInfo?.unit || r.unit || "";
return {
...r,
item_name: itemInfo?.name || "",
unit: itemInfo?.unit || resolve("unit", r.unit),
unit: resolve("item_unit", rawUnit) || rawUnit,
warehouse_code: whMap.get(r.warehouse_code) || r.warehouse_code || "",
warehouse_name: whMap.get(r.warehouse_code) || r.warehouse_code || "",
status: resolve("status", r.status),
_isLow: r.safety_qty && Number(r.current_qty) < Number(r.safety_qty),
@@ -613,7 +631,7 @@ export default function InventoryStatusPage() {
<TableCell className="truncate max-w-[150px]">
{h.remark || h.reason || ""}
</TableCell>
<TableCell>{h.writer || h.created_by || ""}</TableCell>
<TableCell>{userMap[h.writer] || userMap[h.created_by] || h.writer || h.created_by || ""}</TableCell>
</TableRow>
))}
</TableBody>
@@ -311,6 +311,34 @@ export default function OutboundPage() {
const [editMode, setEditMode] = useState(false);
const [editItemIds, setEditItemIds] = useState<string[]>([]);
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 소스 데이터
const [sourceKeyword, setSourceKeyword] = useState("");
const [sourceLoading, setSourceLoading] = useState(false);
@@ -1337,6 +1365,7 @@ export default function OutboundPage() {
data={pagedItems}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1772,10 +1801,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1826,8 +1857,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>
@@ -353,17 +353,45 @@ export default function ReceivingPage() {
// 구매관리 division 코드 (라벨 기준 조회)
const [purchaseDivisionCode, setPurchaseDivisionCode] = useState<string>("");
// 카테고리 코드→라벨 매핑 (재질, 단위)
const [catMap, setCatMap] = useState<Record<string, Record<string, string>>>({});
// 구매관리 division 코드 로드
// 구매관리 division 코드 + 재질/단위 카테고리 로드
useEffect(() => {
const flatten = (arr: any[]): { code: string; label: string }[] => {
const result: { code: string; label: string }[] = [];
for (const v of arr) {
result.push({ code: v.valueCode || v.value_code || v.code, label: v.valueLabel || v.value_label || v.label });
if (v.children?.length) result.push(...flatten(v.children));
}
return result;
};
// division 카테고리에서 "구매관리" 라벨의 코드 조회
apiClient.get("/table-categories/item_info/division/values").then((res) => {
const vals = res.data?.data || [];
const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리");
const found = vals.find((v: any) => (v.valueLabel || v.value_label || v.label) === "구매관리");
if (found) setPurchaseDivisionCode(found.value_code || found.code);
}).catch(() => {});
// 재질, 단위 카테고리
const map: Record<string, Record<string, string>> = {};
Promise.all(
["material", "unit"].map(async (col) => {
try {
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
const items = flatten(res.data?.data || []);
map[col] = {};
for (const item of items) map[col][item.code] = item.label;
} catch { /* skip */ }
})
).then(() => setCatMap(map));
}, []);
// 카테고리 코드→라벨 변환
const resolveCat = useCallback((col: string, code: string) => {
if (!code) return "";
return catMap[col]?.[code] || code;
}, [catMap]);
// 목록 조회
const fetchList = useCallback(async () => {
setLoading(true);
@@ -1287,6 +1315,7 @@ export default function ReceivingPage() {
data={items}
onAdd={addItem}
selectedKeys={selectedItems.map((s) => s.key)}
resolveCat={resolveCat}
/>
)}
</div>
@@ -1742,10 +1771,12 @@ function SourceItemTable({
data,
onAdd,
selectedKeys,
resolveCat,
}: {
data: ItemSource[];
onAdd: (item: ItemSource) => void;
selectedKeys: string[];
resolveCat: (col: string, code: string) => string;
}) {
if (data.length === 0) {
return (
@@ -1798,8 +1829,8 @@ function SourceItemTable({
</div>
</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.spec || "-"}>{item.spec || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={item.material || "-"}>{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate p-2" title={resolveCat("material", item.material) || "-"}>{resolveCat("material", item.material) || "-"}</TableCell>
<TableCell className="p-2">{resolveCat("unit", item.unit) || "-"}</TableCell>
<TableCell className="p-2 text-right">
{Number(item.standard_price).toLocaleString()}
</TableCell>