Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Project Overview

DASH (Da/sh) — a Korean-language dance class discovery and booking platform built with Next.js 15 App Router. Targets mobile viewports (375–430px). The app connects dance students with instructor/dancer profiles and classes.

## Commands

| Command | Description |
|---|---|
| `pnpm dev` | Start Next.js dev server |
| `pnpm build` | Production build |
| `pnpm lint` | ESLint (`eslint .`) |
| `pnpm storybook` | Storybook on port 6006 |
| `pnpm svgr` | Convert SVGs in `public/svg/` → TSX components in `src/shared/assets/svg/` |

## Architecture

### Directory Structure

- **`src/app/`** — Next.js App Router: pages, layouts, and BFF API routes
- **`src/common/`** — React-level shared code: reusable UI components, hooks, Zustand stores
- **`src/shared/`** — Domain-specific shared code: API clients, constants, types, utils
- **`src/mocks/`** — MSW v2 mock handlers and data for development

### Feature Colocation

Each route directory under `src/app/` colocates its own:
- `apis/` — data fetching functions and query hooks
- `components/` — page-specific components
- `hooks/`, `types/`, `constants/`, `utils/`, `schema/` (Zod) as needed

### Route Groups

- `src/app/(home)/` — home page
- `src/app/my/(student)/` — student sub-pages (classes, edit-profile)
- `src/app/my/(instructor)/` — instructor sub-pages (create-class, manage-classes, manage-profile, manage-account)

### BFF Proxy

`src/app/api/[...path]/route.ts` — catch-all proxy that forwards browser requests to the backend, injecting the `Authorization: Bearer` header from cookies. Auth-specific endpoints live under `src/app/api/auth/`.

## Key Patterns

### Data Fetching

- **HTTP client**: `ky`
- Two instances in `src/shared/apis/kyInstance.ts`:
- `kyInstance` — unauthenticated (public/SSG)
- `kyInstanceWithToken` — authenticated (injects token on SSR via `next/headers` cookies, handles 401 refresh)
- **Server state**: TanStack React Query v5 with `@lukemorales/query-key-factory`
- Query keys centralized in `src/shared/constants/queryKey.ts`
- QueryClient configured in `src/app/queryClient.ts` (1min staleTime, throwOnError: true)
- **API URL constants**: `src/shared/constants/apiURL.ts` — the `API_URL` object

### Styling

- **Vanilla Extract CSS** (`.css.ts` files)
- Design tokens in `src/shared/styles/theme.css.ts` using `vars.colors`, `vars.fonts`, `vars.zIndex`
- Uses `@vanilla-extract/recipes` for variant styles and `clsx` for conditional classes

### State Management

- **Server state**: React Query (primary)
- **Global Client state**: Zustand v5 (stores in `src/common/stores/`)
- **Form state**: react-hook-form + Zod via `@hookform/resolvers`

### Auth

Cookie-based JWT (`ACCESS_TOKEN`, `TEMP_ACCESS_TOKEN` for onboarding). Enforced via:
- `src/middleware.ts` — route guards (auth-required, guest-only, onboarding, reservation, withdraw)
- BFF proxy — injects token from cookies into API requests

### Error Handling

Custom `ApiError` class in `src/shared/types/ApiError.ts` wraps ky `HTTPError`, named by status code for Sentry grouping. Mutation errors show toast notifications via `mutation.meta`.

## Conventions

### Naming

- Components/classes: `PascalCase`
- Files (non-component), folders, variables, functions: `camelCase`
- Constants: `BIG_SNAKE_CASE`
- Event handlers: `handle + feature + event` (e.g., `handleBtnClick`); prop keys: `on + event`
- Booleans: `is/can/should/has + state`
- API functions: `HTTP method + noun` (e.g., `getList`, `postReservation`)
- Props types: `ComponentName + PropTypes`; response types: `OOO + ResponseTypes`
- Use `interface` for objects, `type` alias for single values

### Coding

- Arrow functions only (no `function` keyword for components/handlers)
- `const` before `let`; no `var`
- Template literals for string composition (no `+`)
- Strict `===`/`!==` comparison
- `async/await` over `.then/.catch`
- Early return pattern encouraged
- `<button>` must have explicit `type` attribute
- No `index` as `key` in `.map()` — use server-provided `id` or uuid
- SVGs: run `pnpm svgr` then import from `src/shared/assets/svg/`

### Git

- **Default branch**: `develop`
- **Branch naming**: `feat/#issue/description`, `fix/#issue/description`, `refactor/#issue/description`
- **Commit prefixes**: `feat:`, `fix:`, `refactor:`, `style:`, `api:`, `chore:`, `setting:`, `docs:`, `rename:`, `remove:`, `test:`, `!HOTFIX:`, `!BREAKING CHANGE:`
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"react-hook-form": "^7.56.1",
"react-hot-toast": "^2.5.1",
"react-image-file-resizer": "^0.4.8",
"swiper": "^11.2.1",
"zod": "^3.24.3",
"zustand": "^5.0.7"
},
Expand All @@ -56,7 +55,6 @@
"@trivago/prettier-plugin-sort-imports": "^5.2.1",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/swiper": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"@typescript-eslint/parser": "^8.19.1",
"@vanilla-extract/next-plugin": "^2.4.10",
Expand Down
20 changes: 0 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 9 additions & 46 deletions src/app/(home)/components/HomeCarousel/HomeCarousel.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,14 @@
'use client';

