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
4 changes: 2 additions & 2 deletions apps/web/index.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!doctype html>
<html lang="en">
<html lang="kr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link
href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css"
rel="stylesheet"
Expand Down
Binary file added apps/web/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/images/community.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/images/navigate.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/images/track.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion apps/web/public/vite.svg

This file was deleted.

5 changes: 3 additions & 2 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,7 +11,7 @@ export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/community" replace />} />
<Route path="/" element={<Home />} />
<Route path="/community" element={<CommunityList />} />
<Route path="/community/feed/:type" element={<CommunityFeed />} />
<Route path="/community/:id" element={<CommunityDetail />} />
Expand Down
106 changes: 106 additions & 0 deletions apps/web/src/hooks/useConstellation.ts
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 }[] =
[];
Comment on lines +17 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

스타일 가이드에 따라 stars 배열의 타입을 인라인으로 정의하는 대신, 별도의 interfacetype으로 분리하는 것이 좋습니다.1 아래와 같이 Star 인터페이스를 훅 외부나 파일 상단에 정의하고 사용하면 코드 가독성이 향상되고 타입 재사용이 용이해집니다.

interface Star {
  x: number;
  y: number;
  vx: number;
  vy: number;
  r: number;
}
Suggested change
const stars: { x: number; y: number; vx: number; vy: number; r: number }[] =
[];
const stars: Star[] = [];

Style Guide References

Footnotes

  1. Props, State, Navigation Param 등은 interface 또는 type alias로 관리합니다.


const gen = () => {
stars.length = 0;
const { clientWidth: w, clientHeight: h } = canvas;
const count = Math.floor((w * h) / 14000); // 화면 크기에 따른 밀도

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

코드 내에 14000과 같은 매직 넘버가 여러 곳에 사용되고 있습니다. 스타일 가이드에 따라 이러한 값들은 의미를 알 수 있는 이름의 상수로 선언하여 관리하는 것이 좋습니다.1 예를 들어, STAR_DENSITY_FACTOR = 14000과 같이 정의할 수 있습니다. 다른 매직 넘버들(e.g., 0.15, 1.6, 120, 20)도 마찬가지로 상수로 추출해주세요.

Style Guide References

Footnotes

  1. 매직 넘버(예: 200, 0.5 등)는 이름 있는 상수로 치환합니다.

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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

스타일 가이드에 따르면 재사용 가능한 색상은 theme 또는 constants 파일로 관리해야 합니다.1 #fff와 같은 하드코딩된 색상 값을 디자인 시스템의 일부인 theme.colors.white와 같은 형태로 참조하도록 변경하는 것을 권장합니다. 73번째 줄의 #ffffff도 마찬가지입니다.

Style Guide References

Footnotes

  1. 재사용 가능한 색상, 폰트, spacing은 theme 또는 constants 파일로 관리합니다.

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;
}
192 changes: 192 additions & 0 deletions apps/web/src/pages/Home/index.tsx
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

스타일 가이드에 따라 DL_URL과 같은 상수는 컴포넌트 파일 내에 하드코딩하기보다 src/constants.ts와 같은 파일에서 관리하는 것이 좋습니다.1 이렇게 하면 상수를 중앙에서 관리할 수 있어 유지보수성이 향상됩니다.

Style Guide References

Footnotes

  1. 상수는 하드코딩하지 말고 constants.ts 또는 theme.ts에 관리합니다.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

스타일 가이드에 따라 색상, 간격(spacing), 폰트 크기 등 재사용 가능한 스타일 속성은 theme 또는 constants 파일로 관리하는 것이 좋습니다.1 현재 Wrap, Card 등 여러 컴포넌트에 스타일 값이 하드코딩되어 있습니다. 예를 들어 gap: 18px, padding: 24px 16px 40px, background: #000 등을 theme.spacing.md, theme.colors.black와 같이 theme 객체를 참조하도록 리팩토링하면 일관성 유지와 유지보수에 도움이 됩니다.

Style Guide References

Footnotes

  1. 재사용 가능한 색상, 폰트, spacing은 theme 또는 constants 파일로 관리합니다.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

컴포넌트의 이름을 H, P와 같이 한 글자로 지정하면 코드의 가독성을 해칠 수 있습니다. 다른 개발자가 코드를 이해하기 어렵게 만들고, 유지보수를 어렵게 할 수 있습니다. FeatureTitle, FeatureDescription과 같이 역할이 명확히 드러나는 이름으로 변경하고, JSX에서도 <H> 대신 <FeatureTitle>로 사용하는 것을 권장합니다.


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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

height: 380px로 높이를 고정하면 다양한 화면 크기에서 이미지 비율이 왜곡될 수 있습니다. aspect-ratio 속성을 사용하여 이미지의 종횡비를 유지하거나, height: auto로 설정하여 자연스러운 크기를 유지하는 것을 고려해 보세요. 이는 반응형 디자인에 더 적합합니다.

object-fit: cover;
border-radius: 10px;
margin: 0 0 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
`;
Loading