Skip to content

[feature] 활동사진 그리드 레이아웃 및 Swiper 기반 모달 네비게이션 구현#965

Merged
oesnuj merged 2 commits intodevelop-fefrom
feature/#953-club-detail-activity-photos-MOA-447
Dec 23, 2025
Merged

[feature] 활동사진 그리드 레이아웃 및 Swiper 기반 모달 네비게이션 구현#965
oesnuj merged 2 commits intodevelop-fefrom
feature/#953-club-detail-activity-photos-MOA-447

Conversation

@oesnuj
Copy link
Member

@oesnuj oesnuj commented Dec 23, 2025

#️⃣연관된 이슈

#953

📝작업 내용

활동사진 UI를 피드/그리드 형태로 전면 개선

그리드 레이아웃 적용

  • 기존 단일 컬럼 리스트에서 반응형 그리드 레이아웃으로 변경
  • 화면 크기별 최적화된 컬럼 수 적용
화면 크기 컬럼 수
노트북, 데스크톱 (700px~) 4열
모바일, 태블릿 (~700px) 3열
  • 이미지 카드 형태로 시각적 개선
  • 호버 효과 및 인터랙션 추가

Before (리스트 형태)

image

After (좌우 스크롤 형태)

데스크톱 모바일
데스크톱 그리드 모바일 그리드

사진 모달 인터랙션 강화

  • Swiper 라이브러리를 활용한 이미지 슬라이드 네비게이션 구현
  • 하단 썸네일 뷰어 추가로 빠른 이미지 이동 지원
  • 키보드 방향키 및 화살표 버튼으로 네비게이션

레이아웃 최적화

  • 이미지 표시 영역 최대화 및 여백 최소화
  • 모바일/태블릿 반응형 처리
  • 썸네일 크기 및 간격 조정

중점적으로 리뷰받고 싶은 부분(선택)

  • 그리드 레이아웃의 반응형 브레이크포인트가 적절한지
  • 이미지 로딩 성능 및 최적화 방안
  • 모달 내 Swiper 네비게이션 UX

논의하고 싶은 부분(선택)

  • 그리드 컬럼 수를 현재보다 더 조정해야 할지
  • 이미지 lazy loading 적용 여부

🫡 참고사항

  • Instagram/Pinterest 스타일의 피드 형태로 사용자 경험 개선
  • 모바일에서 스와이프 제스처로 이미지 네비게이션 가능

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 동아리 사진 그리드 보기 추가
    • 개선된 사진 모달 뷰어 도입 (터치/키보드 네비게이션 지원)
    • 썸네일 스크롤을 통한 빠른 사진 선택 기능
  • 개선 사항

    • 모달 인터페이스를 전체 화면 레이아웃으로 재설계
    • 모바일 및 태블릿 환경 최적화
    • 사진 표시 품질 및 반응성 향상

✏️ Tip: You can customize this high-level summary in your review settings.

- Swiper 라이브러리 적용으로 좌우 슬라이드 및 키보드 네비게이션 구현
- 하단 썸네일 리스트 추가 및 자동 스크롤 기능
- 이미지 표시 영역 최적화 및 불필요한 코드 제거
@oesnuj oesnuj requested a review from seongwon030 December 23, 2025 15:22
@oesnuj oesnuj self-assigned this Dec 23, 2025
@oesnuj oesnuj added ✨ Feature 기능 개발 🎨 Design 마크업 & 스타일링 💻 FE Frontend labels Dec 23, 2025
@vercel
Copy link

vercel bot commented Dec 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
moadong Ready Ready Preview, Comment Dec 23, 2025 3:22pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 23, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • 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

개요

동아리 활동 사진을 표시하는 새로운 ClubFeed 컴포넌트를 추가하고, PhotoModal을 Swiper 기반 이미지 뷰어로 전환했습니다. 모달 레이아웃을 전체 뷰포트로 확장하고 썸네일 동기화 기능을 구현했습니다.

변경사항

