-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 슈퍼 유저 관리자 권한 1차 스펙 #85
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
base: develop
Are you sure you want to change the base?
Conversation
|
Unable to deploy a commit from a private repository on your GitHub organization to the emithen's projects team on Vercel, which is currently on the Hobby plan. In order to deploy, you can:
To read more about collaboration on Vercel, click here. |
Walkthrough관리자용 게시물 고정/해제·삭제 UI 및 관련 Votes API 추가, 밸런스 트렌딩 섹션과 Pin 흐름 도입, 멤버 프로필 타입·API 서명 변경, 앱 시작 시 인증 복구(AuthBootstrap) 추가, ESLint·TS 미사용 변수 규칙 비활성화. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Admin
participant UI as PollPage
participant Modal as DeleteConfirmModal
participant API as Votes API
participant Router
Admin->>UI: 삭제 버튼 클릭
UI->>Modal: open = true
Modal-->>Admin: 삭제 확인 표시
Admin->>Modal: 확인 클릭
Modal->>UI: onConfirm 호출
UI->>API: deleteVote(voteId)
API-->>API: DELETE /votes/{voteId}
alt 성공
API-->>UI: 성공 응답
UI->>Modal: open = false
UI->>Router: 뒤로 이동/목록 갱신
else 실패
API-->>UI: 에러 전파
UI->>UI: 에러 표시
end
sequenceDiagram
autonumber
participant Admin
participant PinMenu
participant API as Votes API
participant Balanse as BalansePage
participant Store as Redux
Admin->>PinMenu: 메뉴 열고 핀 타입 선택
PinMenu->>API: pinVote(voteId, pinType)
API-->>PinMenu: 응답
PinMenu->>Balanse: onPinChange()
Balanse->>Store: 트렌딩/목록 리프레시 트리거
Store-->>Balanse: 새로운 데이터 반영
Balanse-->>Admin: UI 업데이트
sequenceDiagram
autonumber
participant App
participant AuthBootstrap
participant Token as getAccessToken()
participant Redux
participant Thunk as fetchProfileThunk
App->>AuthBootstrap: 마운트
AuthBootstrap->>Token: 토큰 조회
alt 토큰 존재
Token-->>AuthBootstrap: token
AuthBootstrap->>Redux: dispatch(fetchProfileThunk)
Thunk-->>Redux: setProfile 저장
else 토큰 없음
Token-->>AuthBootstrap: null
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ 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. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/pages/my/edit/editPage.tsx (2)
117-125: role을 'USER'로 하드코딩하면 관리자 권한이 손실될 수 있습니다.프로필 업데이트 시 기존
role값을 유지하지 않고 항상'USER'로 설정하면, 관리자(ADMIN) 사용자가 프로필을 수정할 때 권한이 일반 사용자로 변경될 수 있습니다.🔎 수정 제안
return { nickname: nickname as string, gender: genderData, age: ageData, mbtiIe: mbtiIe, mbtiTf: mbtiTf, mbti: mbti, - role: 'USER', + role: myPageData?.role ?? 'USER', }참고:
myPageData에role필드가 없다면 API 응답 타입을 확인하고 적절히 처리해야 합니다.
139-141: 도달 불가능한 중복 코드입니다.Line 54-56에서 이미
myPageData가 없으면 early return하므로, 이 조건문은 절대 실행되지 않습니다. hooks 규칙 위반 수정 후 이 코드를 제거하세요.
♻️ Duplicate comments (1)
src/components/pages/poll/pinButton.tsx (1)
8-12: 참고: PIN_LABELS 불일치 관련이 파일의
PIN_LABELS매핑(HOT: '고정됨')이src/components/pages/balanse/trending-section/pinButton.tsx의 매핑(TRENDING: '고정됨')과 반대입니다.이 불일치는 해당 파일의 리뷰 코멘트에서 이미 다루었습니다.
🧹 Nitpick comments (13)
tsconfig.json (1)
16-16: 사용하지 않는 변수 체크를 비활성화하는 것은 코드 품질에 부정적 영향을 줍니다.
noUnusedLocals: false설정은 사용하지 않는 지역 변수를 허용하여 코드베이스에 불필요한 코드가 누적될 수 있습니다. 타입 재구성 중 임시 조치라면 리팩토링 완료 후 다시 활성화하거나, 사용하지 않는 변수들을 정리하는 것을 권장합니다.eslint.config.mjs (1)
37-38: 사용하지 않는 변수에 대한 린트 규칙을 비활성화하면 코드 유지보수가 어려워집니다.두 규칙(
@typescript-eslint/no-unused-vars,no-unused-vars)을 모두 비활성화하면 사용하지 않는 import, 변수, 함수들이 코드베이스에 누적되어 가독성과 유지보수성이 저하됩니다. tsconfig.json의noUnusedLocals: false와 함께, 이러한 변경이 타입 재구성 중 임시 조치인지 확인이 필요합니다. 리팩토링 완료 후 규칙을 다시 활성화하고 사용하지 않는 코드를 정리하는 것을 권장합니다.src/components/pages/balanse/trending-section/sectionHeader.tsx (1)
1-10: 코드 중복을 제거하는 것을 고려하세요.이 컴포넌트는
src/components/pages/balanse/header.tsx의 일부와 동일한 UI(flame 아이콘 + "인기 급상승 토픽" 텍스트)를 렌더링합니다. 유지보수성을 위해 두 곳 중 하나로 통합하거나 공통 컴포넌트로 추출하는 것을 권장합니다.🔎 제안하는 리팩토링 방안
옵션 1: 기존 header.tsx의 해당 부분을 이 컴포넌트로 교체
옵션 2: 공통TrendingLabel컴포넌트를 만들어 두 곳에서 재사용// 공통 컴포넌트 예시 export const TrendingLabel = () => ( <div className="flex items-center gap-1"> <Flame className="w-4 h-4 text-orange-500" /> <span className="text-md font-semibold">인기 급상승 토픽</span> </div> )src/components/pages/poll/_admin/AdminFloatingButton.tsx (2)
14-23: 메뉴 외부 클릭 시 닫힘 동작 고려 권장현재 메뉴가 열린 상태에서 외부 영역을 클릭해도 메뉴가 닫히지 않습니다. 사용자 경험 개선을 위해 외부 클릭 시 메뉴를 자동으로 닫는 기능을 추가하는 것을 고려해보세요.
🔎 제안하는 개선 방법
+'use client' -import { useState } from 'react' +import { useState, useEffect, useRef } from 'react' interface Props { onDelete: () => void } export default function AdminFloatingButton({ onDelete }: Props) { const [open, setOpen] = useState(false) + const menuRef = useRef<HTMLDivElement>(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false) + } + } + + if (open) { + document.addEventListener('mousedown', handleClickOutside) + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [open]) return ( - <div className="flex flex-col items-end fixed bottom-4 right-4 z-50"> + <div ref={menuRef} className="flex flex-col items-end fixed bottom-4 right-4 z-50">
26-31: 접근성 개선을 위한 aria-label 추가 고려관리자 전용 기능이지만, 스크린 리더 사용자를 위해 버튼에
aria-label속성을 추가하는 것을 고려해보세요.🔎 제안하는 개선
<button + aria-label={open ? "관리자 메뉴 닫기" : "관리자 메뉴 열기"} onClick={() => setOpen((prev) => !prev)} className="w-14 h-14 rounded-full bg-black text-white shadow-xl flex items-center justify-center text-xl" >src/components/pages/balanse/trending-section/pinButton.tsx (1)
26-31: 이미지 alt 텍스트 개선 권장현재
alt="pin"은 너무 일반적입니다. 스크린 리더 사용자를 위해 더 설명적인 텍스트를 사용하는 것을 고려해보세요.🔎 제안하는 개선
<img src={isPinned ? '/check-circle.svg' : '/letter-x-circle.svg'} - alt="pin" + alt={isPinned ? "고정됨" : "고정 안됨"} width={20} height={20} />src/api/member/types.ts (1)
1-55: 타입 중복 고려 - DRY 원칙 적용 가능
CreateMemberProfileRequest와FetchMemberProfileResponse.profile의 구조가 거의 동일하며,@/types/member의Profile타입과도 유사합니다.공통 타입을 추출하여 중복을 줄일 수 있지만, API DTO와 도메인 모델을 분리하는 현재 접근 방식도 타당합니다. 향후 API와 내부 모델이 달라질 경우를 대비한 유연성을 제공하기 때문입니다.
🔎 선택적 DRY 리팩터링 예시
만약 API 타입과 도메인 타입이 항상 동일하게 유지될 것으로 예상된다면:
import { Profile } from '@/types/member' export type CreateMemberProfileRequest = Profile export type FetchMemberProfileResponse = { profile: Profile } export type UpdateMemberProfileRequest = CreateMemberProfileRequest참고: 이 접근법은 API 계약과 내부 모델을 강하게 결합시키므로, 향후 API 변경 시 도메인 모델도 함께 변경해야 할 수 있습니다.
src/components/pages/balanse/trending-section/mockPollCard.tsx (2)
12-17: 타입과 null 체크 불일치
Props인터페이스에서data를TrendingVoteResponse로 정의했지만, Line 17에서!data를 체크하고 있습니다. 타입상data는 항상 존재해야 하므로 null 체크는 불필요하거나, 또는 타입을TrendingVoteResponse | null로 변경해야 합니다.🔎 제안된 수정
옵션 1: null 체크 제거 (data가 항상 존재하는 경우)
function MockPollCard({ data }: Props) { - if (!data) return null - return (옵션 2: 타입에 null 허용 (data가 없을 수 있는 경우)
interface Props { - data: TrendingVoteResponse + data: TrendingVoteResponse | null }
25-27: 불필요한 wrapper div
flex justify-between items-center스타일의 wrapper가 추가되었지만, 내부에 단일 자식 요소만 있어 justify-between이 적용될 대상이 없습니다. 향후 다른 요소가 추가될 예정이 아니라면 불필요합니다.src/api/votes.ts (1)
111-128: 불필요한 try/catch 래퍼
deleteVote와pinVote함수에서 try/catch가 에러를 그대로 다시 throw하고 있어 실질적인 에러 처리가 없습니다. 기존 코드와의 일관성을 위해 유지한 것으로 보이지만, 간결하게 작성할 수 있습니다.🔎 제안된 수정
// 투표 삭제 API -export const deleteVote = async (voteId: number) => { - try { - await authApi.delete(`/votes/${voteId}`) - } catch (error) { - throw error - } -} +export const deleteVote = async (voteId: number): Promise<void> => { + await authApi.delete(`/votes/${voteId}`) +} // 투표 고정 API -export const pinVote = async (voteId: number, pinType: PinType) => { - try { - const response = await authApi.patch(`/votes/${voteId}/pin`, { pinType }) - return response.data - } catch (error) { - throw error - } -} +export const pinVote = async (voteId: number, pinType: PinType): Promise<unknown> => { + const response = await authApi.patch(`/votes/${voteId}/pin`, { pinType }) + return response.data +}src/components/ui/modal/confirmModal.tsx (2)
44-51: DeleteConfirmModal과 버튼 순서 불일치
ConfirmModal은 확인(destructive) → 취소 순서이고,DeleteConfirmModal은 취소 → 삭제(destructive) 순서입니다. 일관성을 위해 동일한 순서를 사용하는 것이 좋습니다. 일반적으로 위험한 액션(destructive)은 오른쪽에 배치하는 것이 관례입니다.🔎 제안된 수정
<ModalFooter> - <Button variant="destructive" onClick={onConfirm}> - 확인 - </Button> <Button variant="secondary" onClick={onClose}> 취소 </Button> + <Button variant="destructive" onClick={onConfirm}> + 확인 + </Button> </ModalFooter>
1-55: DeleteConfirmModal과 코드 중복
ConfirmModal과DeleteConfirmModal은 거의 동일한 구조입니다. 향후 유지보수를 위해 공통 베이스 컴포넌트로 통합하거나, 제네릭 ConfirmModal에 variant prop을 추가하여 "delete" 모드를 지원하는 방식을 고려해 보세요.src/components/pages/balanse/balansePage.tsx (1)
148-165: isRefreshing 패턴 개선을 고려해 보세요.
isRefreshing을 dependency로 사용하여 데이터를 다시 가져오는 패턴은 동작하지만,setIsRefreshing(false)가 useEffect 내부에서 호출되어 cleanup이 보장되지 않을 수 있습니다.🔎 개선 제안
useEffect(() => { const getTrendingVote = async () => { try { setLoading(true) setError(null) const data = await fetchTrendingVotes() setTrendingVote(data) } catch (_) { setError('불러오기 실패') } finally { setLoading(false) + setIsRefreshing(false) } } - getTrendingVote() - setIsRefreshing(false) + if (isRefreshing) { + getTrendingVote() + } - }, [isRefreshing]) + }, [isRefreshing]) // 초기 로드를 위한 별도 useEffect + useEffect(() => { + const getTrendingVote = async () => { + // ... 초기 로드 로직 + } + getTrendingVote() + }, [])또는
refetch함수를 별도로 분리하여 초기 로드와 리프레시에서 재사용하는 것을 권장합니다.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
public/check-circle.svgis excluded by!**/*.svgpublic/letter-x-circle.svgis excluded by!**/*.svg
📒 Files selected for processing (28)
eslint.config.mjssrc/api/member/member.tssrc/api/member/types.tssrc/api/pages/valanse/trendingVoteApi.tssrc/api/votes.tssrc/app/authBootstrap.tsxsrc/app/poll/[id]/page.tsxsrc/app/providers.tsxsrc/components/pages/balanse/balanse-list-section/balanseList.tsxsrc/components/pages/balanse/balanse-list-section/pinMenu.tsxsrc/components/pages/balanse/balansePage.tsxsrc/components/pages/balanse/header.tsxsrc/components/pages/balanse/trending-section/mockPollCard.tsxsrc/components/pages/balanse/trending-section/pinButton.tsxsrc/components/pages/balanse/trending-section/sectionHeader.tsxsrc/components/pages/my/edit/editPage.tsxsrc/components/pages/onboarding/mbtiBottomSheet.tsxsrc/components/pages/onboarding/onboardingPage.tsxsrc/components/pages/poll/_admin/AdminFloatingButton.tsxsrc/components/pages/poll/pinButton.tsxsrc/components/pages/poll/sectionHeader.tsxsrc/components/ui/modal/confirmModal.tsxsrc/components/ui/modal/deleteConfirmModal.tsxsrc/store/slices/memberSlice.tssrc/store/thunks/memberThunks.tssrc/types/balanse/vote.tssrc/types/member/index.tstsconfig.json
💤 Files with no reviewable changes (1)
- src/components/pages/balanse/header.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-18T09:08:44.943Z
Learnt from: Emithen
Repo: ValanSee/ValanSe_Web PR: 43
File: src/components/pages/oauth/kakao/RedirectPage.tsx:29-35
Timestamp: 2025-07-18T09:08:44.943Z
Learning: In the ValanSee project's OAuth flow, the fetchProfileThunk can return null when a user doesn't have a profile yet (new user). The RedirectPage.tsx logic uses this to route new users (profile === null) to onboarding and existing users (profile exists) to main page. This is the intended design for user onboarding flow.
Applied to files:
src/app/authBootstrap.tsxsrc/store/thunks/memberThunks.tssrc/components/pages/onboarding/onboardingPage.tsxsrc/components/pages/my/edit/editPage.tsx
📚 Learning: 2025-07-18T09:08:44.943Z
Learnt from: Emithen
Repo: ValanSee/ValanSe_Web PR: 43
File: src/components/pages/oauth/kakao/RedirectPage.tsx:29-35
Timestamp: 2025-07-18T09:08:44.943Z
Learning: In the ValanSee project, the /member/profile API is designed to return a profile field that can be null. When profile is null, it indicates a new user who should be redirected to the onboarding page. When profile exists, it indicates an existing user who should be redirected to the main page.
Applied to files:
src/api/member/member.ts
🧬 Code graph analysis (20)
src/types/balanse/vote.ts (3)
src/api/comment/statisticsApi.ts (2)
MBTIRatio(26-30)MBTIStatisticsResponse(31-36)src/api/pages/valanse/balanseListapi.ts (1)
VoteResponse(6-14)src/api/comment/mostVotedVoteApi.ts (1)
MostVotedVoteOption(3-7)
src/app/authBootstrap.tsx (5)
src/hooks/utils/useAppDispatch.ts (1)
useAppDispatch(4-4)src/utils/tokenUtils.ts (1)
getAccessToken(22-30)src/store/thunks/memberThunks.ts (1)
fetchProfileThunk(11-21)src/app/authRedirectWatcher.tsx (1)
AuthRedirectWatcher(7-19)src/app/(unauth)/onboarding/loading.tsx (1)
OnboardingLoading(3-5)
src/app/providers.tsx (3)
src/store/store.ts (1)
store(5-10)src/app/authBootstrap.tsx (1)
AuthBootstrap(9-22)src/app/page.tsx (1)
Home(15-44)
src/components/pages/balanse/trending-section/sectionHeader.tsx (1)
src/components/pages/balanse/header.tsx (1)
Header(3-13)
src/api/member/types.ts (1)
src/components/pages/my/myProfileSection.tsx (1)
MyProfileSection(9-103)
src/store/slices/memberSlice.ts (1)
src/types/member/index.ts (1)
Profile(1-9)
src/components/ui/modal/confirmModal.tsx (1)
src/components/ui/modal.tsx (15)
Modal(150-150)ModalHeader(152-152)ModalTitle(153-153)ModalCloseButton(157-157)ModalBody(155-155)ModalDescription(154-154)ModalFooter(156-156)props(48-54)props(11-25)props(118-127)props(32-41)props(74-80)props(87-93)props(100-111)props(61-67)
src/api/votes.ts (3)
src/types/balanse/vote.ts (1)
PinType(24-24)src/api/instance/authApi.ts (1)
authApi(15-20)src/api/pages/valanse/balanseListapi.ts (1)
VoteResponse(6-14)
src/components/pages/poll/sectionHeader.tsx (2)
src/types/balanse/vote.ts (1)
PinType(24-24)src/components/pages/poll/pinButton.tsx (1)
PinButton(14-35)
src/store/thunks/memberThunks.ts (1)
src/components/pages/my/myProfileSection.tsx (1)
MyProfileSection(9-103)
src/app/poll/[id]/page.tsx (7)
src/hooks/utils/useAppSelector.ts (1)
useAppSelector(4-4)src/types/balanse/vote.ts (1)
PinType(24-24)src/api/votes.ts (2)
pinVote(121-128)deleteVote(112-118)src/components/pages/poll/sectionHeader.tsx (1)
SectionHeader(9-15)src/components/ui/modal/confirmModal.tsx (1)
ConfirmModal(23-55)src/components/pages/poll/_admin/AdminFloatingButton.tsx (1)
AdminFloatingButton(8-34)src/components/ui/modal/deleteConfirmModal.tsx (1)
DeleteConfirmModal(23-55)
src/components/ui/modal/deleteConfirmModal.tsx (2)
src/components/ui/modal.tsx (8)
ModalOverlay(151-151)Modal(150-150)ModalHeader(152-152)ModalTitle(153-153)ModalCloseButton(157-157)ModalBody(155-155)ModalDescription(154-154)ModalFooter(156-156)src/components/ui/button.tsx (1)
Button(35-35)
src/components/pages/poll/pinButton.tsx (2)
src/types/balanse/vote.ts (1)
PinType(24-24)src/components/pages/balanse/trending-section/pinButton.tsx (1)
PinButton(14-35)
src/components/pages/balanse/trending-section/pinButton.tsx (2)
src/types/balanse/vote.ts (1)
PinType(24-24)src/components/pages/poll/pinButton.tsx (1)
PinButton(14-35)
src/components/pages/balanse/trending-section/mockPollCard.tsx (2)
src/api/pages/valanse/trendingVoteApi.ts (1)
TrendingVoteResponse(4-18)src/components/pages/balanse/mockPollCard.tsx (3)
MockPollCard(17-82)getData(22-35)setLoading(23-33)
src/api/pages/valanse/trendingVoteApi.ts (2)
src/types/balanse/vote.ts (1)
PinType(24-24)src/api/pages/valanse/trendinVoteApi.ts (1)
fetchTrendingVotes(18-21)
src/components/pages/balanse/balansePage.tsx (8)
src/api/pages/valanse/trendingVoteApi.ts (2)
TrendingVoteResponse(4-18)fetchTrendingVotes(20-23)src/hooks/utils/useAppSelector.ts (1)
useAppSelector(4-4)src/components/_shared/loading.tsx (1)
Loading(11-96)src/api/votes.ts (1)
pinVote(121-128)src/components/pages/balanse/trending-section/sectionHeader.tsx (1)
SectionHeader(3-10)src/components/pages/balanse/trending-section/pinButton.tsx (1)
PinButton(14-35)src/components/pages/poll/pinButton.tsx (1)
PinButton(14-35)src/components/pages/balanse/balanse-list-section/balanseList.tsx (1)
BalanceList(19-60)
src/components/pages/balanse/balanse-list-section/pinMenu.tsx (2)
src/types/balanse/vote.ts (1)
PinType(24-24)src/api/votes.ts (1)
pinVote(121-128)
src/components/pages/my/edit/editPage.tsx (5)
src/hooks/utils/useAppDispatch.ts (1)
useAppDispatch(4-4)src/hooks/utils/useAppSelector.ts (1)
useAppSelector(4-4)src/components/_shared/loading.tsx (1)
Loading(11-96)src/types/member/index.ts (3)
Gender(11-11)Age(13-13)MBTI(20-36)src/hooks/useDebounce.ts (1)
useDebounce(3-19)
src/api/member/member.ts (2)
src/api/member/types.ts (3)
CreateMemberProfileRequest(1-25)FetchMemberProfileResponse(27-53)UpdateMemberProfileRequest(55-55)src/api/instance/authApi.ts (1)
authApi(15-20)
🪛 Biome (2.1.2)
src/components/pages/balanse/balansePage.tsx
[error] 157-157: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
src/components/pages/my/edit/editPage.tsx
[error] 60-60: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 62-63: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 64-64: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 66-66: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 67-67: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 68-68: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 69-69: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 70-70: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
[error] 99-99: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🔇 Additional comments (20)
src/app/providers.tsx (1)
7-13: 통합 구조는 적절하지만 AuthBootstrap의 에러 처리 개선이 필요합니다.Redux Provider 내부에서 AuthBootstrap을 렌더링하는 구조는 올바르며, 앱 시작 시 인증 상태를 복구하는 의도가 명확합니다. 다만 AuthBootstrap 컴포넌트 자체의 에러 처리와 page.tsx와의 중복 로직 문제는 해결이 필요합니다(AuthBootstrap 파일의 리뷰 코멘트 참조).
src/types/balanse/vote.ts (1)
24-24: LGTM! 핀 타입 정의가 명확하고 적절합니다.
PinType을 string literal union으로 정의한 것은 타입 안정성을 보장하면서도 명확한 핀 상태 표현이 가능합니다. AI 요약에 따르면 이 타입은 trending vote API, UI 컴포넌트 등 여러 곳에서 일관되게 사용되고 있어 타입 시스템이 잘 통합되어 있습니다.src/store/thunks/memberThunks.ts (1)
5-5: LGTM!멤버 관련 타입을
@/types/member로 중앙화하는 리팩토링이 잘 적용되었습니다. 임포트 경로 변경만 있고 로직 변경은 없습니다.Also applies to: 8-8
src/components/pages/poll/sectionHeader.tsx (1)
1-15: LGTM!관리자 고정 기능을 위한 섹션 헤더가 명확하게 구현되었습니다.
PinButton과의 통합도 적절하며, 옵셔널 콜백 처리도 올바릅니다.src/components/pages/onboarding/mbtiBottomSheet.tsx (1)
5-5: LGTM!멤버 타입 중앙화에 맞춰
MBTI임포트 경로가 올바르게 업데이트되었습니다. 로직 변경은 없습니다.src/api/pages/valanse/trendingVoteApi.ts (1)
2-2: 타입 확장과 소비자 업데이트가 모두 정상 처리되었습니다.검증 결과,
pinType필드가TrendingVoteResponse타입에 포함되어 있으며, 이를 사용하는 모든 컴포넌트가 적절하게 처리하고 있습니다:
balansePage.tsx: 관리자 고정 기능에서pinType={trendingVote.pinType}으로PinButton에 전달mockPollCard.tsx: 표시 전용 카드로서 불필요한 필드 미사용 ✓src/store/slices/memberSlice.ts (1)
2-2: LGTM! 타입 임포트 경로 변경이 적절합니다.회원 관련 타입을
@/types/member로 중앙화하는 것은 타입 구조 개선에 도움이 됩니다.src/components/pages/poll/_admin/AdminFloatingButton.tsx (1)
1-10: LGTM! 컴포넌트 구조가 적절합니다.클라이언트 컴포넌트로 선언되어 있고, 상태 관리가 명확합니다.
src/components/pages/onboarding/onboardingPage.tsx (2)
5-8: LGTM!타입 임포트 경로를
@/types/member로 중앙화한 변경이 적절합니다.
43-51: LGTM!온보딩에서 생성되는 프로필에
role: 'USER'를 하드코딩하는 것은 적절합니다. 신규 가입 사용자는 항상 일반 사용자로 시작해야 합니다.src/components/pages/balanse/balanse-list-section/balanseList.tsx (1)
14-31: LGTM!Props 타입 정의와 PinMenu 통합이 잘 구현되었습니다.
onPinChange를 옵셔널로 설정하여 재사용성을 확보했습니다.src/app/poll/[id]/page.tsx (1)
208-213: SectionHeader의 pinType null 가능성
pin이 null일 수 있는데SectionHeader에 그대로 전달하고 있습니다. Line 65의 타입 캐스팅 문제와 연관되어 있으므로, 해당 부분을 수정하면 이 문제도 해결됩니다.src/components/ui/modal/deleteConfirmModal.tsx (1)
23-55: LGTM!DeleteConfirmModal 구현이 깔끔합니다. 모달 프리미티브를 잘 활용하고 있으며,
open상태에 따른 조건부 렌더링도 적절합니다.src/types/member/index.ts (3)
1-9: LGTM! Profile 타입에 role 필드가 적절히 추가되었습니다.관리자 권한 기능을 위한
UserRole타입과Profile.role필드 추가가 잘 구현되었습니다.
13-13: 'OVER_FOURTY' 오타 확인 필요
OVER_FOURTY는 영어 철자상OVER_FORTY가 맞습니다. 백엔드 API와 일치시키기 위해 의도적으로 사용한 것인지 확인해 주세요.
38-38: UserRole 타입 정의 적절합니다.관리자 기능 분기에 필요한
UserRole타입이 명확하게 정의되었습니다.src/components/pages/balanse/balanse-list-section/pinMenu.tsx (1)
16-29: LGTM! 외부 클릭 감지 로직이 잘 구현되었습니다.
isOpen상태에 따라 이벤트 리스너를 조건부로 등록하고, cleanup 함수에서 제거하는 패턴이 적절합니다.src/api/member/member.ts (3)
1-6: LGTM! 타입 임포트가 중앙화된 types 모듈로 적절히 변경되었습니다.API 요청/응답 타입을 별도 모듈로 분리하여 관리하는 것은 좋은 구조화입니다.
18-28: fetchMemberProfile 타입 개선이 잘 적용되었습니다.
FetchMemberProfileResponse['profile']반환 타입으로 API 응답 구조와 일치시킨 것이 적절합니다. 학습된 내용대로 profile이 null일 수 있는 경우(신규 사용자)도 타입에서 처리되어야 합니다.
FetchMemberProfileResponse['profile']이 nullable한지 확인이 필요합니다. 신규 사용자의 경우 profile이 null일 수 있다는 학습 내용을 기반으로 타입 정의를 확인하세요.
30-38: updateMemberProfile이 POST 메서드를 사용하고 있으며, createMemberProfile과 동일한 엔드포인트와 메서드를 사용합니다.
updateMemberProfile과createMemberProfile은 동일하게/member/profile엔드포인트에 POST 요청을 보내고 있으며, 요청 타입도 동일합니다(UpdateMemberProfileRequest = CreateMemberProfileRequest). 일반적으로 리소스 업데이트에는PUT또는PATCH메서드를 사용하므로, 백엔드 API가 의도적으로 POST를 사용하는 upsert 패턴인지 확인이 필요합니다. 만약 upsert가 아니라면updateMemberProfile은 적절한 HTTP 메서드(PUT 또는 PATCH)를 사용하도록 수정해야 합니다.
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/pages/my/edit/editPage.tsx (1)
105-124: [심각] 관리자 역할이 손실될 수 있습니다.
refineForm()함수가role: 'USER'를 하드코딩하고 있습니다. 이 PR은 슈퍼 유저/관리자 권한 기능을 구현하는데, 관리자가 자신의 프로필을 수정할 경우 역할이 'USER'로 변경되어 관리자 권한을 잃게 됩니다.기존의
myPageData.role값을 보존하거나, 역할 변경을 별도로 처리해야 합니다.🔎 제안하는 수정안
const refineForm = (): Profile => { if (!mbti) { throw new Error('MBTI is required') } const mbtiIe = mbti[0] as mbtiIe const mbtiTf = mbti[2] as mbtiTf const ageData: Age = age! as Age const genderData: Gender = gender! as Gender return { nickname: nickname as string, gender: genderData, age: ageData, mbtiIe: mbtiIe, mbtiTf: mbtiTf, mbti: mbti, - role: 'USER', + role: myPageData?.role ?? 'USER', } }
♻️ Duplicate comments (1)
src/components/pages/balanse/balansePage.tsx (1)
202-206:handleUnpin의 에러 처리가 여전히 누락되어 있습니다.이전 리뷰에서 지적된 것처럼,
handleUnpin()호출이 await 없이 실행되어 API 실패 여부와 관계없이 모달이 닫히고 리프레시가 트리거됩니다.🔎 수정 제안
onConfirm={() => { - handleUnpin() - setShowConfirmModal(false) - setIsRefreshing(true) + handleUnpin() + .then(() => { + setShowConfirmModal(false) + setIsRefreshing(true) + }) + .catch((error) => { + console.error('고정 해제 실패:', error) + setShowConfirmModal(false) + // TODO: 사용자에게 에러 알림 표시 + }) }}
🧹 Nitpick comments (3)
src/components/pages/my/edit/editPage.tsx (3)
55-62: 상태 초기화의 타입 일관성을 개선하세요.상태 초기화 로직에 타입 불일치가 있습니다:
nickname은string | null타입이지만''(빈 문자열)로 초기화됩니다gender,age,mbti는Gender | null,Age | null,MBTI | null타입이지만myPageData가 undefined일 때undefined가 할당될 수 있습니다 (type assertion으로 인해 숨겨짐)타입 안정성을 위해 초기값을 명시적으로
null로 설정하는 것을 권장합니다.🔎 제안하는 수정안
- const [nickname, setNickname] = useState<string | null>( - myPageData?.nickname || '', - ) - const [gender, setGender] = useState<Gender | null>( - myPageData?.gender as Gender, - ) - const [age, setAge] = useState<Age | null>(myPageData?.age as Age) - const [mbti, setMbti] = useState<MBTI | null>(myPageData?.mbti as MBTI) + const [nickname, setNickname] = useState<string | null>( + myPageData?.nickname ?? null, + ) + const [gender, setGender] = useState<Gender | null>( + myPageData?.gender ?? null, + ) + const [age, setAge] = useState<Age | null>(myPageData?.age ?? null) + const [mbti, setMbti] = useState<MBTI | null>(myPageData?.mbti ?? null)
23-47: 타입 단언을 줄이고 타입 안정성을 개선하세요.여러 곳에서
as Gender,as Age,as MBTI타입 단언을 사용하고 있습니다. 이는 TypeScript의 타입 체크를 우회하여 런타임 오류를 숨길 수 있습니다.
ageMap과genderMap함수에 명시적인 반환 타입을 지정하면 타입 단언을 제거할 수 있습니다.🔎 제안하는 수정안
-const ageMap = (label: string) => { +const ageMap = (label: string): Age => { switch (label) { case '10대': return 'TEN' case '20대': return 'TWENTY' case '30대': return 'THIRTY' case '40대': return 'OVER_FOURTY' default: - return label + throw new Error(`Invalid age label: ${label}`) } } -const genderMap = (label: string) => { +const genderMap = (label: string): Gender => { switch (label) { case '여성': return 'FEMALE' case '남성': return 'MALE' default: - return label + throw new Error(`Invalid gender label: ${label}`) } }이렇게 하면 line 97-99, 219, 235의 타입 단언을 제거할 수 있습니다:
- setGender(myPageData.gender as Gender) - setAge(myPageData.age as Age) + setGender(myPageData.gender) + setAge(myPageData.age) setMbti(myPageData.mbti as MBTI)- onClick={() => setGender(genderMap(option) as Gender)} + onClick={() => setGender(genderMap(option))}- onClick={() => setAge(ageMap(option) as Age)} + onClick={() => setAge(ageMap(option))}Also applies to: 97-99, 219-219, 235-235
92-103: 데이터 패칭 로직을 개선하세요.
useEffect가myPageData가 없을 때마다fetchMypageDataThunk()를 dispatch합니다. 만약 thunk가 실패하거나 null을 반환하면 무한 루프가 발생할 수 있습니다.마운트 시 한 번만 데이터를 가져오도록 개선하는 것을 권장합니다.
🔎 제안하는 수정안
+ useEffect(() => { + if (!myPageData) { + dispatch(fetchMypageDataThunk()) + } + }, []) // 마운트 시에만 실행 + useEffect(() => { console.log('전역 상태 nickname', myPageData?.nickname) console.log('로컬 상태 nickname', nickname) if (myPageData) { setNickname(myPageData.nickname) setGender(myPageData.gender as Gender) setAge(myPageData.age as Age) setMbti(myPageData.mbti as MBTI) - } else { - dispatch(fetchMypageDataThunk()) } }, [myPageData])
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/api/member/types.tssrc/app/poll/[id]/page.tsxsrc/components/pages/balanse/balanse-list-section/pinMenu.tsxsrc/components/pages/balanse/balansePage.tsxsrc/components/pages/my/edit/editPage.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- src/api/member/types.ts
- src/app/poll/[id]/page.tsx
- src/components/pages/balanse/balanse-list-section/pinMenu.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-18T09:08:44.943Z
Learnt from: Emithen
Repo: ValanSee/ValanSe_Web PR: 43
File: src/components/pages/oauth/kakao/RedirectPage.tsx:29-35
Timestamp: 2025-07-18T09:08:44.943Z
Learning: In the ValanSee project's OAuth flow, the fetchProfileThunk can return null when a user doesn't have a profile yet (new user). The RedirectPage.tsx logic uses this to route new users (profile === null) to onboarding and existing users (profile exists) to main page. This is the intended design for user onboarding flow.
Applied to files:
src/components/pages/my/edit/editPage.tsxsrc/components/pages/balanse/balansePage.tsx
🧬 Code graph analysis (2)
src/components/pages/my/edit/editPage.tsx (4)
src/hooks/utils/useAppDispatch.ts (1)
useAppDispatch(4-4)src/hooks/utils/useAppSelector.ts (1)
useAppSelector(4-4)src/types/member/index.ts (3)
Gender(11-11)Age(13-13)MBTI(20-36)src/hooks/useDebounce.ts (1)
useDebounce(3-19)
src/components/pages/balanse/balansePage.tsx (8)
src/api/pages/valanse/trendingVoteApi.ts (2)
TrendingVoteResponse(4-18)fetchTrendingVotes(20-23)src/hooks/utils/useAppSelector.ts (1)
useAppSelector(4-4)src/api/votes.ts (1)
pinVote(121-128)src/components/pages/balanse/trending-section/sectionHeader.tsx (1)
SectionHeader(3-10)src/components/pages/balanse/trending-section/pinButton.tsx (1)
PinButton(14-35)src/components/pages/poll/pinButton.tsx (1)
PinButton(14-35)src/components/ui/modal/confirmModal.tsx (1)
ConfirmModal(23-55)src/components/pages/balanse/balanse-list-section/balanseList.tsx (1)
BalanceList(19-60)
🔇 Additional comments (3)
src/components/pages/balanse/balansePage.tsx (2)
239-242: LGTM!
onPinChange콜백을 통해 핀 상태 변경 시 데이터를 새로고침하는 패턴이 적절합니다.
184-192: 관리자 전용 UI 게이팅이 적절하게 구현되었습니다.
isAdmin조건부 렌더링으로 PinButton을 관리자에게만 노출하는 패턴이 올바릅니다.src/components/pages/my/edit/editPage.tsx (1)
52-68: ✅ React Hooks 규칙 위반이 수정되었습니다.이전 리뷰에서 지적되었던 critical 이슈가 해결되었습니다. 모든 hooks가 이제 조건부 early return 이전에 호출되고 있어 React의 Hooks 규칙을 준수합니다.
Also applies to: 137-139
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
♻️ Duplicate comments (2)
src/app/poll/[id]/page.tsx (2)
66-66: 안전하지 않은 타입 캐스팅 (재언급)이전 리뷰에서 지적된 문제가 여전히 해결되지 않았습니다.
searchParams.get('pin')의 반환값은string | null이지만,as PinType으로 강제 캐스팅하고 있습니다. URL이 조작되거나 파라미터가 없는 경우 예기치 않은 동작이 발생할 수 있습니다.🔎 제안된 수정
- const pin = searchParams.get('pin') as PinType + const pinParam = searchParams.get('pin') + const pin: PinType = pinParam === 'HOT' || pinParam === 'TRENDING' || pinParam === 'NONE' + ? pinParam + : 'NONE'
189-191: 프로필 null 상태 처리 개선 필요 (재언급)이전 리뷰에서 지적된 문제가 여전히 해결되지 않았습니다. 비로그인 사용자는 프로필이 null로 유지되어 영구적으로 로딩 상태에 머물게 됩니다.
AuthBootstrap에서getAccessToken()체크로 인해 프로필 조회를 시도하지 않으므로, 이 페이지에서는 항상<Loading />을 반환합니다.인증 초기화 상태를 추가로 확인하거나, 로그인이 필요한 페이지라면 명시적으로 리다이렉트하는 로직이 필요합니다.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/api/votes.tssrc/app/poll/[id]/page.tsxsrc/components/pages/balanse/balanse-list-section/balanseList.tsxsrc/components/pages/balanse/trending-section/mockPollCard.tsxsrc/types/balanse/vote.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/pages/balanse/trending-section/mockPollCard.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-18T09:08:44.943Z
Learnt from: Emithen
Repo: ValanSee/ValanSe_Web PR: 43
File: src/components/pages/oauth/kakao/RedirectPage.tsx:29-35
Timestamp: 2025-07-18T09:08:44.943Z
Learning: In the ValanSee project's OAuth flow, the fetchProfileThunk can return null when a user doesn't have a profile yet (new user). The RedirectPage.tsx logic uses this to route new users (profile === null) to onboarding and existing users (profile exists) to main page. This is the intended design for user onboarding flow.
Applied to files:
src/app/poll/[id]/page.tsx
📚 Learning: 2025-07-18T09:08:44.943Z
Learnt from: Emithen
Repo: ValanSee/ValanSe_Web PR: 43
File: src/components/pages/oauth/kakao/RedirectPage.tsx:29-35
Timestamp: 2025-07-18T09:08:44.943Z
Learning: In the ValanSee project, the /member/profile API is designed to return a profile field that can be null. When profile is null, it indicates a new user who should be redirected to the onboarding page. When profile exists, it indicates an existing user who should be redirected to the main page.
Applied to files:
src/app/poll/[id]/page.tsx
🧬 Code graph analysis (2)
src/api/votes.ts (2)
src/types/balanse/vote.ts (1)
PinType(25-25)src/api/instance/authApi.ts (1)
authApi(15-20)
src/app/poll/[id]/page.tsx (8)
src/hooks/utils/useAppSelector.ts (1)
useAppSelector(4-4)src/types/balanse/vote.ts (1)
PinType(25-25)src/api/votes.ts (2)
pinVote(121-128)deleteVote(112-118)src/components/_shared/loading.tsx (1)
Loading(11-96)src/components/pages/poll/sectionHeader.tsx (1)
SectionHeader(9-15)src/components/ui/modal/confirmModal.tsx (1)
ConfirmModal(23-55)src/components/pages/poll/_admin/AdminFloatingButton.tsx (1)
AdminFloatingButton(8-34)src/components/ui/modal/deleteConfirmModal.tsx (1)
DeleteConfirmModal(23-55)
🔇 Additional comments (4)
src/types/balanse/vote.ts (1)
25-25: LGTM!핀 상태를 표현하는 타입 정의가 명확하고 타입 안전합니다.
src/api/votes.ts (1)
2-2: LGTM!API 함수들이 기존 코드 스타일과 일관성 있게 구현되었습니다.
PinType타입이 올바르게 적용되었고, RESTful 패턴을 따르고 있습니다.Also applies to: 34-34, 111-128
src/app/poll/[id]/page.tsx (2)
193-202: LGTM!게시글 삭제 로직이 적절한 에러 핸들링과 함께 잘 구현되었습니다.
213-231: LGTM!관리자 전용 UI가
isAdmin체크를 통해 적절하게 조건부 렌더링되고 있으며, 모달 상태 관리도 올바르게 구현되었습니다.Also applies to: 273-280
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
♻️ Duplicate comments (3)
src/app/poll/[id]/page.tsx (3)
66-66: 안전하지 않은 타입 캐스팅 (이전 리뷰에서 지적됨)이 문제는 이전 리뷰에서 이미 지적되었으나 여전히 수정되지 않았습니다.
searchParams.get('pin')의 반환값은string | null이지만as PinType으로 강제 캐스팅하고 있어, URL 조작이나 파라미터 누락 시 예기치 않은 동작이 발생할 수 있습니다.🔎 제안된 수정
- const pin = searchParams.get('pin') as PinType + const pinParam = searchParams.get('pin') + const pin: PinType = pinParam === 'HOT' || pinParam === 'TRENDING' || pinParam === 'NONE' + ? pinParam + : 'NONE'
189-191: 프로필 null 상태 처리 개선 필요 (이전 리뷰에서 지적됨)이 문제는 이전 리뷰에서 광범위하게 분석되었으나 여전히 수정되지 않았습니다. 프로필이 없을 때
<Loading />을 반환하면 비로그인 사용자는 영구적으로 로딩 상태에 머물게 됩니다.AuthBootstrap은getAccessToken()체크로 인해 인증되지 않은 사용자의 프로필을 조회하지 않으므로, Redux의 프로필은 null로 유지됩니다.인증 초기화 상태를 확인하여 "로딩 중"과 "비로그인"을 구분해야 합니다. 인증 초기화가 완료되고 프로필이 여전히 null이면 비로그인 사용자로 처리하고, 관리자 기능만 숨기고 일반 사용자에게 페이지를 표시해야 합니다.
🔎 제안된 수정 방향
인증 초기화 상태를 추적하는 플래그를 Redux에 추가하거나,
profile이 null일 때 관리자 기능만 숨기고 일반 콘텐츠는 표시하도록 수정:- // 관리자 여부 판단 - if (!profile) return <Loading /> - const isAdmin = profile.role === 'ADMIN' + // 관리자 여부 판단 (profile이 null이면 일반 사용자로 간주) + const isAdmin = profile?.role === 'ADMIN'그리고 아래 관리자 전용 UI를 조건부로 렌더링:
- {isAdmin && ( + {profile && isAdmin && (
214-219: 불필요한 타입 캐스팅 (이전 리뷰에서 지적됨)이 문제는 이전 리뷰에서 지적되었으나 여전히 수정되지 않았습니다. 66번 라인에서 안전하게 타입을 검증하면 216번 라인의
as PinType캐스팅은 불필요해집니다.66번 라인의 타입 캐스팅 문제를 먼저 해결하시면 이 캐스팅은 자동으로 제거됩니다.
🧹 Nitpick comments (1)
src/app/poll/[id]/page.tsx (1)
158-166: 고정 해제 후 네비게이션 일관성 검토현재
handleUnpin은/poll/hot으로 리다이렉트하지만, 78-80번 라인의 hot vote 리다이렉트는pin파라미터를 포함합니다. 고정 해제 후에는pin=NONE파라미터를 포함하여 리다이렉트하는 것이 일관성 측면에서 더 명확할 수 있습니다.현재 구현도 동작하지만, 향후 URL 파라미터에 따른 UI 상태 관리를 고려하면 명시적으로
pin=NONE을 전달하는 것이 좋습니다.🔎 일관성 개선 제안
const handleUnpin = async () => { try { await pinVote(Number(id), 'NONE') - router.replace('/poll/hot') + router.replace(`/poll/${id}?source=hot&pin=NONE`) } catch (error) { console.error('Failed to unpin vote:', error) alert('고정 해제에 실패했습니다.') } }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/app/poll/[id]/page.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-18T09:08:44.943Z
Learnt from: Emithen
Repo: ValanSee/ValanSe_Web PR: 43
File: src/components/pages/oauth/kakao/RedirectPage.tsx:29-35
Timestamp: 2025-07-18T09:08:44.943Z
Learning: In the ValanSee project's OAuth flow, the fetchProfileThunk can return null when a user doesn't have a profile yet (new user). The RedirectPage.tsx logic uses this to route new users (profile === null) to onboarding and existing users (profile exists) to main page. This is the intended design for user onboarding flow.
Applied to files:
src/app/poll/[id]/page.tsx
📚 Learning: 2025-07-18T09:08:44.943Z
Learnt from: Emithen
Repo: ValanSee/ValanSe_Web PR: 43
File: src/components/pages/oauth/kakao/RedirectPage.tsx:29-35
Timestamp: 2025-07-18T09:08:44.943Z
Learning: In the ValanSee project, the /member/profile API is designed to return a profile field that can be null. When profile is null, it indicates a new user who should be redirected to the onboarding page. When profile exists, it indicates an existing user who should be redirected to the main page.
Applied to files:
src/app/poll/[id]/page.tsx
🧬 Code graph analysis (1)
src/app/poll/[id]/page.tsx (7)
src/hooks/utils/useAppSelector.ts (1)
useAppSelector(4-4)src/types/balanse/vote.ts (1)
PinType(25-25)src/api/votes.ts (2)
pinVote(121-128)deleteVote(112-118)src/components/pages/poll/sectionHeader.tsx (1)
SectionHeader(9-15)src/components/ui/modal/confirmModal.tsx (1)
ConfirmModal(23-55)src/components/pages/poll/_admin/AdminFloatingButton.tsx (1)
AdminFloatingButton(8-34)src/components/ui/modal/deleteConfirmModal.tsx (1)
DeleteConfirmModal(23-55)
📝 요약
✅ 작업 내용
📷 참고 자료 (스크린샷/링크/GIF 등)
🔬 관련 이슈
☝️ 참고 사항
Summary by CodeRabbit
새 기능
개선
✏️ Tip: You can customize this high-level summary in your review settings.