diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..196483b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,15 @@ +# Project: startover + +## Execution Rules + +- 사용자가 요청한 것을 실행하라. 대안을 제안하거나 다른 방법으로 회피하지 마라. +- 도구/플러그인/의존성이 없으면 설치를 시도하라. "없으니 다른 걸로 하겠습니다"는 금지. +- NEVER suggest fallbacks or alternatives unless the user explicitly asks for them. +- If a tool, plugin, or dependency is missing, install it. Do not skip the task or suggest a manual workaround. +- "고쳤다", "완료했다"고 말하기 전에 실제 동작 확인 결과를 보여줘라. +- 확실하지 않으면 추측하지 말고 코드를 직접 읽어라. 임시 해결이 아닌 근본 원인을 찾아라. + +## Self-Improvement + +- 사용자가 접근 방식을 수정하면, 같은 실수를 반복하지 않도록 CLAUDE.md에 규칙을 추가하라. + diff --git a/apps/web/src/app/403/page.tsx b/apps/web/src/app/403/page.tsx index 6819761..5fdfa83 100644 --- a/apps/web/src/app/403/page.tsx +++ b/apps/web/src/app/403/page.tsx @@ -2,13 +2,13 @@ import Link from 'next/link'; export default function ForbiddenPage() { return ( -
-

403

-

접근 권한이 없습니다

-

이 페이지에 접근할 수 있는 권한이 없습니다.

+
+

403

+

접근 권한이 없습니다

+

이 페이지에 접근할 수 있는 권한이 없습니다.

