Skip to content

Commit c007e54

Browse files
trz-21claude
andcommitted
feat: 랜딩 페이지 + 푸터 — 미인증 방문자용 신뢰 화면
- 랜딩(Landing): 히어로/핵심가치 3/작동방식/신뢰 요소(데이터 출처·AI 방법론·보안)/요금제 3종 + 하단 CTA - 루트 라우팅 변경: 미인증 → 랜딩 렌더, 인증(토큰) → /dashboard (root-page 테스트 갱신) - 공통 Footer(브랜드/제품/회사/법적 링크·문의) + /terms·/privacy 플레이스홀더 페이지 - 신규 의존성 없음, 73개 테스트 전체 통과 Refs #22 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fa8a46a commit c007e54

8 files changed

Lines changed: 669 additions & 15 deletions

File tree

frontend/app/page.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
import { useEffect, useState } from 'react';
44
import { useRouter } from 'next/navigation';
55
import { useAuthStore } from '@/src/features/auth/store';
6+
import { Landing } from '@/src/shared/components/Landing';
67

78
/**
8-
* 루트 진입점. 별도 랜딩 화면 없이 인증 상태에 따라 리다이렉트한다.
9-
* - 로그인 상태(토큰 보유) → /dashboard
10-
* - 비로그인 → /login
9+
* 루트 진입점. 인증 상태에 따라 화면을 분기한다.
10+
* - 로그인 상태(토큰 보유) → /dashboard 로 리다이렉트
11+
* - 비로그인 → 랜딩 페이지(<Landing/>)를 렌더 (리다이렉트하지 않음)
1112
*
1213
* 토큰은 zustand persist(localStorage)에서 복원되지만, SSR/CSR 간
13-
* 하이드레이션 미스매치를 피하려고 mount 이후에만 판별·이동한다.
14+
* 하이드레이션 미스매치를 피하려고 mount 이후에만 판별한다.
15+
* 마운트 전에는 깜빡임을 막기 위해 빈 화면을 잠깐 보여주고,
16+
* 마운트 후 토큰이 없으면 랜딩을, 토큰이 있으면 대시보드로 보낸다.
1417
*/
1518
export default function Home() {
1619
const router = useRouter();
@@ -22,14 +25,20 @@ export default function Home() {
2225
}, []);
2326

2427
useEffect(() => {
25-
if (mounted) {
26-
router.replace(token ? '/dashboard' : '/login');
28+
if (mounted && token) {
29+
router.replace('/dashboard');
2730
}
2831
}, [mounted, token, router]);
2932

30-
return (
31-
<div className="flex min-h-screen items-center justify-center bg-gray-50">
32-
<span className="text-sm text-gray-500">로딩 중...</span>
33-
</div>
34-
);
33+
// 인증된 사용자는 대시보드로 이동 중이므로 랜딩 대신 빈 화면을 잠깐 노출.
34+
if (mounted && token) {
35+
return (
36+
<div className="flex min-h-screen items-center justify-center bg-gray-50">
37+
<span className="text-sm text-gray-500">로딩 중...</span>
38+
</div>
39+
);
40+
}
41+
42+
// 미인증 방문자(또는 마운트 전)에게는 랜딩 페이지를 보여준다.
43+
return <Landing />;
3544
}

frontend/app/privacy/page.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Link from 'next/link';
2+
import { Container } from '@/src/shared/components/ui';
3+
import { Footer } from '@/src/shared/components/Footer';
4+
5+
export default function PrivacyPage() {
6+
return (
7+
<div className="min-h-screen bg-white">
8+
<Container className="py-12">
9+
<h1 className="text-3xl font-bold text-gray-900">개인정보 처리방침</h1>
10+
<p className="mt-2 text-sm text-gray-500">시행일: 2026년 6월 14일</p>
11+
12+
<div className="mt-8 space-y-6 text-gray-700 leading-relaxed">
13+
<p>
14+
본 개인정보 처리방침은 현재 서비스 준비 중인 상태로, 정식 출시 전
15+
임시 플레이스홀더 문서입니다. 실제 내용은 서비스 정식 출시 시점에
16+
업데이트될 예정입니다.
17+
</p>
18+
19+
<section>
20+
<h2 className="text-xl font-semibold text-gray-900">
21+
1. 수집하는 개인정보 항목
22+
</h2>
23+
<p className="mt-2">
24+
서비스는 회원 가입 및 서비스 제공을 위해 이메일 주소 등 최소한의
25+
정보를 수집할 수 있습니다. 구체적인 수집 항목은 정식 출시 시점에
26+
명시됩니다.
27+
</p>
28+
</section>
29+
30+
<section>
31+
<h2 className="text-xl font-semibold text-gray-900">
32+
2. 개인정보의 이용 목적
33+
</h2>
34+
<p className="mt-2">
35+
수집한 개인정보는 회원 식별, 서비스 제공 및 운영, 고객 문의 응대
36+
등의 목적으로만 이용되며, 명시된 목적 외의 용도로는 사용되지
37+
않습니다.
38+
</p>
39+
</section>
40+
41+
<section>
42+
<h2 className="text-xl font-semibold text-gray-900">
43+
3. 개인정보의 보관 기간
44+
</h2>
45+
<p className="mt-2">
46+
개인정보는 수집 및 이용 목적이 달성된 후 관련 법령에 따른 보관
47+
기간을 준수하여 지체 없이 파기합니다. 구체적인 보관 기간은 정식
48+
방침에서 안내됩니다.
49+
</p>
50+
</section>
51+
52+
<section>
53+
<h2 className="text-xl font-semibold text-gray-900">
54+
4. 이용자의 권리
55+
</h2>
56+
<p className="mt-2">
57+
이용자는 언제든지 자신의 개인정보에 대한 열람·정정·삭제 및 처리
58+
정지를 요청할 수 있습니다. 요청 방법은 정식 출시 시점에
59+
안내됩니다.
60+
</p>
61+
</section>
62+
</div>
63+
64+
<div className="mt-12">
65+
<Link href="/" className="text-brand-600 hover:text-brand-700">
66+
← 홈으로 돌아가기
67+
</Link>
68+
</div>
69+
</Container>
70+
<Footer />
71+
</div>
72+
);
73+
}

