diff --git a/src/ui/components/TimelineItem.tsx b/src/ui/components/TimelineItem.tsx index 1cf0299..7b0fbcf 100644 --- a/src/ui/components/TimelineItem.tsx +++ b/src/ui/components/TimelineItem.tsx @@ -34,7 +34,7 @@ export const TimelineItem = ({ const notification = status.notification; const displayStatus = notification?.target ?? status.reblog ?? status; const boostedBy = notification ? null : status.reblog ? status.boostedBy : null; - const [activeImageUrl, setActiveImageUrl] = useState(null); + const [activeImageIndex, setActiveImageIndex] = useState(null); const [showContent, setShowContent] = useState(() => displayStatus.spoilerText.length === 0); const [imageZoom, setImageZoom] = useState(1); const [imageOffset, setImageOffset] = useState({ x: 0, y: 0 }); @@ -47,6 +47,44 @@ export const TimelineItem = ({ ); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const attachments = displayStatus.mediaAttachments; + const activeImageUrl = activeImageIndex !== null ? attachments[activeImageIndex]?.url ?? null : null; + + const goToPrevImage = useCallback(() => { + if (activeImageIndex === null || attachments.length <= 1) return; + const prevIndex = activeImageIndex === 0 ? attachments.length - 1 : activeImageIndex - 1; + setActiveImageIndex(prevIndex); + setImageZoom(1); + setImageOffset({ x: 0, y: 0 }); + }, [activeImageIndex, attachments.length]); + + const goToNextImage = useCallback(() => { + if (activeImageIndex === null || attachments.length <= 1) return; + const nextIndex = activeImageIndex === attachments.length - 1 ? 0 : activeImageIndex + 1; + setActiveImageIndex(nextIndex); + setImageZoom(1); + setImageOffset({ x: 0, y: 0 }); + }, [activeImageIndex, attachments.length]); + + useEffect(() => { + if (activeImageIndex === null) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + goToPrevImage(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + goToNextImage(); + } else if (event.key === "Escape") { + event.preventDefault(); + setActiveImageIndex(null); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [activeImageIndex, goToPrevImage, goToNextImage]); + const previewCard = displayStatus.card; const mentionNames = useMemo(() => { if (!displayStatus.mentions || displayStatus.mentions.length === 0) { @@ -627,7 +665,7 @@ export const TimelineItem = ({ ) : null} {showContent - ? attachments.map((item) => ( + ? attachments.map((item, index) => ( + {attachments.length > 1 ? ( + + ) : null} + {attachments.length > 1 ? ( + + ) : null} 첨부 이미지 원본 + {attachments.length > 1 ? ( +
+ {(activeImageIndex ?? 0) + 1} / {attachments.length} +
+ ) : null} ) : null} diff --git a/src/ui/styles/components.css b/src/ui/styles/components.css index 6a2cc2e..c06d935 100644 --- a/src/ui/styles/components.css +++ b/src/ui/styles/components.css @@ -1343,6 +1343,49 @@ button.ghost { z-index: 2; } +.image-modal-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: white; + border: none; + border-radius: 50%; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 2; + transition: background 0.2s; +} + +.image-modal-nav:hover { + background: rgba(0, 0, 0, 0.7); +} + +.image-modal-nav-prev { + left: 12px; +} + +.image-modal-nav-next { + right: 12px; +} + +.image-modal-counter { + position: absolute; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.6); + color: white; + padding: 6px 14px; + border-radius: 16px; + font-size: 14px; + z-index: 2; +} + .confirm-modal { position: fixed; inset: 0;