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
20 changes: 20 additions & 0 deletions app/[universityName]/[lectureId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Metadata } from 'next';
import { ReviewCardListSkeleton, ReviewListServer } from '@/domains/review';
import { LectureInfoSkeleton, LectureInfoServer } from '@/domains/lecture';
import { Suspense } from 'react';
Expand All @@ -7,6 +8,25 @@ import { universityNames } from '@/constants';
import { getLectureListStatic } from '@/lib';
import styles from '@/styles/global.module.css';

export async function generateMetadata({
params,
}: {
params: Promise<{ universityName: string; lectureId: string }>;
}): Promise<Metadata> {
const { universityName, lectureId } = await params;
const decodedUniversity = decodeURIComponent(universityName);

return {
title: `강의 상세정보`,
description: `${decodedUniversity} 강의의 상세정보와 학생 리뷰를 확인하세요. 실제 수강생들의 생생한 후기!`,
openGraph: {
title: `강의 상세정보 | AllClass`,
description: `${decodedUniversity} 강의의 상세정보와 학생 리뷰를 확인하세요.`,
},
keywords: [decodedUniversity, '강의상세', '강의리뷰', '수강후기', '평점', 'AllClass'],
};
}

export async function generateStaticParams() {
const allParams = await Promise.allSettled(
universityNames.map(async (universityName) => {
Expand Down
20 changes: 20 additions & 0 deletions app/[universityName]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Metadata } from 'next';
import { universityNames } from '@/constants';
import { getLectureListStatic } from '@/lib/lecture';
import styles from '@/styles/global.module.css';
Expand All @@ -12,6 +13,25 @@ export async function generateStaticParams() {
return universityNames.map((name) => ({ universityName: encodeURIComponent(name) }));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ universityName: string }>;
}): Promise<Metadata> {
const { universityName } = await params;
const decoded = decodeURIComponent(universityName);

return {
title: `${decoded} 강의정보`,
description: `${decoded}의 모든 강의정보와 학생 리뷰를 확인하세요. 평점, 수강후기, 교수님 정보까지!`,
openGraph: {
title: `${decoded} 강의정보 | AllClass`,
description: `${decoded}의 모든 강의정보와 학생 리뷰를 확인하세요.`,
},
keywords: [decoded, '강의정보', '수강후기', '평점', '강의리뷰', 'AllClass'],
};
}

export default async function UniversityPage({
params,
}: {
Expand Down
53 changes: 51 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,57 @@ const geistMono = Geist_Mono({
});

export const metadata: Metadata = {
title: 'AllClass',
description: 'AllClass를 통해 쉽고 빠르게 부산 소속 대학교 강의정보를 확인해보세요.',
title: {
default: 'AllClass - 부산소속 대학교 강의정보 플랫폼',
template: '%s | AllClass',
},
description:
'AllClass를 통해 쉽고 빠르게 부산 소속 대학교 강의정보를 확인해보세요. 강의 리뷰, 수강신청 가이드, 학점 관리까지 한 번에!',
keywords: [
'부산대학교',
'강의정보',
'수강신청',
'강의리뷰',
'대학교',
'부산',
'학점관리',
'AllClass',
],
authors: [{ name: 'AllClass Team' }],
creator: 'AllClass',
publisher: 'AllClass',
robots: {
index: true,
follow: true,
},
openGraph: {
type: 'website',
locale: 'ko_KR',
url: 'https://all-class.vercel.app',
title: 'AllClass - 부산소속 대학교 강의정보 플랫폼',
description: 'AllClass를 통해 쉽고 빠르게 부산 소속 대학교 강의정보를 확인해보세요.',
siteName: 'AllClass',
images: [
{
url: '/assets/logo.svg',
width: 1200,
height: 630,
alt: 'AllClass 로고',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'AllClass - 부산소속 대학교 강의정보 플랫폼',
description: 'AllClass를 통해 쉽고 빠르게 부산 소속 대학교 강의정보를 확인해보세요.',
images: ['/assets/logo.svg'],
},
icons: {
icon: '/assets/logo.svg',
shortcut: '/assets/logo.svg',
apple: '/assets/logo.svg',
},
manifest: '/manifest.json',
};

export default function RootLayout({
Expand Down
10 changes: 10 additions & 0 deletions app/mypage/curriculum/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type { Metadata } from 'next';
import { DashboardLayout } from '@/components/layout';

export const metadata: Metadata = {
title: '커리큘럼',
description: '내 커리큘럼 정보를 확인하고 관리하세요.',
robots: {
index: false,
follow: false,
},
};

export default function CurriculumPage() {
return (
<DashboardLayout title="커리큘럼" subtitle="우수 졸업생들의 커리큘럼을 구매해보세요">
Expand Down
10 changes: 10 additions & 0 deletions app/mypage/lectures/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type { Metadata } from 'next';
import { Suspense } from 'react';
import { DashboardLayout } from '@/components/layout';
import { LoadingSpinner } from '@/components/common/loading/LoadingSpinner';
import { MyLectureListServer } from '@/domains/mypage/server/components/MyLectureListServer';

export const metadata: Metadata = {
title: '수강한 강의',
description: '내가 수강한 강의 목록을 확인하세요.',
robots: {
index: false,
follow: false,
},
};

export default function LecturesPage() {
return (
<DashboardLayout title="수업" subtitle="내가 수강한 강의 목록을 확인하세요">
Expand Down
10 changes: 10 additions & 0 deletions app/mypage/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type { Metadata } from 'next';
import { DashboardLayout } from '@/components/layout';

export const metadata: Metadata = {
title: '내 정보',
description: '프로필 정보를 관리하고 개인정보를 업데이트하세요.',
robots: {
index: false,
follow: false,
},
};

export default function ProfilePage() {
return (
<DashboardLayout title="내정보" subtitle="프로필 정보를 관리하세요">
Expand Down
10 changes: 10 additions & 0 deletions app/mypage/reviews/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type { Metadata } from 'next';
import { Suspense } from 'react';
import { DashboardLayout } from '@/components/layout';
import { LoadingSpinner } from '@/components/common/loading/LoadingSpinner';
import { MyReviewListServer } from '@/domains/mypage/server/components/MyReviewListServer';

export const metadata: Metadata = {
title: '리뷰 관리',
description: '내가 작성한 리뷰를 관리하세요.',
robots: {
index: false,
follow: false,
},
};

export default function ReviewsPage() {
return (
<DashboardLayout title="리뷰관리" subtitle="내가 작성한 리뷰를 관리하세요">
Expand Down
11 changes: 11 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import type { Metadata } from 'next';
import { Header } from '@/components/ui';
import { LoginTriggerWrapper } from '@/components/common';

export const metadata: Metadata = {
title: 'AllClass',
description:
'AllClass에서 부산소속 대학교 강의정보를 한 눈에 확인하세요. 강의 리뷰, 평점, 수강신청 정보까지!',
openGraph: {
title: 'AllClass - 부산소속 대학교 강의정보 플랫폼',
description: 'AllClass에서 부산소속 대학교 강의정보를 한 눈에 확인하세요.',
},
};

export default async function Home({
searchParams,
}: {
Expand Down
44 changes: 44 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MetadataRoute } from 'next';
import { universityNames } from '@/constants';
import { getLectureListStatic } from '@/lib/lecture';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://all-class.vercel.app';

const staticPages = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
];

const universityPages = universityNames.map((universityName) => ({
url: `${baseUrl}/${encodeURIComponent(universityName)}`,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 0.8,
}));

const lecturePages: MetadataRoute.Sitemap = [];

for (const universityName of universityNames) {
try {
const lectures = await getLectureListStatic(universityName);
if (lectures.success && lectures.lectures) {
const pages = lectures.lectures.map((lecture) => ({
url: `${baseUrl}/${encodeURIComponent(universityName)}/${lecture.lectureId}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.7,
}));
lecturePages.push(...pages);
}
} catch (error) {
console.error(`Error generating sitemap for ${universityName}:`, error);
}
}

return [...staticPages, ...universityPages, ...lecturePages];
}
85 changes: 43 additions & 42 deletions components/common/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,58 @@ export interface ModalRef {
close: () => void;
}

const Modal = forwardRef<ModalRef, ModalProps>(
({ title, children, size = 'medium', onClose, open }, ref) => {
const [internalOpen, setInternalOpen] = useState(false);
const Modal = forwardRef<ModalRef, ModalProps>(function Modal(
{ title, children, size = 'medium', onClose, open },
ref
) {
const [internalOpen, setInternalOpen] = useState(false);

useImperativeHandle(ref, () => ({
open: () => setInternalOpen(true),
close: () => setInternalOpen(false),
}));
useImperativeHandle(ref, () => ({
open: () => setInternalOpen(true),
close: () => setInternalOpen(false),
}));

const handleClose = useCallback(() => {
setInternalOpen(false);
onClose?.();
}, [onClose]);
const handleClose = useCallback(() => {
setInternalOpen(false);
onClose?.();
}, [onClose]);

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose();
}
};

const isOpen = typeof open === 'boolean' ? open : internalOpen;
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose();
}

return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [open, internalOpen, handleClose]);
};

const isOpen = typeof open === 'boolean' ? open : internalOpen;
if (!isOpen) return null;
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}

return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [open, internalOpen, handleClose]);

const isOpen = typeof open === 'boolean' ? open : internalOpen;
if (!isOpen) return null;

return (
<div className={styles.overlay} onClick={handleClose}>
<div className={`${styles.modal} ${styles[size]}`} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
{title && <h2 className={styles.modalTitle}>{title}</h2>}
<button className={styles.closeButton} onClick={handleClose}>
<X size={20} />
</button>
</div>
<div className={styles.modalContent}>{children}</div>
return (
<div className={styles.overlay} onClick={handleClose}>
<div className={`${styles.modal} ${styles[size]}`} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
{title && <h2 className={styles.modalTitle}>{title}</h2>}
<button className={styles.closeButton} onClick={handleClose}>
<X size={20} />
</button>
</div>
<div className={styles.modalContent}>{children}</div>
</div>
);
}
);
</div>
);
});

Modal.displayName = 'Modal';

Expand Down
Loading
Loading