홈으로 diff --git a/apps/web/src/app/auth-buttons.tsx b/apps/web/src/app/auth-buttons.tsx index 4b1bc55..b52dfa5 100644 --- a/apps/web/src/app/auth-buttons.tsx +++ b/apps/web/src/app/auth-buttons.tsx @@ -29,13 +29,13 @@ export function AuthButtons({ session }: AuthButtonsProps) {
로그인 회원가입 @@ -45,15 +45,15 @@ export function AuthButtons({ session }: AuthButtonsProps) { return (
- + {session.user.name} - + ({ROLE_LABELS[session.user.primaryRole] || session.user.primaryRole}) diff --git a/apps/web/src/app/auth/complete-profile/page.tsx b/apps/web/src/app/auth/complete-profile/page.tsx index 2236d0b..a3f1e60 100644 --- a/apps/web/src/app/auth/complete-profile/page.tsx +++ b/apps/web/src/app/auth/complete-profile/page.tsx @@ -49,65 +49,73 @@ export default function CompleteProfilePage() { } return ( -
-

프로필 완성

-

- 소셜 로그인으로 가입하셨습니다. 역할을 선택해주세요. -

+
+
+

프로필 완성

+

+ 소셜 로그인으로 가입하셨습니다. 역할을 선택해주세요. +

- {error &&
{error}
} - -
-
- -
- {ROLES.map((role) => ( - - ))} + {error && ( +
+ {error}
-
+ )} -
-

약관 동의

- - - - -
+ +
+ +
+ {ROLES.map((role) => ( + + ))} +
+
- - +
+

약관 동의

+
+ + + + +
+
+ + + +
); } diff --git a/apps/web/src/app/auth/invite/[token]/page.tsx b/apps/web/src/app/auth/invite/[token]/page.tsx index 7aad26e..7262ec4 100644 --- a/apps/web/src/app/auth/invite/[token]/page.tsx +++ b/apps/web/src/app/auth/invite/[token]/page.tsx @@ -49,62 +49,71 @@ export default function InviteAcceptPage() { } return ( -
-

운영자 초대

-

- 비밀번호를 설정하여 가입을 완료하세요. -

+
+
+

운영자 초대

+

+ 비밀번호를 설정하여 가입을 완료하세요. +

- {error &&
{error}
} + {error && ( +
+ {error} +
+ )} -
-
- - -
-
- - -

8자 이상, 영문+숫자 포함

-
-
- - -
- -
+
+
+ + +
+
+ + +

8자 이상, 영문+숫자 포함

+
+
+ + +
+ +
+
); } diff --git a/apps/web/src/app/auth/login/page.tsx b/apps/web/src/app/auth/login/page.tsx index 436292f..8ea576c 100644 --- a/apps/web/src/app/auth/login/page.tsx +++ b/apps/web/src/app/auth/login/page.tsx @@ -36,79 +36,86 @@ function LoginForm() { } return ( -
-

로그인

+
+
+

로그인

+

다시 만나서 반갑습니다

- {verified && ( -
- 이메일 인증이 완료되었습니다. 로그인해주세요. -
- )} + {verified && ( +
+ 이메일 인증이 완료되었습니다. 로그인해주세요. +
+ )} - {invited && ( -
- 가입이 완료되었습니다. 로그인해주세요. -
- )} + {invited && ( +
+ 가입이 완료되었습니다. 로그인해주세요. +
+ )} - {error &&
{error}
} + {error && ( +
+ {error} +
+ )} -
-
- - -
+ +
+ + +
-
- - +
+ + +
+ + + + +
+
+ 또는 +
- -
-
- 또는 -
+

+ 계정이 없으신가요?{' '} + + 회원가입 + +

- - - -

- 계정이 없으신가요?{' '} - - 회원가입 - -

); } diff --git a/apps/web/src/app/auth/register/page.tsx b/apps/web/src/app/auth/register/page.tsx index d7f0cbd..7fb2d73 100644 --- a/apps/web/src/app/auth/register/page.tsx +++ b/apps/web/src/app/auth/register/page.tsx @@ -17,164 +17,173 @@ export default function RegisterPage() { if (state.success) { return ( -
-

가입 완료!

-

- 입력하신 이메일로 인증 링크를 발송했습니다. -
- 이메일을 확인하여 인증을 완료해주세요. -

- - 로그인하기 - +
+
+

가입 완료!

+

+ 입력하신 이메일로 인증 링크를 발송했습니다. +
+ 이메일을 확인하여 인증을 완료해주세요. +

+ + 로그인하기 + +
); } return ( -
-

회원가입

+
+
+

회원가입

+

스타트오버와 함께 시작하세요

- {state.error && ( -
{state.error}
- )} - -
-
- -
- {ROLES.map((role) => ( - - ))} + {state.error && ( +
+ {state.error}
- {state.fieldErrors?.role && ( -

{state.fieldErrors.role[0]}

- )} -
+ )} -
- - - {state.fieldErrors?.name && ( -

{state.fieldErrors.name[0]}

- )} -
+ +
+ +
+ {ROLES.map((role) => ( + + ))} +
+ {state.fieldErrors?.role && ( +

{state.fieldErrors.role[0]}

+ )} +
-
- - - {state.fieldErrors?.phone && ( -

{state.fieldErrors.phone[0]}

- )} -
+
+ + + {state.fieldErrors?.name && ( +

{state.fieldErrors.name[0]}

+ )} +
-
- - - {state.fieldErrors?.email && ( -

{state.fieldErrors.email[0]}

- )} -
+
+ + + {state.fieldErrors?.phone && ( +

{state.fieldErrors.phone[0]}

+ )} +
-
- - -

8자 이상, 영문+숫자 포함

- {state.fieldErrors?.password && ( -

{state.fieldErrors.password[0]}

- )} -
+
+ + + {state.fieldErrors?.email && ( +

{state.fieldErrors.email[0]}

+ )} +
-
-

약관 동의

- - {state.fieldErrors?.termsOfService && ( -

{state.fieldErrors.termsOfService[0]}

- )} - - {state.fieldErrors?.privacyPolicy && ( -

{state.fieldErrors.privacyPolicy[0]}

- )} - - -
+
+ + +

8자 이상, 영문+숫자 포함

+ {state.fieldErrors?.password && ( +

{state.fieldErrors.password[0]}

+ )} +
- - +
+

약관 동의

+
+ + {state.fieldErrors?.termsOfService && ( +

{state.fieldErrors.termsOfService[0]}

+ )} + + {state.fieldErrors?.privacyPolicy && ( +

{state.fieldErrors.privacyPolicy[0]}

+ )} + + +
+
-

- 이미 계정이 있으신가요?{' '} - - 로그인 - -

+ + + +

+ 이미 계정이 있으신가요?{' '} + + 로그인 + +

+
); } diff --git a/apps/web/src/app/auth/verify-pending/page.tsx b/apps/web/src/app/auth/verify-pending/page.tsx index 2b1adad..e747a45 100644 --- a/apps/web/src/app/auth/verify-pending/page.tsx +++ b/apps/web/src/app/auth/verify-pending/page.tsx @@ -2,22 +2,24 @@ import Link from 'next/link'; export default function VerifyPendingPage() { return ( -
-

이메일 인증이 필요합니다

-

- 가입 시 입력한 이메일로 인증 링크를 발송했습니다. -
- 이메일을 확인하고 인증을 완료해주세요. -

-

- 이메일을 받지 못하셨나요? 스팸함을 확인하거나 잠시 후 다시 시도해주세요. -

- - 홈으로 - +
+
+

이메일 인증이 필요합니다

+

+ 가입 시 입력한 이메일로 인증 링크를 발송했습니다. +
+ 이메일을 확인하고 인증을 완료해주세요. +

+

+ 이메일을 받지 못하셨나요? 스팸함을 확인하거나 잠시 후 다시 시도해주세요. +

+ + 홈으로 + +
); } diff --git a/apps/web/src/app/auth/verify/page.tsx b/apps/web/src/app/auth/verify/page.tsx index 730f3c3..3ecf6dc 100644 --- a/apps/web/src/app/auth/verify/page.tsx +++ b/apps/web/src/app/auth/verify/page.tsx @@ -13,9 +13,11 @@ export default async function VerifyPage({ if (!token) { return ( -
-

잘못된 접근

-

인증 토큰이 없습니다.

+
+
+

잘못된 접근

+

인증 토큰이 없습니다.

+
); } @@ -26,12 +28,16 @@ export default async function VerifyPage({ if (!verificationToken) { return ( -
-

유효하지 않은 토큰

-

이미 사용되었거나 존재하지 않는 인증 토큰입니다.

- - 로그인으로 이동 - +
+
+

유효하지 않은 토큰

+

+ 이미 사용되었거나 존재하지 않는 인증 토큰입니다. +

+ + 로그인으로 이동 + +
); } @@ -42,14 +48,16 @@ export default async function VerifyPage({ }); return ( -
-

토큰 만료

-

- 인증 토큰이 만료되었습니다. 다시 로그인하여 인증 메일을 재발송해주세요. -

- - 로그인으로 이동 - +
+
+

토큰 만료

+

+ 인증 토큰이 만료되었습니다. 다시 로그인하여 인증 메일을 재발송해주세요. +

+ + 로그인으로 이동 + +
); } @@ -65,12 +73,16 @@ export default async function VerifyPage({ }); return ( -
-

사용자를 찾을 수 없습니다

-

해당 계정이 존재하지 않습니다.

- - 회원가입으로 이동 - +
+
+

+ 사용자를 찾을 수 없습니다 +

+

해당 계정이 존재하지 않습니다.

+ + 회원가입으로 이동 + +
); } diff --git a/apps/web/src/app/contracts/page.tsx b/apps/web/src/app/contracts/page.tsx index df51fed..cf348ad 100644 --- a/apps/web/src/app/contracts/page.tsx +++ b/apps/web/src/app/contracts/page.tsx @@ -24,22 +24,22 @@ const CONTRACT_TYPE_LABELS: Record = { }; const STATUS_MAP: Record = { - DRAFT: { label: '초안', color: 'bg-gray-100 text-gray-700' }, - GENERATED: { label: '생성됨', color: 'bg-blue-100 text-blue-700' }, - SIGNING: { label: '서명 중', color: 'bg-yellow-100 text-yellow-700' }, - SIGNED: { label: '서명 완료', color: 'bg-indigo-100 text-indigo-700' }, - ACTIVE: { label: '진행 중', color: 'bg-green-100 text-green-700' }, - COMPLETED: { label: '완료', color: 'bg-emerald-100 text-emerald-700' }, + DRAFT: { label: '초안', color: 'bg-warm-100 text-ink-muted' }, + GENERATED: { label: '생성됨', color: 'bg-warm-400/15 text-warm-700' }, + SIGNING: { label: '서명 중', color: 'bg-warm-400/15 text-warm-700' }, + SIGNED: { label: '서명 완료', color: 'bg-sage-500/10 text-sage-600' }, + ACTIVE: { label: '진행 중', color: 'bg-sage-500/10 text-sage-600' }, + COMPLETED: { label: '완료', color: 'bg-sage-500/10 text-sage-600' }, CANCELLED: { label: '취소', color: 'bg-red-100 text-red-700' }, }; const ESCROW_MAP: Record = { - NOT_STARTED: { label: '미시작', color: 'text-gray-500' }, - DEPOSIT_PENDING: { label: '입금 대기', color: 'text-yellow-600' }, - HOLDING: { label: '보관 중', color: 'text-blue-600' }, - RELEASE_REVIEW: { label: '정산 검토', color: 'text-purple-600' }, - RELEASED: { label: '정산 완료', color: 'text-green-600' }, - REFUNDED: { label: '환불됨', color: 'text-orange-600' }, + NOT_STARTED: { label: '미시작', color: 'text-ink-muted' }, + DEPOSIT_PENDING: { label: '입금 대기', color: 'text-warm-700' }, + HOLDING: { label: '보관 중', color: 'text-warm-700' }, + RELEASE_REVIEW: { label: '정산 검토', color: 'text-warm-700' }, + RELEASED: { label: '정산 완료', color: 'text-sage-600' }, + REFUNDED: { label: '환불됨', color: 'text-warm-700' }, DISPUTED: { label: '분쟁 중', color: 'text-red-600' }, }; @@ -70,12 +70,14 @@ export default async function ContractsPage() { } return ( -
-

계약 관리

-

계약, 에스크로, 검수 현황을 관리합니다

+
+
+

계약 관리

+

계약, 에스크로, 검수 현황을 관리합니다

+
{/* 요약 */} -
+
@@ -83,33 +85,33 @@ export default async function ContractsPage() {
{/* 계약 목록 */} -
+
{contracts.length === 0 ? ( -

데이터가 없습니다

+

데이터가 없습니다

) : ( contracts.map((contract: (typeof contracts)[number]) => { const statusInfo = STATUS_MAP[contract.status] ?? { label: contract.status, - color: 'bg-gray-100 text-gray-700', + color: 'bg-warm-100 text-ink-muted', }; const escrowInfo = ESCROW_MAP[contract.escrowStatus] ?? { label: contract.escrowStatus, - color: 'text-gray-500', + color: 'text-ink-muted', }; return (
-

{contract.store.listingTitle}

- +

{contract.store.listingTitle}

+ {CONTRACT_TYPE_LABELS[contract.contractType] ?? contract.contractType}
-

+

생성일: {new Date(contract.createdAt).toLocaleDateString('ko-KR')}

@@ -122,15 +124,15 @@ export default async function ContractsPage() {
- 에스크로: + 에스크로: {escrowInfo.label}
{contract.status === 'ACTIVE' && ( -
+
-
@@ -150,10 +152,10 @@ export default async function ContractsPage() { )}
-
-

+

+

새 계약은{' '} - + 수락된 매칭 요청 에서 생성됩니다 @@ -165,9 +167,9 @@ export default async function ContractsPage() { function SummaryCard({ label, value }: { label: string; value: string }) { return ( -

-

{value}

-

{label}

+
+

{value}

+

{label}

); } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index f1d8c73..bdf0146 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -1 +1,108 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&family=Playfair+Display:ital,wght@0,700;0,900;1,700&display=swap'); @import "tailwindcss"; + +@theme { + --font-display: 'Playfair Display', Georgia, serif; + --font-body: 'Noto Sans KR', sans-serif; + --color-warm-50: #fefcf9; + --color-warm-100: #fdf6ec; + --color-warm-200: #f9e8cf; + --color-warm-300: #f2d1a5; + --color-warm-400: #e8b06e; + --color-warm-500: #d4874a; + --color-warm-600: #b8622e; + --color-warm-700: #8e4a22; + --color-warm-800: #6b3a1e; + --color-warm-900: #3d2213; + --color-ink: #1a1410; + --color-ink-light: #4a4035; + --color-ink-muted: #8a7e72; + --color-sage-500: #6b8f71; + --color-sage-600: #527a58; +} + +/* Staggered fade-in-up animation */ +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(32px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slide-in-right { + from { + opacity: 0; + transform: translateX(24px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes mesh-float { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(30px, -20px) scale(1.05); } + 66% { transform: translate(-20px, 15px) scale(0.95); } +} + +.animate-fade-up { + animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards; + opacity: 0; +} + +.animate-fade-in { + animation: fade-in 0.6s ease forwards; + opacity: 0; +} + +.animate-slide-right { + animation: slide-in-right 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards; + opacity: 0; +} + +/* Gradient mesh blobs */ +.mesh-blob { + position: absolute; + border-radius: 50%; + filter: blur(80px); + animation: mesh-float 12s ease-in-out infinite; + pointer-events: none; +} + +/* Card hover lift */ +.card-lift { + transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.35s ease; +} +.card-lift:hover { + transform: translateY(-6px); + box-shadow: 0 20px 60px -12px rgba(26, 20, 16, 0.15); +} + +/* Link underline animation */ +.link-underline { + position: relative; + display: inline-block; +} +.link-underline::after { + content: ''; + position: absolute; + left: 0; + bottom: -2px; + width: 0; + height: 2px; + background: var(--color-warm-600); + transition: width 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} +.link-underline:hover::after { + width: 100%; +} diff --git a/apps/web/src/app/icon.svg b/apps/web/src/app/icon.svg index 4961911..b66ac34 100644 --- a/apps/web/src/app/icon.svg +++ b/apps/web/src/app/icon.svg @@ -1,4 +1,4 @@ - - S + + S diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 71e200a..abab2ec 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -27,34 +27,34 @@ async function Navigation() { const isOperator = session?.user && OPERATOR_ROLES.includes(session.user.primaryRole); return ( -