[feature] 활동사진 그리드 레이아웃 및 Swiper 기반 모달 네비게이션 구현#965
Conversation
- Swiper 라이브러리 적용으로 좌우 슬라이드 및 키보드 네비게이션 구현 - 하단 썸네일 리스트 추가 및 자동 스크롤 기능 - 이미지 표시 영역 최적화 및 불필요한 코드 제거
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| 응집단 / 파일 | 변경 요약 |
|---|---|
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 콜백
예상 코드 리뷰 노력
🎯 4 (복잡함) | ⏱️ ~50 분
관련 이슈
- [feature] MOA-447 동아리 상세페이지 > 활동 사진 컴포넌트 구현 #953: 새로운 ClubFeed 컴포넌트, PhotoModal 스타일 재설계, Swiper 기반 이미지 뷰어 통합으로 "동아리 활동 사진" 컴포넌트 구현 요청사항을 직접 구현합니다.
관련 PR
- [fix] swipe/css를 RecommendedClubs에서 App으로 이동 #819: 이 PR의 PhotoModal이 Swiper 라이브러리를 사용하므로, 전역
import 'swiper/css'를 App.tsx로 이동하는 PR과 직접 연관됩니다. - [release] v1.1.0 #676: PhotoModal이 Swiper 기반 네비게이션으로 전환되므로, Swiper 의존성 추가 및 webpack CSS 처리를 담당하는 PR과 의존 관계가 있습니다.
- [feature]상세페이지에 동아리 추천 추가 #665: PhotoModal의 Swiper CSS 사용이 해당 PR의 Swiper 의존성 추가에 직접 의존합니다.
제안 리뷰어
- lepitaaar
- Zepelown
Pre-merge checks and finishing touches
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | 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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
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의#f5f5f5도colors.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.
📒 Files selected for processing (4)
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.tsxfrontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.tsfrontend/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 withif/elseor 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.tsxfrontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.tsfrontend/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.tsxfrontend/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.tsxfrontend/src/pages/clubDetailPage2/components/ClubFeed/ClubFeed.styles.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoModal/PhotoModal.styles.tsfrontend/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 목표인 "이미지 표시 영역 최대화, 여백 최소화"에 부합합니다.
| 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; | ||
| } | ||
| `; |
There was a problem hiding this comment.
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.
| 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.
#️⃣연관된 이슈
📝작업 내용
활동사진 UI를 피드/그리드 형태로 전면 개선
그리드 레이아웃 적용
Before (리스트 형태)
After (좌우 스크롤 형태)
사진 모달 인터랙션 강화
레이아웃 최적화
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.