Skip to content

Comments

[feature] 활동사진 확대 모달 구현 및 PhotoList 구조 리팩토링#371

Merged
oesnuj merged 20 commits intodevelop-fefrom
feature/#367-feed-image-modal-FE-101
May 9, 2025
Merged

[feature] 활동사진 확대 모달 구현 및 PhotoList 구조 리팩토링#371
oesnuj merged 20 commits intodevelop-fefrom
feature/#367-feed-image-modal-FE-101

Conversation

@oesnuj
Copy link
Member

@oesnuj oesnuj commented May 3, 2025

#️⃣ 연관된 이슈

#367

📝 작업 내용

활동사진을 클릭해 전체화면으로 볼 수 있는 모달 기능을 구현하고, 관련 로직들을 책임 단위로 분리하는 구조적 리팩토링을 진행했습니다.
또한 모바일 환경에서의 카드 크기 및 동작에 대한 UX 개선 작업도 병행

📸 1. 기능 추가

  • 활동사진 클릭 시 나오는 해당 사진 확대 모달(PhotoModal)
  • 모달 내 좌우 화살표 및 키보드 방향키로 사진 탐색 가능
  • 외부 영역 클릭 시 모달 닫힘 처리
  • 카드 클릭 시 clubName 전달 처리
💻 데스크탑 화면 📱 모바일 화면

🧼 2. 리팩토링

PhotoList가 너무 많은 책임을 지고 있었기 때문에 상태와 로직, 컴포넌트를 명확히 분리했습니다.
(useState가 10개가 넘었...)

🔧 상태/로직 관련

  • usePhotoNavigation

    • 리스트 뷰에서 카드 슬라이딩 탐색 로직 담당
    • 카드 너비/컨테이너 넓이 기반 translateX 계산
    • 좌우 버튼 클릭 시 인덱스 이동 및 애니메이션 적용
    • 모바일 여부에 따라 탐색 한계 조절
    • Mixpanel 이벤트 트래킹 연동 포함
  • useModalNavigation

    • 모달 내 좌/우 순환 탐색 로직 담당
    • 경계 도달 시 첫/끝으로 순환 처리
  • usePhotoModal

    • 모달의 열림 상태(isOpen) 및 현재 인덱스 관리
    • open(index) / close() 및 외부에서 setIndex 제공
  • useResponsiveLayout

    • 디바이스 크기에 따라 카드 너비 및 컨테이너 폭 계산
    • cardWidth memoization 및 isMobile 판별 포함

🧩 컴포넌트 분리

  • PhotoModal: 전체화면 확대 보기 컴포넌트
  • PhotoCardList: 썸네일 카드 리스트 전용 컴포넌트

🐛 버그 수정

  • 모바일 너비 400px (예: Galaxy S24)에서 카드 짤림 현상 → 너비 300px로 조정

    • 다만 이러니 다음 카드가 일부 보이는 UX 이슈 존재
    • 확대 고려해 차라리 짤리는 것보다 낫다고 판단하여 유지
  • 불필요한 lastCard 및 placeholder 관련 코드 제거

    • 스타일 복잡성을 유발하고, 유의미한 역할이 없어 제거

🔍 중점적으로 리뷰받고 싶은 부분

  • 훅/컴포넌트 책임 분리 방향이 적절한지
  • 모달 내 UX 흐름 (특히 모바일에서)이 자연스러운지
  • 놓친 실수나 더 간결해질 수 있는 부분이 있다면 가감 없이 피드백 부탁드립니다

🧠 논의하고 싶은 부분

커서 AI 써봤는데 시작 틀은 엄청 잘짜주는데, 일정 수준 이상 복잡해지니 코드를 망쳐 버리더라구요....
어느 정도 이후는 챗GPT랑 이야기하면서 짜는 방식으로 마무리 했습니다!
추후에는 Swiper 도입 방향으로 리팩토링하시죠
사진 앨범 하나에 이렇게 시간이 오래 걸릴 줄은 몰랐네요 (6시간.....)

