+
{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 멘토링
+ }
+ iconPosition="left"
+ disabled={!isAuthenticated}
+ onClick={() => {
+ // GA4 이벤트 전송
+ sendGTMEvent({
+ event: 'mentor_register_click',
+ location: 'mentoring_page',
+ is_authenticated: isAuthenticated,
+ });
+
+ setIsComingSoonModalOpen(true);
+ }}
+ >
+ 멘토 등록하기
+
+
+
+ {/* 멘토 프로필 리스트 */}
+
+
+ {/* 곧 오픈 예정 모달 */}
+
+
+
+
+
+
+ 멘토 등록
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 곧 오픈 예정입니다!
+
+
+ 멘토 등록 기능을 준비하고 있어요.
+
+ 조금만 기다려주시면 멘토로 등록하여
+
+ 멘티들에게 지식을 나눌 수 있어요.
+
+
+ 곧 만나요! 🚀
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
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 (