jskim-node #396
|
|
@ -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<string | null>(null);
|
||||
const [displayObjid, setDisplayObjid] = React.useState<string>("");
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue