From e622013b3d822c6a7316ef82dedec46d24d3f424 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 26 Feb 2026 17:32:39 +0900 Subject: [PATCH] feat: Enhance image handling in TableCellImage component - Updated the TableCellImage component to support multiple image inputs, displaying a representative image when available. - Implemented a new helper function `loadImageBlob` for loading images from blob URLs, improving image loading efficiency. - Refactored image loading logic to handle both single and multiple objid cases, ensuring robust error handling and loading states. - Enhanced user experience by allowing direct URL usage for non-objid image paths. --- .../v2-table-list/TableListComponent.tsx | 120 ++++++++++++------ 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index ebdf9d2b..4170360d 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -14,53 +14,71 @@ import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core"; // 🖼️ 테이블 셀 이미지 썸네일 컴포넌트 // objid인 경우 인증된 API로 blob URL 생성, 경로인 경우 직접 URL 사용 +// 다중 이미지(콤마 구분)인 경우 대표 이미지를 우선 표시 const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => { const [imgSrc, setImgSrc] = React.useState(null); + const [displayObjid, setDisplayObjid] = React.useState(""); const [error, setError] = React.useState(false); const [loading, setLoading] = React.useState(true); React.useEffect(() => { let mounted = true; - // 다중 이미지인 경우 대표 이미지(첫 번째)만 사용 const rawValue = String(value); - const strValue = rawValue.includes(",") ? rawValue.split(",")[0].trim() : rawValue; - const isObjid = /^\d+$/.test(strValue); + const parts = rawValue.split(",").map(s => s.trim()).filter(Boolean); - if (isObjid) { - // objid인 경우: 인증된 API로 blob 다운로드 - const loadImage = async () => { - try { - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get(`/files/preview/${strValue}`, { - responseType: "blob", - }); - if (mounted) { - const blob = new Blob([response.data]); - const url = window.URL.createObjectURL(blob); - setImgSrc(url); - setLoading(false); - } - } catch { - if (mounted) { - setError(true); - setLoading(false); - } - } - }; - loadImage(); - } else { - // 경로인 경우: 직접 URL 사용 - setImgSrc(getFullImageUrl(strValue)); - setLoading(false); + // 단일 값 또는 경로인 경우 + if (parts.length <= 1) { + const strValue = parts[0] || rawValue; + setDisplayObjid(strValue); + const isObjid = /^\d+$/.test(strValue); + + if (isObjid) { + loadImageBlob(strValue, mounted, setImgSrc, setError, setLoading); + } else { + setImgSrc(getFullImageUrl(strValue)); + setLoading(false); + } + return () => { mounted = false; }; } - return () => { - mounted = false; - // blob URL 해제 - if (imgSrc && imgSrc.startsWith("blob:")) { - window.URL.revokeObjectURL(imgSrc); + // 다중 objid: 대표 이미지를 찾아서 표시 + const objids = parts.filter(s => /^\d+$/.test(s)); + if (objids.length === 0) { + setLoading(false); + setError(true); + return () => { mounted = false; }; + } + + (async () => { + try { + const { getFileInfoByObjid } = await import("@/lib/api/file"); + let representativeId: string | null = null; + + // 각 objid의 대표 여부를 확인 + for (const objid of objids) { + const info = await getFileInfoByObjid(objid); + if (info.success && info.data?.isRepresentative) { + representativeId = objid; + break; + } + } + + // 대표 이미지가 없으면 첫 번째 사용 + const targetObjid = representativeId || objids[0]; + if (mounted) { + setDisplayObjid(targetObjid); + loadImageBlob(targetObjid, mounted, setImgSrc, setError, setLoading); + } + } catch { + if (mounted) { + // 대표 조회 실패 시 첫 번째 사용 + setDisplayObjid(objids[0]); + loadImageBlob(objids[0], mounted, setImgSrc, setError, setLoading); + } } - }; + })(); + + return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); @@ -91,10 +109,8 @@ const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => { style={{ maxWidth: "40px", maxHeight: "40px" }} onClick={(e) => { e.stopPropagation(); - const rawValue = String(value); - const strValue = rawValue.includes(",") ? rawValue.split(",")[0].trim() : rawValue; - const isObjid = /^\d+$/.test(strValue); - const openUrl = isObjid ? getFilePreviewUrl(strValue) : getFullImageUrl(strValue); + const isObjid = /^\d+$/.test(displayObjid); + const openUrl = isObjid ? getFilePreviewUrl(displayObjid) : getFullImageUrl(displayObjid); window.open(openUrl, "_blank"); }} onError={() => setError(true)} @@ -104,6 +120,32 @@ const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => { }); TableCellImage.displayName = "TableCellImage"; +// 이미지 blob 로딩 헬퍼 +function loadImageBlob( + objid: string, + mounted: boolean, + setImgSrc: (url: string) => void, + setError: (err: boolean) => void, + setLoading: (loading: boolean) => void, +) { + import("@/lib/api/client").then(({ apiClient }) => { + apiClient.get(`/files/preview/${objid}`, { responseType: "blob" }) + .then((response) => { + if (mounted) { + const blob = new Blob([response.data]); + setImgSrc(window.URL.createObjectURL(blob)); + setLoading(false); + } + }) + .catch(() => { + if (mounted) { + setError(true); + setLoading(false); + } + }); + }); +} + // 🆕 RelatedDataButtons 전역 레지스트리 타입 선언 declare global { interface Window {