From bb5d7bbe95ec3a66641a074868c2f3dd1f650bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Thu, 29 Jan 2026 20:20:45 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat=20:=201:1=20=EB=A9=98=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 그룹스터디/멘토스터디 구조 컨벤션 맞춤 --- src/app/(service)/mentoring/page.tsx | 31 +++ src/components/card/mentor-card.tsx | 249 ++++++++++++++++++ src/components/layout/header-nav.tsx | 1 + .../mentoring/mentor-profile-list.tsx | 79 ++++++ src/components/pages/mentoring-list-page.tsx | 90 +++++++ 5 files changed, 450 insertions(+) create mode 100644 src/app/(service)/mentoring/page.tsx create mode 100644 src/components/card/mentor-card.tsx create mode 100644 src/components/mentoring/mentor-profile-list.tsx create mode 100644 src/components/pages/mentoring-list-page.tsx diff --git a/src/app/(service)/mentoring/page.tsx b/src/app/(service)/mentoring/page.tsx new file mode 100644 index 00000000..80b74fe3 --- /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 ( +
+
+ 로딩 중... +
+
+ ); +} \ No newline at end of file diff --git a/src/components/card/mentor-card.tsx b/src/components/card/mentor-card.tsx new file mode 100644 index 00000000..34c864fb --- /dev/null +++ b/src/components/card/mentor-card.tsx @@ -0,0 +1,249 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; +import { + ExternalLink, + Sparkles, + XIcon, + MessageCircle, + Phone, + Users, +} from 'lucide-react'; +import UserAvatar from '@/components/ui/avatar'; +import Button from '@/components/ui/button'; +import Badge from '@/components/ui/badge'; +import { Modal } from '@/components/ui/modal'; +import { cn } from '@/components/ui/(shadcn)/lib/utils'; + +interface Mentor { + id: number; + name: string; + nickname: string; + imageUrl?: string; + field: string; + keywords: string[]; + description: string; + notionUrl: string; + googleFormUrl: 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 = () => { + if (mentor.notionUrl) { + window.open(mentor.notionUrl, '_blank', 'noopener,noreferrer'); + } + }; + + const handleApplyClick = (e: React.MouseEvent) => { + e.stopPropagation(); + 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/layout/header-nav.tsx b/src/components/layout/header-nav.tsx index 05a6a613..7c48f3d6 100644 --- a/src/components/layout/header-nav.tsx +++ b/src/components/layout/header-nav.tsx @@ -10,6 +10,7 @@ interface HeaderNavProps { const NAV_ITEMS = [ { href: '/home', loginRequired: true, 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/mentoring/mentor-profile-list.tsx b/src/components/mentoring/mentor-profile-list.tsx new file mode 100644 index 00000000..2c637c57 --- /dev/null +++ b/src/components/mentoring/mentor-profile-list.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useMemo } from 'react'; +import MentorCard from '@/components/card/mentor-card'; + +// TODO: 추후 API로 대체 +const MOCK_MENTORS = [ + { + id: 1, + name: '김개발', + nickname: '개발자킴', + imageUrl: '', + field: '프론트엔드 개발', + keywords: ['React', 'TypeScript', 'Next.js'], + description: '5년차 프론트엔드 개발자로, React와 Next.js 전문가입니다.', + notionUrl: 'https://notion.so/mentor1', + googleFormUrl: 'https://forms.gle/mentor1', + availableMethods: { + chat: true, // 채팅상담 + call: true, // 전화/온라인 상담 + offline: false, // 대면 컨설팅 + }, + }, + { + id: 2, + name: '이백엔드', + nickname: '백엔드리', + imageUrl: '', + field: '백엔드 개발', + keywords: ['Spring', 'Java', 'AWS'], + description: '7년차 백엔드 개발자로, 마이크로서비스 아키텍처 전문가입니다.', + notionUrl: 'https://notion.so/mentor2', + googleFormUrl: 'https://forms.gle/mentor2', + availableMethods: { + chat: true, + call: false, + offline: true, + }, + }, + { + id: 3, + name: '박데이터', + nickname: '데이터박', + imageUrl: '', + field: '데이터 분석', + keywords: ['Python', 'SQL', 'Machine Learning'], + description: '데이터 분석 및 머신러닝 전문가로, 비즈니스 인사이트 도출에 강합니다.', + notionUrl: 'https://notion.so/mentor3', + googleFormUrl: 'https://forms.gle/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/mentoring-list-page.tsx b/src/components/pages/mentoring-list-page.tsx new file mode 100644 index 00000000..bc8d93d1 --- /dev/null +++ b/src/components/pages/mentoring-list-page.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useState } from 'react'; +import { Plus, Sparkles, X } from 'lucide-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'; + +export default function MentoringListPage() { + const { isAuthenticated } = useAuth(); + const [isComingSoonModalOpen, setIsComingSoonModalOpen] = useState(false); + + return ( +
+ {/* 헤더 */} +
+

+ 1:1 멘토링 +

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

+ 곧 오픈 예정입니다! +

+

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

+

+ 곧 만나요! 🚀 +

+
+
+ + + + +
+
+
+
+ ); +} + From c55b3a717ecd1ee0402efdfe8053068094971ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=EC=A7=84?= Date: Fri, 30 Jan 2026 01:37:39 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=20fix=20:=201:1=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=EA=B3=B5=EA=B0=9C=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사이드바 삭제예정, 1:1스터디원 이름->닉네임은 백엔드해서 넘겨줘야됨 --- src/components/layout/header-nav.tsx | 2 +- .../study/schedule/model/use-schedule-query.ts | 4 ++-- src/features/study/schedule/ui/study-card.tsx | 12 ++++++++++-- src/middleware.ts | 2 +- src/widgets/home/sidebar.tsx | 15 ++++++++++++++- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/layout/header-nav.tsx b/src/components/layout/header-nav.tsx index 7c48f3d6..f7b74883 100644 --- a/src/components/layout/header-nav.tsx +++ b/src/components/layout/header-nav.tsx @@ -9,7 +9,7 @@ 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: '멘토스터디' }, 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..70381b1e 100644 --- a/src/features/study/schedule/ui/study-card.tsx +++ b/src/features/study/schedule/ui/study-card.tsx @@ -15,6 +15,7 @@ import { getKoreaDisplayMonday, } from '@/utils/time'; import StudyListSection from '../../../../widgets/home/study-list-table'; +import { useAuth } from '@/hooks/common/use-auth'; // 스터디 주차 구하는 함수 function getWeekly(date: Date): { month: number; week: number } { @@ -62,12 +63,19 @@ function getWeekly(date: Date): { month: number; week: number } { export default function StudyCard() { const [selectedDate, setSelectedDate] = useState(new Date()); - + 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/sidebar.tsx b/src/widgets/home/sidebar.tsx index 1323a489..366207ff 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,19 @@ export default async function Sidebar() { const memberIdStr = await getServerCookie('memberId'); const memberId = Number(memberIdStr); - const userProfile = await getUserProfileInServer(memberId); + console.log('memberId', memberId); + console.log('memberIdStr', memberIdStr); + // 비회원 접근시 사이드바 빈 페이지 반환 + let userProfile = null; + try { + if (!memberIdStr || !isNumeric(memberIdStr)) { + return null; + } + + userProfile = await getUserProfileInServer(memberId); + } catch (error) { + console.error(error); + } return (