From b6f2dd6a048b37af11b9d72ce6ded73d4ff708f7 Mon Sep 17 00:00:00 2001 From: manNomi Date: Fri, 27 Mar 2026 22:29:23 +0900 Subject: [PATCH] feat(web): split server/client role and move admin view switching to fab --- .../src/app/mentor/_ui/MentorClient/index.tsx | 45 ++-------- .../src/app/my/_ui/MyProfileContent/index.tsx | 35 +------- .../app/my/match/_ui/MatchContent/index.tsx | 33 +------ .../GlobalLayout/ui/AIInspectorFab/index.tsx | 89 +++++++++++++++---- .../mentor/MentorApplyCountContent/index.tsx | 4 +- .../hooks/useExpandCardClickHandler.ts | 4 +- apps/web/src/lib/zustand/useAuthStore.ts | 60 ++++++++++--- 7 files changed, 139 insertions(+), 131 deletions(-) diff --git a/apps/web/src/app/mentor/_ui/MentorClient/index.tsx b/apps/web/src/app/mentor/_ui/MentorClient/index.tsx index 1dc682a3..12e00647 100644 --- a/apps/web/src/app/mentor/_ui/MentorClient/index.tsx +++ b/apps/web/src/app/mentor/_ui/MentorClient/index.tsx @@ -6,7 +6,7 @@ import { postReissueToken } from "@/apis/Auth"; import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { UserRole } from "@/types/mentor"; -import { isTokenExpired, tokenParse } from "@/utils/jwtUtils"; +import { isTokenExpired } from "@/utils/jwtUtils"; // 레이지 로드 컴포넌트 const MenteePage = lazy(() => import("./_ui/MenteePage")); @@ -14,13 +14,10 @@ const MentorPage = lazy(() => import("./_ui/MentorPage")); const MentorClient = () => { const router = useRouter(); - const { isLoading, accessToken, isInitialized, refreshStatus, setRefreshStatus } = useAuthStore(); + const { isLoading, accessToken, clientRole, isInitialized, refreshStatus, setRefreshStatus } = useAuthStore(); const [isRefreshing, setIsRefreshing] = useState(false); const hasValidAccessToken = Boolean(accessToken && !isTokenExpired(accessToken)); - // 어드민 전용: 뷰 전환 상태 (true: 멘토 뷰, false: 멘티 뷰) - const [showMentorView, setShowMentorView] = useState(true); - // 토큰 재발급 로직 useEffect(() => { const attemptTokenRefresh = async () => { @@ -62,40 +59,14 @@ const MentorClient = () => { return ; } - const parsedToken = tokenParse(accessToken); - const userRole = parsedToken?.role; - const isMentor = userRole === UserRole.MENTOR || userRole === UserRole.ADMIN; - const isAdmin = userRole === UserRole.ADMIN; - - // 어드민이 아닌 경우 기존 로직대로 - const shouldShowMentorView = isAdmin ? showMentorView : isMentor; + if (!clientRole) { + return ; + } return ( - <> - {/* 어드민 전용 뷰 전환 버튼 */} - {isAdmin && ( -
- - -
- )} - - }>{shouldShowMentorView ? : } - + }> + {clientRole === UserRole.MENTOR ? : } + ); }; diff --git a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx index 35fe6c91..b7759198 100644 --- a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx +++ b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx @@ -1,11 +1,11 @@ "use client"; import Link from "next/link"; -import { useState } from "react"; import { useDeleteUserAccount, usePostLogout } from "@/apis/Auth"; import { type MyInfoResponse, useGetMyInfo } from "@/apis/MyPage"; import LinkedTextWithIcon from "@/components/ui/LinkedTextWithIcon"; import ProfileWithBadge from "@/components/ui/ProfileWithBadge"; +import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { IconLikeFill } from "@/public/svgs/mentor"; import { @@ -25,47 +25,20 @@ const MyProfileContent = () => { const { data: profileData = {} as MyInfoResponse } = useGetMyInfo(); const { mutate: deleteUserAccount } = useDeleteUserAccount(); const { mutate: postLogout } = usePostLogout(); + const clientRole = useAuthStore((state) => state.clientRole); const { nickname, email, profileImageUrl } = profileData; - const isAdmin = profileData.role === UserRole.ADMIN; const isMentor = profileData.role === UserRole.MENTOR || profileData.role === UserRole.ADMIN; - - // 어드민 전용: 뷰 전환 상태 (true: 멘토 뷰, false: 멘티 뷰) - const [showMentorView, setShowMentorView] = useState(true); - - // 어드민이 아닌 경우 기존 로직대로, 어드민인 경우 토글 상태에 따라 - const viewAsMentor = isAdmin ? showMentorView : isMentor; + const viewAsMentor = clientRole ? clientRole === UserRole.MENTOR : isMentor; const university = profileData.role === UserRole.MENTOR || profileData.role === UserRole.ADMIN ? profileData.attendedUniversity : null; const favoriteLocation = - profileData.role === UserRole.MENTEE ? profileData.interestedCountries?.slice(0, 3).join(", ") || "없음" : null; + profileData.role === UserRole.MENTEE ? profileData.interestedCountries.slice(0, 3).join(", ") || "없음" : "없음"; return (
- {/* 어드민 전용 뷰 전환 버튼 */} - {isAdmin && ( -
- - -
- )} -

{nickname}님은

diff --git a/apps/web/src/app/my/match/_ui/MatchContent/index.tsx b/apps/web/src/app/my/match/_ui/MatchContent/index.tsx index 2a84f396..60a6e632 100644 --- a/apps/web/src/app/my/match/_ui/MatchContent/index.tsx +++ b/apps/web/src/app/my/match/_ui/MatchContent/index.tsx @@ -1,51 +1,24 @@ "use client"; -import { useState } from "react"; import { useGetChatRooms } from "@/apis/chat"; import { type MyInfoResponse, useGetMyInfo } from "@/apis/MyPage"; import MentorCard from "@/components/mentor/MentorCard"; import MentorChatCard from "@/components/mentor/MentorChatCard"; +import useAuthStore from "@/lib/zustand/useAuthStore"; import { UserRole } from "@/types/mentor"; const MatchContent = () => { const { data: myInfo = {} as MyInfoResponse } = useGetMyInfo(); const { data: chatRoom = [] } = useGetChatRooms(); + const clientRole = useAuthStore((state) => state.clientRole); - const isAdmin = myInfo.role === UserRole.ADMIN; const isMentor = myInfo.role === UserRole.MENTOR || myInfo.role === UserRole.ADMIN; - - // 어드민 전용: 뷰 전환 상태 (true: 멘토 뷰, false: 멘티 뷰) - const [showMentorView, setShowMentorView] = useState(true); - - // 어드민이 아닌 경우 기존 로직대로, 어드민인 경우 토글 상태에 따라 - const viewAsMentor = isAdmin ? showMentorView : isMentor; + const viewAsMentor = clientRole ? clientRole === UserRole.MENTOR : isMentor; const { nickname } = myInfo; return (

- {/* 어드민 전용 뷰 전환 버튼 */} - {isAdmin && ( -
- - -
- )} -

{nickname ? `${nickname}님의` : "회원님이"}
diff --git a/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx b/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx index ccfaa105..c397ea62 100644 --- a/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx +++ b/apps/web/src/components/layout/GlobalLayout/ui/AIInspectorFab/index.tsx @@ -12,13 +12,14 @@ import { toast } from "@/lib/zustand/useToastStore"; import { UserRole } from "@/types/mentor"; const AIInspectorFab = () => { - const { userRole, isInitialized, accessToken } = useAuthStore(); - const isAdmin = isInitialized && userRole === UserRole.ADMIN; + const { serverRole, clientRole, setClientRole, isInitialized, accessToken } = useAuthStore(); + const isAdmin = isInitialized && serverRole === UserRole.ADMIN; const { isInspecting, setIsInspecting, hoverRect, selection, clearSelection, resetInspector } = useAiInspectorSelection({ isEnabled: isAdmin }); const [instruction, setInstruction] = useState(""); const [isSaving, setIsSaving] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); if (!isAdmin) { return null; @@ -29,6 +30,22 @@ const AIInspectorFab = () => { resetInspector(); }; + const handleToggleInspector = () => { + setIsInspecting((prev) => !prev); + clearSelection(); + setInstruction(""); + }; + + const handleSwitchToMentorView = () => { + setClientRole(UserRole.MENTOR); + toast.success("멘토 UI 보기로 전환되었습니다."); + }; + + const handleSwitchToMenteeView = () => { + setClientRole(UserRole.MENTEE); + toast.success("멘티 UI 보기로 전환되었습니다."); + }; + const handleSave = async () => { if (!selection) { toast.error("먼저 수정할 요소를 선택해주세요."); @@ -84,21 +101,6 @@ const AIInspectorFab = () => { )}

- - {selection && (
@@ -141,6 +143,59 @@ const AIInspectorFab = () => {
)} + + {isMenuOpen && ( +
+ + +
뷰 전환
+
+ + +
+
+ )} + +
); diff --git a/apps/web/src/components/mentor/MentorApplyCountContent/index.tsx b/apps/web/src/components/mentor/MentorApplyCountContent/index.tsx index bdef425e..6ca46e76 100644 --- a/apps/web/src/components/mentor/MentorApplyCountContent/index.tsx +++ b/apps/web/src/components/mentor/MentorApplyCountContent/index.tsx @@ -8,8 +8,8 @@ import { UserRole } from "@/types/mentor"; const MentorApplyCountContent = () => { const router = useRouter(); - const { isInitialized, isAuthenticated, userRole } = useAuthStore(); - const isMentor = userRole === UserRole.MENTOR; + const { isInitialized, isAuthenticated, serverRole } = useAuthStore(); + const isMentor = serverRole === UserRole.MENTOR; const { data: count, isSuccess } = useGetUnconfirmedMentoringCount(isInitialized && isAuthenticated && isMentor); diff --git a/apps/web/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts b/apps/web/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts index fea368a4..34609484 100644 --- a/apps/web/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts +++ b/apps/web/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts @@ -17,8 +17,8 @@ const useExpandCardClickHandler = ({ mentoringId, initChecked = false, }: UseExpandCardClickHandlerProps): UseExpandCardClickHandlerReturn => { - const userRole = useAuthStore((state) => state.userRole); - const isMentor = userRole === UserRole.MENTOR; + const clientRole = useAuthStore((state) => state.clientRole); + const isMentor = clientRole === UserRole.MENTOR; const [isExpanded, setIsExpanded] = useState(false); const [isCheckedState, setIsCheckedState] = useState(initChecked || false); diff --git a/apps/web/src/lib/zustand/useAuthStore.ts b/apps/web/src/lib/zustand/useAuthStore.ts index 17cd5938..3c5e5647 100644 --- a/apps/web/src/lib/zustand/useAuthStore.ts +++ b/apps/web/src/lib/zustand/useAuthStore.ts @@ -20,16 +20,31 @@ const parseUserRoleFromToken = (token: string | null): UserRole | null => { }; type RefreshStatus = "idle" | "refreshing" | "success" | "failed"; +type ClientRole = UserRole.MENTOR | UserRole.MENTEE; + +const resolveClientRole = (serverRole: UserRole | null, currentClientRole: ClientRole | null): ClientRole | null => { + if (serverRole === UserRole.ADMIN) { + return currentClientRole ?? UserRole.MENTOR; + } + + if (serverRole === UserRole.MENTOR || serverRole === UserRole.MENTEE) { + return serverRole; + } + + return null; +}; interface AuthState { accessToken: string | null; - userRole: UserRole | null; + serverRole: UserRole | null; + clientRole: ClientRole | null; isAuthenticated: boolean; isLoading: boolean; isInitialized: boolean; refreshStatus: RefreshStatus; setAccessToken: (token: string) => void; clearAccessToken: () => void; + setClientRole: (role: ClientRole) => void; setLoading: (loading: boolean) => void; setInitialized: (initialized: boolean) => void; setRefreshStatus: (status: RefreshStatus) => void; @@ -39,27 +54,34 @@ const useAuthStore = create()( persist( (set) => ({ accessToken: null, - userRole: null, + serverRole: null, + clientRole: null, isAuthenticated: false, isLoading: false, isInitialized: false, refreshStatus: "idle", setAccessToken: (token) => { - set({ - accessToken: token, - userRole: parseUserRoleFromToken(token), - isAuthenticated: true, - isLoading: false, - isInitialized: true, - refreshStatus: "success", + set((state) => { + const serverRole = parseUserRoleFromToken(token); + + return { + accessToken: token, + serverRole, + clientRole: resolveClientRole(serverRole, state.clientRole), + isAuthenticated: true, + isLoading: false, + isInitialized: true, + refreshStatus: "success", + }; }); }, clearAccessToken: () => { set({ accessToken: null, - userRole: null, + serverRole: null, + clientRole: null, isAuthenticated: false, isLoading: false, isInitialized: true, @@ -67,6 +89,16 @@ const useAuthStore = create()( }); }, + setClientRole: (role) => { + set((state) => { + if (state.serverRole !== UserRole.ADMIN) { + return {}; + } + + return { clientRole: role }; + }); + }, + setLoading: (loading) => { set({ isLoading: loading }); }, @@ -83,6 +115,7 @@ const useAuthStore = create()( name: "auth-storage", partialize: (state) => ({ accessToken: state.accessToken, + clientRole: state.clientRole, isAuthenticated: state.isAuthenticated, }), onRehydrateStorage: () => (state) => { @@ -92,11 +125,14 @@ const useAuthStore = create()( if (!hasValidToken) { state.accessToken = null; - state.userRole = null; + state.serverRole = null; + state.clientRole = null; state.isAuthenticated = false; state.refreshStatus = "idle"; } else { - state.userRole = parseUserRoleFromToken(state.accessToken); + const serverRole = parseUserRoleFromToken(state.accessToken); + state.serverRole = serverRole; + state.clientRole = resolveClientRole(serverRole, state.clientRole); state.isAuthenticated = true; state.refreshStatus = "success"; }