feat(legal): privacy/terms/contact + themed 404/500 — 8.2
This commit is contained in:
253
frontend/src/app/contact/page.tsx
Normal file
253
frontend/src/app/contact/page.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import type { Metadata } from 'next';
|
||||
import styled from 'styled-components';
|
||||
import LegalLayout from '@/components/legal/LegalLayout';
|
||||
import { theme } from '@/styles/theme';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '문의하기',
|
||||
description: 'ReLoop 문의 채널 안내',
|
||||
};
|
||||
|
||||
const lastUpdated = '2026-04-12';
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<LegalLayout lastUpdated={lastUpdated}>
|
||||
<Header>
|
||||
<Title>문의하기</Title>
|
||||
<Lead>
|
||||
계정, 결제, 서비스 이용 중 불편 사항이 있다면 아래 채널로 남겨 주세요.
|
||||
영업일 기준 3일 이내에 순차적으로 답변합니다.
|
||||
</Lead>
|
||||
</Header>
|
||||
|
||||
<Grid>
|
||||
<InfoCard>
|
||||
<CardTitle>연락 채널</CardTitle>
|
||||
<InfoList>
|
||||
<InfoItem>
|
||||
<InfoLabel>이메일</InfoLabel>
|
||||
<InfoValue href="mailto:support@reloop.nabomhalang.co.kr">
|
||||
support@reloop.nabomhalang.co.kr
|
||||
</InfoValue>
|
||||
</InfoItem>
|
||||
<InfoItem>
|
||||
<InfoLabel>카카오톡 채널</InfoLabel>
|
||||
<InfoText>@reloop_support</InfoText>
|
||||
</InfoItem>
|
||||
<InfoItem>
|
||||
<InfoLabel>응답 시간</InfoLabel>
|
||||
<InfoText>영업일 기준 3일 이내</InfoText>
|
||||
</InfoItem>
|
||||
</InfoList>
|
||||
</InfoCard>
|
||||
|
||||
<FormCard>
|
||||
<CardTitle>간단한 문의 보내기</CardTitle>
|
||||
<Form
|
||||
action="mailto:support@reloop.nabomhalang.co.kr"
|
||||
method="post"
|
||||
encType="text/plain"
|
||||
>
|
||||
<Field>
|
||||
<StyledLabel htmlFor="contact-name">이름</StyledLabel>
|
||||
<StyledInput
|
||||
id="contact-name"
|
||||
name="name"
|
||||
placeholder="이름을 입력해 주세요"
|
||||
data-selectable="true"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<StyledLabel htmlFor="contact-email">이메일</StyledLabel>
|
||||
<StyledInput
|
||||
id="contact-email"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="답변받을 이메일"
|
||||
data-selectable="true"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<StyledLabel htmlFor="contact-message">메시지</StyledLabel>
|
||||
<StyledTextarea
|
||||
id="contact-message"
|
||||
name="message"
|
||||
placeholder="문의 내용을 적어 주세요"
|
||||
rows={6}
|
||||
data-selectable="true"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<SubmitButton type="submit">메일 앱으로 문의 보내기</SubmitButton>
|
||||
</Form>
|
||||
<FormHint>
|
||||
제출 시 기본 메일 앱이 열립니다. 메일 앱 사용이 어려우면 위 이메일 주소로 직접
|
||||
보내 주세요.
|
||||
</FormHint>
|
||||
</FormCard>
|
||||
</Grid>
|
||||
</LegalLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const Header = styled.header`
|
||||
margin-bottom: 2rem;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0 0 0.75rem;
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
`;
|
||||
|
||||
const Lead = styled.p`
|
||||
margin: 0;
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.7;
|
||||
`;
|
||||
|
||||
const Grid = styled.div`
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
`;
|
||||
|
||||
const BaseCard = styled.section`
|
||||
padding: 1.5rem;
|
||||
border: 1px solid ${theme.color.borderSoftAlpha};
|
||||
border-radius: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
`;
|
||||
|
||||
const InfoCard = styled(BaseCard)``;
|
||||
|
||||
const FormCard = styled(BaseCard)``;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
margin: 0 0 1rem;
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const InfoList = styled.div`
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
const InfoItem = styled.div`
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
`;
|
||||
|
||||
const InfoLabel = styled.p`
|
||||
margin: 0;
|
||||
color: ${theme.color.textMute};
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
`;
|
||||
|
||||
const InfoText = styled.p`
|
||||
margin: 0;
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.7;
|
||||
`;
|
||||
|
||||
const InfoValue = styled.a`
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.7;
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
const Form = styled.form`
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
const Field = styled.div`
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.label`
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
padding: 0.75rem 0.875rem;
|
||||
border: 1px solid ${theme.color.border};
|
||||
border-radius: ${theme.radius.md};
|
||||
background: rgba(28, 35, 64, 0.92);
|
||||
color: ${theme.color.textMain};
|
||||
font-size: 1rem;
|
||||
|
||||
&::placeholder {
|
||||
color: ${theme.color.textMute};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${theme.color.accent};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTextarea = styled.textarea`
|
||||
width: 100%;
|
||||
min-height: 9rem;
|
||||
padding: 0.875rem;
|
||||
border: 1px solid ${theme.color.border};
|
||||
border-radius: ${theme.radius.md};
|
||||
background: rgba(28, 35, 64, 0.92);
|
||||
color: ${theme.color.textMain};
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
|
||||
&::placeholder {
|
||||
color: ${theme.color.textMute};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${theme.color.accent};
|
||||
}
|
||||
`;
|
||||
|
||||
const SubmitButton = styled.button`
|
||||
min-height: 3rem;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: ${theme.radius.md};
|
||||
background: ${theme.color.brandGradient};
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: ${theme.shadow.glowIndigoStrong};
|
||||
}
|
||||
`;
|
||||
|
||||
const FormHint = styled.p`
|
||||
margin: 0.875rem 0 0;
|
||||
color: ${theme.color.textMute};
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
`;
|
||||
107
frontend/src/app/error.tsx
Normal file
107
frontend/src/app/error.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import styled from 'styled-components';
|
||||
import { theme } from '@/styles/theme';
|
||||
|
||||
type ErrorPageProps = {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export default function ErrorPage({ error, reset }: ErrorPageProps) {
|
||||
return (
|
||||
<Shell>
|
||||
<Card>
|
||||
<Title>문제가 생겼어</Title>
|
||||
<Description>예상치 못한 오류야. 잠시 후 다시 시도해봐.</Description>
|
||||
|
||||
<Actions>
|
||||
<PrimaryButton type="button" onClick={() => reset()}>
|
||||
다시 시도
|
||||
</PrimaryButton>
|
||||
<SecondaryLink href="/">홈으로</SecondaryLink>
|
||||
</Actions>
|
||||
|
||||
{error.digest ? <Digest>{error.digest}</Digest> : null}
|
||||
</Card>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
const Shell = styled.main`
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 2rem;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(79, 70, 229, 0.12), transparent 30%),
|
||||
${theme.color.bgDeep};
|
||||
color: ${theme.color.textBright};
|
||||
`;
|
||||
|
||||
const Card = styled.section`
|
||||
width: min(100%, 32rem);
|
||||
padding: 2.25rem;
|
||||
border: 1px solid ${theme.color.borderSoftAlpha};
|
||||
border-radius: 1.5rem;
|
||||
background: rgba(21, 21, 28, 0.92);
|
||||
box-shadow: ${theme.shadow.cardElevated};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
margin: 0.75rem 0 0;
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.975rem;
|
||||
line-height: 1.7;
|
||||
`;
|
||||
|
||||
const Actions = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1.5rem;
|
||||
`;
|
||||
|
||||
const PrimaryButton = styled.button`
|
||||
min-height: 3rem;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: ${theme.radius.md};
|
||||
background: ${theme.color.brandGradient};
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const SecondaryLink = styled(Link)`
|
||||
min-height: 3rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border: 1px solid ${theme.color.border};
|
||||
border-radius: ${theme.radius.md};
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const Digest = styled.code`
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
color: ${theme.color.textMute};
|
||||
font-size: 0.75rem;
|
||||
font-family: ${theme.font.mono};
|
||||
word-break: break-all;
|
||||
`;
|
||||
81
frontend/src/app/global-error.tsx
Normal file
81
frontend/src/app/global-error.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
type GlobalErrorPageProps = {
|
||||
error: Error & { digest?: string };
|
||||
};
|
||||
|
||||
export default function GlobalErrorPage({ error }: GlobalErrorPageProps) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body
|
||||
style={{
|
||||
margin: 0,
|
||||
minHeight: '100vh',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
padding: '32px',
|
||||
background:
|
||||
'radial-gradient(circle at top, rgba(79, 70, 229, 0.16), transparent 34%), #0F0F14',
|
||||
color: '#F8FAFC',
|
||||
fontFamily:
|
||||
"'Pretendard Variable', 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
}}
|
||||
>
|
||||
<main
|
||||
style={{
|
||||
width: 'min(100%, 520px)',
|
||||
padding: '32px',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '24px',
|
||||
background: 'rgba(21, 21, 28, 0.92)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: '32px', lineHeight: 1.2 }}>심각한 오류</h1>
|
||||
<p
|
||||
style={{
|
||||
margin: '12px 0 0',
|
||||
color: '#94A3B8',
|
||||
fontSize: '16px',
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
Something went wrong
|
||||
</p>
|
||||
{error.digest ? (
|
||||
<p
|
||||
style={{
|
||||
margin: '14px 0 0',
|
||||
color: '#64748B',
|
||||
fontFamily:
|
||||
"var(--font-jetbrains-mono), 'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '12px',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{error.digest}
|
||||
</p>
|
||||
) : null}
|
||||
<a
|
||||
href="/"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '48px',
|
||||
marginTop: '24px',
|
||||
padding: '0 20px',
|
||||
borderRadius: '12px',
|
||||
background: 'linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%)',
|
||||
color: '#FFFFFF',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
홈으로
|
||||
</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
142
frontend/src/app/not-found.tsx
Normal file
142
frontend/src/app/not-found.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { animations, theme } from '@/styles/theme';
|
||||
|
||||
export default function NotFound() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<GlowOne aria-hidden="true" />
|
||||
<GlowTwo aria-hidden="true" />
|
||||
|
||||
<Content>
|
||||
<Code>404</Code>
|
||||
<Title>찾을 수 없는 페이지야</Title>
|
||||
<Description>URL을 다시 확인하거나 홈으로 돌아가봐</Description>
|
||||
<Actions>
|
||||
<SecondaryButton type="button" onClick={() => router.back()}>
|
||||
← 이전 페이지
|
||||
</SecondaryButton>
|
||||
<PrimaryLink href="/">홈으로</PrimaryLink>
|
||||
</Actions>
|
||||
</Content>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
const Shell = styled.main`
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
padding: 2rem;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(79, 70, 229, 0.14), transparent 35%),
|
||||
${theme.color.bgDeep};
|
||||
color: ${theme.color.textBright};
|
||||
`;
|
||||
|
||||
const Glow = styled.div`
|
||||
position: absolute;
|
||||
width: 26rem;
|
||||
height: 26rem;
|
||||
border-radius: 50%;
|
||||
filter: blur(90px);
|
||||
opacity: 0.65;
|
||||
`;
|
||||
|
||||
const GlowOne = styled(Glow)`
|
||||
top: -6rem;
|
||||
left: -8rem;
|
||||
background: rgba(79, 70, 229, 0.3);
|
||||
animation: ${animations.float} 8s ease-in-out infinite;
|
||||
`;
|
||||
|
||||
const GlowTwo = styled(Glow)`
|
||||
right: -8rem;
|
||||
bottom: -10rem;
|
||||
background: rgba(124, 58, 237, 0.2);
|
||||
animation: ${animations.float} 10s ease-in-out infinite reverse;
|
||||
`;
|
||||
|
||||
const Content = styled.section`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(100%, 34rem);
|
||||
padding: 2.5rem;
|
||||
border: 1px solid ${theme.color.borderSoftAlpha};
|
||||
border-radius: 1.75rem;
|
||||
background: rgba(21, 21, 28, 0.9);
|
||||
box-shadow: ${theme.shadow.cardElevated};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Code = styled.p`
|
||||
margin: 0 0 0.75rem;
|
||||
font-family: ${theme.font.display};
|
||||
font-size: clamp(4.5rem, 18vw, 7rem);
|
||||
font-weight: 800;
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.06em;
|
||||
background: ${theme.color.brandGradient};
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
margin: 0.75rem 0 0;
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.975rem;
|
||||
line-height: 1.7;
|
||||
`;
|
||||
|
||||
const Actions = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1.75rem;
|
||||
`;
|
||||
|
||||
const actionStyles = css`
|
||||
min-height: 3rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border-radius: ${theme.radius.md};
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`;
|
||||
|
||||
const SecondaryButton = styled.button`
|
||||
${actionStyles}
|
||||
border: 1px solid ${theme.color.border};
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: ${theme.color.textBright};
|
||||
`;
|
||||
|
||||
const PrimaryLink = styled(Link)`
|
||||
${actionStyles}
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: ${theme.color.brandGradient};
|
||||
color: white;
|
||||
`;
|
||||
@@ -460,9 +460,9 @@ export default function LandingPage() {
|
||||
</FooterColumn>
|
||||
<FooterColumn>
|
||||
<FooterHeading>회사</FooterHeading>
|
||||
<FooterText>이용약관</FooterText>
|
||||
<FooterText>개인정보처리방침</FooterText>
|
||||
<FooterText>support@reloop.co</FooterText>
|
||||
<FooterAnchor href="/terms">이용약관</FooterAnchor>
|
||||
<FooterAnchor href="/privacy">개인정보처리방침</FooterAnchor>
|
||||
<FooterAnchor href="/contact">문의하기</FooterAnchor>
|
||||
</FooterColumn>
|
||||
</FooterLinks>
|
||||
</FooterInner>
|
||||
@@ -1435,11 +1435,6 @@ const FooterAnchor = styled.a`
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterText = styled.p`
|
||||
margin: 0;
|
||||
color: ${theme.color.textSub};
|
||||
`;
|
||||
|
||||
const FooterBottom = styled.div`
|
||||
${widthClamp}
|
||||
padding-top: 2rem;
|
||||
|
||||
111
frontend/src/app/privacy/page.tsx
Normal file
111
frontend/src/app/privacy/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { Metadata } from 'next';
|
||||
import styled from 'styled-components';
|
||||
import LegalLayout from '@/components/legal/LegalLayout';
|
||||
import { theme } from '@/styles/theme';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '개인정보처리방침',
|
||||
description: 'ReLoop 개인정보처리방침 안내',
|
||||
};
|
||||
|
||||
const lastUpdated = '2026-04-12';
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<LegalLayout lastUpdated={lastUpdated}>
|
||||
<Header>
|
||||
<Title>개인정보처리방침</Title>
|
||||
</Header>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>1. 수집하는 개인정보 항목</SectionTitle>
|
||||
<BodyText>
|
||||
ReLoop v2는 서비스 제공을 위해 이메일, 닉네임, 프로필 사진(선택), 페르소나
|
||||
설정, 학습 기록을 수집할 수 있습니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>2. 개인정보의 수집 및 이용 목적</SectionTitle>
|
||||
<BodyText>
|
||||
수집한 정보는 회원 식별, 서비스 제공, 복습 큐 생성, 개인화된 학습 경험 제공,
|
||||
통계 계산 및 서비스 품질 개선을 위해 사용됩니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>3. 개인정보의 보유 및 이용 기간</SectionTitle>
|
||||
<BodyText>
|
||||
개인정보는 회원 탈퇴 시까지 보유하며, 탈퇴가 완료된 뒤에는 관련 법령상 보관
|
||||
의무가 없는 한 30일 이내에 삭제합니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>4. 개인정보의 제3자 제공</SectionTitle>
|
||||
<BodyText>
|
||||
ReLoop v2는 이용자의 개인정보를 외부 제3자에게 제공하지 않습니다. 다만
|
||||
법령에 따른 요청이 있는 경우에는 관련 절차에 따라 처리할 수 있습니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>5. 쿠키 사용</SectionTitle>
|
||||
<BodyText>
|
||||
서비스는 로그인 상태 유지를 위해 필요한 JWT 토큰만 저장하며, 그 외의 추적
|
||||
목적 쿠키는 기본적으로 사용하지 않습니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>6. 이용자의 권리</SectionTitle>
|
||||
<BodyText>
|
||||
이용자는 자신의 개인정보에 대해 조회, 수정, 삭제를 요청할 수 있으며, 관련
|
||||
문의는 아래 개인정보보호책임자 연락처를 통해 접수할 수 있습니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>7. 개인정보보호책임자</SectionTitle>
|
||||
<BodyText>
|
||||
개인정보 관련 문의는 아래 연락처로 접수해 주세요.
|
||||
<br />
|
||||
이메일: support@reloop.nabomhalang.co.kr
|
||||
</BodyText>
|
||||
</Section>
|
||||
</LegalLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const Header = styled.header`
|
||||
margin-bottom: 2rem;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
`;
|
||||
|
||||
const Section = styled.section`
|
||||
& + & {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const SectionTitle = styled.h2`
|
||||
margin: 0 0 0.75rem;
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
`;
|
||||
|
||||
const BodyText = styled.p`
|
||||
margin: 0;
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.7;
|
||||
`;
|
||||
127
frontend/src/app/terms/page.tsx
Normal file
127
frontend/src/app/terms/page.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { Metadata } from 'next';
|
||||
import styled from 'styled-components';
|
||||
import LegalLayout from '@/components/legal/LegalLayout';
|
||||
import { theme } from '@/styles/theme';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '이용약관',
|
||||
description: 'ReLoop 이용약관 안내',
|
||||
};
|
||||
|
||||
const lastUpdated = '2026-04-12';
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<LegalLayout lastUpdated={lastUpdated}>
|
||||
<Header>
|
||||
<Title>이용약관</Title>
|
||||
</Header>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>1. 목적</SectionTitle>
|
||||
<BodyText>
|
||||
이 약관은 ReLoop v2 서비스의 이용 조건, 절차, 권리와 의무 및 책임 사항을
|
||||
명확히 하는 것을 목적으로 합니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>2. 용어의 정의</SectionTitle>
|
||||
<BodyText>
|
||||
이 약관에서 사용하는 주요 용어는 다음과 같습니다. 이용자는 ReLoop v2에
|
||||
가입하여 서비스를 이용하는 회원을 의미하고, 콘텐츠는 서비스 내에서 제공되는
|
||||
문제, 기록, 메모 등의 정보를 말합니다. 복습 큐는 학습 기록을 바탕으로
|
||||
자동으로 생성되는 복습 대상 목록이며, 페르소나는 이용자의 학습 특성을 반영한
|
||||
개인화 설정을 뜻합니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>3. 약관의 효력 및 변경</SectionTitle>
|
||||
<BodyText>
|
||||
본 약관은 서비스 화면에 게시되는 시점부터 효력이 발생합니다. 운영상 필요나
|
||||
관련 법령의 변경이 있을 경우 약관은 수정될 수 있으며, 중요한 변경 사항은
|
||||
사전에 공지합니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>4. 서비스의 제공</SectionTitle>
|
||||
<BodyText>
|
||||
ReLoop v2는 개인화된 복습 시스템, 문제집 카탈로그, 학습 통계와 같은 기능을
|
||||
제공합니다. 서비스 내용은 운영 정책에 따라 일부 변경되거나 추가될 수
|
||||
있습니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>5. 이용자의 의무</SectionTitle>
|
||||
<BodyText>
|
||||
이용자는 자신의 계정 정보를 안전하게 관리해야 하며, 타인의 계정을 도용하거나
|
||||
허가 없이 사용하는 행위를 해서는 안 됩니다. 또한 서비스 내에 등록하거나
|
||||
업로드하는 자료에 대해 필요한 권리를 보유해야 하며, 타인의 저작권을 침해하는
|
||||
행위를 해서는 안 됩니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>6. 서비스 이용 제한</SectionTitle>
|
||||
<BodyText>
|
||||
비정상적인 요청 반복, 자동화 어뷰징, 서비스 운영을 방해하는 행위가 확인되는
|
||||
경우 운영자는 사전 통지 후 또는 긴급한 경우 즉시 서비스 이용을 일시 정지할 수
|
||||
있습니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>7. 책임의 한계</SectionTitle>
|
||||
<BodyText>
|
||||
운영자는 천재지변, 시스템 점검, 통신 장애 등 불가피한 사유로 인한 일시적
|
||||
서비스 중단에 대해 책임을 지지 않습니다. 다만 서비스 안정성 확보를 위해
|
||||
합리적인 범위에서 노력합니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>8. 분쟁 해결</SectionTitle>
|
||||
<BodyText>
|
||||
서비스 이용과 관련하여 분쟁이 발생한 경우 관련 법령에 따르며, 관할 법원은
|
||||
서울중앙지방법원으로 합니다.
|
||||
</BodyText>
|
||||
</Section>
|
||||
</LegalLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const Header = styled.header`
|
||||
margin-bottom: 2rem;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
`;
|
||||
|
||||
const Section = styled.section`
|
||||
& + & {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const SectionTitle = styled.h2`
|
||||
margin: 0 0 0.75rem;
|
||||
color: ${theme.color.textBright};
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
`;
|
||||
|
||||
const BodyText = styled.p`
|
||||
margin: 0;
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.7;
|
||||
`;
|
||||
258
frontend/src/components/legal/LegalLayout.tsx
Normal file
258
frontend/src/components/legal/LegalLayout.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import styled from 'styled-components';
|
||||
import { Icon } from '@/components/ui/Icon';
|
||||
import { theme } from '@/styles/theme';
|
||||
|
||||
type LegalLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
lastUpdated: string;
|
||||
};
|
||||
|
||||
const legalLinks = [
|
||||
{ href: '/', label: '홈' },
|
||||
{ href: '/login', label: '로그인' },
|
||||
{ href: '/pricing', label: '가격' },
|
||||
{ href: '/contact', label: '문의' },
|
||||
];
|
||||
|
||||
const footerLinks = [
|
||||
{ href: '/privacy', label: '개인정보처리방침' },
|
||||
{ href: '/terms', label: '이용약관' },
|
||||
{ href: '/contact', label: '문의' },
|
||||
];
|
||||
|
||||
export default function LegalLayout({ children, lastUpdated }: LegalLayoutProps) {
|
||||
return (
|
||||
<Shell>
|
||||
<BackgroundLayer aria-hidden="true">
|
||||
<TopGlow />
|
||||
<BottomGlow />
|
||||
</BackgroundLayer>
|
||||
|
||||
<TopBar>
|
||||
<TopBarInner>
|
||||
<BrandLink href="/" aria-label="ReLoop 홈">
|
||||
<BrandMark>
|
||||
<BrandGlow />
|
||||
<Icon name="infinity" size={18} weight="bold" />
|
||||
</BrandMark>
|
||||
<BrandWordmark>ReLoop</BrandWordmark>
|
||||
</BrandLink>
|
||||
|
||||
<Nav aria-label="법적 페이지 탐색">
|
||||
{legalLinks.map((item) => (
|
||||
<NavLink key={item.href} href={item.href}>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</Nav>
|
||||
</TopBarInner>
|
||||
</TopBar>
|
||||
|
||||
<Main>
|
||||
<ContentCard>
|
||||
<MetaText>최종 업데이트 {lastUpdated}</MetaText>
|
||||
{children}
|
||||
</ContentCard>
|
||||
</Main>
|
||||
|
||||
<Footer>
|
||||
<FooterInner>
|
||||
<FooterCopy>© 2026 ReLoop. All rights reserved.</FooterCopy>
|
||||
<FooterNav aria-label="법적 링크">
|
||||
{footerLinks.map((item) => (
|
||||
<FooterLink key={item.href} href={item.href}>
|
||||
{item.label}
|
||||
</FooterLink>
|
||||
))}
|
||||
</FooterNav>
|
||||
</FooterInner>
|
||||
</Footer>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
const Shell = styled.div`
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(79, 70, 229, 0.14), transparent 34%),
|
||||
${theme.color.bgDeep};
|
||||
color: ${theme.color.textBright};
|
||||
`;
|
||||
|
||||
const BackgroundLayer = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const Glow = styled.div`
|
||||
position: absolute;
|
||||
width: 26rem;
|
||||
height: 26rem;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.6;
|
||||
`;
|
||||
|
||||
const TopGlow = styled(Glow)`
|
||||
top: -10rem;
|
||||
left: -7rem;
|
||||
background: rgba(79, 70, 229, 0.24);
|
||||
`;
|
||||
|
||||
const BottomGlow = styled(Glow)`
|
||||
right: -8rem;
|
||||
bottom: -12rem;
|
||||
background: rgba(124, 58, 237, 0.16);
|
||||
`;
|
||||
|
||||
const TopBar = styled.header`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid ${theme.color.borderSoftAlpha};
|
||||
background: rgba(11, 16, 32, 0.78);
|
||||
backdrop-filter: blur(18px);
|
||||
`;
|
||||
|
||||
const TopBarInner = styled.div`
|
||||
width: min(100%, 72rem);
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.mobile}) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
const BrandLink = styled(Link)`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
const BrandMark = styled.span`
|
||||
position: relative;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
background: ${theme.color.brandGradient};
|
||||
box-shadow: ${theme.shadow.glowIndigo};
|
||||
color: white;
|
||||
`;
|
||||
|
||||
const BrandGlow = styled.span`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 60%);
|
||||
`;
|
||||
|
||||
const BrandWordmark = styled.span`
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
`;
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const NavLink = styled(Link)`
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${theme.color.textBright};
|
||||
}
|
||||
`;
|
||||
|
||||
const Main = styled.main`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 3rem 1.5rem 4rem;
|
||||
`;
|
||||
|
||||
const ContentCard = styled.div`
|
||||
width: min(100%, 45rem);
|
||||
margin: 0 auto;
|
||||
padding: clamp(1.5rem, 4vw, 3rem);
|
||||
border: 1px solid ${theme.color.borderSoftAlpha};
|
||||
border-radius: 1.5rem;
|
||||
background: rgba(21, 21, 28, 0.88);
|
||||
box-shadow: ${theme.shadow.cardElevated};
|
||||
`;
|
||||
|
||||
const MetaText = styled.p`
|
||||
margin: 0 0 1.25rem;
|
||||
color: ${theme.color.textMute};
|
||||
font-size: 0.875rem;
|
||||
font-family: ${theme.font.mono};
|
||||
`;
|
||||
|
||||
const Footer = styled.footer`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-top: 1px solid ${theme.color.borderSoftAlpha};
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
`;
|
||||
|
||||
const FooterInner = styled.div`
|
||||
width: min(100%, 72rem);
|
||||
margin: 0 auto;
|
||||
padding: 1.25rem 1.5rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.mobile}) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterCopy = styled.p`
|
||||
margin: 0;
|
||||
color: ${theme.color.textMute};
|
||||
font-size: 0.875rem;
|
||||
`;
|
||||
|
||||
const FooterNav = styled.nav`
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const FooterLink = styled(Link)`
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 0.875rem;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${theme.color.textBright};
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user