import 'swiper/css';
import 'swiper/css/pagination';
import { Autoplay, Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/react';
import SliderItem from '@/app/(home)/components/SliderItem/SliderItem';

// Next: public 폴더 정적 파일은 경로 문자열로 참조 (import 불가)
const ChoreohongImg = '/images/image_chorehong.webp';
const KkukgirlImg = '/images/image_kkukgirl.webp';
const BannerImg = '/images/img_banner_750.webp';

const ADVERTISEMENTS = [
{
imageUrl: KkukgirlImg,
description: `5주 집중 커리큘럼, 스걸파 출연 댄서의 \n 피메일 힙합 기본기 튼튼하게 다지기`,
id: 25,
},
{ imageUrl: ChoreohongImg, description: `코레홍과 함께하는 \n NMIXX-DASH 코레오그래피`, id: 24 },
{
imageUrl: BannerImg,
description: `알고 싶던 댄서나 관심있는 클래스를 \n 한 눈에 모아볼 수 있어요!`,
id: 0,
},
];

const HomeCarousel = () => {
return (
<Swiper
loop={true}
pagination={true}
speed={500}
modules={[Pagination, Autoplay]}
slidesPerView={1}
autoplay={{
delay: 4500,
disableOnInteraction: false,
}}>
{ADVERTISEMENTS.map((advertisement, index) => {
return (
<SwiperSlide key={`${advertisement.id}`}>
<SliderItem {...advertisement} priority={index === 0} />
</SwiperSlide>
);
})}
</Swiper>
);
};
import { HOME_ADVERTISEMENTS } from '@/app/(home)/constants';
import Carousel from '@/common/components/Carousel/Carousel';

const HomeCarousel = () => (
<Carousel
slides={HOME_ADVERTISEMENTS}
renderSlide={(slide, isPriority) => <SliderItem {...slide} priority={isPriority} />}
/>
);

export default HomeCarousel;
14 changes: 0 additions & 14 deletions src/app/(home)/components/HomeCarousel/homeCarousel.css.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/app/(home)/constants/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ import IcAssetTop1 from '@/shared/assets/svg/IcAssetTop1';
import IcAssetTop2 from '@/shared/assets/svg/IcAssetTop2';
import IcAssetTop3 from '@/shared/assets/svg/IcAssetTop3';

const ChoreohongImg = '/images/image_chorehong.webp';
const KkukgirlImg = '/images/image_kkukgirl.webp';
const BannerImg = '/images/img_banner_750.webp';

export const HOME_ADVERTISEMENTS = [
{
imageUrl: KkukgirlImg,
description: `5주 집중 커리큘럼, 스걸파 출연 댄서의 \n 피메일 힙합 기본기 튼튼하게 다지기`,
id: 25,
},
{ imageUrl: ChoreohongImg, description: `코레홍과 함께하는 \n NMIXX-DASH 코레오그래피`, id: 24 },
{
imageUrl: BannerImg,
description: `알고 싶던 댄서나 관심있는 클래스를 \n 한 눈에 모아볼 수 있어요!`,
id: 0,
},
];

export const GENRE_ICONS = [
<IcAssetTop1 width={44} height={44} />,
<IcAssetTop2 width={44} height={44} />,
Expand Down
7 changes: 7 additions & 0 deletions src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import type { Metadata } from 'next';
import Footer from '@/app/(home)/components/Footer/Footer';
import HomeCarousel from '@/app/(home)/components/HomeCarousel/HomeCarousel';
import LatestLessons from '@/app/(home)/components/LatestLessons/LatestLessons';
import PopularGenre from '@/app/(home)/components/PopularGenre/PopularGenre';
import UpcomingLessons from '@/app/(home)/components/UpcomingLessons/UpcomingLessons';

export const metadata: Metadata = {
title: 'Dash 대쉬 | 꿈꾸던 댄스 클래스를 만나다',
description: '원데이 클래스부터 프라이빗 스쿨까지, 대쉬에서 댄서와 클래스를 검색하고 지금 바로 신청하세요!',
alternates: { canonical: '/' },
};

export default function Page() {
return (
<main>
Expand Down
2 changes: 0 additions & 2 deletions src/app/(home)/types/swiper-css.d.ts

This file was deleted.

29 changes: 29 additions & 0 deletions src/app/class/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Metadata } from 'next';
import { getLessonDetail } from '@/app/class/[id]/apis/ky';
import { getFallbackClassMetadata, lessonDetailToMetadata } from '@/app/class/[id]/utils/buildClassMetadata';

function parseLessonId(id: string): number | null {
const n = Number(id);
if (!Number.isInteger(n) || n <= 0) return null;
return n;
}

// eslint-disable-next-line react-refresh/only-export-components -- Next.js generateMetadata
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params;
const lessonId = parseLessonId(id);
if (lessonId === null) {
return getFallbackClassMetadata();
}

try {
const data = await getLessonDetail(lessonId);
return lessonDetailToMetadata(data);
} catch {
return getFallbackClassMetadata();
}
}

export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
Loading
Loading