Skip to content

Commit 1e38713

Browse files
authored
Merge pull request #24 from boostcampwm-snu-2026-1/feature/issue-20-design-system
feat: 디자인 시스템 도입 — 브랜드 토큰 + 공통 UI 프리미티브 (#20)
2 parents 31c34b5 + a2c2d6c commit 1e38713

25 files changed

Lines changed: 378 additions & 92 deletions

frontend/app/(auth)/login/page.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Link from 'next/link';
55
import { useRouter } from 'next/navigation';
66
import { login } from '@/src/features/auth/api';
77
import { useAuthStore } from '@/src/features/auth/store';
8+
import { Button, Card, Input } from '@/src/shared/components/ui';
9+
import { Wordmark } from '@/src/shared/components/Wordmark';
810

911
export default function LoginPage() {
1012
const router = useRouter();
@@ -29,46 +31,41 @@ export default function LoginPage() {
2931
}
3032

3133
return (
32-
<main className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
33-
<div className="w-full max-w-sm rounded-lg border border-gray-200 bg-white p-8 shadow-sm">
34+
<main className="flex min-h-screen items-center justify-center bg-brand-50/40 p-4">
35+
<Card className="w-full max-w-sm p-8">
36+
<Wordmark className="mb-6" />
3437
<h1 className="mb-6 text-2xl font-bold text-gray-900">로그인</h1>
3538
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
3639
<label className="flex flex-col gap-1 text-sm font-medium text-gray-700">
3740
이메일
38-
<input
41+
<Input
3942
type="email"
4043
value={email}
4144
onChange={(e) => setEmail(e.target.value)}
4245
required
43-
className="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:border-blue-500 focus:outline-none"
4446
/>
4547
</label>
4648
<label className="flex flex-col gap-1 text-sm font-medium text-gray-700">
4749
비밀번호
48-
<input
50+
<Input
4951
type="password"
5052
value={password}
5153
onChange={(e) => setPassword(e.target.value)}
5254
required
53-
className="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:border-blue-500 focus:outline-none"
5455
/>
5556
</label>
5657
{error && <p className="text-sm text-red-600">{error}</p>}
57-
<button
58-
type="submit"
59-
disabled={loading}
60-
className="mt-2 rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
61-
>
58+
<Button type="submit" disabled={loading} className="mt-2">
6259
{loading ? '로그인 중...' : '로그인'}
63-
</button>
60+
</Button>
6461
</form>
6562
<p className="mt-4 text-center text-sm text-gray-600">
6663
계정이 없으신가요?{' '}
67-
<Link href="/register" className="font-medium text-blue-600 hover:underline">
64+
<Link href="/register" className="font-medium text-brand-600 hover:underline">
6865
회원가입
6966
</Link>
7067
</p>
71-
</div>
68+
</Card>
7269
</main>
7370
);
7471
}

frontend/app/(auth)/register/page.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Link from 'next/link';
55
import { useRouter } from 'next/navigation';
66
import { register } from '@/src/features/auth/api';
77
import { useAuthStore } from '@/src/features/auth/store';
8+
import { Button, Card, Input } from '@/src/shared/components/ui';
9+
import { Wordmark } from '@/src/shared/components/Wordmark';
810

911
export default function RegisterPage() {
1012
const router = useRouter();
@@ -29,48 +31,43 @@ export default function RegisterPage() {
2931
}
3032

3133
return (
32-
<main className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
33-
<div className="w-full max-w-sm rounded-lg border border-gray-200 bg-white p-8 shadow-sm">
34+
<main className="flex min-h-screen items-center justify-center bg-brand-50/40 p-4">
35+
<Card className="w-full max-w-sm p-8">
36+
<Wordmark className="mb-6" />
3437
<h1 className="mb-6 text-2xl font-bold text-gray-900">회원가입</h1>
3538
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
3639
<label className="flex flex-col gap-1 text-sm font-medium text-gray-700">
3740
이메일
38-
<input
41+
<Input
3942
type="email"
4043
value={email}
4144
onChange={(e) => setEmail(e.target.value)}
4245
required
43-
className="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:border-blue-500 focus:outline-none"
4446
/>
4547
</label>
4648
<label className="flex flex-col gap-1 text-sm font-medium text-gray-700">
4749
비밀번호
48-
<input
50+
<Input
4951
type="password"
5052
value={password}
5153
onChange={(e) => setPassword(e.target.value)}
5254
required
5355
minLength={8}
54-
className="rounded-md border border-gray-300 px-3 py-2 text-gray-900 focus:border-blue-500 focus:outline-none"
5556
/>
5657
</label>
5758
<p className="text-xs text-gray-500">비밀번호는 8자 이상이어야 합니다.</p>
5859
{error && <p className="text-sm text-red-600">{error}</p>}
59-
<button
60-
type="submit"
61-
disabled={loading}
62-
className="mt-2 rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:opacity-50"
63-
>
60+
<Button type="submit" disabled={loading} className="mt-2">
6461
{loading ? '가입 중...' : '회원가입'}
65-
</button>
62+
</Button>
6663
</form>
6764
<p className="mt-4 text-center text-sm text-gray-600">
6865
이미 계정이 있으신가요?{' '}
69-
<Link href="/login" className="font-medium text-blue-600 hover:underline">
66+
<Link href="/login" className="font-medium text-brand-600 hover:underline">
7067
로그인
7168
</Link>
7269
</p>
73-
</div>
70+
</Card>
7471
</main>
7572
);
7673
}

frontend/app/(dashboard)/analyses/[id]/page.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Link from 'next/link';
44
import { useParams } from 'next/navigation';
55
import { useAnalysisPolling } from '@/src/features/analysis/hooks/useAnalysisPolling';
66
import { InsightReport } from '@/src/features/analysis/components/InsightReport';
7+
import { Card } from '@/src/shared/components/ui';
78
import type { AnalysisStatus } from '@/src/features/analysis/types';
89

910
const PROGRESS_LABEL: Record<AnalysisStatus, string> = {
@@ -16,35 +17,35 @@ const PROGRESS_LABEL: Record<AnalysisStatus, string> = {
1617

1718
function BackLink() {
1819
return (
19-
<Link href="/dashboard" className="text-sm text-blue-600 hover:underline">
20+
<Link href="/dashboard" className="text-sm text-brand-600 hover:underline">
2021
← 대시보드로 돌아가기
2122
</Link>
2223
);
2324
}
2425

2526
function ProgressView({ status }: { status: AnalysisStatus }) {
2627
return (
27-
<div className="rounded-lg border border-gray-200 bg-white p-8 text-center shadow-sm">
28+
<Card className="p-8 text-center">
2829
<div
29-
className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-600"
30+
className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-brand-600"
3031
role="status"
3132
aria-label="로딩 중"
3233
/>
3334
<p className="font-medium text-gray-900">{PROGRESS_LABEL[status]}</p>
3435
<p className="mt-1 text-sm text-gray-500">분석 중입니다. 잠시만 기다려 주세요.</p>
3536
<div className="mx-auto mt-4 h-1.5 w-48 overflow-hidden rounded-full bg-gray-100">
36-
<div className="h-full w-1/2 animate-pulse rounded-full bg-blue-500" />
37+
<div className="h-full w-1/2 animate-pulse rounded-full bg-brand-500" />
3738
</div>
38-
</div>
39+
</Card>
3940
);
4041
}
4142

4243
function ErrorView({ message }: { message: string }) {
4344
return (
44-
<div className="rounded-lg border border-red-200 bg-red-50 p-6">
45+
<Card className="border-red-200 bg-red-50 p-6">
4546
<p className="font-medium text-red-800">분석에 실패했습니다.</p>
4647
<p className="mt-1 text-sm text-red-700">{message}</p>
47-
</div>
48+
</Card>
4849
);
4950
}
5051

frontend/app/(dashboard)/dashboard/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useQuery } from '@tanstack/react-query';
66
import { createAnalysis, listAnalyses } from '@/src/features/analysis/api';
77
import { UrlInput } from '@/src/features/analysis/components/UrlInput';
88
import { AnalysisCard } from '@/src/features/analysis/components/AnalysisCard';
9+
import { Card } from '@/src/shared/components/ui';
910

1011
export default function DashboardPage() {
1112
const router = useRouter();
@@ -33,11 +34,14 @@ export default function DashboardPage() {
3334

3435
return (
3536
<div className="flex flex-col gap-8">
36-
<section className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
37+
<Card className="p-6">
3738
<h1 className="mb-4 text-xl font-bold text-gray-900">경쟁 상품 분석</h1>
39+
<p className="mb-4 text-sm text-gray-500">
40+
경쟁 상품의 쿠팡 URL을 입력하면 AI가 리뷰를 분석해 개선 포인트를 알려드립니다.
41+
</p>
3842
<UrlInput onSubmit={handleSubmit} loading={submitting} />
3943
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
40-
</section>
44+
</Card>
4145

4246
<section>
4347
<h2 className="mb-3 text-lg font-semibold text-gray-900">최근 분석</h2>

frontend/app/(dashboard)/layout.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { useEffect, useState } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { useAuthStore } from '@/src/features/auth/store';
6+
import { Button, Container } from '@/src/shared/components/ui';
7+
import { Wordmark } from '@/src/shared/components/Wordmark';
68

79
export default function DashboardLayout({
810
children,
@@ -34,18 +36,14 @@ export default function DashboardLayout({
3436
return (
3537
<div className="min-h-screen bg-gray-50">
3638
<header className="border-b border-gray-200 bg-white">
37-
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
38-
<span className="text-lg font-bold text-gray-900">Coupang Review AI</span>
39-
<button
40-
type="button"
41-
onClick={handleLogout}
42-
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
43-
>
39+
<Container className="flex items-center justify-between py-3">
40+
<Wordmark />
41+
<Button variant="outline" size="sm" onClick={handleLogout}>
4442
로그아웃
45-
</button>
46-
</div>
43+
</Button>
44+
</Container>
4745
</header>
48-
<main className="mx-auto max-w-5xl px-4 py-6">{children}</main>
46+
<Container className="py-6">{children}</Container>
4947
</div>
5048
);
5149
}

frontend/app/globals.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
:root {
66
--background: #ffffff;
7-
--foreground: #171717;
7+
--foreground: #0f172a;
88
}
99

1010
@media (prefers-color-scheme: dark) {
@@ -17,7 +17,7 @@
1717
body {
1818
color: var(--foreground);
1919
background: var(--background);
20-
font-family: Arial, Helvetica, sans-serif;
20+
font-family: var(--font-geist-sans), system-ui, sans-serif;
2121
}
2222

2323
@layer utilities {

frontend/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const geistMono = localFont({
1616

1717
export const metadata: Metadata = {
1818
title: "Coupang Review AI",
19-
description: "쿠팡 리뷰 AI 분석",
19+
description: "쿠팡 경쟁 상품 리뷰를 AI가 분석해 개선 인사이트를 제공하는 셀러 도구",
2020
};
2121

2222
export default function RootLayout({

frontend/src/features/analysis/components/AnalysisCard.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Link from 'next/link';
2+
import { Badge, type BadgeProps } from '@/src/shared/components/ui';
23
import type { Analysis, AnalysisStatus } from '@/src/features/analysis/types';
34

45
interface AnalysisCardProps {
@@ -13,12 +14,12 @@ const STATUS_LABEL: Record<AnalysisStatus, string> = {
1314
failed: '실패',
1415
};
1516

16-
const STATUS_BADGE: Record<AnalysisStatus, string> = {
17-
pending: 'bg-amber-100 text-amber-800',
18-
crawling: 'bg-amber-100 text-amber-800',
19-
analyzing: 'bg-amber-100 text-amber-800',
20-
completed: 'bg-green-100 text-green-800',
21-
failed: 'bg-red-100 text-red-800',
17+
const STATUS_VARIANT: Record<AnalysisStatus, NonNullable<BadgeProps['variant']>> = {
18+
pending: 'warning',
19+
crawling: 'warning',
20+
analyzing: 'warning',
21+
completed: 'success',
22+
failed: 'danger',
2223
};
2324

2425
function summarizeUrls(urls: string[]): string {
@@ -36,14 +37,10 @@ export function AnalysisCard({ analysis }: AnalysisCardProps) {
3637
return (
3738
<Link
3839
href={`/analyses/${analysis.id}`}
39-
className="block rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition hover:border-blue-400 hover:shadow"
40+
className="block rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition hover:border-brand-400 hover:shadow"
4041
>
4142
<div className="flex items-center justify-between gap-2">
42-
<span
43-
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${STATUS_BADGE[analysis.status]}`}
44-
>
45-
{STATUS_LABEL[analysis.status]}
46-
</span>
43+
<Badge variant={STATUS_VARIANT[analysis.status]}>{STATUS_LABEL[analysis.status]}</Badge>
4744
<span className="text-xs text-gray-500">{formatDate(analysis.created_at)}</span>
4845
</div>
4946
<p className="mt-2 truncate text-sm text-gray-800">{summarizeUrls(analysis.urls)}</p>

frontend/src/features/analysis/components/InsightReport.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1+
import { Badge, Card, type BadgeProps } from '@/src/shared/components/ui';
12
import type { AnalysisResult, ProductSummary } from '@/src/features/analysis/types';
23

34
interface InsightReportProps {
45
result: AnalysisResult;
56
}
67

7-
const SEVERITY_BADGE: Record<string, string> = {
8-
high: 'bg-red-100 text-red-800',
9-
medium: 'bg-amber-100 text-amber-800',
10-
low: 'bg-gray-100 text-gray-700',
8+
const SEVERITY_VARIANT: Record<string, NonNullable<BadgeProps['variant']>> = {
9+
high: 'danger',
10+
medium: 'warning',
11+
low: 'neutral',
1112
};
1213

13-
function severityBadge(severity: string): string {
14-
return SEVERITY_BADGE[severity] ?? 'bg-gray-100 text-gray-700';
14+
function severityVariant(severity: string): NonNullable<BadgeProps['variant']> {
15+
return SEVERITY_VARIANT[severity] ?? 'neutral';
1516
}
1617

1718
function Section({ title, children }: { title: string; children: React.ReactNode }) {
1819
return (
19-
<section className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
20+
<Card className="p-6">
2021
<h2 className="mb-4 text-lg font-semibold text-gray-900">{title}</h2>
2122
{children}
22-
</section>
23+
</Card>
2324
);
2425
}
2526

@@ -79,7 +80,7 @@ export function InsightReport({ result }: InsightReportProps) {
7980
<ol className="flex flex-col gap-4">
8081
{improvements.map((point) => (
8182
<li key={point.rank} className="flex gap-3">
82-
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-blue-600 text-sm font-bold text-white">
83+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-brand-600 text-sm font-bold text-white">
8384
{point.rank}
8485
</span>
8586
<div>
@@ -115,7 +116,7 @@ export function InsightReport({ result }: InsightReportProps) {
115116
{insights.purchase_drivers.map((driver, i) => (
116117
<li
117118
key={i}
118-
className="rounded-full bg-blue-50 px-3 py-1 text-sm text-blue-700"
119+
className="rounded-full bg-brand-50 px-3 py-1 text-sm text-brand-700"
119120
>
120121
{driver}
121122
</li>
@@ -136,11 +137,7 @@ export function InsightReport({ result }: InsightReportProps) {
136137
<li key={i} className="flex items-center justify-between gap-2">
137138
<span className="text-sm text-gray-800">{c.text}</span>
138139
<span className="flex items-center gap-2">
139-
<span
140-
className={`rounded-full px-2 py-0.5 text-xs font-medium ${severityBadge(c.severity)}`}
141-
>
142-
{c.severity}
143-
</span>
140+
<Badge variant={severityVariant(c.severity)}>{c.severity}</Badge>
144141
<span className="text-xs text-gray-500">{c.count}</span>
145142
</span>
146143
</li>

0 commit comments

Comments
 (0)