-
Notifications
You must be signed in to change notification settings - Fork 0
회원 상세 조회 페이지 구현 ( #issue feat/#340 ) #344
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
Walkthrough관리자 페이지에서 회원 상세 정보를 보여주는 새로운 컴포넌트와 스타일 파일, 관련 훅이 추가되었습니다. 회원 상세 조회를 위한 라우트 및 데이터 구조가 확장되었으며, 회원 상세 조회 화면에서 사용자 정보를 불러오고 관리할 수 있도록 구현되었습니다. 기존 회원 목록, 카드, 사이드바 등도 일부 관리 기능에 맞춰 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant AdminUserDetail as AdminUserDetail 컴포넌트
participant useGetUserInfo as useGetUserInfo 훅
participant API as getUserInfo API
participant Sidebar as Sidebar
participant Outlet as Outlet (Nested Route)
AdminUserDetail->>useGetUserInfo: 사용자 ID로 정보 요청
useGetUserInfo->>API: getUserInfo(id)
API-->>useGetUserInfo: 사용자 데이터 반환
useGetUserInfo-->>AdminUserDetail: userData, isLoading 등 반환
AdminUserDetail->>Sidebar: 사용자 정보 및 탭 렌더링
AdminUserDetail->>Outlet: userData context로 하위 라우트 렌더링
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Suggested labels
Suggested reviewers
Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
npm error Exit handler never called! 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms (1)
✨ Finishing Touches
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. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🧹 Nitpick comments (23)
src/constants/admin/mainItems.ts (1)
17-20: key 값 및 템플릿 문자열을 간결하게 정리하세요
key: 'allUsers'는 기존 라우트 명세와 달라 가독성이 떨어집니다.users등으로 통일하면 유지보수성이 좋아집니다.link: `${ADMIN_ROUTE.users}`는 템플릿 리터럴이 불필요합니다. 바로ADMIN_ROUTE.users로 충분합니다.- key: 'allUsers', - title: '회원 조회', - link: `${ADMIN_ROUTE.users}`, + key: 'users', + title: '회원 조회', + link: ADMIN_ROUTE.users,src/models/auth.ts (1)
65-69:AllUserInfo인터페이스가 중복 필드를 선언합니다
AllUser에 이미warning,createdAt이 포함돼 있어 불필요한 재-선언입니다. 유지보수 시 타입 정의가 두 군데로 갈라지면 불일치 위험이 높아집니다.-export interface AllUserInfo extends AllUser { - email: string; - warning: number; - createdAt: string; -} +export type AllUserInfo = AllUser; // 필요 시 별칭으로 충분src/constants/routes.ts (1)
50-56: 유사 라우트 상수가 난립하고 있습니다
appliedProject,applyingProject(복수),joinedProject(단수) 등 패턴이 일관되지 않아 혼동을 줍니다.
- 네이밍 규칙(동사 시제/복수형)과 실제 path(
apply-projectvsapplied-projects)가 매칭되는지 재점검 필요- 중복/오타 여부를 전역 검색으로 검증해 주세요.
src/models/userInfo.ts (1)
27-31:UserInfoAll의 중복 선언 제거 권고
warning,createdAt은 이미 상위 인터페이스에서 필수입니다. 타입 중복은 유지보수 비용만 늘립니다.type UserInfoAll = UserInfo;또는 제네릭 활용을 고려하세요.src/hooks/admin/useGetUserInfo.ts (1)
17-18: 리턴 네이밍 통일성
userData보다는 API 명세와 동일하게data또는userInfo등이 일관성 있습니다. 작은 부분이지만 훅들의 사용성을 높여줍니다.src/components/user/mypage/activityLog/ActivityLog.tsx (2)
9-11: admin 여부 판별은 상수화된 경로·정확한 매칭으로 교체 권장
pathname.includes('/admin')문자열 검색은
/administer,/myadmin같은 예상치 못한 경로에서도true가 나올 수 있어 오검 가능성이 있습니다.
관리자 영역을 식별하는 고유 prefix (예:ROUTES.adminRoot)나pathname.startsWith()/ 정규식으로 정확히 매칭하도록 리팩터링하면 추후 라우트 구조 변경 시 안전합니다.-const isAdmin = pathname.includes('/admin'); +const isAdmin = pathname.startsWith(ROUTES.adminRoot);
13-16: 삼항 연산자 사용 👍 – 추가로useMemo로 불필요 렌더 방지 가능현재 구현은 문제 없으나,
filter배열은 변하지 않는 상수이므로 아래처럼useMemo로 래핑하면 렌더 시 객체 비교가 불필요해집니다.-<ContentTab - $justifyContent='space-around' - filter={isAdmin ? ACTIVITY_FILTER_ADMIN : ACTIVITY_FILTER} -/> +const filter = useMemo( + () => (isAdmin ? ACTIVITY_FILTER_ADMIN : ACTIVITY_FILTER), + [isAdmin], +); + +<ContentTab $justifyContent='space-around' filter={filter} />src/pages/admin/adminUser/AdminUser.styled.ts (2)
6-7:Spinner스타일 확장만으로는 시멘틱 차별화 부족
styled(SpinnerContainer)``;로 클래스만 복사되었는데,
관리자 페이지 전용 크기·정렬이 필요하다면 이곳에서 명시적으로 정의해 두는 편이 유지보수에 도움이 됩니다.
예) margin, height 등을 지정하거나role="status"보강.-export const Spinner = styled(SpinnerContainer)``; +export const Spinner = styled(SpinnerContainer)` + margin-top: 4rem; + width: 100%; + justify-content: center; +`;
8-10: 고정 px 보다 rem 사용을 고려
margin-top: 120px;→7.5rem처럼 상대 단위를 사용하면 다양한 DPI 환경에서 일관된 간격을 제공할 수 있습니다.src/components/common/sidebar/Sidebar.styled.ts (1)
52-68: Admin/사용자 메뉴 배경색 분기 – 테마 변수로 묶어 중복 제거삼항 중첩으로 가독성이 다소 떨어집니다.
const bgActive = $isAdmin ? theme.color.white : theme.color.lightgrey;같이 변수로 분리하거나, theme 에sidebar.bg.active.admin / user를 정의하면 스타일 일관성과 유지보수가 쉬워집니다.src/components/user/mypage/myProfile/profile/Profile.tsx (4)
18-22: 타입 정의를 컴포넌트 외부로 분리
useOutletContext의 제네릭 타입을 컴포넌트 내부에서 직접 객체 리터럴로 작성하면 재사용성이 떨어집니다. 별도 타입 별칭을 선언해 두면 다른 하위 컴포넌트에서도 동일 타입을 쉽게 import 할 수 있습니다.
24-25: 불필요한 삼항 연산자 제거
includes(...)는 이미 boolean 을 반환합니다. 아래처럼 간결하게 표현할 수 있습니다.-const myPage = location.pathname.includes('mypage') ? true : false; -const admin = location.pathname.includes('admin') ? true : false; +const myPage = location.pathname.includes('mypage'); +const admin = location.pathname.includes('admin');🧰 Tools
🪛 Biome (1.9.4)
[error] 24-24: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
[error] 25-25: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
39-44: 옵셔널 체이닝으로 가독성 향상두 단계의 if 문 대신 옵셔널 체이닝을 사용하면 의도가 더 명확합니다.
-useEffect(() => { - if (scrollRef) { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } - } -}, [scrollRef]); +useEffect(() => { + scrollRef?.current && (scrollRef.current.scrollTop = 0); +}, [scrollRef]);
60-83: 관리자 전용 정보 렌더링 중 코드 중복
<MyProfileWrapper>블록이 세 번 반복됩니다. label 과 value 만 바뀌므로 컴포넌트로 추출하거나 map 순회로 줄이면 유지보수가 쉬워집니다.src/components/admin/adminUserDetail/AdminUserDetail.styled.ts (3)
1-4: 파일 주석 경로 오타주석 경로가
userDetail로 표기돼 있습니다. 실제 디렉터리(adminUserDetail) 와 맞지 않아 추후 검색 시 혼란을 줄 수 있으니 수정 권장합니다.
38-44: MainContent 높이 단위 재검토
height: 80%는 부모 요소의 높이에 강하게 의존합니다. 부모가 auto 일 경우 의도한 레이아웃이 깨질 수 있으므로min-height또는 flex-box 기반 레이아웃으로 대체하는 편이 안전합니다.
51-58: DetailContent 의 80vh 는 이중 스크롤 유발 가능성상위 컨테이너도 스크롤이 있을 때, 내부 80vh 고정은 UX 상 불필요한 이중 스크롤을 만들 수 있습니다. 필요하다면
max-height를 사용하거나 상위 컨테이너에만 스크롤을 주는 구조를 고려해 주세요.src/components/admin/userCard/UserCard.tsx (1)
20-21: 불필요한 주석 제거
{/* {userData.userState !== '정지' && */주석은 의미를 알기 어렵고 코드 가독성을 해칩니다. 기능 완성 전이라도 불필요한 주석은 삭제해 주세요.src/pages/admin/adminUser/AdminUser.tsx (2)
12-12: Spinner 컴포넌트 위치 일관성 확인유저 목록 페이지에서도 공통 스피너 컴포넌트를 사용하셨는데, 다른 Admin 페이지는
LoadingSpinner를 사용하고 있습니다. 컴포넌트를 통일하거나 import 경로를 통합해 일관성을 유지해 주세요.
19-25: 로딩 상태 Spinner 컨테이너 재사용 권장
<S.Spinner>래퍼가 여기서만 쓰인다면 styled-component 를 분리할 필요 없이Spinner자체에 margin 을 주거나 공통 래퍼를 만드는 편이 코드를 줄일 수 있습니다.src/components/admin/adminUserDetail/AdminUserDetail.tsx (3)
30-66: TabKey 불일치:'inquiry'타입만 선언되고 실제 탭이 없음
TabKey유니온 타입에'inquiry'가 정의돼 있지만tabs배열엔 존재하지 않습니다. 타입 신뢰성이 떨어지니 둘 중 하나를 맞춰주세요.
30-66: 불필요한 매 렌더 배열 생성
tabs배열은 props 에 의존하지 않아도 되므로useMemo로 래핑해 재생성 비용을 줄일 수 있습니다.- const tabs: { ... }[] = [ + const tabs = React.useMemo( + () => [ // ... - ]; + ], + [userId] + );
70-97: 접근성: “목록으로 이동” 링크에aria-label추가 권장시각장애인 사용자가 목적을 명확히 파악할 수 있도록
aria-label을 부여하는 것이 좋습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
src/components/admin/adminUserDetail/AdminUserDetail.styled.ts(1 hunks)src/components/admin/adminUserDetail/AdminUserDetail.tsx(1 hunks)src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx(1 hunks)src/components/admin/userCard/UserCard.styled.ts(1 hunks)src/components/admin/userCard/UserCard.tsx(1 hunks)src/components/common/noContent/NoContent.tsx(1 hunks)src/components/common/sidebar/Sidebar.styled.ts(1 hunks)src/components/common/sidebar/Sidebar.tsx(2 hunks)src/components/user/mypage/ContentTab.tsx(2 hunks)src/components/user/mypage/activityLog/ActivityLog.tsx(1 hunks)src/components/user/mypage/myProfile/profile/Profile.styled.ts(1 hunks)src/components/user/mypage/myProfile/profile/Profile.tsx(3 hunks)src/constants/admin/mainItems.ts(1 hunks)src/constants/routes.ts(1 hunks)src/constants/user/myPageFilter.ts(1 hunks)src/hooks/admin/useGetUserInfo.ts(1 hunks)src/models/auth.ts(1 hunks)src/models/userInfo.ts(1 hunks)src/pages/admin/adminUser/AdminUser.styled.ts(1 hunks)src/pages/admin/adminUser/AdminUser.tsx(3 hunks)src/routes/AdminRoutes.tsx(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (12)
src/constants/admin/mainItems.ts (1)
src/constants/routes.ts (1)
ADMIN_ROUTE(34-56)
src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx (1)
src/constants/routes.ts (1)
ADMIN_ROUTE(34-56)
src/components/user/mypage/ContentTab.tsx (1)
src/components/user/customerService/MoveInquiredLink.tsx (1)
MovedInquiredLink(5-13)
src/constants/user/myPageFilter.ts (1)
src/constants/routes.ts (1)
ROUTES(1-32)
src/routes/AdminRoutes.tsx (2)
src/constants/routes.ts (1)
ADMIN_ROUTE(34-56)src/components/user/mypage/notifications/Notifications.tsx (1)
Notifications(4-8)
src/hooks/admin/useGetUserInfo.ts (3)
src/models/userInfo.ts (1)
ApiUserInfo(33-35)src/hooks/queries/keys.ts (1)
userInfoKey(16-20)src/api/userpage.api.ts (1)
getUserInfo(5-14)
src/components/user/mypage/activityLog/ActivityLog.tsx (2)
src/components/user/mypage/ContentTab.tsx (1)
ContentTab(20-80)src/constants/user/myPageFilter.ts (2)
ACTIVITY_FILTER_ADMIN(30-33)ACTIVITY_FILTER(25-28)
src/models/userInfo.ts (1)
src/models/tags.ts (2)
PositionTag(10-14)SkillTag(3-8)
src/components/admin/adminUserDetail/AdminUserDetail.styled.ts (1)
src/components/common/sidebar/Sidebar.styled.ts (1)
Container(3-13)
src/components/admin/userCard/UserCard.styled.ts (3)
src/components/admin/mainCard/MainCard.styled.ts (1)
Wrapper(39-42)src/components/admin/previewComponent/inquiresPreview/InquiresPreview.styled.ts (1)
Wrapper(11-16)src/components/admin/previewComponent/reportsPreview/ReportsPreview.styled.ts (1)
Wrapper(11-16)
src/components/user/mypage/myProfile/profile/Profile.tsx (2)
src/models/userInfo.ts (1)
UserInfoAll(27-31)src/util/formatDate.ts (1)
formatDate(3-6)
src/components/admin/userCard/UserCard.tsx (1)
src/models/auth.ts (1)
AllUser(60-63)
🪛 Biome (1.9.4)
src/components/user/mypage/myProfile/profile/Profile.tsx
[error] 24-24: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
[error] 25-25: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
🔇 Additional comments (9)
src/models/auth.ts (1)
71-74:totalPages→totalPage변경이 사용처에 미치는 영향 확인 필요
프로퍼티명을 단·복수형으로 바꾸면서 컴파일 오류가 발생할 가능성이 큽니다. 기존 컴포넌트/훅에서totalPages를 참조하는지 전역 검색으로 확인해 주세요.src/models/userInfo.ts (1)
19-20:warning필드를 필수값으로 바꾼 영향 점검
기존 코드에서warning을 선택적으로 다루던 곳이 있다면 컴파일 에러가 발생합니다. 전역적으로warning?사용처를 확인하고 기본값 혹은 null-safe 처리를 추가해 주세요.src/components/common/noContent/NoContent.tsx (1)
20-22: 관리자 뷰와 문구 일관성 확보 확인
'내'접두사를 제거한 변경이 다른 관리자용 필터(예:ACTIVITY_FILTER_ADMIN)와 표현을 통일시키는 목적임을 확인했습니다. 추가적인 문제는 보이지 않습니다.src/components/user/mypage/ContentTab.tsx (1)
63-64: 조건부 렌더링 OK – 유틸 분리 시 함께 교체 필요
!isAdmin && <MovedInquiredLink />조건은 의도대로 동작합니다. 위에서 제안한 공용 훅으로isAdmin판별을 변경할 경우 이 부분도 같이 수정해 주세요.src/components/common/sidebar/Sidebar.tsx (1)
61-62: 스타일드 컴포넌트 prop 타입 확인 필요
$isAdmin={isAdmin}를 전달했으므로MenuItem스타일 정의에($isAdmin?: boolean)타입이 선언되어 있는지 다시 한번 확인해 주세요. 없으면 TS 오류가 발생할 수 있습니다.src/constants/user/myPageFilter.ts (1)
30-33: 관리자 전용 필터 추가 확인
ACTIVITY_FILTER_ADMIN상수가 잘 추가되었습니다. 동일한 URL/ID를 갖는 두 상수 간의 분기 로직이 제대로 적용됐는지만 테스트로 확인해 주세요.src/components/user/mypage/myProfile/profile/Profile.styled.ts (1)
135-139:aspect-ratio브라우저 지원 확인 필요
aspect-ratio: 1 / 1;는 최신 브라우저에서는 지원되지만, 일부 구형 Safari/Android WebView에서는 무시됩니다. 대응이 필요하다면padding-top:100%트릭을 fallback 으로 추가하거나, 호환성 문서에 명시해 주세요.src/routes/AdminRoutes.tsx (2)
156-171: 중첩 라우트의 index 리다이렉트는 👍
/log하위에서comments로 리다이렉트하도록 설정한 부분이 명확합니다. 추가 의견 없습니다.
64-72:Details
✅ Verification successful
불필요하게 깊은 상대 경로, 실제 파일 위치 확인 필요
../components/user/mypage/activityLog/commentsActivity/CommentsActivity과../components/user/mypage/activityLog/inquiries/Inquiries경로가 실제 디렉터리 구조와 불일치할 가능성이 있습니다. 폴더 이름이 조금이라도 달리 작성되면 런타임에서 Lazy-loading 실패가 발생합니다.tsconfig의paths별칭을 사용하거나, import 경로를 한 번 더 검증해 주세요.
다음으로 실제 파일 위치를 확인해 보겠습니다. 아래 스크립트를 실행하여
CommentsActivity와Inquiries컴포넌트 파일이 프로젝트 내 어디에 있는지 검색합니다.
🏁 Script executed:
#!/bin/bash # 컴포넌트 파일 위치 검색 echo "🔍 CommentsActivity 파일 위치 검색:" fd -e tsx CommentsActivity echo "🔍 Inquiries 파일 위치 검색:" fd -e tsx InquiriesLength of output: 364
경로 유효성 확인 완료: 불일치 없음
src/components/user/mypage/activityLog/commentsActivity/CommentsActivity.tsx및
src/components/user/mypage/activityLog/inquiries/Inquiries.tsx파일이 실제 존재하며, 현재 lazy import 경로와 일치합니다.
따라서 런타임 에러는 발생하지 않으며, 수정이 필요하지 않습니다.참고로, 가독성 향상을 위해
tsconfig.json의paths별칭 설정을 적용하는 것은 선택 사항입니다.
| const useGetUserInfo = (id: number) => { | ||
| const isLoggedIn = useAuthStore.getState().isLoggedIn; | ||
|
|
||
| const { data, isLoading, isFetching } = useQuery<ApiUserInfo>({ | ||
| queryKey: [userInfoKey.userProfile, id], | ||
| queryFn: () => getUserInfo(id), | ||
| staleTime: 1 * 60 * 1000, | ||
| enabled: isLoggedIn, | ||
| }); |
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
스토어 구독 누락으로 로그인 상태 변화가 반영되지 않습니다
useAuthStore.getState() 로 스냅샷만 가져오면 로그아웃/로그인 시 enabled 가 즉시 재평가되지 않습니다. 훅 안에서 구독형 사용을 권장합니다.
-const isLoggedIn = useAuthStore.getState().isLoggedIn;
+const isLoggedIn = useAuthStore(state => state.isLoggedIn);또한 queryKey 가 중첩 배열([['userProfile'], id]) 형태가 됩니다. spread 로 평탄화하면 예측 가능성이 높습니다.
-queryKey: [userInfoKey.userProfile, id],
+queryKey: [...userInfoKey.userProfile, id],📝 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 useGetUserInfo = (id: number) => { | |
| const isLoggedIn = useAuthStore.getState().isLoggedIn; | |
| const { data, isLoading, isFetching } = useQuery<ApiUserInfo>({ | |
| queryKey: [userInfoKey.userProfile, id], | |
| queryFn: () => getUserInfo(id), | |
| staleTime: 1 * 60 * 1000, | |
| enabled: isLoggedIn, | |
| }); | |
| const useGetUserInfo = (id: number) => { | |
| // Subscribe to auth store so isLoggedIn updates trigger a re-evaluation | |
| const isLoggedIn = useAuthStore(state => state.isLoggedIn); | |
| const { data, isLoading, isFetching } = useQuery<ApiUserInfo>({ | |
| // Flatten the key array for predictability | |
| queryKey: [...userInfoKey.userProfile, id], | |
| queryFn: () => getUserInfo(id), | |
| staleTime: 1 * 60 * 1000, | |
| enabled: isLoggedIn, | |
| }); |
🤖 Prompt for AI Agents
In src/hooks/admin/useGetUserInfo.ts around lines 7 to 15, replace the direct
state snapshot call useAuthStore.getState() with a subscription hook like
useAuthStore(state => state.isLoggedIn) to ensure the hook re-renders on login
state changes. Also, flatten the queryKey array by spreading
userInfoKey.userProfile and id into a single-level array to avoid nested arrays
and improve predictability.
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.
getState() 함수 사용과 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.
@layout-SY 좋은 질문이네요! 두 방식의 핵심적인 차이점을 설명드릴게요.
getState() vs useAuthStore(selector)의 차이
1. getState() - 스냅샷 방식
const isLoggedIn = useAuthStore.getState().isLoggedIn;- 한 번만 실행: 컴포넌트가 처음 렌더링될 때 그 순간의 상태값만 가져옴
- 구독 없음: 이후
isLoggedIn상태가 변해도 컴포넌트는 모름 - 정적 값: 변수에 저장된 값은 계속 초기값 그대로 유지
2. Selector 방식 - 구독
const isLoggedIn = useAuthStore(state => state.isLoggedIn);- 구독 생성: 스토어의
isLoggedIn값을 계속 감시 - 자동 업데이트: 값이 변할 때마다 컴포넌트가 자동으로 리렌더링
- 반응형: 로그인/로그아웃 시 즉시 반영
실제 동작 예시
// 시나리오: 사용자가 로그아웃한 경우
// ❌ getState() 방식
const useGetUserInfo = (id) => {
const isLoggedIn = useAuthStore.getState().isLoggedIn; // 처음에 true였다면 계속 true
return useQuery({
enabled: isLoggedIn, // 로그아웃해도 계속 true -> 쿼리가 계속 실행됨 ⚠️
// ...
});
};
// ✅ selector 방식
const useGetUserInfo = (id) => {
const isLoggedIn = useAuthStore(state => state.isLoggedIn); // 로그아웃시 즉시 false로 변경
return useQuery({
enabled: isLoggedIn, // 로그아웃하면 즉시 false -> 쿼리 중단 ✅
// ...
});
};요약: getState()는 "지금 이 순간 값이 뭐야?"이고, selector는 "값이 바뀔 때마다 알려줘"입니다. React 컴포넌트에서는 상태 변화에 반응해야 하므로 selector 방식이 적합해요! 🎯
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.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| const isAdmin = pathname.includes('/admin'); | ||
|
|
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
isAdmin 판별 로직 개선 제안
pathname.includes('/admin')는 /administrator 같이 의도치 않은 경로까지 관리자 페이지로 오인할 가능성이 있습니다. 정규식이나 startsWith를 사용해 세그먼트 단위로 확인하거나 공용 유틸/훅(useIsAdminPath())으로 분리해 중복을 제거하는 편이 안전합니다.
-const isAdmin = pathname.includes('/admin');
+const isAdmin = /^\/admin(\/|$)/.test(pathname); // 혹은 공통 훅으로 추출📝 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 isAdmin = pathname.includes('/admin'); | |
| // 기존: const isAdmin = pathname.includes('/admin'); | |
| const isAdmin = /^\/admin(\/|$)/.test(pathname); // 혹은 공통 훅(useIsAdminPath)으로 추출 |
🤖 Prompt for AI Agents
In src/components/user/mypage/ContentTab.tsx around lines 23 to 24, the current
check using pathname.includes('/admin') can mistakenly identify paths like
'/administrator' as admin. Replace this with a more precise check using a
regular expression or pathname.startsWith('/admin') to ensure only exact admin
paths match. Alternatively, extract this logic into a shared utility or custom
hook like useIsAdminPath() to centralize and reuse the admin path detection
logic safely.
| const isAdmin = location.pathname.includes('/admin'); | ||
|
|
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
isAdmin 중복 로직 공통화 권장
여러 컴포넌트에서 동일한 pathname.includes('/admin') 체크가 반복되고 있습니다. 전역 util 함수나 커스텀 훅으로 추출해 중복을 제거하면 유지보수성이 높아집니다.
🤖 Prompt for AI Agents
In src/components/common/sidebar/Sidebar.tsx around lines 27 to 28, the check
for isAdmin using location.pathname.includes('/admin') is duplicated across
multiple components. Refactor by extracting this logic into a global utility
function or a custom hook that returns the isAdmin boolean. Replace the inline
check with a call to this shared function or hook to improve maintainability and
reduce code duplication.
src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx
Outdated
Show resolved
Hide resolved
| export const Wrapper = styled.div` | ||
| position: relative; | ||
| display: flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| `; | ||
|
|
||
| export const BanArea = styled.div` | ||
| position: absolute; | ||
| top: 0px; | ||
| right: 0px; | ||
| `; | ||
|
|
||
| export const BanButton = styled.button` | ||
| width: 40px; | ||
| border: 1px solid ${({ theme }) => theme.color.lightgrey}; | ||
| border-radius: ${({ theme }) => theme.borderRadius.primary}; | ||
| background-color: ${({ theme }) => theme.color.red}; | ||
| color: ${({ theme }) => theme.color.white}; | ||
| `; | ||
|
|
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
Ban 버튼 접근성·사용성 강화 필요
cursor: pointer;가 없어 버튼임을 시각적으로 전달하지 못합니다.- 높이 미지정으로 0 height 가 렌더될 수 있습니다(내용 없는 icon 상황).
:hover,:focus-visible상태 색상 정의가 없어 키보드 내비게이션 시 피드백 부족합니다.
export const BanButton = styled.button`
+ height: 40px;
width: 40px;
border: 1px solid ${({ theme }) => theme.color.lightgrey};
border-radius: ${({ theme }) => theme.borderRadius.primary};
background-color: ${({ theme }) => theme.color.red};
color: ${({ theme }) => theme.color.white};
+ cursor: pointer;
+
+ &:hover,
+ &:focus-visible {
+ filter: brightness(1.1);
+ outline: 2px solid ${({ theme }) => theme.color.red};
+ }
`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const Wrapper = styled.div` | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| `; | |
| export const BanArea = styled.div` | |
| position: absolute; | |
| top: 0px; | |
| right: 0px; | |
| `; | |
| export const BanButton = styled.button` | |
| width: 40px; | |
| border: 1px solid ${({ theme }) => theme.color.lightgrey}; | |
| border-radius: ${({ theme }) => theme.borderRadius.primary}; | |
| background-color: ${({ theme }) => theme.color.red}; | |
| color: ${({ theme }) => theme.color.white}; | |
| `; | |
| export const Wrapper = styled.div` | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| `; | |
| export const BanArea = styled.div` | |
| position: absolute; | |
| top: 0px; | |
| right: 0px; | |
| `; | |
| export const BanButton = styled.button` | |
| height: 40px; | |
| width: 40px; | |
| border: 1px solid ${({ theme }) => theme.color.lightgrey}; | |
| border-radius: ${({ theme }) => theme.borderRadius.primary}; | |
| background-color: ${({ theme }) => theme.color.red}; | |
| color: ${({ theme }) => theme.color.white}; | |
| cursor: pointer; | |
| &:hover, | |
| &:focus-visible { | |
| filter: brightness(1.1); | |
| outline: 2px solid ${({ theme }) => theme.color.red}; | |
| } | |
| `; |
🤖 Prompt for AI Agents
In src/components/admin/userCard/UserCard.styled.ts between lines 13 and 33, the
BanButton styled component lacks accessibility and usability enhancements. Add
'cursor: pointer;' to visually indicate it is clickable, specify a height to
prevent zero height rendering when content is absent, and define distinct
':hover' and ':focus-visible' state styles to provide visual feedback during
keyboard navigation and mouse hover.
| <S.BanArea | ||
| onClick={(e) => { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| onBan(userData.id); | ||
| }} | ||
| > | ||
| <S.BanButton>퇴출</S.BanButton> | ||
| </S.BanArea> |
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.
onBan 콜백이 구현되지 않았습니다
현재 onBan 은 상위에서 빈 함수로 전달됩니다. 실제 정지 요청을 서버에 보내거나 모달을 띄우는 로직이 필요합니다. 미구현이면 TODO 주석이라도 남겨 명시해 주세요.
🤖 Prompt for AI Agents
In src/components/admin/userCard/UserCard.tsx around lines 22 to 30, the onBan
callback is currently a no-op passed from the parent component. Implement the
onBan function to either send a ban request to the server or open a confirmation
modal. If the implementation is pending, add a TODO comment in the code to
clearly indicate that this functionality needs to be completed.
| if (isLoading || isFetching) { | ||
| return ( | ||
| <S.Spinner> | ||
| <Spinner /> | ||
| </S.Spinner> | ||
| ); | ||
| } |
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
로딩 외 에러 상태 미처리
isLoading, isFetching 외에 error(혹은 isError) 상태가 있을 경우 사용자에게 빈 화면이 노출됩니다. 에러 상태를 명시적으로 처리해 주세요.
🤖 Prompt for AI Agents
In src/components/admin/adminUserDetail/AdminUserDetail.tsx around lines 22 to
28, the current code handles loading and fetching states but does not handle
error states like error or isError. Update the component to explicitly check for
error conditions and render an appropriate error message or UI to inform the
user instead of showing a blank screen.
| const AdminUserDetail = () => { | ||
| const { userId } = useParams(); | ||
| const { userData, isLoading, isFetching } = useGetUserInfo(Number(userId)); | ||
|
|
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.
userId 유효성 체크 누락으로 NaN 호출 가능
useParams()가 문자열 또는 undefined를 반환할 수 있는데, 곧바로 Number()를 호출하면 NaN이 발생합니다. 훅 내부에서 NaN 을 처리하지 않으면 불필요한 API 호출이나 런타임 오류가 발생할 수 있습니다.
- const { userData, isLoading, isFetching } = useGetUserInfo(Number(userId));
+ // guard: userId 가 없거나 숫자가 아니면 404 로 리다이렉트
+ const parsedId = Number(userId);
+ if (!userId || Number.isNaN(parsedId)) {
+ return <Navigate to="/404" replace />;
+ }
+ const { userData, isLoading, isFetching } = useGetUserInfo(parsedId);📝 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 AdminUserDetail = () => { | |
| const { userId } = useParams(); | |
| const { userData, isLoading, isFetching } = useGetUserInfo(Number(userId)); | |
| const AdminUserDetail = () => { | |
| const { userId } = useParams(); | |
| // guard: userId 가 없거나 숫자가 아니면 404 로 리다이렉트 | |
| const parsedId = Number(userId); | |
| if (!userId || Number.isNaN(parsedId)) { | |
| return <Navigate to="/404" replace />; | |
| } | |
| const { userData, isLoading, isFetching } = useGetUserInfo(parsedId); |
🤖 Prompt for AI Agents
In src/components/admin/adminUserDetail/AdminUserDetail.tsx around lines 18 to
21, the userId from useParams() is converted to a number without validation,
which can result in NaN if userId is undefined or not a valid number. Add a
check to ensure userId is defined and a valid number before calling
useGetUserInfo. If invalid, handle the case appropriately to prevent unnecessary
API calls or runtime errors.
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
🧹 Nitpick comments (4)
src/components/admin/adminUserDetail/AdminUserDetail.styled.ts (4)
1-1: 파일 주석 경로가 실제 위치와 불일치합니다주석에는
src/components/admin/userDetail로 표기되어 있으나 실제 파일 경로는adminUserDetail입니다. 검색-기반 도구나 IDE에서 빠르게 찾기 어렵게 만들 수 있으므로 주석을 맞춰 주세요.-// src/components/admin/userDetail/AdminUserDetail.styled.ts +// src/components/admin/adminUserDetail/AdminUserDetail.styled.ts
68-72: 스크롤바 의사요소에 불필요한position·left속성이 포함되어 있습니다
::-webkit-scrollbar의사요소에는position,left속성이 적용되지 않으며 일부 브라우저에서 경고가 발생할 수 있습니다. 제거하는 편이 낫습니다.&::-webkit-scrollbar { width: 8px; - position: relative; - left: 0px; }
74-76: 알파값이 포함된 색상은 테마와 통일하는 것이 좋습니다
rgba(0, 0, 0, 0.2)색상은 테마 색상과 톤이 달라 다크모드 대응에 취약합니다. theme에 투명도 색상을 추가하거나theme.color.black를 활용해 계산하는 방안을 고려해 주세요.
81-84: 하드코딩된 보더·라운드 값이 테마와 불일치합니다다른 컴포넌트에서는
theme.borderRadius.large등을 쓰는데 여기서는 직접2px,30px을 사용했습니다. 통일성을 위해 theme 값을 사용하는 편이 유지보수에 유리합니다.- border: 2px solid #f0f0f0; - border-radius: 30px; + border: 2px solid ${({ theme }) => theme.color.grey100}; + border-radius: ${({ theme }) => theme.borderRadius.large};
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/components/admin/adminUserDetail/AdminUserDetail.styled.ts(1 hunks)src/components/admin/adminUserDetail/AdminUserDetail.tsx(1 hunks)src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx(1 hunks)src/components/admin/userCard/UserCard.styled.ts(1 hunks)src/components/common/sidebar/Sidebar.styled.ts(2 hunks)src/components/common/sidebar/Sidebar.tsx(2 hunks)src/pages/admin/adminUser/AdminUser.tsx(3 hunks)src/routes/AdminRoutes.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx
- src/components/common/sidebar/Sidebar.tsx
- src/routes/AdminRoutes.tsx
- src/components/admin/userCard/UserCard.styled.ts
- src/pages/admin/adminUser/AdminUser.tsx
- src/components/admin/adminUserDetail/AdminUserDetail.tsx
- src/components/common/sidebar/Sidebar.styled.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: accessibility-test
| export const Container = styled.div` | ||
| width: 100%; | ||
| height: 800px; | ||
| margin: 6rem auto 0; | ||
| display: flex; | ||
| flex-direction: column; | ||
| `; |
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
고정 높이(800px)는 반응형 이슈를 유발할 수 있습니다
Container 높이를 픽스 값으로 두면 뷰포트가 작은 노트북이나 모바일 환경에서 컨텐츠가 잘리거나 스크롤이 불필요하게 중첩될 수 있습니다.
-export const Container = styled.div`
- width: 100%;
- height: 800px;
+export const Container = styled.div`
+ width: 100%;
+ min-height: 100vh; /* 또는 calc(100vh - 헤더높이) */
margin: 6rem auto 0;
display: flex;
flex-direction: column;
`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const Container = styled.div` | |
| width: 100%; | |
| height: 800px; | |
| margin: 6rem auto 0; | |
| display: flex; | |
| flex-direction: column; | |
| `; | |
| export const Container = styled.div` | |
| width: 100%; | |
| min-height: 100vh; /* 또는 calc(100vh - 헤더높이) */ | |
| margin: 6rem auto 0; | |
| display: flex; | |
| flex-direction: column; | |
| `; |
🤖 Prompt for AI Agents
In src/components/admin/adminUserDetail/AdminUserDetail.styled.ts lines 7 to 13,
the Container component uses a fixed height of 800px, which can cause
responsiveness issues on smaller screens. Replace the fixed height with a
flexible value such as min-height or use relative units like vh or percentages
to allow the container to adapt to different viewport sizes and prevent content
clipping or unnecessary scrolling.
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
🧹 Nitpick comments (15)
src/components/user/userPage/userProjectList/UserProjectList.tsx (2)
11-12: 관리자 컨텍스트 감지 로직을 더 견고하게 개선하는 것을 고려해보세요.현재
pathname.includes('/admin')를 사용한 관리자 감지 방식은 '/admin'을 포함하지만 실제로는 관리자 라우트가 아닌 경로에서 false positive를 발생시킬 수 있습니다.더 정확한 검사를 위해 다음과 같이 개선할 수 있습니다:
- const isAdmin = pathname.includes('/admin'); + const isAdmin = pathname.startsWith('/admin');또는 라우트 상수를 활용한 방법:
- const isAdmin = pathname.includes('/admin'); + const isAdmin = pathname.startsWith(ROUTES.admin); // ROUTES.admin이 정의되어 있다면
10-10: 컴포넌트명과 파일명의 일관성을 맞춰주세요.파일명은
UserProjectList.tsx이지만 export된 컴포넌트명은UserProjects입니다. 일관성을 위해 둘 중 하나로 통일하는 것이 좋겠습니다.다음 중 하나를 선택하여 일관성을 맞춰주세요:
-export default function UserProjects() { +export default function UserProjectList() {또는 파일명을
UserProjects.tsx로 변경하는 방법도 있습니다.src/api/admin/customerService/notice.api.ts (3)
5-12: 에러 처리 및 입력 검증 개선 제안현재 구현은 기본적인 CRUD 작업을 올바르게 수행하지만, 다음과 같은 개선사항을 고려해보세요:
- 더 구체적인 에러 메시지 추가
- 입력 매개변수 검증
export const postNotice = async (formData: WriteBody) => { + if (!formData.title || !formData.content) { + throw new Error('제목과 내용은 필수 항목입니다.'); + } try { await httpClient.post<ApiCommonBasicType>(`/notice`, formData); } catch (e) { - console.error(e); + console.error('공지사항 등록 중 오류가 발생했습니다:', e); throw e; } };
14-21: 일관성 있는 에러 처리 패턴 적용putNotice 함수에도 동일한 개선사항을 적용하세요.
export const putNotice = async (id: string, formData: WriteBody) => { + if (!id) { + throw new Error('공지사항 ID가 필요합니다.'); + } + if (!formData.title || !formData.content) { + throw new Error('제목과 내용은 필수 항목입니다.'); + } try { await httpClient.put<ApiCommonBasicType>(`/notice/${id}`, formData); } catch (e) { - console.error(e); + console.error('공지사항 수정 중 오류가 발생했습니다:', e); throw e; } };
23-30: 삭제 함수 에러 처리 개선deleteNotice 함수에도 입력 검증과 구체적인 에러 메시지를 추가하세요.
export const deleteNotice = async (id: string) => { + if (!id) { + throw new Error('삭제할 공지사항 ID가 필요합니다.'); + } try { await httpClient.delete<ApiCommonBasicType>(`/notice/${id}`); } catch (e) { - console.error(e); + console.error('공지사항 삭제 중 오류가 발생했습니다:', e); throw e; } };src/api/admin/customerService/inquiry.api.ts (4)
19-30: 문의 상세 조회 함수의 에러 처리 개선getInquiryDetail 함수에 구체적인 에러 메시지와 입력 검증을 추가하세요.
export const getInquiryDetail = async (id: string) => { + if (!id) { + throw new Error('문의 ID가 필요합니다.'); + } try { const response = await httpClient.get<ApiAdminInquiryDetail>( `/inquiry/${id}` ); return response.data.data; } catch (e) { - console.error(e); + console.error('문의 상세 조회 중 오류가 발생했습니다:', e); throw e; } };
32-41: 문의 답변 등록 함수 개선postInquiryAnswer 함수에 입력 검증과 구체적인 에러 메시지를 추가하세요.
export const postInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => { + if (!id || !answer) { + throw new Error('문의 ID와 답변 내용이 모두 필요합니다.'); + } try { await httpClient.post<ApiCommonBasicType>(`/inquiry/${id}/answer`, { answer, }); } catch (e) { - console.error(e); + console.error('문의 답변 등록 중 오류가 발생했습니다:', e); throw e; } };
43-52: 문의 답변 수정 함수 개선patchInquiryAnswer 함수에도 동일한 개선사항을 적용하세요.
export const patchInquiryAnswer = async ({ id, answer }: InquiryAnswerBody) => { + if (!id || !answer) { + throw new Error('문의 ID와 답변 내용이 모두 필요합니다.'); + } try { await httpClient.patch<ApiCommonBasicType>(`/inquiry/${id}/answer`, { answer, }); } catch (e) { - console.error(e); + console.error('문의 답변 수정 중 오류가 발생했습니다:', e); throw e; } };
54-61: 문의 삭제 함수 개선deleteInquiry 함수에 입력 검증과 구체적인 에러 메시지를 추가하세요.
export const deleteInquiry = async (id: string) => { + if (!id) { + throw new Error('삭제할 문의 ID가 필요합니다.'); + } try { await httpClient.delete<ApiCommonBasicType>(`/inquiry/${id}`); } catch (e) { - console.error(e); + console.error('문의 삭제 중 오류가 발생했습니다:', e); throw e; } };src/components/admin/adminTags/skills/AdminSkillTagItems.tsx (2)
4-4: 인터페이스 이름의 오타를 수정해주세요
AdminSKillTagItemsProps에서AdminSkillTagItemsProps로 수정이 필요합니다.-interface AdminSKillTagItemsProps { +interface AdminSkillTagItemsProps {
9-12: 인터페이스 이름 일관성을 유지해주세요함수 매개변수 타입도 수정된 인터페이스 이름을 사용하도록 업데이트해주세요.
}: AdminSKillTagItemsProps) { }: AdminSkillTagItemsProps) {src/components/common/skillTagBox/SkillTagBox.tsx (1)
39-44: 불필요한 삼항 연산자를 단순화하세요.정적 분석 도구가 지적한 대로, 불필요한 삼항 연산자를 제거할 수 있습니다.
다음과 같이 단순화할 수 있습니다:
$select={ - selectedTag?.includes(skillTagData.id) || - ((isMain && - searchFiltersSkillTag?.includes(skillTagData.id)) || - (isCreate && selectedTag?.includes(skillTagData.id)) - ? true - : false) + selectedTag?.includes(skillTagData.id) || + (isMain && searchFiltersSkillTag?.includes(skillTagData.id)) || + (isCreate && selectedTag?.includes(skillTagData.id)) }src/api/admin/tag.api.ts (1)
5-12: API 함수 구조는 일관되지만 에러 처리를 개선하세요.현재 구현은 작동하지만 다음 개선사항을 고려하세요:
- 프로덕션 환경에서
console.error사용은 적절하지 않을 수 있습니다- 에러 타입에 따른 구체적인 처리가 필요할 수 있습니다
공통 에러 처리 함수를 생성하여 중복을 줄일 수 있습니다:
const handleApiError = (error: unknown, operation: string) => { // 로깅은 환경에 따라 다르게 처리 if (process.env.NODE_ENV === 'development') { console.error(`${operation} failed:`, error); } throw error; }; export const postSkillTag = async (formData: FormData) => { try { await httpClient.post<ApiCommonBasicType>(`/skill-tag`, formData); } catch (e) { handleApiError(e, 'postSkillTag'); } };src/components/admin/adminTags/AdminTagsBasic.tsx (1)
13-15: URL 기반 태그 타입 결정 로직을 더 견고하게 만드세요.현재 구현은 작동하지만 URL 구조 변경에 취약합니다.
더 명시적인 방법을 고려해보세요:
- const witchTag: TWitchTag = pathname.includes('skill') ? 'skill' : 'position'; + const witchTag: TWitchTag = pathname.endsWith('/skill') || pathname.includes('/skill/') + ? 'skill' + : 'position';또는 라우트 매개변수를 사용하는 것이 더 좋을 수 있습니다.
src/components/admin/adminTags/AdminTagCRUD.tsx (1)
109-117: 타입 단언을 더 안전하게 개선하세요.현재의 타입 단언은 작동하지만 더 안전한 방법을 사용할 수 있습니다.
- state.handlePutTag({ params: formData, id: itemId } as { - params: T; - id: number; - }); + state.handlePutTag({ + params: formData as T, + id: itemId + });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (39)
src/api/admin/customerService/inquiry.api.ts(1 hunks)src/api/admin/customerService/notice.api.ts(1 hunks)src/api/admin/tag.api.ts(1 hunks)src/components/admin/adminTags/AdminTagCRUD.styled.ts(1 hunks)src/components/admin/adminTags/AdminTagCRUD.tsx(1 hunks)src/components/admin/adminTags/AdminTagsBasic.styled.ts(1 hunks)src/components/admin/adminTags/AdminTagsBasic.tsx(1 hunks)src/components/admin/adminTags/positions/AdminPositionItems.styled.ts(1 hunks)src/components/admin/adminTags/positions/AdminPositionItems.tsx(1 hunks)src/components/admin/adminTags/positions/AdminPositionTags.tsx(1 hunks)src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts(1 hunks)src/components/admin/adminTags/skills/AdminSkillTagItems.tsx(1 hunks)src/components/admin/adminTags/skills/AdminSkillTags.tsx(1 hunks)src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx(1 hunks)src/components/common/positionButton/PositionButton.styled.ts(0 hunks)src/components/common/positionButton/PositionButton.tsx(1 hunks)src/components/common/skillTagBox/SkillTagBox.tsx(2 hunks)src/components/common/skillTagBox/skillTag/SkillTag.tsx(1 hunks)src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx(1 hunks)src/components/user/home/projectCardLists/cardList/CardList.tsx(0 hunks)src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx(1 hunks)src/components/user/mypage/myProfile/editProfile/EditProfile.tsx(2 hunks)src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx(2 hunks)src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx(3 hunks)src/components/user/userPage/userProjectList/UserProjectList.tsx(3 hunks)src/constants/admin/sidebar.ts(1 hunks)src/constants/routes.ts(2 hunks)src/constants/user/modalMessage.ts(1 hunks)src/hooks/admin/useAdminInquiry.ts(1 hunks)src/hooks/admin/useAdminNotice.ts(1 hunks)src/hooks/admin/useAdminTag.ts(1 hunks)src/hooks/admin/useGetAllInquiries.ts(1 hunks)src/hooks/queries/keys.ts(1 hunks)src/hooks/user/useSearchFilteringTags.ts(1 hunks)src/models/tags.ts(2 hunks)src/pages/admin/adminTags/AdminTags.tsx(0 hunks)src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx(1 hunks)src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx(1 hunks)src/routes/AdminRoutes.tsx(6 hunks)
💤 Files with no reviewable changes (3)
- src/components/user/home/projectCardLists/cardList/CardList.tsx
- src/components/common/positionButton/PositionButton.styled.ts
- src/pages/admin/adminTags/AdminTags.tsx
✅ Files skipped from review due to trivial changes (15)
- src/components/admin/adminTags/positions/AdminPositionTags.tsx
- src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx
- src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx
- src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts
- src/hooks/admin/useAdminInquiry.ts
- src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx
- src/components/admin/adminTags/positions/AdminPositionItems.styled.ts
- src/hooks/admin/useAdminNotice.ts
- src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx
- src/components/admin/adminTags/skills/AdminSkillTags.tsx
- src/constants/user/modalMessage.ts
- src/constants/admin/sidebar.ts
- src/hooks/queries/keys.ts
- src/components/admin/adminTags/AdminTagsBasic.styled.ts
- src/components/admin/adminTags/AdminTagCRUD.styled.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/routes/AdminRoutes.tsx
- src/constants/routes.ts
🧰 Additional context used
🧬 Code Graph Analysis (11)
src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx (2)
src/hooks/user/useSearchFilteringTags.ts (1)
useSearchFilteringTags(11-49)src/hooks/user/useSaveSearchFiltering.ts (1)
useSaveSearchFiltering(4-10)
src/hooks/user/useSearchFilteringTags.ts (3)
src/models/tags.ts (3)
SkillTag(3-8)PositionTag(10-14)MethodTag(16-20)src/hooks/queries/keys.ts (1)
Tag(73-77)src/api/projectSearchFiltering.api.ts (2)
getSkillTag(4-13)getPositionTag(15-23)
src/components/user/mypage/myProfile/editProfile/EditProfile.tsx (1)
src/hooks/user/useSearchFilteringTags.ts (1)
useSearchFilteringTags(11-49)
src/hooks/admin/useAdminTag.ts (3)
src/api/admin/tag.api.ts (6)
postSkillTag(5-12)putSkillTag(14-27)deleteSkillTag(29-36)postPositionTag(38-45)putPositionTag(47-60)deletePositionTag(62-69)src/hooks/queries/keys.ts (1)
Tag(73-77)src/models/tags.ts (1)
TagFormType(34-37)
src/components/admin/adminTags/positions/AdminPositionItems.tsx (2)
src/hooks/user/useSearchFilteringTags.ts (1)
useSearchFilteringTags(11-49)src/components/common/positionButton/PositionButton.tsx (1)
PositionButton(11-29)
src/api/admin/customerService/notice.api.ts (3)
src/models/customerService.ts (1)
WriteBody(59-62)src/api/http.api.ts (1)
httpClient(80-80)src/models/apiCommon.ts (1)
ApiCommonBasicType(6-8)
src/components/admin/adminTags/skills/AdminSkillTagItems.tsx (1)
src/components/common/skillTagBox/SkillTagBox.tsx (1)
SkillTagBox(16-61)
src/components/common/skillTagBox/SkillTagBox.tsx (1)
src/hooks/user/useSearchFilteringTags.ts (1)
useSearchFilteringTags(11-49)
src/api/admin/customerService/inquiry.api.ts (3)
src/api/http.api.ts (1)
httpClient(80-80)src/models/inquiry.ts (3)
ApiAdminInquiry(19-21)ApiAdminInquiryDetail(23-25)InquiryAnswerBody(27-30)src/models/apiCommon.ts (1)
ApiCommonBasicType(6-8)
src/components/common/positionButton/PositionButton.tsx (1)
src/components/common/positionButton/PositionButton.styled.ts (1)
PositionButton(3-39)
src/api/admin/tag.api.ts (2)
src/api/http.api.ts (1)
httpClient(80-80)src/models/apiCommon.ts (1)
ApiCommonBasicType(6-8)
🪛 Biome (1.9.4)
src/components/common/skillTagBox/SkillTagBox.tsx
[error] 40-44: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
🔇 Additional comments (33)
src/components/user/userPage/userProjectList/UserProjectList.tsx (2)
1-1: useLocation 훅 추가가 적절합니다.관리자 컨텍스트 감지를 위한 useLocation 훅 import가 올바르게 추가되었습니다.
31-32: 보안 속성이 올바르게 적용되었습니다.관리자 컨텍스트에서 새 탭으로 링크를 열 때
target="_blank"와rel="noopener noreferrer"속성이 적절히 적용되어 보안상 안전합니다.src/hooks/admin/useGetAllInquiries.ts (1)
3-3: 임포트 경로 수정 승인API 모듈의 경로가 올바른 케이싱으로 수정되었습니다. 표준화된 파일명 규칙에 맞는 좋은 변경사항입니다.
src/api/admin/customerService/inquiry.api.ts (1)
9-17: 전체 문의 조회 함수 승인getAllInquiries 함수는 잘 구현되었습니다. 구체적인 에러 메시지가 포함되어 있어 디버깅에 도움이 됩니다.
src/components/common/skillTagBox/skillTag/SkillTag.tsx (1)
17-17: 이미지 캐시 무효화를 위한 updatedAt 전달이 잘 구현되었습니다.스킬 태그 이미지의 업데이트를 반영하기 위해
updatedAt타임스탬프를 전달하는 구현이 적절합니다.src/models/tags.ts (2)
7-7: SkillTag 인터페이스의 updatedAt 필드 변경이 적절합니다.이미지 캐시 무효화를 위해
createdAt에서updatedAt으로 변경한 것이 합리적입니다.
34-37: 새로운 TagFormType 인터페이스가 잘 정의되었습니다.관리자 태그 CRUD 작업을 위한 폼 타입이 명확하게 정의되어 있습니다.
src/components/user/mypage/myProfile/editProfile/EditProfile.tsx (2)
13-13: 훅 이름 변경이 일관되게 적용되었습니다.
useSearchFilteringSkillTag에서useSearchFilteringTags로의 리팩토링이 올바르게 적용되었습니다.
37-37: 새로운 훅 사용법이 올바르게 적용되었습니다.구조 분해 할당이 새로운 훅의 반환 타입과 일치하게 업데이트되었습니다.
src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx (3)
6-6: 훅 임포트 변경이 일관되게 적용되었습니다.전체 리팩토링의 일환으로
useSearchFilteringTags훅으로의 변경이 올바르게 반영되었습니다.
13-13: 새로운 훅 사용이 올바르게 구현되었습니다.
positionTagsData와methodTagsData를 새로운 훅에서 올바르게 가져오고 있습니다.
21-21: 이벤트 타입이 더 구체적으로 개선되었습니다.
React.MouseEvent<HTMLElement>에서React.MouseEvent<HTMLDivElement>로 변경하여 타입 안전성이 향상되었습니다.src/hooks/user/useSearchFilteringTags.ts (3)
9-9: 중앙화된 쿼리 키 상수 사용이 좋은 개선입니다.
Tag상수를 임포트하여 쿼리 키 관리를 중앙화한 것이 캐시 관리 일관성을 향상시킵니다.
11-11: 훅 이름 변경이 적절합니다.
useSearchFilteringTags로 변경하여 스킬 태그뿐만 아니라 포지션 태그와 메소드 태그도 다룬다는 것을 더 명확하게 표현합니다.
19-19: 정적 쿼리 키 사용으로 캐시 관리가 개선되었습니다.동적 배열 키에서 정적
Tag상수로 변경하여 React Query 캐시 관리의 일관성과 안정성이 향상되었습니다.Also applies to: 25-25, 31-31
src/hooks/admin/useAdminTag.ts (1)
1-97: 잘 구현된 커스텀 훅입니다!React Query 패턴을 올바르게 따르고 있으며, 각 mutation마다 적절한 에러 핸들링과 캐시 무효화가 구현되어 있습니다. 스킬 태그와 포지션 태그의 CRUD 작업을 중앙에서 관리하는 좋은 구조입니다.
src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx (2)
6-6: 좋은 캐시 무효화 구현입니다!
updatedAt프로퍼티를 추가하여 이미지 캐싱 문제를 해결하는 좋은 접근 방식입니다.
19-23: 이미지 캐시 무효화가 올바르게 구현되었습니다쿼리 파라미터로
updatedAt을 사용하여 이미지가 업데이트될 때 브라우저 캐시를 무효화하는 표준적인 방법입니다.src/components/admin/adminTags/positions/AdminPositionItems.tsx (1)
1-26: 깔끔하게 구현된 컴포넌트입니다!
useSearchFilteringTags훅을 사용하여 데이터를 가져오고, 각 포지션 태그에 대해 클릭 핸들러를 올바르게 구현했습니다. 콜백 패턴을 적절히 사용하여 부모 컴포넌트와의 상호작용을 처리하고 있습니다.src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx (3)
1-1: React import 추가가 적절합니다TypeScript strict mode에서 필요한 React import를 추가했습니다.
31-36: 클릭 핸들링 로직이 잘 리팩토링되었습니다인덱스 증가 로직을 별도의 래퍼 함수로 분리하여 코드 가독성을 향상시켰습니다.
47-47: 이벤트 핸들링이 올바르게 업데이트되었습니다인라인 화살표 함수에서
onClickSelectprop으로 변경하여 일관성을 유지했습니다.src/components/admin/adminTags/skills/AdminSkillTagItems.tsx (2)
13-23: 견고한 클릭 이벤트 핸들링 구현입니다
data-id속성을 추출하기 위해 직접적인 타겟과 상위 요소 검색을 모두 사용하는 방식이 좋습니다. 이벤트 전파 중단도 적절히 처리되어 있습니다.
25-29: SkillTagBox 컴포넌트 사용이 올바릅니다선택된 태그 ID를
selectedTagprop으로 전달하는 구조가 적절합니다.src/components/common/skillTagBox/SkillTagBox.tsx (2)
2-2: 훅 리팩토링이 올바르게 적용되었습니다.
useSearchFilteringSkillTag에서useSearchFilteringTags로의 변경이 새로운 태그 관리 시스템과 일치합니다.
9-17: Props 개선이 잘 적용되었습니다.
widthprop을 선택적으로 만들고 기본값을 제공한 것이 컴포넌트 사용성을 향상시킵니다.src/components/common/positionButton/PositionButton.tsx (2)
5-8: Props 및 이벤트 타입 개선이 적절합니다.
fontSize를 선택적으로 만들고 이벤트 타입을 버튼 요소에 맞게 변경한 것이 올바릅니다.
19-27: 컴포넌트 구조 개선으로 접근성이 향상되었습니다.외부 컨테이너 div를 제거하고 버튼 요소를 직접 사용하는 것은 의미적 HTML과 접근성을 개선합니다.
src/components/admin/adminTags/AdminTagsBasic.tsx (2)
28-48: tagState 구성 객체가 잘 설계되었습니다.각 태그 타입에 대한 설정과 핸들러를 체계적으로 구성했습니다. 제네릭 타입 사용도 적절합니다.
57-69: 조건부 렌더링에서 제네릭 타입 사용이 훌륭합니다.FormData와 string 타입에 대한 제네릭 타입 사용이 타입 안전성을 제공합니다.
src/components/admin/adminTags/AdminTagCRUD.tsx (3)
58-76: 폼 검증 로직이 올바르게 구현되었습니다.입력값 검증과 스킬 태그의 이미지 필수 검증이 적절합니다.
182-188: 메모리 누수 방지를 위한 정리 로직이 훌륭합니다.Object URL 정리를 통한 메모리 관리가 잘 구현되어 있습니다.
151-158: ```shell
#!/bin/bash
rg -n "fileInputRef" src/components/admin/adminTags/AdminTagCRUD.tsx</details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| export const postPositionTag = async (name: string) => { | ||
| try { | ||
| await httpClient.post<ApiCommonBasicType>(`/position-tag`, { name }); | ||
| } catch (e) { | ||
| console.error(e); | ||
| throw e; | ||
| } | ||
| }; |
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
포지션 태그 API에 입력 검증을 추가하세요.
빈 문자열이나 유효하지 않은 이름에 대한 검증이 필요합니다.
export const postPositionTag = async (name: string) => {
+ if (!name || name.trim().length === 0) {
+ throw new Error('Tag name cannot be empty');
+ }
try {
- await httpClient.post<ApiCommonBasicType>(`/position-tag`, { name });
+ await httpClient.post<ApiCommonBasicType>(`/position-tag`, { name: name.trim() });
} catch (e) {
console.error(e);
throw e;
}
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const postPositionTag = async (name: string) => { | |
| try { | |
| await httpClient.post<ApiCommonBasicType>(`/position-tag`, { name }); | |
| } catch (e) { | |
| console.error(e); | |
| throw e; | |
| } | |
| }; | |
| export const postPositionTag = async (name: string) => { | |
| if (!name || name.trim().length === 0) { | |
| throw new Error('Tag name cannot be empty'); | |
| } | |
| try { | |
| await httpClient.post<ApiCommonBasicType>(`/position-tag`, { name: name.trim() }); | |
| } catch (e) { | |
| console.error(e); | |
| throw e; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In src/api/admin/tag.api.ts around lines 38 to 45, the postPositionTag function
lacks input validation for the name parameter. Add validation to check if the
name is a non-empty string and meets any required format criteria before making
the HTTP POST request. If the validation fails, throw an appropriate error or
reject the input to prevent sending invalid data to the API.
src/hooks/admin/useGetUserInfo.ts
Outdated
| queryKey: [userInfoKey.userProfile, id], | ||
| queryFn: () => getUserInfo(id), | ||
| staleTime: 1 * 60 * 1000, | ||
| enabled: isLoggedIn, |
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.
위에서 number가 없을 가능성에 대해 처리하지 않고 있으니 enabled는 !!id 일 때가 맞는 것 같긴합니당
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 useGetUserInfo = (id: number) => { | ||
| const isLoggedIn = useAuthStore.getState().isLoggedIn; | ||
|
|
||
| const { data, isLoading, isFetching } = useQuery<ApiUserInfo>({ | ||
| queryKey: [userInfoKey.userProfile, id], | ||
| queryFn: () => getUserInfo(id), | ||
| staleTime: 1 * 60 * 1000, | ||
| enabled: isLoggedIn, | ||
| }); |
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.
컴포넌트에서 불러올때 재렌더되면 다시 불러오는거 아냐?
구현내용
연관이슈
관련이슈 번호를 close해주세요.
close #340
Summary by CodeRabbit
신규 기능
버그 수정
스타일
기타