응집단 / 파일 변경 요약
PhotoModal 스타일 재설계
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts
모달 오버레이에서 중앙 정렬 제거; 모달 콘텐츠를 90vw/90vh에서 100vw/100vh 전체 뷰포트로 변경 및 보더 라디우스/그림자 제거. 이미지 컨테이너를 고정 높이에서 유연한 레이아웃(flex: 1)으로 변경. 썸네일 컨테이너에 배경, 상단 테두리, 패딩 추가. 모바일/태블릿에서 내비게이션 버튼 숨김.
PhotoModal Swiper 통합
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx
수동 이미지 네비게이션 로직을 Swiper 기반 캐러셀로 대체. Swiper Navigation/Keyboard 모듈 추가 및 썸네일 클릭 시 슬라이드 이동 기능 구현. 썸네일 스크롤링 동기화 효과 추가.
ClubFeed 컴포넌트 및 스타일
frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx, frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts
동아리 활동 사진을 반응형 그리드로 표시하는 새 컴포넌트 생성. 사진 클릭 시 PhotoModal 열기, 빈 상태 처리, usePhotoModal 훅과 통합.

시퀀스 다이어그램

sequenceDiagram
    actor User
    participant ClubFeed as ClubFeed<br/>Component
    participant PhotoModal as PhotoModal<br/>Component
    participant Swiper as Swiper<br/>Instance
    participant Thumbnail as Thumbnail<br/>Navigation

    User->>ClubFeed: 사진 클릭
    ClubFeed->>PhotoModal: isOpen, currentIndex 전달
    PhotoModal->>Swiper: initialSlide 설정 및 초기화
    Swiper-->>PhotoModal: onSwiper 콜백
    
    rect rgb(200, 220, 255)
        Note over User,Thumbnail: 썸네일 네비게이션
        User->>Thumbnail: 썸네일 클릭
        Thumbnail->>Swiper: slideToLoop(index) 호출
        Swiper->>PhotoModal: onSlideChange 트리거
        PhotoModal->>Thumbnail: 현재 썸네일 활성화 업데이트
        Thumbnail->>Thumbnail: 스크롤 동기화
    end
    
    rect rgb(220, 255, 220)
        Note over User,Swiper: 버튼 네비게이션
        User->>Swiper: prev/next 버튼 클릭
        Swiper->>PhotoModal: onSlideChange 콜백
        PhotoModal->>Thumbnail: 썸네일 위치 업데이트
    end
    
    User->>PhotoModal: 모달 닫기
    PhotoModal->>ClubFeed: onClose 콜백
Loading

예상 코드 리뷰 노력

🎯 4 (복잡함) | ⏱️ ~50 분

관련 이슈

관련 PR

제안 리뷰어

  • lepitaaar
  • Zepelown

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 반응형 그리드 레이아웃과 Swiper 기반 모달 네비게이션 구현을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 MOA-447의 모든 주요 목표를 충족합니다: 활동사진 컴포넌트 생성, 여러 이미지 렌더링 구조, 모바일/데스크톱 반응형 레이아웃, 이미지 개수 변화에 대한 레이아웃 안정성.
Out of Scope Changes check ✅ Passed 모든 코드 변경사항이 활동사진 그리드 레이아웃과 Swiper 모달 구현이라는 정의된 범위 내에 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#953-club-detail-activity-photos-MOA-447

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 1

🧹 Nitpick comments (4)
frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx (1)

25-28: 변수 섀도잉 이슈: index 변수명 충돌

Line 11에서 usePhotoModal로부터 구조분해한 index와 Line 25의 map 콜백 파라미터 index가 충돌합니다. 이로 인해 open(index) 호출 시 의도치 않게 map의 index가 사용되어 현재는 정상 동작하지만, 코드 가독성과 유지보수성에 문제가 됩니다.

