This commit is contained in:
DDD1542
2026-04-07 11:52:13 +09:00
parent 00b388c3ab
commit 77d5b52265
4 changed files with 1183 additions and 675 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -127,6 +127,7 @@ export default function SupplierManagementPage() {
const [itemSelectOpen, setItemSelectOpen] = useState(false);
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
const [itemSearchResults, setItemSearchResults] = useState<any[]>([]);
const [itemTotalCount, setItemTotalCount] = useState(0);
const [itemSearchLoading, setItemSearchLoading] = useState(false);
const [itemCheckedIds, setItemCheckedIds] = useState<Set<string>>(new Set());
@@ -817,12 +818,13 @@ export default function SupplierManagementPage() {
autoFilter: true,
});
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 PURCHASE_CODE = "CAT_MMDJB7R4_TO3T";
const PURCHASE_CODES = ["s"]; // 구매관리 카테고리 코드
setItemSearchResults(allItems.filter((item: any) => {
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
const div = item.division || "";
return div.includes(PURCHASE_CODE) || div.includes("구매");
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
}));
} catch { /* skip */ } finally { setItemSearchLoading(false); }
};
@@ -911,27 +913,27 @@ export default function SupplierManagementPage() {
[itemKey]: (prev[itemKey] || []).map((r) => {
if (r._id !== rowId) return r;
const updated = { ...r, [field]: value };
if (["base_price", "discount_type", "discount_value", "rounding_unit_value"].includes(field)) {
if (["base_price", "discount_type", "discount_value", "rounding_unit_value", "rounding_type"].includes(field)) {
const bp = Number(updated.base_price) || 0;
const dv = Number(updated.discount_value) || 0;
const dt = updated.discount_type;
let calc = bp;
if (dt === "CAT_MLAMBEC8_URQA") calc = bp * (1 - dv / 100);
else if (dt === "CAT_MLAMBLFM_JTLO") calc = bp - dv;
// 절삭/반올림 적용
// 반올림 유형 + 단위 적용
const rv = updated.rounding_unit_value;
const rt = updated.rounding_type;
const roundOpts = priceCategoryOptions["rounding_unit_value"] || [];
const roundLabel = roundOpts.find((o) => o.code === rv)?.label || "";
if (roundLabel.includes("절삭") || roundLabel.includes("버림") || roundLabel.includes("floor")) {
calc = Math.floor(calc);
} else if (roundLabel.includes("올림") || roundLabel.includes("ceil")) {
calc = Math.ceil(calc);
} else if (roundLabel.includes("반올림") || roundLabel.includes("round")) {
calc = Math.round(calc);
} else if (rv) {
// 단위 값 기반 (예: 10원 단위 절삭)
const unit = Number(rv);
if (!isNaN(unit) && unit > 0) calc = Math.floor(calc / unit) * unit;
const unitOpts = priceCategoryOptions["rounding_type"] || [];
const unitLabel = unitOpts.find((o) => o.code === rt)?.label || "";
const unit = parseInt(unitLabel) || 1; // "10원" → 10, "100원" → 100
if (roundLabel === "반올림") {
calc = Math.round(calc / unit) * unit;
} else if (roundLabel === "절삭") {
calc = Math.floor(calc / unit) * unit;
} else if (roundLabel === "올림") {
calc = Math.ceil(calc / unit) * unit;
}
updated.calculated_price = String(Math.floor(calc));
}
@@ -2370,7 +2372,7 @@ export default function SupplierManagementPage() {
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> .</DialogDescription>
<DialogDescription> . (: {itemTotalCount} / : {itemSearchResults.length}{itemCheckedIds.size > 0 ? ` / 선택: ${itemCheckedIds.size}` : ""})</DialogDescription>
</DialogHeader>
<div className="flex gap-2 mb-3">
<Input
@@ -2616,66 +2618,74 @@ export default function SupplierManagementPage() {
</Select>
</div>
</div>
{/* 기준가/할인/반올림 */}
<div className="flex gap-2 items-center">
<div className="w-[90px]">
<Select
value={price.base_price_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 기준유형 + 기준가 */}
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
<Select
value={price.base_price_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준유형" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={price.base_price ? Number(price.base_price).toLocaleString() : ""}
onChange={(e) => {
const raw = e.target.value.replace(/[^\d.-]/g, "");
updatePriceRow(itemKey, price._id, "base_price", raw);
}}
className="h-9 text-[13px] text-right flex-1"
className="h-9 text-[13px] text-right col-span-3"
placeholder="기준가"
/>
<div className="w-[90px]">
<Select
value={price.discount_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{(priceCategoryOptions["discount_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 할인 + 반올림 */}
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
<Select
value={price.discount_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{(priceCategoryOptions["discount_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={price.discount_value ? Number(price.discount_value).toLocaleString() : ""}
onChange={(e) => {
const raw = e.target.value.replace(/[^\d.-]/g, "");
updatePriceRow(itemKey, price._id, "discount_value", raw);
}}
className="h-9 text-[13px] text-right w-[60px]"
className="h-9 text-[13px] text-right"
placeholder="0"
/>
<div className="w-[90px]">
<Select
value={price.rounding_unit_value}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select
value={price.rounding_unit_value}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={price.rounding_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 계산 단가 */}
<div className="flex items-center justify-end gap-1.5 pt-2 border-t">

View File

@@ -127,6 +127,7 @@ export default function CustomerManagementPage() {
const [itemSelectOpen, setItemSelectOpen] = useState(false);
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
const [itemSearchResults, setItemSearchResults] = useState<any[]>([]);
const [itemTotalCount, setItemTotalCount] = useState(0);
const [itemSearchLoading, setItemSearchLoading] = useState(false);
const [itemCheckedIds, setItemCheckedIds] = useState<Set<string>>(new Set());
@@ -817,12 +818,13 @@ export default function CustomerManagementPage() {
autoFilter: true,
});
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 SALES_CODE = "CAT_ML8ZFVEL_1TOR";
const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드
setItemSearchResults(allItems.filter((item: any) => {
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
const div = item.division || "";
return div.includes(SALES_CODE) || div.includes("영업");
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
return divCodes.some((code: string) => SALES_CODES.includes(code));
}));
} catch { /* skip */ } finally { setItemSearchLoading(false); }
};
@@ -911,27 +913,27 @@ export default function CustomerManagementPage() {
[itemKey]: (prev[itemKey] || []).map((r) => {
if (r._id !== rowId) return r;
const updated = { ...r, [field]: value };
if (["base_price", "discount_type", "discount_value", "rounding_unit_value"].includes(field)) {
if (["base_price", "discount_type", "discount_value", "rounding_unit_value", "rounding_type"].includes(field)) {
const bp = Number(updated.base_price) || 0;
const dv = Number(updated.discount_value) || 0;
const dt = updated.discount_type;
let calc = bp;
if (dt === "CAT_MLAMBEC8_URQA") calc = bp * (1 - dv / 100);
else if (dt === "CAT_MLAMBLFM_JTLO") calc = bp - dv;
// 절삭/반올림 적용
// 반올림 유형 + 단위 적용
const rv = updated.rounding_unit_value;
const rt = updated.rounding_type;
const roundOpts = priceCategoryOptions["rounding_unit_value"] || [];
const roundLabel = roundOpts.find((o) => o.code === rv)?.label || "";
if (roundLabel.includes("절삭") || roundLabel.includes("버림") || roundLabel.includes("floor")) {
calc = Math.floor(calc);
} else if (roundLabel.includes("올림") || roundLabel.includes("ceil")) {
calc = Math.ceil(calc);
} else if (roundLabel.includes("반올림") || roundLabel.includes("round")) {
calc = Math.round(calc);
} else if (rv) {
// 단위 값 기반 (예: 10원 단위 절삭)
const unit = Number(rv);
if (!isNaN(unit) && unit > 0) calc = Math.floor(calc / unit) * unit;
const unitOpts = priceCategoryOptions["rounding_type"] || [];
const unitLabel = unitOpts.find((o) => o.code === rt)?.label || "";
const unit = parseInt(unitLabel) || 1; // "10원" → 10, "100원" → 100
if (roundLabel === "반올림") {
calc = Math.round(calc / unit) * unit;
} else if (roundLabel === "절삭") {
calc = Math.floor(calc / unit) * unit;
} else if (roundLabel === "올림") {
calc = Math.ceil(calc / unit) * unit;
}
updated.calculated_price = String(Math.floor(calc));
}
@@ -2368,7 +2370,7 @@ export default function CustomerManagementPage() {
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> .</DialogDescription>
<DialogDescription> . (: {itemTotalCount} / : {itemSearchResults.length}{itemCheckedIds.size > 0 ? ` / 선택: ${itemCheckedIds.size}` : ""})</DialogDescription>
</DialogHeader>
<div className="flex gap-2 mb-3">
<Input
@@ -2614,66 +2616,74 @@ export default function CustomerManagementPage() {
</Select>
</div>
</div>
{/* 기준가/할인/반올림 */}
<div className="flex gap-2 items-center">
<div className="w-[90px]">
<Select
value={price.base_price_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 기준유형 + 기준가 */}
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
<Select
value={price.base_price_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준유형" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={price.base_price ? Number(price.base_price).toLocaleString() : ""}
onChange={(e) => {
const raw = e.target.value.replace(/[^\d.-]/g, "");
updatePriceRow(itemKey, price._id, "base_price", raw);
}}
className="h-9 text-[13px] text-right flex-1"
className="h-9 text-[13px] text-right col-span-3"
placeholder="기준가"
/>
<div className="w-[90px]">
<Select
value={price.discount_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{(priceCategoryOptions["discount_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 할인 + 반올림 */}
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
<Select
value={price.discount_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{(priceCategoryOptions["discount_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={price.discount_value ? Number(price.discount_value).toLocaleString() : ""}
onChange={(e) => {
const raw = e.target.value.replace(/[^\d.-]/g, "");
updatePriceRow(itemKey, price._id, "discount_value", raw);
}}
className="h-9 text-[13px] text-right w-[60px]"
className="h-9 text-[13px] text-right"
placeholder="0"
/>
<div className="w-[90px]">
<Select
value={price.rounding_unit_value}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select
value={price.rounding_unit_value}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={price.rounding_type}
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_type", v)}
>
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 계산 단가 */}
<div className="flex items-center justify-end gap-1.5 pt-2 border-t">

View File

@@ -13,12 +13,13 @@ import React, { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Users, Search, X, Settings2 } from "lucide-react";
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Users, Package, Search, X, Settings2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { useAuth } from "@/hooks/useAuth";
@@ -55,7 +56,7 @@ const ITEM_GRID_COLUMNS = [
export default function SalesItemPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
const { confirm, ConfirmDialogComponent, isConfirmOpenRef } = useConfirmDialog();
const ts = useTableSettings("c16-sales-item", ITEM_TABLE, ITEM_GRID_COLUMNS);
// 좌측: 품목
@@ -361,14 +362,25 @@ export default function SalesItemPage() {
[custKey]: (prev[custKey] || []).map((r) => {
if (r._id !== rowId) return r;
const updated = { ...r, [field]: value };
if (["base_price", "discount_type", "discount_value"].includes(field)) {
if (["base_price", "discount_type", "discount_value", "rounding_unit_value", "rounding_type"].includes(field)) {
const bp = Number(updated.base_price) || 0;
const dv = Number(updated.discount_value) || 0;
const dt = updated.discount_type;
let calc = bp;
if (dt === "CAT_MLAMBEC8_URQA") calc = bp * (1 - dv / 100);
else if (dt === "CAT_MLAMBLFM_JTLO") calc = bp - dv;
updated.calculated_price = String(Math.round(calc));
// 반올림 유형 + 단위 적용
const rv = updated.rounding_unit_value;
const rt = updated.rounding_type;
const roundOpts = priceCategoryOptions["rounding_unit_value"] || [];
const roundLabel = roundOpts.find((o) => o.code === rv)?.label || "";
const unitOpts = priceCategoryOptions["rounding_type"] || [];
const unitLabel = unitOpts.find((o) => o.code === rt)?.label || "";
const unit = parseInt(unitLabel) || 1;
if (roundLabel === "반올림") calc = Math.round(calc / unit) * unit;
else if (roundLabel === "절삭") calc = Math.floor(calc / unit) * unit;
else if (roundLabel === "올림") calc = Math.ceil(calc / unit) * unit;
updated.calculated_price = String(Math.floor(calc));
}
return updated;
}),
@@ -619,8 +631,7 @@ export default function SalesItemPage() {
];
return (
<div className="flex h-full flex-col gap-3 p-3">
<div className="flex h-full flex-col gap-3 p-4">
{/* 검색 필터 (DynamicSearchFilter) */}
<DynamicSearchFilter
tableName={ITEM_TABLE}
@@ -628,40 +639,48 @@ export default function SalesItemPage() {
onFilterChange={setSearchFilters}
dataCount={items.length}
externalFilterConfig={ts.filterConfig}
extraActions={
<div className="flex gap-1.5">
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5" />
</Button>
<Button variant="outline" size="sm" className="h-8" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5" />
</Button>
</div>
}
/>
{/* ── 마스터-디테일 분할 패널 ── */}
<div className="flex-1 overflow-hidden border rounded-lg bg-background">
{/* 액션 버튼 영역 */}
<div className="flex items-center gap-2 px-4 shrink-0">
<div className="flex gap-1.5 ml-auto">
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
<FileSpreadsheet className="w-3.5 h-3.5 mr-1" />
</Button>
<Button variant="outline" size="sm" className="h-8" onClick={handleExcelDownload}>
<Download className="w-3.5 h-3.5 mr-1" />
</Button>
</div>
</div>
{/* 마스터-디테일 분할 패널 */}
<div className="flex-1 overflow-hidden border rounded-lg bg-background shadow-sm">
<ResizablePanelGroup direction="horizontal">
{/* 좌측: 판매품목 목록 (마스터) */}
{/* 좌측: 판매품목 목록 */}
<ResizablePanel defaultSize={55} minSize={30}>
<div className="flex flex-col h-full bg-muted">
<div className="flex flex-col h-full">
{/* 패널 헤더 */}
<div className="flex items-center justify-between px-4 py-2.5 border-b bg-muted shrink-0">
<div className="flex items-center gap-2">
<span className="text-[13px] font-bold text-foreground"> </span>
<span className="text-[11px] font-semibold text-primary bg-primary/10 px-2 py-0.5 rounded-full">
<div className="flex items-center justify-between px-4 h-[42px] border-b bg-muted shrink-0">
<div className="flex items-center gap-2.5">
<span className="text-[13px] font-bold"> </span>
<span className="text-[11px] font-semibold text-primary bg-primary/[0.08] px-2 py-0.5 rounded-full">
{itemCount}
</span>
</div>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => ts.setOpen(true)}>
<Settings2 className="w-3.5 h-3.5" />
</Button>
<div className="flex items-center gap-1.5">
<Button variant="outline" size="sm" disabled={!selectedItemId} onClick={openEditItem}>
<Pencil className="w-3.5 h-3.5 mr-1" />
</Button>
<Button variant="outline" size="sm" onClick={() => ts.setOpen(true)}>
<Settings2 className="w-3.5 h-3.5" />
</Button>
</div>
</div>
{/* 테이블 영역 */}
{/* 거래처 테이블 */}
<EDataTable
columns={itemColumns}
data={ts.groupData(items)}
@@ -672,7 +691,8 @@ export default function SalesItemPage() {
onSelect={(id) => setSelectedItemId(id)}
onRowDoubleClick={() => openEditItem()}
showRowNumber
showPagination={false}
showPagination
defaultPageSize={20}
draggableColumns={false}
columnOrderKey="c16-sales-item"
/>
@@ -681,69 +701,49 @@ export default function SalesItemPage() {
<ResizableHandle withHandle />
{/* 우측: 거래처 정보 (디테일) */}
{/* 우측: 디테일 패널 */}
<ResizablePanel defaultSize={45} minSize={25}>
<div className="flex flex-col h-full bg-muted">
<div className="flex flex-col h-full">
{!selectedItemId ? (
/* 빈 상태 */
<div className="flex-1 flex items-center justify-center m-5">
<div className="flex flex-col items-center gap-4 border-2 border-dashed border-border rounded-lg p-10 text-center">
<Users className="w-12 h-12 text-muted-foreground/40" />
<div>
<div className="text-sm font-semibold text-muted-foreground"> </div>
<div className="text-xs text-muted-foreground mt-1"> </div>
</div>
<div className="flex-1 flex items-center justify-center p-5">
<div className="flex flex-col items-center justify-center text-center border-2 border-dashed border-border rounded-lg px-10 py-16">
<Package className="w-12 h-12 text-muted-foreground/40 mb-4" />
<div className="text-sm font-semibold text-muted-foreground mb-1.5"> </div>
<div className="text-xs text-muted-foreground"> </div>
</div>
</div>
) : (
<>
{/* 선택 품목 상세 헤더 */}
<div className="flex items-center justify-between px-4 py-2.5 border-b bg-muted shrink-0">
<div className="flex items-center gap-2 min-w-0">
<span className="text-[13px] font-bold text-foreground truncate">{selectedItem?.item_name || "-"}</span>
<span className="text-[11px] font-mono text-muted-foreground bg-muted-foreground/10 px-2 py-0.5 rounded-full shrink-0">
{selectedItem?.item_number || ""}
</span>
</div>
<Button variant="outline" size="sm" className="h-8 shrink-0" onClick={openEditItem}>
<Pencil className="w-3.5 h-3.5" />
</Button>
</div>
{/* 거래처별 단가 서브헤더 */}
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted shrink-0">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground"> </span>
<span className="text-[11px] font-semibold text-primary bg-primary/10 px-1.5 py-0.5 rounded-full">
{customerItems.length}
</span>
{/* 거래처별 단가 헤더 */}
<div className="flex items-center justify-between h-[42px] border-b bg-muted shrink-0 pr-3">
<div className="flex items-center gap-2.5 px-4">
<Users className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs font-semibold"> </span>
{customerItems.length > 0 && (
<Badge variant="secondary" className="ml-0.5 text-[10px] px-1.5 py-0">{customerItems.length}</Badge>
)}
</div>
<div className="flex gap-1.5">
<Button
variant="default"
size="sm"
className="h-7 text-xs"
onClick={() => { setCustCheckedIds(new Set()); setCustSelectOpen(true); searchCustomers(); }}
>
<Plus className="w-3.5 h-3.5" />
<Plus className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
variant="destructive"
size="sm"
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
disabled={customerCheckedIds.length === 0}
onClick={handleCustomerMappingDelete}
>
<Trash2 className="w-3.5 h-3.5" />
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</div>
{/* 거래처 테이블 */}
<div className="flex-1 overflow-auto">
<div className="flex-1 min-h-0 overflow-auto pt-px">
{customerLoading ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
@@ -755,8 +755,8 @@ export default function SalesItemPage() {
) : (
<Table noWrapper>
<TableHeader className="sticky top-0 z-10">
<TableRow className="bg-muted hover:bg-muted">
<TableHead className="sticky top-0 z-10 bg-card w-[36px] text-center">
<TableRow className="bg-muted hover:bg-muted h-10">
<TableHead className="w-[40px] text-center px-2">
<Checkbox
checked={customerItems.length > 0 && customerCheckedIds.length === customerItems.length}
onCheckedChange={(checked) => {
@@ -765,13 +765,13 @@ export default function SalesItemPage() {
}}
/>
</TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[110px]"></TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground min-w-[120px]"></TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[100px]"></TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[100px]"></TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[80px] text-right"></TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[80px] text-right"></TableHead>
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[50px]"></TableHead>
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -779,12 +779,20 @@ export default function SalesItemPage() {
<TableRow
key={row.id}
className={cn(
"cursor-pointer transition-all",
"cursor-pointer h-[41px]",
customerCheckedIds.includes(row.id) ? "bg-primary/[0.08]" : "hover:bg-accent"
)}
onDoubleClick={() => openEditCust(row)}
>
<TableCell className="text-center">
<TableCell
className="text-center px-2"
onClick={(e) => {
e.stopPropagation();
setCustomerCheckedIds((prev) =>
prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id]
);
}}
>
<Checkbox
checked={customerCheckedIds.includes(row.id)}
onCheckedChange={(checked) => {
@@ -1104,66 +1112,74 @@ export default function SalesItemPage() {
</Select>
</div>
</div>
{/* 기준가 + 할인 + 반올림 */}
<div className="flex gap-2 items-center">
<div className="w-[90px]">
<Select
value={price.base_price_type}
onValueChange={(v) => updatePriceRow(custKey, price._id, "base_price_type", v)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="기준" />
</SelectTrigger>
<SelectContent>
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 기준유형 + 기준가 */}
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
<Select
value={price.base_price_type}
onValueChange={(v) => updatePriceRow(custKey, price._id, "base_price_type", v)}
>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="기준유형" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={price.base_price}
onChange={(e) => updatePriceRow(custKey, price._id, "base_price", e.target.value)}
className="h-8 text-xs text-right flex-1"
value={price.base_price ? Number(price.base_price).toLocaleString() : ""}
onChange={(e) => {
const raw = e.target.value.replace(/[^\d.-]/g, "");
updatePriceRow(custKey, price._id, "base_price", raw);
}}
className="h-8 text-xs text-right col-span-3"
placeholder="기준가"
/>
<div className="w-[90px]">
<Select
value={price.discount_type}
onValueChange={(v) => updatePriceRow(custKey, price._id, "discount_type", v)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="할인" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{(priceCategoryOptions["discount_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 할인 + 반올림 */}
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
<Select
value={price.discount_type}
onValueChange={(v) => updatePriceRow(custKey, price._id, "discount_type", v)}
>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="할인" /></SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{(priceCategoryOptions["discount_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Input
value={price.discount_value}
onChange={(e) => updatePriceRow(custKey, price._id, "discount_value", e.target.value)}
className="h-8 text-xs text-right w-[60px]"
value={price.discount_value ? Number(price.discount_value).toLocaleString() : ""}
onChange={(e) => {
const raw = e.target.value.replace(/[^\d.-]/g, "");
updatePriceRow(custKey, price._id, "discount_value", raw);
}}
className="h-8 text-xs text-right"
placeholder="0"
/>
<div className="w-[90px]">
<Select
value={price.rounding_unit_value}
onValueChange={(v) => updatePriceRow(custKey, price._id, "rounding_unit_value", v)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="반올림" />
</SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Select
value={price.rounding_unit_value}
onValueChange={(v) => updatePriceRow(custKey, price._id, "rounding_unit_value", v)}
>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="반올림" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={price.rounding_type}
onValueChange={(v) => updatePriceRow(custKey, price._id, "rounding_type", v)}
>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
<SelectContent>
{(priceCategoryOptions["rounding_type"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 계산 단가 */}
<div className="flex items-center justify-end gap-2 pt-1 border-t">