기능 | 갤러리 - ssseok/wedding.invitation GitHub Wiki

목적

  • 신랑신부의 이야기와 관계를 사진을 통해 시각적으로 전달합니다.

설치

bunx --bun shadcn@latest add dialog
bunx --bun shadcn@latest add button

코드

import { useState } from 'react';
import { Dialog, DialogContent } from '@/common/components/ui/dialog';
import { Button } from '@/common/components/ui/button';
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
import Intersect from '@/common/components/intersect';

const SLICE_SIZE = 9;

export default function Gallery({ images }: { images: string[] }) {
  const [displayCount, setDisplayCount] = useState<number>(SLICE_SIZE);
  const [selectedImage, setSelectedImage] = useState<string | null>(null);
  const [currentIndex, setCurrentIndex] = useState<number>(0);

  const handleImageClick = (image: string, index: number) => {
    setSelectedImage(image);
    setCurrentIndex(index);
  };

  const handlePrevious = () => {
    setCurrentIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1));
  };

  const handleNext = () => {
    setCurrentIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0));
  };

  return (
    <div>
      <h2 className='text-center text-xl font-en'>gallery</h2>
      <div className='flex justify-center my-4'>
        <img src='/ring.png' alt='반지' className='w-6 h-6 aspect-square' />
      </div>

      {/* 이미지 그리드 */}
      <div className='grid grid-cols-3 gap-2'>
        {images.slice(0, displayCount).map((image, index) => (
          <Intersect key={index} type='data-animate'>
            <div
              className='aspect-square cursor-pointer overflow-hidden rounded-lg'
              onClick={() => handleImageClick(image, index)}
              data-animate-stage={(index % SLICE_SIZE) + 1}
            >
              <img
                src={image}
                alt={`Wedding photo ${index + 1}`}
                className='w-full h-full object-cover hover:scale-105 transition-transform duration-300'
              />
            </div>
          </Intersect>
        ))}
      </div>

      {/* 더보기 버튼 */}
      {displayCount < images.length && (
        <div className='flex justify-center mt-6'>
          <Button
            variant='outline'
            onClick={() => setDisplayCount((prev) => prev + SLICE_SIZE)}
          >
            <ChevronDown className='h-6 w-6' />
            사진 더보기
          </Button>
        </div>
      )}

      {/* 이미지 모달 */}
      <Dialog
        open={!!selectedImage}
        onOpenChange={() => setSelectedImage(null)}
      >
        <DialogContent className='max-w-4xl p-0 bg-transparent border-none'>
          <div className='relative'>
            <img
              src={images[currentIndex]}
              alt={`Wedding photo ${currentIndex + 1}`}
              className='w-full h-auto rounded-lg'
            />
            <Button
              variant='ghost'
              size='icon'
              className='absolute left-2 top-1/2 -translate-y-1/2 bg-background/80 hover:bg-background'
              onClick={handlePrevious}
            >
              <ChevronLeft className='h-6 w-6' />
            </Button>
            <Button
              variant='ghost'
              size='icon'
              className='absolute right-2 top-1/2 -translate-y-1/2 bg-background/80 hover:bg-background'
              onClick={handleNext}
            >
              <ChevronRight className='h-6 w-6' />
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  );
}
  • 상태 관리
    • displayCoun => 현재 화면에 표시되는 이미지 수를 관리합니다. 초기값은 SLICE_SIZE(9)입니다.
    • selectedImage => 현재 선택된 이미지의 URL을 저장합니다. null이면 모달이 닫힌 상태입니다.
    • currentIndex => 현재 모달(Dialog)에 보이는 이미지 중 이미지의 배열 내 인덱스를 저장합니다. 이전/다음 이미지 탐색에 사용됩니다.
  • 모달(Dialog)을 열어 이전/다음 함수
    • handleImageClick => 이미지 클릭 시 해당 이미지를 선택하고 모달(Dialog)을 열며, 현재 인덱스를 설정합니다.
    • handlePrevious => 이전 이미지로 이동하는 함수로, 첫 번째 이미지에서는 마지막 이미지로 순환합니다.
    • handleNext => 다음 이미지로 이동하는 함수로, 마지막 이미지에서는 첫 번째 이미지로 순환합니다.
  • 이미지 grid
    • 3열 grid로 이미지를 배치하였습니다.
    • slice 배열 함수를 사용하여 SLICE_SIZE(9)에 맞게 처음에는 9장이 보이도록 하였습니다.
    • 스크롤 애니메이션을 사용하기에 data-animate-stage={(index % SLICE_SIZE) + 1}을 사용하여 순차적으로 나올 수 있게 설계하였습니다.
    • 더보기 버튼을 만들어 표시된 이미지 수가 전체 이미지 수보다 적을 때만 버튼을 표시합니다.
  • 모달(Dialog)
    • Dialog에 있는 open을 통해 open={!!selectedImage}로 선택된 이미지가 있을 때만 모달을 표시합니다.
    • onOpenChange={() => setSelectedImage(null)}로 모달이 닫힐 때 선택된 이미지를 초기화합니다.
⚠️ **GitHub.com Fallback** ⚠️