diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01adbcf..78b9a49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -433,66 +433,79 @@ packages: resolution: {integrity: sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.58.0': resolution: {integrity: sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.58.0': resolution: {integrity: sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.58.0': resolution: {integrity: sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.58.0': resolution: {integrity: sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.58.0': resolution: {integrity: sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.58.0': resolution: {integrity: sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.58.0': resolution: {integrity: sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.58.0': resolution: {integrity: sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.58.0': resolution: {integrity: sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.58.0': resolution: {integrity: sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.58.0': resolution: {integrity: sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.58.0': resolution: {integrity: sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.58.0': resolution: {integrity: sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==} @@ -562,24 +575,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.0': resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.0': resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.0': resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.0': resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==} @@ -1274,24 +1291,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} diff --git a/public/all-notes-preview.png b/public/all-notes-preview.png new file mode 100644 index 0000000..0c98af5 Binary files /dev/null and b/public/all-notes-preview.png differ diff --git a/public/final-summary-preview.png b/public/final-summary-preview.png new file mode 100644 index 0000000..71c26f6 Binary files /dev/null and b/public/final-summary-preview.png differ diff --git a/public/mypage-preview-backup.png b/public/mypage-preview-backup.png new file mode 100644 index 0000000..0c98af5 Binary files /dev/null and b/public/mypage-preview-backup.png differ diff --git a/public/mypage-preview.png b/public/mypage-preview.png new file mode 100644 index 0000000..954d4fc Binary files /dev/null and b/public/mypage-preview.png differ diff --git a/public/recording-preview.png b/public/recording-preview.png new file mode 100644 index 0000000..75a848e Binary files /dev/null and b/public/recording-preview.png differ diff --git a/src/App.tsx b/src/App.tsx index 3701e73..c426dd0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import LoginPage from '@/pages/LoginPage'; +import MainPage from '@/pages/MainPage'; import MyPage from '@/pages/MyPage'; import RecordingPage from '@/pages/RecordingPage'; @@ -9,6 +10,7 @@ function App() { } /> } /> + } /> } /> } /> diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx new file mode 100644 index 0000000..9b8468d --- /dev/null +++ b/src/pages/MainPage.tsx @@ -0,0 +1,407 @@ +import { useEffect, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; + +const SHOWCASE_STEPS = [ + { + eyebrow: 'FEATURE 01', + keyword: 'Real-time', + title: '말하는 순간 바로 보이는\n실시간 자막', + description: + '수업과 회의가 진행되는 동안 발화가 즉시 텍스트로 정리됩니다. 사람 1, 사람 2처럼 화자를 구분해 같은 순간을 더 선명하게 공유합니다.', + image: '/recording-preview.png', + alt: 'SpeakView 실시간 자막 화면', + }, + { + eyebrow: 'FEATURE 02', + keyword: 'Summary', + title: '끝난 뒤에는\n핵심만 남는 전체 요약', + description: + '긴 대화를 다시 처음부터 훑지 않아도 되도록, SpeakView가 핵심 문장과 중요한 맥락만 남겨 빠르게 복습할 수 있는 구조를 만듭니다.', + image: '/final-summary-preview.png', + alt: 'SpeakView 전체 요약 화면', + }, + { + eyebrow: 'FEATURE 03', + keyword: 'My Page', + title: '나만의 모든 기록이\n체계적으로 관리되는 마이페이지', + description: + '학업 정보와 일자별 스크립트 관리,\n요약 노트와 복습 퀴즈를 통해\n학습 효율을 극대화할 수 있습니다.', + image: '/mypage-preview.png', + alt: 'SpeakView 마이페이지 화면', + }, +] as const; + +export default function MainPage() { + const [scrollY, setScrollY] = useState(0); + const [time, setTime] = useState(0); + const [isHeroVisible, setIsHeroVisible] = useState(false); + const [showcaseProgress, setShowcaseProgress] = useState(0); + const [viewportHeight, setViewportHeight] = useState(0); + const [showcaseMetrics, setShowcaseMetrics] = useState({ + top: 0, + height: 0, + }); + const showcaseRef = useRef(null); + + useEffect(() => { + const handleScroll = () => { + setScrollY(window.scrollY); + setViewportHeight(window.innerHeight); + + if (showcaseRef.current) { + const rect = showcaseRef.current.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const totalScrollable = Math.max(rect.height - viewportHeight, 1); + const progressed = Math.min( + totalScrollable, + Math.max(0, -rect.top), + ); + const nextMetrics = { + top: window.scrollY + rect.top, + height: rect.height, + }; + + setShowcaseProgress( + Math.min(1, Math.max(0, progressed / totalScrollable)), + ); + setShowcaseMetrics(nextMetrics); + } + }; + + handleScroll(); + window.addEventListener('scroll', handleScroll, { passive: true }); + window.addEventListener('resize', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', handleScroll); + }; + }, []); + + useEffect(() => { + let frameId = 0; + + const tick = () => { + setTime(Date.now()); + frameId = window.requestAnimationFrame(tick); + }; + + frameId = window.requestAnimationFrame(tick); + + return () => window.cancelAnimationFrame(frameId); + }, []); + + useEffect(() => { + const timer = window.setTimeout(() => { + setIsHeroVisible(true); + }, 220); + + return () => window.clearTimeout(timer); + }, []); + + const glowOffset = Math.min(scrollY * 0.42, 92); + const secondaryGlowOffset = Math.min(scrollY * 0.28, 58); + const primaryFloatX = + Math.sin(time / 1100) * 320 + Math.cos(time / 2100) * 140; + const primaryFloatY = + Math.cos(time / 1500) * 190 + Math.sin(time / 2400) * 90; + const primaryScale = 1.04 + (Math.sin(time / 1400) + 1) * 0.07; + const primaryOpacity = 0.14 + ((Math.sin(time / 1300) + 1) / 2) * 0.08; + const secondaryFloatX = + Math.cos(time / 1250) * 340 + Math.sin(time / 1900) * 150; + const secondaryFloatY = + Math.sin(time / 1000) * 200 + Math.cos(time / 2200) * 80; + const secondaryScale = 1.02 + (Math.cos(time / 1350) + 1) * 0.08; + const secondaryOpacity = 0.12 + ((Math.cos(time / 1200) + 1) / 2) * 0.1; + + // 3-step showcase animation logic + const realtimeOpacity = clamp(1 - (showcaseProgress - 0.28) / 0.12, 0, 1); + const summaryOpacity = clamp( + showcaseProgress < 0.4 ? (showcaseProgress - 0.28) / 0.12 : 1 - (showcaseProgress - 0.62) / 0.12, + 0, + 1 + ); + const mypageOpacity = clamp((showcaseProgress - 0.62) / 0.12, 0, 1); + + const realtimeContentProgress = clamp(showcaseProgress / 0.4, 0, 1); + const summaryContentProgress = clamp((showcaseProgress - 0.3) / 0.4, 0, 1); + const mypageContentProgress = clamp((showcaseProgress - 0.6) / 0.4, 0, 1); + + const realtimeContentTranslateY = + realtimeContentProgress < 0.25 + ? 36 - (realtimeContentProgress / 0.25) * 88 + : realtimeContentProgress < 0.75 + ? -52 + : -52 - ((realtimeContentProgress - 0.75) / 0.25) * 92; + + const summaryContentTranslateY = + summaryContentProgress < 0.25 + ? 36 - (summaryContentProgress / 0.25) * 88 + : summaryContentProgress < 0.75 + ? -52 + : -52 - ((summaryContentProgress - 0.75) / 0.25) * 92; + + const mypageContentTranslateY = + mypageContentProgress < 0.25 + ? 36 - (mypageContentProgress / 0.25) * 88 + : -52; + + const showcaseSectionTop = showcaseMetrics.top; + const showcaseSectionHeight = showcaseMetrics.height; + const showcaseCanvasHeight = Math.max(viewportHeight - 80, 0); + const showcasePinStart = showcaseSectionTop - 80; + const showcasePinEnd = showcaseSectionTop + showcaseSectionHeight - viewportHeight; + const isShowcasePinned = + scrollY >= showcasePinStart && scrollY <= showcasePinEnd; + const isShowcaseAfter = + showcaseCanvasHeight > 0 && scrollY > showcasePinEnd; + const showcaseFrameTop = isShowcasePinned + ? 80 + : isShowcaseAfter + ? showcaseSectionHeight - showcaseCanvasHeight + : 0; + + return ( + + + + + SpeakView + + + + + 서비스소개 + + + 실시간자막 + + 전체 노트 + + 마이페이지 + + + + + + + + + + + + + + + + + + + 실시간 자막 + + 으로 + + + 같은 순간 + + 을 공유해요 + + + + 실시간 자막 바로가기 + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function ShowcaseCard({ + step, +}: { + step: (typeof SHOWCASE_STEPS)[number]; +}) { + return ( + + + {step.eyebrow} + + + {step.title.split('\n').map((line) => ( + + {line} + + + ))} + + + {step.description.split('\n').map((line, i) => ( + + {line} + + + ))} + + + ); +} + +function ShowcaseScene({ + step, + mode, + contentTranslateY, +}: { + step: (typeof SHOWCASE_STEPS)[number]; + mode: 'base' | 'overlay'; + contentTranslateY: number; +}) { + const isOverlay = mode === 'overlay'; + + return ( + + + {step.keyword} + + + + + + + + + + + + + + + ); +} + +function clamp(value: number, min: number, max: number) { + return Math.min(max, Math.max(min, value)); +} diff --git a/src/pages/MyPage.tsx b/src/pages/MyPage.tsx index 8bed915..2e8a0ab 100644 --- a/src/pages/MyPage.tsx +++ b/src/pages/MyPage.tsx @@ -49,34 +49,55 @@ const lectures = [ }, ] as const; -const days = [ - ['월', '11'], - ['화', '12'], - ['수', '13'], - ['목', '14'], - ['금', '15'], - ['토', '16'], - ['일', '17'], -] as const; +const DAY_NAMES = ['일', '월', '화', '수', '목', '금', '토'] as const; + +function getTodayInfo() { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const date = String(now.getDate()).padStart(2, '0'); + const dayName = DAY_NAMES[now.getDay()]; + + // Get current week (Mon-Sun) + const currentDay = now.getDay(); // 0 (Sun) to 6 (Sat) + const diffToMonday = currentDay === 0 ? -6 : 1 - currentDay; + const monday = new Date(now); + monday.setDate(now.getDate() + diffToMonday); + + const weekDays = Array.from({ length: 7 }, (_, i) => { + const d = new Date(monday); + d.setDate(monday.getDate() + i); + return [DAY_NAMES[d.getDay()], String(d.getDate())] as [string, string]; + }); + + return { + fullDate: `${year}.${month}.${date}`, + dayName, + weekDays, + todayDate: String(now.getDate()), + }; +} export default function MyPage() { + const { fullDate, dayName, weekDays, todayDate } = getTodayInfo(); + return ( - - + + SpeakView - - + + 서비스소개 - + 실시간자막 @@ -88,43 +109,49 @@ export default function MyPage() { - - + + - - - - - - - - 최세연 - - - 님 - - - + + + + + + + - + - + + + + 최세연 + + + 님 + + - + {lectures.map((lecture) => ( - - - - 오늘, 월요일 + + + + + 오늘, {dayName}요일 + {fullDate} - 2026.05.11 - + 더보기 - - {days.map(([day, date]) => { - const active = day === '월'; + + {weekDays.map(([day, date]) => { + const active = date === todayDate; return ( - - + + {day} {date} @@ -173,7 +210,7 @@ function ScheduleCard() { })} - + {scheduleItems.map((item) => ( ))} @@ -198,13 +235,13 @@ function ScheduleItem({ }; return ( - + {subject} - + {title} @@ -219,38 +256,39 @@ function StatsCard() { ]; return ( - + {stats.map(([label, value], index) => ( 0 ? 'border-l border-[#c4c4c4]' : '' + index > 0 ? 'border-l border-gray-100' : '' }`} > - {label} - {value} + {label} + {value} ))} ); } + function LectureTabs() { return ( - - - - 강의목록 - + + + + + 강의목록 + + + - - 날짜순 - 목록순 + + 날짜순 + 목록순 - - - ); } @@ -267,18 +305,19 @@ function LectureRow({ color: string; }) { return ( - - + + {subject} - {period} + {period} {notes} - + ); } +
+ {step.eyebrow} +
+ {step.description.split('\n').map((line, i) => ( + + {line} + + + ))} +
{fullDate}
2026.05.11
{label}
{value}