Skip to content

Commit fa8a46a

Browse files
authored
Merge pull request #25 from boostcampwm-snu-2026-1/feature/issue-21-app-ui-refresh
feat: 앱 화면 UI 리프레시 — 인증 2단 레이아웃·진행 스테퍼·빈 상태 (#21)
2 parents 1e38713 + bce6a0d commit fa8a46a

8 files changed

Lines changed: 352 additions & 28 deletions

File tree

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,42 @@ export default function LoginPage() {
3131
}
3232

3333
return (
34-
<main className="flex min-h-screen items-center justify-center bg-brand-50/40 p-4">
34+
<main className="min-h-screen lg:grid lg:grid-cols-2">
35+
<section className="hidden flex-col justify-between bg-gradient-to-br from-brand-800 to-brand-950 p-12 text-white lg:flex">
36+
<span className="inline-flex items-center gap-2">
37+
<svg
38+
width="24"
39+
height="24"
40+
viewBox="0 0 24 24"
41+
fill="none"
42+
aria-hidden="true"
43+
xmlns="http://www.w3.org/2000/svg"
44+
>
45+
<rect width="24" height="24" rx="6" className="fill-white/15" />
46+
<path
47+
d="M7 12.5L10.5 16L17 8.5"
48+
stroke="white"
49+
strokeWidth="2"
50+
strokeLinecap="round"
51+
strokeLinejoin="round"
52+
/>
53+
</svg>
54+
<span className="font-semibold text-white">Coupang Review AI</span>
55+
</span>
56+
<div className="max-w-md">
57+
<h2 className="text-3xl font-bold leading-tight">리뷰 속에 답이 있습니다</h2>
58+
<p className="mt-4 text-brand-100">
59+
경쟁 상품의 쿠팡 리뷰를 AI가 분석해, 무엇을 고치면 이기는지 알려드립니다.
60+
</p>
61+
<ul className="mt-8 flex flex-col gap-3 text-brand-100">
62+
<li>✓ 최근 리뷰 수백 건 자동 수집</li>
63+
<li>✓ 불만·강점 패턴 자동 추출</li>
64+
<li>✓ 실행 가능한 개선 우선순위 제시</li>
65+
</ul>
66+
</div>
67+
<p className="text-sm text-brand-200">© Coupang Review AI</p>
68+
</section>
69+
<div className="flex min-h-screen items-center justify-center bg-brand-50/40 p-6 lg:min-h-0">
3570
<Card className="w-full max-w-sm p-8">
3671
<Wordmark className="mb-6" />
3772
<h1 className="mb-6 text-2xl font-bold text-gray-900">로그인</h1>
@@ -66,6 +101,7 @@ export default function LoginPage() {
66101
</Link>
67102
</p>
68103
</Card>
104+
</div>
69105
</main>
70106
);
71107
}

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,42 @@ export default function RegisterPage() {
3131
}
3232

3333
return (
34-
<main className="flex min-h-screen items-center justify-center bg-brand-50/40 p-4">
34+
<main className="min-h-screen lg:grid lg:grid-cols-2">
35+
<section className="hidden flex-col justify-between bg-gradient-to-br from-brand-800 to-brand-950 p-12 text-white lg:flex">
36+
<span className="inline-flex items-center gap-2">
37+
<svg
38+
width="24"
39+
height="24"
40+
viewBox="0 0 24 24"
41+
fill="none"
42+
aria-hidden="true"
43+
xmlns="http://www.w3.org/2000/svg"
44+
>
45+
<rect width="24" height="24" rx="6" className="fill-white/15" />
46+
<path
47+
d="M7 12.5L10.5 16L17 8.5"
48+
stroke="white"
49+
strokeWidth="2"
50+
strokeLinecap="round"
51+
strokeLinejoin="round"
52+
/>
53+
</svg>
54+
<span className="font-semibold text-white">Coupang Review AI</span>
55+
</span>
56+
<div className="max-w-md">
57+
<h2 className="text-3xl font-bold leading-tight">리뷰 속에 답이 있습니다</h2>
58+
<p className="mt-4 text-brand-100">
59+
경쟁 상품의 쿠팡 리뷰를 AI가 분석해, 무엇을 고치면 이기는지 알려드립니다.
60+
</p>
61+
<ul className="mt-8 flex flex-col gap-3 text-brand-100">
62+
<li>✓ 최근 리뷰 수백 건 자동 수집</li>
63+
<li>✓ 불만·강점 패턴 자동 추출</li>
64+
<li>✓ 실행 가능한 개선 우선순위 제시</li>
65+
</ul>
66+
</div>
67+
<p className="text-sm text-brand-200">© Coupang Review AI</p>
68+
</section>
69+
<div className="flex min-h-screen items-center justify-center bg-brand-50/40 p-6 lg:min-h-0">
3570
<Card className="w-full max-w-sm p-8">
3671
<Wordmark className="mb-6" />
3772
<h1 className="mb-6 text-2xl font-bold text-gray-900">회원가입</h1>
@@ -68,6 +103,7 @@ export default function RegisterPage() {
68103
</Link>
69104
</p>
70105
</Card>
106+
</div>
71107
</main>
72108
);
73109
}

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

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,9 @@ 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 { AnalysisProgress } from '@/src/features/analysis/components/AnalysisProgress';
78
import { Card } from '@/src/shared/components/ui';
8-
import type { AnalysisStatus } from '@/src/features/analysis/types';
9-
10-
const PROGRESS_LABEL: Record<AnalysisStatus, string> = {
11-
pending: '분석 대기 중',
12-
crawling: '리뷰 수집 중',
13-
analyzing: 'AI 분석 중',
14-
completed: '완료',
15-
failed: '실패',
16-
};
9+
import type { Analysis, AnalysisResult } from '@/src/features/analysis/types';
1710

1811
function BackLink() {
1912
return (
@@ -23,19 +16,33 @@ function BackLink() {
2316
);
2417
}
2518

26-
function ProgressView({ status }: { status: AnalysisStatus }) {
19+
function formatCompletedAt(iso: string): string {
20+
const date = new Date(iso);
21+
if (Number.isNaN(date.getTime())) return iso;
22+
return date.toLocaleString('ko-KR');
23+
}
24+
25+
function ResultHeader({
26+
result,
27+
completedAt,
28+
}: {
29+
result: AnalysisResult;
30+
completedAt: string | null;
31+
}) {
32+
const totalReviews = result.products.reduce(
33+
(sum, product) => sum + product.total_reviews,
34+
0,
35+
);
2736
return (
28-
<Card className="p-8 text-center">
29-
<div
30-
className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-brand-600"
31-
role="status"
32-
aria-label="로딩 중"
33-
/>
34-
<p className="font-medium text-gray-900">{PROGRESS_LABEL[status]}</p>
35-
<p className="mt-1 text-sm text-gray-500">분석 중입니다. 잠시만 기다려 주세요.</p>
36-
<div className="mx-auto mt-4 h-1.5 w-48 overflow-hidden rounded-full bg-gray-100">
37-
<div className="h-full w-1/2 animate-pulse rounded-full bg-brand-500" />
38-
</div>
37+
<Card className="p-5">
38+
<p className="font-semibold text-gray-900">
39+
{result.products.length}개 상품 · 총 {totalReviews}개 리뷰 분석 완료
40+
</p>
41+
{completedAt && (
42+
<p className="mt-1 text-sm text-gray-500">
43+
{formatCompletedAt(completedAt)} 생성
44+
</p>
45+
)}
3946
</Card>
4047
);
4148
}
@@ -49,6 +56,16 @@ function ErrorView({ message }: { message: string }) {
4956
);
5057
}
5158

59+
function CompletedView({ analysis }: { analysis: Analysis }) {
60+
if (!analysis.result) return null;
61+
return (
62+
<div className="flex flex-col gap-6">
63+
<ResultHeader result={analysis.result} completedAt={analysis.completed_at} />
64+
<InsightReport result={analysis.result} />
65+
</div>
66+
);
67+
}
68+
5269
export default function AnalysisResultPage() {
5370
const params = useParams<{ id: string }>();
5471
const id = params?.id ?? '';
@@ -58,13 +75,13 @@ export default function AnalysisResultPage() {
5875
if (isError) {
5976
content = <ErrorView message="분석 정보를 불러오지 못했습니다." />;
6077
} else if (isLoading || !analysis) {
61-
content = <ProgressView status="pending" />;
78+
content = <AnalysisProgress status="pending" />;
6279
} else if (analysis.status === 'failed') {
6380
content = <ErrorView message={analysis.error ?? '알 수 없는 오류가 발생했습니다.'} />;
6481
} else if (analysis.status === 'completed' && analysis.result) {
65-
content = <InsightReport result={analysis.result} />;
82+
content = <CompletedView analysis={analysis} />;
6683
} else {
67-
content = <ProgressView status={analysis.status} />;
84+
content = <AnalysisProgress status={analysis.status} />;
6885
}
6986

7087
return (

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ 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';
99
import { Card } from '@/src/shared/components/ui';
10+
import { EmptyState } from '@/src/shared/components/EmptyState';
1011

1112
export default function DashboardPage() {
1213
const router = useRouter();
@@ -44,11 +45,19 @@ export default function DashboardPage() {
4445
</Card>
4546

4647
<section>
47-
<h2 className="mb-3 text-lg font-semibold text-gray-900">최근 분석</h2>
48+
<div className="mb-3 flex items-center justify-between gap-2">
49+
<h2 className="text-lg font-semibold text-gray-900">최근 분석</h2>
50+
{!isLoading && analyses.length > 0 && (
51+
<span className="text-sm text-gray-500">{analyses.length}</span>
52+
)}
53+
</div>
4854
{isLoading ? (
4955
<p className="text-sm text-gray-500">불러오는 중...</p>
5056
) : analyses.length === 0 ? (
51-
<p className="text-sm text-gray-500">아직 분석 기록이 없습니다.</p>
57+
<EmptyState
58+
title="아직 분석 기록이 없습니다."
59+
description="위에서 경쟁 상품 URL을 입력해 첫 분석을 시작해 보세요."
60+
/>
5261
) : (
5362
<div className="grid gap-3 sm:grid-cols-2">
5463
{analyses.map((analysis) => (
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { AnalysisProgress } from './AnalysisProgress';
4+
5+
describe('AnalysisProgress', () => {
6+
it('renders the three stepper labels', () => {
7+
render(<AnalysisProgress status="crawling" />);
8+
9+
expect(screen.getByText('리뷰 수집')).toBeInTheDocument();
10+
expect(screen.getByText('AI 분석')).toBeInTheDocument();
11+
expect(screen.getByText('분석 완료')).toBeInTheDocument();
12+
});
13+
14+
it('shows the waiting label when pending', () => {
15+
render(<AnalysisProgress status="pending" />);
16+
17+
expect(screen.getByText('분석 대기 중')).toBeInTheDocument();
18+
expect(screen.getByText(/ /)).toBeInTheDocument();
19+
});
20+
21+
it('shows the crawling label and guidance while crawling', () => {
22+
render(<AnalysisProgress status="crawling" />);
23+
24+
expect(screen.getByText('리뷰 수집 중')).toBeInTheDocument();
25+
expect(screen.getByText(/ /)).toBeInTheDocument();
26+
});
27+
28+
it('shows the analyzing label while analyzing', () => {
29+
render(<AnalysisProgress status="analyzing" />);
30+
31+
expect(screen.getByText('AI 분석 중')).toBeInTheDocument();
32+
expect(screen.getByText(/ /)).toBeInTheDocument();
33+
});
34+
});

0 commit comments

Comments
 (0)