refactor: Integrate ImageUpload component for mold image handling

- Replaced manual image upload logic with the ImageUpload component for better management of mold images.
- Updated image source handling to ensure proper display of images based on their URL format.
- Enhanced error handling for image display to improve user experience.

These changes aim to streamline the image upload process and enhance the overall functionality of the mold information page across multiple companies.
This commit is contained in:
kjs
2026-04-11 14:50:18 +09:00
parent a232399cbc
commit 972a0143ad
29 changed files with 293 additions and 478 deletions
@@ -26,6 +26,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { ImageUpload } from "@/components/common/ImageUpload";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useConfirmDialog } from "@/components/common/ConfirmDialog";
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null); const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null); const [previewCode, setPreviewCode] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null); const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
const moldImageRef = React.useRef<HTMLInputElement>(null);
const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialModalOpen, setSerialModalOpen] = useState(false);
const [serialForm, setSerialForm] = useState<Record<string, any>>({}); const [serialForm, setSerialForm] = useState<Record<string, any>>({});
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
setMoldModalOpen(true); setMoldModalOpen(true);
}; };
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
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);
};
const handleSaveMold = async () => { const handleSaveMold = async () => {
// 등록 모드에서 채번이 없으면 수동 입력 필수 // 등록 모드에서 채번이 없으면 수동 입력 필수
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
)}> )}>
{mold.image_path ? ( {mold.image_path ? (
<img <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} alt={mold.mold_name}
className="w-full h-full object-cover" 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" /> <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"> <div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
{selectedMold.image_path ? ( {selectedMold.image_path ? (
<img <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} alt={selectedMold.mold_name}
className="w-full h-full object-cover rounded-lg" 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" /> <ImageIcon className="w-16 h-16 text-muted-foreground/40" />
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label className="text-xs"> </Label> <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"> <ImageUpload
{moldImagePreview ? ( value={moldForm.image_path || ""}
<> onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" /> tableName="mold_mng"
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> recordId={moldForm.id || ""}
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}> columnName="image_path"
<Upload className="w-3 h-3 mr-1" /> height="h-32"
</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}
/> />
</div> </div>
</div> </div>
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; 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_number}</TableCell>
<TableCell className="text-[13px]">{item.item_name}</TableCell> <TableCell className="text-[13px]">{item.item_name}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell> <TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{item.material}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{item.unit}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <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="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>
<div className="flex gap-4 p-4"> <div className="flex gap-4 p-4">
@@ -195,10 +195,12 @@ export default function SupplierManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2435,8 +2437,8 @@ export default function SupplierManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2478,7 +2480,7 @@ export default function SupplierManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -195,10 +195,12 @@ export default function CustomerManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2438,8 +2440,8 @@ export default function CustomerManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2481,7 +2483,7 @@ export default function CustomerManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -26,6 +26,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { ImageUpload } from "@/components/common/ImageUpload";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useConfirmDialog } from "@/components/common/ConfirmDialog";
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null); const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null); const [previewCode, setPreviewCode] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null); const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
const moldImageRef = React.useRef<HTMLInputElement>(null);
const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialModalOpen, setSerialModalOpen] = useState(false);
const [serialForm, setSerialForm] = useState<Record<string, any>>({}); const [serialForm, setSerialForm] = useState<Record<string, any>>({});
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
setMoldModalOpen(true); setMoldModalOpen(true);
}; };
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
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);
};
const handleSaveMold = async () => { const handleSaveMold = async () => {
// 등록 모드에서 채번이 없으면 수동 입력 필수 // 등록 모드에서 채번이 없으면 수동 입력 필수
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
)}> )}>
{mold.image_path ? ( {mold.image_path ? (
<img <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} alt={mold.mold_name}
className="w-full h-full object-cover" 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" /> <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"> <div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
{selectedMold.image_path ? ( {selectedMold.image_path ? (
<img <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} alt={selectedMold.mold_name}
className="w-full h-full object-cover rounded-lg" 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" /> <ImageIcon className="w-16 h-16 text-muted-foreground/40" />
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label className="text-xs"> </Label> <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"> <ImageUpload
{moldImagePreview ? ( value={moldForm.image_path || ""}
<> onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" /> tableName="mold_mng"
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> recordId={moldForm.id || ""}
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}> columnName="image_path"
<Upload className="w-3 h-3 mr-1" /> height="h-32"
</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}
/> />
</div> </div>
</div> </div>
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; 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_number}</TableCell>
<TableCell className="text-[13px]">{item.item_name}</TableCell> <TableCell className="text-[13px]">{item.item_name}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell> <TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{item.material}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{item.unit}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <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="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>
<div className="flex gap-4 p-4"> <div className="flex gap-4 p-4">
@@ -195,10 +195,12 @@ export default function SupplierManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2469,8 +2471,8 @@ export default function SupplierManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2512,7 +2514,7 @@ export default function SupplierManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -195,10 +195,12 @@ export default function CustomerManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2487,8 +2489,8 @@ export default function CustomerManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2530,7 +2532,7 @@ export default function CustomerManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -26,6 +26,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { ImageUpload } from "@/components/common/ImageUpload";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useConfirmDialog } from "@/components/common/ConfirmDialog";
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null); const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null); const [previewCode, setPreviewCode] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null); const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
const moldImageRef = React.useRef<HTMLInputElement>(null);
const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialModalOpen, setSerialModalOpen] = useState(false);
const [serialForm, setSerialForm] = useState<Record<string, any>>({}); const [serialForm, setSerialForm] = useState<Record<string, any>>({});
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
setMoldModalOpen(true); setMoldModalOpen(true);
}; };
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
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);
};
const handleSaveMold = async () => { const handleSaveMold = async () => {
// 등록 모드에서 채번이 없으면 수동 입력 필수 // 등록 모드에서 채번이 없으면 수동 입력 필수
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
)}> )}>
{mold.image_path ? ( {mold.image_path ? (
<img <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} alt={mold.mold_name}
className="w-full h-full object-cover" 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" /> <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"> <div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
{selectedMold.image_path ? ( {selectedMold.image_path ? (
<img <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} alt={selectedMold.mold_name}
className="w-full h-full object-cover rounded-lg" 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" /> <ImageIcon className="w-16 h-16 text-muted-foreground/40" />
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label className="text-xs"> </Label> <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"> <ImageUpload
{moldImagePreview ? ( value={moldForm.image_path || ""}
<> onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" /> tableName="mold_mng"
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> recordId={moldForm.id || ""}
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}> columnName="image_path"
<Upload className="w-3 h-3 mr-1" /> height="h-32"
</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}
/> />
</div> </div>
</div> </div>
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; 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_number}</TableCell>
<TableCell className="text-[13px]">{item.item_name}</TableCell> <TableCell className="text-[13px]">{item.item_name}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell> <TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{item.material}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{item.unit}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <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="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>
<div className="flex gap-4 p-4"> <div className="flex gap-4 p-4">
@@ -195,10 +195,12 @@ export default function SupplierManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2435,8 +2437,8 @@ export default function SupplierManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2478,7 +2480,7 @@ export default function SupplierManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -195,10 +195,12 @@ export default function CustomerManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2438,8 +2440,8 @@ export default function CustomerManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2481,7 +2483,7 @@ export default function CustomerManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -26,6 +26,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { ImageUpload } from "@/components/common/ImageUpload";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useConfirmDialog } from "@/components/common/ConfirmDialog";
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null); const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null); const [previewCode, setPreviewCode] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null); const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
const moldImageRef = React.useRef<HTMLInputElement>(null);
const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialModalOpen, setSerialModalOpen] = useState(false);
const [serialForm, setSerialForm] = useState<Record<string, any>>({}); const [serialForm, setSerialForm] = useState<Record<string, any>>({});
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
setMoldModalOpen(true); setMoldModalOpen(true);
}; };
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
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);
};
const handleSaveMold = async () => { const handleSaveMold = async () => {
// 등록 모드에서 채번이 없으면 수동 입력 필수 // 등록 모드에서 채번이 없으면 수동 입력 필수
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
)}> )}>
{mold.image_path ? ( {mold.image_path ? (
<img <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} alt={mold.mold_name}
className="w-full h-full object-cover" 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" /> <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"> <div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
{selectedMold.image_path ? ( {selectedMold.image_path ? (
<img <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} alt={selectedMold.mold_name}
className="w-full h-full object-cover rounded-lg" 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" /> <ImageIcon className="w-16 h-16 text-muted-foreground/40" />
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label className="text-xs"> </Label> <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"> <ImageUpload
{moldImagePreview ? ( value={moldForm.image_path || ""}
<> onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" /> tableName="mold_mng"
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> recordId={moldForm.id || ""}
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}> columnName="image_path"
<Upload className="w-3 h-3 mr-1" /> height="h-32"
</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}
/> />
</div> </div>
</div> </div>
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; 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_number}</TableCell>
<TableCell className="text-[13px]">{item.item_name}</TableCell> <TableCell className="text-[13px]">{item.item_name}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell> <TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{item.material}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{item.unit}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <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="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>
<div className="flex gap-4 p-4"> <div className="flex gap-4 p-4">
@@ -195,10 +195,12 @@ export default function SupplierManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2435,8 +2437,8 @@ export default function SupplierManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2478,7 +2480,7 @@ export default function SupplierManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -195,10 +195,12 @@ export default function CustomerManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2438,8 +2440,8 @@ export default function CustomerManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2481,7 +2483,7 @@ export default function CustomerManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -26,6 +26,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { ImageUpload } from "@/components/common/ImageUpload";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useConfirmDialog } from "@/components/common/ConfirmDialog";
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null); const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null); const [previewCode, setPreviewCode] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null); const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
const moldImageRef = React.useRef<HTMLInputElement>(null);
const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialModalOpen, setSerialModalOpen] = useState(false);
const [serialForm, setSerialForm] = useState<Record<string, any>>({}); const [serialForm, setSerialForm] = useState<Record<string, any>>({});
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
setMoldModalOpen(true); setMoldModalOpen(true);
}; };
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
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);
};
const handleSaveMold = async () => { const handleSaveMold = async () => {
// 등록 모드에서 채번이 없으면 수동 입력 필수 // 등록 모드에서 채번이 없으면 수동 입력 필수
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
)}> )}>
{mold.image_path ? ( {mold.image_path ? (
<img <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} alt={mold.mold_name}
className="w-full h-full object-cover" 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" /> <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"> <div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
{selectedMold.image_path ? ( {selectedMold.image_path ? (
<img <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} alt={selectedMold.mold_name}
className="w-full h-full object-cover rounded-lg" 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" /> <ImageIcon className="w-16 h-16 text-muted-foreground/40" />
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label className="text-xs"> </Label> <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"> <ImageUpload
{moldImagePreview ? ( value={moldForm.image_path || ""}
<> onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" /> tableName="mold_mng"
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> recordId={moldForm.id || ""}
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}> columnName="image_path"
<Upload className="w-3 h-3 mr-1" /> height="h-32"
</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}
/> />
</div> </div>
</div> </div>
@@ -141,11 +141,13 @@ export default function SubcontractorManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
// item_info의 division 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용) // item_info의 division/unit/material 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; 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_number}</TableCell>
<TableCell className="text-[13px]">{item.item_name}</TableCell> <TableCell className="text-[13px]">{item.item_name}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell> <TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{item.material}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{item.unit}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -1227,7 +1229,7 @@ export default function SubcontractorManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <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="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>
<div className="flex gap-4 p-4"> <div className="flex gap-4 p-4">
@@ -195,10 +195,12 @@ export default function SupplierManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2435,8 +2437,8 @@ export default function SupplierManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2478,7 +2480,7 @@ export default function SupplierManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -195,10 +195,12 @@ export default function CustomerManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2438,8 +2440,8 @@ export default function CustomerManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2481,7 +2483,7 @@ export default function CustomerManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -26,6 +26,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { ImageUpload } from "@/components/common/ImageUpload";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useConfirmDialog } from "@/components/common/ConfirmDialog";
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null); const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null); const [previewCode, setPreviewCode] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null); const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
const moldImageRef = React.useRef<HTMLInputElement>(null);
const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialModalOpen, setSerialModalOpen] = useState(false);
const [serialForm, setSerialForm] = useState<Record<string, any>>({}); const [serialForm, setSerialForm] = useState<Record<string, any>>({});
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
setMoldModalOpen(true); setMoldModalOpen(true);
}; };
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
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);
};
const handleSaveMold = async () => { const handleSaveMold = async () => {
// 등록 모드에서 채번이 없으면 수동 입력 필수 // 등록 모드에서 채번이 없으면 수동 입력 필수
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
)}> )}>
{mold.image_path ? ( {mold.image_path ? (
<img <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} alt={mold.mold_name}
className="w-full h-full object-cover" 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" /> <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"> <div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
{selectedMold.image_path ? ( {selectedMold.image_path ? (
<img <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} alt={selectedMold.mold_name}
className="w-full h-full object-cover rounded-lg" 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" /> <ImageIcon className="w-16 h-16 text-muted-foreground/40" />
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label className="text-xs"> </Label> <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"> <ImageUpload
{moldImagePreview ? ( value={moldForm.image_path || ""}
<> onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" /> tableName="mold_mng"
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> recordId={moldForm.id || ""}
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}> columnName="image_path"
<Upload className="w-3 h-3 mr-1" /> height="h-32"
</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}
/> />
</div> </div>
</div> </div>
@@ -141,11 +141,13 @@ export default function SubcontractorManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
// item_info의 division 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용) // item_info의 division/unit/material 카테고리도 로드 (품목 검색 시 외주관리 코드 조회용)
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; 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_number}</TableCell>
<TableCell className="text-[13px]">{item.item_name}</TableCell> <TableCell className="text-[13px]">{item.item_name}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell> <TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{item.material}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{item.unit}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -1227,7 +1229,7 @@ export default function SubcontractorManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <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="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>
<div className="flex gap-4 p-4"> <div className="flex gap-4 p-4">
@@ -195,10 +195,12 @@ export default function SupplierManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2435,8 +2437,8 @@ export default function SupplierManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2478,7 +2480,7 @@ export default function SupplierManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -195,10 +195,12 @@ export default function CustomerManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2438,8 +2440,8 @@ export default function CustomerManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2481,7 +2483,7 @@ export default function CustomerManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -26,6 +26,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
import { ImageUpload } from "@/components/common/ImageUpload";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner"; import { toast } from "sonner";
import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { useConfirmDialog } from "@/components/common/ConfirmDialog";
@@ -102,8 +103,8 @@ export default function MoldInfoPage() {
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null); const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
const [previewCode, setPreviewCode] = useState<string | null>(null); const [previewCode, setPreviewCode] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// moldImagePreview는 상세 표시용으로 유지 (ImageUpload는 모달에서만 사용)
const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null); const [moldImagePreview, setMoldImagePreview] = useState<string | null>(null);
const moldImageRef = React.useRef<HTMLInputElement>(null);
const [serialModalOpen, setSerialModalOpen] = useState(false); const [serialModalOpen, setSerialModalOpen] = useState(false);
const [serialForm, setSerialForm] = useState<Record<string, any>>({}); const [serialForm, setSerialForm] = useState<Record<string, any>>({});
@@ -240,17 +241,7 @@ export default function MoldInfoPage() {
setMoldModalOpen(true); setMoldModalOpen(true);
}; };
const handleMoldImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { // 이미지 업로드는 ImageUpload 컴포넌트가 처리 → objid를 image_path에 저장
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);
};
const handleSaveMold = async () => { const handleSaveMold = async () => {
// 등록 모드에서 채번이 없으면 수동 입력 필수 // 등록 모드에서 채번이 없으면 수동 입력 필수
@@ -460,9 +451,10 @@ export default function MoldInfoPage() {
)}> )}>
{mold.image_path ? ( {mold.image_path ? (
<img <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} alt={mold.mold_name}
className="w-full h-full object-cover" 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" /> <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"> <div className="w-[200px] min-w-[200px] h-[200px] bg-muted rounded-lg flex items-center justify-center border">
{selectedMold.image_path ? ( {selectedMold.image_path ? (
<img <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} alt={selectedMold.mold_name}
className="w-full h-full object-cover rounded-lg" 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" /> <ImageIcon className="w-16 h-16 text-muted-foreground/40" />
@@ -1143,39 +1136,13 @@ export default function MoldInfoPage() {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label className="text-xs"> </Label> <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"> <ImageUpload
{moldImagePreview ? ( value={moldForm.image_path || ""}
<> onChange={(v) => setMoldForm((prev) => ({ ...prev, image_path: v }))}
<img src={moldImagePreview} alt="금형 이미지" className="w-full h-full object-contain" /> tableName="mold_mng"
<div className="absolute inset-0 bg-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"> recordId={moldForm.id || ""}
<Button size="sm" variant="secondary" className="h-7 text-xs" type="button" onClick={() => moldImageRef.current?.click()}> columnName="image_path"
<Upload className="w-3 h-3 mr-1" /> height="h-32"
</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}
/> />
</div> </div>
</div> </div>
@@ -141,10 +141,12 @@ export default function SubcontractorManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; 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_number}</TableCell>
<TableCell className="text-[13px]">{item.item_name}</TableCell> <TableCell className="text-[13px]">{item.item_name}</TableCell>
<TableCell className="text-[13px]">{item.size}</TableCell> <TableCell className="text-[13px]">{item.size}</TableCell>
<TableCell className="text-[13px]">{item.material}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-[13px]">{item.unit}</TableCell> <TableCell className="text-[13px]">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -1226,7 +1228,7 @@ export default function SubcontractorManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <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="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>
<div className="flex gap-4 p-4"> <div className="flex gap-4 p-4">
@@ -195,10 +195,12 @@ export default function SupplierManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2435,8 +2437,8 @@ export default function SupplierManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2478,7 +2480,7 @@ export default function SupplierManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
@@ -195,10 +195,12 @@ export default function CustomerManagementPage() {
if (res.data?.success) optMap[col] = flatten(res.data.data || []); if (res.data?.success) optMap[col] = flatten(res.data.data || []);
} catch { /* skip */ } } catch { /* skip */ }
} }
try { for (const col of ["division", "unit", "material"]) {
const res = await apiClient.get(`/table-categories/item_info/division/values`); try {
if (res.data?.success) optMap["item_division"] = flatten(res.data.data || []); const res = await apiClient.get(`/table-categories/item_info/${col}/values`);
} catch { /* skip */ } if (res.data?.success) optMap[`item_${col}`] = flatten(res.data.data || []);
} catch { /* skip */ }
}
setCategoryOptions(optMap); setCategoryOptions(optMap);
const priceOpts: Record<string, { code: string; label: string }[]> = {}; const priceOpts: Record<string, { code: string; label: string }[]> = {};
@@ -2438,8 +2440,8 @@ export default function CustomerManagementPage() {
</TableCell> </TableCell>
<TableCell className="text-sm">{item.item_name}</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.size}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{item.material}</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">{item.unit}</TableCell> <TableCell className="text-[13px] text-muted-foreground">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -2481,7 +2483,7 @@ export default function CustomerManagementPage() {
{/* 품목 헤더 */} {/* 품목 헤더 */}
<div className="px-5 py-3 bg-muted/30 border-b"> <div className="px-5 py-3 bg-muted/30 border-b">
<div className="font-bold">{idx + 1}. {item.item_name || itemKey}</div> <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>
<div className="flex gap-4 p-5 items-stretch"> <div className="flex gap-4 p-5 items-stretch">
+4
View File
@@ -28,6 +28,10 @@ const nextConfig = {
source: "/api/:path*", source: "/api/:path*",
destination: `${backendUrl}/api/:path*`, destination: `${backendUrl}/api/:path*`,
}, },
{
source: "/uploads/:path*",
destination: `${backendUrl}/uploads/:path*`,
},
]; ];
}, },