Conversation
- margin-top 제거
- IntersectionObserver로 로딩 시점 설정
- sectionRef 추가하여 자동 스크롤 가능 - feeds 전달
✅ Deploy Preview for moadong ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (12)
frontend/src/utils/convertGoogleDriveUrl.ts (1)
10-10: 로그 메시지 언어 일관성 검토에러 로그 메시지가 한국어로 작성되어 있습니다. 코드베이스의 로그 메시지 언어 정책에 따라 일관성 있게 사용하는 것이 좋습니다.
- console.error('URL 변환 중 오류 발생:', error); + console.error('Error during URL conversion:', error);frontend/src/hooks/LazyImage.tsx (1)
1-40: 잘 구현된 LazyImage 컴포넌트입니다!이미지 지연 로딩을 위한 컴포넌트가 효율적으로 구현되었습니다. IntersectionObserver를 사용하여 뷰포트에 이미지가 들어올 때만 실제 이미지를 로드하는 방식은 성능 최적화에 매우 좋습니다.
몇 가지 개선 가능한 제안 사항:
- 이미지 로딩 실패 시 대체 UI(fallback)를 제공하는 것이 좋을 것 같습니다.
- 로딩 중 상태를 표시하는 스켈레톤 UI나 로딩 인디케이터를 추가하는 것을 고려해보세요.
return isVisible ? ( <img ref={imgRef} src={src} alt={alt} onError={onError} /> ) : ( - <div ref={imgRef} style={{ width: '100%', height: '100%' }} /> + <div ref={imgRef} style={{ width: '100%', height: '100%', backgroundColor: '#f0f0f0' }} /> );frontend/src/hooks/usePhotoNavigation.ts (1)
1-67: 잘 구현된 사진 네비게이션 훅입니다.사진 갤러리 네비게이션을 위한 커스텀 훅이 체계적으로 구현되었습니다. 다양한 상태 조건을 계산하고 적절한 핸들러 함수를 제공하여 복잡한 네비게이션 로직을 잘 추상화했습니다.
몇 가지 개선 가능한 제안 사항:
- 각 조건문과 계산에 주석을 추가하여 로직의 이해도를 높이는 것이 좋을 것 같습니다.
containerWidth - 400에서 사용된 숫자 400은 매직 넘버로 보입니다. 이 값을 의미 있는 상수나 파라미터로 추출하는 것이 좋겠습니다.- 복잡한 조건문(특히
canScrollRight)에 대한 설명을 추가하면 코드의 가독성이 향상될 것입니다.+ // 마지막 카드가 표시 영역 중앙에 위치하는지 확인 const isLastCardInMiddle = isLastCard && containerWidth > 0 && translateX > -(containerWidth - 400); + // 마지막에서 두 번째 카드가 시작 위치에 있는지 확인 const isSecondLastCardAtStart = currentIndex === photosLength - 2 && translateX === 0;+ // 오른쪽으로 스크롤 가능한지 확인 (다음 사진이 있고, 특수 케이스가 아닌 경우) const canScrollRight = currentIndex < photosLength - 1 && !isLastCardInMiddle && !isSecondLastCardAtStart && currentIndex < photosLength - 2 && photosLength > 2;frontend/src/hooks/usePhotoSwipe.ts (4)
24-25: 주석을 영어로 번역하는 것이 좋습니다.코드베이스의 일관성을 유지하기 위해 한글 주석을 영어로 번역하는 것이 좋습니다.
- isLastCardInMiddle, // 마지막 카드를 절반만 보여주는 상태인지 - isSecondLastCardAtStart, // 마지막에서 2번째 카드가 가장 왼쪽에 있을 때인지 + isLastCardInMiddle, // Whether the last card is shown only halfway + isSecondLastCardAtStart, // Whether the second last card is positioned at the leftmost
43-43: 매직 넘버를 상수로 추출하세요.코드에 하드코딩된 숫자(100, 400, 50 등)를 상수로 추출하면 가독성과 유지보수성이 향상됩니다.
+ const MAX_SLIDE_DISTANCE_MOBILE = 100; + const MAX_SLIDE_DISTANCE_DESKTOP = cardWidth; + const MIN_WIDTH_THRESHOLD = 500; + const CARD_VIEW_WIDTH = 400; + const SWIPE_THRESHOLD_MOBILE = 50; + const SWIPE_THRESHOLD_DESKTOP = 100; const maxSlideDistance = window.innerWidth <= 500 ? 100 : cardWidth; // 이후 코드에서도 해당 상수 사용 // if (newTranslate < containerWidth - 400) 등Also applies to: 62-62, 75-75
51-67: 복잡한 조건부 로직에 주석을 추가하세요.
handleTouchMove함수의 복잡한 조건문에 각 조건이 처리하는 상황에 대한 주석을 추가하면 코드의 의도를 더 명확하게 이해할 수 있습니다.+ // 마지막 사진에서 오른쪽으로 더 이상 스와이프되지 않도록 방지 if (currentIndex === photosLength - 1 && newTranslate > 0) { setTranslateX(0); + // 첫 번째 사진에서 왼쪽으로 더 이상 스와이프되지 않도록 방지 } else if (currentIndex === 0 && newTranslate < 0) { setTranslateX(0); + // 마지막에서 두 번째 이상의 사진에서 왼쪽으로 스와이프 제한 } else if ( currentIndex >= photosLength - 2 && newTranslate < currentTranslate ) { setTranslateX(currentTranslate); + // 마지막 사진의 위치 조정 (컨테이너 너비에 맞춤) } else if ( currentIndex === photosLength - 1 && newTranslate < containerWidth - 400 ) { setTranslateX(containerWidth - 400); } else { setTranslateX(newTranslate); }
70-93: handleTouchEnd 함수에도 주석 추가가 필요합니다.
handleTouchEnd함수의 조건 로직에도 주석을 추가하여 각 조건의 의도를 명확히 하는 것이 좋습니다.const handleTouchEnd = () => { if (!isDragging) return; setIsDragging(false); const diff = currentTranslate - translateX; const threshold = window.innerWidth <= 500 ? 50 : 100; + // 스와이프 거리가 임계값을 넘었을 때 페이지 전환 if (Math.abs(diff) > threshold) { + // 오른쪽으로 스와이프하여 이전 사진으로 이동 if (diff > 0 && currentIndex > 0) { handlePrev(); + // 왼쪽으로 스와이프하여 다음 사진으로 이동 (특정 조건 충족시) } else if ( diff < 0 && currentIndex < photosLength - 1 && !isLastCardInMiddle && !isSecondLastCardAtStart ) { handleNext(); } else { setTranslateX(currentTranslate); } } else { + // 스와이프 거리가 임계값을 넘지 않으면 원래 위치로 복원 setTranslateX(currentTranslate); } };frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx (3)
23-23: 하드코딩된 값을 상수로 추출하세요.
cardWidth = 428값을 상수 또는 테마 변수로 추출하여 코드의 유지보수성을 높이는 것이 좋습니다. 이는 다른 곳에서도 이 값을 참조할 수 있도록 해줍니다.- const cardWidth = 428; // 400px + 28px gap + const CARD_WIDTH = 428; // 400px + 28px gap + // 또는 상수 파일이나 테마에서 가져오는 것이 더 좋습니다 + // import { CARD_WIDTH } from '@/constants/photoList';
80-101: handleProgressBarClick 함수에 주석을 추가하세요.
handleProgressBarClick함수의 로직이 복잡하므로, 주석을 추가하여 각 단계의 의도를 명확히 하는 것이 좋습니다.const handleProgressBarClick = (e: React.MouseEvent<HTMLDivElement>) => { if (!progressBarRef.current) return; + // 프로그레스 바의 위치와 크기를 가져옴 const rect = progressBarRef.current.getBoundingClientRect(); + // 클릭한 위치의 X 좌표 계산 const x = e.clientX - rect.left; + // 클릭 위치를 퍼센트로 변환 const percentage = (x / rect.width) * 100; + // 퍼센트를 기반으로 새로운 인덱스 계산 const newIndex = Math.floor((percentage / 100) * photos.length); + // 유효한 새 인덱스인지 확인하고 변경 적용 if ( newIndex !== currentIndex && newIndex >= 0 && newIndex < photos.length ) { const diff = newIndex - currentIndex; setCurrentIndex(newIndex); + // 마지막 사진으로 이동한 경우 특수 처리 if (newIndex === photos.length - 1) { setTranslateX(containerWidth - 400); } else { + // 일반적인 경우 카드 너비에 따라 이동 setTranslateX((prev) => prev - diff * cardWidth); } } };
109-162: 키보드 접근성을 추가하는 것이 좋습니다.현재 구현은 터치 및 마우스 이벤트에만 초점을 맞추고 있습니다. 키보드 접근성을 향상시키기 위해 화살표 키를 사용한 사진 탐색 기능을 추가하는 것이 좋습니다.
+ // 컴포넌트 상단에 다음 함수 추가 + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft' && canScrollLeft) { + handlePrev(); + } else if (e.key === 'ArrowRight' && canScrollRight) { + handleNext(); + } + }, + [canScrollLeft, canScrollRight, handleNext, handlePrev] + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); // PhotoListContainer에 tabIndex 속성 추가 <Styled.PhotoListContainer + tabIndex={0} ref={(el) => { sectionRefs.current[3] = el; }}>frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts (2)
95-95: 주석 처리된 z-index 속성을 처리하세요.주석 처리된
z-index속성이 있습니다. 필요하지 않다면 제거하고, 필요하다면 주석을 해제하고 주석에 이유를 설명하는 것이 좋습니다.- /* z-index: 2; */ + z-index: 2; // 네비게이션 버튼이 다른 요소 위에 표시되도록 함또는
- /* z-index: 2; */
9-10: 하드코딩된 색상 값을 테마 변수로 추출하세요.여러 곳에서 하드코딩된 색상 값(#dcdcdc, #cdcdcd, rgba(255, 255, 255, 0.9) 등)을 사용하고 있습니다. 이러한 값들을 테마 변수나 상수로 추출하면 일관된 디자인을 유지하고 유지보수성을 향상시킬 수 있습니다.
+ // theme.ts 또는 constants 파일에 추가 + export const colors = { + border: '#dcdcdc', + background: { + light: '#f5f5f5', + card: '#cdcdcd', + button: 'rgba(255, 255, 255, 0.9)', + buttonHover: 'rgba(255, 255, 255, 1)', + }, + text: { + placeholder: '#666', + }, + progressBar: 'rgba(0, 0, 0, 0.2)', + }; // 그리고 각 스타일 컴포넌트에서 이런 식으로 사용: border: 1px solid ${({ theme }) => theme.colors.border}; background-color: ${({ theme }) => theme.colors.background.card};Also applies to: 61-62, 86-87, 135-136, 149-150
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
frontend/src/hooks/LazyImage.tsx(1 hunks)frontend/src/hooks/queries/club/useUpdateClubDescription.ts(1 hunks)frontend/src/hooks/queries/club/useUpdateClubDetail.ts(1 hunks)frontend/src/hooks/usePhotoNavigation.ts(1 hunks)frontend/src/hooks/usePhotoSwipe.ts(1 hunks)frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx(1 hunks)frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx(2 hunks)frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts(1 hunks)frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx(3 hunks)frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx(2 hunks)frontend/src/pages/ClubDetailPage/components/IntroduceBox/IntroduceBox.styles.ts(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/types/club.ts(1 hunks)frontend/src/utils/convertGoogleDriveUrl.ts(1 hunks)
🔇 Additional comments (18)
frontend/src/pages/ClubDetailPage/components/IntroduceBox/IntroduceBox.styles.ts (1)
19-19: 모바일 뷰에서의 시각적 구분을 위한 적절한 스타일 추가모바일 뷰에서 하단 테두리를 추가하여 소개 박스와 새로 추가된 활동사진 섹션 간의 시각적 구분을 만들어 주었습니다. 이는 사용자 경험을 향상시키는 좋은 개선사항입니다.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts (1)
105-105: 이미지 컨테이너 간격 조정으로 UI 개선ImageContainer의 gap 값을 37px에서 15px로 줄여 더 컴팩트한 레이아웃을 구성했습니다. 이는 이미지 관련 UI 요소들 간의 간격을 적절히 조정하여 시각적으로 더 좋은 사용자 경험을 제공하는 변경사항입니다.
frontend/src/hooks/queries/club/useUpdateClubDetail.ts (1)
1-14:Details
❓ Verification inconclusive
쿼리 캐시 무효화 로직 제거 검토 필요
useQueryClient와 관련된 onSuccess 콜백을 제거하여 코드가 단순화되었습니다. 이는 의도적인 아키텍처 변경으로 보이지만, 이 변경으로 인해 데이터 갱신 로직이 어디서 처리되는지 확인이 필요합니다.
이 변경이 의도적인 것이라면 문제가 없지만, 만약 실수로 제거된 것이라면 데이터가 갱신되지 않는 문제가 발생할 수 있습니다. 동일한 변경이 useUpdateClubDescription.ts에도 적용된 것으로 보아 의도적인 아키텍처 변경으로 보입니다.
다음 명령어를 통해 캐시 무효화 로직이 다른 곳에서 처리되는지 확인해보세요:
🏁 Script executed:
#!/bin/bash # clubDetail 쿼리 무효화 로직이 어디에서 처리되는지 확인 echo "클럽 디테일 쿼리 무효화 로직 검색:" rg -n "invalidateQueries\(\[?['\"]clubDetail" --type tsLength of output: 110
쿼리 캐시 무효화 로직 관련 검토 요청
현재
useUpdateClubDetail훅에서 쿼리 캐시 무효화 로직(예:useQueryClient를 통한invalidateQueries호출)이 제거되어 코드가 단순화되었습니다. 의도한 아키텍처 변경이라면 데이터 갱신 처리가 다른 곳에서 이루어지고 있어야 하는데,
- 검색 결과에서는
'clubDetail'관련invalidateQueries호출이 확인되지 않았습니다.- 동일한 변경이 적용된
useUpdateClubDescription.ts등 다른 곳에서도 동일한 무효화 로직 제거가 이루어졌는지, 그리고 데이터 갱신 처리가 어디서 담당되고 있는지 재검토가 필요합니다.변경 의도라면 관련 로직이 다른 부분에 구현되어 있는지, 혹은 누락으로 인한 데이터 갱신 문제는 없는지 수동으로 추가 확인 부탁드립니다.
frontend/src/types/club.ts (1)
27-35: 이미지 관련 컴포넌트의 재사용 가능한 타입 정의 추가ImageUploadProps와 ImagePreviewProps 인터페이스 추가는 코드의 타입 안전성과 재사용성을 향상시킵니다. 이미지 관련 컴포넌트에서 사용되는 프롭스 타입을 중앙 집중화하여 관리함으로써 코드 일관성이 개선되었습니다.
이 변경사항은 PR 목표 중 하나인 "활동 사진 추가" 기능과 완벽하게 일치하며, 코드베이스의 유지 관리성을 향상시키는 좋은 리팩토링입니다.
frontend/src/hooks/queries/club/useUpdateClubDescription.ts (1)
1-14: 코드 간소화로 인한 관심사 분리가 잘 이루어졌습니다.
useQueryClient와 관련된 로직이 제거되어 더 간결해졌네요. 이전에는 mutation 성공 후 자동으로clubDetail쿼리를 무효화했을 텐데, 이제는 상위 컴포넌트에서 필요에 따라 처리하는 방식으로 변경된 것 같습니다. 이렇게 관심사를 분리하는 것은 코드의 유지보수성과 재사용성을 높이는 좋은 접근입니다.frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (2)
10-10: 새로운 기능 추가가 깔끔하게 이루어졌습니다.
PhotoList컴포넌트의 추가로 상세페이지에 활동사진 기능이 구현되었네요. 이는 PR 목표와 일치하는 좋은 변경사항입니다.
60-60: 컴포넌트 통합이 잘 이루어졌습니다.
PhotoList컴포넌트가 기존 페이지 구조에 자연스럽게 통합되었습니다.sectionRefs와feedsprops를 적절하게 전달하여 스크롤 기능과 데이터를 모두 처리할 수 있게 했네요.frontend/src/utils/convertGoogleDriveUrl.ts (1)
1-13: Google Drive URL 변환 유틸리티가 효율적으로 구현되었습니다.정규식을 사용해 Google Drive 파일 ID를 추출하고 썸네일 URL로 변환하는 로직이 명확하게 구현되었습니다. 두 가지 형식의 URL(
id=쿼리 파라미터와/d/경로 세그먼트)을 모두 처리할 수 있는 것은 좋은 접근입니다.오류 처리도 적절히 구현되어 있어 예외 상황에서도 원본 URL이 반환됩니다.
frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx (1)
3-5: 타입 관리가 개선되었습니다.인라인 타입 정의 대신 중앙화된 타입 정의를 가져와 사용하도록 리팩토링했네요. 이는 코드의 일관성과 유지보수성을 향상시키는 좋은 변화입니다. 동일한 타입이 여러 컴포넌트에서 재사용될 때 특히 유용합니다.
frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx (2)
5-10: 타입 리팩토링이 잘 되었습니다.외부 타입 정의(
ImageUploadProps)를 사용하여 컴포넌트 props를 개선한 것은 좋은 변경입니다. 이렇게 하면 타입 정의를 중앙화하고 재사용성을 높일 수 있습니다.
40-40: default export 추가 확인default export를 추가한 것은 컴포넌트 임포트 방식의 일관성을 위한 좋은 변경입니다.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (5)
1-16: 임포트 구문 및 타입 구성이 개선되었습니다.임포트 구문의 재배치와 타입 가져오기가 잘 정리되었습니다. 컴포넌트 임포트도 일관된 방식으로 변경된 점이 좋습니다.
17-18: 상수 선언 위치 개선
MAX_IMAGES와TEMP_CLUB_ID상수를 파일 상단으로 이동한 것은 좋은 변경입니다. 다만,TEMP_CLUB_ID는 임시적인 값으로 보이는데, 향후 실제 값으로 대체하는 것을 고려해야 합니다.
TEMP_CLUB_ID가 실제 개발/프로덕션 환경에서 어떻게 사용될 예정인지 확인해주세요. 임시 값이 프로덕션 코드에 남아있지 않도록 주의가 필요합니다.
31-31: 상태 초기화 위치 개선
imageList상태 초기화 위치를 다른 상태들과 함께 배치한 것은 코드 구조 개선에 도움이 됩니다.
135-135: FIXME 주석 처리 필요"[x]FIXME: div 컴포넌트 수정" 주석이 있습니다. 이 주석은 PR 제출 전에 해결되거나 제거되어야 합니다. 또는 해당 작업을 추적할 수 있는 이슈를 생성하는 것이 좋습니다.
해당 FIXME가 이번 PR에서 해결해야 할 작업인지, 아니면 향후 작업을 위한 메모인지 확인해주세요.
211-211: 헤딩 계층 구조 개선"활동 사진 편집" 섹션의 헤딩을
<h3>로 변경한 것은 페이지의 계층 구조를 일관되게 유지하는 데 도움이 됩니다.frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx (1)
1-166: 전체적으로 잘 구현된 컴포넌트입니다.코드가 잘 구조화되어 있고, 적절한 오류 처리와 반응형 디자인이 포함되어 있습니다.
usePhotoSwipe와usePhotoNavigation훅을 분리하여 관심사 분리도 잘 되어 있습니다. LazyImage 컴포넌트를 사용한 최적화도 좋은 접근입니다.frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts (1)
3-151: 전반적으로 잘 구현된 스타일 컴포넌트입니다.스타일 컴포넌트가 전체적으로 잘 구성되어 있으며, 반응형 디자인도 적절하게 구현되어 있습니다. 각 컴포넌트의 역할이 명확하며, 모바일과 데스크톱 환경 모두를 고려한 설계가 돋보입니다.
|
@seongwon030 PR 리뷰를 시작하겠습니다. 요청해주셔서 감사합니다. ✅ Actions performedReview triggered.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough이번 PR에서는 다양한 컴포넌트와 커스텀 훅이 추가되고, 일부 기존 로직이 제거 및 리팩토링되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant PL as PhotoList
participant PS as usePhotoSwipe
participant PN as usePhotoNavigation
participant LI as LazyImage
U->>PL: 사진 스와이프 시작
PL->>PS: 터치 이벤트 처리 (handleTouchStart/Move/End)
PS->>PL: translateX 업데이트
PL->>PN: 내비게이션 상태 요청
PN->>PL: 이전/다음 인덱스 반환
PL->>LI: 현재 사진 렌더링 요청
LI-->>PL: 이미지 레이지 로딩 처리
Assessment against linked issues
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 5
🔭 Outside diff range comments (1)
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (1)
50-53: 🛠️ Refactor suggestion이미지 추가 함수에서 상태 업데이트 최적화
addImage함수에서는 API 호출 결과를 기다리지 않고 바로 상태를 업데이트하고 있습니다. API 호출이 실패하면 UI와 실제 데이터가 불일치할 수 있습니다. API 호출 성공 후 상태를 업데이트하는 것이 좋습니다.const addImage = (newImage: string) => { - updateFeedImages({ feeds: [...imageList, newImage], clubId: TEMP_CLUB_ID }); - setImageList([...imageList, newImage]); + const newImageList = [...imageList, newImage]; + updateFeedImages( + { feeds: newImageList, clubId: clubDetail?.id || TEMP_CLUB_ID }, + { + onSuccess: () => { + setImageList(newImageList); + }, + onError: (error) => { + console.error('이미지 업데이트 실패:', error); + // 에러 처리 로직 (예: 사용자에게 알림) + } + } + ); };
🧹 Nitpick comments (14)
frontend/src/utils/convertGoogleDriveUrl.ts (1)
1-15: 구글 드라이브 URL 변환 로직이 효과적으로 구현되었습니다.함수가 정상적으로 구글 드라이브 URL에서 파일 ID를 추출하고 썸네일 URL로 변환하는 작업을 수행합니다. 정규식 패턴과 에러 처리도 잘 되어 있습니다.
다만, 몇 가지 개선 사항을 고려해볼 수 있습니다:
- 썸네일 크기가
w2048로 하드코딩되어 있어 다양한 사용 사례에 맞게 크기를 선택할 수 있도록 매개변수화하는 것이 좋을 수 있습니다.- 정규식이 일반적인 구글 드라이브 URL 패턴을 다루고 있지만, 다른 형식의 URL(예: 공유 링크)도 처리할 수 있는지 확인해보세요.
-const convertGoogleDriveUrl = (url: string) => { +const convertGoogleDriveUrl = (url: string, size: string = 'w2048') => { try { const fileIdMatch = url.match(/(?:id=|\/d\/)([-\w]{25,})/); if (!fileIdMatch) return url; const fileId = fileIdMatch[1]; - return `https://drive.google.com/thumbnail?id=${fileId}&sz=w2048`; + return `https://drive.google.com/thumbnail?id=${fileId}&sz=${size}`; } catch (error) { console.error('URL 변환 중 오류 발생:', error); return url; } };frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
3-13: 임포트 순서가 재정렬되었습니다.임포트 순서 변경은 기능에 영향을 주지 않지만, 일관된 임포트 순서를 유지하는 것이 코드 가독성을 향상시킵니다. 프로젝트 전체에서 임포트 순서에 대한 일관된 규칙을 따르는 것이 좋습니다.
frontend/src/hooks/LazyImage.tsx (2)
15-31: IntersectionObserver 사용 최적화threshold 값을 props로 받아서 사용자가 조정할 수 있게 하는 것이 좋습니다. 또한 브라우저 호환성 문제를 고려하여 polyfill이나 fallback 메커니즘을 추가하는 것이 좋습니다.
const LazyImage = ({ src, alt, onError, + threshold = 0.1, }: { src: string; alt: string; onError?: () => void; + threshold?: number; }) => { // ... useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, - { threshold: 0.1 }, + { threshold }, ); // ... }, []);
33-37: 로딩 상태 관리 추가 고려현재 이미지가 로딩되는 동안의 상태를 관리하지 않고 있습니다. 이미지 로딩 중에 로딩 인디케이터나 스켈레톤 UI를 표시하는 것이 사용자 경험을 향상시킬 수 있습니다.
const LazyImage = ({ src, alt, onError, }: { src: string; alt: string; onError?: () => void; }) => { const [isVisible, setIsVisible] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); const imgRef = useRef<HTMLImageElement | null>(null); // ... return isVisible ? ( - <img ref={imgRef} src={src} alt={alt} onError={onError} /> + <> + {!isLoaded && <div className="loading-indicator" />} + <img + ref={imgRef} + src={src} + alt={alt} + onLoad={() => setIsLoaded(true)} + onError={() => { + setIsLoaded(true); + onError && onError(); + }} + style={{ display: isLoaded ? 'block' : 'none' }} + /> + </> ) : ( <div ref={imgRef} style={{ width: '100%', height: '100%' }} /> );frontend/src/hooks/usePhotoNavigation.ts (2)
19-22: 명확한 변수명과 문서화 필요
isLastCardInMiddle과isSecondLastCardAtStart변수는 이해하기 어려운 복잡한 조건을 가지고 있습니다. 이러한 변수들에 대한 설명을 주석으로 추가하거나, 더 명확한 이름으로 변경하는 것이 좋습니다.
1-64: 창 크기 변경에 대한 처리 부족현재 훅은 컨테이너나 카드 크기가 변경될 때 대응하는 로직이 없습니다. 이는 반응형 UI에서 문제가 될 수 있습니다. 상위 컴포넌트에서 창 크기 변경 이벤트를 처리하고 필요한 값을 업데이트하는 것이 필요합니다.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (1)
135-135: FIXME 주석 구체화 필요코드에 "div 컴포넌트 수정" FIXME 주석이 있지만, 어떤 문제가 있는지, a어떻게 수정해야 하는지 명확하지 않습니다. 주석에 더 많은 정보를 추가하는 것이 좋습니다.
-// [x]FIXME: div 컴포넌트 수정 +// [x]FIXME: div 컴포넌트를 의미론적으로 더 적절한 컴포넌트로 수정하고, 스타일 개선 필요frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx (2)
13-24: 컴포넌트 설계가 잘 되어 있지만, cardWidth 계산 방식을 개선할 수 있습니다.컴포넌트의 기본 구조와 props 정의가 잘 되어 있습니다. 다만, 카드 너비(cardWidth)가 하드코딩되어 있어 스타일이 변경될 경우 일관성을 유지하기 어려울 수 있습니다.
- const cardWidth = 428; // 400px + 28px gap + const CARD_WIDTH = 400; + const CARD_GAP = 28; + const cardWidth = CARD_WIDTH + CARD_GAP;이렇게 수정하면 카드 넓이와 간격을 분리하여 관리할 수 있고, 이후 스타일 변경 시 일관성을 유지하기 쉬워집니다.
109-162: 컴포넌트 렌더링 및 접근성 개선이 필요합니다.전체적인 컴포넌트 렌더링 로직은 잘 구현되어 있으나, 접근성(a11y) 측면에서 개선이 필요합니다. 네비게이션 버튼에 적절한 aria 속성이 없고, 키보드 접근성 지원이 부족합니다.
<Styled.NavigationButton direction='left' onClick={handlePrev}> - ← + <span aria-hidden="true">←</span> + <span className="sr-only">이전 사진</span> </Styled.NavigationButton> <Styled.NavigationButton direction='right' onClick={handleNext}> - → + <span aria-hidden="true">→</span> + <span className="sr-only">다음 사진</span> </Styled.NavigationButton>추가로, styled-components에 다음과 같은 스타일을 추가하면 좋겠습니다:
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }또한 키보드 탐색을 위한 keydown 이벤트 핸들러도 추가하는 것이 좋겠습니다.
frontend/src/hooks/usePhotoSwipe.ts (3)
16-31: 훅 구현에 한글 주석이 포함되어 있습니다.코드에 한글 주석이 포함되어 있습니다. 프로젝트의 주석 컨벤션에 따라 영어로 통일하거나, 팀 내 합의된 언어로 주석을 작성하는 것이 좋습니다.
- isLastCardInMiddle, // 마지막 카드를 절반만 보여주는 상태인지 - isSecondLastCardAtStart, // 마지막에서 2번째 카드가 가장 왼쪽에 있을 때인지 + isLastCardInMiddle, // If the last card is only partially visible + isSecondLastCardAtStart, // If the second-to-last card is at the leftmost position또한 상태 관리를 위한 초기 값들이 잘 설정되어 있습니다.
38-68: 터치 이동 처리에 하드코딩된 반응형 값이 있습니다.터치 이동 처리 로직에 window.innerWidth를 직접 확인하는 코드가 있습니다. 이러한 방식은 커스텀 훅이나 Context를 통해 관리하는 것이 더 유지보수하기 좋습니다.
- const maxSlideDistance = window.innerWidth <= 500 ? 100 : cardWidth; + const isMobile = window.innerWidth <= 500; + const maxSlideDistance = isMobile ? 100 : cardWidth;또한 62번 라인의 하드코딩된 값(400)도 상수로 관리하는 것이 좋습니다:
currentIndex === photosLength - 1 && - newTranslate < containerWidth - 400 + newTranslate < containerWidth - CARD_WIDTH이렇게 하면 코드의 일관성과 유지보수성이 향상됩니다.
70-93: 터치 종료 처리에도 하드코딩된 반응형 값과 임계값이 있습니다.터치 종료 처리 로직에도 window.innerWidth를 직접 체크하는 코드가 있습니다. 앞서 제안한 것처럼 이러한 값들은 상수나 설정으로 관리하는 것이 좋습니다.
- const threshold = window.innerWidth <= 500 ? 50 : 100; + const MOBILE_THRESHOLD = 50; + const DESKTOP_THRESHOLD = 100; + const threshold = window.innerWidth <= 500 ? MOBILE_THRESHOLD : DESKTOP_THRESHOLD;전반적으로 터치 이벤트 처리 로직은 잘 구현되어 있지만, 마지막 스와이프 조건이 복잡하여 주석으로 설명을 추가하면 더 이해하기 쉬울 것입니다.
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts (2)
78-109: NavigationButton에 주석 처리된 코드가 있습니다.NavigationButton 스타일에 주석 처리된 z-index 속성이 있습니다. 불필요한 주석은 제거하는 것이 좋습니다.
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - /* z-index: 2; */ transition: all 0.3s ease;전반적으로 네비게이션 버튼 스타일은 호버 효과를 포함하여 잘 구현되어 있습니다.
111-141: ProgressBar가 모바일에서 숨겨지는 문제가 있습니다.ProgressBar 컴포넌트가 모바일 뷰(500px 이하)에서 표시되지 않습니다. 모바일 사용자에게도 진행 상황을 제공하는 것이 좋을 수 있습니다.
모바일에서도 진행 상황을 표시하는 방법을 고려해보세요. 예를 들어, 점(dot) 형태의 인디케이터로 대체하거나, 간소화된 형태의 진행 막대를 제공할 수 있습니다.
export const ProgressBar = styled.div<{ progress: number }>` width: ${({ progress }) => progress}%; height: 100%; background-color: rgba(0, 0, 0, 0.2); transition: width 0.3s ease; @media (max-width: 500px) { - display: none; + height: 4px; + background-color: rgba(0, 0, 0, 0.3); } `;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
frontend/src/hooks/LazyImage.tsx(1 hunks)frontend/src/hooks/queries/club/useUpdateClubDescription.ts(1 hunks)frontend/src/hooks/queries/club/useUpdateClubDetail.ts(1 hunks)frontend/src/hooks/usePhotoNavigation.ts(1 hunks)frontend/src/hooks/usePhotoSwipe.ts(1 hunks)frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx(1 hunks)frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx(2 hunks)frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts(1 hunks)frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx(3 hunks)frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx(2 hunks)frontend/src/pages/ClubDetailPage/components/IntroduceBox/IntroduceBox.styles.ts(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/types/club.ts(1 hunks)frontend/src/utils/convertGoogleDriveUrl.ts(1 hunks)
🧰 Additional context used
🧬 Code Definitions (3)
frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx (1)
frontend/src/types/club.ts (1)
ImageUploadProps(27-30)
frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx (1)
frontend/src/types/club.ts (1)
ImagePreviewProps(32-35)
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (3)
frontend/src/types/club.ts (1)
ClubDetail(12-20)frontend/src/apis/updateFeedImages.ts (1)
updateFeedImages(3-19)frontend/src/hooks/queries/club/useUpdateFeedImages.ts (1)
useUpdateFeedImages(4-9)
🔇 Additional comments (23)
frontend/src/pages/ClubDetailPage/components/IntroduceBox/IntroduceBox.styles.ts (1)
19-19: 모바일 화면에서의 시각적 구분선이 추가되었네요!모바일 화면(500px 이하)에서 IntroduceBoxWrapper에 하단 테두리를 추가하여 컴포넌트 간의 시각적 구분을 개선했습니다. 이렇게 하면 모바일 환경에서 콘텐츠 영역이 더 명확하게 구분되어 사용자 경험이 향상될 것입니다.
frontend/src/hooks/queries/club/useUpdateClubDetail.ts (1)
1-14:Details
❓ Verification inconclusive
queryClient 캐시 무효화 로직이 제거되었습니다.
useQueryClient와 onSuccess 콜백에서의 queryClient.invalidateQueries(['clubDetail']) 로직이 제거되었습니다. 이는 의도적인 변경인지 확인이 필요합니다. 해당 로직이 제거됨으로써 클럽 정보가 업데이트된 후 캐시가 자동으로 갱신되지 않을 수 있습니다.
다음을 확인해주세요:
- 다른 곳에서 캐시를 갱신하는 로직이 구현되어 있는지
- 데이터 업데이트 후 사용자에게 최신 데이터가 정확히 보여지는지
- 이러한 변경이 PR 목적(활동사진 추가)과 연관이 있는지
🏁 Script executed:
#!/bin/bash # 다른 파일에서 clubDetail 쿼리 캐시 무효화 처리 여부 확인 rg "invalidateQueries\(\['clubDetail'\]\)" --type ts -g "!*.test.ts" -g "!*/node_modules/*"Length of output: 99
캐시 무효화 로직 재검증 필요
- 현재 파일(
frontend/src/hooks/queries/club/useUpdateClubDetail.ts)에서 queryClient 캐시 무효화 로직이 제거된 상태입니다.- 코드베이스 전체에서
invalidateQueries(['clubDetail'])또는queryClient.invalidateQueries관련 코드가 검색되지 않아, 다른 곳에서 캐시를 갱신하는 로직이 구현되어 있지 않은 것으로 보입니다.- 데이터 업데이트 후 클럽 정보가 최신 데이터로 정확히 반영되는지, 그리고 해당 변경사항이 '활동사진 추가' PR 목적과 연관된 의도적인 변경인지 추가 확인이 필요합니다.
해당 사항에 대해 직접 재검증해주시기 바랍니다.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts (1)
102-107: ImageContainer의 간격 조정 및 margin-top 제거ImageContainer 컴포넌트의 gap이 변경되었고, margin-top 속성이 제거된 것으로 보입니다. 이러한 변경은 이미지 컨테이너의 레이아웃을 더 조밀하게 만들고, 상위 요소와의 간격을 없애는 효과가 있습니다.
해당 변경이 활동사진 추가 기능과 연관된 UI 개선이라면 적절한 변경으로 보입니다.
frontend/src/hooks/queries/club/useUpdateClubDescription.ts (1)
1-14:Details
❓ Verification inconclusive
queryClient 캐시 무효화 로직이 제거되었습니다.
useUpdateClubDetail과 마찬가지로, 이 훅에서도 useQueryClient와 onSuccess 콜백에서의 queryClient.invalidateQueries(['clubDetail']) 로직이 제거되었습니다. 이는 데이터 갱신 방식에 대한 의도적인 변경인지 확인이 필요합니다.
해당 변경이 의도적이라면 다음 사항들을 고려해야 합니다:
- 데이터 변경 후 UI가 즉시 업데이트되는지 확인
- 상태 관리 방식이 변경되었다면, 해당 변경에 맞게 다른 관련 훅들도 업데이트 필요
- 캐시 갱신이 어떻게 처리되는지 문서화 필요
🏁 Script executed:
#!/bin/bash # 관련 API 호출 이후 데이터 갱신 처리 방식 확인 rg "updateClubDescription|updateClubDetail" --type ts -A 10 -B 5 -g "!*.test.ts" -g "!*/node_modules/*"Length of output: 7734
캐시 무효화 로직 제거 변경 사항 확인 및 문서화 요청
현재
useUpdateClubDescription훅에서 기존에 사용되던useQueryClient및onSuccess에서의queryClient.invalidateQueries(['clubDetail'])로직이 제거된 것을 확인했습니다. RecruitEditTab.tsx에서 해당 훅 호출 시 직접적으로 상태 업데이트(setDescription)를 통해 UI 갱신을 처리하고 있는 점을 볼 수 있습니다.변경이 의도적이라면 아래 사항을 확인 및 문서화해주시기 바랍니다:
- 데이터 변경 후 UI가 즉시 업데이트 되는지 (수동 상태 업데이트 또는 다른 캐시 갱신 처리 방식을 통해)
- 상태 관리 방식 변경에 따라, 관련된 다른 훅들(예:
useUpdateClubDetail)에도 동일한 변경이 적용되었는지- 변경된 캐시 갱신 처리 방식에 대한 이유와 동작 방식의 명확한 문서화
frontend/src/types/club.ts (1)
26-35: 타입 정의가 명확하게 구현되었습니다.두 인터페이스(
ImageUploadProps와ImagePreviewProps)가 각각의 컴포넌트에 필요한 속성을 잘 정의하고 있습니다. 이러한 타입 분리를 통해 코드 재사용성과 타입 안전성이 향상됩니다.frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx (2)
5-10: 타입 임포트 및 적용이 적절하게 이루어졌습니다.
ImageUploadProps인터페이스를 가져와서 컴포넌트 프롭스에 적용한 것은 코드 가독성과 유지보수성을 향상시킵니다. 인라인 타입 정의 대신 중앙화된 타입을 사용함으로써 일관된 타입 관리가 가능해졌습니다.
40-40: default export로 변경된 점이 적절합니다.명명된 내보내기(named export)에서 기본 내보내기(default export)로 변경함으로써 임포트 시 더 간결한 구문을 사용할 수 있게 되었습니다.
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
60-60:Details
✅ Verification successful
PhotoList 컴포넌트가 적절하게 추가되었습니다.
ClubDetailPage에 PhotoList 컴포넌트를 추가하여 활동 사진을 표시하는 기능이 잘 구현되었습니다. 컴포넌트에 필요한 props인
sectionRefs와feeds가 적절하게 전달되고 있습니다.구글 드라이브 URL 변환 유틸리티를 이 컴포넌트에서 사용하는지 확인해보는 것이 좋을 것 같습니다. 만약
PhotoList가feeds배열의 각 항목에 대해 구글 드라이브 URL을 처리한다면, 변환 로직이 올바르게 적용되고 있는지 확인하세요.
🏁 Script executed:
#!/bin/bash # PhotoList 컴포넌트에서 Google Drive URL 변환 유틸리티 사용 확인 echo "PhotoList 컴포넌트 코드 확인:" fd "PhotoList.tsx" | grep -r "frontend/src/pages/ClubDetailPage/components/PhotoList" | xargs cat echo -e "\nPhotoList에서 convertGoogleDriveUrl 임포트 확인:" fd "PhotoList.tsx" | grep -r "frontend/src/pages/ClubDetailPage/components/PhotoList" | xargs grep -r "convertGoogleDriveUrl" || echo "PhotoList에서 convertGoogleDriveUrl을 임포트하지 않음"Length of output: 925
PhotoList 컴포넌트 및 URL 변환 유틸리티 정상 작동 확인
ClubDetailPage에 전달된
sectionRefs와feedsprops를 통해 PhotoList 컴포넌트가 올바르게 구현되었으며, 내부에서@/utils/convertGoogleDriveUrl을 임포트하여 구글 드라이브 URL 변환 로직이 정확하게 적용되었습니다. 모든 확인 결과, 관련 기능은 의도한 대로 동작하고 있음을 확인했습니다.frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx (1)
3-5: 타입 개선이 잘 이루어졌습니다.인터페이스를 중앙 집중화하여 타입 안정성과 유지보수성을 개선한 것은 좋은 리팩토링입니다.
frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx (1)
211-211: 제목 계층 구조 개선이 잘 이루어졌습니다.
<h3>태그를 사용하여 일관된 제목 계층 구조를 유지한 것은, 시맨틱 HTML 관점에서 좋은 변경입니다.frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx (5)
25-31: Google Drive URL 변환 로직이 적절히 구현되어 있습니다.useMemo를 사용하여 불필요한 재계산을 방지하고, 조건부 체크를 통해 안전하게 배열을 처리하는 점이 좋습니다.
33-46: 윈도우 이벤트 리스너 정리를 적절히 처리하고 있습니다.컴포넌트 마운트 시 컨테이너 너비를 업데이트하고, 리사이즈 이벤트를 적절히 관리하고 있습니다. useEffect 클린업 함수를 통해 이벤트 리스너가 정상적으로 제거되어 메모리 누수를 방지합니다.
48-65: 커스텀 훅을 활용한 관심사 분리가 잘 되어 있습니다.usePhotoNavigation 훅을 사용하여 탐색 로직을 컴포넌트에서 분리한 접근 방식이 좋습니다. 이를 통해 컴포넌트 코드가 더 깔끔해지고 유지보수성이 향상됩니다.
66-78: usePhotoSwipe 훅을 효과적으로 사용하고 있습니다.터치 이벤트 관련 로직을 커스텀 훅으로 분리하여 코드의 가독성과 재사용성을 높였습니다.
103-105: 이미지 에러 처리 로직이 잘 구현되어 있습니다.이미지 로드 실패를 적절히 처리하는 함수입니다. 에러가 발생한 이미지 인덱스를 기록하여 대체 UI를 표시하는 방식이 좋습니다.
frontend/src/hooks/usePhotoSwipe.ts (3)
3-14: 인터페이스 정의가 명확하고 타입이 잘 지정되어 있습니다.UsePhotoSwipeProps 인터페이스가 잘 정의되어 있어 훅의 사용 방법을 명확히 이해할 수 있습니다.
32-36: 터치 시작 이벤트 처리가 적절합니다.터치 이벤트 시작 시 현재 상태를 캡처하는 로직이 잘 구현되어 있습니다.
95-101: 훅의 반환값이 명확하게 정의되어 있습니다.훅이 반환하는 값들이 명확하게 정의되어 있어서 사용하기 쉽습니다.
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.styles.ts (5)
3-19: PhotoListContainer 스타일이 적절하게 정의되어 있습니다.반응형 디자인이 잘 구현되어 있으며, 모바일 뷰에서 적절하게 스타일이 변경됩니다.
21-26: PhotoListWrapper 스타일이 간결하게 정의되어 있습니다.오버플로우 처리와 터치 동작이 적절하게 설정되어 있습니다.
28-54: PhotoList 스타일이 복잡한 조건부 로직을 포함하고 있습니다.PhotoList 컴포넌트에 다양한 props를 받아 처리하는 로직이 있습니다. 조건부 스타일링이 복잡해질 경우, 이를 별도의 유틸리티 함수로 분리하는 것을 고려해 볼 수 있습니다.
56-76: PhotoCard 스타일이 잘 정의되어 있습니다.이미지 카드의 스타일이 잘 정의되어 있으며, 모바일 뷰에서도 적절하게 조정됩니다.
143-151: NoImageContainer가 적절하게 구현되어 있습니다.이미지가 없을 때 표시되는 컨테이너 스타일이 잘 정의되어 있습니다.
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoList.tsx
Outdated
Show resolved
Hide resolved
- updateLayout -> updateIsMobile - updateContainer -> updateContainerWidth - handleResize -> handleWindowResize
- 사진 리스트 끝에 가짜 카드를 추가하여 레이아웃을 유지하도록 수정. - `usePhotoNavigation`에서 가짜 카드까지 슬라이드가 가능하도록 로직 수정. - `PhotoCard` 스타일에 `isPlaceholder` 속성을 추가하여 가짜 카드를 숨김 처리. - 모바일 환경에서 카드 크기와 슬라이드 동작이 올바르게 작동하도록 조정.
- calculateTranslateX로 슬라이드 함수 분리 - 필요없는 상수 제거 - mobile상태 제거
#️⃣연관된 이슈
📝작업 내용
활동사진 추가 2087981
구글드라이브 url 변환 유틸함수 0e7e9c1
활동사진 컴포넌트 내 hook 분리
스크롤 인덱스 enum 추가 bb842b9
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항