diff --git a/apps/web/index.html b/apps/web/index.html
index fd0e947..138fe63 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -1,8 +1,8 @@
-
+
-
+
\ No newline at end of file
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index b993ea1..04cc6d2 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -1,4 +1,5 @@
-import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
+import Home from '@/pages/Home';
import CommunityList from '@/pages/Community/CommunityList';
import CommunityFeed from './pages/Community/CommunityFeed';
import CommunityDetail from '@/pages/Community/CommunityDetail';
@@ -10,7 +11,7 @@ export default function App() {
return (
- } />
+ } />
} />
} />
} />
diff --git a/apps/web/src/hooks/useConstellation.ts b/apps/web/src/hooks/useConstellation.ts
new file mode 100644
index 0000000..1560abf
--- /dev/null
+++ b/apps/web/src/hooks/useConstellation.ts
@@ -0,0 +1,106 @@
+// src/hooks/useConstellation.ts
+import { useEffect, useRef } from 'react';
+
+export function useConstellation() {
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const canvas = ref.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ let raf = 0;
+ const DPR = Math.min(window.devicePixelRatio || 1, 2);
+
+ const stars: { x: number; y: number; vx: number; vy: number; r: number }[] =
+ [];
+
+ const gen = () => {
+ stars.length = 0;
+ const { clientWidth: w, clientHeight: h } = canvas;
+ const count = Math.floor((w * h) / 14000); // 화면 크기에 따른 밀도
+ for (let i = 0; i < count; i++) {
+ stars.push({
+ x: Math.random() * w,
+ y: Math.random() * h,
+ vx: (Math.random() - 0.5) * 0.15,
+ vy: (Math.random() - 0.5) * 0.15,
+ r: Math.random() * 1.6 + 0.6,
+ });
+ }
+ };
+
+ const resize = () => {
+ const { clientWidth, clientHeight } = canvas;
+ canvas.width = Math.floor(clientWidth * DPR);
+ canvas.height = Math.floor(clientHeight * DPR);
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
+ gen();
+ };
+
+ const draw = () => {
+ const { width, height } = canvas;
+ ctx.clearRect(0, 0, width, height);
+ const w = canvas.clientWidth;
+ const h = canvas.clientHeight;
+
+ // 별
+ ctx.save();
+ for (const s of stars) {
+ ctx.globalAlpha = 0.9;
+ ctx.beginPath();
+ ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
+ ctx.fillStyle = '#fff';
+ ctx.fill();
+ }
+
+ // 연결선
+ for (let i = 0; i < stars.length; i++) {
+ for (let j = i + 1; j < stars.length; j++) {
+ const dx = stars[i].x - stars[j].x;
+ const dy = stars[i].y - stars[j].y;
+ const dist = Math.hypot(dx, dy);
+ const max = 120;
+ if (dist < max) {
+ const a = 1 - dist / max;
+ ctx.globalAlpha = a * 0.5;
+ ctx.beginPath();
+ ctx.moveTo(stars[i].x, stars[i].y);
+ ctx.lineTo(stars[j].x, stars[j].y);
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = '#ffffff';
+ ctx.stroke();
+ }
+ }
+ }
+ ctx.restore();
+
+ // 이동
+ for (const s of stars) {
+ s.x += s.vx;
+ s.y += s.vy;
+ if (s.x < -20) s.x = w + 20;
+ if (s.x > w + 20) s.x = -20;
+ if (s.y < -20) s.y = h + 20;
+ if (s.y > h + 20) s.y = -20;
+ }
+
+ raf = requestAnimationFrame(draw);
+ };
+
+ const onResize = () => resize();
+
+ resize();
+ draw();
+ window.addEventListener('resize', onResize);
+
+ return () => {
+ cancelAnimationFrame(raf);
+ window.removeEventListener('resize', onResize);
+ };
+ }, []);
+
+ return ref;
+}
diff --git a/apps/web/src/pages/Home/index.tsx b/apps/web/src/pages/Home/index.tsx
new file mode 100644
index 0000000..b46a0f2
--- /dev/null
+++ b/apps/web/src/pages/Home/index.tsx
@@ -0,0 +1,192 @@
+import styled from '@emotion/styled';
+import { useConstellation } from '@/hooks/useConstellation';
+
+const DL_URL =
+ 'https://github.com/prgrms-fullstack-devcourse/Runova_FE/releases/download/v1.0.0/runova.apk';
+
+export default function Home() {
+ const canvasRef = useConstellation();
+
+ return (
+
+
+
+
+ Runova
+
+ 도시 위에 GPS 아트를 새기는 러닝 앱. 달려서 나만의 별자리
+ 지도를 완성해 보세요.
+
+
+
+ 다운로드 링크
+
+
+
+ Android APK (v1.0.0)
+ ⬇
+
+
+
+
+
+
+
+
+ 경로 안내 제공
+
+ 실시간 경로 트래킹과 경로 안내를 통해, 나만의 GPS 아트를 따라
+ 달려보세요.
+
+
+
+
+ 경로 설계
+ 원하는 모양대로 경로를 그리고 다른 사람들과 공유해 보세요.
+
+
+
+ 커뮤니티
+
+ 완주 인증 사진, 경로 공유, 팀메이트 구하기까지, 커뮤니티에서 함께
+ 나눠요.
+
+
+
+
+
+ );
+}
+
+const Wrap = styled.main`
+ position: relative;
+ min-height: 100dvh;
+ display: grid;
+ place-items: center;
+ gap: 18px;
+ padding: 24px 16px 40px;
+ background: #000;
+ color: #fff;
+ overflow: hidden;
+`;
+
+const StarsCanvas = styled.canvas`
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+`;
+
+const Glow = styled.div`
+ position: absolute;
+ inset: -20%;
+ background:
+ radial-gradient(
+ 60% 50% at 50% 40%,
+ rgba(255, 255, 255, 0.08),
+ rgba(255, 255, 255, 0) 60%
+ ),
+ radial-gradient(
+ 40% 35% at 70% 70%,
+ rgba(255, 255, 255, 0.06),
+ rgba(255, 255, 255, 0) 65%
+ );
+ pointer-events: none;
+`;
+
+const Card = styled.section`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ width: min(760px, 92vw);
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ border-radius: 20px;
+ padding: 28px;
+ background: rgba(16, 16, 16, 0.72);
+ backdrop-filter: blur(6px);
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
+`;
+
+const Title = styled.h1`
+ font-size: 28px;
+ line-height: 1.2;
+ margin: 0 0 8px;
+`;
+
+const Desc = styled.p`
+ margin: 0 10px 18px 0;
+ color: #d8d8d8;
+ font-size: 14px;
+ line-height: 1.6;
+`;
+
+const Features = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+
+ @media (min-width: 720px) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+`;
+
+const Feature = styled.div`
+ padding: 14px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.04);
+`;
+
+const H = styled.h3`
+ margin: 0 0 6px;
+ font-size: 16px;
+ line-height: 1.3;
+`;
+
+const P = styled.p`
+ margin: 0;
+ color: #cfcfcf;
+ font-size: 13px;
+ line-height: 1.5;
+`;
+
+const List = styled.ul`
+ display: grid;
+ gap: 12px;
+ margin: 12px 0 4px;
+ padding: 0;
+ list-style: none;
+`;
+
+const A = styled.a`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 14px 16px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ border-radius: 14px;
+ text-decoration: none;
+ color: #fff;
+ transition:
+ transform 120ms ease,
+ background 120ms ease,
+ border-color 120ms ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ background: rgba(255, 255, 255, 0.06);
+ border-color: rgba(255, 255, 255, 0.28);
+ }
+`;
+
+const FeatImg = styled.img`
+ width: 100%;
+ height: 380px;
+ object-fit: cover;
+ border-radius: 10px;
+ margin: 0 0 10px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+`;
diff --git a/apps/web/src/styles/global.tsx b/apps/web/src/styles/global.tsx
index dd626f3..06fdd43 100644
--- a/apps/web/src/styles/global.tsx
+++ b/apps/web/src/styles/global.tsx
@@ -5,41 +5,70 @@ export default function GlobalStyle() {
return (
);