Skip to content

Conversation

@Emithen
Copy link
Collaborator

@Emithen Emithen commented Dec 15, 2025

📝 요약

  • 슈퍼 유저 관리자 권한 1차 스펙

✅ 작업 내용

  • 댓글 삭제 API 연동
  • 게시물 삭제 API 연동 및 관리자 전용 UI 개발
  • 게시물 고정 API 연동 및 관리자 전용 UI 개발

📷 참고 자료 (스크린샷/링크/GIF 등)

image

🔬 관련 이슈

☝️ 참고 사항

  • 참고하세요~

Summary by CodeRabbit

  • 새 기능

    • 관리자용 투표 관리 UI 추가: 게시글 삭제, 투표 고정(핫/트렌딩)/해제 및 확인 모달
    • 인기 급상승 토픽 섹션과 핀 메뉴/핀 버튼 도입
    • 앱 기동 시 인증·프로필 자동 복구 실행
  • 개선

    • 프로필 편집 UX 강화(닉네임 검증, 역할 정보 포함)
    • 핀 상태 변경 시 리스트 동기화 및 자동 새로고침 지원

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Dec 15, 2025

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:

  • Make your repository public or
  • Upgrade to Pro. A Pro subscription is required to deploy from a private organization repository.

To read more about collaboration on Vercel, click here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 15, 2025

Walkthrough

관리자용 게시물 고정/해제·삭제 UI 및 관련 Votes API 추가, 밸런스 트렌딩 섹션과 Pin 흐름 도입, 멤버 프로필 타입·API 서명 변경, 앱 시작 시 인증 복구(AuthBootstrap) 추가, ESLint·TS 미사용 변수 규칙 비활성화.

Changes

