diff --git a/src/app/(service)/home/page.tsx b/src/app/(service)/home/page.tsx index 0e2b8815..4a653e5a 100644 --- a/src/app/(service)/home/page.tsx +++ b/src/app/(service)/home/page.tsx @@ -1,8 +1,9 @@ import { Metadata } from 'next'; +import StartStudyButton from '@/components/home/start-study-button'; import StudyCard from '@/features/study/schedule/ui/study-card'; import { generateMetadata as generateSEOMetadata } from '@/utils/seo'; import Banner from '@/widgets/home/banner'; -import Sidebar from '@/widgets/home/sidebar'; +import FeedbackLink from '@/widgets/home/feedback-link'; export const metadata: Metadata = generateSEOMetadata({ title: '홈 - ZERO-ONE', @@ -15,15 +16,13 @@ export const metadata: Metadata = generateSEOMetadata({ export default async function Home() { return ( -
-
+
+
+ +
- -
); } diff --git a/src/app/(service)/insights/page.tsx b/src/app/(service)/insights/page.tsx index c725e109..c996bc7c 100644 --- a/src/app/(service)/insights/page.tsx +++ b/src/app/(service)/insights/page.tsx @@ -6,10 +6,8 @@ import { fetchArticles, fetchCategories, } from '@/api/strapi/api/fetch-articles'; -import PageContainer from '@/components/layout/page-container'; import { generateMetadata as generateSEOMetadata } from '@/utils/seo'; -import { getServerCookie } from '@/utils/server-cookie'; -import Sidebar from '@/widgets/home/sidebar'; +import Banner from '@/widgets/home/banner'; export const revalidate = 60; @@ -44,9 +42,6 @@ interface BlogPageProps { } export default async function BlogPage({ searchParams }: BlogPageProps) { - const memberIdStr = await getServerCookie('memberId'); - const isLoggedIn = !!memberIdStr; - const { category: selectedCategorySlug } = await searchParams; // 카테고리 목록과 아티클 목록을 병렬로 가져오기 @@ -59,8 +54,13 @@ export default async function BlogPage({ searchParams }: BlogPageProps) { const articles = articlesRes.data ?? []; return ( - +
+ {/* 배너 */} +
+ +
+
ZERO-ONE 인사이트 @@ -144,7 +144,6 @@ export default async function BlogPage({ searchParams }: BlogPageProps) { )}
- {isLoggedIn && } - +
); } diff --git a/src/app/(service)/mentoring/page.tsx b/src/app/(service)/mentoring/page.tsx new file mode 100644 index 00000000..0f665086 --- /dev/null +++ b/src/app/(service)/mentoring/page.tsx @@ -0,0 +1,31 @@ +import { Metadata } from 'next'; +import { Suspense } from 'react'; +import MentoringListPage from '@/components/pages/mentoring-list-page'; +import { generateMetadata as generateSEOMetadata } from '@/utils/seo'; + +export const metadata: Metadata = generateSEOMetadata({ + title: '1:1 멘토링 - ZERO-ONE', + description: + '전문 멘토와 1:1로 만나 맞춤형 상담과 지식을 얻어보세요. 텍스트 답변, 온라인 상담, 대면 컨설팅 등 다양한 방식으로 멘토링을 받을 수 있습니다.', + path: '/mentoring', + keywords: ['1:1 멘토링', '멘토링', '상담', '컨설팅', '전문가 상담'], + canonicalUrl: 'https://www.zeroone.it.kr/mentoring', +}); + +export default function MentoringPage() { + return ( + }> + + + ); +} + +function MentoringListPageSkeleton() { + return ( +
+
+ 로딩 중... +
+
+ ); +} diff --git a/src/components/card/mentor-card.tsx b/src/components/card/mentor-card.tsx new file mode 100644 index 00000000..7c8d8dc3 --- /dev/null +++ b/src/components/card/mentor-card.tsx @@ -0,0 +1,264 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import { + ExternalLink, + Sparkles, + XIcon, + MessageCircle, + Phone, + Users, +} from 'lucide-react'; +import Image from 'next/image'; +import { useState } from 'react'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import UserAvatar from '@/components/ui/avatar'; +import Badge from '@/components/ui/badge'; +import Button from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal'; + +interface Mentor { + id: number; + // name: string; + nickname: string; + imageUrl?: string; + field: string; + keywords: string[]; + description: string; + notionUrl: string; + availableMethods: { + chat: boolean; // 채팅상담 + call: boolean; // 전화/온라인 상담 + offline: boolean; // 대면 컨설팅 + }; +} + +interface MentorCardProps { + mentor: Mentor; +} + +export default function MentorCard({ mentor }: MentorCardProps) { + const [isComingSoonModalOpen, setIsComingSoonModalOpen] = useState(false); + + const handleProfileClick = () => { + // GA4 이벤트 전송 (멘토 프로필 클릭) + sendGTMEvent({ + event: 'mentor_profile_click', + mentor_id: mentor.id, + mentor_nickname: mentor.nickname, + mentor_field: mentor.field, + location: 'mentoring_page', + }); + + if (mentor.notionUrl) { + window.open(mentor.notionUrl, '_blank', 'noopener,noreferrer'); + } + }; + + const handleApplyClick = (e: React.MouseEvent) => { + e.stopPropagation(); + + // GA4 이벤트 전송 + sendGTMEvent({ + event: 'mentoring_help_request_click', + mentor_id: mentor.id, + mentor_nickname: mentor.nickname, + mentor_field: mentor.field, + location: 'mentoring_page', + }); + + setIsComingSoonModalOpen(true); + }; + + return ( +
{ + // 모달이 열려 있으면 카드 클릭 무시 + if (!isComingSoonModalOpen) { + handleProfileClick(); + } + }} + > + {/* 프로필 이미지 영역 */} +
+ {mentor.imageUrl ? ( + {mentor.nickname} + ) : ( +
+ +
+ )} +
+ + {/* 컨텐츠 영역 */} +
+ {/* 뱃지 */} +
+ {mentor.field} +
+ + {/* 제목 */} +
+

+ {mentor.nickname} +

+ +
+ + {/* 설명 */} +

+ {mentor.description} +

+ + {/* 키워드 */} + {mentor.keywords.length > 0 && ( +
+ {mentor.keywords.map((keyword) => ( + + {keyword} + + ))} +
+ )} + + {/* 멘토링 방식 */} +
+
+ + + 채팅상담 + +
+
+ + + 전화/온라인 상담 + +
+
+ + + 대면 컨설팅 + +
+
+ + {/* 멘토링 문의하기 버튼 */} + +
+ + {/* 곧 오픈 예정 모달 */} + + + + e.stopPropagation()}> + + + 멘토링 서비스 + + + + + + + +
+ +
+ +
+

+ 곧 오픈 예정입니다! +

+

+ 1:1 멘토링 서비스를 준비하고 있어요. +
+ 조금만 기다려주시면 멘토와 함께 +
+ 성장할 수 있는 기회를 제공해드릴게요. +

+

+ 곧 만나요! 🚀 +

+
+
+ + + + +
+
+
+
+ ); +} diff --git a/src/components/home/start-study-button.tsx b/src/components/home/start-study-button.tsx new file mode 100644 index 00000000..bff323d9 --- /dev/null +++ b/src/components/home/start-study-button.tsx @@ -0,0 +1,42 @@ +'use client'; + +import Image from 'next/image'; +import { useUserProfileQuery } from '@/entities/user/model/use-user-profile-query'; +import StartStudyModal from '@/features/study/participation/ui/start-study-modal'; +import { useAuth } from '@/hooks/common/use-auth'; + +export default function StartStudyButton() { + const { data: authData } = useAuth(); + const memberId = authData?.memberId ?? null; + const isLoggedIn = !!memberId; + + const { data: userProfile } = useUserProfileQuery(memberId ?? 0); + + if (!isLoggedIn || !memberId || !userProfile || userProfile.studyApplied) { + return null; + } + + return ( + +

+ + CS 스터디를 시작해 보세요! + + + 스터디 신청하기 + +

+ 스터디 시작 버튼 + + } + /> + ); +} diff --git a/src/components/home/study-matching-toggle.tsx b/src/components/home/study-matching-toggle.tsx new file mode 100644 index 00000000..56883428 --- /dev/null +++ b/src/components/home/study-matching-toggle.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ToggleSwitch } from '@/components/ui/toggle'; +import { + usePatchAutoMatchingMutation, + useUserProfileQuery, +} from '@/entities/user/model/use-user-profile-query'; +import { usePhoneVerificationStore } from '@/features/phone-verification/model/store'; +import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal'; +import StartStudyModal from '@/features/study/participation/ui/start-study-modal'; +import { useAuth } from '@/hooks/common/use-auth'; + +export default function StudyMatchingToggle() { + const { data: authData } = useAuth(); + const memberId = authData?.memberId ?? null; + const isLoggedIn = !!memberId; + + const { data: userProfile } = useUserProfileQuery(memberId ?? 0); + const { mutate: patchAutoMatching, isPending } = + usePatchAutoMatchingMutation(); + + const { isVerified, setVerified } = usePhoneVerificationStore(); + const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false); + + const [enabled, setEnabled] = useState(false); + const [isStartStudyModalOpen, setIsStartStudyModalOpen] = useState(false); + + useEffect(() => { + if (userProfile) { + setEnabled(userProfile.autoMatching ?? false); + } + }, [userProfile]); + + if (!isLoggedIn || !memberId || !userProfile) { + return null; + } + + const handleVerificationComplete = (phoneNumber: string) => { + setVerified(phoneNumber); + setIsVerificationModalOpen(false); + + // 인증 완료 후 원래 동작 수행 + if (!userProfile.studyApplied) { + setIsStartStudyModalOpen(true); + } else { + // 스터디 신청한 상태에서 토글을 켤 때 + setEnabled(true); + patchAutoMatching( + { memberId, autoMatching: true }, + { + onError: () => { + setEnabled(false); + }, + }, + ); + } + }; + + const handleToggleChange = (checked: boolean) => { + // 토글을 켤 때만 본인인증 체크 (끌 때는 체크 안 함) + if (checked && !isVerified) { + setIsVerificationModalOpen(true); + + return; + } + + if (!userProfile.studyApplied) { + setIsStartStudyModalOpen(true); + + return; + } + + setEnabled(checked); + patchAutoMatching( + { memberId, autoMatching: checked }, + { + onError: () => { + setEnabled(!checked); + }, + }, + ); + }; + + return ( + <> + + +
+ + 1:1 스터디 매칭 + + +
+ + ); +} diff --git a/src/components/layout/header-nav.tsx b/src/components/layout/header-nav.tsx index 05a6a613..f7b74883 100644 --- a/src/components/layout/header-nav.tsx +++ b/src/components/layout/header-nav.tsx @@ -9,7 +9,8 @@ interface HeaderNavProps { } const NAV_ITEMS = [ - { href: '/home', loginRequired: true, label: '1:1 스터디' }, + { href: '/home', loginRequired: false, label: '1:1 스터디' }, + { href: '/mentoring', loginRequired: false, label: '1:1 멘토링' }, { href: '/group-study', loginRequired: false, label: '그룹스터디' }, { href: '/premium-study', loginRequired: false, label: '멘토스터디' }, { href: '/insights', loginRequired: false, label: '인사이트' }, diff --git a/src/components/layout/page-container.tsx b/src/components/layout/page-container.tsx index 034ba0a2..a5882f23 100644 --- a/src/components/layout/page-container.tsx +++ b/src/components/layout/page-container.tsx @@ -11,7 +11,7 @@ export default function PageContainer({ className, }: PageContainerProps) { return ( -
+
{children}
); diff --git a/src/components/mentoring/mentor-profile-list.tsx b/src/components/mentoring/mentor-profile-list.tsx new file mode 100644 index 00000000..fa435ade --- /dev/null +++ b/src/components/mentoring/mentor-profile-list.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { useMemo } from 'react'; +import MentorCard from '@/components/card/mentor-card'; + +// TODO: 추후 API로 대체 +const MOCK_MENTORS = [ + { + id: 1, + nickname: '이호수', + imageUrl: '', + field: '프론트엔드 개발', + keywords: ['React', 'TypeScript', 'Next.js'], + description: '5년차 프론트엔드 개발자로, React와 Next.js 전문가입니다.', + notionUrl: + 'https://gaan.notion.site/ZERO-ONE-2f7fbb391d7980a593ddc58566d4d305?source=copy_link', + availableMethods: { + chat: true, // 채팅상담 + call: true, // 전화/온라인 상담 + offline: false, // 대면 컨설팅 + }, + }, + { + id: 2, + nickname: '김용휘', + imageUrl: '', + field: '백엔드 개발', + keywords: ['Spring', 'Java', 'AWS'], + description: '7년차 백엔드 개발자로, 마이크로서비스 아키텍처 전문가입니다.', + notionUrl: + 'https://gaan.notion.site/ZERO-ONE-2f7fbb391d798007b830c022307b3a4d?source=copy_link', + availableMethods: { + chat: true, + call: false, + offline: true, + }, + }, + { + id: 3, + nickname: '윤동주', + imageUrl: '', + field: '데이터 분석', + keywords: ['Python', 'SQL', 'Machine Learning'], + description: + '데이터 분석 및 머신러닝 전문가로, 비즈니스 인사이트 도출에 강합니다.', + notionUrl: 'https://notion.so/mentor3', + availableMethods: { + chat: true, + call: true, + offline: true, + }, + }, +]; + +export default function MentorProfileList() { + const mentors = useMemo(() => MOCK_MENTORS, []); + + if (mentors.length === 0) { + return ( +
+

+ 등록된 멘토가 없습니다. +

+
+ ); + } + + return ( +
+ {mentors.map((mentor) => ( + + ))} +
+ ); +} diff --git a/src/components/pages/group-study-list-page.tsx b/src/components/pages/group-study-list-page.tsx index 29ac5cc9..58c8d477 100644 --- a/src/components/pages/group-study-list-page.tsx +++ b/src/components/pages/group-study-list-page.tsx @@ -1,6 +1,7 @@ 'use client'; import { Plus } from 'lucide-react'; +import dynamic from 'next/dynamic'; import { useRouter, useSearchParams } from 'next/navigation'; import { useCallback, useMemo, useState } from 'react'; import type { @@ -20,6 +21,11 @@ import GroupStudyFormModal from '../../features/study/group/ui/group-study-form- import GroupStudyPagination from '../../features/study/group/ui/group-study-pagination'; import GroupStudyList from '../lists/group-study-list'; +// Carousel이 클라이언트 전용이므로 dynamic import로 로드 +const Banner = dynamic(() => import('@/widgets/home/banner'), { + ssr: false, +}); + const PAGE_SIZE = 15; export default function GroupStudyListPage() { @@ -146,7 +152,12 @@ export default function GroupStudyListPage() { } return ( -
+
+ {/* 배너 */} +
+ +
+ {/* 헤더 */}

diff --git a/src/components/pages/mentoring-list-page.tsx b/src/components/pages/mentoring-list-page.tsx new file mode 100644 index 00000000..f34ec149 --- /dev/null +++ b/src/components/pages/mentoring-list-page.tsx @@ -0,0 +1,108 @@ +'use client'; + +import { sendGTMEvent } from '@next/third-parties/google'; +import { Plus, Sparkles, X } from 'lucide-react'; +import dynamic from 'next/dynamic'; +import { useState } from 'react'; +import MentorProfileList from '@/components/mentoring/mentor-profile-list'; +import Button from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal'; +import { useAuth } from '@/hooks/common/use-auth'; + +// Carousel이 클라이언트 전용이므로 dynamic import로 로드 +const Banner = dynamic(() => import('@/widgets/home/banner'), { + ssr: false, +}); + +export default function MentoringListPage() { + const { isAuthenticated } = useAuth(); + const [isComingSoonModalOpen, setIsComingSoonModalOpen] = useState(false); + + return ( +
+ {/* 배너 */} +
+ +
+ + {/* 헤더 */} +
+

1:1 멘토링

+ +
+ + {/* 멘토 프로필 리스트 */} + + + {/* 곧 오픈 예정 모달 */} + + + + + + + 멘토 등록 + + + + + + + +
+ +
+ +
+

+ 곧 오픈 예정입니다! +

+

+ 멘토 등록 기능을 준비하고 있어요. +
+ 조금만 기다려주시면 멘토로 등록하여 +
+ 멘티들에게 지식을 나눌 수 있어요. +

+

+ 곧 만나요! 🚀 +

+
+
+ + + + +
+
+
+
+ ); +} diff --git a/src/components/pages/premium-study-list-page.tsx b/src/components/pages/premium-study-list-page.tsx index d182665d..0d9ed711 100644 --- a/src/components/pages/premium-study-list-page.tsx +++ b/src/components/pages/premium-study-list-page.tsx @@ -1,6 +1,7 @@ 'use client'; import { Plus } from 'lucide-react'; +import dynamic from 'next/dynamic'; import { useRouter, useSearchParams } from 'next/navigation'; import { useCallback, useMemo, useState } from 'react'; import type { @@ -20,6 +21,11 @@ import GroupStudyFormModal from '@/features/study/group/ui/group-study-form-moda import { useAuth } from '@/hooks/common/use-auth'; import { useGetStudies } from '@/hooks/queries/study-query'; +// Carousel이 클라이언트 전용이므로 dynamic import로 로드 +const Banner = dynamic(() => import('@/widgets/home/banner'), { + ssr: false, +}); + const PAGE_SIZE = 15; export default function PremiumStudyListPage() { @@ -146,7 +152,12 @@ export default function PremiumStudyListPage() { } return ( -
+
+ {/* 배너 */} +
+ +
+ {/* 헤더 */}

diff --git a/src/features/study/participation/ui/reservation-list.tsx b/src/features/study/participation/ui/reservation-list.tsx index 8b610e40..061e5fa8 100644 --- a/src/features/study/participation/ui/reservation-list.tsx +++ b/src/features/study/participation/ui/reservation-list.tsx @@ -7,6 +7,8 @@ import { useUserProfileQuery, } from '@/entities/user/model/use-user-profile-query'; import ProfileDefault from '@/entities/user/ui/icon/profile-default.svg'; +import { usePhoneVerificationStore } from '@/features/phone-verification/model/store'; +import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal'; import ReservationCard from '@/features/study/participation/ui/reservation-user-card'; import StartStudyModal from '@/features/study/participation/ui/start-study-modal'; import { useAuth } from '@/hooks/common/use-auth'; @@ -33,6 +35,9 @@ export default function ReservationList({ const { data: userProfile } = useUserProfileQuery(memberId ?? 0); const autoMatching = userProfile?.autoMatching ?? false; + const { isVerified, setVerified } = usePhoneVerificationStore(); + const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false); + const firstMemberId = useMemo( () => (autoMatching && memberId !== null ? memberId : null), [autoMatching, memberId], @@ -69,9 +74,28 @@ export default function ReservationList({ const items = data?.items ?? []; const studyApplied = userProfile?.studyApplied ?? false; + const handleVerificationComplete = (phoneNumber: string) => { + setVerified(phoneNumber); + setIsVerificationModalOpen(false); + + // 인증 완료 후 원래 동작 수행 + if (studyApplied) { + patchAutoMatching({ memberId: memberId!, autoMatching: true }); + } else { + setIsModalOpen(true); + } + }; + const handleApplyClick = () => { if (!memberId) return; + // 본인인증이 안되어 있으면 본인인증 모달 먼저 열기 + if (!isVerified) { + setIsVerificationModalOpen(true); + + return; + } + if (studyApplied) { if (isPending) return; patchAutoMatching({ memberId, autoMatching: true }); @@ -143,11 +167,19 @@ export default function ReservationList({

{memberId && ( - + <> + + + )}
); diff --git a/src/features/study/participation/ui/start-study-modal.tsx b/src/features/study/participation/ui/start-study-modal.tsx index 50ef2c7e..19d9b940 100644 --- a/src/features/study/participation/ui/start-study-modal.tsx +++ b/src/features/study/participation/ui/start-study-modal.tsx @@ -25,7 +25,6 @@ import { import { usePhoneVerificationStore } from '@/features/phone-verification/model/store'; import PhoneVerificationModal from '@/features/phone-verification/ui/phone-verification-modal'; -import { studySteps } from '@/features/study/participation/const/participation-const'; import { StartStudyFormSchema, @@ -202,7 +201,6 @@ function StartStudyForm({ const { mutate: updateProfileInfo } = useUpdateUserProfileInfoMutation(memberId); - const { isVerified, phoneNumber } = usePhoneVerificationStore(); const { data: profile } = useUserProfileQuery(memberId); const methods = useForm({ @@ -211,7 +209,7 @@ function StartStudyForm({ defaultValues: buildStartStudyDefaultValues(profile), }); - const { handleSubmit, setValue, reset } = methods; + const { handleSubmit, reset } = methods; // 프로필 정보가 로드되면 폼 기본값 업데이트 useEffect(() => { diff --git a/src/features/study/schedule/model/use-schedule-query.ts b/src/features/study/schedule/model/use-schedule-query.ts index 68c9d28a..aa8bfc4f 100644 --- a/src/features/study/schedule/model/use-schedule-query.ts +++ b/src/features/study/schedule/model/use-schedule-query.ts @@ -13,12 +13,12 @@ import { } from '@/features/study/schedule/api/schedule-types'; // 스터디 주간 참여 유무 확인 query -export const useWeeklyParticipation = (params: string) => { +export const useWeeklyParticipation = (params: string, enabled: boolean) => { return useQuery({ queryKey: ['weeklyParticipation', params], queryFn: () => getWeeklyParticipation(params), staleTime: 60 * 1000, - enabled: !!params, + enabled: !!params && enabled, }); }; diff --git a/src/features/study/schedule/ui/study-card.tsx b/src/features/study/schedule/ui/study-card.tsx index e38ea184..f0afcf94 100644 --- a/src/features/study/schedule/ui/study-card.tsx +++ b/src/features/study/schedule/ui/study-card.tsx @@ -9,6 +9,7 @@ import { } from '@/features/study/schedule/model/use-schedule-query'; import DateSelector from '@/features/study/schedule/ui/data-selector'; import TodayStudyCard from '@/features/study/schedule/ui/today-study-card'; +import { useAuth } from '@/hooks/common/use-auth'; import { formatKoreaYMD, getKoreaDate, @@ -65,9 +66,19 @@ export default function StudyCard() { const studyDate = formatKoreaYMD(selectedDate); + // 로그인 여부 확인 + const { data: authData } = useAuth(); + const isLoggedIn = !!authData?.memberId; + + // 공개 API const { data: status } = useStudyStatusQuery(); - const { data: participationData } = useWeeklyParticipation(studyDate); + // 인증 API (로그인 한 사용자만 호출) + const { data: participationData } = useWeeklyParticipation( + studyDate, + isLoggedIn, + ); + const isParticipate = participationData?.isParticipate ?? false; const displayMonday = useMemo( diff --git a/src/middleware.ts b/src/middleware.ts index 0c49ed31..4577f3cf 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -160,7 +160,7 @@ export const config = { matcher: [ '/', '/login', - '/home', + // '/home', // 공개 페이지로 변경하여 제거 '/my-page', '/payment-management', '/settlement-management', diff --git a/src/widgets/home/header.tsx b/src/widgets/home/header.tsx index 04413abc..ad8dc964 100644 --- a/src/widgets/home/header.tsx +++ b/src/widgets/home/header.tsx @@ -1,5 +1,6 @@ import Image from 'next/image'; import Link from 'next/link'; +import StudyMatchingToggle from '@/components/home/study-matching-toggle'; import HeaderNav from '@/components/layout/header-nav'; import NotificationDropdown from '@/components/modals/notification-dropdown'; import Button from '@/components/ui/button'; @@ -46,7 +47,12 @@ export default async function Header() { - {accessTokenStr && } + {accessTokenStr && ( +
+ + +
+ )}
{isLoggedIn ? ( diff --git a/src/widgets/home/sidebar.tsx b/src/widgets/home/sidebar.tsx index 1323a489..4577a83e 100644 --- a/src/widgets/home/sidebar.tsx +++ b/src/widgets/home/sidebar.tsx @@ -3,6 +3,7 @@ import { getUserProfileInServer } from '@/entities/user/api/get-user-profile.ser import MyProfileCard from '@/entities/user/ui/my-profile-card'; import StartStudyModal from '@/features/study/participation/ui/start-study-modal'; import { getServerCookie } from '@/utils/server-cookie'; +import { isNumeric } from '@/utils/validation'; import Calendar from '@/widgets/home/calendar'; import FeedbackLink from '@/widgets/home/feedback-link'; import TodoList from '@/widgets/home/todo-list'; @@ -11,7 +12,17 @@ export default async function Sidebar() { const memberIdStr = await getServerCookie('memberId'); const memberId = Number(memberIdStr); - const userProfile = await getUserProfileInServer(memberId); + // 비회원 접근시 사이드바 빈 페이지 반환 + let userProfile = null; + try { + if (!memberIdStr || !isNumeric(memberIdStr)) { + return null; + } + + userProfile = await getUserProfileInServer(memberId); + } catch (error) { + console.error(error); + } return (