Skip to content

Commit 63d8d18

Browse files
authored
feat: 리뷰 작성 레이아웃 (#132)
* feat: 리뷰 작성 연결 페이지 재정의 * feat: 리뷰 UI 개발사항 반영 * fix: formatting * feat: QnA 컴포넌트 추가
1 parent 4ebeeef commit 63d8d18

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2573
-3481
lines changed

CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Moyeoit - A platform for IT professionals. Next.js 15 application with React 19, TypeScript, and Tailwind CSS 4.
8+
9+
## Development Commands
10+
11+
```bash
12+
# Development
13+
pnpm dev # Start dev server with Turbopack
14+
pnpm dev:mock-ssr # Dev server with MSW mock API
15+
16+
# Code Quality
17+
pnpm lint # Run ESLint
18+
pnpm lint:fix # ESLint with auto-fix
19+
pnpm format # Format with Prettier
20+
pnpm type-check # TypeScript type checking
21+
22+
# Build & Production
23+
pnpm build # Production build
24+
pnpm start # Start production server
25+
26+
# Testing
27+
pnpm test # Run Vitest tests
28+
pnpm storybook # Start Storybook on port 6006
29+
```
30+
31+
## Architecture
32+
33+
### Directory Structure
34+
35+
- **`src/app/`** - Next.js App Router with route groups `(root)/(routes)/`
36+
- **`src/components/`** - Atomic design: `atoms/` -> `molecules/` -> `(pages)/`
37+
- **`src/features/`** - Feature-Sliced Architecture by domain (clubs, review, like, oauth, user, etc.)
38+
- **`src/shared/`** - Cross-cutting concerns: configs, hooks, providers, utils, types
39+
- **`src/mocks/`** - MSW handlers and mock data
40+
41+
### Key Patterns
42+
43+
**Component Variants**: Use Class Variance Authority (CVA) for type-safe styling variants
44+
```typescript
45+
const buttonVariants = cva([...], {
46+
variants: { variant: {...}, size: {...} },
47+
defaultVariants: { variant: 'solid', size: 'small' }
48+
})
49+
```
50+
51+
**Styling**: Tailwind CSS with `cn()` utility (clsx + tailwind-merge) from `@/shared/utils/cn`
52+
53+
**Routing**: Centralized path constants in `src/shared/configs/appPath.ts`
54+
55+
**Data Fetching**: TanStack React Query with Axios; MSW for API mocking
56+
57+
**Forms**: React Hook Form + Zod validation with `@hookform/resolvers`
58+
59+
**UI Components**: Radix UI primitives wrapped via shadcn/ui (New York style)
60+
61+
## Code Conventions
62+
63+
### Commit Messages (Conventional Commits)
64+
- `feat`: New feature
65+
- `fix`: Bug fix
66+
- `style`: CSS related
67+
- `refactor`: Code refactoring
68+
- `docs`: Documentation
69+
- `chore`: Miscellaneous changes
70+
- `test`: Test code
71+
72+
### Pre-commit Hooks
73+
Husky runs: lint -> format -> build before each commit
74+
75+
## Environment Variables
76+
77+
```bash
78+
NEXT_PUBLIC_API_ADDRESS # Backend API URL
79+
NEXT_PUBLIC_API_MOCKING # MSW toggle (enabled/disabled)
80+
NEXT_PUBLIC_GA_ID # Google Analytics
81+
```
82+
83+
## Tech Stack Quick Reference
84+
85+
| Category | Technology |
86+
|----------|------------|
87+
| Framework | Next.js 15 (App Router, Turbopack) |
88+
| UI | Radix UI + shadcn/ui + Tailwind CSS 4 |
89+
| State | TanStack React Query v5 |
90+
| Forms | React Hook Form + Zod |
91+
| Testing | Vitest + Storybook 9 + Playwright |
92+
| Mocking | MSW 2 |
93+
| Monitoring | Sentry |

src/app/(root)/(routes)/review/new/[kind]/[type]/page.tsx

Lines changed: 0 additions & 112 deletions
This file was deleted.
Lines changed: 56 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,74 @@
1-
import type { Metadata } from 'next'
2-
import Link from 'next/link'
3-
import { notFound } from 'next/navigation'
4-
import { DocumentPencilIcon, DocumentDiamondIcon } from '@/assets/icons'
1+
'use client'
52

6-
const validKinds = ['paper', 'interview', 'activity'] as const
3+
import { Suspense, useEffect, useState } from 'react'
4+
import { notFound, redirect } from 'next/navigation'
5+
import {
6+
FormFactory,
7+
isValidFormKind,
8+
type FormKind,
9+
} from '@/components/pages/review/new/forms'
10+
import AppPath from '@/shared/configs/appPath'
11+
import { useAuth } from '@/shared/providers/auth-provider'
712

8-
export async function generateMetadata({
9-
params,
10-
}: {
13+
interface PageProps {
1114
params: Promise<{ kind: string }>
12-
}): Promise<Metadata> {
13-
const { kind } = await params
14-
const isValidKind = (validKinds as readonly string[]).includes(kind)
15+
}
1516

16-
if (!isValidKind) {
17-
return {
18-
title: '페이지를 찾을 수 없습니다',
19-
}
20-
}
17+
export default function Page({ params }: PageProps) {
18+
const { user, isLoading } = useAuth()
19+
const [formKind, setFormKind] = useState<FormKind | null>(null)
2120

22-
const getKindDisplayName = (kind: string) => {
23-
switch (kind) {
24-
case 'paper':
25-
return '서류'
26-
case 'interview':
27-
return '인터뷰/면접'
28-
case 'activity':
29-
return '활동'
30-
default:
31-
return kind
32-
}
33-
}
21+
useEffect(() => {
22+
async function loadParams() {
23+
const { kind } = await params
3424

35-
const kindName = getKindDisplayName(kind)
25+
if (!isValidFormKind(kind)) {
26+
notFound()
27+
}
3628

37-
return {
38-
title: `${kindName} 후기 작성`,
39-
description: `IT 동아리 ${kindName} 후기를 작성해보세요. 일반 후기와 프리미엄 후기 중 선택하여 경험을 공유하고 다른 분들에게 도움을 주세요.`,
40-
keywords: [`IT 동아리 ${kindName} 후기`, '동아리 후기 작성', '경험 공유'],
41-
openGraph: {
42-
title: `${kindName} 후기 작성 | 모여잇`,
43-
description: `IT 동아리 ${kindName} 후기를 작성해보세요. 일반 후기와 프리미엄 후기 중 선택하여 경험을 공유하고 다른 분들에게 도움을 주세요.`,
44-
},
45-
}
46-
}
29+
setFormKind(kind as FormKind)
30+
}
4731

48-
export default async function Page({
49-
params,
50-
}: {
51-
params: Promise<{ kind: string }>
52-
}) {
53-
const { kind } = await params
54-
const isValidKind = (validKinds as readonly string[]).includes(kind)
55-
if (!isValidKind) {
56-
notFound()
57-
}
32+
loadParams()
33+
}, [params])
5834

59-
const getKindDisplayName = (kind: string) => {
60-
switch (kind) {
61-
case 'paper':
62-
return '서류'
63-
case 'interview':
64-
return '인터뷰/면접'
65-
case 'activity':
66-
return '활동'
67-
default:
68-
return kind
35+
useEffect(() => {
36+
if (!isLoading && !user) {
37+
redirect(AppPath.login())
6938
}
70-
}
39+
}, [user, isLoading])
7140

72-
return (
73-
<main className="w-full h-full">
74-
<div className="max-w-[530px] h-full mx-auto flex flex-col items-center justify-center">
75-
{/* 제목 */}
76-
<div className="text-center mb-8">
77-
<h2 className="typo-title-1 text-black-color mb-8">
78-
{getKindDisplayName(kind)} 후기 작성
79-
</h2>
41+
if (isLoading || !user) {
42+
return (
43+
<main className="">
44+
<div className="max-w-[800px] mx-auto pt-20">
45+
<div className="text-center">
46+
<p className="typo-body-2-r text-grey-color-4">로딩 중...</p>
47+
</div>
8048
</div>
49+
</main>
50+
)
51+
}
8152

82-
{/* 카드 컨테이너 */}
83-
<div className="p-6 rounded-2xl bg-white-color flex flex-col gap-8 items-center w-[530px] shadow-sm">
84-
<p className="typo-body-2-sb text-grey-color-4">
85-
작성하실 후기 스타일을 선택해주세요
86-
</p>
87-
<div className="w-full flex flex-col gap-4">
88-
{/* 일반 후기 카드 */}
89-
<Link href={`/review/new/${kind}/normal`}>
90-
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group">
91-
<div className="flex items-center space-x-4">
92-
<div className="flex-shrink-0">
93-
<DocumentPencilIcon role="img" aria-label="일반 후기" />
94-
</div>
95-
<div className="flex-1">
96-
<h3 className="typo-body-3-b text-black-color mb-1">
97-
일반 후기
98-
</h3>
99-
<p className="typo-button-m text-grey-color-3">
100-
짧고 간단한 3분 후기 작성하기
101-
</p>
102-
</div>
103-
</div>
104-
</div>
105-
</Link>
106-
{/* 프리미엄 후기 카드 */}
107-
<Link href={`/review/new/${kind}/premium`}>
108-
<div className="w-full p-6 border border-gray-200 rounded-xl cursor-pointer group">
109-
<div className="flex items-center space-x-4">
110-
<div className="flex-shrink-0">
111-
<DocumentDiamondIcon
112-
role="img"
113-
aria-label="프리미엄 후기"
114-
/>
115-
</div>
116-
<div className="flex-1">
117-
<h3 className="typo-body-3-b text-black-color mb-1">
118-
프리미엄 후기
119-
</h3>
120-
<p className="typo-button-m text-grey-color-3">
121-
가이드 따라 상세 후기 작성하고 기프티콘 혜택 받기
122-
</p>
123-
</div>
124-
</div>
125-
</div>
126-
</Link>
53+
if (!formKind) {
54+
return (
55+
<main className="">
56+
<div className="max-w-[800px] mx-auto pt-20">
57+
<div className="text-center">
58+
<p className="typo-body-2-r text-grey-color-4">로딩 중...</p>
12759
</div>
12860
</div>
129-
</div>
61+
</main>
62+
)
63+
}
64+
65+
return (
66+
<main className="">
67+
<Suspense>
68+
<div className="max-w-[800px] mx-auto pt-20 pb-20">
69+
<FormFactory kind={formKind} />
70+
</div>
71+
</Suspense>
13072
</main>
13173
)
13274
}

0 commit comments

Comments
 (0)