[feat] 마이페이지 > MY 앨범, MY 좋아요, 내 정보 관리 UI 구현#39
Conversation
- 같은 wifi 네트워크의 모바일 기기에서 개발 서버 접근 가능 - 로컬 개발 환경에서 크로스 디바이스 테스트 지원
- 내부 상태 관리를 useSingleSelect로 통일 - filterList props 제거 및 SortType export - 부모 컴포넌트에서 정렬 상태를 제어할 수 있도록 개선
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Walkthrough라우팅과 레이아웃을 재구성해 Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant App
participant Router as AppRoutes
participant Guard as PrivateRoute
participant Auth as useAuth
participant MyPage
User->>App: 요청
App->>Router: 라우트 렌더링
Router->>Guard: /mypage 진입
Guard->>Auth: isAuth 확인
alt 인증됨
Guard-->>Router: children 허용
Router->>MyPage: MyPage 렌더
else 미인증
Guard->>User: 로그인 필요 Modal 표시
User->>Guard: 로그인/취소 선택 (navigate('/login') 또는 뒤로)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Summary of Changes
Hello @hansololiviakim, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
이 PR은 마이페이지의 핵심 UI를 구현하고, 관련 인프라를 구축하는 데 중점을 둡니다. 사용자 프로필 관리, MY 앨범 및 MY 좋아요 목록 표시 기능을 포함하며, 라우팅 및 인증 가드(PrivateRoute)를 설정하여 페이지 접근 제어를 강화했습니다. 또한, Input 및 Header와 같은 공통 UI 컴포넌트의 유연성을 높이고, 개발 환경의 편의성을 개선하며, 반응형 디자인을 위한 레이아웃 조정을 포함합니다. 새로운 useSingleSelect 훅을 도입하여 상태 관리 로직을 간소화하고, 전반적인 코드 품질과 사용자 경험을 향상시키는 데 기여합니다.
Highlights
- 마이페이지 UI 구현: 사용자 프로필 정보 표시 및 편집, MY 앨범 및 MY 좋아요 플레이리스트 목록을 포함하는 마이페이지 UI가 구현되었습니다.
- 라우터 및 PrivateRoute 설정: 인증이 필요한 페이지에 대한 접근을 제어하는
PrivateRoute컴포넌트가 추가되었으며, 마이페이지 경로에 적용되었습니다. - 공통 컴포넌트 개선: Input 컴포넌트에
value와defaultValueprop이 추가되었고, Header 및 Input 컴포넌트의 스타일링에 스타일드 컴포넌트의 transient props($)가 적용되어 CSS 속성 충돌을 방지했습니다. - 개발 환경 및 반응형 레이아웃 개선: Vite 개발 서버에
--host플래그가 추가되어 동일 네트워크 내 모바일 기기에서 로컬 개발 환경에 접근할 수 있게 되었고,MainLayout의 최대 너비가 430px로 확장되어 iPhone Pro 및 Galaxy Ultra와 같은 넓은 모바일 디바이스에 대응하도록 개선되었습니다. - 새로운 훅 추가 및 ContentHeader 리팩토링: 단일 선택 상태를 관리하는 범용
useSingleSelect훅이 추가되었으며,ContentHeader컴포넌트가 이 훅을 사용하도록 리팩토링되어 코드의 재사용성과 유연성이 향상되었습니다. - 내비게이션 바 통합 및 스타일링: 전체 레이아웃에 하단 내비게이션 바가 추가되었고, 내비게이션 바 영역에 그라데이션 배경이 적용되어 시각적 일관성을 높였습니다.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
|
🎵 Storybook Link 🎵 |
There was a problem hiding this comment.
Code Review
이번 PR은 마이페이지 UI 구현과 관련된 다양한 변경 사항을 포함하고 있습니다. 전반적으로 FSD 아키텍처를 잘 따르고 있으며, useSingleSelect와 같은 재사용 가능한 훅을 도입하여 코드 품질을 개선한 점이 좋습니다. 몇 가지 개선점을 제안합니다. App.tsx에서 매직 넘버와 하드코딩된 색상 값을 테마 시스템을 사용하도록 수정하고, UserProfile.tsx에서는 파일 업로드 오류 처리 로직을 개선하여 사용자 경험을 향상시키는 것을 권장합니다. 또한, 여러 컴포넌트에 중복된 타입 정의와 로직을 공유 모듈로 추출하여 코드의 유지보수성을 높이는 방향을 제안합니다.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (29)
package.json (1)
13-13: vite 설정 중복 확인 필요Vite 설정 파일(vite.config.js 또는 vite.config.ts)에 이미
server.host가 명시되어 있지 않은지 직접 확인해 주세요.
호스트가 중복 설정되면 의도치 않게 LAN에 노출될 수 있습니다.
- 설정 파일 경로 확인 후
server.host설정 유무 점검- README나 개발자 문서에 LAN 모드 사용 조건 명시 권장
src/shared/ui/NavBar.tsx (1)
16-18: 활성화 판단 로직 개선 제안: 중첩 라우트 대응 및 접근성 보강
location.pathname === path비교는/mypage/*같은 중첩 경로에서 비활성 처리될 수 있습니다. 홈(/)은 정확 일치, 그 외는 prefix 매칭으로 처리하거나, react-router-dom의NavLink/useMatch를 활용하는 방식을 권장합니다. 또한 현재 페이지에aria-current="page"를 부여하면 접근성이 개선됩니다.예시:
// 간단 prefix 매칭(홈만 예외) const isActive = path === '/' ? location.pathname === '/' : location.pathname.startsWith(path) // 접근성 보강 <NavLink to={path} aria-current={isActive ? 'page' : undefined}> ... </NavLink>또는
NavLink사용:import { NavLink as RouterNavLink } from 'react-router-dom' const StyledLink = styled(RouterNavLink)` text-decoration: none; display: flex; ` <StyledLink to={path} end={path === '/'} // 홈은 정확일치, 나머지는 중첩 허용 > <NavItem $active={isActive}>...</NavItem> </StyledLink>추가로, 현재
styled(Link)컴포넌트 명을NavLink로 사용 중이라 react-router-dom의NavLink와 혼동될 소지가 있습니다.StyledLink와 같이 명확한 이름으로 변경하면 가독성이 좋아집니다.src/pages/myPage/mock/myPlaylist.json (1)
1-32: 목데이터 추가 OK — 번들 포함/위치 구조 및 타입 안정성 고려
UI 개발 편의성 측면에서 적절합니다. 다만 다음 사항을 확인·검토해 주세요:
- 번들에 포함될 수 있으니, 실제 API 연동 시 mock 데이터 제거 또는 별도 분리 계획 수립
- FSD 구조에서 mock 파일은 페이지 레이어가 아닌
shared/fixtures또는pages/myPage/__mocks__등으로 격리- TS 타입 체크 이점을 위해 JSON 대신 TS 모듈로 전환 고려 (예:
)export const myPlaylist: readonly Playlist[] = [ { id: 1, title: "...", username: "...", isPrimary: true }, … ] as const;- JSON import를 사용하려면
tsconfig*.json에"resolveJsonModule": true설정이 필요하니 수동으로 확인 부탁드립니다src/shared/hooks/useSingleSelect.ts (1)
1-11: 반환 객체 메모이제이션으로 불필요한 리렌더 예방소비자가
{ selected, onSelect }객체를 의존성으로 사용(shallow compare 등)하는 경우를 대비해 반환 객체를 useMemo로 감싸면 좋습니다. onSelect는 이미 안정적이므로 효과가 있습니다.아래처럼 적용 가능합니다.
-import { useState, useCallback } from 'react' +import { useState, useCallback, useMemo } from 'react' export const useSingleSelect = <T>(initialValue: T) => { const [selected, setSelected] = useState(initialValue) const onSelect = useCallback((newValue: T) => { setSelected(newValue) }, []) - return { selected, onSelect } + const api = useMemo(() => ({ selected, onSelect }), [selected, onSelect]) + return api }src/entities/user/model/useAuth.ts (2)
5-13: 하드코딩 상태에 useState 불필요 — 상수/타입 명시로 단순화 권장현재 값이 변하지 않는 하드코딩이라 useState가 오히려 의도를 흐립니다. 상수와 명시적 타입으로 단순화하고, 반환 객체는 useMemo로 고정하면 안정성이 올라갑니다.
아래처럼 정리해 보세요.
-import { useState } from 'react' +import { useMemo } from 'react' -// TODO: PrivateRoute 화면 구현 용 임시 하드코딩, 퍼블리싱(~8.17) 이후 로그인 및 인증 로직 구현 +// TODO: PrivateRoute 화면 구현 용 임시 하드코딩, 퍼블리싱(~8.17) 이후 로그인 및 인증 로직 구현 export const useAuth = () => { - const [isAuth] = useState(true) - const [userInfo] = useState({ - id: '1', - nickname: '김들락', - profileImg: '', - }) - const [isLoading] = useState(false) - const [error] = useState(null) + type UserInfo = { id: string; nickname: string; profileImg: string } + const isAuth = true + const userInfo: UserInfo = { id: '1', nickname: '김들락', profileImg: '' } + const isLoading = false + const error: unknown | null = null그리고 반환부는 아래와 같이 메모이즈를 추천합니다.
- return { - isAuth, // 로그인 여부 - userInfo, // 유저 정보 - isLoading, - error, - login, // 로그인 함수 - logout, // 로그아웃 함수 - } + return useMemo( + () => ({ + isAuth, // 로그인 여부 + userInfo, // 유저 정보 + isLoading, + error, + login, // 로그인 함수 + logout, // 로그아웃 함수 + }), + [isAuth, userInfo, isLoading, error] + )
14-15: login/logout는 임시 함수임을 명확히 표시하고 개발/운영 분기 고려무해하지만 실서비스 전에는 작동하는 구현으로 교체되어야 합니다. 개발 환경에서만 true/목데이터를 쓰고 운영에서는 실제 인증 흐름을 타도록 분기(예: import.meta.env.MODE)하는 가드를 추가하면 안전합니다.
운영 빌드에서 임시 하드코딩이 남아있지 않은지 확인해 주세요.
src/shared/ui/ContentHeader.tsx (3)
26-26: totalCount 타입과 사용 방식 일치시키기Prop 타입이
number로 고정인데, 렌더링에서는totalCount ?? 0으로 방어하고 있습니다. 둘 중 하나로 통일하는 것이 좋습니다.
totalCount가 항상 전달된다면 아래처럼 간소화하세요.- <span>총 {totalCount ?? 0}개</span> + <span>총 {totalCount}개</span>반대로 미전달 가능성이 있으면 Prop 타입을
number | undefined로 열어주세요.
27-34: 정렬 바텀시트 트리거에 접근성 속성 추가BottomSheet 토글 버튼에
aria-haspopup/aria-expanded/aria-controls를 부여해 접근성을 개선할 수 있습니다. BottomSheet 루트에 id도 함께 지정하세요.아래처럼 적용 가능합니다.
- <FilterButton type="button" onClick={() => setIsOpen(true)}> + <FilterButton + type="button" + aria-haspopup="dialog" + aria-expanded={isOpen} + aria-controls="sort-bottom-sheet" + onClick={() => setIsOpen(true)} + > <Filter /> <span>{currentSort === 'latest' ? '최신순' : '인기순'}</span> </FilterButton> ... - <BottomSheet isOpen={isOpen} onClose={() => setIsOpen(false)} height="200px"> + <BottomSheet + id="sort-bottom-sheet" + isOpen={isOpen} + onClose={() => setIsOpen(false)} + height="200px" + >
33-48: 바텀시트 고정 높이 매직 넘버 최소화
height="200px"은 매직 넘버입니다. 테마 토큰/상수로 추출하거나 콘텐츠 기반 높이(컴포넌트 내부 auto-fit 지원 시)를 고려해 주세요.예시:
- <BottomSheet isOpen={isOpen} onClose={() => setIsOpen(false)} height="200px"> + {/* TODO: 높이 토큰/상수로 관리 - 작성자 */} + <BottomSheet isOpen={isOpen} onClose={() => setIsOpen(false)} height="200px">또는 상수 도입:
const SHEET_HEIGHT = '200px' ... <BottomSheet ... height={SHEET_HEIGHT}>src/stories/Input.stories.ts (1)
61-69: 에러 스토리에 defaultValue 추가, 의도 명확하고 좋습니다. Storybook 컨트롤에도 value/defaultValue 노출 추천에러 케이스 재현이 쉬워져서 👍🏻 하지만 새로 추가한 API를 문서화/실험하기 쉽게 argTypes에 value/defaultValue 컨트롤도 같이 노출하면 더 좋습니다.
다음과 같이 argTypes에 두 컨트롤을 추가해 보세요:
argTypes: { type: { control: { type: 'select' }, options: ['text', 'search', 'url'], }, + value: { + control: { type: 'text' }, + }, + defaultValue: { + control: { type: 'text' }, + }, iconPosition: { control: { type: 'select' }, options: ['left', 'right'], }, error: { control: { type: 'boolean' }, }, width: { control: { type: 'text' }, }, },src/pages/myPage/ui/components/Divider.tsx (2)
14-17: 100dvw + 좌우 -20px 마진 조합은 일부 환경에서 가로 스크롤(overflow)을 유발할 수 있습니다부모가 패딩 20px을 갖고, 자식이 width: 100dvw 이면서 margin: 0 -20px이면 실제 렌더 폭이 뷰포트보다 커질 수 있습니다. 모바일 브라우저/환경(특히 iOS Safari)에서 수평 스크롤이 생길 가능성이 있습니다.
권장 수정: 컨테이너 너비를 기준(%)으로 계산하고 필요 시 최대폭만 제한하는 방식이 안정적입니다.
const StyledDivider = styled.div<{ $deviceType: DeviceType }>` margin: 0 -20px; - width: ${({ $deviceType }) => - $deviceType === 'mobile' ? 'clamp(320px, 100dvw, 430px)' : '375px'}; + /* 컨테이너 기준으로 꽉 채우고, 기기별 최대 폭만 제한 */ + width: 100%; + max-width: ${({ $deviceType }) => ($deviceType === 'mobile' ? '430px' : '375px')}; height: 12px; background-color: ${({ theme }) => theme.COLOR['gray-800']}; `추가로, -20px은 레이아웃 패딩과 강하게 결합된 매직 넘버입니다. theme 혹은 전역 spacing 토큰이 있다면 그 상수를 사용해 결합도를 낮추는 것도 고려해주세요.
검증 팁: iOS Safari(실기/시뮬레이터), Android Chrome에서 DevTools로 뷰포트 폭 변화를 주고, body에 overflow-x가 생기지 않는지 확인해 주세요.
5-8: useDevice 의존 없이 CSS만으로 동일 요구사항을 충족 가능해 보입니다현재 로직은 디바이스 타입 분기와 훅 의존이 있으나, CSS clamp/백분율을 쓰면 훅/프롭 없이도 목표 레이아웃을 만들 수 있습니다. 리사이즈 대응성, 테스트 용이성, 의존성 감소 측면에서 이점이 있습니다.
간소화 예시:
-import styled from 'styled-components' - -import { useDevice, type DeviceType } from '@/shared/hooks/useDevice' +import styled from 'styled-components' const Divider = () => { - const deviceType = useDevice() - return <StyledDivider $deviceType={deviceType} /> + return <StyledDivider /> } export default Divider -const StyledDivider = styled.div<{ $deviceType: DeviceType }>` +const StyledDivider = styled.div` margin: 0 -20px; - width: ${({ $deviceType }) => - $deviceType === 'mobile' ? 'clamp(320px, 100dvw, 430px)' : '375px'}; + /* 컨테이너 기준으로 100%, 최소/최대 폭만 제한 */ + width: clamp(320px, 100%, 430px); height: 12px; background-color: ${({ theme }) => theme.COLOR['gray-800']}; `이렇게 하면 useDevice 변경/미반영(리사이즈) 이슈도 자연스럽게 해소됩니다.
Also applies to: 12-18
src/shared/ui/Profile.tsx (1)
36-40: 이미지 프리로드 시 언마운트/경쟁 상태 방지용 cleanup 추가 제안프로필 URL이 빠르게 바뀌거나 컴포넌트가 언마운트될 때, 이전 Image 인스턴스의 onload/onerror가 뒤늦게 실행되어 setState가 호출될 수 있습니다. 작은 확률이지만 경합을 줄이고 메모리 참조를 정리하도록 cleanup을 추가하는 것을 권장합니다.
아래와 같이 isActive 가드와 이벤트 핸들러 정리를 추가해 주세요.
- const img = new Image() - img.onload = () => setImgSrc(profileUrl) - img.onerror = () => setImgSrc(DefaultProfile) - img.src = profileUrl + const img = new Image() + let isActive = true + img.onload = () => { + if (isActive) setImgSrc(profileUrl) + } + img.onerror = () => { + if (isActive) setImgSrc(DefaultProfile) + } + img.src = profileUrl + return () => { + isActive = false + img.onload = null + img.onerror = null + }추가로, src 상태의 타입 명시를 강화하려면 다음처럼 적용을 고려해도 좋습니다(선택):
const [imgSrc, setImgSrc] = useState<string>(DefaultProfile)src/widgets/authGuard/PrivateRoute.tsx (2)
16-17: isAuth 변경 시 모달 열림 상태 동기화 필요초기값을
!isAuth로 잡는 것만으로는 이후 인증 상태가 변할 때 모달 열림 상태가 동기화되지 않습니다. 로그인/로그아웃 시 UX 일관성을 위해useEffect로 동기화하는 것을 권장합니다.-import { useState } from 'react' +import { useEffect, useState } from 'react' @@ const { isAuth } = useAuth() const [isModalOpen, setIsModalOpen] = useState(!isAuth) + useEffect(() => { + setIsModalOpen(!isAuth) + }, [isAuth])
28-41: ProtectedRoute에서navigate(-1)대신 명시적 로그인 페이지 리다이렉트 적용 권장현재
PrivateRoute.tsx의 28–41행에서 인증되지 않은 경우navigate(-1)로 뒤로가기를 시도하고 있는데,
새 탭·딥링크 등으로 보호 경로에 직진입할 경우 브라우저 히스토리에 이전 기록이 없어 빈 화면이 노출될 수 있습니다.제안 사항:
navigate('/login')또는 상수화된 로그인 경로(ROUTES.LOGIN등)로 명시적 리다이렉트- 취소 동작(“다음에 하기”) 시에도 홈(
'/') 등 안전한 기본 경로로 이동하도록 추가 처리아울러, 로그인 경로가 실제로
<Route path="/login" …>또는 상수 매핑 형태로 정의되어 있는지 확인 부탁드립니다.src/App.tsx (1)
47-61: NavContainer의 -20px 마진 해크 제거 및 폭 계산 단순화 제안고정 네비 컨테이너에
margin: 0 -20px 0 0해크는 레이아웃 정밀도를 떨어뜨릴 수 있습니다. 컨테이너 폭을100%/max-width로 맞추고 좌우 패딩을 부여하는 방식이 더 단순하고 예측 가능합니다.const NavContainer = styled.div<{ $layoutWidth: string $layoutBottomGap: number }>` z-index: 100; position: fixed; - left: 50%; + left: 50%; bottom: 0; - margin: 0 -20px 0 0px; + margin: 0 auto; ${flexRowCenter} - width: ${({ $layoutWidth }) => $layoutWidth}; - padding-bottom: ${({ $layoutBottomGap }) => $layoutBottomGap}px; + width: 100%; + max-width: ${({ $layoutWidth }) => $layoutWidth}; + padding: 0 20px ${({ $layoutBottomGap }) => $layoutBottomGap}px; transform: translateX(-50%); background: linear-gradient(to bottom, rgba(15, 16, 20, 0), rgba(15, 16, 20, 1), rgb(15, 16, 20)); `src/pages/myPage/ui/index.tsx (4)
43-56: 아이콘 버튼 접근성 향상: 시각적 레이블이 없어 aria-label 추가 필요SvgButton은 텍스트가 없어 스크린리더에 목적이 전달되지 않습니다. 명확한 aria-label을 부여해 주세요.
<SvgButton icon={Notification} width={24} height={24} - onClick={() => navigate('/mypage/notification')} + aria-label="알림 보기" + onClick={() => navigate('/mypage/notification')} /> <SvgButton icon={Gear} width={24} height={24} - onClick={() => navigate('/mypage/settings')} + aria-label="설정 열기" + onClick={() => navigate('/mypage/settings')} />
61-69: 탭 위젯의 ARIA 역할/상태 지정 제안탭 리스트/탭에 role과 aria-selected를 지정하면 접근성이 크게 향상됩니다.
- <TabContainer> + <TabContainer role="tablist"> {TAB_LIST.map((tab) => ( - <TabItem key={tab.value} $isActive={currentTab === tab.value}> - <button type="button" onClick={() => setCurrentTab(tab.value)}> + <TabItem key={tab.value} $isActive={currentTab === tab.value}> + <button + type="button" + role="tab" + aria-selected={currentTab === tab.value} + onClick={() => setCurrentTab(tab.value)} + > {tab.label} </button> </TabItem> ))} </TabContainer>
24-31: 타입 중복 방지 위해 아이템 타입 별도 선언/공유 권장
currentPlaylist의 인라인 타입은PlaylistGrid에서 기대하는 아이템 타입과 중복될 가능성이 있습니다.PlaylistItem같은 타입을 선언해 공유하면 유지보수성이 상승합니다.
32-35: 정적 TAB_LIST는 컴포넌트 외부로 hoist 권장렌더마다 재생성되지 않도록 컴포넌트 밖 상수로 이동하면 미세하지만 불필요한 비용을 줄일 수 있습니다.
src/stories/ContentHeader.stories.tsx (1)
21-25: 스토리 상호작용 가시성 향상: onSortChange를 actions에 연결 제안Docs/Controls에서 이벤트 확인이 쉽도록 action 바인딩을 권장합니다. 현재 args의 빈 함수는 제거 가능합니다.
예시(메타 정의 보완):
const meta: Meta<typeof ContentHeader> = { title: 'Shared/ContentHeader', component: ContentHeader, parameters: { layout: 'centered' }, tags: ['autodocs'], argTypes: { onSortChange: { action: 'onSortChange' }, }, }src/pages/myPage/ui/components/PlaylistGrid.tsx (4)
26-29: 접근성: 추가 타일에 명확한 라벨을 부여하세요.Plus 아이콘만 있는 버튼은 스크린 리더에서 용도를 파악하기 어렵습니다. aria-label을 추가해 주세요.
아래처럼 수정 제안합니다:
- <SvgButton icon={Plus} width={40} height={40} /> + <SvgButton icon={Plus} width={40} height={40} aria-label="플레이리스트 추가" />
56-60: gap이 적용되지 않습니다 (display 미설정).CdContainer는 기본 display가 list-item이므로 gap이 동작하지 않습니다. column 레이아웃 의도가 맞다면 flex로 전환하세요.
-const CdContainer = styled.li` - gap: 12px; - width: 104px; -` +const CdContainer = styled.li` + display: flex; + flex-direction: column; + gap: 12px; + width: 104px; +`
48-55: 그리드(col 1fr)와 고정 폭 아이템(104px) 혼용으로 생길 수 있는 정렬 이슈 검토리스트 아이템이 고정 폭(104px)인 반면, grid-template-columns는 1fr 3등분입니다. 뷰포트/컨테이너 폭에 따라 각 column 내에서 좌우 여백이 과하게 생길 수 있습니다. 균일한 타일 배치를 원하시면 다음과 같은 대안 고려를 권장합니다.
- repeat(3, 104px) + justify-content: space-between (혹은 center)
- repeat(3, minmax(0, 104px)) + justify-content 조정
현재도 동작은 하지만, 디자인 픽셀 퍼펙트를 지향한다면 검토해 주세요.
13-20: 재사용 가능한 타입 분리 제안currentPlaylist의 아이템 타입을 inline으로 정의하고 있습니다. 다른 컴포넌트/페이지에서도 재사용될 가능성이 높으므로 별도 타입(예: PlaylistItem)으로 분리해 공용 모듈(entities/types 등)에서 관리하면 유지보수성이 좋아집니다.
src/shared/ui/index.ts (1)
1-13: 배럴 파일에서 네임드 export 노출 필요성 확인 (NAV_HEIGHT, 타입 등)이 배럴은 default export만 재노출합니다. NavBar의 NAV_HEIGHT, ContentHeader의 SortType, Profile의 ProfileUrl 같은 네임드 export를 배럴에서 바로 사용할 수 있게 노출하면 import 경로가 단순해지고 일관성이 좋아집니다. 실제로 해당 심볼들이 각 모듈에서 export되고 있고, 배럴에서 사용하려는 곳이 있다면 아래와 같이 보강을 제안합니다.
export { default as NavBar } from './NavBar' export { default as Overlay } from './Overlay' export { default as Profile } from './Profile' export { default as SvgButton } from './SvgButton' + +// 필요 시 네임드 export도 함께 노출 +// (각 모듈에서 해당 심볼을 실제 export하고 있는지 확인 필요) +export { default as NavBar, NAV_HEIGHT } from './NavBar' +export type { SortType } from './ContentHeader' +export type { ProfileUrl } from './Profile'네임드 심볼을 배럴에서 노출할 의사가 없다면, 관련 사용처가 개별 모듈 경로를 바라보도록 정리해 주시면 됩니다.
src/pages/myPage/ui/components/UserProfile.tsx (1)
45-46: 상수 분리로 가독성과 재사용성 개선MAX_FILE_SIZE는 컴포넌트 렌더마다 재생성됩니다. 파일 상단에 상수로 분리하면 의미가 명확해지고 테스트/재사용에 유리합니다.
예:
const MAX_FILE_SIZE_MB = 5 const MAX_FILE_SIZE = 1024 * 1024 * MAX_FILE_SIZE_MBsrc/shared/ui/Input.tsx (2)
56-65: onChange 미제공 시 제어/비제어 경고 방지value를 전달하면서 onChange가 없으면 React 콘솔 경고가 발생할 수 있습니다. 내부적으로 readOnly를 조건부 부여하여 안전하게 처리하는 것을 권장합니다.
<StyledInput type={type} placeholder={placeholder} value={value} defaultValue={defaultValue} maxLength={maxLength} + readOnly={value !== undefined && !onChange} onChange={onChange} onFocus={onFocus} onBlur={onBlur} />
12-13: 지원 input type 확장 고려현재 InputType이 'text' | 'search' | 'url'로 제한되어 있습니다. 향후 폼 활용(로그인/회원가입 등)을 고려하면 'email' | 'password' | 'number' | 'tel' 등도 포함하면 재사용성이 높아집니다.
-type InputType = 'text' | 'search' | 'url' +type InputType = 'text' | 'search' | 'url' | 'email' | 'password' | 'number' | 'tel'
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (2)
src/assets/icons/icn_camera.svgis excluded by!**/*.svgsrc/assets/icons/icn_plus.svgis excluded by!**/*.svg
📒 Files selected for processing (26)
package.json(1 hunks)src/App.tsx(2 hunks)src/app/routes/routes.tsx(1 hunks)src/assets/icons/index.ts(2 hunks)src/entities/user/model/useAuth.ts(1 hunks)src/entities/user/model/userStore.ts(0 hunks)src/pages/myPage/mock/likePlaylist.json(1 hunks)src/pages/myPage/mock/myPlaylist.json(1 hunks)src/pages/myPage/ui/components/Divider.tsx(1 hunks)src/pages/myPage/ui/components/PlaylistGrid.tsx(1 hunks)src/pages/myPage/ui/components/UserProfile.tsx(1 hunks)src/pages/myPage/ui/components/index.ts(1 hunks)src/pages/myPage/ui/index.tsx(1 hunks)src/shared/config/navItems.ts(1 hunks)src/shared/hooks/useSingleSelect.ts(1 hunks)src/shared/hooks/useSort.ts(0 hunks)src/shared/styles/theme.ts(1 hunks)src/shared/ui/ContentHeader.tsx(1 hunks)src/shared/ui/Header.tsx(2 hunks)src/shared/ui/Input.tsx(6 hunks)src/shared/ui/NavBar.tsx(2 hunks)src/shared/ui/Profile.tsx(2 hunks)src/shared/ui/index.ts(1 hunks)src/stories/ContentHeader.stories.tsx(2 hunks)src/stories/Input.stories.ts(1 hunks)src/widgets/authGuard/PrivateRoute.tsx(1 hunks)
💤 Files with no reviewable changes (2)
- src/entities/user/model/userStore.ts
- src/shared/hooks/useSort.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit Configuration File
**/*.{ts,tsx,js,jsx}: ## 1. 일반적인 코딩 컨벤션포맷팅
.prettierrc설정에 따라 포맷팅 확인- 들여쓰기: 2칸 스페이스
- 최대 줄 길이: 100자
- 세미콜론 사용 안함
- 따옴표: 작은따옴표 사용
- 괄호 안 공백: 있음
- 화살표 함수 괄호: 항상 사용
- 줄바꿈: LF 사용
네이밍 컨벤션
- 컴포넌트: PascalCase (예: UserProfile)
- 유틸리티/훅/변수: camelCase (예: getUserData, useUserInfo)
- 상수: UPPER_SNAKE_CASE (예: API_BASE_URL)
- 이미지 파일: kebab-case (예: user-profile-icon.png)
주석 사용
- 복잡한 로직에만 주석 추가
- 불필요한 주석 지양 (코드로 설명 가능한 것)
- TODO/FIXME 형식:
// TODO: 설명 - 작성자가독성
- 매직 넘버 지양, 의미있는 상수 사용
- 함수는 하나의 책임만 가지도록 작성 (최대 20줄 권장)
- 중첩 깊이 최소화 (3단계 이하 권장)
2. React 모범 사례
컴포넌트 작성
- 최신 React hooks 사용 권장
- 컴포넌트는 단일 책임 원칙 준수
- Presentational/Container 컴포넌트 분리
- 성능 최적화: memo, useCallback, useMemo 적절히 사용
- 대용량 리스트는 가상화 라이브러리 사용 고려
상태 관리
- Zustand와 Tanstack Query를 일관되게 사용
- 상태 구조는 정규화된 형태로 관리
- 에러 처리: Error Boundary와 try-catch 또는 onError 콜백 활용
3. 스타일링
Styled Components
- Styled Components 일관되게 사용
- 스타일드 컴포넌트명은 의미있게 작성
- 동적 스타일링은 props나 CSS 변수 활용
- 테마 시스템 활용하여 글로벌 스타일 관리
- 재사용 가능한 스타일은 mixin이나 확장으로 관리
- CSS 포맷팅 가독성 유지
- 사용하지 않는 스타일이나 중복 스타일 제거
4. Vite 및 빌드 최적화
- 모듈 import 최적화 (tree-shaking 고려)
- 환경변수는 .env 파일로 관리
- vite.config.ts에서 빌드 성능 튜닝 (sourcemap 설정, 플러그인 최적화 등)
5. 아키텍처 및 개발 환경
폴더 구조
- Feature-Sliced Design (FSD) 구조 준수
- 레이어별 참조 규칙 엄격히 적용
타입스크립트
- strict 모드 사용
- 타입 명시적으로 작성 (any 사용 지양)
- path alias (@/_) 절대 경로 import 사용
Git 훅
- Husky 설정으로 pre-commit, commit-msg 린팅 확인
6. 기타 가이드라인
- 충분한 근거와 함께 리뷰 제공
- 정보 검증 후 답변
- 간결하고 명확한 응답
- 필요시 추가 컨텍스트 요청
- 검증되지 않은 주장 지양
- 가능한 경우 출처 명시
- 별도 언급 없으면 JavaScript 기준
- 한국어로 응답
- 대부분 브라우저에서 지원하는 ES6+ 기능 활용
- 코드 리뷰를 통한 유지보수성 향상에 적극 활용
Files:
src/shared/hooks/useSingleSelect.tssrc/entities/user/model/useAuth.tssrc/pages/myPage/ui/components/index.tssrc/assets/icons/index.tssrc/shared/config/navItems.tssrc/pages/myPage/ui/components/UserProfile.tsxsrc/shared/ui/index.tssrc/pages/myPage/ui/components/Divider.tsxsrc/shared/styles/theme.tssrc/shared/ui/NavBar.tsxsrc/app/routes/routes.tsxsrc/pages/myPage/ui/components/PlaylistGrid.tsxsrc/stories/Input.stories.tssrc/shared/ui/Header.tsxsrc/widgets/authGuard/PrivateRoute.tsxsrc/pages/myPage/ui/index.tsxsrc/shared/ui/Profile.tsxsrc/stories/ContentHeader.stories.tsxsrc/App.tsxsrc/shared/ui/ContentHeader.tsxsrc/shared/ui/Input.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-12T18:31:01.433Z
Learnt from: maylh
PR: dnd-side-project/dnd-13th-8-frontend#14
File: src/stories/NavBar.stories.ts:1-1
Timestamp: 2025-08-12T18:31:01.433Z
Learning: Storybook에서 Meta, StoryObj 타입은 'storybook/react'가 아닌 프레임워크별 패키지에서 import해야 함. React-Vite 프로젝트의 경우 'storybook/react-vite'에서 import하는 것이 공식 가이드에 따른 올바른 방식임.
Applied to files:
src/stories/ContentHeader.stories.tsx
📚 Learning: 2025-08-15T06:26:48.282Z
Learnt from: hansololiviakim
PR: dnd-side-project/dnd-13th-8-frontend#27
File: src/shared/styles/globalStyle.ts:34-50
Timestamp: 2025-08-15T06:26:48.282Z
Learning: In the dnd-13th-8-frontend project, the team uses a centralized approach with the custom Input component (src/shared/ui/Input.tsx) for all input elements, implementing custom focus styling with :focus-within instead of relying on native browser focus styles. This architectural decision allows them to safely use global input outline resets in globalStyle.ts.
Applied to files:
src/shared/ui/Input.tsx
🧬 Code Graph Analysis (9)
src/pages/myPage/ui/components/UserProfile.tsx (4)
src/entities/user/model/useAuth.ts (1)
useAuth(4-25)src/shared/ui/Profile.tsx (1)
ProfileUrl(8-8)src/shared/styles/mixins.ts (2)
flexColCenter(10-15)flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-116)
src/pages/myPage/ui/components/Divider.tsx (2)
src/shared/hooks/useDevice.ts (2)
useDevice(14-17)DeviceType(3-3)src/shared/styles/theme.ts (1)
theme(3-116)
src/pages/myPage/ui/components/PlaylistGrid.tsx (2)
src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-116)
src/shared/ui/Header.tsx (2)
src/shared/styles/theme.ts (1)
theme(3-116)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/widgets/authGuard/PrivateRoute.tsx (1)
src/entities/user/model/useAuth.ts (1)
useAuth(4-25)
src/pages/myPage/ui/index.tsx (4)
src/shared/hooks/useSingleSelect.ts (1)
useSingleSelect(3-11)src/shared/ui/ContentHeader.tsx (1)
SortType(8-8)src/shared/styles/theme.ts (1)
theme(3-116)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/stories/ContentHeader.stories.tsx (2)
src/shared/hooks/useSingleSelect.ts (1)
useSingleSelect(3-11)src/shared/ui/ContentHeader.tsx (1)
SortType(8-8)
src/App.tsx (5)
src/shared/hooks/useDevice.ts (1)
useDevice(14-17)src/app/routes/routes.tsx (1)
AppRoutes(7-20)src/shared/ui/NavBar.tsx (1)
NAV_HEIGHT(8-8)src/shared/styles/theme.ts (1)
theme(3-116)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/shared/ui/Input.tsx (1)
src/shared/styles/theme.ts (1)
theme(3-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Chromatic
🔇 Additional comments (17)
src/shared/ui/NavBar.tsx (2)
8-9: NAV_HEIGHT 상수 추출 좋습니다레이아웃 하단 패딩 계산 등 다른 레이어에서 재사용하기 용이해졌습니다.
45-46: 고정 높이를 상수로 치환해 일관성 확보스타일 변경 시 단일 소스만 수정하면 되어 유지보수성이 좋아졌습니다.
src/shared/styles/theme.ts (1)
11-12: 에러 컬러 토큰 추가 적절
'common-error': '#ff5454'추가로 상태 표현(에러) 일관성을 유지하기 좋아졌습니다. 기존 토큰 네이밍 컨벤션(kebab-case)과도 일치합니다.src/shared/hooks/useSingleSelect.ts (1)
3-11: 단일 선택 훅 구현, 심플하고 적절합니다
- 제네릭 지원, 상태/업데이터 분리(onSelect) 모두 적절합니다
- onSelect를 useCallback으로 고정해 소비자 입장에서 참조 안정성도 확보됐습니다
src/pages/myPage/mock/likePlaylist.json (1)
1-27: 목데이터 스키마 일관성 양호PlaylistGrid에서 기대하는 id/title/username 형태에 맞습니다. i18n/한글 문자열도 문제 없습니다. 필요 시 썸네일이나 카운트 등의 확장 필드는 후속 API 연동 시 추가하면 되겠습니다.
src/shared/ui/Header.tsx (2)
12-16: transient prop($position) 도입 적절DOM에 불필요한 prop가 전달되지 않도록 한 점 좋습니다. 좌/우 영역 배치도 명확합니다.
29-44: flexColCenter export 확인 완료
src/shared/styles/mixins.ts에서export const flexColCenter가 정의되어 있어 import가 정상 작동합니다. 추가 조치 불필요합니다.src/assets/icons/index.ts (1)
1-16: 아이콘 배럴 확장 좋습니다 (Camera, Plus 추가).새 컴포넌트에서의 사용과 네이밍 컨벤션이 기존과 일관됩니다. 빌드 시점 SVGR 설정(?react)과 타입이 이미 구성되어 있다면 추가 작업 없이 동작할 것으로 보입니다.
src/pages/myPage/ui/components/index.ts (1)
1-3: 배럴 export로 import 경로가 단순해져 사용성이 좋아졌습니다.라이트한 레벨에서 트리쉐이킹에도 문제 없어 보입니다.
src/app/routes/routes.tsx (1)
7-20: PrivateRoute에서 children 패턴 지원 확인 완료
src/widgets/authGuard/PrivateRoute.tsx의PrivateRouteProps에children: React.ReactNode가 정의되어 있습니다.- 권한 체크 후(
isAuth), 승인 시return <>{children}</>형태로 children을 그대로 렌더링합니다.- 런타임에
element프롭을 기대하지 않으므로<PrivateRoute><MyPage/></PrivateRoute>패턴 사용 시 에러가 발생하지 않습니다.위 내용을 바탕으로 해당 리뷰 코멘트는 해결되었습니다.
src/shared/ui/Profile.tsx (2)
30-34: 파일 입력 처리와 URL.revokeObjectURL 정리 훌륭합니다File 인풋에 대해 object URL 생성 후 cleanup에서 revoke 처리한 점이 메모리 관리 측면에서 적절합니다. 이로 인해 파일 변경/언마운트 시 누수 가능성을 예방합니다.
8-13: ProfileUrl 타입 확장에 따른 사용처 호환성 점검 필요
Profile컴포넌트가string | File | null타입을 받도록 변경되었으므로, 소비 지점에서 여전히 문자열만 처리하도록 가정하고 있지 않은지 확인해 주세요. 특히 File 객체를 그대로 전달했을 때 UI나 로직에 문제가 없는지 점검이 필요합니다.확인된 사용처:
- src/pages/myPage/ui/components/UserProfile.tsx:64
•<Profile size="L" profileUrl={isEditMode ? updatedProfile.profileImg : userInfo.profileImg} />updatedProfile.profileImg와userInfo.profileImg가 File일 경우, 내부에서 URL 변환(URL.createObjectURL)이나 에러 발생 방지 로직이 있는지 검토
• 문자열 전용 처리(예: 빈 문자열 검사, URL 유효성 검사 등)로 가정된 부분이 없는지 확인위 외에 더 많은 사용처가 있을 수 있으니
rg '<Profile\b' --glob '*.tsx'등으로 전체 코드를 한 번 더 검색해 보시기 바랍니다.src/pages/myPage/ui/index.tsx (1)
21-23: 상태 관리 간결하고 일관적입니다 (useSingleSelect 적용 Good)탭/정렬 상태를 제네릭 훅으로 일관되게 관리한 점이 읽기성과 재사용성 모두에 도움이 됩니다.
src/stories/ContentHeader.stories.tsx (2)
1-1: Storybook 타입 import 경로 적절합니다 (react-vite 사용 OK)장기 학습 노트에 맞게 '@storybook/react-vite'에서 Meta/StoryObj를 import한 점 확인했습니다.
51-62: render에서 훅 사용 패턴 적절합니다Story의 render는 React 함수 컴포넌트로 실행되므로 훅 사용이 적합합니다. 데모 목적의 컨테이너/설명 텍스트 구성도 이해에 도움이 됩니다.
src/shared/ui/index.ts (1)
1-13: 배럴 구성 자체는 적절합니다UI 컴포넌트를 단일 진입점으로 모으는 방향은 프로젝트 전반의 import 일관성과 DX에 도움이 됩니다.
src/shared/ui/Input.tsx (1)
74-96: 스타일 로직은 명확하고, 테마 연동도 적절합니다
- 에러/포커스 상태의 보더 색상이 테마 토큰에 잘 매핑되어 있습니다.
- iconPosition에 따른 row/row-reverse 전환도 깔끔합니다.
- 프로필 이미지 파일 초과로 미업로드 시 null 할당 → 사용자 프로필 재할당으로 수정 - React 컴포넌트 라이프사이클에 맞춰 blob: URL 해제 및 메모리 릭 방지
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/pages/myPage/ui/components/UserProfile.tsx (1)
103-105: 상태 갱신은 함수형 업데이트로 변경 권장현재 스프레드 기반 갱신은 오래된 클로저를 잡을 가능성이 있어, 함수형 업데이트가 더 안전합니다.
- onChange={(e) => - setUpdatedProfile({ ...updatedProfile, nickname: e.target.value.trim() }) - } + onChange={(e) => + setUpdatedProfile((prev) => ({ ...prev, nickname: e.target.value.trim() })) + }
🧹 Nitpick comments (5)
src/pages/myPage/ui/components/UserProfile.tsx (5)
68-71: 동일 파일 재선택 허용 +window.접두어 제거 제안
- 동일 파일을 다시 선택해도
onChange가 트리거되도록 성공 처리 후 파일 input 값을 비워두는 것이 좋습니다.- 브라우저 환경에서
URL.createObjectURL은window.접두어 없이 사용 가능합니다. SSR 호환성/가독성 측면에서window.제거를 권장합니다.아래처럼 수정 제안드립니다.
- setIsFileError(false) - const image = window.URL.createObjectURL(file) - setUpdatedProfile((prev) => ({ ...prev, profileImg: image })) + setIsFileError(false) + const image = URL.createObjectURL(file) + setUpdatedProfile((prev) => ({ ...prev, profileImg: image })) + // 같은 파일 재선택 시 onChange가 동작하도록 초기화 + if (fileInputRef.current) { + fileInputRef.current.value = '' + }
97-106: 입력 중 실시간 trim은 사용자 경험 저하 가능 — 저장/blur 시 trim으로 이동 고려타이핑 중 공백이 즉시 제거되면 사용자가 의아해할 수 있습니다. onChange에서는 원문을 유지하고, onBlur 또는 저장 시점에 trim 적용을 권장합니다.
예시:
- onChange={(e) => - setUpdatedProfile((prev) => ({ ...prev, nickname: e.target.value.trim() })) - } + onChange={(e) => + setUpdatedProfile((prev) => ({ ...prev, nickname: e.target.value })) + } + onBlur={(e) => + setUpdatedProfile((prev) => ({ ...prev, nickname: e.target.value.trim() })) + }
39-51: 저장 시 로컬 편집 내용이 곧바로 사라짐 — 의도 확인 요청현재 저장 클릭 시
updatedProfile을 즉시userInfo로 되돌려 미리보기가 원상태(서버 상태)로 복구됩니다. API 연동 전까지는 사용자가 저장 결과를 즉시 확인하지 못해 다소 혼란스러울 수 있습니다. 의도된 임시 동작인지 확인 부탁드립니다. 필요하다면 임시로 낙관적 반영(뷰 모드에서도updatedProfile사용)하거나, “저장되었습니다(실제 반영은 서버 연동 후)” 토스트 안내를 추가하는 방식을 고려해볼 수 있습니다.
18-25: userInfo 갱신 시 로컬 편집 상태 동기화 보강 제안외부에서
userInfo가 변동될 수 있는 구조라면(예: API로 최신 프로필 동기화), 편집 중이 아닐 때updatedProfile을userInfo와 동기화하는 효과를 추가하면 일관성이 좋아집니다.const [isFileError, setIsFileError] = useState(false) + // 외부 userInfo 업데이트에 따른 로컬 상태 동기화 (편집 중 제외) + useEffect(() => { + if (!isEditMode) { + setUpdatedProfile({ + nickname: userInfo.nickname, + profileImg: userInfo.profileImg, + }) + } + }, [userInfo.nickname, userInfo.profileImg, isEditMode])
93-93: 에러 메시지에 스크린 리더 알림 속성 추가 권장접근성 향상을 위해 에러 메시지에
role="alert"/aria-live="polite"를 부여하면 화면 읽기 프로그램이 변화를 인지할 수 있습니다.- {isFileError && <FileErrMsg>5MB 이하의 파일만 업로드 가능해요</FileErrMsg>} + {isFileError && ( + <FileErrMsg role="alert" aria-live="polite"> + 5MB 이하의 파일만 업로드 가능해요 + </FileErrMsg> + )}
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/App.tsx(2 hunks)src/entities/user/model/useAuth.ts(1 hunks)src/pages/myPage/ui/components/UserProfile.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/entities/user/model/useAuth.ts
- src/App.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit Configuration File
**/*.{ts,tsx,js,jsx}: ## 1. 일반적인 코딩 컨벤션포맷팅
.prettierrc설정에 따라 포맷팅 확인- 들여쓰기: 2칸 스페이스
- 최대 줄 길이: 100자
- 세미콜론 사용 안함
- 따옴표: 작은따옴표 사용
- 괄호 안 공백: 있음
- 화살표 함수 괄호: 항상 사용
- 줄바꿈: LF 사용
네이밍 컨벤션
- 컴포넌트: PascalCase (예: UserProfile)
- 유틸리티/훅/변수: camelCase (예: getUserData, useUserInfo)
- 상수: UPPER_SNAKE_CASE (예: API_BASE_URL)
- 이미지 파일: kebab-case (예: user-profile-icon.png)
주석 사용
- 복잡한 로직에만 주석 추가
- 불필요한 주석 지양 (코드로 설명 가능한 것)
- TODO/FIXME 형식:
// TODO: 설명 - 작성자가독성
- 매직 넘버 지양, 의미있는 상수 사용
- 함수는 하나의 책임만 가지도록 작성 (최대 20줄 권장)
- 중첩 깊이 최소화 (3단계 이하 권장)
2. React 모범 사례
컴포넌트 작성
- 최신 React hooks 사용 권장
- 컴포넌트는 단일 책임 원칙 준수
- Presentational/Container 컴포넌트 분리
- 성능 최적화: memo, useCallback, useMemo 적절히 사용
- 대용량 리스트는 가상화 라이브러리 사용 고려
상태 관리
- Zustand와 Tanstack Query를 일관되게 사용
- 상태 구조는 정규화된 형태로 관리
- 에러 처리: Error Boundary와 try-catch 또는 onError 콜백 활용
3. 스타일링
Styled Components
- Styled Components 일관되게 사용
- 스타일드 컴포넌트명은 의미있게 작성
- 동적 스타일링은 props나 CSS 변수 활용
- 테마 시스템 활용하여 글로벌 스타일 관리
- 재사용 가능한 스타일은 mixin이나 확장으로 관리
- CSS 포맷팅 가독성 유지
- 사용하지 않는 스타일이나 중복 스타일 제거
4. Vite 및 빌드 최적화
- 모듈 import 최적화 (tree-shaking 고려)
- 환경변수는 .env 파일로 관리
- vite.config.ts에서 빌드 성능 튜닝 (sourcemap 설정, 플러그인 최적화 등)
5. 아키텍처 및 개발 환경
폴더 구조
- Feature-Sliced Design (FSD) 구조 준수
- 레이어별 참조 규칙 엄격히 적용
타입스크립트
- strict 모드 사용
- 타입 명시적으로 작성 (any 사용 지양)
- path alias (@/_) 절대 경로 import 사용
Git 훅
- Husky 설정으로 pre-commit, commit-msg 린팅 확인
6. 기타 가이드라인
- 충분한 근거와 함께 리뷰 제공
- 정보 검증 후 답변
- 간결하고 명확한 응답
- 필요시 추가 컨텍스트 요청
- 검증되지 않은 주장 지양
- 가능한 경우 출처 명시
- 별도 언급 없으면 JavaScript 기준
- 한국어로 응답
- 대부분 브라우저에서 지원하는 ES6+ 기능 활용
- 코드 리뷰를 통한 유지보수성 향상에 적극 활용
Files:
src/pages/myPage/ui/components/UserProfile.tsx
🧬 Code Graph Analysis (1)
src/pages/myPage/ui/components/UserProfile.tsx (4)
src/entities/user/model/useAuth.ts (1)
useAuth(4-25)src/shared/ui/Profile.tsx (1)
ProfileUrl(8-8)src/shared/styles/mixins.ts (2)
flexColCenter(10-15)flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Chromatic
🔇 Additional comments (2)
src/pages/myPage/ui/components/UserProfile.tsx (2)
27-37: blob: URL 정리 로직 추가 굿 — 메모리 릭 방지 OK
updatedProfile.profileImg변경/언마운트 시URL.revokeObjectURL로 정리하는 패턴이 적절합니다. 의도대로 이전 blob URL만 정리하도록 조건 검사도 올바릅니다.
59-66: 대용량 파일 처리 UX 개선 확인파일 사이즈 초과 시 에러 상태 표시, input 초기화, 미리보기 복구(기존
userInfo.profileImg) 흐름이 자연스럽습니다. 이전 리뷰에서 지적된 "null로 리셋" 문제도 해소되었습니다.
maylh
left a comment
There was a problem hiding this comment.
진짜 너무너무너무너무너무 수고하셨습니다 (_ _) .. 😍😍😍😍😍😍
| import { Routes, Route } from 'react-router-dom' | ||
|
|
||
| import PrivateRoute from '@widgets/authGuard/PrivateRoute' | ||
|
|
||
| import MyPage from '@/pages/myPage/ui' | ||
|
|
||
| export const AppRoutes = () => { | ||
| return ( | ||
| <Routes> | ||
| <Route | ||
| path="/mypage/*" | ||
| element={ | ||
| <PrivateRoute> | ||
| <MyPage /> | ||
| </PrivateRoute> | ||
| } | ||
| /> | ||
| </Routes> | ||
| ) | ||
| } |
There was a problem hiding this comment.
lazy loading와 Suspense 적용하는 것 어떨까요 ?
There was a problem hiding this comment.
오! 생각 못해봤는데 좋네요😍 그럼 일단 로딩 스피너는 framer에 있는 예제 코드 임시로 넣어놓고 피그마에 요청 남겨둘게요!
| </> | ||
| )} | ||
| </ProfileImgContainer> | ||
| {isFileError && <FileErrMsg>5MB 이하의 파일만 업로드 가능해요</FileErrMsg>} |
There was a problem hiding this comment.
에러 타입별 메시지를 상수 객체로 분리하는 것 어떻게 생각하세요 ?
파일 에러, 링크 에러 등 다양한 케이스를 하나의 객체에서 관리할 수 있고,
더 넓게 보면 404나 네트워크 에러 메시지도 같은 방식으로 관리할 수 있을 것 같습니다 !
There was a problem hiding this comment.
좋아요! 나중에 API 응답 에러 값도 다 맞추면 편할 것 같아요ㅎ.ㅎ
폴더/파일 구조랑 위치 고민하는데 시간이 살짝 걸려서🥲 상수 객체 위치 추후에 정해지면 싹 교체하겠습니다 !
There was a problem hiding this comment.
네 알겠습니다 ! 퍼블리싱 끝나고 파일 구조 관련해서 의논해보아요 ..
| }, | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "dev": "vite --host", |
There was a problem hiding this comment.
이렇게 하면 모바일 기기에서도 확인 가능하나보네요 !!!
몰 랐 어 ....... 🥸
There was a problem hiding this comment.
대 박 편 함 이 슈 . . . 맥은 어떻게 하면 모바일에서 콘솔도 볼 수 있다던데 그것도 나중에 찾아봐야겠어요
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (8)
src/app/routes/routes.tsx (1)
17-17: fallback Loading의 width/height는 생략 권장 (max-width에 의해 무의미)Loading 컴포넌트는 내부에서 max-width를 강제하고 있어, width="100%"를 줘도 max-width(375px/420px)에 의해 실제로는 가로가 제한됩니다. 여기서는 오버레이 중앙 정렬만 필요하므로 width/height prop을 생략하는 편이 자연스럽습니다.
- <Suspense fallback={<Loading isLoading width="100%" height="100%" />}> + <Suspense fallback={<Loading isLoading />}>src/stories/Loading.stories.tsx (2)
24-26: Story args를 실제로 활용하도록 render 시그니처 개선 제안현재 argTypes에 isLoading을 노출하지만, render 내부에서 로컬 상태만 사용하여 컨트롤 패널이 무의미합니다. args를 초기값으로 받아 사용하도록 변경하면 컨트롤과 데모 버튼을 모두 활용할 수 있습니다.
-export const Default: Story = { - render: () => { - const [isLoading, setIsLoading] = useState(false) +export const Default: Story = { + args: { isLoading: false }, + render: (args) => { + const [isLoading, setIsLoading] = useState(Boolean(args.isLoading))
28-35: setTimeout 정리(cleanup) 추가로 메모리/타이밍 이슈 예방빠르게 토글될 때 이전 타이머가 남아있을 수 있습니다. clearTimeout을 추가해 부작용을 방지하세요.
- useEffect(() => { - if (isLoading) { - setTimeout(() => { - setIsLoading(false) - }, 3000) - } - }, [isLoading]) + useEffect(() => { + if (!isLoading) return + const id = window.setTimeout(() => { + setIsLoading(false) + }, 3000) + return () => window.clearTimeout(id) + }, [isLoading])src/shared/ui/Loading.tsx (3)
35-37: staggerChildren은 양수 사용 권장framer-motion에서 staggerChildren는 보통 양의 초 단위 값을 사용합니다. 현재 -0.2는 동작이 명확하지 않을 수 있으니 0.2 등 양수로 두고, 역순은 staggerDirection으로 제어하세요.
- transition={{ staggerChildren: -0.2, staggerDirection: -1 }} + transition={{ staggerChildren: 0.2, staggerDirection: -1 }}참고: 변경 후 애니메이션 타이밍이 의도와 다른지 UI에서 한 번 확인해 주세요.
34-41: 접근성(ARIA) 속성 추가 제안보조기기에서 로딩 상태를 인지할 수 있도록 role/aria 속성을 부여하는 것을 권장합니다.
<LoadingContainer animate="jump" transition={{ staggerChildren: -0.2, staggerDirection: -1 }} $width={width} $maxWidth={maxWidth} $height={height} + role="status" + aria-live="polite" + aria-busy={true} >
18-28: variants 객체 메모이제이션으로 불필요한 재생성 방지미세 최적화지만, 렌더마다 variants 객체가 재생성되지 않도록 useMemo를 사용할 수 있습니다.
- const dotVariants: Variants = { - jump: { - y: -30, - transition: { - duration: 0.8, - repeat: Infinity, - repeatType: 'mirror', - ease: 'easeInOut', - }, - }, - } + const dotVariants = useMemo<Variants>( + () => ({ + jump: { + y: -30, + transition: { + duration: 0.8, + repeat: Infinity, + repeatType: 'mirror', + ease: 'easeInOut', + }, + }, + }), + [] + )컴포넌트 상단에 다음 import 추가가 필요합니다:
import { useMemo } from 'react'src/shared/ui/BottomSheet.tsx (2)
30-35: 모달 접근성 보강 제안: 접근 가능한 이름(aria-label/labelledby)과 초기 포커스 이동
role="dialog"및aria-modal="true"는 적절하지만, 대화상자에 접근 가능한 이름이 없습니다. 또한 열릴 때 포커스가 모달 내부로 이동하지 않아 스크린 리더/키보드 사용자 경험이 저하될 수 있습니다. 선택적 props를 추가해 접근성을 보강하는 것을 제안합니다.아래와 같이 최소 변경으로 개선할 수 있습니다.
interface BottomSheetProps { isOpen: boolean onClose: () => void children: React.ReactNode height?: string + ariaLabel?: string // 접근 가능한 이름 제공(대체: ariaLabelledby 도입 가능) } -const BottomSheet = ({ - isOpen, - onClose, - children, - height = BOTTOM_SHEET_CONSTANTS.DEFAULT_HEIGHT, -}: BottomSheetProps) => { +const BottomSheet = ({ + isOpen, + onClose, + children, + height = BOTTOM_SHEET_CONSTANTS.DEFAULT_HEIGHT, + ariaLabel, +}: BottomSheetProps) => { const prevOverflowRef = useRef<string | null>(null) + const sheetRef = useRef<HTMLDivElement | null>(null) const deviceType = useDevice() useEffect(() => { if (isOpen) { // 현재 overflow 값 저장 prevOverflowRef.current = document.body.style.overflow document.addEventListener('keydown', handleEscape) document.body.style.overflow = 'hidden' + // 모달 열릴 때 포커스 이동 + // 다음 프레임에 포커스하여 애니메이션/DOM 반영 이후로 지연 + requestAnimationFrame(() => { + sheetRef.current?.focus() + }) } return () => { document.removeEventListener('keydown', handleEscape) // 기존 overflow 값으로 복원 document.body.style.overflow = prevOverflowRef.current || 'visible' } }, [isOpen, handleEscape]) <Overlay isOpen={isOpen} onClose={onClose} useAnimation childrenAlign="bottom"> <SheetContainer + ref={sheetRef} role="dialog" aria-modal="true" + aria-label={ariaLabel} tabIndex={-1} drag="y" dragConstraints={{ top: 0 }} // 위로는 드래그 불가 dragElastic={BOTTOM_SHEET_CONSTANTS.DRAG_ELASTIC} onDragEnd={handleDragEnd} variants={sheetVariants} initial="hidden" animate="visible" exit="exit" $deviceType={deviceType} $height={height} > <Handle type="button" aria-label="바텀시트 닫기" onClick={handleHandleClick} /> <Content>{children}</Content> </SheetContainer> </Overlay>보완 여지:
- 명시적 제목 요소를 제공할 수 있다면
aria-labelledby={id}접근을 권장합니다.- 포커스 트랩(모달 내부에서 탭 순환)을 추가하면 완성도가 더 높아집니다.
Also applies to: 37-45, 55-68, 112-129
139-141: 모바일 폭 상한 420px → 430px로 상향하여 MainLayout과 일치 제안PR 설명에 따르면 MainLayout max width가 430px로 증가했습니다. BottomSheet의 모바일 상한이 420px로 고정되어 있어 시각적 불일치가 발생할 수 있습니다. 상한을 430px로 맞추는 것을 권장합니다.
- width: ${({ $deviceType }) => - $deviceType === 'mobile' ? 'clamp(320px, 100dvw, 420px)' : '375px'}; + width: ${({ $deviceType }) => + $deviceType === 'mobile' ? 'clamp(320px, 100dvw, 430px)' : '375px'};변경 시 다른 화면(스토리북 포함)에서 레이아웃이 의도대로 보이는지 한 번만 확인 부탁드립니다.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/app/routes/routes.tsx(1 hunks)src/shared/ui/BottomSheet.tsx(1 hunks)src/shared/ui/Loading.tsx(1 hunks)src/stories/Loading.stories.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit Configuration File
**/*.{ts,tsx,js,jsx}: ## 1. 일반적인 코딩 컨벤션포맷팅
.prettierrc설정에 따라 포맷팅 확인- 들여쓰기: 2칸 스페이스
- 최대 줄 길이: 100자
- 세미콜론 사용 안함
- 따옴표: 작은따옴표 사용
- 괄호 안 공백: 있음
- 화살표 함수 괄호: 항상 사용
- 줄바꿈: LF 사용
네이밍 컨벤션
- 컴포넌트: PascalCase (예: UserProfile)
- 유틸리티/훅/변수: camelCase (예: getUserData, useUserInfo)
- 상수: UPPER_SNAKE_CASE (예: API_BASE_URL)
- 이미지 파일: kebab-case (예: user-profile-icon.png)
주석 사용
- 복잡한 로직에만 주석 추가
- 불필요한 주석 지양 (코드로 설명 가능한 것)
- TODO/FIXME 형식:
// TODO: 설명 - 작성자가독성
- 매직 넘버 지양, 의미있는 상수 사용
- 함수는 하나의 책임만 가지도록 작성 (최대 20줄 권장)
- 중첩 깊이 최소화 (3단계 이하 권장)
2. React 모범 사례
컴포넌트 작성
- 최신 React hooks 사용 권장
- 컴포넌트는 단일 책임 원칙 준수
- Presentational/Container 컴포넌트 분리
- 성능 최적화: memo, useCallback, useMemo 적절히 사용
- 대용량 리스트는 가상화 라이브러리 사용 고려
상태 관리
- Zustand와 Tanstack Query를 일관되게 사용
- 상태 구조는 정규화된 형태로 관리
- 에러 처리: Error Boundary와 try-catch 또는 onError 콜백 활용
3. 스타일링
Styled Components
- Styled Components 일관되게 사용
- 스타일드 컴포넌트명은 의미있게 작성
- 동적 스타일링은 props나 CSS 변수 활용
- 테마 시스템 활용하여 글로벌 스타일 관리
- 재사용 가능한 스타일은 mixin이나 확장으로 관리
- CSS 포맷팅 가독성 유지
- 사용하지 않는 스타일이나 중복 스타일 제거
4. Vite 및 빌드 최적화
- 모듈 import 최적화 (tree-shaking 고려)
- 환경변수는 .env 파일로 관리
- vite.config.ts에서 빌드 성능 튜닝 (sourcemap 설정, 플러그인 최적화 등)
5. 아키텍처 및 개발 환경
폴더 구조
- Feature-Sliced Design (FSD) 구조 준수
- 레이어별 참조 규칙 엄격히 적용
타입스크립트
- strict 모드 사용
- 타입 명시적으로 작성 (any 사용 지양)
- path alias (@/_) 절대 경로 import 사용
Git 훅
- Husky 설정으로 pre-commit, commit-msg 린팅 확인
6. 기타 가이드라인
- 충분한 근거와 함께 리뷰 제공
- 정보 검증 후 답변
- 간결하고 명확한 응답
- 필요시 추가 컨텍스트 요청
- 검증되지 않은 주장 지양
- 가능한 경우 출처 명시
- 별도 언급 없으면 JavaScript 기준
- 한국어로 응답
- 대부분 브라우저에서 지원하는 ES6+ 기능 활용
- 코드 리뷰를 통한 유지보수성 향상에 적극 활용
Files:
src/shared/ui/BottomSheet.tsxsrc/shared/ui/Loading.tsxsrc/stories/Loading.stories.tsxsrc/app/routes/routes.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-12T18:31:01.433Z
Learnt from: maylh
PR: dnd-side-project/dnd-13th-8-frontend#14
File: src/stories/NavBar.stories.ts:1-1
Timestamp: 2025-08-12T18:31:01.433Z
Learning: Storybook에서 Meta, StoryObj 타입은 'storybook/react'가 아닌 프레임워크별 패키지에서 import해야 함. React-Vite 프로젝트의 경우 'storybook/react-vite'에서 import하는 것이 공식 가이드에 따른 올바른 방식임.
Applied to files:
src/stories/Loading.stories.tsx
📚 Learning: 2025-08-16T02:25:22.238Z
Learnt from: hansololiviakim
PR: dnd-side-project/dnd-13th-8-frontend#27
File: src/stories/Input.stories.ts:1-1
Timestamp: 2025-08-16T02:25:22.238Z
Learning: dnd-13th-8-frontend 프로젝트에서는 Storybook 타입들을 'storybook/react-vite'에서 import하는 것이 올바른 방식임. package.json에 "storybook/react-vite": "^9.1.1"로 설치되어 있어서 'storybook/react-vite' 경로를 사용해야 함.
Applied to files:
src/stories/Loading.stories.tsx
🧬 Code Graph Analysis (1)
src/shared/ui/Loading.tsx (3)
src/shared/hooks/useDevice.ts (1)
useDevice(14-17)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)src/shared/styles/theme.ts (1)
theme(3-116)
🔇 Additional comments (5)
src/app/routes/routes.tsx (1)
8-21: lazy + Suspense + PrivateRoute 조합 훌륭합니다마이페이지 라우트를 lazy 로드하고, PrivateRoute로 가드한 뒤 Suspense fallback을 주는 구조가 명확하고 확장에도 유리합니다. 현재 요구사항에 잘 부합합니다.
src/stories/Loading.stories.tsx (1)
3-3: Storybook 타입 import 경로 올바릅니다'react-vite' 프레임워크 전용 패키지에서 Meta/StoryObj를 import한 점이 프로젝트 가이드와 일치합니다. 잘 하셨습니다.
src/shared/ui/Loading.tsx (1)
15-47: 로딩 오버레이 구성 깔끔합니다
- 디바이스 타입에 따른 maxWidth 처리
- 테마 컬러 적용 및 간결한 애니메이션
- Overlay 래핑으로 호출부 단순화
구현 방향과 API가 일관적입니다.
src/shared/ui/BottomSheet.tsx (2)
37-42: React.FC 제거 및 디스트럭처링 전환, 기본값 지정 모두 적절합니다
React.FC를 지양하고 명시적 props 타입을 부여한 함수 컴포넌트 형태로 전환한 점 좋습니다.height기본값도 상수로 통일되어 일관성이 있습니다.
44-45: SSR 안전성 확인 완료 — 추가 수정 불필요합니다
useDevice내부의getDeviceType()함수는 최상단에typeof navigator === 'undefined'검사를 수행하여, 서버 사이드 렌더링 시에도 안전하게 ‘pc’를 반환하도록 구현되어 있습니다.
- 렌더 단계에서 직접
window또는document에 접근하지 않으므로 런타임 오류 발생 가능성이 없습니다.이에 따라 별도의
useEffect이동이나 추가적인 SSR 가드는 필요치 않습니다.
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
src/shared/ui/Header.tsx (1)
32-47: css 믹스인 적용 버그: 문자열 리턴 내${theme.FONT.heading1}은 동작하지 않습니다
styled-components의css믹스인은 템플릿 리터럴 최상위 인터폴레이션에서만 정상적으로 병합됩니다. 현재처럼 삼항 연산자에서 문자열을 리턴하며 그 안에${theme.FONT.heading1}을 포함하면, 실제로는 CSS로 파싱되지 않고[object Object]가 되어 의도한 타이포그래피가 적용되지 않습니다.css헬퍼를 사용해 조건부 블록을 반환하도록 수정해 주세요.아래 diff는 해당 블록 교정 예시입니다:
- ${({ $position, theme }) => - $position === 'left' - ? ` - justify-content: flex-start; - ${theme.FONT.heading1}; - font-weight: 600; - color: ${theme.COLOR['common-white']}; - ` - : ` - justify-content: flex-end; - gap: 8px; - `} + ${({ $position, theme }) => + $position === 'left' + ? css` + justify-content: flex-start; + ${theme.FONT.heading1}; + font-weight: 600; + color: ${theme.COLOR['common-white']}; + ` + : css` + justify-content: flex-end; + gap: 8px; + `}추가로,
css헬퍼 임포트가 필요합니다(파일 상단):import styled, { css } from 'styled-components'
♻️ Duplicate comments (2)
src/App.tsx (2)
13-14: 매직 넘버 34 명확화 + Safe Area 폴백 의도 드러내기
34값의 의미가 코드만으로 드러나지 않습니다. 의미를 담은 상수명으로 교체해 가독성과 유지보수성을 높이는 것을 권장합니다. 또한 iOS safe area 대응이 폴백임을 나타내면 더 명확합니다. (기존 봇 코멘트와 동일 맥락)아래와 같이 상수명을 바꾸고 사용하는 곳을 함께 정리해주세요.
- const LAYOUT_BOTTOM_GAP = 34 + // iOS safe-area inset 미지원 환경에서 사용할 폴백(px) + const SAFE_AREA_BOTTOM_FALLBACK = 34 @@ - $layoutWidth={LAYOUT_WIDTH} - $layoutBottomGap={LAYOUT_BOTTOM_GAP} + $layoutWidth={LAYOUT_WIDTH} + $layoutBottomGap={SAFE_AREA_BOTTOM_FALLBACK} @@ - <NavContainer $layoutWidth={LAYOUT_WIDTH} $layoutBottomGap={LAYOUT_BOTTOM_GAP}> + <NavContainer $layoutWidth={LAYOUT_WIDTH} $layoutBottomGap={SAFE_AREA_BOTTOM_FALLBACK}>Also applies to: 19-24
55-61: NavContainer: 음수 마진 제거, max-width 패턴과 테마 기반 그라데이션으로 정리
margin: 0 -20px 0 0px;는 고정 포지션 요소에 예측 불가한 오버플로/정렬 이슈를 유발할 수 있습니다.- 메인 레이아웃과 동일하게
width: 100% + max-width패턴을 사용하면 더 단단합니다.- bottom padding에도 safe-area를 반영하면 장치 의존 이슈를 줄일 수 있습니다.
- 하드코딩된
rgb/rgba(15,16,20)대신 테마를 사용하세요. (기존 코멘트와 동일 맥락)- margin: 0 -20px 0 0px; ${flexRowCenter} - width: ${({ $layoutWidth }) => $layoutWidth}; - padding-bottom: ${({ $layoutBottomGap }) => $layoutBottomGap}px; + /* 메인 레이아웃과 정렬 일치: 음수 마진 제거, max-width 패턴 적용 */ + width: 100%; + max-width: ${({ $layoutWidth }) => $layoutWidth}; + padding: 0 20px; + padding-bottom: calc(${({ $layoutBottomGap }) => $layoutBottomGap}px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); - background: linear-gradient(to bottom, rgba(15, 16, 20, 0), rgba(15, 16, 20, 1), rgb(15, 16, 20)); + background: linear-gradient( + to bottom, + transparent, + ${({ theme }) => theme.COLOR['gray-900']} + );
🧹 Nitpick comments (4)
src/assets/icons/index.ts (1)
16-17: 내보내기 순서(Play/Plus) 사소한 정렬 니트픽다른 아이콘들과 함께 가독성을 위해 알파벳 순 정렬을 유지하면 스캔이 쉬워집니다. 강제 사항은 아니지만 팀 합의된 규칙이 있다면 아래처럼 정렬을 권장합니다.
적용 diff:
-export { default as Plus } from './icn_plus.svg?react' -export { default as Play } from './icn_play.svg?react' +export { default as Play } from './icn_play.svg?react' +export { default as Plus } from './icn_plus.svg?react'src/shared/ui/Header.tsx (1)
21-30: 헤더 가로 패딩 제거 후 안전 영역(safe-area) 및 탭 터치 영역 확인 권장
padding: 18px 0px변경은 의도대로 좌우 패딩을 제거합니다. 다만 다음을 한 번 확인해 주세요:
- iOS Safari(특히 가로 모드)에서 좌/우 safe-area로 인해 내용이 베젤까지 붙어 보이지는 않는지
- 가장자리 아이콘(뒤로 가기 등) 터치 영역이 너무 붙어 오작동하지 않는지
필요 시 아래처럼 보완을 고려할 수 있습니다.
- 부모 패딩 상쇄 로직은 유지하고, 안전 영역만 보완:
- 예:
padding-inline: max(env(safe-area-inset-left), 0px) max(env(safe-area-inset-right), 0px)src/App.tsx (2)
16-20: 사용되지 않는 transient prop 제거 제안: $deviceType
$deviceType를 스타일에서 사용하지 않으므로 전달/타이핑을 제거하면 소음이 줄고 타입 의존성이 낮아집니다. 향후 스타일 조건부 분기 계획이 없다면 정리 권장합니다.적용 예(이 범위 내 변경):
<MainLayout - $deviceType={deviceType} $layoutWidth={LAYOUT_WIDTH} $layoutBottomGap={SAFE_AREA_BOTTOM_FALLBACK} >아래 범위 밖 추가 변경(참고용):
// styled props 제네릭에서 $deviceType 제거 const MainLayout = styled.main<{ $layoutWidth: string $layoutBottomGap: number }>` /* ... */ `
38-41: iOS safe-area 반영 + 폭 계산 안정화(width→max-width 패턴)
- bottom padding에
env(safe-area-inset-bottom)을 포함해 장치 안전 영역을 반영하는 것이 안전합니다.- 글로벌 box-sizing이 border-box라도,
width: 100% + max-width패턴이 레이아웃 일관성과 오버플로 방지에 더 견고합니다. (과거 제안 보완)- padding: 0 20px ${({ $layoutBottomGap }) => NAV_HEIGHT + $layoutBottomGap}px 20px; - width: ${({ $layoutWidth }) => $layoutWidth}; + /* bottom padding에 iOS safe-area를 반영(미지원 브라우저 폴백 0px) */ + padding: 0 20px calc(${({ $layoutBottomGap }) => NAV_HEIGHT + $layoutBottomGap}px + env(safe-area-inset-bottom, 0px)) 20px; + width: 100%; + max-width: ${({ $layoutWidth }) => $layoutWidth};
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/App.tsx(2 hunks)src/assets/icons/index.ts(2 hunks)src/shared/ui/Header.tsx(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit Configuration File
**/*.{ts,tsx,js,jsx}: ## 1. 일반적인 코딩 컨벤션포맷팅
.prettierrc설정에 따라 포맷팅 확인- 들여쓰기: 2칸 스페이스
- 최대 줄 길이: 100자
- 세미콜론 사용 안함
- 따옴표: 작은따옴표 사용
- 괄호 안 공백: 있음
- 화살표 함수 괄호: 항상 사용
- 줄바꿈: LF 사용
네이밍 컨벤션
- 컴포넌트: PascalCase (예: UserProfile)
- 유틸리티/훅/변수: camelCase (예: getUserData, useUserInfo)
- 상수: UPPER_SNAKE_CASE (예: API_BASE_URL)
- 이미지 파일: kebab-case (예: user-profile-icon.png)
주석 사용
- 복잡한 로직에만 주석 추가
- 불필요한 주석 지양 (코드로 설명 가능한 것)
- TODO/FIXME 형식:
// TODO: 설명 - 작성자가독성
- 매직 넘버 지양, 의미있는 상수 사용
- 함수는 하나의 책임만 가지도록 작성 (최대 20줄 권장)
- 중첩 깊이 최소화 (3단계 이하 권장)
2. React 모범 사례
컴포넌트 작성
- 최신 React hooks 사용 권장
- 컴포넌트는 단일 책임 원칙 준수
- Presentational/Container 컴포넌트 분리
- 성능 최적화: memo, useCallback, useMemo 적절히 사용
- 대용량 리스트는 가상화 라이브러리 사용 고려
상태 관리
- Zustand와 Tanstack Query를 일관되게 사용
- 상태 구조는 정규화된 형태로 관리
- 에러 처리: Error Boundary와 try-catch 또는 onError 콜백 활용
3. 스타일링
Styled Components
- Styled Components 일관되게 사용
- 스타일드 컴포넌트명은 의미있게 작성
- 동적 스타일링은 props나 CSS 변수 활용
- 테마 시스템 활용하여 글로벌 스타일 관리
- 재사용 가능한 스타일은 mixin이나 확장으로 관리
- CSS 포맷팅 가독성 유지
- 사용하지 않는 스타일이나 중복 스타일 제거
4. Vite 및 빌드 최적화
- 모듈 import 최적화 (tree-shaking 고려)
- 환경변수는 .env 파일로 관리
- vite.config.ts에서 빌드 성능 튜닝 (sourcemap 설정, 플러그인 최적화 등)
5. 아키텍처 및 개발 환경
폴더 구조
- Feature-Sliced Design (FSD) 구조 준수
- 레이어별 참조 규칙 엄격히 적용
타입스크립트
- strict 모드 사용
- 타입 명시적으로 작성 (any 사용 지양)
- path alias (@/_) 절대 경로 import 사용
Git 훅
- Husky 설정으로 pre-commit, commit-msg 린팅 확인
6. 기타 가이드라인
- 충분한 근거와 함께 리뷰 제공
- 정보 검증 후 답변
- 간결하고 명확한 응답
- 필요시 추가 컨텍스트 요청
- 검증되지 않은 주장 지양
- 가능한 경우 출처 명시
- 별도 언급 없으면 JavaScript 기준
- 한국어로 응답
- 대부분 브라우저에서 지원하는 ES6+ 기능 활용
- 코드 리뷰를 통한 유지보수성 향상에 적극 활용
Files:
src/assets/icons/index.tssrc/shared/ui/Header.tsxsrc/App.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-09T22:48:22.427Z
Learnt from: hansololiviakim
PR: dnd-side-project/dnd-13th-8-frontend#10
File: src/shared/styles/GlobalStyle.ts:31-43
Timestamp: 2025-08-09T22:48:22.427Z
Learning: CSS responsive design: The clamp() function is preferred for setting responsive widths. It takes three parameters (minimum, preferred, maximum) and returns the middle value. For example, clamp(320px, 100vw, 420px) ensures the width is at least 320px, at most 420px, and dynamically adjusts between these values based on viewport width. This is cleaner and more maintainable than using separate width, min-width, and max-width properties.
Applied to files:
src/App.tsx
🧬 Code Graph Analysis (2)
src/shared/ui/Header.tsx (2)
src/shared/styles/theme.ts (1)
theme(3-116)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
src/App.tsx (5)
src/shared/hooks/useDevice.ts (1)
useDevice(14-17)src/app/routes/routes.tsx (1)
AppRoutes(10-25)src/shared/ui/NavBar.tsx (1)
NAV_HEIGHT(8-8)src/shared/styles/theme.ts (1)
theme(3-116)src/shared/styles/mixins.ts (1)
flexRowCenter(3-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run Chromatic
🔇 Additional comments (5)
src/assets/icons/index.ts (1)
1-1: 아이콘 SVG 및 배럴 import 검증 완료
- src/assets/icons/icn_camera.svg, icn_plus.svg 파일이 정상적으로 존재합니다.
- PlaylistGrid.tsx에서 Plus, UserProfile.tsx에서 Camera 아이콘을
@/assets/icons배럴로 import 하고 있습니다.- vite.config.ts 및 package.json에
vite-plugin-svgr설정이 올바르게 적용돼 있습니다.변경 사항 그대로 승인합니다. LGTM!
src/shared/ui/Header.tsx (1)
13-15: Transient prop 적용 굿:$position으로 DOM 속성 누수 방지
Side에$position을 사용하는 패턴으로 전환한 점 좋습니다. 기존position이 DOM에 전달되는 문제를 예방하고,Input과의 일관성도 맞습니다.src/App.tsx (3)
3-7: 라우팅/훅/컴포넌트 임포트 구성 적절FSD 경로 별칭 사용과 공용 UI/훅 의존성 구성이 일관적입니다. 불필요한 임포트도 없습니다.
12-12: clamp 사용으로 모바일 레이아웃 폭 제어 👍
clamp(320px, 100dvw, 430px)선택이 이전 논의(Responsive에 clamp 선호)와 일치합니다. 유지해주세요.
43-44: 테마 토큰 사용 좋습니다
gray-900,gray-10을 테마에서 참조하도록 바꾼 점이 디자인 일관성 측면에서 적절합니다.
🛰️ 관련 이슈
✨ 주요 변경 사항
1️⃣ router 세팅 및 Private Router 설정
2️⃣ Input 컴포넌트 value, defaultValue 속성 props 추가
3️⃣ MainLayout padding 지정에 따른 Header 컴포넌트 x축 padding 제거
4️⃣ Header, Input 컴포넌트 $ transient props 적용
5️⃣ package.json dev 스크립트에 --host 플래그 추가하여 같은 wifi 네트워크일 경우 모바일 디바이스에서 로컬 서버 접근 가능하도록 설정
6️⃣ iPhone Pro, Galaxy Ultra 디바이스 width 대응하여 MainLayout max width 430px으로 확대
7️⃣ theme에 common-error 색상 추가 (#ff5454)
8️⃣ useSingleSelect hook 추가 및 해당 훅 이용하여 ContentHeader 컴포넌트 리팩토링 (👉 스토리북에 사용 방법 예제 코드랑 같이 넣어뒀습니다!)
9️⃣ 전체 레이아웃에 NavBar 추가 및 nav background 영역 그라데이션 적용
🔟 마이페이지 > 마이페이지 > MY 앨범, MY 좋아요, 내 정보 관리 UI 구현
🔍 테스트 방법 / 체크리스트
🗯️ PR 포인트
pnpm dev하면 아래와 같이network 주소도 같이 뜰텐데요,pc랑 휴대폰 같은 wifi 사용할 경우 해당 주소로 접속하면 로컬 화면 모바일 디바이스로 실시간 테스트 가능합니다!
🚀 알게된 점
📖 참고 자료 (선택)
🖼️ Demo
ScreenRecording_08-16-2025-23-07-37_1.mp4
Summary by CodeRabbit
신기능
개선
작업