Skip to content
Merged
13 changes: 6 additions & 7 deletions src/app/(service)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -15,15 +16,13 @@ export const metadata: Metadata = generateSEOMetadata({

export default async function Home() {
return (
<div className="mx-auto flex w-[1496px] gap-600 px-600 py-600">
<div className="flex flex-1 flex-col gap-500">
<div className="mx-auto w-[1280px] px-400 py-600">
<div className="flex flex-col gap-500">
<Banner />
<FeedbackLink />
<StartStudyButton />
<StudyCard />
</div>

<aside className="w-[335px] shrink-0">
<Sidebar />
</aside>
</div>
);
}
17 changes: 8 additions & 9 deletions src/app/(service)/insights/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

// 카테고리 목록과 아티클 목록을 병렬로 가져오기
Expand All @@ -59,8 +54,13 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
const articles = articlesRes.data ?? [];

return (
<PageContainer className="flex gap-600 py-600">
<div className="mx-auto w-[1280px] px-400 py-600">
<div className="flex flex-1 flex-col gap-500">
{/* 배너 */}
<div className="mb-600">
<Banner />
</div>

<div className="flex justify-between">
<span className="font-designer-28b text-[#181D27]">
ZERO-ONE 인사이트
Expand Down Expand Up @@ -144,7 +144,6 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
</ul>
)}
</div>
{isLoggedIn && <Sidebar />}
</PageContainer>
</div>
);
}
31 changes: 31 additions & 0 deletions src/app/(service)/mentoring/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense fallback={<MentoringListPageSkeleton />}>
<MentoringListPage />
</Suspense>
);
}

function MentoringListPageSkeleton() {
return (
<div className="mx-auto w-[1280px] px-400 py-600">
<div className="flex h-[400px] items-center justify-center">
<span className="text-text-subtle">로딩 중...</span>
</div>
</div>
);
}
264 changes: 264 additions & 0 deletions src/components/card/mentor-card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="hover:shadow-2 hover:border-border-brand rounded-150 cursor-pointer overflow-hidden border border-[#E5E7EB] bg-white transition-all"
onClick={() => {
// 모달이 열려 있으면 카드 클릭 무시
if (!isComingSoonModalOpen) {
handleProfileClick();
}
}}
>
{/* 프로필 이미지 영역 */}
<div className="relative flex h-[180px] items-center justify-center bg-linear-to-br from-[#F87171] to-[#EC4899]">
{mentor.imageUrl ? (
<Image
src={mentor.imageUrl}
alt={mentor.nickname}
fill
className="object-cover"
/>
) : (
<div className="flex h-400 w-400 items-center justify-center overflow-hidden rounded-full bg-white/20">
<UserAvatar size={100} image="" alt={mentor.nickname} />
</div>
)}
</div>

{/* 컨텐츠 영역 */}
<div className="px-300 py-200">
{/* 뱃지 */}
<div className="mb-100">
<Badge color="blue">{mentor.field}</Badge>
</div>

{/* 제목 */}
<div className="mb-100 flex items-center gap-100">
<h3 className="font-designer-20b text-text-default truncate">
{mentor.nickname}
</h3>
<ExternalLink className="text-text-subtle h-16 w-16 shrink-0" />
</div>

{/* 설명 */}
<p className="font-designer-16r text-text-subtle mb-150 line-clamp-2">
{mentor.description}
</p>

{/* 키워드 */}
{mentor.keywords.length > 0 && (
<div className="mb-200 flex flex-wrap gap-100">
{mentor.keywords.map((keyword) => (
<span
key={keyword}
className="bg-fill-neutral-subtle-default text-text-subtle font-designer-12r rounded-75 px-100 py-50"
>
{keyword}
</span>
))}
</div>
)}

{/* 멘토링 방식 */}
<div className="mb-200 flex items-center gap-200">
<div className="flex items-center gap-100">
<MessageCircle
className={cn(
'h-20 w-20',
mentor.availableMethods.chat
? 'text-text-brand'
: 'text-text-subtlest',
)}
/>
<span
className={cn(
'font-designer-12r',
mentor.availableMethods.chat
? 'text-text-default'
: 'text-text-subtlest',
)}
>
채팅상담
</span>
</div>
<div className="flex items-center gap-100">
<Phone
className={cn(
'h-20 w-20',
mentor.availableMethods.call
? 'text-text-brand'
: 'text-text-subtlest',
)}
/>
<span
className={cn(
'font-designer-12r',
mentor.availableMethods.call
? 'text-text-default'
: 'text-text-subtlest',
)}
>
전화/온라인 상담
</span>
</div>
<div className="flex items-center gap-100">
<Users
className={cn(
'h-20 w-20',
mentor.availableMethods.offline
? 'text-text-brand'
: 'text-text-subtlest',
)}
/>
<span
className={cn(
'font-designer-12r',
mentor.availableMethods.offline
? 'text-text-default'
: 'text-text-subtlest',
)}
>
대면 컨설팅
</span>
</div>
</div>

{/* 멘토링 문의하기 버튼 */}
<Button
color="primary"
size="medium"
className="w-full"
onClick={handleApplyClick}
>
도움 요청
</Button>
</div>

{/* 곧 오픈 예정 모달 */}
<Modal.Root
open={isComingSoonModalOpen}
onOpenChange={setIsComingSoonModalOpen}
>
<Modal.Portal>
<Modal.Overlay />
<Modal.Content size="small" onClick={(e) => e.stopPropagation()}>
<Modal.Header className="border-border-default flex items-center justify-between border-b">
<Modal.Title className="font-designer-20b text-text-strong">
멘토링 서비스
</Modal.Title>
<Modal.Close>
<XIcon />
</Modal.Close>
</Modal.Header>

<Modal.Body className="flex flex-col items-center gap-300 py-400">
<div className="bg-fill-brand-subtle-default flex h-[80px] w-[80px] items-center justify-center rounded-full">
<Sparkles className="text-text-brand h-[40px] w-[40px]" />
</div>

<div className="flex flex-col items-center gap-200 text-center">
<h3 className="font-designer-20b text-text-strong">
곧 오픈 예정입니다!
</h3>
<p className="font-designer-16r text-text-default">
1:1 멘토링 서비스를 준비하고 있어요.
<br />
조금만 기다려주시면 멘토와 함께
<br />
성장할 수 있는 기회를 제공해드릴게요.
</p>
<p className="font-designer-14r text-text-subtle mt-100">
곧 만나요! 🚀
</p>
</div>
</Modal.Body>

<Modal.Footer className="flex justify-end">
<Button
color="primary"
size="medium"
onClick={(e) => {
e.stopPropagation();
setIsComingSoonModalOpen(false);
}}
>
확인
</Button>
</Modal.Footer>
</Modal.Content>
</Modal.Portal>
</Modal.Root>
</div>
);
}
Loading
Loading