frontend/app/terms/page.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Link from 'next/link';
2+
import { Container } from '@/src/shared/components/ui';
3+
import { Footer } from '@/src/shared/components/Footer';
4+
5+
export default function TermsPage() {
6+
return (
7+
<div className="min-h-screen bg-white">
8+
<Container className="py-12">
9+
<h1 className="text-3xl font-bold text-gray-900">이용약관</h1>
10+
<p className="mt-2 text-sm text-gray-500">시행일: 2026년 6월 14일</p>
11+
12+
<div className="mt-8 space-y-6 text-gray-700 leading-relaxed">
13+
<p>
14+
본 이용약관은 현재 서비스 준비 중인 상태로, 정식 출시 전 임시
15+
플레이스홀더 문서입니다. 실제 약관 내용은 서비스 정식 출시 시점에
16+
업데이트될 예정입니다.
17+
</p>
18+
19+
<section>
20+
<h2 className="text-xl font-semibold text-gray-900">제1조 (목적)</h2>
21+
<p className="mt-2">
22+
본 약관은 Coupang Review AI(이하 &ldquo;서비스&rdquo;)가 제공하는
23+
리뷰 분석 및 인사이트 제공 서비스의 이용 조건과 절차, 이용자와
24+
서비스 운영자의 권리·의무 및 책임 사항을 규정함을 목적으로 합니다.
25+
</p>
26+
</section>
27+
28+
<section>
29+
<h2 className="text-xl font-semibold text-gray-900">
30+
제2조 (약관의 효력 및 변경)
31+
</h2>
32+
<p className="mt-2">
33+
본 약관은 서비스 화면에 게시하거나 기타 방법으로 이용자에게
34+
공지함으로써 효력이 발생합니다. 운영자는 관련 법령을 위반하지 않는
35+
범위에서 약관을 변경할 수 있으며, 변경 시 적용일자 및 변경 사유를
36+
명시하여 사전에 공지합니다.
37+
</p>
38+
</section>
39+
40+
<section>
41+
<h2 className="text-xl font-semibold text-gray-900">
42+
제3조 (서비스의 제공)
43+
</h2>
44+
<p className="mt-2">
45+
서비스는 쿠팡 경쟁 상품의 공개 리뷰 데이터를 분석하여 개선
46+
인사이트를 제공합니다. 구체적인 기능 및 이용 범위는 추후 정식
47+
약관에서 상세히 안내됩니다.
48+
</p>
49+
</section>
50+
</div>
51+
52+
<div className="mt-12">
53+
<Link href="/" className="text-brand-600 hover:text-brand-700">
54+
← 홈으로 돌아가기
55+
</Link>
56+
</div>
57+
</Container>
58+
<Footer />
59+
</div>
60+
);
61+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { Footer } from './Footer';
4+
5+
describe('Footer', () => {
6+
it('renders copyright text', () => {
7+
render(<Footer />);
8+
expect(
9+
screen.getByText(/© 2026 Coupang Review AI. All rights reserved\./)
10+
).toBeInTheDocument();
11+
});
12+
13+
it('renders the one-line introduction', () => {
14+
render(<Footer />);
15+
expect(
16+
screen.getByText(
17+
'쿠팡 경쟁 상품 리뷰를 AI가 분석해 개선 인사이트를 제공합니다.'
18+
)
19+
).toBeInTheDocument();
20+
});
21+
22+
it('renders legal links', () => {
23+
render(<Footer />);
24+
25+
const terms = screen.getByRole('link', { name: '이용약관' });
26+
expect(terms).toBeInTheDocument();
27+
expect(terms).toHaveAttribute('href', '/terms');
28+
29+
const privacy = screen.getByRole('link', { name: '개인정보 처리방침' });
30+
expect(privacy).toBeInTheDocument();
31+
expect(privacy).toHaveAttribute('href', '/privacy');
32+
});
33+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import Link from 'next/link';
2+
import { Container } from '@/src/shared/components/ui';
3+
4+
export function Footer() {
5+
return (
6+
<footer className="bg-brand-950 text-brand-100">
7+
<Container className="py-12">
8+
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-5">
9+
<div className="lg:col-span-2">
10+
<span className="text-lg font-semibold text-white">
11+
Coupang Review AI
12+
</span>
13+
<p className="mt-3 max-w-xs text-sm text-brand-300">
14+
쿠팡 경쟁 상품 리뷰를 AI가 분석해 개선 인사이트를 제공합니다.
15+
</p>
16+
</div>
17+
18+
<div>
19+
<h2 className="text-sm font-semibold text-white">제품</h2>
20+
<ul className="mt-3 space-y-2 text-sm">
21+
<li>
22+
<Link href="/#features" className="hover:text-white">
23+
기능
24+
</Link>
25+
</li>
26+
<li>
27+
<Link href="/#pricing" className="hover:text-white">
28+
요금
29+
</Link>
30+
</li>
31+
</ul>
32+
</div>
33+
34+
<div>
35+
<h2 className="text-sm font-semibold text-white">회사</h2>
36+
<ul className="mt-3 space-y-2 text-sm">
37+
<li>
38+
<Link href="/#about" className="hover:text-white">
39+
소개
40+
</Link>
41+
</li>
42+
</ul>
43+
</div>
44+
45+
<div>
46+
<h2 className="text-sm font-semibold text-white">법적</h2>
47+
<ul className="mt-3 space-y-2 text-sm">
48+
<li>
49+
<Link href="/terms" className="hover:text-white">
50+
이용약관
51+
</Link>
52+
</li>
53+
<li>
54+
<Link href="/privacy" className="hover:text-white">
55+
개인정보 처리방침
56+
</Link>
57+
</li>
58+
</ul>
59+
</div>
60+
</div>
61+
62+
<div className="mt-8 text-sm">
63+
<a
64+
href="mailto:support@coupang-review-ai.example"
65+
className="hover:text-white"
66+
>
67+
문의: support@coupang-review-ai.example
68+
</a>
69+
</div>
70+
71+
<div className="mt-8 border-t border-brand-800 pt-6">
72+
<p className="text-sm text-brand-300">
73+
© 2026 Coupang Review AI. All rights reserved.
74+
</p>
75+
</div>
76+
</Container>
77+
</footer>
78+
);
79+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { Landing } from './Landing';
4+
5+
describe('Landing', () => {
6+
it('renders the hero headline', () => {
7+
render(<Landing />);
8+
expect(
9+
screen.getByRole('heading', {
10+
level: 1,
11+
name: '리뷰가 알려주는 경쟁사를 이기는 법',
12+
})
13+
).toBeInTheDocument();
14+
});
15+
16+
it('renders register and login CTA links', () => {
17+
render(<Landing />);
18+
19+
const registerLinks = screen.getAllByRole('link', {
20+
name: / /,
21+
});
22+
expect(registerLinks.length).toBeGreaterThan(0);
23+
expect(registerLinks[0]).toHaveAttribute('href', '/register');
24+
25+
const loginLinks = screen.getAllByRole('link', { name: '로그인' });
26+
expect(loginLinks.length).toBeGreaterThan(0);
27+
expect(loginLinks[0]).toHaveAttribute('href', '/login');
28+
});
29+
30+
it('renders the three pricing plans', () => {
31+
render(<Landing />);
32+
expect(
33+
screen.getByRole('heading', { level: 3, name: '무료' })
34+
).toBeInTheDocument();
35+
expect(
36+
screen.getByRole('heading', { level: 3, name: '스타터' })
37+
).toBeInTheDocument();
38+
expect(
39+
screen.getByRole('heading', { level: 3, name: '프로' })
40+
).toBeInTheDocument();
41+
});
42+
43+
it('renders the footer copyright', () => {
44+
render(<Landing />);
45+
expect(
46+
screen.getByText(/© 2026 Coupang Review AI. All rights reserved\./)
47+
).toBeInTheDocument();
48+
});
49+
});

0 commit comments

Comments
 (0)