gc-wing/apps/web/src/widgets/shipImage/ShipImageModal.tsx

144 lines
5.0 KiB
TypeScript
Raw Normal View 히스토리

import { useCallback, useEffect, useState } from 'react';
import type { ShipImageInfo } from '../../entities/shipImage/model/types';
import { fetchShipImagesByImo, toHighResUrl } from '../../entities/shipImage/api/fetchShipImages';
interface ShipImageModalProps {
/** 갤러리에서 전달받은 전체 이미지 목록 (있으면 API 호출 생략) */
images?: ShipImageInfo[];
/** 시작 인덱스 */
initialIndex?: number;
/** fallback: 첫 번째 이미지 경로 (images 없을 때) */
initialImagePath?: string;
/** 전체 이미지 수 (images 없을 때 표시용) */
totalCount?: number;
/** IMO — images 없을 때 API 호출용 */
imo?: number;
vesselName?: string;
onClose: () => void;
}
const ShipImageModal = ({
images: preloadedImages,
initialIndex = 0,
initialImagePath,
totalCount,
imo,
vesselName,
onClose,
}: ShipImageModalProps) => {
const [index, setIndex] = useState(initialIndex);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
// 전체 이미지 목록: preloaded가 있으면 그것을 사용, 없으면 API로 로드
const [fetchedImages, setFetchedImages] = useState<ShipImageInfo[] | null>(null);
const needsFetch = !preloadedImages && !!imo && imo > 0;
const [fetchingAll, setFetchingAll] = useState(needsFetch);
const allImages = preloadedImages ?? fetchedImages;
const total = allImages ? allImages.length : (totalCount ?? 1);
const hasPrev = index > 0;
const hasNext = index < total - 1;
// 현재 이미지 URL 결정 (모달은 항상 고화질)
const currentImageUrl = (() => {
if (allImages && allImages[index]) return toHighResUrl(allImages[index].path);
if (index === 0 && initialImagePath) return toHighResUrl(initialImagePath);
return null;
})();
// preloaded 없을 때만 API 호출
useEffect(() => {
if (!needsFetch) return;
const ac = new AbortController();
fetchShipImagesByImo(imo!, ac.signal).then((result) => {
if (ac.signal.aborted) return;
setFetchedImages(result.length > 0 ? result : null);
setFetchingAll(false);
});
return () => ac.abort();
}, [needsFetch, imo]);
const goPrev = useCallback(() => {
if (hasPrev) { setIndex((i) => i - 1); setLoading(true); setError(false); }
}, [hasPrev]);
const goNext = useCallback(() => {
if (!hasNext) return;
setIndex((i) => i + 1);
setLoading(true);
setError(false);
}, [hasNext]);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
else if (e.key === 'ArrowLeft') goPrev();
else if (e.key === 'ArrowRight') goNext();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, goPrev, goNext]);
// 현재 이미지 메타데이터
const currentMeta = allImages?.[index] ?? null;
return (
<div className="ship-image-modal" onClick={onClose}>
<div className="ship-image-modal__content" onClick={(e) => e.stopPropagation()}>
{/* 헤더 */}
<div className="ship-image-modal__header">
<span className="ship-image-modal__title">
{vesselName && <strong>{vesselName}</strong>}
{total > 1 && <span className="ship-image-modal__counter">{index + 1} / {total}</span>}
</span>
<button className="ship-image-modal__close" onClick={onClose} aria-label="닫기">
</button>
</div>
{/* 이미지 영역 */}
<div className="ship-image-modal__body">
{hasPrev && (
<button className="ship-image-modal__nav ship-image-modal__nav--prev" onClick={goPrev} aria-label="이전">
</button>
)}
<div className="ship-image-modal__img-wrap">
{(loading || fetchingAll) && !error && <div className="ship-image-modal__spinner" />}
{error && <div className="ship-image-modal__error"> </div>}
{currentImageUrl ? (
<img
key={currentImageUrl}
src={currentImageUrl}
alt={vesselName || '선박 사진'}
className="ship-image-modal__img"
style={{ opacity: loading ? 0 : 1 }}
onLoad={() => setLoading(false)}
onError={() => { setLoading(false); setError(true); }}
/>
) : fetchingAll ? null : (
<div className="ship-image-modal__error"> </div>
)}
</div>
{hasNext && (
<button className="ship-image-modal__nav ship-image-modal__nav--next" onClick={goNext} aria-label="다음">
</button>
)}
</div>
{/* 푸터 */}
<div className="ship-image-modal__footer">
{currentMeta?.copyright && <span>{currentMeta.copyright}</span>}
{currentMeta?.date && <span>{currentMeta.date}</span>}
</div>
</div>
</div>
);
};
export default ShipImageModal;