Cohort / File(s) 변경 요약
ESLint & TS 설정
\eslint.config.mjs`, `tsconfig.json``
@typescript-eslint/no-unused-varsno-unused-vars 오버라이드로 비활성화, compilerOptions.noUnusedLocals: false 추가
멤버 API 및 타입
\src/api/member/member.ts`, `src/api/member/types.ts`, `src/types/member/index.ts``
authApi import 경로 조정, CreateMemberProfileRequest/FetchMemberProfileResponse/UpdateMemberProfileRequest 추가, create/fetch/update 시그니처·반환 타입 변경, UserRoleProfile.role 추가
투표 API·타입 및 기능
\src/api/votes.ts`, `src/api/pages/valanse/trendingVoteApi.ts`, `src/types/balanse/vote.ts``
PinType 유니온 추가, Best/Trending 응답에 pinType 포함, deleteVote·pinVote API 함수 추가
앱 초기화(인증 복구)
\src/app/authBootstrap.tsx`, `src/app/providers.tsx``
AuthBootstrap 컴포넌트 추가 및 Providers 내 주입(마운트 시 토큰 확인 후 프로필 페치)
밸런스 페이지·트렌딩 UI
\src/components/pages/balanse/...`, `src/components/pages/balanse/balanse-list-section/pinMenu.tsx`, `src/components/pages/balanse/balanse-list-section/balanseList.tsx`, `src/components/pages/balanse/trending-section/*``
트렌딩 섹션(SectionHeader, PinButton, MockPollCard 리팩토링) 및 PinMenu 추가, BalanceListonPinChange 콜백 추가, 트렌드↔리스트 동기화 로직 도입
폴 상세 페이지·관리자 UI
\src/app/poll/[id]/page.tsx`, `src/components/pages/poll/_admin/AdminFloatingButton.tsx`, `src/components/pages/poll/pinButton.tsx`, `src/components/pages/poll/sectionHeader.tsx``
AdminFloatingButton, 관리자 전용 핀/삭제 UI 및 모달 흐름(핀 상태 처리, 삭제 확인, 네비게이션) 추가
공통 모달 컴포넌트
\src/components/ui/modal/confirmModal.tsx`, `src/components/ui/modal/deleteConfirmModal.tsx``
ConfirmModal 및 DeleteConfirmModal 신규 추가(확인/삭제 다이얼로그)
타입 임포트 경로 갱신·리팩토링
\src/components/pages/my/edit/editPage.tsx`, `src/components/pages/onboarding/...`, `src/store/slices/memberSlice.ts`, `src/store/thunks/memberThunks.ts``
여러 컴포넌트에서 Profile/MBTI 타입 임포트 경로를 @/types/member로 변경, memberSlice.setProfile 페이로드 타입 좁힘, thunks 경로 갱신

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
Loading
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 업데이트
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 깡총깡총, 관리자 버튼 누르니 반짝
핀을 꽂고 뽑아 트렌드 춤을 추네
부팅할 땐 프로필이 살며시 깨어나
리스트는 다시 읽히고, 모달은 묵직히 묻네
당근 축하해요, 배포에 껑충! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Out of Scope Changes check ❓ Inconclusive 대부분의 변경 사항이 관리자 권한 기능 범위 내이나, eslint 규칙 비활성화와 tsconfig 옵션 추가는 이슈 #84와 직접 관련이 없는 일반적인 린트/설정 변경입니다. eslint.config.mjs의 규칙 비활성화와 tsconfig.json의 noUnusedLocals 옵션 추가 사유를 확인하세요. 필요시 별도의 설정 변경 PR로 분리하는 것을 권장합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 "슈퍼 유저 관리자 권한 1차 스펙"으로, 관리자 권한 기능 구현을 명확히 나타내며 주요 변경 사항(삭제/고정 기능)과 일치합니다.
Linked Issues check ✅ Passed 코드 변경 사항이 이슈 #84의 완료된 항목들을 충족합니다: 게시물/댓글 삭제 기능[#84], 삭제 확인 모달[#84], 게시물 고정 API/UI[#84]. 모든 주요 코딩 요구사항이 구현되었습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link
Contributor

vercel bot commented Jan 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
valan-se-web Ready Ready Preview, Comment Jan 3, 2026 11:56am
valanse-origin-repo Ready Ready Preview, Comment Jan 3, 2026 11:56am

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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',
     }

참고: myPageDatarole 필드가 없다면 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 원칙 적용 가능

CreateMemberProfileRequestFetchMemberProfileResponse.profile의 구조가 거의 동일하며, @/types/memberProfile 타입과도 유사합니다.

공통 타입을 추출하여 중복을 줄일 수 있지만, 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 인터페이스에서 dataTrendingVoteResponse로 정의했지만, 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 래퍼

deleteVotepinVote 함수에서 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과 코드 중복

ConfirmModalDeleteConfirmModal은 거의 동일한 구조입니다. 향후 유지보수를 위해 공통 베이스 컴포넌트로 통합하거나, 제네릭 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

📥 Commits

Reviewing files that changed from the base of the PR and between 437994f and 0441e4b.

⛔ Files ignored due to path filters (2)
  • public/check-circle.svg is excluded by !**/*.svg
  • public/letter-x-circle.svg is excluded by !**/*.svg
📒 Files selected for processing (28)
  • eslint.config.mjs
  • src/api/member/member.ts
  • src/api/member/types.ts
  • src/api/pages/valanse/trendingVoteApi.ts
  • src/api/votes.ts
  • src/app/authBootstrap.tsx
  • src/app/poll/[id]/page.tsx
  • src/app/providers.tsx
  • src/components/pages/balanse/balanse-list-section/balanseList.tsx
  • src/components/pages/balanse/balanse-list-section/pinMenu.tsx
  • src/components/pages/balanse/balansePage.tsx
  • src/components/pages/balanse/header.tsx
  • src/components/pages/balanse/trending-section/mockPollCard.tsx
  • src/components/pages/balanse/trending-section/pinButton.tsx
  • src/components/pages/balanse/trending-section/sectionHeader.tsx
  • src/components/pages/my/edit/editPage.tsx
  • src/components/pages/onboarding/mbtiBottomSheet.tsx
  • src/components/pages/onboarding/onboardingPage.tsx
  • src/components/pages/poll/_admin/AdminFloatingButton.tsx
  • src/components/pages/poll/pinButton.tsx
  • src/components/pages/poll/sectionHeader.tsx
  • src/components/ui/modal/confirmModal.tsx
  • src/components/ui/modal/deleteConfirmModal.tsx
  • src/store/slices/memberSlice.ts
  • src/store/thunks/memberThunks.ts
  • src/types/balanse/vote.ts
  • src/types/member/index.ts
  • tsconfig.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.tsx
  • src/store/thunks/memberThunks.ts
  • src/components/pages/onboarding/onboardingPage.tsx
  • src/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과 동일한 엔드포인트와 메서드를 사용합니다.

updateMemberProfilecreateMemberProfile은 동일하게 /member/profile 엔드포인트에 POST 요청을 보내고 있으며, 요청 타입도 동일합니다(UpdateMemberProfileRequest = CreateMemberProfileRequest). 일반적으로 리소스 업데이트에는 PUT 또는 PATCH 메서드를 사용하므로, 백엔드 API가 의도적으로 POST를 사용하는 upsert 패턴인지 확인이 필요합니다. 만약 upsert가 아니라면 updateMemberProfile은 적절한 HTTP 메서드(PUT 또는 PATCH)를 사용하도록 수정해야 합니다.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: 상태 초기화의 타입 일관성을 개선하세요.

상태 초기화 로직에 타입 불일치가 있습니다:

  • nicknamestring | null 타입이지만 '' (빈 문자열)로 초기화됩니다
  • gender, age, mbtiGender | 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의 타입 체크를 우회하여 런타임 오류를 숨길 수 있습니다.

ageMapgenderMap 함수에 명시적인 반환 타입을 지정하면 타입 단언을 제거할 수 있습니다.

🔎 제안하는 수정안
-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: 데이터 패칭 로직을 개선하세요.

useEffectmyPageData가 없을 때마다 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0441e4b and f85f4e7.

📒 Files selected for processing (5)
  • src/api/member/types.ts
  • src/app/poll/[id]/page.tsx
  • src/components/pages/balanse/balanse-list-section/pinMenu.tsx
  • src/components/pages/balanse/balansePage.tsx
  • src/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.tsx
  • src/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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between f85f4e7 and 8d0fcd4.

📒 Files selected for processing (5)
  • src/api/votes.ts
  • src/app/poll/[id]/page.tsx
  • src/components/pages/balanse/balanse-list-section/balanseList.tsx
  • src/components/pages/balanse/trending-section/mockPollCard.tsx
  • src/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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 />을 반환하면 비로그인 사용자는 영구적으로 로딩 상태에 머물게 됩니다. AuthBootstrapgetAccessToken() 체크로 인해 인증되지 않은 사용자의 프로필을 조회하지 않으므로, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d0fcd4 and a904a5f.

📒 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 슈퍼 유저 관리자 권한 1차 스펙

2 participants