feat: Implement searchable category comboboxes and enhance item management
- Added searchable category combobox and multi-category combobox components to improve item selection in the purchase and sales item pages. - Updated the supplier management page to utilize useCallback for item search, enhancing performance and responsiveness. - Implemented real-time search functionality for item selection, ensuring a smoother user experience. - Enhanced the handling of item mappings and prices, allowing for soft deletion of supplier connections while retaining data integrity. These changes aim to improve the overall user experience by providing more intuitive item management and selection processes across multiple company implementations.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -813,7 +813,7 @@ export default function SupplierManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
@@ -834,7 +834,14 @@ export default function SupplierManagementPage() {
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실��간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
@@ -971,6 +978,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -991,7 +999,8 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1043,6 +1052,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1092,12 +1102,13 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1167,7 +1178,7 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1199,40 +1210,63 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 (소프트 삭제 — supplier_id를 null 처리)
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 공급업체 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → supplier_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → supplier_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedSupplierId;
|
||||
setSelectedSupplierId(null);
|
||||
setTimeout(() => setSelectedSupplierId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -814,14 +814,13 @@ export default function CustomerManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters },
|
||||
@@ -830,21 +829,41 @@ export default function CustomerManagementPage() {
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const kw = itemSearchKeyword.toLowerCase();
|
||||
const seenNumbers = new Set<string>();
|
||||
const deduped = allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
if (item.item_number && seenNumbers.has(item.item_number)) return false;
|
||||
if (item.item_number) seenNumbers.add(item.item_number);
|
||||
if (kw) {
|
||||
const name = (item.item_name || "").toLowerCase();
|
||||
const code = (item.item_number || "").toLowerCase();
|
||||
if (!name.includes(kw) && !code.includes(kw)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setItemSearchResults(deduped);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실시간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]);
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
const selected = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (selected.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const raw = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (raw.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const seenKeys = new Set<string>();
|
||||
const selected = raw.filter((i) => {
|
||||
const k = i.item_number || i.id;
|
||||
if (seenKeys.has(k)) return false;
|
||||
seenKeys.add(k);
|
||||
return true;
|
||||
});
|
||||
setSelectedItemsForDetail(selected);
|
||||
const mappings: typeof itemMappings = {};
|
||||
const prices: typeof itemPrices = {};
|
||||
@@ -976,6 +995,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -996,7 +1016,8 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1034,8 +1055,11 @@ export default function CustomerManagementPage() {
|
||||
const isEditingExisting = !!editItemData;
|
||||
setSaving(true);
|
||||
try {
|
||||
const processedKeys = new Set<string>();
|
||||
for (const item of selectedItemsForDetail) {
|
||||
const itemKey = item.item_number || item.id;
|
||||
if (processedKeys.has(itemKey)) continue;
|
||||
processedKeys.add(itemKey);
|
||||
const mappingRows = itemMappings[itemKey] || [];
|
||||
|
||||
if (isEditingExisting && editItemData?.id) {
|
||||
@@ -1048,6 +1072,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1097,12 +1122,13 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1172,7 +1198,7 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1204,40 +1230,63 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 — 선택한 품목의 모든 매핑 + 단가에서 customer_id를 null 처리
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 거래처 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → customer_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → customer_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedCustomerId;
|
||||
setSelectedCustomerId(null);
|
||||
setTimeout(() => setSelectedCustomerId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2393,7 +2442,7 @@ export default function CustomerManagementPage() {
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> 조회</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[350px] border rounded-lg">
|
||||
<div className="overflow-auto h-[350px] border rounded-lg">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted h-10">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -813,7 +813,7 @@ export default function SupplierManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
@@ -834,7 +834,14 @@ export default function SupplierManagementPage() {
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실��간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
@@ -971,6 +978,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -991,7 +999,8 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1043,6 +1052,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1092,12 +1102,13 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1167,7 +1178,7 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1199,40 +1210,63 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 (소프트 삭제 — supplier_id를 null 처리)
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 공급업체 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → supplier_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → supplier_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedSupplierId;
|
||||
setSelectedSupplierId(null);
|
||||
setTimeout(() => setSelectedSupplierId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -814,14 +814,13 @@ export default function CustomerManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters },
|
||||
@@ -830,21 +829,41 @@ export default function CustomerManagementPage() {
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const kw = itemSearchKeyword.toLowerCase();
|
||||
const seenNumbers = new Set<string>();
|
||||
const deduped = allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
if (item.item_number && seenNumbers.has(item.item_number)) return false;
|
||||
if (item.item_number) seenNumbers.add(item.item_number);
|
||||
if (kw) {
|
||||
const name = (item.item_name || "").toLowerCase();
|
||||
const code = (item.item_number || "").toLowerCase();
|
||||
if (!name.includes(kw) && !code.includes(kw)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setItemSearchResults(deduped);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실시간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]);
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
const selected = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (selected.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const raw = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (raw.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const seenKeys = new Set<string>();
|
||||
const selected = raw.filter((i) => {
|
||||
const k = i.item_number || i.id;
|
||||
if (seenKeys.has(k)) return false;
|
||||
seenKeys.add(k);
|
||||
return true;
|
||||
});
|
||||
setSelectedItemsForDetail(selected);
|
||||
const mappings: typeof itemMappings = {};
|
||||
const prices: typeof itemPrices = {};
|
||||
@@ -976,6 +995,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -996,7 +1016,8 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1034,8 +1055,11 @@ export default function CustomerManagementPage() {
|
||||
const isEditingExisting = !!editItemData;
|
||||
setSaving(true);
|
||||
try {
|
||||
const processedKeys = new Set<string>();
|
||||
for (const item of selectedItemsForDetail) {
|
||||
const itemKey = item.item_number || item.id;
|
||||
if (processedKeys.has(itemKey)) continue;
|
||||
processedKeys.add(itemKey);
|
||||
const mappingRows = itemMappings[itemKey] || [];
|
||||
|
||||
if (isEditingExisting && editItemData?.id) {
|
||||
@@ -1048,6 +1072,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1097,12 +1122,13 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1172,7 +1198,7 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1204,40 +1230,63 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 — 선택한 품목의 모든 매핑 + 단가에서 customer_id를 null 처리
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 거래처 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → customer_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → customer_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedCustomerId;
|
||||
setSelectedCustomerId(null);
|
||||
setTimeout(() => setSelectedCustomerId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2393,7 +2442,7 @@ export default function CustomerManagementPage() {
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> 조회</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[350px] border rounded-lg">
|
||||
<div className="overflow-auto h-[350px] border rounded-lg">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted h-10">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -813,7 +813,7 @@ export default function SupplierManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
@@ -834,7 +834,14 @@ export default function SupplierManagementPage() {
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실��간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
@@ -971,6 +978,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -991,7 +999,8 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1043,6 +1052,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1092,12 +1102,13 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1167,7 +1178,7 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1199,40 +1210,63 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 (소프트 삭제 — supplier_id를 null 처리)
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 공급업체 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → supplier_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → supplier_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedSupplierId;
|
||||
setSelectedSupplierId(null);
|
||||
setTimeout(() => setSelectedSupplierId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -814,14 +814,13 @@ export default function CustomerManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters },
|
||||
@@ -830,21 +829,41 @@ export default function CustomerManagementPage() {
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const kw = itemSearchKeyword.toLowerCase();
|
||||
const seenNumbers = new Set<string>();
|
||||
const deduped = allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
if (item.item_number && seenNumbers.has(item.item_number)) return false;
|
||||
if (item.item_number) seenNumbers.add(item.item_number);
|
||||
if (kw) {
|
||||
const name = (item.item_name || "").toLowerCase();
|
||||
const code = (item.item_number || "").toLowerCase();
|
||||
if (!name.includes(kw) && !code.includes(kw)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setItemSearchResults(deduped);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실시간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]);
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
const selected = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (selected.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const raw = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (raw.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const seenKeys = new Set<string>();
|
||||
const selected = raw.filter((i) => {
|
||||
const k = i.item_number || i.id;
|
||||
if (seenKeys.has(k)) return false;
|
||||
seenKeys.add(k);
|
||||
return true;
|
||||
});
|
||||
setSelectedItemsForDetail(selected);
|
||||
const mappings: typeof itemMappings = {};
|
||||
const prices: typeof itemPrices = {};
|
||||
@@ -976,6 +995,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -996,7 +1016,8 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1034,8 +1055,11 @@ export default function CustomerManagementPage() {
|
||||
const isEditingExisting = !!editItemData;
|
||||
setSaving(true);
|
||||
try {
|
||||
const processedKeys = new Set<string>();
|
||||
for (const item of selectedItemsForDetail) {
|
||||
const itemKey = item.item_number || item.id;
|
||||
if (processedKeys.has(itemKey)) continue;
|
||||
processedKeys.add(itemKey);
|
||||
const mappingRows = itemMappings[itemKey] || [];
|
||||
|
||||
if (isEditingExisting && editItemData?.id) {
|
||||
@@ -1048,6 +1072,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1097,12 +1122,13 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1172,7 +1198,7 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1204,40 +1230,63 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 — 선택한 품목의 모든 매핑 + 단가에서 customer_id를 null 처리
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 거래처 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → customer_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → customer_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedCustomerId;
|
||||
setSelectedCustomerId(null);
|
||||
setTimeout(() => setSelectedCustomerId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2393,7 +2442,7 @@ export default function CustomerManagementPage() {
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> 조회</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[350px] border rounded-lg">
|
||||
<div className="overflow-auto h-[350px] border rounded-lg">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted h-10">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -813,7 +813,7 @@ export default function SupplierManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
@@ -834,7 +834,14 @@ export default function SupplierManagementPage() {
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실��간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
@@ -971,6 +978,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -991,7 +999,8 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1043,6 +1052,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1092,12 +1102,13 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1167,7 +1178,7 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1199,40 +1210,63 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 (소프트 삭제 — supplier_id를 null 처리)
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 공급업체 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → supplier_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → supplier_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedSupplierId;
|
||||
setSelectedSupplierId(null);
|
||||
setTimeout(() => setSelectedSupplierId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -814,14 +814,13 @@ export default function CustomerManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters },
|
||||
@@ -830,21 +829,41 @@ export default function CustomerManagementPage() {
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const kw = itemSearchKeyword.toLowerCase();
|
||||
const seenNumbers = new Set<string>();
|
||||
const deduped = allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
if (item.item_number && seenNumbers.has(item.item_number)) return false;
|
||||
if (item.item_number) seenNumbers.add(item.item_number);
|
||||
if (kw) {
|
||||
const name = (item.item_name || "").toLowerCase();
|
||||
const code = (item.item_number || "").toLowerCase();
|
||||
if (!name.includes(kw) && !code.includes(kw)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setItemSearchResults(deduped);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실시간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]);
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
const selected = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (selected.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const raw = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (raw.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const seenKeys = new Set<string>();
|
||||
const selected = raw.filter((i) => {
|
||||
const k = i.item_number || i.id;
|
||||
if (seenKeys.has(k)) return false;
|
||||
seenKeys.add(k);
|
||||
return true;
|
||||
});
|
||||
setSelectedItemsForDetail(selected);
|
||||
const mappings: typeof itemMappings = {};
|
||||
const prices: typeof itemPrices = {};
|
||||
@@ -976,6 +995,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -996,7 +1016,8 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1034,8 +1055,11 @@ export default function CustomerManagementPage() {
|
||||
const isEditingExisting = !!editItemData;
|
||||
setSaving(true);
|
||||
try {
|
||||
const processedKeys = new Set<string>();
|
||||
for (const item of selectedItemsForDetail) {
|
||||
const itemKey = item.item_number || item.id;
|
||||
if (processedKeys.has(itemKey)) continue;
|
||||
processedKeys.add(itemKey);
|
||||
const mappingRows = itemMappings[itemKey] || [];
|
||||
|
||||
if (isEditingExisting && editItemData?.id) {
|
||||
@@ -1048,6 +1072,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1097,12 +1122,13 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1172,7 +1198,7 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1204,40 +1230,63 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 — 선택한 품목의 모든 매핑 + 단가에서 customer_id를 null 처리
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 거래처 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → customer_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → customer_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedCustomerId;
|
||||
setSelectedCustomerId(null);
|
||||
setTimeout(() => setSelectedCustomerId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2393,7 +2442,7 @@ export default function CustomerManagementPage() {
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> 조회</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[350px] border rounded-lg">
|
||||
<div className="overflow-auto h-[350px] border rounded-lg">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted h-10">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -813,7 +813,7 @@ export default function SupplierManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
@@ -834,7 +834,14 @@ export default function SupplierManagementPage() {
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실��간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
@@ -971,6 +978,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -991,7 +999,8 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1043,6 +1052,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1092,12 +1102,13 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1167,7 +1178,7 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1199,40 +1210,63 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 (소프트 삭제 — supplier_id를 null 처리)
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 공급업체 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → supplier_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → supplier_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedSupplierId;
|
||||
setSelectedSupplierId(null);
|
||||
setTimeout(() => setSelectedSupplierId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -814,14 +814,13 @@ export default function CustomerManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters },
|
||||
@@ -830,21 +829,41 @@ export default function CustomerManagementPage() {
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const kw = itemSearchKeyword.toLowerCase();
|
||||
const seenNumbers = new Set<string>();
|
||||
const deduped = allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
if (item.item_number && seenNumbers.has(item.item_number)) return false;
|
||||
if (item.item_number) seenNumbers.add(item.item_number);
|
||||
if (kw) {
|
||||
const name = (item.item_name || "").toLowerCase();
|
||||
const code = (item.item_number || "").toLowerCase();
|
||||
if (!name.includes(kw) && !code.includes(kw)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setItemSearchResults(deduped);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실시간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]);
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
const selected = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (selected.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const raw = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (raw.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const seenKeys = new Set<string>();
|
||||
const selected = raw.filter((i) => {
|
||||
const k = i.item_number || i.id;
|
||||
if (seenKeys.has(k)) return false;
|
||||
seenKeys.add(k);
|
||||
return true;
|
||||
});
|
||||
setSelectedItemsForDetail(selected);
|
||||
const mappings: typeof itemMappings = {};
|
||||
const prices: typeof itemPrices = {};
|
||||
@@ -976,6 +995,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -996,7 +1016,8 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1034,8 +1055,11 @@ export default function CustomerManagementPage() {
|
||||
const isEditingExisting = !!editItemData;
|
||||
setSaving(true);
|
||||
try {
|
||||
const processedKeys = new Set<string>();
|
||||
for (const item of selectedItemsForDetail) {
|
||||
const itemKey = item.item_number || item.id;
|
||||
if (processedKeys.has(itemKey)) continue;
|
||||
processedKeys.add(itemKey);
|
||||
const mappingRows = itemMappings[itemKey] || [];
|
||||
|
||||
if (isEditingExisting && editItemData?.id) {
|
||||
@@ -1048,6 +1072,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1097,12 +1122,13 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1172,7 +1198,7 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1204,40 +1230,63 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 — 선택한 품목의 모든 매핑 + 단가에서 customer_id를 null 처리
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 거래처 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → customer_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → customer_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedCustomerId;
|
||||
setSelectedCustomerId(null);
|
||||
setTimeout(() => setSelectedCustomerId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2393,7 +2442,7 @@ export default function CustomerManagementPage() {
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> 조회</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[350px] border rounded-lg">
|
||||
<div className="overflow-auto h-[350px] border rounded-lg">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted h-10">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -813,7 +813,7 @@ export default function SupplierManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
@@ -834,7 +834,14 @@ export default function SupplierManagementPage() {
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실��간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
@@ -971,6 +978,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -991,7 +999,8 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1043,6 +1052,7 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1092,12 +1102,13 @@ export default function SupplierManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1167,7 +1178,7 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1199,40 +1210,63 @@ export default function SupplierManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 (소프트 삭제 — supplier_id를 null 처리)
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 공급업체 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → supplier_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → supplier_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "supplier_id", operator: "equals", value: selectedSupplier!.supplier_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { supplier_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedSupplierId;
|
||||
setSelectedSupplierId(null);
|
||||
setTimeout(() => setSelectedSupplierId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -814,14 +814,13 @@ export default function CustomerManagementPage() {
|
||||
};
|
||||
|
||||
// 품목 검색
|
||||
const searchItems = async () => {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters },
|
||||
@@ -830,21 +829,41 @@ export default function CustomerManagementPage() {
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const kw = itemSearchKeyword.toLowerCase();
|
||||
const seenNumbers = new Set<string>();
|
||||
const deduped = allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
if (item.item_number && seenNumbers.has(item.item_number)) return false;
|
||||
if (item.item_number) seenNumbers.add(item.item_number);
|
||||
if (kw) {
|
||||
const name = (item.item_name || "").toLowerCase();
|
||||
const code = (item.item_number || "").toLowerCase();
|
||||
if (!name.includes(kw) && !code.includes(kw)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setItemSearchResults(deduped);
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
|
||||
// 실시간 검색 (2글자 이상)
|
||||
useEffect(() => {
|
||||
if (!itemSelectOpen) return;
|
||||
if (itemSearchKeyword.length > 0 && itemSearchKeyword.length < 2) return;
|
||||
searchItems();
|
||||
}, [itemSearchKeyword, itemSelectOpen]);
|
||||
|
||||
// 품목 선택 완료 → 상세 입력 모달로 전환
|
||||
const goToItemDetail = () => {
|
||||
const selected = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (selected.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const raw = itemSearchResults.filter((i) => itemCheckedIds.has(i.id));
|
||||
if (raw.length === 0) { toast.error("품목을 선택해주세요."); return; }
|
||||
const seenKeys = new Set<string>();
|
||||
const selected = raw.filter((i) => {
|
||||
const k = i.item_number || i.id;
|
||||
if (seenKeys.has(k)) return false;
|
||||
seenKeys.add(k);
|
||||
return true;
|
||||
});
|
||||
setSelectedItemsForDetail(selected);
|
||||
const mappings: typeof itemMappings = {};
|
||||
const prices: typeof itemPrices = {};
|
||||
@@ -976,6 +995,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
mappingRows = allMappings
|
||||
@@ -996,7 +1016,8 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
priceRows = allPriceData.map((p: any) => ({
|
||||
_id: `p_existing_${p.id}`,
|
||||
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
|
||||
@@ -1034,8 +1055,11 @@ export default function CustomerManagementPage() {
|
||||
const isEditingExisting = !!editItemData;
|
||||
setSaving(true);
|
||||
try {
|
||||
const processedKeys = new Set<string>();
|
||||
for (const item of selectedItemsForDetail) {
|
||||
const itemKey = item.item_number || item.id;
|
||||
if (processedKeys.has(itemKey)) continue;
|
||||
processedKeys.add(itemKey);
|
||||
const mappingRows = itemMappings[itemKey] || [];
|
||||
|
||||
if (isEditingExisting && editItemData?.id) {
|
||||
@@ -1048,6 +1072,7 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
sort: { columnName: "created_date", order: "asc" },
|
||||
});
|
||||
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
|
||||
} catch { /* skip */ }
|
||||
@@ -1097,12 +1122,13 @@ export default function CustomerManagementPage() {
|
||||
{ columnName: "item_id", operator: "equals", value: itemKey },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
|
||||
existingPriceRows = (existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [])
|
||||
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
|
||||
} catch { /* skip */ }
|
||||
|
||||
// 단가 upsert
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
const usedPriceIds = new Set<string>();
|
||||
for (let pi = 0; pi < priceRows.length; pi++) {
|
||||
@@ -1172,7 +1198,7 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
|
||||
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
|
||||
(p.base_price && Number(p.base_price) > 0) || p.start_date
|
||||
p.base_price || p.start_date || p.currency_code || p.base_price_type
|
||||
);
|
||||
for (const price of priceRows) {
|
||||
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
|
||||
@@ -1204,40 +1230,63 @@ export default function CustomerManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 품목 매핑 삭제
|
||||
// 품목 매핑 해제 — 선택한 품목의 모든 매핑 + 단가에서 customer_id를 null 처리
|
||||
const handlePriceItemDelete = async () => {
|
||||
if (priceCheckedIds.length === 0) return;
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, {
|
||||
description: "관련된 단가 정보도 함께 삭제됩니다.",
|
||||
variant: "destructive", confirmText: "삭제",
|
||||
const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목의 연결을 해제하시겠습니까?`, {
|
||||
description: "해당 품목의 거래처 연결이 해제됩니다. (데이터는 유지)",
|
||||
variant: "destructive", confirmText: "해제",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
for (const mappingId of priceCheckedIds) {
|
||||
const itemIds = priceCheckedIds.map((mid) => {
|
||||
const group = Object.values(priceGroups).find((g) => g.master.id === mid);
|
||||
return group?.master.item_id || group?.master.item_number || "";
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const itemId of itemIds) {
|
||||
// 해당 품목의 모든 매핑 조회 → customer_id null 처리
|
||||
const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
|
||||
for (const m of allMappings) {
|
||||
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
|
||||
originalData: { id: m.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
|
||||
// 해당 품목의 모든 단가 조회 → customer_id null 처리
|
||||
try {
|
||||
const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] },
|
||||
autoFilter: true,
|
||||
dataFilter: { enabled: true, filters: [
|
||||
{ columnName: "customer_id", operator: "equals", value: selectedCustomer!.customer_code },
|
||||
{ columnName: "item_id", operator: "equals", value: itemId },
|
||||
]}, autoFilter: true,
|
||||
});
|
||||
const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
|
||||
if (prices.length > 0) {
|
||||
await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, {
|
||||
data: prices.map((p: any) => ({ id: p.id })),
|
||||
for (const p of prices) {
|
||||
await apiClient.put(`/table-management/tables/${PRICE_TABLE}/edit`, {
|
||||
originalData: { id: p.id },
|
||||
updatedData: { customer_id: null },
|
||||
});
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
|
||||
data: priceCheckedIds.map((id) => ({ id })),
|
||||
});
|
||||
toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`);
|
||||
|
||||
toast.success(`${priceCheckedIds.length}개 품목의 연결이 해제되었습니다.`);
|
||||
setPriceCheckedIds([]);
|
||||
const cid = selectedCustomerId;
|
||||
setSelectedCustomerId(null);
|
||||
setTimeout(() => setSelectedCustomerId(cid), 50);
|
||||
} catch {
|
||||
toast.error("삭제에 실패했습니다.");
|
||||
toast.error("연결 해제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2393,7 +2442,7 @@ export default function CustomerManagementPage() {
|
||||
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> 조회</>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-auto max-h-[350px] border rounded-lg">
|
||||
<div className="overflow-auto h-[350px] border rounded-lg">
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted h-10">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user