Summary by CodeRabbit

  • 신규 기능

    • 클럽 상세 페이지 사진 리스트에 모달 뷰어가 추가되어 사진을 크게 감상할 수 있습니다.
    • 사진 리스트가 모바일 및 데스크톱 환경에 따라 반응형으로 동작합니다.
    • 썸네일 목록과 좌우 이동 버튼을 통한 사진 탐색 기능이 제공됩니다.
    • 사진 카드 레이아웃과 간격이 정의된 상수로 일관성 있게 적용됩니다.
  • 리팩터링

    • 사진 리스트 및 모달 관련 로직이 커스텀 훅과 컴포넌트로 분리되어 구조가 단순화되고 유지보수가 용이해졌습니다.
    • 이미지 오류 처리, 네비게이션 로직, 반응형 레이아웃 관리가 모바일 환경을 고려해 개선되었습니다.
    • 사진 리스트 컴포넌트가 모듈화되고, 내부 상태 관리가 간소화되었습니다.
  • 스타일

    • 사진 리스트와 모달에 개선된 스타일이 적용되어 사용자 경험이 향상되었습니다.
    • 모달 내 썸네일과 내비게이션 버튼에 터치 및 호버 효과가 추가되었습니다.
    • 이미지 선택 및 드래그 방지, 터치 기기에서의 탭 하이라이트 제거 등 사용자 인터랙션이 개선되었습니다.

@oesnuj oesnuj added ✨ Feature 기능 개발 🎨 Design 마크업 & 스타일링 🔨 Refactor 코드 리팩토링 💻 FE Frontend 🛠Fix 기능이 의도한 대로 동작하지 않는 버그를 수정 labels May 3, 2025
@oesnuj oesnuj requested review from Copilot and seongwon030 May 3, 2025 10:32
@oesnuj oesnuj self-assigned this May 3, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 3, 2025

Warning

Rate limit exceeded

@oesnuj has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 7 minutes and 32 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 41854a0 and 7e2b62f.

📒 Files selected for processing (1)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx (1 hunks)

"""

Walkthrough

이 변경사항은 동아리 상세 페이지에서 활동사진을 확대해서 볼 수 있는 모달 기능을 추가하는 대규모 리팩토링 및 신규 구현입니다. 사진 레이아웃 관련 상수와 반응형 레이아웃, 사진 리스트, 모달 상태 관리, 사진 내비게이션을 위한 커스텀 훅들이 새로 도입되었습니다. PhotoList 컴포넌트가 리팩토링되어 사진 목록, 에러 처리, 클릭 시 모달 오픈, 내비게이션 버튼 등 모든 사진 관련 UI/UX를 담당하게 되었으며, 사진 모달 전용 컴포넌트와 스타일 파일도 새로 추가되었습니다.

Changes

파일/경로 그룹 변경 요약
frontend/src/constants/photoLayout.ts 사진 카드 레이아웃 관련 상수 파일 신설
frontend/src/hooks/PhotoList/usePhotoModal.ts
frontend/src/hooks/PhotoList/useResponsiveLayout.ts
frontend/src/hooks/PhotoModal/useModalNavigation.ts
사진 모달 상태, 반응형 레이아웃, 모달 내비게이션용 커스텀 훅 신설
frontend/src/hooks/PhotoList/usePhotoNavigation.ts 내비게이션 훅에 isMobile 파라미터 추가, 내비게이션 경계 조건 및 반환값 변경, named export로 전환
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx PhotoList 컴포넌트에 clubName prop 추가
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx 사진 카드 리스트 전용 컴포넌트 신설, 이미지 클릭/에러 핸들링 지원
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts PhotoList, PhotoCard 스타일 컴포넌트 단순화 및 props 축소, 이미지 드래그/선택 방지 추가
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx PhotoList 리팩토링: 사진 URL, 에러, 반응형, 내비게이션, 모달 등 역할 분리 및 훅/컴포넌트화, 모달 연동
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts PhotoModal 전용 스타일 컴포넌트 신설, 반응형/내비게이션/썸네일 등 세부 스타일 정의
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx PhotoModal 컴포넌트 신설: 오버레이, 닫기, 사진 전환, 썸네일, 키보드 내비게이션 등 구현
frontend/src/utils/debounce.ts debounce 함수 선언에서 화살표 함수 표현식으로 변경, 타입 안전성 강화

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PhotoList
    participant PhotoModal

    User->>PhotoList: 사진 카드 클릭(index)
    PhotoList->>PhotoModal: 모달 오픈(index, 사진 URL 배열, clubName)
    User->>PhotoModal: 좌/우 내비게이션 클릭 또는 썸네일 클릭
    PhotoModal->>PhotoModal: 현재 사진 인덱스 변경
    User->>PhotoModal: ESC 키 또는 오버레이 클릭
    PhotoModal->>PhotoList: 모달 닫기
Loading

Assessment against linked issues

Objective Addressed Explanation
동아리 상세페이지 이미지 피드 모달 기능을 추가 (FE-101)
사진 확대, 모달 내비게이션, 반응형 지원 등 구현 (FE-101)

Possibly related PRs

  • Moadong/moadong#238: LazyImage 컴포넌트 및 usePhotoNavigation 훅 도입, 본 PR에서 해당 컴포넌트와 훅을 활용 및 확장함.
  • Moadong/moadong#283: usePhotoNavigation 훅에 Mixpanel 트래킹 추가, 본 PR의 훅 변경과 코드 레벨에서 연관됨.

Suggested reviewers

  • seongwon030
    """
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@netlify
Copy link

