-
Notifications
You must be signed in to change notification settings - Fork 1
feat(client): 사이드바 구글 프로필 추가 #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough구글 및 사용자 프로필 조회 API와 React Query 훅을 추가하고, 사이드바에 프로필 버튼과 ProfilePopup을 통합했으며, 사이드바 네비게이션 훅에 라우터 기반 동기화 로직을 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Sidebar as Sidebar UI
participant Hooks as React Query Hooks
participant API as Client API
participant Server as Backend /api/v2/users
participant Popup as ProfilePopup
Sidebar->>Hooks: useGetGoogleProfile / useGetMyProfile 호출
Hooks->>API: getGoogleProfile() / getMyProfile() 실행
API->>Server: HTTP GET /api/v2/users/me(/google-profile)
Server-->>API: 200 { data: { ... } }
API-->>Hooks: 응답의 data.data 반환
Hooks-->>Sidebar: 프로필 데이터 제공(캐시 포함)
Sidebar->>Sidebar: 프로필 버튼 렌더 / 클릭으로 open 상태 토글
Sidebar->>Popup: props(profileImage, name, email, remindTime, open)
Popup->>Sidebar: 외부 클릭 시 onClose 호출 (닫기)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/client/src/shared/apis/queries.ts (1)
141-147:staleTime: Infinity설정 재검토 고려.사용자 프로필 데이터에
staleTime: Infinity를 설정하면 캐시된 데이터가 만료되지 않아, 사용자가 Google 프로필을 변경해도 애플리케이션에 즉시 반영되지 않습니다. 프로필 변경 빈도가 낮다면 현재 설정도 괜찮지만, 사용자 경험 개선을 위해 적절한 staleTime 값(예: 5분, 1시간 등) 설정을 고려해보세요.예시:
export const useGetGoogleProfile = () => { return useQuery({ queryKey: ['googleProfile'], queryFn: getGoogleProfile, - staleTime: Infinity, + staleTime: 1000 * 60 * 60, // 1시간 }); };apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
39-39: 타입 안전성 개선 필요.
googleProfileData?.googleProfile접근 시 타입 정의가 없어googleProfile필드의 존재 여부를 컴파일 타임에 확인할 수 없습니다. 옵셔널 체이닝(?.)으로 런타임 안전성은 확보했지만,apps/client/src/shared/apis/axios.ts의getGoogleProfile함수에 적절한 반환 타입을 추가하면 타입 안전성이 향상됩니다.
axios.ts에서 타입을 정의한 후:// @shared/types/api.ts에 추가 export interface GoogleProfileResponse { googleProfile: string; }이 파일에서 타입 단언 또는 타입 가드를 사용할 수 있습니다:
- const profileImageUrl = googleProfileData?.googleProfile || null; + const profileImageUrl = googleProfileData?.googleProfile ?? null;(참고:
||대신??를 사용하면 빈 문자열도 유효한 값으로 처리됩니다)
146-152: 접근성 개선: 대체 텍스트 구체화 고려.프로필 이미지의
alt속성이 "프로필 이미지"로 일반적입니다. 가능하다면 사용자 이름이나 이메일을 포함하여 더 구체적인 대체 텍스트를 제공하면 스크린 리더 사용자에게 더 나은 경험을 제공할 수 있습니다.예시 (사용자 정보를 가져올 수 있다면):
<img src={profileImageUrl} - alt="프로필 이미지" + alt={`${userName || '사용자'}의 프로필 이미지`} className="h-full w-full object-cover" />
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/shared/apis/axios.ts(1 hunks)apps/client/src/shared/apis/queries.ts(2 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
apps/client/src/shared/apis/queries.ts (1)
useGetGoogleProfile(141-147)packages/design-system/src/icons/components/icon.tsx (1)
Icon(71-114)
apps/client/src/shared/apis/queries.ts (1)
apps/client/src/shared/apis/axios.ts (1)
getGoogleProfile(75-78)
⏰ 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: storybook
🔇 Additional comments (1)
apps/client/src/shared/apis/axios.ts (1)
75-78: 반환 타입 어노테이션 추가 필요 및 GoogleProfileResponse 타입 정의 필수.
getGoogleProfile함수에 반환 타입 어노테이션이 누락되어 있습니다. 또한 이 함수를 사용하는useGetGoogleProfile훅도 타입 매개변수가 지정되지 않았습니다.다음 작업이 필요합니다:
apps/client/src/shared/types/api.ts에GoogleProfileResponse타입을 정의합니다.getGoogleProfile함수에Promise<GoogleProfileResponse>반환 타입을 추가합니다.useGetGoogleProfile훅을UseQueryResult<GoogleProfileResponse, AxiosError>타입으로 명시합니다.참고로 프로젝트 내 다른 유사한 함수들(
getDashboardCategories,getAcorns등)도 반환 타입이 정의되지 않았으므로, 일관성 있는 타입 안전성을 위해 함께 개선하는 것을 권장합니다.
| const { mutate: createCategory } = usePostCategory(); | ||
| const { data, isPending } = useGetArcons(); | ||
| const { mutate: deleteCategory } = useDeleteCategory(); | ||
| const { data: googleProfileData } = useGetGoogleProfile(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로필 로딩 및 에러 상태 처리 부재.
useGetGoogleProfile에서 isLoading과 isError 상태를 추출하지 않아, 프로필 로딩 중이거나 요청이 실패해도 사용자에게 피드백이 없습니다. 특히 하단의 MyLevelItem은 로딩 스켈레톤을 보여주는 반면(226-228줄), 프로필 영역은 일관성 있는 로딩 처리가 없습니다.
다음과 같이 수정하세요:
- const { data: googleProfileData } = useGetGoogleProfile();
+ const { data: googleProfileData, isLoading: isProfileLoading } = useGetGoogleProfile();그리고 UI에서 로딩 상태를 처리하세요:
<button
className="h-[3.6rem] w-[3.6rem] flex-shrink-0 overflow-hidden rounded-full border border-gray-200"
onClick={() => console.log('프로필 클릭', profileImageUrl)}
>
- {profileImageUrl ? (
+ {isProfileLoading ? (
+ <div className="h-full w-full animate-pulse bg-gray-200" />
+ ) : profileImageUrl ? (
<img
src={profileImageUrl}
alt="프로필 이미지"
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-gray-200" />
)}
</button>📝 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.
| const { data: googleProfileData } = useGetGoogleProfile(); | |
| const { data: googleProfileData, isLoading: isProfileLoading } = useGetGoogleProfile(); |
🤖 Prompt for AI Agents
apps/client/src/shared/components/sidebar/Sidebar.tsx around line 37:
useGetGoogleProfile call only destructures data and omits isLoading/isError, so
the profile section lacks loading/error handling; update the hook call to
extract { data: googleProfileData, isLoading: isGoogleProfileLoading, isError:
isGoogleProfileError } (or similar names), then in the JSX conditionally render
a loading skeleton when isGoogleProfileLoading, an error fallback/message when
isGoogleProfileError, and the normal profile UI when data is present; also
ensure MyLevelItem’s existing skeleton behavior remains consistent with the new
profile-loading state so the sidebar shows a unified loading state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 API pending관련 처리는 이후에 추가될까요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@constantly-dev, 좋은 질문입니다!
현재 PR에서 로딩 상태 처리를 추가하실 계획인가요, 아니면 추후 별도 PR에서 진행하실 예정인가요?
만약 이후에 추가하실 계획이시라면, 추적을 위해 별도 이슈를 생성해드릴까요? 이렇게 하면 향후 UX 개선 작업 시 놓치지 않고 처리하실 수 있을 것 같습니다. 😊
🧠 Learnings used
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
constantly-dev
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨어요~~ 사소한 코멘트만 확인해주세요!!
| export const useGetGoogleProfile = () => { | ||
| return useQuery({ | ||
| queryKey: ['googleProfile'], | ||
| queryFn: getGoogleProfile, | ||
| staleTime: Infinity, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 따로 바뀌는 경우가 없어서 staleTime을 Infinity로 지정하신거죠??
| <button | ||
| className="h-[3.6rem] w-[3.6rem] flex-shrink-0 overflow-hidden rounded-full border border-gray-200" | ||
| onClick={() => console.log('프로필 클릭', profileImageUrl)} | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
button 태그는 항상 명시적으로 type을 지정해주세요!
| const { mutate: createCategory } = usePostCategory(); | ||
| const { data, isPending } = useGetArcons(); | ||
| const { mutate: deleteCategory } = useDeleteCategory(); | ||
| const { data: googleProfileData } = useGetGoogleProfile(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 API pending관련 처리는 이후에 추가될까요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/client/src/shared/hooks/useSidebarNav.ts (1)
37-62: 콜백 함수 내 중복된 상태 업데이트를 제거하세요.각 콜백 함수(
goRemind,goBookmarks,selectCategory,goLevel)는 상태를 업데이트한 후navigate()를 호출하고 있습니다. 이후useEffect가 실행되어 동일한 상태를 다시 업데이트합니다. 이는 중복된 상태 업데이트로 인해 불필요한 리렌더링을 유발할 수 있습니다.URL을 단일 진실 공급원(single source of truth)으로 사용하는 패턴에서는 콜백이
navigate()만 호출하고,useEffect가 모든 상태 동기화를 담당하도록 리팩토링하는 것이 권장됩니다.다음 diff를 적용하여 중복을 제거하세요:
const goRemind = useCallback(() => { - setActiveTab('remind'); - setSelectedCategoryId(null); navigate('/'); }, [navigate]); const goBookmarks = useCallback(() => { - setActiveTab('mybookmark'); - setSelectedCategoryId(null); navigate('/my-bookmarks'); }, [navigate]); const selectCategory = useCallback( (id: number, name: string) => { - setActiveTab('mybookmark'); - setSelectedCategoryId(id); navigate(`/my-bookmarks?id=${id}&category=${name}`); }, [navigate] ); const goLevel = useCallback(() => { - setActiveTab('level'); - setSelectedCategoryId(null); navigate('/level'); }, [navigate]);이렇게 하면
useEffect가 URL 변경을 감지하여 상태를 일관되게 업데이트합니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/client/src/shared/hooks/useSidebarNav.ts(1 hunks)
⏰ 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: storybook
🔇 Additional comments (1)
apps/client/src/shared/hooks/useSidebarNav.ts (1)
1-2: URL 동기화를 위한 필수 import가 올바르게 추가되었습니다.useLocation과 useSearchParams를 통해 URL 상태를 추적하고, useEffect로 동기화하는 패턴은 브라우저 뒤로가기/앞으로가기를 올바르게 처리합니다.
| useEffect(() => { | ||
| const path = location.pathname; | ||
|
|
||
| if (path.startsWith('/my-bookmarks')) { | ||
| setActiveTab('mybookmark'); | ||
|
|
||
| const id = searchParams.get('id'); | ||
| if (id) { | ||
| setSelectedCategoryId(Number(id)); | ||
| } else { | ||
| setSelectedCategoryId(null); | ||
| } | ||
| } else if (path === '/' || path.startsWith('/remind')) { | ||
| setActiveTab('remind'); | ||
| setSelectedCategoryId(null); | ||
| } else if (path.startsWith('/level')) { | ||
| setActiveTab('level'); | ||
| setSelectedCategoryId(null); | ||
| } | ||
| }, [location.pathname, searchParams]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
URL 쿼리 파라미터 파싱 시 유효성 검증이 필요합니다.
Line 24에서 Number(id)는 id가 유효한 숫자 문자열이 아닐 경우 NaN을 반환합니다. 이는 selectedCategoryId의 타입이 number | null임에도 불구하고 NaN이 설정될 수 있어 하위 컴포넌트에서 예상치 못한 동작을 유발할 수 있습니다.
다음 diff를 적용하여 안전하게 파싱하세요:
const id = searchParams.get('id');
if (id) {
- setSelectedCategoryId(Number(id));
+ const numId = parseInt(id, 10);
+ setSelectedCategoryId(isNaN(numId) ? null : numId);
} else {
setSelectedCategoryId(null);
}🤖 Prompt for AI Agents
In apps/client/src/shared/hooks/useSidebarNav.ts around lines 16 to 35, the code
directly uses Number(id) (line 24) which can produce NaN if the query param is
not a valid numeric string; instead, validate and parse the id safely: check
that id is not null and matches a numeric pattern (or use parseInt and verify
Number.isFinite/!Number.isNaN and optionally Number.isInteger) before calling
setSelectedCategoryId; if validation fails, setSelectedCategoryId(null). Ensure
the branch logic only calls setSelectedCategoryId with a valid number or null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
142-156: 버튼 type 속성 올바르게 설정됨
type="button"을 명시적으로 지정하여 의도하지 않은 폼 제출을 방지하고 있습니다. 잘 처리되었습니다!선택적 개선 제안: 이미지 로딩 실패 처리
프로필 이미지 로드에 실패할 경우를 대비한 폴백 처리를 추가하면 더 안정적인 UX를 제공할 수 있습니다.
<img src={profileImageUrl} alt="프로필 이미지" className="h-full w-full object-cover" + onError={(e) => { + e.currentTarget.style.display = 'none'; + e.currentTarget.parentElement!.classList.add('bg-gray-200'); + }} />또는 상태 관리를 통한 처리:
const [imageError, setImageError] = useState(false); // 렌더링 {profileImageUrl && !imageError ? ( <img src={profileImageUrl} alt="프로필 이미지" className="h-full w-full object-cover" onError={() => setImageError(true)} /> ) : ( <div className="h-full w-full bg-gray-200" /> )}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx(3 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-07-15T20:00:13.756Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.756Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-07-11T20:47:15.055Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 43
File: apps/client/src/shared/components/ui/cards/BookmarkCard.tsx:0-0
Timestamp: 2025-07-11T20:47:15.055Z
Learning: React 컴포넌트에서 button 요소를 사용할 때는 항상 type 속성을 명시적으로 지정해야 합니다. 일반적인 클릭 버튼의 경우 type="button"을, 폼 제출 버튼의 경우 type="submit"을 명시해야 합니다. 이렇게 해야 의도하지 않은 폼 제출 동작을 방지할 수 있습니다.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-07-13T09:18:25.323Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 63
File: packages/design-system/src/components/commonBtn/CommonBtn.tsx:37-42
Timestamp: 2025-07-13T09:18:25.323Z
Learning: React 버튼 컴포넌트에서 submit, button 등 다양한 버튼 타입을 사용해야 하는 경우, buttonType prop을 추가하여 HTML button 요소의 type 속성을 설정할 수 있도록 해야 합니다. 기본값은 'button'으로 설정하여 의도하지 않은 폼 제출을 방지해야 합니다.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-08-19T17:18:57.678Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
🧬 Code graph analysis (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
apps/client/src/shared/apis/queries.ts (1)
useGetGoogleProfile(141-147)
⏰ 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: storybook
🔇 Additional comments (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (1)
19-19: LGTM - 임포트 추가 확인새로운 프로필 조회 훅을 정확하게 임포트했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (4)
apps/client/src/shared/components/profilePopup/ProfilePopup.tsx (2)
24-33: 키보드 접근성 지원 추가를 권장합니다.현재 마우스 클릭으로만 팝업을 닫을 수 있습니다. ESC 키 지원을 추가하면 사용자 경험이 개선됩니다.
🔎 ESC 키 핸들러 추가 예시
useEffect(() => { function handleClickOutside(e: MouseEvent) { if (popupRef.current && !popupRef.current.contains(e.target as Node)) { onClose(); } } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + onClose(); + } + } if (open) document.addEventListener('mousedown', handleClickOutside); + if (open) document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }; }, [open, onClose]);
38-70: 접근성 및 UX 개선 제안.팝업이 모달 역할을 하지만 다음 요소들이 누락되어 있습니다:
- 배경 오버레이(backdrop) 스타일링
- ARIA 속성 (
role="dialog",aria-modal="true",aria-labelledby)- 포커스 트랩 (팝업 열릴 때 내부로 포커스 이동)
🔎 개선 예시
return ( - <div className="fixed inset-0 z-[2000] flex items-start justify-start pl-[19rem] pt-[7rem]"> + <div + className="fixed inset-0 z-[2000] flex items-start justify-start pl-[19rem] pt-[7rem] bg-black/20" + role="dialog" + aria-modal="true" + aria-labelledby="profile-title" + > <div ref={popupRef} className="common-shadow flex w-[26rem] flex-col items-center rounded-[1.2rem] bg-white pb-[2.4rem] pt-[3.2rem]" > {/* ... */} - <p className="sub1-sb">{name}</p> + <p id="profile-title" className="sub1-sb">{name}</p>apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
40-48: 프로필 로딩 상태 처리를 권장합니다.이전 리뷰에서 논의된 사항이지만, 여전히
isLoading과isError상태가 처리되지 않고 있습니다. 프로필 데이터를 가져오는 동안 사용자에게 시각적 피드백이 없으며, 요청이 실패해도 알림이 없습니다.Based on learnings, 이전에 이 사항이 논의되었고 추후 작업 가능성이 언급되었습니다. 별도 이슈로 추적하시겠습니까?
🔎 로딩 상태 처리 예시
- const { data: googleProfileData } = useGetGoogleProfile(); - const { data: myProfile } = useGetMyProfile(); + const { data: googleProfileData, isLoading: isGoogleProfileLoading } = useGetGoogleProfile(); + const { data: myProfile, isLoading: isMyProfileLoading } = useGetMyProfile(); + + const isProfileLoading = isGoogleProfileLoading || isMyProfileLoading;그리고 UI에서 로딩 상태를 반영:
<button type="button" className="h-[3.6rem] w-[3.6rem] flex-shrink-0 overflow-hidden rounded-full border border-gray-200" onClick={() => setProfileOpen(true)} > - {profileImageUrl ? ( + {isProfileLoading ? ( + <div className="h-full w-full animate-pulse bg-gray-200" /> + ) : profileImageUrl ? ( <img src={profileImageUrl} alt="프로필" className="h-full w-full object-cover" /> ) : ( <div className="h-full w-full bg-gray-200" /> )} </button>
43-48: 변수명 일관성 개선 제안.Line 45의
chippiImageUrl변수명이 소스(myProfile?.profileImage)나 용도(프로필 이미지)와 일치하지 않아 코드를 읽을 때 혼란을 줄 수 있습니다.🔎 제안하는 변경
- const chippiImageUrl = myProfile?.profileImage ?? null; + const myProfileImageUrl = myProfile?.profileImage ?? null;그리고 사용처 업데이트:
<ProfilePopup open={profileOpen} onClose={() => setProfileOpen(false)} - profileImage={chippiImageUrl} + profileImage={myProfileImageUrl} name={profileName} email={profileEmail} remindTime={remindAt} />
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/client/src/shared/apis/axios.ts(1 hunks)apps/client/src/shared/apis/queries.ts(2 hunks)apps/client/src/shared/components/profilePopup/ProfilePopup.tsx(1 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/shared/apis/queries.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-07-15T20:00:13.756Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.756Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Applied to files:
apps/client/src/shared/components/profilePopup/ProfilePopup.tsxapps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-07-11T20:47:15.055Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 43
File: apps/client/src/shared/components/ui/cards/BookmarkCard.tsx:0-0
Timestamp: 2025-07-11T20:47:15.055Z
Learning: React 컴포넌트에서 button 요소를 사용할 때는 항상 type 속성을 명시적으로 지정해야 합니다. 일반적인 클릭 버튼의 경우 type="button"을, 폼 제출 버튼의 경우 type="submit"을 명시해야 합니다. 이렇게 해야 의도하지 않은 폼 제출 동작을 방지할 수 있습니다.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-07-13T09:18:25.323Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 63
File: packages/design-system/src/components/commonBtn/CommonBtn.tsx:37-42
Timestamp: 2025-07-13T09:18:25.323Z
Learning: React 버튼 컴포넌트에서 submit, button 등 다양한 버튼 타입을 사용해야 하는 경우, buttonType prop을 추가하여 HTML button 요소의 type 속성을 설정할 수 있도록 해야 합니다. 기본값은 'button'으로 설정하여 의도하지 않은 폼 제출을 방지해야 합니다.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-08-19T17:18:57.678Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-09-03T08:57:48.626Z
Learnt from: jjangminii
Repo: Pinback-Team/pinback-client PR: 52
File: apps/client/src/shared/components/sidebar/SideItem.tsx:57-60
Timestamp: 2025-09-03T08:57:48.626Z
Learning: SideItem 컴포넌트에서 아이콘 디자인이 일반적인 기대와 반대여서, 회전 로직도 반대로 구현되었습니다. 이를 명확히 하기 위해 prop 이름을 open에서 close로 변경하여 실제 동작과 일치시켰습니다.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/client/src/shared/components/sidebar/Sidebar.tsx
🧬 Code graph analysis (1)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
apps/client/src/shared/apis/queries.ts (2)
useGetGoogleProfile(142-148)useGetMyProfile(150-155)apps/client/src/shared/components/profilePopup/ProfilePopup.tsx (1)
ProfilePopup(14-72)
⏰ 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: storybook
🔇 Additional comments (3)
apps/client/src/shared/apis/axios.ts (1)
75-83: LGTM! API 함수 구현이 적절합니다.두 프로필 조회 함수 모두 기존 코드 패턴을 따르고 있으며, 엔드포인트 구조도 RESTful 규칙에 부합합니다.
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
146-161: LGTM! 프로필 버튼 구현이 완료되었습니다.이전 리뷰에서 지적된 플레이스홀더 코드가 제거되고, 프로필 팝업 토글 기능이 올바르게 구현되었습니다.
type="button"속성도 명시되어 있어 의도하지 않은 폼 제출을 방지합니다.Based on learnings, 버튼 타입 명시 규칙을 잘 따르고 있습니다.
250-257: LGTM! ProfilePopup 통합이 적절합니다.프로필 팝업이 올바르게 렌더링되고 필요한 모든 props가 전달되고 있습니다. 상태 관리와 이벤트 핸들러도 정확하게 구현되어 있습니다.
| if (!open) return null; | ||
|
|
||
| return ( | ||
| <div className="fixed inset-0 z-[2000] flex items-start justify-start pl-[19rem] pt-[7rem]"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하드코딩된 위치 값으로 인한 유지보수성 저하.
pl-[19rem] pt-[7rem]은 사이드바 레이아웃에 강하게 결합되어 있습니다. 사이드바 너비나 헤더 높이가 변경되면 팝업 위치가 어긋나게 됩니다.
🔎 개선 방안
다음 중 하나를 선택하여 개선하세요:
방안 1: CSS 변수 사용
-<div className="fixed inset-0 z-[2000] flex items-start justify-start pl-[19rem] pt-[7rem]">
+<div className="fixed inset-0 z-[2000] flex items-start justify-start pl-[var(--sidebar-width)] pt-[var(--header-height)]">방안 2: 동적 위치 계산
버튼 요소의 위치를 기준으로 동적으로 계산하는 앵커 포지셔닝 사용 (기존 useAnchoredMenu 훅 참고)
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/client/src/shared/components/profilePopup/ProfilePopup.tsx around line
38, the popup is positioned using hardcoded padding classes `pl-[19rem]
pt-[7rem]`, which couples it to the sidebar/header layout; replace this with a
maintainable approach by either (a) using CSS custom properties (e.g.,
--sidebar-width and --header-height) and applying padding via utility classes or
inline styles that reference those variables so layout changes update position
automatically, or (b) switching to dynamic anchor positioning: compute the popup
location from the trigger/button element (reuse the existing useAnchoredMenu
hook pattern) to place the popup relative to the anchor instead of fixed rem
offsets; update classnames/styles accordingly and remove the hardcoded padding
values.
| <div className="w-full px-[7.6rem]"> | ||
| <Button variant="secondary" size="small"> | ||
| 로그아웃 | ||
| </Button> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그아웃 버튼 기능이 구현되지 않았습니다.
버튼이 렌더링되지만 onClick 핸들러가 없어 클릭해도 아무 동작도 하지 않습니다. 사용자가 로그아웃을 시도할 수 없습니다.
로그아웃 기능 구현을 도와드릴까요? Firebase 인증 로그아웃 로직과 라우팅 처리가 필요합니다.
🔎 임시 해결책
기능 구현 전까지 버튼을 비활성화하거나 제거하는 것을 권장합니다:
- <div className="w-full px-[7.6rem]">
- <Button variant="secondary" size="small">
- 로그아웃
- </Button>
- </div>
+ {/* TODO: 로그아웃 기능 구현 필요 */}📝 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.
| <div className="w-full px-[7.6rem]"> | |
| <Button variant="secondary" size="small"> | |
| 로그아웃 | |
| </Button> | |
| </div> | |
| <div className="w-full px-[7.6rem]"> | |
| <Button | |
| variant="secondary" | |
| size="small" | |
| onClick={handleLogout} | |
| > | |
| 로그아웃 | |
| </Button> | |
| </div> |
🤖 Prompt for AI Agents
In apps/client/src/shared/components/profilePopup/ProfilePopup.tsx around lines
64 to 68, the Logout button is rendered without an onClick handler so it
performs no action; implement an onClick that calls Firebase Auth signOut,
awaits completion, handles errors (log/show toast), and then navigates the user
to the login (or landing) route; while signing out, disable the button (or show
loading) to prevent duplicate clicks and clear any client-side user state
(context/store) before/after navigation as appropriate; ensure necessary imports
for Firebase auth and the router (or use existing auth/logout utilities) are
added and any promise rejections are caught and reported.
jllee000
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
작업 속도가 매우 빠르네여,, 소통 이슈로 같은 구간 작업이 겹치긴 헀지만, 훨씬 깔끔하고 꼼꼼하게 짜주신 것 같아요!! 제가 UI상 확인은 못했는데ㅠㅠ시간 포맷팅(19:00->오후 07:00)만 확인해주세요!
| onClose: () => void; | ||
| profileImage: string | null; | ||
| email: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
프로필 이미지가 null일 경우에는 이미지가 비어있게 되나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기본 동그라미로 대체됩니다-!
| const { data: myProfile } = useGetMyProfile(); | ||
|
|
||
| const profileImageUrl = googleProfileData?.googleProfile || null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사이드바 프로필 클릭할때, 그때 유저정보 불러오는게 아니라,
사이드바 전체 fetch할때 한번에 팝업 내용도 받아오는 구조인거죠? 좋네용
| const chippiImageUrl = myProfile?.profileImage ?? null; | ||
| const profileEmail = myProfile?.email ?? ''; | ||
| const profileName = myProfile?.name ?? ''; | ||
| const remindAt = myProfile?.remindAt ?? 'AM 09:00'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제꺼 알람시간 설정 변동이 어려워서, 다른 시간대 (PM 09:00? 등의 경우)는 UI 확인 못하였는데ㅠㅠ
서버에서 remindAt을
"remindAt": "9:00" 그냥 이렇게 주는 걸루 아는데, 오전/오후 포맷팅까지 적용된걸까용!
(아마 시간포맷팅 로직: 패키지/ds에 공통함수로 쓸 수 있도록 빼두긴했슴!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
시간 포맷팅은 확인하지 못했네요.. 수정하겠습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/shared/components/profilePopup/ProfilePopup.tsx(1 hunks)apps/client/src/shared/utils/formatRemindTime.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/shared/components/profilePopup/ProfilePopup.tsx
⏰ 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: storybook
| function formatRemindTime(time: string | undefined): string { | ||
| if (!time) return ''; | ||
|
|
||
| const [period, timePart] = time.split(' '); | ||
| const [hourStr, minute] = timePart.split(':'); | ||
|
|
||
| let hour = Number(hourStr); | ||
|
|
||
| if (isNaN(hour)) return ''; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력 형식 검증 및 에러 처리가 누락되었습니다.
다음과 같은 문제가 있습니다:
- 런타임 에러 위험: Line 4에서
split(' ')의 결과가 2개 미만의 요소를 가질 경우,timePart이undefined가 되어 Line 5에서 런타임 에러가 발생합니다. - 검증 부족:
period가 "AM" 또는 "PM"인지 검증하지 않습니다. - 불완전한 검증:
minute이undefined인 경우를 처리하지 않아, Line 19에서 "PM 1:undefined" 같은 잘못된 출력이 발생할 수 있습니다.
🔎 제안하는 수정 사항
function formatRemindTime(time: string | undefined): string {
if (!time) return '';
const [period, timePart] = time.split(' ');
+
+ // 입력 형식 검증
+ if (!period || !timePart || (period !== 'AM' && period !== 'PM')) {
+ return '';
+ }
+
const [hourStr, minute] = timePart.split(':');
+
+ if (!hourStr || !minute) {
+ return '';
+ }
let hour = Number(hourStr);
if (isNaN(hour)) return '';📝 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.
| function formatRemindTime(time: string | undefined): string { | |
| if (!time) return ''; | |
| const [period, timePart] = time.split(' '); | |
| const [hourStr, minute] = timePart.split(':'); | |
| let hour = Number(hourStr); | |
| if (isNaN(hour)) return ''; | |
| function formatRemindTime(time: string | undefined): string { | |
| if (!time) return ''; | |
| const [period, timePart] = time.split(' '); | |
| // 입력 형식 검증 | |
| if (!period || !timePart || (period !== 'AM' && period !== 'PM')) { | |
| return ''; | |
| } | |
| const [hourStr, minute] = timePart.split(':'); | |
| if (!hourStr || !minute) { | |
| return ''; | |
| } | |
| let hour = Number(hourStr); | |
| if (isNaN(hour)) return ''; |
🤖 Prompt for AI Agents
In apps/client/src/shared/utils/formatRemindTime.ts around lines 1 to 10, the
function assumes time.split(' ') returns two parts and that timePart.split(':')
returns hour and minute and that period is "AM"/"PM"; add input validation and
early returns: verify time is a non-empty string, split by whitespace and ensure
you have exactly two parts (period and timePart), trim and uppercase period and
validate it is "AM" or "PM", ensure timePart contains ':' and that both hour and
minute exist and are numeric (return '' on any validation failure), parse hour
safely and adjust for AM/PM rules, and finally format the minute with
zero-padding before returning.
| if (period === 'PM') { | ||
| hour = hour === 12 ? 12 : hour - 12; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PM 시간 변환 로직에 치명적인 버그가 있습니다.
현재 로직 hour = hour === 12 ? 12 : hour - 12은 다음과 같은 문제를 발생시킵니다:
- 입력이 "PM 1:00" (12시간 형식)인 경우:
hour = 1 - 12 = -11→ 출력 "PM -11:00" ❌ - 입력이 "PM 2:00" (12시간 형식)인 경우:
hour = 2 - 12 = -10→ 출력 "PM -10:00" ❌
이 로직은 입력이 "PM 13:00", "PM 14:00" 같은 혼합 형식(AM/PM 접두사 + 24시간 형식 시)일 때만 올바르게 작동합니다. 그러나 이는 매우 비표준적인 형식입니다.
올바른 변환 로직:
- 24시간 → 12시간 변환:
hour = hour > 12 ? hour - 12 : hour - 12시간 형식이 이미 입력이라면 변환이 필요 없음
실제 입력 데이터의 형식을 확인하기 위해 다음 스크립트를 실행해주세요:
#!/bin/bash
# Description: ProfilePopup에서 remindTime이 어떤 형식으로 전달되는지 확인
# remindTime 사용처 검색
rg -n -C5 'remindTime' --type=tsx --type=ts🤖 Prompt for AI Agents
In apps/client/src/shared/utils/formatRemindTime.ts around lines 11-13, the PM
conversion uses hour = hour === 12 ? 12 : hour - 12 which produces negative
hours for standard 12-hour inputs; change the logic to only convert when the
input is in 24-hour form (i.e., when hour > 12) by using hour = hour > 12 ? hour
- 12 : hour for PM handling, and ensure that if input appears to already be
12-hour (1–12) you leave it unchanged; also add a quick runtime check or comment
to document expected input format and run the provided ripgrep command to verify
remindTime formats across the codebase before committing.
| if (period === 'AM' && hour === 0) { | ||
| hour = 12; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
AM 시간 처리 로직이 혼란스러운 입력 형식을 가정하고 있습니다.
Line 15-17의 로직은 hour === 0을 체크하여 12로 변환하는데, 이는 "AM 00:30" 같은 입력(자정을 24시간 형식 + AM 접두사로 표현)을 가정합니다.
문제점:
- 표준 12시간 형식에서는 시간이 0이 될 수 없습니다 (1-12 범위)
- 표준 24시간 형식에서는 AM/PM 접두사가 없습니다
- 현재 함수는 비표준 혼합 형식을 가정하고 있어 유지보수와 이해가 어렵습니다
권장사항:
입력 형식을 명확히 정의하고, 다음 중 하나를 선택하세요:
- 표준 12시간 형식 ("1:00 PM", "12:00 AM") → 파싱만 하고 변환 불필요
- 24시간 형식 ("13:00", "00:00") → 12시간 형식으로 변환하고 AM/PM 추가
🔎 24시간 → 12시간 변환을 위한 올바른 구현 예시
function formatRemindTime(time: string | undefined): string {
if (!time) return '';
// 24시간 형식 "HH:MM" 파싱
const [hourStr, minute] = time.split(':');
if (!hourStr || !minute) return '';
let hour = Number(hourStr);
if (isNaN(hour) || hour < 0 || hour > 23) return '';
// 24시간 → 12시간 변환
const period = hour >= 12 ? 'PM' : 'AM';
const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
return `${period} ${hour12}:${minute}`;
}🤖 Prompt for AI Agents
In apps/client/src/shared/utils/formatRemindTime.ts around lines 15-17, the
current check converting hour 0 to 12 assumes a nonstandard mixed 24h+AM format;
instead choose and enforce one input format and implement proper parsing: if you
accept 24-hour inputs, parse "HH:MM", validate hour 0-23 and minute 00-59,
compute period = hour >= 12 ? 'PM' : 'AM', compute hour12 = hour === 0 ? 12 :
hour > 12 ? hour - 12 : hour, and return `${period} ${hour12}:${minute}`; if you
accept 12-hour inputs, validate the hour is 1-12 with an AM/PM suffix and simply
normalize/return without converting; also add input validation, clear typings,
and update tests or callers to match the chosen format.
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
로그아웃은 따로 api 연동이 아닌 로컬스토리지에 있는 이메일과 토큰을 지우는 방식으로 했습니다
현재 저희 로직으로는 토큰만 지우면 이메일 가지고 토큰을 재발급 받기에 이메일도 같이 지우도록 했습니다.
📷 Screenshot
Summary by CodeRabbit
새로운 기능
개선
✏️ Tip: You can customize this high-level summary in your review settings.