🔎 제안하는 수정
-          {photos.map((photo, index) => (
+          {photos.map((photo, photoIndex) => (
             <Styled.PhotoItem
-              key={`${photo}-${index}`}
-              onClick={() => open(index)}
+              key={`${photo}-${photoIndex}`}
+              onClick={() => open(photoIndex)}
             >
               <Styled.PhotoImage
                 src={photo}
-                alt={`활동사진 ${index + 1}`}
+                alt={`활동사진 ${photoIndex + 1}`}
                 loading='lazy'
               />
             </Styled.PhotoItem>
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx (2)

23-24: thumbnailRefs 배열이 urls 변경 시 정리되지 않을 수 있습니다.

thumbnailRefs.current 배열이 urls 배열 크기 변경 시 이전 참조가 남아있을 수 있습니다. 예를 들어 이전에 10개 이미지가 있다가 5개로 줄어들면, 인덱스 5-9의 참조가 여전히 존재합니다.

🔎 제안하는 수정
+ // urls 변경 시 refs 배열 정리
+ useEffect(() => {
+   thumbnailRefs.current = thumbnailRefs.current.slice(0, urls.length);
+ }, [urls.length]);
+
  // 현재 인덱스가 변경되면 해당 썸네일로 스크롤
  useEffect(() => {

83-95: SwiperSlide의 key로 URL만 사용 시 중복 문제

동일한 사진 URL이 여러 번 존재할 경우 React 키 충돌 경고가 발생할 수 있습니다. 썸네일(Line 120)에서도 동일한 패턴이 사용되고 있습니다.

🔎 제안하는 수정
-              {urls.map((url, idx) => (
-                <SwiperSlide
-                  key={url}
+              {urls.map((url, idx) => (
+                <SwiperSlide
+                  key={`${url}-${idx}`}

Line 120의 Thumbnail도 동일하게 수정:

-                <Styled.Thumbnail
-                  key={url}
+                <Styled.Thumbnail
+                  key={`${url}-${idx}`}
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts (1)

184-198: 테마 색상 상수 사용 권장

하드코딩된 색상값 #ff5414는 테마의 colors.primary[900]과 동일합니다. 일관성을 위해 테마 색상을 사용하는 것이 좋습니다. Line 155의 #f5f5f5colors.gray[100]과 동일합니다.

🔎 제안하는 수정

먼저 colors import 추가:

import { colors } from '@/styles/theme/colors';

그 후 색상값 교체:

 export const Thumbnail = styled.button<{ isActive: boolean }>`
-  border: 2px solid ${({ isActive }) => (isActive ? '#ff5414' : 'transparent')};
+  border: 2px solid ${({ isActive }) => (isActive ? colors.primary[900] : 'transparent')};
   ...
   &:hover {
-    border-color: ${({ isActive }) => (isActive ? '#ff5414' : '#ddd')};
+    border-color: ${({ isActive }) => (isActive ? colors.primary[900] : colors.gray[400])};
   }
 export const ThumbnailContainer = styled.div`
-  background: #f5f5f5;
+  background: ${colors.gray[100]};
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b2f514c and 01cb6c0.

📒 Files selected for processing (4)
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx
  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts
  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries with if/else or IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling

Files:

  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx
  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx
frontend/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling

Files:

  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

Use consistent return types for similar functions/hooks

Files:

  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx
  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts
  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsx
🧠 Learnings (4)
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Replace magic numbers with named constants for clarity

Applied to files:

  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{tsx,jsx} : Separate significantly different conditional UI/logic into distinct components

Applied to files:

  • frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts
📚 Learning: 2025-07-19T05:05:10.196Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 548
File: frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx:17-57
Timestamp: 2025-07-19T05:05:10.196Z
Learning: ClubDetailPage.tsx에서 notJoinedClubNames 배열의 하드코딩은 의도적인 설계 결정입니다. 개발자가 명시적으로 하드코딩을 선택했으므로 이에 대한 리팩토링 제안을 하지 않아야 합니다.

Applied to files:

  • frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts
🧬 Code graph analysis (3)
frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx (1)
frontend/src/hooks/PhotoList/usePhotoModal.ts (1)
  • usePhotoModal (3-14)
frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts (2)
frontend/src/styles/mediaQuery.ts (1)
  • media (8-14)
frontend/src/styles/theme/colors.ts (1)
  • colors (1-81)
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.ts (1)
frontend/src/styles/mediaQuery.ts (1)
  • media (8-14)
🔇 Additional comments (5)
frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.tsx (1)

10-19: LGTM! 빈 상태 처리 및 컴포넌트 구조가 적절합니다.

usePhotoModal 훅을 활용한 모달 상태 관리와 빈 상태 처리가 잘 구현되어 있습니다. lazy loading 적용도 적절합니다.

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

60-96: LGTM! Swiper 통합이 잘 구현되었습니다.

Swiper의 Navigation, Keyboard 모듈을 활용한 구현이 적절합니다. loop 모드에서 realIndex 사용, initialSlide 동기화, 키보드 네비게이션 지원 등이 잘 되어 있습니다.

frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts (1)

20-31: LGTM! 호버 효과 구현이 적절합니다.

aspect-ratio를 활용한 일관된 이미지 비율 유지와 호버 시 부드러운 전환 효과가 잘 구현되어 있습니다.

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

141-148: 태블릿에서 네비게이션 버튼 숨김 의도 확인 필요

모바일과 태블릿 모두에서 네비게이션 버튼이 숨겨집니다. 태블릿 사용자는 스와이프 제스처로만 이미지를 탐색해야 합니다. 이것이 의도된 디자인인지 확인해 주세요.


29-37: LGTM! 전체 뷰포트 모달 레이아웃

100vw/100vh 전체 화면 모달로 변경하여 이미지 표시 영역을 최대화한 것이 적절합니다. PR 목표인 "이미지 표시 영역 최대화, 여백 최소화"에 부합합니다.

Comment on lines +9 to +18
export const PhotoGrid = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;

${media.tablet} {
grid-template-columns: repeat(3, 1fr);
gap: 2px;
}
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

PR 요구사항의 모바일 2열 레이아웃이 누락되었습니다.

PR 설명에 "모바일(~767px) 2열 적용"이라고 명시되어 있으나, 현재 구현에는 모바일 브레이크포인트가 없습니다. 태블릿(3열)까지만 구현되어 있습니다.

🔎 제안하는 수정
 export const PhotoGrid = styled.div`
   display: grid;
   grid-template-columns: repeat(4, 1fr);
   gap: 4px;

   ${media.tablet} {
     grid-template-columns: repeat(3, 1fr);
     gap: 2px;
   }
+
+  ${media.mobile} {
+    grid-template-columns: repeat(2, 1fr);
+    gap: 2px;
+  }
 `;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const PhotoGrid = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
${media.tablet} {
grid-template-columns: repeat(3, 1fr);
gap: 2px;
}
`;
export const PhotoGrid = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
${media.tablet} {
grid-template-columns: repeat(3, 1fr);
gap: 2px;
}
${media.mobile} {
grid-template-columns: repeat(2, 1fr);
gap: 2px;
}
`;
🤖 Prompt for AI Agents
In frontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.ts
around lines 9 to 18, the PhotoGrid styled component is missing the mobile
breakpoint referenced in the PR (“모바일(~767px) 2열 적용”); add a mobile media rule
using the project's existing media helper (e.g., media.mobile or media.phone) to
set grid-template-columns: repeat(2, 1fr) and an appropriate gap (e.g., 2px),
placing it so it overrides tablet/desktop rules at small viewports.

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.

LGTM

@oesnuj oesnuj merged commit 8eded52 into develop-fe Dec 23, 2025
3 checks passed
@oesnuj oesnuj changed the title [Feature] 활동사진 그리드 레이아웃 및 Swiper 기반 모달 네비게이션 구현 [feature] 활동사진 그리드 레이아웃 및 Swiper 기반 모달 네비게이션 구현 Dec 23, 2025
@oesnuj oesnuj deleted the feature/#953-club-detail-activity-photos-MOA-447 branch January 25, 2026 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🎨 Design 마크업 & 스타일링 💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments