Skip to content

Commit 4adfa83

Browse files
trz-21claude
andcommitted
feat: 대시보드·분석 상세뷰 UI 리프레시 — 그라데이션 히어로·섹션 액센트
대시보드와 분석 상세뷰의 시각 디자인을 네이비/블루 브랜드 위에 깊이·위계· 색상 액센트로 개선. 공유 프리미티브/tailwind.config/globals.css 미변경, 신규 의존성 없이 인라인 SVG만 사용. 기존 테스트 텍스트/동작 전부 보존. - 대시보드: 입력 영역을 brand 그라데이션 히어로(rounded-2xl)로, AnalysisCard 상태 dot·아이콘·호버 리프트, EmptyState 아이콘, 헤더 sticky+backdrop-blur - 분석 상세: 결과 헤더 스탯 타일 히어로, InsightReport 섹션별 액센트 색·아이콘 (TOP3=블루/약점=앰버/구매요인=에메랄드/불만·긍정=red·green), 심각도 한글화 (높음/보통/낮음), AnalysisProgress 폴리시 Refs #28 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7e9687e commit 4adfa83

7 files changed

Lines changed: 343 additions & 79 deletions

File tree

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

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ import { useParams } from 'next/navigation';
55
import { useAnalysisPolling } from '@/src/features/analysis/hooks/useAnalysisPolling';
66
import { InsightReport } from '@/src/features/analysis/components/InsightReport';
77
import { AnalysisProgress } from '@/src/features/analysis/components/AnalysisProgress';
8-
import { Card } from '@/src/shared/components/ui';
98
import type { Analysis, AnalysisResult } from '@/src/features/analysis/types';
109

1110
function BackLink() {
1211
return (
13-
<Link href="/dashboard" className="text-sm text-brand-600 hover:underline">
14-
← 대시보드로 돌아가기
12+
<Link
13+
href="/dashboard"
14+
className="inline-flex items-center gap-1 self-start rounded-lg px-3 py-1.5 text-sm text-gray-600 transition hover:bg-gray-100 hover:text-brand-700"
15+
>
16+
<svg viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4" aria-hidden="true">
17+
<path fillRule="evenodd" d="M12.7 4.3a1 1 0 010 1.4L8.42 10l4.29 4.3a1 1 0 01-1.42 1.4l-5-5a1 1 0 010-1.4l5-5a1 1 0 011.42 0z" clipRule="evenodd" />
18+
</svg>
19+
대시보드로 돌아가기
1520
</Link>
1621
);
1722
}
@@ -34,25 +39,50 @@ function ResultHeader({
3439
0,
3540
);
3641
return (
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-
)}
46-
</Card>
42+
<div className="rounded-2xl bg-gradient-to-br from-brand-700 to-brand-900 p-6 text-white shadow-md">
43+
<p className="text-sm font-medium text-brand-100">분석 완료</p>
44+
<div className="mt-4 grid grid-cols-3 gap-4">
45+
<div>
46+
<p className="text-2xl font-bold sm:text-3xl">{result.products.length}</p>
47+
<p className="mt-0.5 text-xs text-brand-100">분석 상품</p>
48+
</div>
49+
<div>
50+
<p className="text-2xl font-bold sm:text-3xl">{totalReviews}</p>
51+
<p className="mt-0.5 text-xs text-brand-100">총 리뷰 수</p>
52+
</div>
53+
<div>
54+
<p className="text-base font-semibold sm:text-lg">
55+
{completedAt ? formatCompletedAt(completedAt) : '—'}
56+
</p>
57+
<p className="mt-0.5 text-xs text-brand-100">생성 시각</p>
58+
</div>
59+
</div>
60+
</div>
61+
);
62+
}
63+
64+
function WarningIcon() {
65+
return (
66+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
67+
<path d="M10.3 3.86 1.82 18a1 1 0 0 0 .86 1.5h18.64a1 1 0 0 0 .86-1.5L13.7 3.86a1 1 0 0 0-1.72 0z" />
68+
<path d="M12 9v4M12 17h.01" />
69+
</svg>
4770
);
4871
}
4972

5073
function ErrorView({ message }: { message: string }) {
5174
return (
52-
<Card className="border-red-200 bg-red-50 p-6">
53-
<p className="font-medium text-red-800">분석에 실패했습니다.</p>
54-
<p className="mt-1 text-sm text-red-700">{message}</p>
55-
</Card>
75+
<div className="rounded-2xl border border-red-200 bg-red-50 p-6">
76+
<div className="flex items-start gap-3">
77+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-red-100 text-red-600">
78+
<WarningIcon />
79+
</span>
80+
<div>
81+
<p className="font-semibold text-red-800">분석에 실패했습니다.</p>
82+
<p className="mt-1 text-sm text-red-700">{message}</p>
83+
</div>
84+
</div>
85+
</div>
5686
);
5787
}
5888

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ 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';
109
import { EmptyState } from '@/src/shared/components/EmptyState';
1110

1211
export default function DashboardPage() {
@@ -34,21 +33,43 @@ export default function DashboardPage() {
3433
const analyses = data?.analyses ?? [];
3534

3635
return (
37-
<div className="flex flex-col gap-8">
38-
<Card className="p-6">
39-
<h1 className="mb-4 text-xl font-bold text-gray-900">경쟁 상품 분석</h1>
40-
<p className="mb-4 text-sm text-gray-500">
36+
<div className="flex flex-col gap-8 sm:gap-10">
37+
<div className="overflow-hidden rounded-2xl bg-gradient-to-br from-brand-700 to-brand-900 p-6 shadow-lg sm:p-8">
38+
<div className="flex items-center gap-2.5">
39+
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/10 text-white ring-1 ring-inset ring-white/20">
40+
<svg
41+
xmlns="http://www.w3.org/2000/svg"
42+
viewBox="0 0 24 24"
43+
fill="none"
44+
stroke="currentColor"
45+
strokeWidth={2}
46+
strokeLinecap="round"
47+
strokeLinejoin="round"
48+
className="h-5 w-5"
49+
aria-hidden="true"
50+
>
51+
<circle cx="11" cy="11" r="7" />
52+
<path d="m21 21-4.3-4.3" />
53+
</svg>
54+
</span>
55+
<h1 className="text-xl font-bold text-white sm:text-2xl">경쟁 상품 분석</h1>
56+
</div>
57+
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-brand-100">
4158
경쟁 상품의 쿠팡 URL을 입력하면 AI가 리뷰를 분석해 개선 포인트를 알려드립니다.
4259
</p>
43-
<UrlInput onSubmit={handleSubmit} loading={submitting} />
44-
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
45-
</Card>
60+
<div className="mt-5 rounded-xl bg-white p-4 shadow-sm sm:p-5">
61+
<UrlInput onSubmit={handleSubmit} loading={submitting} />
62+
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
63+
</div>
64+
</div>
4665

4766
<section>
48-
<div className="mb-3 flex items-center justify-between gap-2">
67+
<div className="mb-4 flex items-center justify-between gap-2">
4968
<h2 className="text-lg font-semibold text-gray-900">최근 분석</h2>
5069
{!isLoading && analyses.length > 0 && (
51-
<span className="text-sm text-gray-500">{analyses.length}</span>
70+
<span className="rounded-full bg-brand-50 px-2.5 py-0.5 text-xs font-medium text-brand-700">
71+
{analyses.length}
72+
</span>
5273
)}
5374
</div>
5475
{isLoading ? (
@@ -59,7 +80,7 @@ export default function DashboardPage() {
5980
description="위에서 경쟁 상품 URL을 입력해 첫 분석을 시작해 보세요."
6081
/>
6182
) : (
62-
<div className="grid gap-3 sm:grid-cols-2">
83+
<div className="grid gap-4 sm:grid-cols-2">
6384
{analyses.map((analysis) => (
6485
<AnalysisCard key={analysis.id} analysis={analysis} />
6586
))}

frontend/app/(dashboard)/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ export default function DashboardLayout({
3535

3636
return (
3737
<div className="min-h-screen bg-gray-50">
38-
<header className="border-b border-gray-200 bg-white">
38+
<header className="sticky top-0 z-10 border-b border-gray-200 bg-white/80 backdrop-blur">
3939
<Container className="flex items-center justify-between py-3">
4040
<Wordmark />
4141
<Button variant="outline" size="sm" onClick={handleLogout}>
4242
로그아웃
4343
</Button>
4444
</Container>
4545
</header>
46-
<Container className="py-6">{children}</Container>
46+
<Container className="py-8">{children}</Container>
4747
</div>
4848
);
4949
}

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ const STATUS_VARIANT: Record<AnalysisStatus, NonNullable<BadgeProps['variant']>>
2222
failed: 'danger',
2323
};
2424

25+
const STATUS_DOT: Record<AnalysisStatus, string> = {
26+
pending: 'bg-amber-400',
27+
crawling: 'bg-amber-400',
28+
analyzing: 'bg-amber-400',
29+
completed: 'bg-emerald-500',
30+
failed: 'bg-red-500',
31+
};
32+
2533
function summarizeUrls(urls: string[]): string {
2634
if (urls.length === 0) return 'URL 없음';
2735
const [first, ...rest] = urls;
@@ -34,16 +42,41 @@ function formatDate(iso: string): string {
3442
}
3543

3644
export function AnalysisCard({ analysis }: AnalysisCardProps) {
45+
const isCompleted = analysis.status === 'completed';
3746
return (
3847
<Link
3948
href={`/analyses/${analysis.id}`}
40-
className="block rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition hover:border-brand-400 hover:shadow"
49+
className="group block rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition hover:-translate-y-0.5 hover:border-brand-300 hover:shadow-md"
4150
>
4251
<div className="flex items-center justify-between gap-2">
43-
<Badge variant={STATUS_VARIANT[analysis.status]}>{STATUS_LABEL[analysis.status]}</Badge>
44-
<span className="text-xs text-gray-500">{formatDate(analysis.created_at)}</span>
52+
<Badge variant={STATUS_VARIANT[analysis.status]} className="inline-flex items-center gap-1.5">
53+
<span className={`h-1.5 w-1.5 rounded-full ${STATUS_DOT[analysis.status]}`} aria-hidden="true" />
54+
{STATUS_LABEL[analysis.status]}
55+
</Badge>
56+
<span className="text-xs text-gray-400">{formatDate(analysis.created_at)}</span>
57+
</div>
58+
<div className="mt-3 flex items-center gap-2">
59+
<svg
60+
xmlns="http://www.w3.org/2000/svg"
61+
viewBox="0 0 24 24"
62+
fill="none"
63+
stroke="currentColor"
64+
strokeWidth={1.8}
65+
strokeLinecap="round"
66+
strokeLinejoin="round"
67+
className="h-4 w-4 shrink-0 text-gray-400"
68+
aria-hidden="true"
69+
>
70+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
71+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
72+
</svg>
73+
<p className="truncate text-sm text-gray-800">{summarizeUrls(analysis.urls)}</p>
4574
</div>
46-
<p className="mt-2 truncate text-sm text-gray-800">{summarizeUrls(analysis.urls)}</p>
75+
{isCompleted && (
76+
<p className="mt-3 text-right text-xs font-medium text-brand-600">
77+
결과 보기 <span className="transition group-hover:translate-x-0.5"></span>
78+
</p>
79+
)}
4780
</Link>
4881
);
4982
}

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Card } from '@/src/shared/components/ui';
21
import type { AnalysisStatus } from '@/src/features/analysis/types';
32

43
const PROGRESS_LABEL: Record<AnalysisStatus, string> = {
@@ -64,20 +63,21 @@ function CheckIcon() {
6463
function StepIndicator({ state, index }: { state: StepState; index: number }) {
6564
if (state === 'done') {
6665
return (
67-
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-brand-600 text-white">
66+
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-brand-600 text-white shadow-sm shadow-brand-600/30 transition-colors duration-300">
6867
<CheckIcon />
6968
</span>
7069
);
7170
}
7271
if (state === 'current') {
7372
return (
74-
<span className="flex h-10 w-10 shrink-0 animate-pulse items-center justify-center rounded-full border-2 border-brand-600 text-sm font-semibold text-brand-600">
75-
{index + 1}
73+
<span className="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-full border-2 border-brand-600 bg-brand-50 text-sm font-semibold text-brand-700 transition-colors duration-300">
74+
<span className="absolute inset-0 animate-ping rounded-full border-2 border-brand-400/60" aria-hidden="true" />
75+
<span className="relative">{index + 1}</span>
7676
</span>
7777
);
7878
}
7979
return (
80-
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 border-gray-300 text-sm font-semibold text-gray-400">
80+
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full border-2 border-gray-200 bg-gray-50 text-sm font-semibold text-gray-400 transition-colors duration-300">
8181
{index + 1}
8282
</span>
8383
);
@@ -87,8 +87,8 @@ export function AnalysisProgress({ status }: { status: AnalysisStatus }) {
8787
const isActive = status !== 'completed' && status !== 'failed';
8888

8989
return (
90-
<Card className="p-8">
91-
<ol className="flex items-center">
90+
<div className="rounded-2xl border border-gray-200 bg-white p-8 shadow-sm">
91+
<ol className="flex items-start">
9292
{STEPS.map((step, index) => {
9393
const state = stepStateFor(index, status);
9494
const isLast = index === STEPS.length - 1;
@@ -103,17 +103,17 @@ export function AnalysisProgress({ status }: { status: AnalysisStatus }) {
103103
<span
104104
className={
105105
state === 'upcoming'
106-
? 'text-xs font-medium text-gray-400'
107-
: 'text-xs font-medium text-brand-700'
106+
? 'text-xs font-medium text-gray-400 transition-colors duration-300'
107+
: 'text-xs font-semibold text-brand-700 transition-colors duration-300'
108108
}
109109
>
110110
{step.label}
111111
</span>
112112
</div>
113113
{!isLast && (
114114
<div
115-
className={`mx-2 mb-6 h-0.5 flex-1 rounded-full ${
116-
connectorDone ? 'bg-brand-600' : 'bg-gray-300'
115+
className={`mx-2 mt-[22px] h-0.5 flex-1 rounded-full transition-colors duration-500 ${
116+
connectorDone ? 'bg-brand-600' : 'bg-gray-200'
117117
}`}
118118
aria-hidden="true"
119119
/>
@@ -124,16 +124,16 @@ export function AnalysisProgress({ status }: { status: AnalysisStatus }) {
124124
</ol>
125125

126126
{isActive && (
127-
<div className="mt-8 flex flex-col items-center gap-2 text-center">
127+
<div className="mt-8 flex flex-col items-center gap-3 rounded-xl bg-brand-50/60 px-4 py-6 text-center">
128128
<div
129-
className="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-brand-600"
129+
className="h-8 w-8 animate-spin rounded-full border-4 border-brand-100 border-t-brand-600"
130130
role="status"
131131
aria-label="로딩 중"
132132
/>
133-
<p className="font-medium text-gray-900">{PROGRESS_LABEL[status]}</p>
133+
<p className="font-semibold text-brand-800">{PROGRESS_LABEL[status]}</p>
134134
<p className="text-sm text-gray-500">분석 중입니다. 잠시만 기다려 주세요.</p>
135135
</div>
136136
)}
137-
</Card>
137+
</div>
138138
);
139139
}

0 commit comments

Comments
 (0)