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.
This commit is contained in:
kjs 2026-02-26 17:32:39 +09:00
parent 17d4cc297c
commit e622013b3d
1 changed files with 81 additions and 39 deletions

View File

@ -14,53 +14,71 @@ import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core";
// 🖼️ 테이블 셀 이미지 썸네일 컴포넌트 // 🖼️ 테이블 셀 이미지 썸네일 컴포넌트
// objid인 경우 인증된 API로 blob URL 생성, 경로인 경우 직접 URL 사용 // objid인 경우 인증된 API로 blob URL 생성, 경로인 경우 직접 URL 사용
// 다중 이미지(콤마 구분)인 경우 대표 이미지를 우선 표시
const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => { const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => {
const [imgSrc, setImgSrc] = React.useState<string | null>(null); const [imgSrc, setImgSrc] = React.useState<string | null>(null);
const [displayObjid, setDisplayObjid] = React.useState<string>("");
const [error, setError] = React.useState(false); const [error, setError] = React.useState(false);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
let mounted = true; let mounted = true;
// 다중 이미지인 경우 대표 이미지(첫 번째)만 사용
const rawValue = String(value); const rawValue = String(value);
const strValue = rawValue.includes(",") ? rawValue.split(",")[0].trim() : rawValue; const parts = rawValue.split(",").map(s => s.trim()).filter(Boolean);
// 단일 값 또는 경로인 경우
if (parts.length <= 1) {
const strValue = parts[0] || rawValue;
setDisplayObjid(strValue);
const isObjid = /^\d+$/.test(strValue); const isObjid = /^\d+$/.test(strValue);
if (isObjid) { if (isObjid) {
// objid인 경우: 인증된 API로 blob 다운로드 loadImageBlob(strValue, mounted, setImgSrc, setError, setLoading);
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 { } else {
// 경로인 경우: 직접 URL 사용
setImgSrc(getFullImageUrl(strValue)); setImgSrc(getFullImageUrl(strValue));
setLoading(false); 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]); }, [value]);
@ -91,10 +109,8 @@ const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => {
style={{ maxWidth: "40px", maxHeight: "40px" }} style={{ maxWidth: "40px", maxHeight: "40px" }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
const rawValue = String(value); const isObjid = /^\d+$/.test(displayObjid);
const strValue = rawValue.includes(",") ? rawValue.split(",")[0].trim() : rawValue; const openUrl = isObjid ? getFilePreviewUrl(displayObjid) : getFullImageUrl(displayObjid);
const isObjid = /^\d+$/.test(strValue);
const openUrl = isObjid ? getFilePreviewUrl(strValue) : getFullImageUrl(strValue);
window.open(openUrl, "_blank"); window.open(openUrl, "_blank");
}} }}
onError={() => setError(true)} onError={() => setError(true)}
@ -104,6 +120,32 @@ const TableCellImage: React.FC<{ value: string }> = React.memo(({ value }) => {
}); });
TableCellImage.displayName = "TableCellImage"; 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 전역 레지스트리 타입 선언 // 🆕 RelatedDataButtons 전역 레지스트리 타입 선언
declare global { declare global {
interface Window { interface Window {