-
Notifications
You must be signed in to change notification settings - Fork 1
feature: 다운로드 페이지 추가 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| // src/hooks/useConstellation.ts | ||
| import { useEffect, useRef } from 'react'; | ||
|
|
||
| export function useConstellation() { | ||
| const ref = useRef<HTMLCanvasElement | null>(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); // 화면 크기에 따른 밀도 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; | ||
|
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| export default function Home() { | ||
| const canvasRef = useConstellation(); | ||
|
|
||
| return ( | ||
| <Wrap> | ||
| <StarsCanvas ref={canvasRef} /> | ||
| <Glow /> | ||
| <Card> | ||
| <Title>Runova</Title> | ||
| <Desc> | ||
| 도시 위에 <b>GPS 아트</b>를 새기는 러닝 앱. 달려서 나만의 별자리 | ||
| 지도를 완성해 보세요. | ||
| </Desc> | ||
| </Card> | ||
| <Card> | ||
| <Title as="h2">다운로드 링크</Title> | ||
| <List> | ||
| <li> | ||
| <A href={DL_URL} rel="noopener"> | ||
| <span>Android APK (v1.0.0)</span> | ||
| <span>⬇</span> | ||
| </A> | ||
| </li> | ||
| </List> | ||
| </Card> | ||
| <Card> | ||
| <Features> | ||
| <Feature> | ||
| <FeatImg src="/images/navigate.jpg" alt="내비게이션" /> | ||
| <H>경로 안내 제공</H> | ||
| <P> | ||
| 실시간 경로 트래킹과 경로 안내를 통해, 나만의 GPS 아트를 따라 | ||
| 달려보세요. | ||
| </P> | ||
| </Feature> | ||
| <Feature> | ||
| <FeatImg src="/images/track.jpg" alt="정밀 기록" /> | ||
| <H>경로 설계</H> | ||
| <P>원하는 모양대로 경로를 그리고 다른 사람들과 공유해 보세요.</P> | ||
| </Feature> | ||
| <Feature> | ||
| <FeatImg src="/images/community.jpg" alt="공유" /> | ||
| <H>커뮤니티</H> | ||
| <P> | ||
| 완주 인증 사진, 경로 공유, 팀메이트 구하기까지, 커뮤니티에서 함께 | ||
| 나눠요. | ||
| </P> | ||
| </Feature> | ||
| </Features> | ||
| </Card> | ||
| </Wrap> | ||
| ); | ||
| } | ||
|
|
||
| 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; | ||
| `; | ||
|
Comment on lines
+61
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스타일 가이드에 따라 색상, 간격(spacing), 폰트 크기 등 재사용 가능한 스타일 속성은 theme 또는 constants 파일로 관리하는 것이 좋습니다.1 현재 Style Guide ReferencesFootnotes
|
||
|
|
||
| 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; | ||
| `; | ||
|
Comment on lines
+142
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| object-fit: cover; | ||
| border-radius: 10px; | ||
| margin: 0 0 10px; | ||
| border: 1px solid rgba(255, 255, 255, 0.1); | ||
| `; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
스타일 가이드에 따라
stars배열의 타입을 인라인으로 정의하는 대신, 별도의interface나type으로 분리하는 것이 좋습니다.1 아래와 같이Star인터페이스를 훅 외부나 파일 상단에 정의하고 사용하면 코드 가독성이 향상되고 타입 재사용이 용이해집니다.Style Guide References
Footnotes
Props, State, Navigation Param 등은 interface 또는 type alias로 관리합니다. ↩