This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
npm run dev # Start dev server with service worker generation
npm run build # TypeScript check + production build with service worker
npm run lint # Run ESLint
npm run preview # Preview production buildThe Firebase Cloud Messaging (FCM) service worker is auto-generated:
npm run generate:sw # Generate firebase-messaging-sw.js from templateThis runs automatically before dev and build commands.
src/
├── domain/ # Reusable business logic and types (cardsets, groups, users, etc.)
├── features/ # Feature modules with page-level components and logic
├── pages/ # Legacy route components (prefer routes/ for new pages)
├── routes/ # TanStack Router file-based routing
├── shared/ # Shared APIs, components, utilities, services
└── stores/ # Zustand global state (auth only)
Key Pattern: This codebase uses a domain-feature separation:
- Domain layer (
domain/): Contains reusable business logic, types, and React Query hooks that can be used across multiple features - Feature layer (
features/): Contains feature-specific implementations, forms, schemas, and UI components - Shared layer (
shared/): Infrastructure code (API clients, socket management, base components)
- File-based routing: Routes are defined in
src/routes/*.tsx - Auto-generated route tree:
routeTree.gen.tsis generated by Vite plugin during build - Auth guards: Use
authGuard()in routebeforeLoadhooks
Example route structure:
// src/routes/some-route.tsx
export const Route = createFileRoute("/some-route")({
component: RouteComponent,
beforeLoad: ({ context }) => {
authGuard({ auth: context.auth, mode: "protected" }); // or "non-protected"
},
validateSearch: (search) => ({ /* query params */ }),
head: () => ({ meta: [/* SEO tags */] })
});Auth guard modes:
"protected": Requires authentication (redirects to/auth/loginif not authenticated)"non-protected": Requires NO authentication (redirects to/if authenticated)"bypass": No auth requirement
Zustand is used for global state (auth only):
- Primary store:
useAuthStoreinsrc/stores/useAuthStore.ts - Manages: user profile, auth initialization, token refresh
- Key methods:
initializeAuth(): Called on app mount to refresh token and sync usersyncUser(): Fetches current user from APIrefreshToken(): Handles token refresh (called by axios interceptor)clearUser(): Logout
Multi-client setup:
- Primary: Axios client (
src/shared/apis/fetch.ts) with interceptors - Secondary: Custom FetchClient (
src/shared/apis/fetchClient.ts)
Axios interceptor handles automatic token refresh:
- On 401 response → attempts token refresh → retries original request
- Skips refresh for auth endpoints (
/auth/login,/auth/register, etc.)
API modules in src/shared/apis/:
- Each domain has its own file:
auth.ts,user.ts,card-set.ts,group.ts, etc. - Type-safe responses using generics:
apiClient.get<ApiResponse<T>>()
Domain hooks in domain/*/hooks/:
- Use
useSuspenseInfiniteQueryfor paginated lists - Query keys include all parameters for proper cache isolation
selectoption transforms API responses into component-friendly shapes
Example pattern:
export const useCardSets = (params?: UseCardSetsParams) => {
return useSuspenseInfiniteQuery({
queryKey: ["cardsets", params],
queryFn: async ({ pageParam }) => cardSetApi.getCardSets({ ...params, page: pageParam }),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.data.hasNext ? lastPage.data.nextPage : undefined,
select: (data) => ({
cardsets: data.pages.flatMap(page => page.data.cardsets),
hasNext: data.pages[data.pages.length - 1].data.hasNext
})
});
};Important: Query client is configured with refetchOnWindowFocus: false.
Socket.io (src/shared/socket/):
- Singleton
SocketManagerensures single WebSocket connection useSockethook provides:connect(),disconnect(),emit(),on(),off()- Socket URL from
VITE_SOCKET_URLenvironment variable
Yjs collaborative editing:
YjsProviderclass wraps Y.Doc and socket communicationuseYjshook manages connection, cards array, and awareness (cursor positions)- Shared structure:
Y.Array<Y.Map>for cards
Socket event types in src/shared/socket/events.ts:
- Server→Client:
sync,awareness,expired,error - Client→Server:
auth,update,awareness
React Hook Form + Zod pattern:
- Define Zod schema in
features/*/schemas/form.schema.ts - Use
zodResolverinuseFormhook - Separate schemas for form validation vs API requests
Example:
// Schema
const schema = z.object({
name: z.string().min(1, "Required"),
category: z.enum(["IT", "LANGUAGE", "etc"]),
});
// Component
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
defaultValues: { /* ... */ }
});Typical feature structure:
src/features/feature-name/
├── components/ # UI components
├── schemas/
│ ├── form.schema.ts # Zod schema for form validation
│ └── request.schema.ts # API request validation
├── hooks/ # Feature-specific React Query hooks
├── model/ # Domain models (if needed)
└── ui/ # UI-specific utilities
Domain modules contain reusable business logic:
src/domain/entity-name/
├── components/ # Reusable components for this domain
├── hooks/ # React Query hooks
├── sub-domain/ # Nested domains (e.g., group/invitation/)
│ ├── hooks/
│ ├── types.ts
│ └── index.ts
└── types.ts # Type definitions + converter functions
Type conversion pattern: Define toEntityName() converter functions that transform API responses into domain types.
Initialization flow (src/shared/libs/firebase.ts + src/shared/services/fcmService.ts):
- Firebase app initialized with env vars
- Service worker registered
- Notification permission requested
- FCM token obtained and registered with backend
- Foreground message listener set up
Called from useAuthStore.initializeAuth():
await registerFCMToken();
initializeForegroundMessageListener();Component library (src/shared/components/):
- Built with Radix UI + Tailwind CSS
- Includes: form inputs, layout components (card, sheet), custom components (flip-card, infinite-scroll-list)
- Skeleton components for Suspense fallbacks
Layouts (src/shared/layouts/):
BaseLayout: GNB + centered content containerSidebarTabLayout: Responsive sidebar + content (switches flex direction on mobile)
S3 presigned URL pattern (src/shared/lib/upload-image.ts):
- Calculate MD5 hash of file
- Request presigned URL from backend (
imageApi.getPresignedUrl()) - PUT file directly to S3 using presigned URL
- Return
imageRefIdfrom backend response
TypeScript path alias configured: @/* → ./src/*
Always use absolute imports:
import { Button } from "@/shared/components/ui/button"
import { useAuthStore } from "@/stores/useAuthStore"Required in .env.development and .env.production:
VITE_BASE_URL: API base URLVITE_SOCKET_URL: WebSocket endpointVITE_FIREBASE_*: Firebase config (API key, auth domain, project ID, etc.)
- TanStack Router plugin: Auto-generates route tree with code splitting
- Proxy:
/api→https://api.flipnote.site/v1(dev only) - Path alias:
@→./src - Tailwind CSS: Integrated via Vite plugin
- Use Tailwind breakpoints:
md:for tablet+ SidebarTabLayoutadapts from horizontal to vertical on mobile- GNB has separate authenticated/unauthenticated UI
-
Query keys: Include all parameters for cache isolation
queryKey: ["entity", params]
-
Error handling:
- 401 errors trigger automatic token refresh via axios interceptor
- Socket connection errors stored in hook state
- Failed token refresh clears auth store (logout)
-
Suspense integration:
- Use
useSuspenseInfiniteQueryfor automatic Suspense - Provide skeleton components as Suspense fallbacks
- Requires Suspense boundary in parent component
- Use
-
Type safety:
- Full TypeScript with strict mode
- Zod for runtime validation
- Type-safe API clients with generics
-
Code splitting:
- TanStack Router handles automatic route-based code splitting
- No manual lazy loading needed for routes