Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 8 additions & 37 deletions apps/web/src/app/mentor/_ui/MentorClient/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,18 @@ 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"));
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<boolean>(true);

// 토큰 재발급 로직
useEffect(() => {
const attemptTokenRefresh = async () => {
Expand Down Expand Up @@ -62,40 +59,14 @@ const MentorClient = () => {
return <CloudSpinnerPage />;
}

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 <CloudSpinnerPage />;
}

return (
<>
{/* 어드민 전용 뷰 전환 버튼 */}
{isAdmin && (
<div className="mb-4 flex gap-2">
<button
onClick={() => setShowMentorView(true)}
className={`flex-1 rounded-lg px-4 py-2.5 transition-colors typo-sb-9 ${
showMentorView ? "bg-primary text-white" : "border border-k-200 bg-white text-k-600 hover:bg-k-50"
}`}
>
멘토 페이지 보기
</button>
<button
onClick={() => setShowMentorView(false)}
className={`flex-1 rounded-lg px-4 py-2.5 transition-colors typo-sb-9 ${
!showMentorView ? "bg-primary text-white" : "border border-k-200 bg-white text-k-600 hover:bg-k-50"
}`}
>
멘티 페이지 보기
</button>
</div>
)}

<Suspense fallback={<CloudSpinnerPage />}>{shouldShowMentorView ? <MentorPage /> : <MenteePage />}</Suspense>
</>
<Suspense fallback={<CloudSpinnerPage />}>
{clientRole === UserRole.MENTOR ? <MentorPage /> : <MenteePage />}
</Suspense>
);
};

Expand Down
35 changes: 4 additions & 31 deletions apps/web/src/app/my/_ui/MyProfileContent/index.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<boolean>(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 (
<div className="px-5 py-2">
{/* 어드민 전용 뷰 전환 버튼 */}
{isAdmin && (
<div className="mb-4 flex gap-2">
<button
onClick={() => setShowMentorView(true)}
className={`flex-1 rounded-lg px-4 py-2.5 transition-colors typo-sb-9 ${
showMentorView ? "bg-primary text-white" : "border border-k-200 bg-white text-k-600 hover:bg-k-50"
}`}
>
멘토 뷰
</button>
<button
onClick={() => setShowMentorView(false)}
className={`flex-1 rounded-lg px-4 py-2.5 transition-colors typo-sb-9 ${
!showMentorView ? "bg-primary text-white" : "border border-k-200 bg-white text-k-600 hover:bg-k-50"
}`}
>
멘티 뷰
</button>
</div>
)}

<div className="mb-4 text-start text-k-700 typo-sb-5">
<p>{nickname}님은</p>
<p>
Expand Down
33 changes: 3 additions & 30 deletions apps/web/src/app/my/match/_ui/MatchContent/index.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(true);

// 어드민이 아닌 경우 기존 로직대로, 어드민인 경우 토글 상태에 따라
const viewAsMentor = isAdmin ? showMentorView : isMentor;
const viewAsMentor = clientRole ? clientRole === UserRole.MENTOR : isMentor;

const { nickname } = myInfo;

return (
<div className="flex h-full flex-col px-5">
{/* 어드민 전용 뷰 전환 버튼 */}
{isAdmin && (
<div className="mb-4 flex gap-2">
<button
onClick={() => setShowMentorView(true)}
className={`flex-1 rounded-lg px-4 py-2.5 transition-colors typo-sb-9 ${
showMentorView ? "bg-primary text-white" : "border border-k-200 bg-white text-k-600 hover:bg-k-50"
}`}
>
멘토 뷰
</button>
<button
onClick={() => setShowMentorView(false)}
className={`flex-1 rounded-lg px-4 py-2.5 transition-colors typo-sb-9 ${
!showMentorView ? "bg-primary text-white" : "border border-k-200 bg-white text-k-600 hover:bg-k-50"
}`}
>
멘티 뷰
</button>
</div>
)}

<p className="font-pretendard text-k-700 typo-sb-4">
{nickname ? `${nickname}님의` : "회원님이"}
<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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("먼저 수정할 요소를 선택해주세요.");
Expand Down Expand Up @@ -84,21 +101,6 @@ const AIInspectorFab = () => {
)}

<div data-ai-inspector-ui="true" className="fixed bottom-20 left-4 z-[100] flex flex-col gap-2">
<button
type="button"
onClick={() => {
setIsInspecting((prev) => !prev);
clearSelection();
setInstruction("");
}}
className={`flex h-12 w-12 items-center justify-center rounded-full text-white shadow-lg transition ${
isInspecting ? "bg-secondary hover:bg-secondary-800" : "bg-primary hover:bg-primary-1"
}`}
aria-label="AI 인스펙터"
>
{isInspecting ? <Target size={20} /> : <Bot size={20} />}
</button>

{selection && (
<div className="w-80 rounded-xl border border-k-100 bg-white p-3 shadow-2xl">
<div className="mb-2 flex items-center justify-between">
Expand Down Expand Up @@ -141,6 +143,59 @@ const AIInspectorFab = () => {
</div>
</div>
)}

{isMenuOpen && (
<div className="w-56 rounded-xl border border-k-100 bg-white p-3 shadow-2xl">
<button
type="button"
onClick={handleToggleInspector}
className={`w-full rounded-md px-3 py-2 text-left typo-medium-4 transition ${
isInspecting ? "bg-secondary-50 text-secondary-800" : "bg-k-50 text-k-700 hover:bg-k-100"
}`}
>
{isInspecting ? "인스펙터 끄기" : "인스펙터 켜기"}
</button>

<div className="mt-3 text-k-500 typo-medium-4">뷰 전환</div>
<div className="mt-2 grid grid-cols-2 gap-2">
<button
type="button"
onClick={handleSwitchToMentorView}
className={`rounded-md px-2 py-2 typo-medium-4 transition ${
clientRole === UserRole.MENTOR
? "bg-primary text-white"
: "border border-k-200 bg-white text-k-700 hover:bg-k-50"
}`}
>
멘토 UI
</button>
<button
type="button"
onClick={handleSwitchToMenteeView}
className={`rounded-md px-2 py-2 typo-medium-4 transition ${
clientRole === UserRole.MENTEE
? "bg-primary text-white"
: "border border-k-200 bg-white text-k-700 hover:bg-k-50"
}`}
>
멘티 UI
</button>
</div>
</div>
)}

<button
type="button"
onClick={() => {
setIsMenuOpen((prev) => !prev);
}}
className={`flex h-12 w-12 items-center justify-center rounded-full text-white shadow-lg transition ${
isMenuOpen ? "bg-secondary hover:bg-secondary-800" : "bg-primary hover:bg-primary-1"
}`}
aria-label="관리자 메뉴"
>
{isMenuOpen ? <X size={20} /> : isInspecting ? <Target size={20} /> : <Bot size={20} />}
</button>
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +20 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

clientRolenull일 때의 엣지 케이스

clientRolenull인 경우 (초기화 중 또는 토큰 만료 시):

  • isMentor = null === UserRole.MENTORfalse
  • 멘토 사용자가 잘못된 API(patchMenteeCheckMentorings)를 호출할 수 있습니다

이 훅을 사용하는 MentorExpandChatCard는 인증된 페이지 내에서 렌더링되지만, 방어적 코딩으로 clientRolenull일 때 API 호출을 건너뛰는 것이 안전합니다.

🛡️ 방어적 가드 추가 제안
 const handleExpandClick = () => {
   setIsExpanded(!isExpanded);
   if (!isCheckedState) {
     setIsCheckedState(true);
   }
-  if (!isCheckedState) {
+  if (!isCheckedState && clientRole) {
     if (isMentor) {
       patchCheckMentorings({ checkedMentoringIds: [mentoringId] });
     } else {
       patchMenteeCheckMentorings({ checkedMentoringIds: [mentoringId] });
     }
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts`
around lines 20 - 21, The hook useExpandCardClickHandler currently computes
isMentor from clientRole which can be null; add a defensive guard so that when
clientRole is null you do not proceed to mentor-only logic or call
patchMenteeCheckMentorings. Specifically, update the handler in
useExpandCardClickHandler to check if clientRole == null (or undefined) and
short-circuit/return (or skip mentor branch) before evaluating isMentor or
invoking patchMenteeCheckMentorings, ensuring mentor-only API calls run only
when clientRole is a non-null UserRole value.


const [isExpanded, setIsExpanded] = useState<boolean>(false);
const [isCheckedState, setIsCheckedState] = useState<boolean>(initChecked || false);
Expand Down
Loading
Loading