- Introduced a new equipment information page that displays a list of equipment alongside inspection items and consumables. - Implemented dynamic loading of categories for equipment types and inspection methods to enhance data representation. - Added functionality for equipment editing, inspection item management, and consumable tracking. - Introduced a department management page that allows for department registration and user assignment. - Enhanced user experience with responsive design, modals for data entry, and real-time data updates.
165 lines
5.7 KiB
TypeScript
165 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* ImageUpload — 공통 이미지 업로드 컴포넌트
|
|
*
|
|
* 기능:
|
|
* - 이미지 파일 선택 (클릭 or 드래그)
|
|
* - 미리보기 표시
|
|
* - /api/files/upload API로 업로드
|
|
* - 업로드 후 파일 objid 반환
|
|
* - 기존 이미지 표시 (objid 또는 URL)
|
|
*
|
|
* 사용법:
|
|
* <ImageUpload
|
|
* value={form.image_path} // 기존 파일 objid 또는 URL
|
|
* onChange={(objid) => setForm(...)} // 업로드 완료 시 objid 반환
|
|
* tableName="equipment_mng" // 연결 테이블
|
|
* recordId="xxx" // 연결 레코드 ID
|
|
* columnName="image_path" // 연결 컬럼명
|
|
* />
|
|
*/
|
|
|
|
import React, { useState, useRef, useCallback } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Upload, X, Loader2, ImageIcon } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { toast } from "sonner";
|
|
|
|
interface ImageUploadProps {
|
|
/** 현재 이미지 값 (파일 objid, URL, 또는 빈 문자열) */
|
|
value?: string;
|
|
/** 업로드 완료 시 콜백 (파일 objid) */
|
|
onChange?: (value: string) => void;
|
|
/** 연결할 테이블명 */
|
|
tableName?: string;
|
|
/** 연결할 레코드 ID */
|
|
recordId?: string;
|
|
/** 연결할 컬럼명 */
|
|
columnName?: string;
|
|
/** 높이 (기본 160px) */
|
|
height?: string;
|
|
/** 비활성화 */
|
|
disabled?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function ImageUpload({
|
|
value, onChange, tableName, recordId, columnName,
|
|
height = "h-40", disabled = false, className,
|
|
}: ImageUploadProps) {
|
|
const [uploading, setUploading] = useState(false);
|
|
const [dragOver, setDragOver] = useState(false);
|
|
const fileRef = useRef<HTMLInputElement>(null);
|
|
|
|
// 이미지 URL 결정
|
|
const imageUrl = value
|
|
? (value.startsWith("http") || value.startsWith("/"))
|
|
? value
|
|
: `/api/files/preview/${value}`
|
|
: null;
|
|
|
|
const handleUpload = useCallback(async (file: File) => {
|
|
if (!file.type.startsWith("image/")) {
|
|
toast.error("이미지 파일만 업로드 가능합니다.");
|
|
return;
|
|
}
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
toast.error("파일 크기는 10MB 이하만 가능합니다.");
|
|
return;
|
|
}
|
|
|
|
setUploading(true);
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append("files", file);
|
|
formData.append("docType", "IMAGE");
|
|
formData.append("docTypeName", "이미지");
|
|
if (tableName) formData.append("linkedTable", tableName);
|
|
if (recordId) formData.append("recordId", recordId);
|
|
if (columnName) formData.append("columnName", columnName);
|
|
if (tableName && recordId) {
|
|
formData.append("autoLink", "true");
|
|
if (columnName) formData.append("isVirtualFileColumn", "true");
|
|
}
|
|
|
|
const res = await apiClient.post("/files/upload", formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
});
|
|
|
|
if (res.data?.success && (res.data.files?.length > 0 || res.data.data?.length > 0)) {
|
|
const file = res.data.files?.[0] || res.data.data?.[0];
|
|
const objid = file.objid;
|
|
onChange?.(objid);
|
|
toast.success("이미지가 업로드되었습니다.");
|
|
} else {
|
|
toast.error(res.data?.message || "업로드에 실패했습니다.");
|
|
}
|
|
} catch (err: any) {
|
|
console.error("이미지 업로드 실패:", err);
|
|
toast.error(err.response?.data?.message || "업로드에 실패했습니다.");
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
}, [tableName, recordId, columnName, onChange]);
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) handleUpload(file);
|
|
e.target.value = "";
|
|
};
|
|
|
|
const handleDrop = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setDragOver(false);
|
|
const file = e.dataTransfer.files?.[0];
|
|
if (file) handleUpload(file);
|
|
};
|
|
|
|
const handleRemove = () => {
|
|
onChange?.("");
|
|
};
|
|
|
|
return (
|
|
<div className={cn("relative", className)}>
|
|
<div
|
|
className={cn(
|
|
"border-2 border-dashed rounded-lg flex flex-col items-center justify-center cursor-pointer transition-colors overflow-hidden",
|
|
height,
|
|
dragOver ? "border-primary bg-primary/5" : imageUrl ? "border-transparent" : "border-muted-foreground/25 hover:border-primary hover:bg-muted/50",
|
|
disabled && "opacity-50 cursor-not-allowed",
|
|
)}
|
|
onClick={() => !disabled && !uploading && fileRef.current?.click()}
|
|
onDragOver={(e) => { e.preventDefault(); if (!disabled) setDragOver(true); }}
|
|
onDragLeave={() => setDragOver(false)}
|
|
onDrop={!disabled ? handleDrop : undefined}
|
|
>
|
|
{uploading ? (
|
|
<div className="flex flex-col items-center gap-2">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">업로드 중...</span>
|
|
</div>
|
|
) : imageUrl ? (
|
|
<img src={imageUrl} alt="이미지" className="w-full h-full object-contain" />
|
|
) : (
|
|
<div className="flex flex-col items-center gap-2">
|
|
<ImageIcon className="h-8 w-8 text-muted-foreground/50" />
|
|
<span className="text-xs text-muted-foreground">클릭 또는 드래그하여 업로드</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 삭제 버튼 */}
|
|
{imageUrl && !disabled && (
|
|
<Button variant="destructive" size="sm" className="absolute top-1 right-1 h-6 w-6 p-0 rounded-full"
|
|
onClick={(e) => { e.stopPropagation(); handleRemove(); }}>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
|
|
<input ref={fileRef} type="file" accept="image/*" onChange={handleFileChange} className="hidden" />
|
|
</div>
|
|
);
|
|
}
|