Merge pull request 'jskim-node' (#20) from jskim-node into main
Reviewed-on: https://g.wace.me/jskim/vexplor_dev/pulls/20
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
||||
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [serialModalOpen, setSerialModalOpen] = useState(false);
|
||||
const [serialForm, setSerialForm] = useState<Record<string, any>>({});
|
||||
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
|
||||
setMoldModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setMoldImagePreview(result);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
// 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
|
||||
|
||||
const handleSaveMold = async () => {
|
||||
// 등록 모드에서 채번이 없으면 수동 입력 필수
|
||||
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
|
||||
)}>
|
||||
{mold.image_path ? (
|
||||
<img
|
||||
src={mold.image_path}
|
||||
src={String(mold.image_path).startsWith("http") || String(mold.image_path).startsWith("/") ? mold.image_path : `/api/files/preview/${mold.image_path}`}
|
||||
alt={mold.mold_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="w-8 h-8 text-muted-foreground/50" />
|
||||
@@ -662,9 +654,10 @@ export default function MoldInfoPage() {
|
||||
<div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
|
||||
{selectedMold.image_path ? (
|
||||
<img
|
||||
src={selectedMold.image_path}
|
||||
src={String(selectedMold.image_path).startsWith("http") || String(selectedMold.image_path).startsWith("/") ? selectedMold.image_path : `/api/files/preview/${selectedMold.image_path}`}
|
||||
alt={selectedMold.mold_name}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-16 h-16 text-muted-foreground/40" />
|
||||
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-xs">금형 이미지</Label>
|
||||
<div className="relative w-full h-32 rounded-lg border-2 border-dashed border-border bg-muted/50 flex items-center justify-center overflow-hidden group">
|
||||
{moldImagePreview ? (
|
||||
<>
|
||||
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" />
|
||||
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}>
|
||||
<Upload className="w-3 h-3 mr-1" /> 변경
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" className="h-7 text-xs" type="button" onClick={() => {
|
||||
setMoldImagePreview(null);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: null }));
|
||||
}}>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-1.5 text-muted-foreground"
|
||||
onClick={() => moldImageRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
<span className="text-xs">이미지 업로드</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={moldImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleMoldImageUpload}
|
||||
<ImageUpload
|
||||
value={moldForm.image_path || ""}
|
||||
onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
|
||||
tableName="mold_mng"
|
||||
recordId={moldForm.id || ""}
|
||||
columnName="image_path"
|
||||
height="h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/division/values`);
|
||||
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -1181,8 +1183,8 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_number}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -820,11 +826,12 @@ export default function SupplierManagementPage() {
|
||||
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_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
const purchaseCode = categoryOptions["item_division"]?.find((o) => o.label === "구매관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
if (!purchaseCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -2430,8 +2437,8 @@ export default function SupplierManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2473,7 +2480,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -811,7 +817,7 @@ export default function CustomerManagementPage() {
|
||||
const searchItems = async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
@@ -2434,8 +2440,8 @@ export default function CustomerManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2477,7 +2483,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
||||
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [serialModalOpen, setSerialModalOpen] = useState(false);
|
||||
const [serialForm, setSerialForm] = useState<Record<string, any>>({});
|
||||
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
|
||||
setMoldModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setMoldImagePreview(result);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
// 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
|
||||
|
||||
const handleSaveMold = async () => {
|
||||
// 등록 모드에서 채번이 없으면 수동 입력 필수
|
||||
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
|
||||
)}>
|
||||
{mold.image_path ? (
|
||||
<img
|
||||
src={mold.image_path}
|
||||
src={String(mold.image_path).startsWith("http") || String(mold.image_path).startsWith("/") ? mold.image_path : `/api/files/preview/${mold.image_path}`}
|
||||
alt={mold.mold_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="w-8 h-8 text-muted-foreground/50" />
|
||||
@@ -662,9 +654,10 @@ export default function MoldInfoPage() {
|
||||
<div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
|
||||
{selectedMold.image_path ? (
|
||||
<img
|
||||
src={selectedMold.image_path}
|
||||
src={String(selectedMold.image_path).startsWith("http") || String(selectedMold.image_path).startsWith("/") ? selectedMold.image_path : `/api/files/preview/${selectedMold.image_path}`}
|
||||
alt={selectedMold.mold_name}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-16 h-16 text-muted-foreground/40" />
|
||||
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-xs">금형 이미지</Label>
|
||||
<div className="relative w-full h-32 rounded-lg border-2 border-dashed border-border bg-muted/50 flex items-center justify-center overflow-hidden group">
|
||||
{moldImagePreview ? (
|
||||
<>
|
||||
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" />
|
||||
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}>
|
||||
<Upload className="w-3 h-3 mr-1" /> 변경
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" className="h-7 text-xs" type="button" onClick={() => {
|
||||
setMoldImagePreview(null);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: null }));
|
||||
}}>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-1.5 text-muted-foreground"
|
||||
onClick={() => moldImageRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
<span className="text-xs">이미지 업로드</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={moldImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleMoldImageUpload}
|
||||
<ImageUpload
|
||||
value={moldForm.image_path || ""}
|
||||
onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
|
||||
tableName="mold_mng"
|
||||
recordId={moldForm.id || ""}
|
||||
columnName="image_path"
|
||||
height="h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/division/values`);
|
||||
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -1181,8 +1183,8 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_number}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -820,11 +826,12 @@ export default function SupplierManagementPage() {
|
||||
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_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
const purchaseCode = categoryOptions["item_division"]?.find((o) => o.label === "구매관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
if (!purchaseCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
}, [itemSearchKeyword, priceItems]);
|
||||
@@ -2464,8 +2471,8 @@ export default function SupplierManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2507,7 +2514,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -811,7 +817,7 @@ export default function CustomerManagementPage() {
|
||||
const searchItems = useCallback(async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
@@ -2483,8 +2489,8 @@ export default function CustomerManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2526,7 +2532,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
||||
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [serialModalOpen, setSerialModalOpen] = useState(false);
|
||||
const [serialForm, setSerialForm] = useState<Record<string, any>>({});
|
||||
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
|
||||
setMoldModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setMoldImagePreview(result);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
// 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
|
||||
|
||||
const handleSaveMold = async () => {
|
||||
// 등록 모드에서 채번이 없으면 수동 입력 필수
|
||||
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
|
||||
)}>
|
||||
{mold.image_path ? (
|
||||
<img
|
||||
src={mold.image_path}
|
||||
src={String(mold.image_path).startsWith("http") || String(mold.image_path).startsWith("/") ? mold.image_path : `/api/files/preview/${mold.image_path}`}
|
||||
alt={mold.mold_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="w-8 h-8 text-muted-foreground/50" />
|
||||
@@ -662,9 +654,10 @@ export default function MoldInfoPage() {
|
||||
<div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
|
||||
{selectedMold.image_path ? (
|
||||
<img
|
||||
src={selectedMold.image_path}
|
||||
src={String(selectedMold.image_path).startsWith("http") || String(selectedMold.image_path).startsWith("/") ? selectedMold.image_path : `/api/files/preview/${selectedMold.image_path}`}
|
||||
alt={selectedMold.mold_name}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-16 h-16 text-muted-foreground/40" />
|
||||
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-xs">금형 이미지</Label>
|
||||
<div className="relative w-full h-32 rounded-lg border-2 border-dashed border-border bg-muted/50 flex items-center justify-center overflow-hidden group">
|
||||
{moldImagePreview ? (
|
||||
<>
|
||||
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" />
|
||||
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}>
|
||||
<Upload className="w-3 h-3 mr-1" /> 변경
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" className="h-7 text-xs" type="button" onClick={() => {
|
||||
setMoldImagePreview(null);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: null }));
|
||||
}}>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-1.5 text-muted-foreground"
|
||||
onClick={() => moldImageRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
<span className="text-xs">이미지 업로드</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={moldImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleMoldImageUpload}
|
||||
<ImageUpload
|
||||
value={moldForm.image_path || ""}
|
||||
onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
|
||||
tableName="mold_mng"
|
||||
recordId={moldForm.id || ""}
|
||||
columnName="image_path"
|
||||
height="h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/division/values`);
|
||||
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -1181,8 +1183,8 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_number}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -820,11 +826,12 @@ export default function SupplierManagementPage() {
|
||||
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_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
const purchaseCode = categoryOptions["item_division"]?.find((o) => o.label === "구매관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
if (!purchaseCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -2430,8 +2437,8 @@ export default function SupplierManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2473,7 +2480,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -811,7 +817,7 @@ export default function CustomerManagementPage() {
|
||||
const searchItems = async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
@@ -2434,8 +2440,8 @@ export default function CustomerManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2477,7 +2483,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
||||
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [serialModalOpen, setSerialModalOpen] = useState(false);
|
||||
const [serialForm, setSerialForm] = useState<Record<string, any>>({});
|
||||
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
|
||||
setMoldModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setMoldImagePreview(result);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
// 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
|
||||
|
||||
const handleSaveMold = async () => {
|
||||
// 등록 모드에서 채번이 없으면 수동 입력 필수
|
||||
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
|
||||
)}>
|
||||
{mold.image_path ? (
|
||||
<img
|
||||
src={mold.image_path}
|
||||
src={String(mold.image_path).startsWith("http") || String(mold.image_path).startsWith("/") ? mold.image_path : `/api/files/preview/${mold.image_path}`}
|
||||
alt={mold.mold_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="w-8 h-8 text-muted-foreground/50" />
|
||||
@@ -662,9 +654,10 @@ export default function MoldInfoPage() {
|
||||
<div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
|
||||
{selectedMold.image_path ? (
|
||||
<img
|
||||
src={selectedMold.image_path}
|
||||
src={String(selectedMold.image_path).startsWith("http") || String(selectedMold.image_path).startsWith("/") ? selectedMold.image_path : `/api/files/preview/${selectedMold.image_path}`}
|
||||
alt={selectedMold.mold_name}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-16 h-16 text-muted-foreground/40" />
|
||||
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-xs">금형 이미지</Label>
|
||||
<div className="relative w-full h-32 rounded-lg border-2 border-dashed border-border bg-muted/50 flex items-center justify-center overflow-hidden group">
|
||||
{moldImagePreview ? (
|
||||
<>
|
||||
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" />
|
||||
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}>
|
||||
<Upload className="w-3 h-3 mr-1" /> 변경
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" className="h-7 text-xs" type="button" onClick={() => {
|
||||
setMoldImagePreview(null);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: null }));
|
||||
}}>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-1.5 text-muted-foreground"
|
||||
onClick={() => moldImageRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
<span className="text-xs">이미지 업로드</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={moldImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleMoldImageUpload}
|
||||
<ImageUpload
|
||||
value={moldForm.image_path || ""}
|
||||
onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
|
||||
tableName="mold_mng"
|
||||
recordId={moldForm.id || ""}
|
||||
columnName="image_path"
|
||||
height="h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/division/values`);
|
||||
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -1181,8 +1183,8 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_number}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -820,11 +826,12 @@ export default function SupplierManagementPage() {
|
||||
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_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
const purchaseCode = categoryOptions["item_division"]?.find((o) => o.label === "구매관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
if (!purchaseCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -2430,8 +2437,8 @@ export default function SupplierManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2473,7 +2480,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -811,7 +817,7 @@ export default function CustomerManagementPage() {
|
||||
const searchItems = async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
@@ -2434,8 +2440,8 @@ export default function CustomerManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2477,7 +2483,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
||||
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [serialModalOpen, setSerialModalOpen] = useState(false);
|
||||
const [serialForm, setSerialForm] = useState<Record<string, any>>({});
|
||||
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
|
||||
setMoldModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setMoldImagePreview(result);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
// 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
|
||||
|
||||
const handleSaveMold = async () => {
|
||||
// 등록 모드에서 채번이 없으면 수동 입력 필수
|
||||
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
|
||||
)}>
|
||||
{mold.image_path ? (
|
||||
<img
|
||||
src={mold.image_path}
|
||||
src={String(mold.image_path).startsWith("http") || String(mold.image_path).startsWith("/") ? mold.image_path : `/api/files/preview/${mold.image_path}`}
|
||||
alt={mold.mold_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="w-8 h-8 text-muted-foreground/50" />
|
||||
@@ -662,9 +654,10 @@ export default function MoldInfoPage() {
|
||||
<div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
|
||||
{selectedMold.image_path ? (
|
||||
<img
|
||||
src={selectedMold.image_path}
|
||||
src={String(selectedMold.image_path).startsWith("http") || String(selectedMold.image_path).startsWith("/") ? selectedMold.image_path : `/api/files/preview/${selectedMold.image_path}`}
|
||||
alt={selectedMold.mold_name}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-16 h-16 text-muted-foreground/40" />
|
||||
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-xs">금형 이미지</Label>
|
||||
<div className="relative w-full h-32 rounded-lg border-2 border-dashed border-border bg-muted/50 flex items-center justify-center overflow-hidden group">
|
||||
{moldImagePreview ? (
|
||||
<>
|
||||
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" />
|
||||
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}>
|
||||
<Upload className="w-3 h-3 mr-1" /> 변경
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" className="h-7 text-xs" type="button" onClick={() => {
|
||||
setMoldImagePreview(null);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: null }));
|
||||
}}>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-1.5 text-muted-foreground"
|
||||
onClick={() => moldImageRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
<span className="text-xs">이미지 업로드</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={moldImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleMoldImageUpload}
|
||||
<ImageUpload
|
||||
value={moldForm.image_path || ""}
|
||||
onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
|
||||
tableName="mold_mng"
|
||||
recordId={moldForm.id || ""}
|
||||
columnName="image_path"
|
||||
height="h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,11 +141,13 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
// item_info의 division 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/division/values`);
|
||||
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
// item_info의 division/unit/material 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -1182,8 +1184,8 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_number}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1227,7 +1229,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -820,11 +826,12 @@ export default function SupplierManagementPage() {
|
||||
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_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
const purchaseCode = categoryOptions["item_division"]?.find((o) => o.label === "구매관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
if (!purchaseCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -2430,8 +2437,8 @@ export default function SupplierManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2473,7 +2480,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -811,7 +817,7 @@ export default function CustomerManagementPage() {
|
||||
const searchItems = async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
@@ -2434,8 +2440,8 @@ export default function CustomerManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2477,7 +2483,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
||||
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [serialModalOpen, setSerialModalOpen] = useState(false);
|
||||
const [serialForm, setSerialForm] = useState<Record<string, any>>({});
|
||||
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
|
||||
setMoldModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setMoldImagePreview(result);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
// 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
|
||||
|
||||
const handleSaveMold = async () => {
|
||||
// 등록 모드에서 채번이 없으면 수동 입력 필수
|
||||
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
|
||||
)}>
|
||||
{mold.image_path ? (
|
||||
<img
|
||||
src={mold.image_path}
|
||||
src={String(mold.image_path).startsWith("http") || String(mold.image_path).startsWith("/") ? mold.image_path : `/api/files/preview/${mold.image_path}`}
|
||||
alt={mold.mold_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="w-8 h-8 text-muted-foreground/50" />
|
||||
@@ -662,9 +654,10 @@ export default function MoldInfoPage() {
|
||||
<div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
|
||||
{selectedMold.image_path ? (
|
||||
<img
|
||||
src={selectedMold.image_path}
|
||||
src={String(selectedMold.image_path).startsWith("http") || String(selectedMold.image_path).startsWith("/") ? selectedMold.image_path : `/api/files/preview/${selectedMold.image_path}`}
|
||||
alt={selectedMold.mold_name}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-16 h-16 text-muted-foreground/40" />
|
||||
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-xs">금형 이미지</Label>
|
||||
<div className="relative w-full h-32 rounded-lg border-2 border-dashed border-border bg-muted/50 flex items-center justify-center overflow-hidden group">
|
||||
{moldImagePreview ? (
|
||||
<>
|
||||
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" />
|
||||
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}>
|
||||
<Upload className="w-3 h-3 mr-1" /> 변경
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" className="h-7 text-xs" type="button" onClick={() => {
|
||||
setMoldImagePreview(null);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: null }));
|
||||
}}>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-1.5 text-muted-foreground"
|
||||
onClick={() => moldImageRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
<span className="text-xs">이미지 업로드</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={moldImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleMoldImageUpload}
|
||||
<ImageUpload
|
||||
value={moldForm.image_path || ""}
|
||||
onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
|
||||
tableName="mold_mng"
|
||||
recordId={moldForm.id || ""}
|
||||
columnName="image_path"
|
||||
height="h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,11 +141,13 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
// item_info의 division 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/division/values`);
|
||||
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
// item_info의 division/unit/material 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -1182,8 +1184,8 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_number}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1227,7 +1229,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -820,11 +826,12 @@ export default function SupplierManagementPage() {
|
||||
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_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
const purchaseCode = categoryOptions["item_division"]?.find((o) => o.label === "구매관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
if (!purchaseCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -2430,8 +2437,8 @@ export default function SupplierManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2473,7 +2480,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -811,7 +817,7 @@ export default function CustomerManagementPage() {
|
||||
const searchItems = async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
@@ -2434,8 +2440,8 @@ export default function CustomerManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2477,7 +2483,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
import { ImageUpload } from "@/components/common/ImageUpload";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
|
||||
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
|
||||
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
|
||||
const [previewCode, setPreviewCode] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
|
||||
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
|
||||
const moldImageRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [serialModalOpen, setSerialModalOpen] = useState(false);
|
||||
const [serialForm, setSerialForm] = useState<Record<string, any>>({});
|
||||
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
|
||||
setMoldModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setMoldImagePreview(result);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: result }));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
// 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
|
||||
|
||||
const handleSaveMold = async () => {
|
||||
// 등록 모드에서 채번이 없으면 수동 입력 필수
|
||||
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
|
||||
)}>
|
||||
{mold.image_path ? (
|
||||
<img
|
||||
src={mold.image_path}
|
||||
src={String(mold.image_path).startsWith("http") || String(mold.image_path).startsWith("/") ? mold.image_path : `/api/files/preview/${mold.image_path}`}
|
||||
alt={mold.mold_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="w-8 h-8 text-muted-foreground/50" />
|
||||
@@ -662,9 +654,10 @@ export default function MoldInfoPage() {
|
||||
<div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
|
||||
{selectedMold.image_path ? (
|
||||
<img
|
||||
src={selectedMold.image_path}
|
||||
src={String(selectedMold.image_path).startsWith("http") || String(selectedMold.image_path).startsWith("/") ? selectedMold.image_path : `/api/files/preview/${selectedMold.image_path}`}
|
||||
alt={selectedMold.mold_name}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="w-16 h-16 text-muted-foreground/40" />
|
||||
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label className="text-xs">금형 이미지</Label>
|
||||
<div className="relative w-full h-32 rounded-lg border-2 border-dashed border-border bg-muted/50 flex items-center justify-center overflow-hidden group">
|
||||
{moldImagePreview ? (
|
||||
<>
|
||||
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" />
|
||||
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}>
|
||||
<Upload className="w-3 h-3 mr-1" /> 변경
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" className="h-7 text-xs" type="button" onClick={() => {
|
||||
setMoldImagePreview(null);
|
||||
setMoldForm((prev) => ({ ...prev, image_path: null }));
|
||||
}}>
|
||||
<XIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-1.5 text-muted-foreground"
|
||||
onClick={() => moldImageRef.current?.click()}
|
||||
>
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
<span className="text-xs">이미지 업로드</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={moldImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleMoldImageUpload}
|
||||
<ImageUpload
|
||||
value={moldForm.image_path || ""}
|
||||
onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
|
||||
tableName="mold_mng"
|
||||
recordId={moldForm.id || ""}
|
||||
columnName="image_path"
|
||||
height="h-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/division/values`);
|
||||
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -1181,8 +1183,8 @@ export default function SubcontractorManagementPage() {
|
||||
<TableCell className="text-[13px]">{item.item_number}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-semibold text-sm">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-4">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function SupplierManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -820,11 +826,12 @@ export default function SupplierManagementPage() {
|
||||
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_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
const purchaseCode = categoryOptions["item_division"]?.find((o) => o.label === "구매관리")?.code;
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
if (!purchaseCode) return true;
|
||||
const div = item.division || "";
|
||||
return div.includes(purchaseCode);
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -2430,8 +2437,8 @@ export default function SupplierManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2473,7 +2480,7 @@ export default function SupplierManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -195,6 +195,12 @@ export default function CustomerManagementPage() {
|
||||
if (res.data?.success) optMap[col] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
for (const col of ["division", "unit", "material"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
|
||||
if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
setCategoryOptions(optMap);
|
||||
|
||||
const priceOpts: Record<string, { code: string; label: string }[]> = {};
|
||||
@@ -811,7 +817,7 @@ export default function CustomerManagementPage() {
|
||||
const searchItems = async () => {
|
||||
setItemSearchLoading(true);
|
||||
try {
|
||||
const salesCode = categoryOptions["division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const salesCode = categoryOptions["item_division"]?.find((o) => o.label === "영업관리")?.code;
|
||||
const filters: any[] = salesCode
|
||||
? [{ columnName: "division", operator: "contains", value: salesCode }]
|
||||
: [];
|
||||
@@ -2434,8 +2440,8 @@ export default function CustomerManagementPage() {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{item.item_name}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.size}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{item.unit}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -2477,7 +2483,7 @@ export default function CustomerManagementPage() {
|
||||
{/* 품목 헤더 */}
|
||||
<div className="px-5 py-3 bg-muted/30 border-b">
|
||||
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {item.unit || ""}</div>
|
||||
<div className="text-xs text-muted-foreground">{itemKey} | {item.size || ""} | {categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 p-5 items-stretch">
|
||||
|
||||
@@ -28,6 +28,10 @@ const nextConfig = {
|
||||
source: "/api/:path*",
|
||||
destination: `${backendUrl}/api/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/uploads/:path*",
|
||||
destination: `${backendUrl}/uploads/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user