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:
parent
17d4cc297c
commit
e622013b3d
|
|
@ -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);
|
||||||
const isObjid = /^\d+$/.test(strValue);
|
|
||||||
|
|
||||||
if (isObjid) {
|
// 단일 값 또는 경로인 경우
|
||||||
// objid인 경우: 인증된 API로 blob 다운로드
|
if (parts.length <= 1) {
|
||||||
const loadImage = async () => {
|
const strValue = parts[0] || rawValue;
|
||||||
try {
|
setDisplayObjid(strValue);
|
||||||
const { apiClient } = await import("@/lib/api/client");
|
const isObjid = /^\d+$/.test(strValue);
|
||||||
const response = await apiClient.get(`/files/preview/${strValue}`, {
|
|
||||||
responseType: "blob",
|
if (isObjid) {
|
||||||
});
|
loadImageBlob(strValue, mounted, setImgSrc, setError, setLoading);
|
||||||
if (mounted) {
|
} else {
|
||||||
const blob = new Blob([response.data]);
|
setImgSrc(getFullImageUrl(strValue));
|
||||||
const url = window.URL.createObjectURL(blob);
|
setLoading(false);
|
||||||
setImgSrc(url);
|
}
|
||||||
setLoading(false);
|
return () => { mounted = false; };
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (mounted) {
|
|
||||||
setError(true);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadImage();
|
|
||||||
} else {
|
|
||||||
// 경로인 경우: 직접 URL 사용
|
|
||||||
setImgSrc(getFullImageUrl(strValue));
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
// 다중 objid: 대표 이미지를 찾아서 표시
|
||||||
mounted = false;
|
const objids = parts.filter(s => /^\d+$/.test(s));
|
||||||
// blob URL 해제
|
if (objids.length === 0) {
|
||||||
if (imgSrc && imgSrc.startsWith("blob:")) {
|
setLoading(false);
|
||||||
window.URL.revokeObjectURL(imgSrc);
|
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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue