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:
jskim
2026-04-11 12:44:48 +00:00
29 changed files with 328 additions and 450 deletions
@@ -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">
+4
View File
@@ -28,6 +28,10 @@ const nextConfig = {
source: "/api/:path*",
destination: `${backendUrl}/api/:path*`,
},
{
source: "/uploads/:path*",
destination: `${backendUrl}/uploads/:path*`,
},
];
},