netlify bot commented May 3, 2025

Deploy Preview for moadong ready!

Name Link
🔨 Latest commit 7e2b62f
🔍 Latest deploy log https://app.netlify.com/sites/moadong/deploys/681db59095ac500008a4524c
😎 Deploy Preview https://deploy-preview-371--moadong.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 38
Accessibility: 87
Best Practices: 92
SEO: 92
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements an activity photo enlargement modal and refactors the PhotoList component to separate concerns by introducing dedicated hooks for navigation, modal state, and responsive layout.

  • Added a PhotoModal component with keyboard navigation and overlay click-to-close behavior.
  • Refactored PhotoList to use new hooks (usePhotoNavigation, useResponsiveLayout, and usePhotoModal) and extracted the thumbnail list into a separate PhotoCardList component.
  • Updated styles for PhotoList and PhotoModal to improve UX on mobile and desktop screens.

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
frontend/src/pages/ClubDetailPage/components/PhotoModal/PhotoModal.tsx Adds modal functionality with keyboard and click-based controls.
frontend/src/pages/ClubDetailPage/components/PhotoModal/PhotoModal.styles.ts Provides styling for the modal overlay, content, and navigation buttons.
frontend/src/pages/ClubDetailPage/components/PhotoList.tsx Refactors PhotoList with new hooks and integrates the PhotoModal.
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts Adjusts styling to support the new layout and navigation logic.
frontend/src/pages/ClubDetailPage/components/PhotoCardList/PhotoCardList.tsx Extracts the thumbnail rendering logic into its own component.
frontend/src/hooks/PhotoModal/useModalNavigation.ts Provides cyclic navigation logic within the modal.
frontend/src/hooks/PhotoList/useResponsiveLayout.ts Manages layout responsiveness by monitoring container width and mobile breakpoints.
frontend/src/hooks/PhotoList/usePhotoNavigation.ts Simplifies card translation logic and navigation for the photo slider.
frontend/src/hooks/PhotoList/usePhotoModal.ts Manages modal open/close state and current index for the photo modal.
frontend/src/constants/photoLayout.ts Defines constants for card dimensions and gap.
Comments suppressed due to low confidence (1)

frontend/src/hooks/PhotoList/usePhotoNavigation.ts:25

  • The translation logic was simplified to '-index * cardWidth' without offset adjustment for the last card. Please verify that on desktop views this behavior is intended and does not result in undesirable empty space when the last card is reached.
const calculateTranslateX = useCallback((index: number) => -index * cardWidth, [cardWidth]);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (17)
frontend/src/constants/photoLayout.ts (1)

5-6: 가독성 향상을 위한 제안

상수 파일에 간단한 주석을 추가하면 이 상수들의 용도와 사용처를 쉽게 이해할 수 있습니다.

+// 카드 간격 및 크기에 대한 레이아웃 상수
 export const CARD_GAP = 20;
 export const DESKTOP_CARD_CONTENT_WIDTH = 400;
 export const MOBILE_CARD_CONTENT_WIDTH = 350;
 
+// 전체 카드 너비 (컨텐츠 + 간격)
 export const DESKTOP_CARD_WIDTH = DESKTOP_CARD_CONTENT_WIDTH + CARD_GAP;
 export const MOBILE_CARD_WIDTH = MOBILE_CARD_CONTENT_WIDTH + CARD_GAP;
frontend/src/hooks/PhotoList/usePhotoModal.ts (2)

7-11: 더 안전한 타입 정의 제안

인덱스 매개변수에 명시적 타입을 추가하면 좋을 것 같습니다.

