diff --git a/README.md b/README.md index 2c6cc97..7a2d0d1 100644 --- a/README.md +++ b/README.md @@ -1 +1,131 @@ -# AllClass \ No newline at end of file +# AllClass + +> 부산 지역 대학생들을 위한 강의 리뷰 플랫폼 + +**배포 링크**: [https://all-class.vercel.app/](https://all-class.vercel.app) + +![AllClass 메인 페이지](./public/images/readme/main-landing.png) + +## 🎯 프로젝트 개요 + +AllClass는 학부 연구실 창업동아리에서 시작하여 예비창업을 준비하기까지 약 2년동안 진행한 수업리뷰 관리 시스템입니다. 교내 학과 50여명이 직접 수강후기를 작성하며 운영되었으며, 부산 전지역으로 확대하는 것을 목표로 했던 플랫폼입니다. + +기존에는 강의 선택 시 참고할 수 있는 정보가 제한적이었지만, AllClass를 통해 실제 수강생들의 생생한 리뷰와 평가를 바탕으로 학생들이 보다 투명하고 신뢰할 수 있는 강의 정보를 얻을 수 있습니다. + +이 프로젝트는 한 달간 진행된 리라이팅 과정의 기록이기도 합니다. 해커톤에서 시작된 레거시 코드를 개선하며 겪은 기술적 도전과 해결 과정을 블로그를 통해 상세히 기록했습니다. + +## 📋 프로젝트 중점사항 + +### 사용자 경험 최적화 +- **렌더링 전략**: SSG/ISR/하이브리드를 활용한 페이지별 최적화 +- **성능 지표**: Core Web Vitals 기준으로 지속적인 성능 모니터링 +- **반응형 디자인**: 다양한 디바이스에서의 일관된 사용자 경험 + +### 확장 가능한 아키텍처 +- **도메인 주도 설계**: 4개 도메인(auth, lecture, review, mypage)으로 명확한 분리 +- **컴포넌트 설계**: 재사용 가능하고 테스트 가능한 모듈형 구조 +- **타입 안전성**: TypeScript 100% 적용으로 런타임 오류 방지 + +### 안정적인 개발 환경 +- **E2E 테스트**: Cypress를 통한 핵심 사용자 플로우 자동화 검증 +- **CI/CD 파이프라인**: GitHub Actions를 통한 배포 자동화 +- **코드 품질**: ESLint, Prettier를 통한 일관된 코드 스타일 유지 + +## 🛠️ 기술적 이슈 해결 과정 + +리라이팅 과정에서 마주한 문제들과 해결 과정을 블로그에 기록했습니다: + +| 단계 | 제목 | 핵심 문제 | 해결 방법 | 성과 | 링크 | +|------|------|-----------|-----------|------|------| +| **1** | LCP 8초짜리 Next.js 프로젝트 리라이팅 계획 | 모든 페이지 SSR로 인한 성능 저하 | SSG/ISR 렌더링 전략 수립 | 프로젝트 방향성 정립 | [📖 Post #5](https://solplog.vercel.app/articles/post-5) | +| **2** | 조건부 렌더링의 브라우저 이미지 캐시 무효화 | 이미지 재로딩으로 인한 UX 저하 | CSS visibility + React.memo 최적화 | 이미지 깜빡임 완전 제거 | [📖 Post #6](https://solplog.vercel.app/articles/post-6) | +| **3** | 도메인 구조에 Next.js 끼얹어보기 | 역할 기반 폴더 구조의 개발 효율성 저하 | 도메인 + 기술적 레이어 하이브리드 설계 | 파일 크기 52.7% 감소 | [📖 Post #7](https://solplog.vercel.app/articles/post-7) | +| **4** | 캐싱 레이어 분리로 평점 즉시 업데이트하기 | 정적 데이터와 실시간 업데이트 충돌 | ISR + React Query 하이브리드 캐싱 | 서버 부하 감소 + 실시간성 확보 | [📖 Post #8](https://solplog.vercel.app/articles/post-8) | +| **5** | 서버-클라이언트 상태 동기화로 UI Flickering 제거 | 페이지 새로고침 시 UI 깜빡임 | SSR Hydration + dehydrate/hydrate | 매끄러운 페이지 전환 | [📖 Post #9](https://solplog.vercel.app/articles/post-9) | +| **6** | 사용자 플로우 중심의 E2E 테스트 개선기 | UI 테스트 과다로 인한 비효율성 | 실제 API + 핵심 플로우 중심 테스트 | 핵심 플로우 자동화 검증 | [📖 Post #10](https://solplog.vercel.app/articles/post-10) | +| **7** | AllClass 리라이팅 프로젝트 회고 | 레거시 시스템 완전 전환 | 정량적 성과 분석 및 교훈 도출 | Lighthouse 성능 69→100점, 코드 18.7% 효율화 | [📖 Post #11](https://solplog.vercel.app/articles/post-11) | + + +## 📊 주요 성과 + +### 🚀 성능 개선 +- **LCP**: 2.5초 → 0.7초 (72% 개선) +- **FCP**: 2.1초 → 0.5초 (76% 개선) +- **Speed Index**: 4.6초 → 0.9초 (80% 개선) +- **정적 페이지 생성**: 8개 → 84개 (10.5배 증가) +- **Lighthouse 성능 점수**: 69 → 100점 (+31점) + +### 🏗️ 아키텍처 개선 +- **코드 라인**: 6,750줄 → 5,488줄 (18.7% 감소) +- **평균 파일 크기**: 78.5줄 → 37.1줄 (52.7% 감소) +- **의존성**: 23개 → 6개 (74% 감소) +- **도메인 분리**: 4개 도메인으로 명확한 구조 + +### 📦 번들 최적화 +- **강의 목록 페이지**: 183kB → 138kB (25% 감소) +- **강의 상세 페이지**: 249kB → 146kB (41% 감소) +- **코드 스플리팅**: 단일 청크 77.4kB → 최대 4.72kB 모듈 +- **TypeScript 적용**: 0% → 100% 타입 안전성 확보 + +## 🔧 사용 기술 및 환경 + +### Frontend +- **Framework**: Next.js 15.3.5 (App Router) +- **Language**: TypeScript 5.8.3 +- **Styling**: CSS Modules +- **State Management**: Zustand 5.0.6 +- **Data Fetching**: TanStack Query 5.83.0 +- **Animation**: Framer Motion 12.23.12 + +### Development & Testing +- **Testing**: Cypress 14.5.1 (E2E) +- **Code Quality**: ESLint, Prettier +- **Package Manager**: Yarn + +### Deployment & Infrastructure +- **Deployment**: Vercel +- **CI/CD**: GitHub Actions +- **Monitoring**: Vercel Analytics + +## 🎨 UI 설계 + +
+ 메인 랜딩 페이지 + 메인 페이지 + 강의 상세 페이지 +
+ +
+ 메인 랜딩 페이지 | 메인 페이지 | 강의 상세 페이지 +
+ +
+ +
+ 마이페이지 강의 + 마이페이지 리뷰 +
+ +
+ 마이페이지 - 강의 관리 | 마이페이지 - 리뷰 관리 +
+ +## 🔗 관련 링크 + +### 📋 Use Case + +**[📖 AllClass Use Case Wiki](https://github.com/all-classs/all-class-server/wiki/Use-Case)** + +### 백엔드 레포지토리 + +**[🔧 AllClass Backend Server](https://github.com/all-classs/all-class-server)** + +### AllClass 프로젝트 레거시 코드 +현재 리라이팅된 버전 이전의 프로젝트 변천사를 확인할 수 있습니다: + +| 버전 | 설명 | 링크 | +|------|------|------| +| **v0.0.0** | 2023년 교내 창업동아리 버전 | [📁 release/0.0.0](https://github.com/all-classs/All-Class/tree/release/0.0.0) | +| **v0.0.1** | 2024년 부산 ICT 해커톤 버전 | [📁 release/0.0.1](https://github.com/all-classs/All-Class/tree/release/0.0.1) | +| **v0.0.2** | 2024년 대한민국 해커톤 버전 | [📁 release/0.0.2](https://github.com/all-classs/All-Class/tree/release/0.0.2) | + diff --git a/app/layout.tsx b/app/layout.tsx index 14b8884..217e17f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,8 @@ import type { Metadata } from 'next'; import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; -import { AppProviders } from './providers'; import AuthHydrationProvider from './providers/AuthHydrationProvider'; +import { Analytics } from '@vercel/analytics/react'; const geistSans = Geist({ variable: '--font-geist-sans', @@ -80,6 +80,7 @@ export default function RootLayout({
{children}
+ ); diff --git a/app/page.tsx b/app/page.tsx index 31b5c46..b2d6c44 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,13 @@ import type { Metadata } from 'next'; import { Header } from '@/components/ui'; import { LoginTriggerWrapper } from '@/components/common'; +import { + HeroSection, + FeaturesSectionLazy, + UniversityLogosLazy, + HowItWorksSectionLazy, + CTASectionLazy, +} from '@/components/landing'; export const metadata: Metadata = { title: 'AllClass', @@ -23,7 +30,11 @@ export default async function Home({ return (
-
Hello World
+ + + + + {showLogin && }
); diff --git a/components/landing/CTASection.tsx b/components/landing/CTASection.tsx new file mode 100644 index 0000000..cdd38b9 --- /dev/null +++ b/components/landing/CTASection.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { motion } from 'framer-motion'; +import styles from './styles/CTASection.module.css'; +import { memo } from 'react'; + +const statsVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.03, + }, + }, +}; + +const statItemVariants = { + hidden: { + opacity: 0, + scale: 0.8, + y: 30, + }, + visible: { + opacity: 1, + scale: 1, + y: 0, + }, +}; + +export const CTASection = memo(function CTASection() { + return ( +
+ + +

+ 지금 바로 AllClass와 함께 +
+ 스마트한 수강신청을 시작하세요! +

+

+ 14개 대학교의 강의 정보와 실제 학생 리뷰가 기다리고 있습니다. +

+ + +
+
+ 완전 무료 +
+
+
+ 즉시 사용 가능 +
+
+
🔒
+ 안전한 데이터 +
+
+
+ + + + +
14
+
개 대학교
+
+ +
1,000+
+
강의 정보
+
+ +
500+
+
학생 리뷰
+
+ +
95%
+
만족도
+
+
+
+
+ +
+
+
+
+
+
+ ); +}); diff --git a/components/landing/FeaturesSection.tsx b/components/landing/FeaturesSection.tsx new file mode 100644 index 0000000..5fb49fa --- /dev/null +++ b/components/landing/FeaturesSection.tsx @@ -0,0 +1,166 @@ +'use client'; + +import { motion } from 'framer-motion'; +import styles from './styles/FeaturesSection.module.css'; +import lectureCardStyles from '@/domains/lecture/styles/LectureCard.module.css'; +import { BookOpen, Tag, Star } from 'lucide-react'; +import { StarRating } from '@/components/common'; +import { memo } from 'react'; + +const features = [ + { + icon: '🏛️', + title: '14개 대학교 지원', + description: + '부산대, 동아대, 경성대 등 부산 지역 주요 대학교의 강의 정보를 한곳에서 확인하세요.', + }, + { + icon: '⭐', + title: '실제 수강생 리뷰', + description: '수강한 학생들의 솔직한 후기와 5점 평점 시스템으로 강의의 실제 모습을 확인하세요.', + }, + { + icon: '📚', + title: '스마트 마이페이지', + description: + '내가 들은 수업이 자동으로 연동되어 마이페이지에서 쉽게 관리하고 리뷰를 작성할 수 있습니다.', + }, + { + icon: '🔍', + title: '다양한 정렬 기능', + description: + '별점순, 좋아요순, 최신순 등 원하는 기준으로 리뷰를 정렬하여 필요한 정보를 빠르게 찾으세요.', + }, +]; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05, + }, + }, +}; + +const itemVariants = { + hidden: { + opacity: 0, + y: 30, + scale: 0.9, + }, + visible: { + opacity: 1, + y: 0, + scale: 1, + }, +}; + +export const FeaturesSection = memo(function FeaturesSection() { + return ( +
+
+ +

+ 왜 AllClass인가요? +

+

학생들을 위한, 학생들에 의한 강의 정보 플랫폼

+
+ + + {features.map((feature, index) => ( + +
{feature.icon}
+

{feature.title}

+

{feature.description}

+
+ ))} +
+ + +
+

강의 정보 한눈에 보기

+
+
+
+
+ +
+
+

고급프로그래밍

+

박교수

+
+
+ + 개설 + +
+
+ +
+
+ + + 학과 + + 컴퓨터공학과 +
+
+ + + 구분 + + + 전공필수 + +
+
+ + + 평점 + +
+ +
+
+
+
+
+
+
+ +
+
+ ); +}); diff --git a/components/landing/HeroSection.tsx b/components/landing/HeroSection.tsx new file mode 100644 index 0000000..7b912bb --- /dev/null +++ b/components/landing/HeroSection.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { useInView } from 'framer-motion'; +import styles from './styles/HeroSection.module.css'; + +const CountUpNumber = ({ + end, + duration = 2000, + suffix = '', +}: { + end: number; + duration?: number; + suffix?: string; +}) => { + const [count, setCount] = useState(0); + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.5 }); + + useEffect(() => { + if (!isInView) return; + + let startTime: number; + const animate = (currentTime: number) => { + if (!startTime) startTime = currentTime; + const progress = Math.min((currentTime - startTime) / duration, 1); + + const easeOutQuart = 1 - Math.pow(1 - progress, 4); + setCount(Math.floor(end * easeOutQuart)); + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + setCount(end); + } + }; + + requestAnimationFrame(animate); + }, [isInView, end, duration]); + + return ( + + {count.toLocaleString()} + {suffix} + + ); +}; + +export const HeroSection = () => { + return ( +
+
+
+

+ 부산 지역 대학교 +
+ 강의정보를 한 눈에 +

+

+ 실제 수강생 리뷰로 현명한 수강신청하세요 +
+ 14개 부산 소속 대학교의 강의 정보와 생생한 후기를 확인해보세요 +

+
+
+ + 개 대학교 +
+
+ + 강의 정보 +
+
+ + 학생 리뷰 +
+
+
+
+
+
+
+
★★★★★
+ 127개 리뷰 +
+

데이터베이스 시스템

+

김교수 • 컴퓨터공학과

+
+ "과제가 많긴 하지만 정말 실무에 도움이 되는 수업입니다!" +
+
+ 추천 + 실무형 +
+
+ +
+
★★★★☆
+

웹프로그래밍

+

박교수

+
"과제는 어렵지만 배우는 게 많아요"
+
+ +
+
★★★★★
+

알고리즘

+

이교수

+
"코딩테스트 준비에 최고!"
+
+ +
+
★★★☆☆
+

운영체제

+

최교수

+
"이론 위주라 조금 지루해요"
+
+ +
+
+
+
+
+
+
+ ); +}; diff --git a/components/landing/HowItWorksSection.tsx b/components/landing/HowItWorksSection.tsx new file mode 100644 index 0000000..34294e3 --- /dev/null +++ b/components/landing/HowItWorksSection.tsx @@ -0,0 +1,115 @@ +'use client'; + +import { motion } from 'framer-motion'; +import styles from './styles/HowItWorksSection.module.css'; +import { memo } from 'react'; + +const steps = [ + { + step: '01', + title: '대학교 선택', + description: '부산 지역 14개 대학교 중 내 학교를 선택해보세요', + icon: '🏫', + }, + { + step: '02', + title: '강의 탐색', + description: '학교별, 학과별로 원하는 강의를 찾아보세요', + icon: '🔍', + }, + { + step: '03', + title: '리뷰 확인', + description: '실제 수강생들의 후기와 평점을 확인해보세요', + icon: '📖', + }, + { + step: '04', + title: '나만의 리뷰', + description: '수강한 강의에 대한 솔직한 후기를 남겨보세요', + icon: '✍️', + }, +]; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05, + }, + }, +}; + +const itemVariants = { + hidden: { + opacity: 0, + y: 40, + scale: 0.9, + }, + visible: { + opacity: 1, + y: 0, + scale: 1, + }, +}; + +export const HowItWorksSection = memo(function HowItWorksSection() { + return ( +
+
+ +

+ AllClass 사용법 +

+

간단한 4단계로 시작하는 스마트한 강의 탐색

+
+ + + {steps.map((step, index) => ( + +
{step.step}
+
{step.icon}
+

{step.title}

+

{step.description}

+
+ ))} +
+ + +

지금 바로 시작해보세요!

+

부산 지역 대학생들이 이미 사용하고 있는 강의 정보 플랫폼

+
+
+
+ ); +}); diff --git a/components/landing/LazySections.tsx b/components/landing/LazySections.tsx new file mode 100644 index 0000000..a5c3e1c --- /dev/null +++ b/components/landing/LazySections.tsx @@ -0,0 +1,22 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +export const FeaturesSectionLazy = dynamic( + () => import('./FeaturesSection').then((m) => m.FeaturesSection), + { ssr: false } +); + +export const UniversityLogosLazy = dynamic( + () => import('./UniversityLogos').then((m) => m.UniversityLogos), + { ssr: false } +); + +export const HowItWorksSectionLazy = dynamic( + () => import('./HowItWorksSection').then((m) => m.HowItWorksSection), + { ssr: false } +); + +export const CTASectionLazy = dynamic(() => import('./CTASection').then((m) => m.CTASection), { + ssr: false, +}); diff --git a/components/landing/UniversityLogos.tsx b/components/landing/UniversityLogos.tsx new file mode 100644 index 0000000..e3a3908 --- /dev/null +++ b/components/landing/UniversityLogos.tsx @@ -0,0 +1,110 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import styles from './styles/UniversityLogos.module.css'; +import { memo } from 'react'; + +const universities = [ + { name: '부산대학교', logo: '/assets/university-logo/busan-uni.svg', path: '부산대학교' }, + { name: '동아대학교', logo: '/assets/university-logo/donga-uni.svg', path: '동아대학교' }, + { name: '경성대학교', logo: '/assets/university-logo/ks-uni.svg', path: '경성대학교' }, + { name: '동서대학교', logo: '/assets/university-logo/dongseo-uni.svg', path: '동서대학교' }, + { + name: '부산외국어대학교', + logo: '/assets/university-logo/bufs-uni.svg', + path: '부산외국어대학교', + }, + { name: '경남정보대학교', logo: '/assets/university-logo/kit-uni.svg', path: '경남정보대학교' }, + { name: '동명대학교', logo: '/assets/university-logo/tongmyong-uni.svg', path: '동명대학교' }, + { name: '동의대학교', logo: '/assets/university-logo/dongeui-uni.svg', path: '동의대학교' }, + { name: '동의과학대학교', logo: '/assets/university-logo/dit-uni.svg', path: '동의과학대학교' }, + { name: '부경대학교', logo: '/assets/university-logo/pukyong.svg', path: '부경대학교' }, + { + name: '부산가톨릭대학교', + logo: '/assets/university-logo/catholic-uni.svg', + path: '부산가톨릭대학교', + }, + { + name: '부산경상대학교', + logo: '/assets/university-logo/gyoungsang-uni.svg', + path: '부산경상대학교', + }, + { name: '신라대학교', logo: '/assets/university-logo/silla-uni.svg', path: '신라대학교' }, + { name: '한국해양대학교', logo: '/assets/university-logo/ocean-uni.svg', path: '한국해양대학교' }, +]; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.03, + }, + }, +}; + +const itemVariants = { + hidden: { + opacity: 0, + scale: 0.8, + y: 20, + }, + visible: { + opacity: 1, + scale: 1, + y: 0, + }, +}; + +export const UniversityLogos = memo(function UniversityLogos() { + return ( +
+
+ + {universities.map((university, index) => ( + + + +
+ {`${university.name} +
+
+ +
+ ))} +
+
+
+ ); +}); diff --git a/components/landing/index.ts b/components/landing/index.ts new file mode 100644 index 0000000..d0105ed --- /dev/null +++ b/components/landing/index.ts @@ -0,0 +1,11 @@ +export { HeroSection } from './HeroSection'; +export { FeaturesSection } from './FeaturesSection'; +export { HowItWorksSection } from './HowItWorksSection'; +export { UniversityLogos } from './UniversityLogos'; +export { CTASection } from './CTASection'; +export { + FeaturesSectionLazy, + UniversityLogosLazy, + HowItWorksSectionLazy, + CTASectionLazy, +} from './LazySections'; diff --git a/components/landing/styles/CTASection.module.css b/components/landing/styles/CTASection.module.css new file mode 100644 index 0000000..07b8247 --- /dev/null +++ b/components/landing/styles/CTASection.module.css @@ -0,0 +1,293 @@ +.cta { + padding: 6rem 0; + background: linear-gradient(135deg, #f8fafc 0%, #ffffff 50%, #f8fafc 100%); + color: #1a202c; + position: relative; + overflow: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; + position: relative; + z-index: 2; + animation: fadeInUp 1s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.content { + z-index: 2; +} + +.title { + font-size: 3rem; + font-weight: 700; + line-height: 1.2; + margin-bottom: 1.5rem; +} + +.highlight { + background: linear-gradient(135deg, #FFD600 0%, #FFA000 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + font-size: 1.2rem; + line-height: 1.6; + margin-bottom: 2.5rem; + opacity: 0.9; +} + +.actions { + display: flex; + gap: 1rem; + margin-bottom: 3rem; + flex-wrap: wrap; +} + +.primaryButton { + background: #ffd700; + color: #333; + border: none; + font-weight: 600; + padding: 0.875rem 2rem; + font-size: 1.1rem; + transition: all 0.3s ease; +} + +.primaryButton:hover { + background: #ffed4a; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(255, 215, 0, 0.4); +} + +.secondaryButton { + background: rgba(255, 255, 255, 0.15); + color: white; + border: 2px solid rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); + padding: 0.875rem 2rem; + font-size: 1.1rem; +} + +.secondaryButton:hover { + background: rgba(255, 255, 255, 0.25); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-2px); +} + +.features { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.feature { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.95rem; +} + +.featureIcon { + font-size: 1.2rem; +} + +.visual { + display: flex; + justify-content: center; + align-items: center; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; +} + +.statCard { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 214, 0, 0.1); + border-radius: 20px; + padding: 2rem 1.5rem; + text-align: center; + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: relative; + overflow: hidden; +} + +.statCard::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 214, 0, 0.1), transparent); + transition: left 0.6s; +} + +.statCard:hover::before { + left: 100%; +} + +.statCard:hover { + background: linear-gradient(135deg, rgba(255, 255, 255, 1) 0%, rgba(255, 254, 247, 1) 100%); + transform: translateY(-8px) scale(1.02); + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); + border-color: rgba(255, 214, 0, 0.2); +} + +.statNumber { + font-size: 2.5rem; + font-weight: 700; + color: #FFD600; + line-height: 1; + margin-bottom: 0.5rem; +} + +.statLabel { + font-size: 0.9rem; + opacity: 0.8; +} + +.background { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 1; +} + +.backgroundShape1 { + position: absolute; + top: -50px; + right: -50px; + width: 300px; + height: 300px; + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + animation: float 6s ease-in-out infinite; +} + +.backgroundShape2 { + position: absolute; + bottom: -100px; + left: -100px; + width: 400px; + height: 400px; + background: rgba(255, 215, 0, 0.1); + border-radius: 50%; + animation: float 8s ease-in-out infinite reverse; +} + +.backgroundShape3 { + position: absolute; + top: 50%; + left: 10%; + width: 200px; + height: 200px; + background: rgba(255, 255, 255, 0.05); + border-radius: 50%; + animation: float 10s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-20px); + } +} + +@media (max-width: 768px) { + .cta { + padding: 4rem 0; + } + + .container { + grid-template-columns: 1fr; + gap: 3rem; + text-align: center; + } + + .title { + font-size: 2.5rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .actions { + justify-content: center; + } + + .features { + justify-content: center; + } + + .statsGrid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } + + .statCard { + padding: 1.5rem 1rem; + } + + .statNumber { + font-size: 2rem; + } +} + +@media (max-width: 480px) { + .title { + font-size: 2rem; + } + + .actions { + flex-direction: column; + align-items: stretch; + } + + .primaryButton, + .secondaryButton { + justify-content: center; + } + + .features { + gap: 1rem; + } + + .statsGrid { + grid-template-columns: 1fr; + } + + .backgroundShape1, + .backgroundShape2, + .backgroundShape3 { + display: none; + } + } \ No newline at end of file diff --git a/components/landing/styles/FeaturesSection.module.css b/components/landing/styles/FeaturesSection.module.css new file mode 100644 index 0000000..e375f98 --- /dev/null +++ b/components/landing/styles/FeaturesSection.module.css @@ -0,0 +1,407 @@ +.features { + padding: 5rem 0; + background: linear-gradient(to bottom, #f8fafc, #ffffff); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.header { + text-align: center; + margin-bottom: 4rem; +} + +.title { + font-size: 3rem; + font-weight: 700; + color: #1a202c; + margin-bottom: 1rem; +} + +.highlight { + color: #FFD600; +} + +.subtitle { + font-size: 1.25rem; + color: #4a5568; + opacity: 0.8; +} + +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 2rem; + margin-bottom: 5rem; + max-width: 800px; + margin-left: auto; + margin-right: auto; + margin-bottom: 5rem; +} + +.card { + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + padding: 2.5rem 2rem; + border-radius: 20px; + text-align: center; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(0, 0, 0, 0.04); + transition: all 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: relative; + overflow: hidden; +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 214, 0, 0.05), transparent); + transition: left 0.2s; +} + +.card:hover::before { + left: 100%; +} + +.card:hover { + transform: translateY(-12px) scale(1.02); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); + background: linear-gradient(135deg, #ffffff 0%, #fffef7 100%); +} + +.icon { + font-size: 3rem; + margin-bottom: 1.5rem; +} + +.cardTitle { + font-size: 1.5rem; + font-weight: 600; + color: #2d3748; + margin-bottom: 1rem; +} + +.cardDescription { + color: #4a5568; + line-height: 1.6; + font-size: 1rem; +} + +.showcase { + background: linear-gradient(135deg, #fffef7 0%, #fefcf0 100%); + border-radius: 20px; + padding: 3rem; + color: #333; + text-align: center; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); +} + +.showcaseContent > h3 { + font-size: 2rem; + font-weight: 600; + margin-bottom: 2rem; +} + +.mockup { + display: flex; + justify-content: center; + align-items: center; +} + +.mockCardWrapper { + width: 400px; + max-width: 100%; + text-align: left; +} + +.lectureCard { + position: relative; + background: white; + color: #333; + padding: 16px; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e2e8f0; + max-width: 400px; + width: 100%; + text-align: left; + min-height: 150px; + display: flex; + flex-direction: column; + justify-content: space-between; + transition: all 0.2s ease; +} + +.lectureCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + border-radius: 0 0 0 10px; + background: linear-gradient(180deg, #9ca3af 0%, #6b7280 100%); +} + +.lectureHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; + padding-left: 12px; +} + +.lectureHeader h4 { + font-size: 16px; + font-weight: 600; + color: #1a202c; + margin: 0 0 6px 0; + line-height: 1.3; +} + +.lectureRating { + padding: 4px 8px; + border-radius: 16px; + font-size: 11px; + font-weight: 600; + background-color: #dcfce7; + color: #166534; + border: 1px solid #bbf7d0; + white-space: nowrap; +} + +.professor { + font-size: 13px; + color: #718096; + margin: 0; + line-height: 1.2; +} + +.cardContainer { + position: relative; + background: white; + border-radius: 10px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + padding: 16px; + margin-bottom: 16px; + transition: all 0.3s ease; + border: 1px solid #e5e7eb; + min-height: 150px; + display: flex; + flex-direction: column; + justify-content: space-between; + cursor: pointer; + overflow: hidden; + max-width: 400px; + width: 100%; +} + +.cardContainer:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.colorBar { + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + border-radius: 0 0 0 10px; + background: linear-gradient(180deg, #9ca3af 0%, #6b7280 100%); +} + +.cardHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 12px; + padding-left: 12px; +} + +.cardInfo { + flex: 1; +} + +.lectureName { + font-size: 16px; + font-weight: 600; + color: #111827; + margin: 0 0 6px 0; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 1; + line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.professor { + font-size: 13px; + color: #6b7280; + margin: 0; + line-height: 1.2; +} + +.cardStatus { + display: flex; + align-items: flex-start; + margin-left: 10px; +} + +.statusBadge { + padding: 4px 8px; + border-radius: 16px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.statusBadge.opened { + background-color: #dcfce7; + color: #166534; + border: 1px solid #bbf7d0; +} + +.statusBadge.closed { + background-color: #fef2f2; + color: #dc2626; + border: 1px solid #fecaca; +} + +.cardBody { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: auto; + padding-left: 12px; +} + +.cardDetail { + display: flex; + justify-content: space-between; + align-items: center; + min-height: 22px; +} + +.label { + font-size: 14px; + color: #6b7280; + font-weight: 500; + min-width: 40px; + display: flex; + align-items: center; + gap: 5px; +} + +.value { + font-size: 14px; + color: #111827; + font-weight: 400; + text-align: right; + max-width: 65%; + word-break: break-word; +} + +.lectureTypeBadge { + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + background-color: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; +} + +.ratingContainer { + display: flex; + align-items: center; + gap: 6px; +} + +.starRating { + display: flex; + align-items: center; + gap: 1px; +} + +.star { + color: #f59e0b; + text-shadow: 0 1px 2px rgba(245, 158, 11, 0.3); + font-size: 16px; +} + +.halfStar { + color: #f59e0b; + opacity: 0.7; + text-shadow: 0 1px 2px rgba(245, 158, 11, 0.3); + font-size: 16px; +} + +.ratingText { + padding-left: 0.4rem; + color: #6b7280; + font-weight: 600; + font-size: 14px; +} + +/* 반응형 디자인 */ +@media (max-width: 768px) { + .features { + padding: 3rem 0; + } + + .title { + font-size: 2.5rem; + } + + .grid { + grid-template-columns: 1fr; + gap: 1.5rem; + margin-bottom: 3rem; + } + + .card { + padding: 2rem 1.5rem; + } + + .showcase { + padding: 2rem; + } + + .showcaseContent h3 { + font-size: 1.5rem; + } + + .lectureCard { + padding: 1.5rem; + } +} + +@media (max-width: 480px) { + .title { + font-size: 2rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .card { + padding: 1.5rem 1rem; + } + + .icon { + font-size: 2.5rem; + } + + .cardTitle { + font-size: 1.25rem; + } +} \ No newline at end of file diff --git a/components/landing/styles/HeroSection.module.css b/components/landing/styles/HeroSection.module.css new file mode 100644 index 0000000..71ae8c2 --- /dev/null +++ b/components/landing/styles/HeroSection.module.css @@ -0,0 +1,599 @@ +.hero { + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 50%, #ffffff 100%); + min-height: 80vh; + display: flex; + align-items: center; + padding: 2rem 0; + color: #1a202c; + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: -50%; + right: -10%; + width: 600px; + height: 600px; + background: radial-gradient(circle, rgba(255, 214, 0, 0.05) 0%, transparent 70%); + animation: float 8s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0) rotate(0deg); + } + 50% { + transform: translateY(-20px) rotate(180deg); + } +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; + +} + +.content { + z-index: 2; + animation: fadeInUp 1s ease-out; + min-height: 350px; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.title { + font-size: 3.5rem; + font-weight: 700; + line-height: 1.2; + margin-bottom: 1.5rem; +} + +.highlight { + background: linear-gradient(135deg, #FFD600 0%, #FFA000 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + font-size: 1.25rem; + line-height: 1.6; + margin-bottom: 2.5rem; + opacity: 0.9; +} + +.actions { + display: flex; + gap: 1rem; + margin-bottom: 3rem; + flex-wrap: wrap; +} + +.primaryButton { + background: #fff; + color: #333; + border: none; + font-weight: 600; + transition: all 0.3s ease; +} + +.primaryButton:hover { + background: #f0f0f0; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(255, 255, 255, 0.3); +} + +.secondaryButton { + background: rgba(255, 255, 255, 0.1); + color: white; + border: 2px solid rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); +} + +.secondaryButton:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.5); +} + +.stats { + display: flex; + gap: 3rem; + animation: fadeInUp 1.2s ease-out 0.3s both; +} + +.stat { + text-align: center; +} + +.statNumber { + display: inline-block; + font-size: 2.5rem; + font-weight: 700; + color: #FFD600; + min-width: 120px; + font-variant-numeric: tabular-nums; +} + +.statLabel { + display: block; + font-size: 0.9rem; + opacity: 0.8; + margin-top: 0.25rem; +} + +.visual { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + position: relative; + perspective: 1200px; + margin-bottom: 130px; + margin-left: 100px; +} + +.floatingCards { + position: relative; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +/* Main featured review card */ +.reviewCard { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + padding: 1.5rem; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + transition: all 0.4s ease; + color: #333; + position: absolute; +} + +.mainCard { + width: 350px; + z-index: 10; + transform: rotateY(-5deg) rotateX(5deg) translateZ(0); + animation: mainCardFloat 6s ease-in-out infinite, fadeInScale 1.2s ease-out; +} + +.mainCard:hover { + transform: rotateY(0deg) rotateX(0deg) translateZ(30px) scale(1.05); + box-shadow: 0 40px 80px rgba(0, 0, 0, 0.2); +} + +@keyframes mainCardFloat { + 0%, 100% { + transform: rotateY(-5deg) rotateX(5deg) translateY(0px); + } + 50% { + transform: rotateY(-5deg) rotateX(5deg) translateY(-10px); + } +} + +.floatingCard1 { + width: 200px; + top: -30px; + left: -90px; + z-index: 5; + transform: rotateZ(-15deg) scale(0.8); + animation: float1 8s ease-in-out infinite, slideInLeft 1s ease-out 0.3s both; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.floatingCard1:hover { + transform: rotateZ(-5deg) scale(0.9) translateY(-10px); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2); +} + +.floatingCard2 { + width: 180px; + top: 70px; + right: -40px; + z-index: 6; + transform: rotateZ(12deg) scale(0.75); + animation: float2 7s ease-in-out infinite 1s, slideInRight 1s ease-out 0.6s both; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.floatingCard2:hover { + transform: rotateZ(5deg) scale(0.85) translateY(-15px); + box-shadow: 0 30px 60px rgba(0, 0, 0, 0.25); +} + +.floatingCard3 { + width: 160px; + bottom: -50px; + left: 70px; + z-index: 4; + transform: rotateZ(-8deg) scale(0.7); + animation: float3 9s ease-in-out infinite 2s, slideInUp 1s ease-out 0.9s both; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.floatingCard3:hover { + transform: rotateZ(-2deg) scale(0.8) translateY(-12px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); + z-index: 12; +} + +@keyframes float1 { + 0%, 100% { + transform: rotateZ(-15deg) scale(0.8) translateY(0px); + } + 50% { + transform: rotateZ(-15deg) scale(0.8) translateY(-15px); + } +} + +@keyframes float2 { + 0%, 100% { + transform: rotateZ(12deg) scale(0.75) translateY(0px); + } + 50% { + transform: rotateZ(12deg) scale(0.75) translateY(12px); + } +} + +@keyframes float3 { + 0%, 100% { + transform: rotateZ(-8deg) scale(0.7) translateY(0px); + } + 50% { + transform: rotateZ(-8deg) scale(0.7) translateY(-8px); + } +} + +/* 등장 애니메이션들 */ +@keyframes fadeInScale { + 0% { + opacity: 0; + transform: rotateY(-5deg) rotateX(5deg) scale(0.8); + } + 100% { + opacity: 1; + transform: rotateY(-5deg) rotateX(5deg) scale(1); + } +} + +@keyframes slideInLeft { + 0% { + opacity: 0; + transform: rotateZ(-15deg) scale(0.8) translateX(-100px); + } + 100% { + opacity: 1; + transform: rotateZ(-15deg) scale(0.8) translateX(0); + } +} + +@keyframes slideInRight { + 0% { + opacity: 0; + transform: rotateZ(12deg) scale(0.75) translateX(100px); + } + 100% { + opacity: 1; + transform: rotateZ(12deg) scale(0.75) translateX(0); + } +} + +@keyframes slideInUp { + 0% { + opacity: 0; + transform: rotateZ(-8deg) scale(0.7) translateY(80px); + } + 100% { + opacity: 1; + transform: rotateZ(-8deg) scale(0.7) translateY(0); + } +} + + + +/* Background decorative elements */ +.bgElement1 { + position: absolute; + top: 10%; + left: 10%; + width: 100px; + height: 100px; + background: radial-gradient(circle, rgba(255, 214, 0, 0.1) 0%, transparent 70%); + border-radius: 50%; + animation: bgFloat1 10s ease-in-out infinite; + z-index: 1; +} + +.bgElement2 { + position: absolute; + bottom: 20%; + right: 15%; + width: 150px; + height: 150px; + background: radial-gradient(circle, rgba(255, 160, 0, 0.08) 0%, transparent 70%); + border-radius: 50%; + animation: bgFloat2 12s ease-in-out infinite 3s; + z-index: 1; +} + +.bgElement3 { + position: absolute; + top: 60%; + left: 20%; + width: 80px; + height: 80px; + background: linear-gradient(45deg, rgba(255, 214, 0, 0.05), rgba(255, 160, 0, 0.05)); + border-radius: 50%; + animation: bgFloat3 8s ease-in-out infinite 1s; + z-index: 1; +} + +@keyframes bgFloat1 { + 0%, 100% { + transform: translate(0, 0) scale(1) rotate(0deg); + } + 33% { + transform: translate(20px, -30px) scale(1.2) rotate(120deg); + } + 66% { + transform: translate(-10px, 25px) scale(0.9) rotate(240deg); + } +} + +@keyframes bgFloat2 { + 0%, 100% { + transform: translate(0, 0) scale(1) rotate(0deg); + } + 33% { + transform: translate(-30px, 20px) scale(0.8) rotate(-90deg); + } + 66% { + transform: translate(25px, -15px) scale(1.1) rotate(-180deg); + } +} + +@keyframes bgFloat3 { + 0%, 100% { + transform: translate(0, 0) rotate(0deg) scale(1); + } + 25% { + transform: translate(15px, -15px) rotate(90deg) scale(1.1); + } + 50% { + transform: translate(-20px, -25px) rotate(180deg) scale(0.8); + } + 75% { + transform: translate(10px, 20px) rotate(270deg) scale(1.2); + } +} + +/* Card content styles */ +.cardHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.rating { + color: #ffd700; + font-size: 1.2rem; + font-weight: 500; +} + +.reviews { + font-size: 0.9rem; + color: #666; +} + +.reviewCard h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: #333; +} + +.reviewCard h4 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.3rem; + color: #333; +} + +.reviewCard p { + color: #666; + margin-bottom: 1rem; + font-size: 0.9rem; +} + +.reviewText { + background: rgba(255, 214, 0, 0.1); + padding: 1rem; + border-radius: 12px; + font-style: italic; + margin-bottom: 1rem; + border-left: 3px solid #FFD600; + color: #555; + font-size: 0.95rem; +} + +.miniReview { + background: rgba(255, 255, 255, 0.5); + padding: 0.5rem; + border-radius: 8px; + font-size: 0.8rem; + color: #555; + font-style: italic; + margin-bottom: 0.5rem; +} + +.tags { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.tag { + background: linear-gradient(135deg, #FFD600, #FFA000); + color: white; + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 500; +} + +/* 네트워크 스타일 */ +.networkContainer { + position: relative; + width: 550px; + height: 350px; + margin: 0 auto; +} + +.connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; +} + +.node { + position: absolute; + background: rgba(255, 255, 255, 0.95); + border: 2px solid rgba(255, 214, 0, 0.4); + border-radius: 50%; + width: 80px; + height: 80px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.3s ease; + z-index: 2; + transform: translate(-50%, -50%); + backdrop-filter: blur(10px); +} + +.node:hover { + border-color: rgba(255, 214, 0, 0.8); + box-shadow: 0 8px 30px rgba(255, 214, 0, 0.3); +} + +.nodeLogo { + width: 50px; + height: 50px; + object-fit: contain; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); +} + +@media (max-width: 768px) { + .container { + grid-template-columns: 1fr; + gap: 2rem; + text-align: center; + } + + .title { + font-size: 2.5rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .actions { + justify-content: center; + } + + .stats { + justify-content: center; + gap: 2rem; + } + + + .floatingCards { + transform: scale(0.9); + } + + .mainCard { + width: 300px; + transform: none; + animation: none; + position: relative; + } + + .floatingCard1, + .floatingCard2, + .floatingCard3 { + display: none; + } + + .visual { + margin-left: 0px; + } +} + +@media (max-width: 480px) { + .hero { + padding: 1rem 0; + min-height: 70vh; + } + + .title { + font-size: 2rem; + } + + .actions { + flex-direction: column; + align-items: stretch; + } + + .stats { + gap: 1.5rem; + } + + .statNumber { + font-size: 2rem; + } + + .networkContainer { + width: 450px; + height: 280px; + } + + .node { + width: 65px; + height: 65px; + } + + .nodeLogo { + width: 40px; + height: 40px; + } +} diff --git a/components/landing/styles/HowItWorksSection.module.css b/components/landing/styles/HowItWorksSection.module.css new file mode 100644 index 0000000..f63b7d9 --- /dev/null +++ b/components/landing/styles/HowItWorksSection.module.css @@ -0,0 +1,221 @@ +.howItWorks { + padding: 5rem 0; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 50%, #ffffff 100%); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.header { + text-align: center; + margin-bottom: 4rem; +} + +.title { + font-size: 3rem; + font-weight: 700; + color: #1a202c; + margin-bottom: 1rem; +} + +.highlight { + background: linear-gradient(135deg, #FFD600 0%, #FFA000 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + font-size: 1.25rem; + color: #4a5568; + opacity: 0.8; +} + +.steps { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; + position: relative; + margin-bottom: 5rem; + animation: fadeInUp 1s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.stepCard { + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + padding: 2.5rem 2rem; + border-radius: 24px; + text-align: center; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(0, 0, 0, 0.04); + position: relative; + transition: all 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94); + overflow: hidden; +} + +.stepCard::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 214, 0, 0.05), transparent); + transition: left 0.2s; +} + +.stepCard:hover::before { + left: 100%; +} + +.stepCard:hover { + transform: translateY(-12px) scale(1.02); + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); + background: linear-gradient(135deg, #ffffff 0%, #fffef7 100%); +} + +.stepNumber { + display: inline-flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + background: linear-gradient(135deg, #FFD600 0%, #FFBF00 100%); + color: #333; + font-size: 1.5rem; + font-weight: 700; + border-radius: 50%; + margin-bottom: 1rem; +} + +.stepIcon { + font-size: 3rem; + margin-bottom: 1.5rem; + display: block; +} + +.stepTitle { + font-size: 1.5rem; + font-weight: 600; + color: #2d3748; + margin-bottom: 1rem; +} + +.stepDescription { + color: #4a5568; + line-height: 1.6; + font-size: 1rem; +} + +.arrow { + position: absolute; + right: -2rem; + top: 50%; + transform: translateY(-50%); + color: #FFD600; + z-index: 10; + background: white; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.cta { + text-align: center; + background: linear-gradient(135deg, #fffef7 0%, #fefcf0 100%); + color: #333; + padding: 3rem 2rem; + border-radius: 20px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); +} + +.cta h3 { + font-size: 2rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.cta p { + font-size: 1.1rem; + opacity: 0.9; +} + +@media (max-width: 1024px) { + .steps { + grid-template-columns: repeat(2, 1fr); + } + + .arrow { + display: none; + } +} + +@media (max-width: 768px) { + .howItWorks { + padding: 3rem 0; + } + + .title { + font-size: 2.5rem; + } + + .steps { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .stepCard { + padding: 2rem 1.5rem; + } + + .arrow { + display: none; + } + + .cta { + padding: 2rem 1.5rem; + } + + .cta h3 { + font-size: 1.5rem; + } +} + +@media (max-width: 480px) { + .title { + font-size: 2rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .stepCard { + padding: 1.5rem 1rem; + } + + .stepIcon { + font-size: 2.5rem; + } + + .stepTitle { + font-size: 1.25rem; + } +} \ No newline at end of file diff --git a/components/landing/styles/UniversityLogos.module.css b/components/landing/styles/UniversityLogos.module.css new file mode 100644 index 0000000..932c336 --- /dev/null +++ b/components/landing/styles/UniversityLogos.module.css @@ -0,0 +1,134 @@ +.universities { + padding: 4rem 0; + background: linear-gradient(135deg, #f8fafc 0%, #ffffff 50%, #f8fafc 100%); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.logoGrid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2rem; + justify-items: center; + align-items: center; + animation: fadeInUp 0.8s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.logoLink { + text-decoration: none; + display: block; +} + +.logoCard { + display: flex; + justify-content: center; + align-items: center; + width: 100px; + height: 100px; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + border-radius: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(0, 0, 0, 0.04); + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.logoCard::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 214, 0, 0.1), transparent); + transition: left 0.6s; +} + +.logoCard:hover::before { + left: 100%; +} + +.logoCard:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15); + border-color: rgba(255, 214, 0, 0.3); + background: linear-gradient(135deg, #ffffff 0%, #fffef7 100%); +} + +.logoWrapper { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + padding: 15px; +} + +.logo { + max-width: 100%; + max-height: 100%; + object-fit: contain; + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); + filter: grayscale(0.2) opacity(0.9); +} + +.logoCard:hover .logo { + transform: scale(1.1) rotate(2deg); + filter: grayscale(0) opacity(1); +} + +@media (max-width: 1024px) { + .logoGrid { + grid-template-columns: repeat(5, 1fr); + gap: 1rem; + } +} + +@media (max-width: 768px) { + .universities { + padding: 2rem 0; + } + + .logoGrid { + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + } + + .logoCard { + width: 80px; + height: 80px; + } +} + +@media (max-width: 480px) { + .logoGrid { + grid-template-columns: repeat(3, 1fr); + gap: 0.8rem; + } + + .logoCard { + width: 70px; + height: 70px; + } + + .logoWrapper { + padding: 10px; + } +} \ No newline at end of file diff --git a/components/ui/header/Header.tsx b/components/ui/header/Header.tsx index 5f1ab95..8368a7a 100644 --- a/components/ui/header/Header.tsx +++ b/components/ui/header/Header.tsx @@ -47,7 +47,8 @@ export default function Header({ showDropdown = false }: HeaderProps) {
- {showDropdown && } + {/* {showDropdown && } */} +
{isLoggedIn ? ( diff --git a/package.json b/package.json index dbae66b..0d39083 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ }, "dependencies": { "@tanstack/react-query": "^5.83.0", + "@vercel/analytics": "^1.5.0", + "framer-motion": "^12.23.12", "lucide-react": "^0.525.0", "next": "15.3.5", "react": "^19.0.0", diff --git a/public/images/readme/lecture-detail.png b/public/images/readme/lecture-detail.png new file mode 100644 index 0000000..2d4243a Binary files /dev/null and b/public/images/readme/lecture-detail.png differ diff --git a/public/images/readme/lecture-main.png b/public/images/readme/lecture-main.png new file mode 100644 index 0000000..f3cafef Binary files /dev/null and b/public/images/readme/lecture-main.png differ diff --git a/public/images/readme/main-landing.png b/public/images/readme/main-landing.png new file mode 100644 index 0000000..d5ab4bb Binary files /dev/null and b/public/images/readme/main-landing.png differ diff --git a/public/images/readme/mypage-lecture.png b/public/images/readme/mypage-lecture.png new file mode 100644 index 0000000..cff2ea0 Binary files /dev/null and b/public/images/readme/mypage-lecture.png differ diff --git a/public/images/readme/mypage-review.png b/public/images/readme/mypage-review.png new file mode 100644 index 0000000..972ba49 Binary files /dev/null and b/public/images/readme/mypage-review.png differ diff --git a/yarn.lock b/yarn.lock index 0548a91..f9e6766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -684,6 +684,11 @@ resolved "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz" integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== +"@vercel/analytics@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-1.5.0.tgz#073f93694897414b21a8495e2619bbf64447dcaa" + integrity sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -1891,6 +1896,15 @@ form-data@~4.0.0: hasown "^2.0.2" mime-types "^2.1.12" +framer-motion@^12.23.12: + version "12.23.12" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.12.tgz#80cf6fd7c111073a0c558e336c85ca36cca80d3d" + integrity sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg== + dependencies: + motion-dom "^12.23.12" + motion-utils "^12.23.6" + tslib "^2.4.0" + fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -2639,6 +2653,18 @@ minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +motion-dom@^12.23.12: + version "12.23.12" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.23.12.tgz#87974046e7e61bc4932f36d35e8eab6bb6f3e434" + integrity sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw== + dependencies: + motion-utils "^12.23.6" + +motion-utils@^12.23.6: + version "12.23.6" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.23.6.tgz#fafef80b4ea85122dd0d6c599a0c63d72881f312" + integrity sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ== + ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"