- const open = (i: number) => {
+ const open = (i: number): void => {
    setIndex(i);
    setIsOpen(true);
  };
- const close = () => setIsOpen(false);
+ const close = (): void => setIsOpen(false);

13-14: 반환 타입 명시를 고려해 보세요

훅의 반환 타입을 명시적으로 정의하면 타입 안전성이 향상되고 사용하는 컴포넌트에서 더 명확한 자동 완성 기능을 제공할 수 있습니다.

- export const usePhotoModal = () => {
+ export const usePhotoModal = (): {
+   isOpen: boolean;
+   index: number;
+   open: (i: number) => void;
+   close: () => void;
+   setIndex: React.Dispatch<React.SetStateAction<number>>;
+ } => {
  // ... 기존 코드
frontend/src/hooks/PhotoModal/useModalNavigation.ts (1)

16-17: 반환 타입 명시를 고려해 보세요

반환 객체에 명시적인 타입을 추가하면 타입 안전성과 자동 완성 기능이 향상됩니다.

- return { handlePrev, handleNext };
+ return { 
+   handlePrev, 
+   handleNext 
+ } as const;

혹은 함수 선언부에 반환 타입을 명시:

export default function useModalNavigation(
  currentIndex: number,
  total: number,
  setIndex: (index: number) => void,
- ) {
+ ): { 
+   handlePrev: () => void; 
+   handleNext: () => void; 
+ } {
frontend/src/hooks/PhotoList/useResponsiveLayout.ts (3)

13-29: 이벤트 리스너 최적화 검토

리사이즈 이벤트 리스너가 모든 창 크기 변경 시 호출됩니다. 성능 최적화를 위해 디바운싱을 적용하는 것이 좋을 것 같습니다.

+ import { useEffect, useState, useMemo, useCallback } from 'react';
+ import { debounce } from 'lodash';

  useEffect(() => {
    const updateIsMobile = () => setIsMobile(window.innerWidth <= breakPoint);
    const updateContainerWidth = () => {
      if (ref.current) {
        setContainerWidth(ref.current.offsetWidth);
      }
    };

-   const handleResize = () => {
+   const handleResize = useCallback(() => {
      updateIsMobile();
      updateContainerWidth();
-   };
+   }, [updateIsMobile, updateContainerWidth]);
+   
+   const debouncedHandleResize = debounce(handleResize, 150);

    handleResize();
-   window.addEventListener('resize', handleResize);
-   return () => window.removeEventListener('resize', handleResize);
+   window.addEventListener('resize', debouncedHandleResize);
+   return () => {
+     window.removeEventListener('resize', debouncedHandleResize);
+     debouncedHandleResize.cancel();
+   };
  }, [ref, breakPoint]);

15-19: ref.current가 null인 경우 처리 강화

현재 ref.current 체크가 있지만, 컨테이너 너비 업데이트 로직을 더 견고하게 만들 수 있습니다.

  const updateContainerWidth = () => {
    if (ref.current) {
      setContainerWidth(ref.current.offsetWidth);
+   } else {
+     // 기본값 설정 또는 로깅
+     setContainerWidth(window.innerWidth);
+     console.debug('Container reference is not available yet');
    }
  };

31-34: 카드 너비 계산 로직에 대한 주석 추가

카드 너비 계산 로직에 대한 설명을 주석으로 추가하면 코드 이해도가 향상될 것입니다.

+ // 디바이스 타입에 따라 적절한 카드 너비를 설정
+ // 모바일에서는 좁은 화면에 맞는 작은 카드 크기를, 데스크톱에서는 더 큰 카드 크기를 사용
  const cardWidth = useMemo(
    () => (isMobile ? MOBILE_CARD_WIDTH : DESKTOP_CARD_WIDTH),
    [isMobile],
  );
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts (1)

43-44: 조건부 transform 로직 개선

photoCount가 2 이하일 때 'none'을 사용하는 부분이 명확한 이유가 없어 보입니다. 이 부분에 주석을 추가하거나 다른 방식으로 접근하는 것이 좋을 것 같습니다.

  transform: ${({ translateX, photoCount }) =>
-   photoCount <= 2 ? 'none' : `translateX(${translateX}px)`};
+   // 사진이 2개 이하일 때는 슬라이딩이 필요하지 않으므로 transform을 적용하지 않음
+   photoCount <= 2 ? 'none' : `translateX(${translateX}px)`};
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx (2)

20-32: 배열 key 속성으로 index 사용에 대한 제안

현재 key로 index를 사용하고 있는데, 가능하다면 고유한 식별자(예: URL 해시나 ID)를 사용하는 것이 React 렌더링 성능에 더 좋습니다.

  {photoUrls.map((url, index) => (
-   <Styled.PhotoCard key={index} onClick={() => onImageClick(index)}>
+   <Styled.PhotoCard 
+     key={`photo-${url.split('/').pop() || index}`} 
+     onClick={() => onImageClick(index)}>

22-30: 이미지 오류 처리 로직 강화

현재 이미지 오류 처리가 잘 구현되어 있습니다만, 오류 상태일 때의 사용자 경험을 더 개선할 수 있습니다.

  {!imageErrors[index] ? (
    <LazyImage
      src={url}
      alt={`활동 사진 ${index + 1}`}
      onError={() => onImageError(index)}
    />
  ) : (
-   <Styled.NoImageContainer>이미지 준비중..</Styled.NoImageContainer>
+   <Styled.NoImageContainer>
+     <div>
+       <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+         <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" fill="#666"/>
+       </svg>
+       <p>이미지 준비중..</p>
+     </div>
+   </Styled.NoImageContainer>
  )}

이에 맞춰 스타일 파일도 업데이트해야 합니다:

// PhotoList.styles.ts에 추가
export const NoImageContainer = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f5f5f5;
  color: #666;
  
  div {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
  }
  
  p {
    font-size: 14px;
  }
`;
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx (3)

37-50: 키보드 이벤트 핸들링 개선 제안

키보드 이벤트 핸들링 로직이 잘 구현되어 있지만, 몇 가지 개선 사항을 제안합니다.

  const handleKeyDown = useCallback(
    (e: KeyboardEvent) => {
      if (!isOpen) return;
-     if (e.key === 'Escape') return onClose();
-     if (e.key === 'ArrowLeft') return handlePrev();
-     if (e.key === 'ArrowRight') return handleNext();
+     switch (e.key) {
+       case 'Escape':
+         e.preventDefault();
+         onClose();
+         break;
+       case 'ArrowLeft':
+         e.preventDefault();
+         handlePrev();
+         break;
+       case 'ArrowRight':
+         e.preventDefault();
+         handleNext();
+         break;
+     }
    },
    [isOpen, onClose, handlePrev, handleNext],
  );

이렇게 하면 방향키 이벤트가 스크롤 등 다른 동작을 트리거하는 것을 방지할 수 있습니다.


54-62: 모달 접근성 개선 제안

모달의 접근성을 향상시키기 위한 ARIA 속성을 추가하는 것이 좋을 것 같습니다.

- <Styled.ModalOverlay onClick={onClose}>
+ <Styled.ModalOverlay 
+   onClick={onClose}
+   role="dialog"
+   aria-modal="true"
+   aria-labelledby="modal-title">
    <Styled.ModalContent onClick={(e) => e.stopPropagation()}>
      <Styled.ModalHeader>
-       <Styled.ClubName>{clubName}</Styled.ClubName>
+       <Styled.ClubName id="modal-title">{clubName}</Styled.ClubName>
        <Styled.CloseButton onClick={onClose} aria-label='닫기'>
          ×
        </Styled.CloseButton>

85-93: 썸네일 리스트 최적화 제안

썸네일 리스트의 경우 사진 수가 많아질 때 성능 문제가 발생할 수 있습니다. 필요에 따라 가상화 또는 페이징 방식을 고려해볼 수 있습니다.

  <Styled.ThumbnailList>
-   {urls.map((url, idx) => (
+   {/* 썸네일이 많을 경우 가상화 또는 페이징 고려 */}
+   {urls.slice(0, 10).map((url, idx) => (
      <Styled.Thumbnail
        key={url}
        isActive={idx === currentIndex}
        onClick={() => onChangeIndex(idx)}>
        <img src={url} alt='썸네일' />
      </Styled.Thumbnail>
    ))}
+   {urls.length > 10 && (
+     <Styled.MoreThumbnails>+{urls.length - 10}</Styled.MoreThumbnails>
+   )}
  </Styled.ThumbnailList>
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx (1)

79-88: PhotoModal 컴포넌트 구현

PhotoModal 컴포넌트를 추가하고 필요한 props를 전달하는 구현은 잘 되어 있습니다. 사진 확대 모달 기능을 별도 컴포넌트로 분리하여 관심사 분리를 잘 적용했습니다.

한 가지 개선점으로는, photos 객체를 구조분해할당으로 직접 전달하는 것이 더 명확할 수 있습니다:

<PhotoModal
  isOpen={isOpen}
  onClose={close}
  clubName={clubName}
- photos={{
-   currentIndex: index,
-   urls: photoUrls,
-   onChangeIndex: setIndex,
- }}
+ currentIndex={index}
+ photoUrls={photoUrls}
+ onChangeIndex={setIndex}
/>

이렇게 하면 PhotoModal 컴포넌트에서 props 사용이 더 직관적이고 타입 안전성도 향상됩니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts (3)

46-61: 닫기 버튼 스타일 구현

닫기 버튼의 위치와 호버 효과가 적절하게 구현되어 있습니다. 특히 opacity 변화를 통한 자연스러운 상호작용은 좋은 디테일입니다.

다만, 버튼에 접근성을 위한 aria-label 속성이 필요할 수 있습니다. 컴포넌트 구현 시 이 점을 고려해 보세요.


120-162: 내비게이션 버튼 스타일

내비게이션 버튼의 스타일링이 매우 세밀하게 구현되어 있습니다. 원형 버튼과 그림자 효과, 호버 시 변화, 반응형 크기 조정 등이 사용자 경험을 향상시키는 좋은 디테일입니다.

다만, 좌우 버튼이 화면 밖으로 벗어나거나 이미지에 가려질 수 있는 상황에 대한 고려가 필요할 수 있습니다. z-index 충돌이 발생하지 않는지 테스트해 보세요.


207-236: 썸네일 스타일

개별 썸네일의 스타일링이 잘 구현되어 있습니다. 활성 상태 표시를 위한 테두리 색상 변경, 호버 효과, 반응형 크기 조정 등이 사용자 경험을 향상시키는 좋은 디테일입니다.

접근성 측면에서 한 가지 개선 사항으로, 현재 선택된 썸네일임을 스크린 리더에게 알려주는 aria-current="true" 속성 추가를 고려해 보세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 548ac6b and 64c8883.

📒 Files selected for processing (11)
  • frontend/src/constants/photoLayout.ts (1 hunks)
  • frontend/src/hooks/PhotoList/usePhotoModal.ts (1 hunks)
  • frontend/src/hooks/PhotoList/usePhotoNavigation.ts (3 hunks)
  • frontend/src/hooks/PhotoList/useResponsiveLayout.ts (1 hunks)
  • frontend/src/hooks/PhotoModal/useModalNavigation.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts (1 hunks)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
frontend/src/hooks/PhotoList/useResponsiveLayout.ts (1)
frontend/src/constants/photoLayout.ts (2)
  • MOBILE_CARD_WIDTH (6-6)
  • DESKTOP_CARD_WIDTH (5-5)
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx (1)
frontend/src/hooks/PhotoModal/useModalNavigation.ts (1)
  • useModalNavigation (3-17)
🔇 Additional comments (30)
frontend/src/constants/photoLayout.ts (1)

1-6: 상수 정의가 명확하고 잘 구성되어 있습니다.

상수를 중앙에서 관리하는 접근 방식은 유지보수성을 높이는 좋은 패턴입니다. 카드 간격과 너비를 명확히 분리하여 정의한 것이 좋습니다.

frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)

64-68: 새로운 clubName prop 추가가 적절합니다.

PhotoList 컴포넌트에 clubName prop을 추가하여 모달에서 클럽 이름을 표시하는 기능이 가능해졌습니다. 이 변경은 간결하고 명확합니다.

frontend/src/hooks/PhotoList/usePhotoModal.ts (1)

1-14: 모달 상태 관리 훅이 잘 구현되었습니다.

usePhotoModal 훅은 모달의 열림/닫힘 상태와 현재 사진 인덱스를 관리하는 책임을 명확히 분리하여 구현했습니다. 단일 책임 원칙(SRP)을 잘 따르고 있으며, 인터페이스가 깔끔합니다.

frontend/src/hooks/PhotoModal/useModalNavigation.ts (2)

3-7: 함수 시그니처가 명확합니다.

함수의 매개변수와 반환 타입이 명확하게 정의되어 있습니다. 모달 내비게이션에 필요한 정보를 적절하게 받고 있습니다.


8-14: 원형 내비게이션 로직이 잘 구현되었습니다.

이전/다음 사진으로 이동하는 로직이 경계 조건(처음과 끝)을 잘 처리하고 있으며, useCallback을 사용하여 성능 최적화도 고려했습니다.

frontend/src/hooks/PhotoList/useResponsiveLayout.ts (1)

1-37: 훅 전체 코드 구조에 대한 피드백

useResponsiveLayout 훅은 컨테이너 참조, 모바일 여부, 컨테이너 너비, 카드 너비 등 여러 관심사를 처리하고 있어 응집도가 높습니다. 코드가 깔끔하게 구현되었고 책임 분리 측면에서도 좋은 접근이라고 생각합니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts (3)

46-46: user-select 속성 적용에 대한 피드백

user-select: none 속성 추가는 사용자 경험 향상에 좋은 개선입니다. 사진 슬라이딩 중 텍스트가 선택되는 문제를 방지할 수 있습니다.


57-57: cursor 속성 설정에 대한 피드백

PhotoCard에 cursor: pointer 속성을 적용한 것은 좋은 UX 개선입니다. 사용자에게 카드가 클릭 가능함을 시각적으로 알려줍니다.


64-65: 이미지 드래그 방지 스타일 추가에 대한 피드백

이미지 요소에 user-select와 -webkit-user-drag 속성을 추가한 것은 사용자 경험을 향상시키는 좋은 개선입니다. 사용자가 실수로 이미지를 드래그하거나 선택하는 것을 방지합니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx (1)

1-37: 컴포넌트 구조 및 책임 분리에 대한 피드백

PhotoCardList 컴포넌트의 책임 분리가 잘 되어 있습니다. 단일 책임 원칙을 잘 따르고 있으며, props 인터페이스도 명확하게 정의되어 있습니다. 이미지 에러 핸들링도 적절히 구현되었습니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx (2)

26-35: body overflow 스타일 처리에 대한 피드백

모달 오픈/클로즈 시 document.body의 overflow 스타일을 직접 조작하는 부분이 잘 구현되어 있습니다. useEffect 클린업 함수에서도 적절하게 처리되어 있습니다.


1-101: 모달 컴포넌트 전체 구현에 대한 피드백

PhotoModal 컴포넌트가 전반적으로 아주 잘 구현되어 있습니다. 키보드 네비게이션 지원, 썸네일 리스트, 사진 이동 버튼 등 사용자 경험을 고려한 다양한 기능들이 포함되어 있습니다. 특히 useModalNavigation 훅을 사용해 순환 탐색 로직을 분리한 것이 좋은 설계 결정입니다.

frontend/src/hooks/PhotoList/usePhotoNavigation.ts (3)

4-4: 네이밍된 export로 변경 및 isMobile 파라미터 추가

기본 export에서 네이밍된 export로 변경하고 isMobile 파라미터를 추가한 것은 좋은 방향입니다. 이를 통해 모바일 환경에 맞는 동작을 구현할 수 있고, 명시적인 import 방식으로 코드 가독성이 향상됩니다.

Also applies to: 11-11, 20-20


24-26: calculateTranslateX 함수 단순화

이전 코드에서 마지막 사진에 대한 특별한 처리 로직을 제거하고 단순화한 것은 코드 유지보수성 측면에서 좋은 개선입니다. 또한 의존성 배열을 [cardWidth]로 최소화하여 불필요한 재계산을 방지했습니다.


46-49: 스크롤 가능 조건 개선

모바일 환경을 고려한 스크롤 조건 개선이 이루어졌습니다. 특히:

  1. canScrollLeft 조건을 photosLength > 1로 변경하여 사진이 2개 이상일 때 좌측 스크롤이 가능하도록 개선
  2. canScrollRight 조건을 모바일 여부에 따라 다르게 처리하여 사용자 경험 향상

이 변경은 모바일과 데스크톱 환경에서의 네비게이션 경험을 최적화하는 좋은 접근입니다.

frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx (10)

1-1: 모듈 분리 및 import 구조 개선

컴포넌트와 훅을 적절히 분리하고 필요한 모듈을 명시적으로 import한 것은 좋은 접근입니다. 특히 PhotoModal, PhotoCardList 컴포넌트와 useResponsiveLayout, usePhotoModal 훅을 분리하여 관심사 분리 원칙을 잘 적용했습니다.

Also applies to: 4-10


15-15: clubName 속성 추가

clubName 속성을 추가하여 모달에서 동아리 이름을 표시할 수 있게 한 것은 사용자 경험 측면에서 좋은 개선입니다.

Also applies to: 18-18


19-22: photoUrls 메모이제이션

useMemo를 사용하여 Google Drive URL 변환 로직을 메모이제이션한 것은 성능 최적화에 도움이 됩니다. 이를 통해 불필요한 재계산을 방지할 수 있습니다.


26-27: 상태 관리 개선

imageErrors 상태를 Record<number, boolean> 타입으로 정의하고, usePhotoModal 훅을 사용하여 모달 상태를 관리하는 방식은 코드 가독성과 유지보수성을 높여줍니다.


29-31: 반응형 레이아웃 처리

useResponsiveLayout 훅을 사용하여 반응형 레이아웃을 처리하는 방식은 기존의 수동 상태 관리 및 window resize 이벤트 처리보다 훨씬 효율적입니다. 코드가 간결해지고 관심사 분리가 잘 이루어졌습니다.


33-43: usePhotoNavigation 훅 활용 개선

usePhotoNavigation 훅에 필요한 파라미터를 적절히 전달하고 있습니다. 특히 isMobile 값을 전달하여 모바일 환경에 맞는 내비게이션 동작을 구현한 것은 좋은 접근입니다.


45-47: handleImageError 최적화

useCallback을 사용하여 handleImageError 함수를 최적화한 것은 좋은 접근입니다. 이를 통해 불필요한 렌더링을 방지할 수 있습니다.


49-49: openModalAt 콜백 함수 추가

openModalAt 콜백 함수를 추가하여 특정 인덱스에서 모달을 열 수 있도록 한 것은 코드의 재사용성과 가독성을 높여줍니다.


59-66: PhotoCardList 컴포넌트 활용

PhotoCardList 컴포넌트를 사용하여 사진 카드 렌더링을 분리한 것은 관심사 분리 측면에서 훌륭한 접근입니다. 이를 통해 코드 복잡성이 줄어들고 유지보수성이 향상됩니다.


69-69: 이미지 리소스 활용

단순 화살표 대신 SlideButton 이미지를 사용하여 네비게이션 버튼의 시각적 품질을 향상시킨 것은 사용자 경험 측면에서 좋은 개선입니다.

Also applies to: 74-74

frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts (5)

3-25: 모달 오버레이 스타일 구현

전체 화면을 커버하는 반투명 오버레이와 블러 효과, 페이드인 애니메이션은 모달 UX를 자연스럽게 만들어 주는 좋은 구현입니다. 이를 통해 사용자는 모달이 열리는 과정을 시각적으로 인지하고 배경과 모달을 명확히 구분할 수 있습니다.


27-44: 모달 콘텐츠 스타일 구현

모달 콘텐츠의 반응형 스타일링이 잘 구현되어 있습니다. 특히 모바일 환경(600px 이하)에서 전체 화면으로 전환되는 처리는 작은 화면에서의 사용자 경험을 고려한 좋은 접근입니다.


63-74: 모달 헤더와 클럽 이름 스타일

헤더의 고정 위치 처리와 클럽 이름 스타일링이 잘 구현되어 있습니다. 이를 통해 사용자는 현재 보고 있는 동아리의 사진임을 계속 인지할 수 있습니다.

Also applies to: 76-80


82-88: 모달 바디와 이미지 컨테이너, 이미지 스타일

모달 바디, 이미지 컨테이너, 이미지 자체의 스타일링이 잘 구현되어 있습니다. 특히:

  1. 이미지의 최대 크기 제한을 통한 적절한 표시
  2. 반응형 패딩 조정으로 모바일 환경 대응
  3. 사용자 드래그 및 선택 방지를 통한 UX 개선

이 모든 스타일링이 사진 확대 모달의 핵심 기능을 잘 지원합니다.

Also applies to: 90-102, 104-118


164-177: 썸네일 컨테이너와 리스트 스타일

썸네일 영역의 스타일링이 전체적으로 잘 구현되어 있습니다. 특히:

  1. 그라데이션 배경을 통한 시각적 구분
  2. 커스텀 스크롤바 스타일링
  3. 반응형 높이 및 패딩 조정

이러한 디테일은 사용자가 여러 사진을 쉽게 탐색할 수 있도록 돕습니다.

Also applies to: 179-205

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

복잡했던 활동사진 로직이 깔끔해져서 좋습니다 ㅎㅎ 정말 고생하셨습니다 궁금한 점 리뷰했습니다...!!!

@oesnuj oesnuj removed the 🎨 Design 마크업 & 스타일링 label May 3, 2025
@oesnuj oesnuj removed the 🛠Fix 기능이 의도한 대로 동작하지 않는 버그를 수정 label May 3, 2025
@oesnuj oesnuj merged commit 79195c5 into develop-fe May 9, 2025
5 checks passed
@oesnuj oesnuj deleted the feature/#367-feed-image-modal-FE-101 branch May 10, 2025 08:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants