Awesome-omni-skill nextjs-frontend-guidelines
Next.js 15 frontend development guidelines for YGS (영영사) React 19/TypeScript application. Modern patterns including App Router, Server/Client Components, shadcn/ui components, Tailwind CSS 4, multi-method authentication (Firebase/Kakao/JWT), admin dashboard patterns, and Korean localization. Use when creating components, pages, API routes, fetching data, styling, or working with frontend code.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/nextjs-frontend-guidelines-neversight" ~/.claude/skills/diegosouzapw-awesome-omni-skill-nextjs-frontend-guidelines && rm -rf "$T"
skills/development/nextjs-frontend-guidelines-neversight/SKILL.mdNext.js 15 Frontend Development Guidelines for YGS
Purpose
Comprehensive guide for YGS (영영사) frontend development with Next.js 15, React 19, emphasizing App Router patterns, Server/Client component separation, shadcn/ui components, Tailwind CSS 4 styling, multi-method authentication, and Korean localization.
When to Use This Skill
- Creating new components or pages
- Building new features with App Router
- Fetching data with Server Components or client-side patterns
- Styling components with shadcn/ui and Tailwind CSS 4
- Setting up API routes or Server Actions
- Authentication flows (Firebase, Kakao, custom JWT)
- Admin dashboard development
- Performance optimization
- Organizing frontend code
- TypeScript best practices
Quick Start
New Component Checklist
Creating a component? Follow this checklist:
- Determine if Server or Client Component
- Use
directive only when needed'use client' - Props type with TypeScript interface
- Use
import alias for project imports@/ - Use shadcn/ui components where applicable
- Use
utility for conditional classescn() - Named export for components
- Async Server Components for data fetching when possible
- Client Components for interactivity (useState, useEffect, event handlers)
- Korean text for UI labels
New Feature Checklist
Creating a feature? Set up this structure:
- Create
directorysrc/components/{feature-name}/ - Separate Server and Client components
- Create API route if needed:
src/app/api/{feature}/route.ts - Set up TypeScript types in
src/types/ - Create route in
src/app/{feature-name}/page.tsx - Use Server Components by default
- Add Client Components only for interactivity
- Use Server Actions for mutations when appropriate
- Add constants/enums to
if neededsrc/constants/enums.ts
Project Structure
Your YGS project structure (import with
@/ alias):
src/ ├── app/ # Next.js App Router │ ├── page.tsx # Home/Landing page │ ├── layout.tsx # Root layout with metadata │ ├── error.tsx # Error boundary │ ├── admin/ # Admin dashboard (protected) │ │ ├── page.tsx # Dashboard stats │ │ ├── layout.tsx # Admin layout with auth check │ │ ├── members/ # Member management │ │ │ ├── page.tsx # Member list │ │ │ └── [id]/page.tsx # Member detail │ │ ├── consultations/ # Consultation management │ │ ├── matching/ # Matching interface │ │ └── couples/ # Couple management │ ├── login/ # Authentication │ │ ├── page.tsx # Login page │ │ └── kakao-callback/ # Kakao OAuth callback │ ├── form/ # User profile form │ ├── match/ # Matching interface │ ├── buy/ # Membership purchase │ └── api/ │ └── auth/session/ # Token sync endpoint ├── components/ # React components (~60 total) │ ├── admin/ # Admin components (27) │ │ ├── DashboardStats.tsx │ │ ├── MemberTable.tsx │ │ ├── MemberFilters.tsx │ │ ├── ConsultationFormModal.tsx │ │ └── modals/ # Edit modals │ ├── auth/ # Auth components (4) │ │ ├── LoginForm.tsx │ │ ├── SignupForm.tsx │ │ └── SocialLoginButton.tsx │ ├── layout/ # Layout components (2) │ │ ├── Navbar.tsx │ │ └── Footer.tsx │ ├── match/ # Match components (3) │ ├── sections/ # Landing page sections (7) │ ├── seo/ # SEO schema components (4) │ └── ui/ # shadcn/ui components (11) │ ├── button.tsx │ ├── card.tsx │ ├── input.tsx │ ├── dialog.tsx │ ├── select.tsx │ └── ... ├── lib/ # Core utilities │ ├── api.ts # Main API client (token management) │ ├── adminApi.ts # Admin-specific API methods │ ├── serverAuth.ts # Server-side auth validation │ ├── firebaseAuth.ts # Firebase SDK integration │ ├── firebase.ts # Firebase config │ ├── kakao.ts # Kakao SDK integration │ ├── s3Upload.ts # S3 upload utilities │ └── utils.ts # cn() helper ├── providers/ # Context providers │ └── AuthProvider.tsx # Auth state context ├── types/ # TypeScript definitions │ ├── index.ts # Common types │ ├── admin.ts # Admin types (362 lines) │ └── match.ts # Match types ├── constants/ # Constants & enums │ └── enums.ts # Enum options with Korean labels └── middleware.ts # Route protection
Import Patterns
| Pattern | Usage | Example |
|---|---|---|
| Project imports (primary) | |
| Relative | Same directory | |
| Type-only imports | |
Common Imports Cheatsheet
// Server Component (no 'use client') import { Suspense } from 'react'; import { redirect } from 'next/navigation'; import { cookies } from 'next/headers'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { getServerSession } from '@/lib/serverAuth'; import type { Metadata } from 'next'; // Client Component 'use client'; import { useState, useEffect, useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useAuth } from '@/providers/AuthProvider'; import { api } from '@/lib/api'; import { cn } from '@/lib/utils'; import { Loader2 } from 'lucide-react'; // Admin API import { getMembers, updateMemberBasic, getDashboardStats } from '@/lib/adminApi'; // Types import type { AdminMember, MemberDetail, MemberFilter } from '@/types/admin'; import type { MatchCard } from '@/types/match'; // Constants import { USER_STATUS_OPTIONS, GENDER_OPTIONS, getEnumLabel } from '@/constants/enums';
Topic Guides
shadcn/ui Overview
What is shadcn/ui?
- Beautifully designed, accessible components built on Radix UI
- Copy/paste components into your project (not a npm package dependency)
- Fully customizable with Tailwind CSS
- TypeScript-first with full type safety
Key Concepts:
- Components live in
src/components/ui/ - Use
utility for class merging (clsx + tailwind-merge)cn() - Variants via class-variance-authority (cva)
- Follows Radix UI accessibility patterns
Available Components in YGS:
- button, input, textarea, card, dialog, select, checkbox, badge, alert, skeleton, image-upload
Adding Components:
npx shadcn@latest add button npx shadcn@latest add card npx shadcn@latest add dialog npx shadcn@latest add form
Component Patterns
Server vs Client Components:
- Server Components (default): Data fetching, static content, no interactivity
- Client Components ('use client'): State, effects, event handlers, browser APIs
Key Concepts:
- Server Components are async and fetch data directly
- Client Components need 'use client' directive at the top
- Minimize Client Components for better performance
- Pass data from Server to Client Components via props
- Component structure: Props -> Hooks -> Handlers -> Render -> Export
YGS-Specific Patterns:
- Most admin components are Client Components (heavy state management)
- Landing page sections are Server Components (static content)
- Forms use manual state management (not react-hook-form)
Complete Guide: resources/component-patterns.md
Authentication (YGS-Specific)
Multi-Method Authentication:
- Firebase Social Auth: Google, Apple
- Kakao OAuth: Server-side token validation with Firebase exchange
- Custom JWT: 60-min access token, 30-day refresh token
AuthProvider Pattern:
'use client'; import { useAuth } from '@/providers/AuthProvider'; export function MyComponent() { const { user, isLoading, isAuthenticated, signupRequired, loginWithKakao, loginWithGoogle, logout, refreshUser, } = useAuth(); if (isLoading) return <Loading />; if (!isAuthenticated) return <LoginPrompt />; return <div>Welcome, {user?.nickname}</div>; }
Server-Side Auth Check (Admin Layout):
// app/admin/layout.tsx import { redirect } from 'next/navigation'; import { getServerSession } from '@/lib/serverAuth'; export default async function AdminLayout({ children }) { const session = await getServerSession(); if (!session.isAuthenticated) { redirect('/login'); } // Check admin claim from JWT const claims = parseJwtClaims(session.accessToken); if (!claims?.is_admin) { redirect('/'); } return ( <div className="flex min-h-screen"> <AdminSidebar /> <main className="flex-1">{children}</main> </div> ); }
Hydration Protection Pattern:
'use client'; import { useState, useEffect } from 'react'; import { useAuth } from '@/providers/AuthProvider'; export function Navbar() { const { user, isLoading, isAuthenticated } = useAuth(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); return ( <nav> {/* Always render static content */} <Logo /> {/* Auth-dependent content only after mount */} {mounted && !isLoading ? ( isAuthenticated ? <UserMenu user={user} /> : <LoginButton /> ) : ( <div className="w-[72px] h-10" /> // Placeholder )} </nav> ); }
Data Fetching
PRIMARY PATTERNS:
Server Component Data Fetching (Recommended):
// app/admin/page.tsx import { getDashboardStats, getRegistrationTrend } from '@/lib/adminApi'; export default async function AdminDashboard() { const [stats, trend] = await Promise.all([ getDashboardStats(), getRegistrationTrend(7), ]); return <DashboardStats stats={stats} trend={trend} />; }
Client-Side Data Fetching (Admin Pattern):
'use client'; import { useState, useEffect } from 'react'; import { getMembers } from '@/lib/adminApi'; import type { AdminMember, MemberFilter } from '@/types/admin'; export function MemberList() { const [members, setMembers] = useState<AdminMember[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { async function fetchMembers() { try { setLoading(true); const response = await getMembers({ skip: 0, limit: 20 }); setMembers(response.items); } catch (err) { setError(err instanceof Error ? err.message : '데이터를 불러오는데 실패했습니다.'); } finally { setLoading(false); } } fetchMembers(); }, []); if (loading) return <Skeleton />; if (error) return <Alert variant="destructive">{error}</Alert>; return <MemberTable members={members} />; }
Complete Guide: resources/data-fetching.md
API Client Patterns
Main API Client (
):lib/api.ts
import { api } from '@/lib/api'; // Token management import { getAccessToken, setTokens, clearTokens, hasTokens } from '@/lib/api'; // Auth methods await api.post('/api/v1/auth/login', { phone, password }); await firebaseLogin(idToken); await kakaoLogin(code, redirectUri); // Generic methods const data = await api.get<ResponseType>('/api/v1/endpoint'); await api.post('/api/v1/endpoint', body); await api.patch('/api/v1/endpoint', changes);
Admin API Client (
):lib/adminApi.ts
import { // Dashboard getDashboardStats, getRegistrationTrend, getGenderRatio, getReferralStats, // Member Management getMembers, getMemberDetail, updateMemberBasic, updateMemberProfile, updateMemberLifestyle, updateMemberPreference, updateMemberSubscription, exportMembersToExcel, // Consultations getConsultations, createConsultation, updateConsultation, deleteConsultation, // Matching getCandidates, getCompatibilityScore, } from '@/lib/adminApi';
Constants & Enums Pattern
Define in
:constants/enums.ts
// constants/enums.ts export const USER_STATUS_OPTIONS = [ { value: "draft", label: "상담 전" }, { value: "pending_review", label: "상담 예정" }, { value: "active", label: "상담 완료" }, { value: "suspended", label: "정지" }, { value: "withdrawn", label: "탈퇴" }, ] as const; export const GENDER_OPTIONS = [ { value: "male", label: "남성" }, { value: "female", label: "여성" }, ] as const; // Helper functions export function getEnumLabel( options: readonly { value: string; label: string }[], value: string | null | undefined ): string { if (!value) return "-"; return options.find(o => o.value === value)?.label ?? value; } export function getUserStatusLabel(value: string | null | undefined): string { return getEnumLabel(USER_STATUS_OPTIONS, value); }
Usage in Components:
import { USER_STATUS_OPTIONS, getEnumLabel } from '@/constants/enums'; // In Select component <Select value={status} onValueChange={setStatus}> <SelectTrigger> <SelectValue placeholder="상태 선택" /> </SelectTrigger> <SelectContent> {USER_STATUS_OPTIONS.map(option => ( <SelectItem key={option.value} value={option.value}> {option.label} </SelectItem> ))} </SelectContent> </Select> // Display label <span>{getEnumLabel(USER_STATUS_OPTIONS, member.status)}</span>
Form Patterns (YGS-Specific)
Pattern 1: Manual State Management (Most Common in YGS):
'use client'; import { useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Loader2 } from 'lucide-react'; interface FormData { phone: string; name: string; gender: string; birthYear: string; } interface FormErrors { phone?: string; name?: string; gender?: string; birthYear?: string; } export function SignupForm() { const [formData, setFormData] = useState<FormData>({ phone: '', name: '', gender: '', birthYear: '' }); const [errors, setErrors] = useState<FormErrors>({}); const [loading, setLoading] = useState(false); const validateForm = (): boolean => { const newErrors: FormErrors = {}; if (!/^010-\d{4}-\d{4}$/.test(formData.phone)) { newErrors.phone = "올바른 전화번호 형식이 아닙니다."; } if (formData.name.length < 2) { newErrors.name = "이름은 2자 이상이어야 합니다."; } if (!formData.gender) { newErrors.gender = "성별을 선택해주세요."; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleInputChange = useCallback((field: keyof FormData, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); if (errors[field]) { setErrors(prev => ({ ...prev, [field]: undefined })); } }, [errors]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) return; setLoading(true); try { await api.post('/signup', formData); } catch (err) { setErrors({ ...errors, phone: "회원가입에 실패했습니다." }); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit} className="space-y-4"> <div className="space-y-2"> <Input value={formData.phone} onChange={e => handleInputChange('phone', e.target.value)} placeholder="전화번호" className={cn(errors.phone && "border-red-500")} /> {errors.phone && <p className="text-sm text-red-500">{errors.phone}</p>} </div> <Button type="submit" disabled={loading} className="w-full"> {loading ? <Loader2 className="animate-spin mr-2 h-4 w-4" /> : null} {loading ? "처리 중..." : "가입하기"} </Button> </form> ); }
Pattern 2: Modal Form (Admin Edit):
'use client'; import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { updateMemberBasic } from '@/lib/adminApi'; import type { MemberDetail } from '@/types/admin'; interface EditModalProps { isOpen: boolean; onClose: () => void; onSuccess: (member: MemberDetail) => void; member: MemberDetail; } export function BasicInfoEditModal({ isOpen, onClose, onSuccess, member }: EditModalProps) { const [formData, setFormData] = useState({ name: '', status: '' }); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); useEffect(() => { if (isOpen) { setFormData({ name: member.name, status: member.status }); setError(''); } }, [isOpen, member]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(''); try { // Only send changed fields const changes: Record<string, string> = {}; if (formData.name !== member.name) changes.name = formData.name; if (formData.status !== member.status) changes.status = formData.status; if (Object.keys(changes).length === 0) { onClose(); return; } const result = await updateMemberBasic(member.id, changes); onSuccess(result); onClose(); } catch (err) { setError(err instanceof Error ? err.message : '수정에 실패했습니다.'); } finally { setLoading(false); } }; return ( <Dialog open={isOpen} onOpenChange={onClose}> <DialogContent> <DialogHeader> <DialogTitle>기본 정보 수정</DialogTitle> </DialogHeader> <form onSubmit={handleSubmit} className="space-y-4"> <Input value={formData.name} onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="이름" /> {error && <p className="text-sm text-red-500">{error}</p>} <DialogFooter> <Button type="button" variant="outline" onClick={onClose}>취소</Button> <Button type="submit" disabled={loading}> {loading ? '저장 중...' : '저장'} </Button> </DialogFooter> </form> </DialogContent> </Dialog> ); }
URL-Based State Pattern (Pagination/Filtering)
'use client'; import { useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import type { MemberFilter } from '@/types/admin'; export function useMemberFilters() { const searchParams = useSearchParams(); const router = useRouter(); const filters: MemberFilter = { status: searchParams.get('status') || '', gender: searchParams.get('gender') || '', search: searchParams.get('search') || '', skip: Number(searchParams.get('skip')) || 0, limit: Number(searchParams.get('limit')) || 20, }; const updateURL = useCallback((newFilters: Partial<MemberFilter>) => { const params = new URLSearchParams(); const merged = { ...filters, ...newFilters }; if (merged.status) params.set('status', merged.status); if (merged.gender) params.set('gender', merged.gender); if (merged.search) params.set('search', merged.search); if (merged.skip) params.set('skip', String(merged.skip)); if (merged.limit !== 20) params.set('limit', String(merged.limit)); router.push(`/admin/members${params.toString() ? `?${params}` : ''}`); }, [filters, router]); return { filters, updateURL }; }
Styling
shadcn/ui + Tailwind CSS 4:
- Primary: shadcn/ui pre-built components with Tailwind
- Customization: Override with Tailwind utility classes
- Class merging: Use
utility for conditional classescn()
cn() Utility (IMPORTANT):
import { cn } from '@/lib/utils'; // Conditional classes <div className={cn( "flex items-center gap-2", isActive && "bg-primary text-white", isScrolled && "shadow-sm", className )}> // Status-based styling const statusColors: Record<string, string> = { draft: "bg-slate-100 text-slate-600", active: "bg-green-100 text-green-700", suspended: "bg-red-100 text-red-600", }; <Badge className={cn(statusColors[status] || "bg-gray-100")}> {getStatusLabel(status)} </Badge>
YGS Color System:
// Primary brand color (Coral/Orange) className="bg-primary text-white" className="hover:bg-primary-dark" className="text-primary" // Gradients className="bg-gradient-to-r from-amber-500 to-orange-500" // Status colors className="text-red-500" // Errors, destructive className="bg-green-50" // Success className="text-gray-500" // Muted
Responsive Patterns:
// Grid className="grid grid-cols-1 lg:grid-cols-4 gap-4" className="grid grid-cols-2 lg:grid-cols-4 gap-4" // Text className="text-xl sm:text-2xl md:text-3xl lg:text-4xl" // Display className="hidden md:flex" // Hidden on mobile className="md:hidden" // Mobile only // Padding className="px-4 sm:px-6 md:px-12"
Complete Guide: resources/styling-guide.md
File Organization
App Router Structure:
src/ app/ page.tsx # Home page (/) layout.tsx # Root layout {route}/ page.tsx # Route page layout.tsx # Route layout (optional) loading.tsx # Route loading error.tsx # Route error api/ {route}/ route.ts # API route handler components/ ui/ # shadcn/ui components button.tsx card.tsx input.tsx admin/ # Admin-specific components DashboardStats.tsx MemberTable.tsx modals/ # Edit modals {feature}/ # Feature-specific components Component.tsx
Component Organization:
- shadcn/ui components in
src/components/ui/ - Feature components in
src/components/{feature}/ - Admin components in
src/components/admin/ - Keep Server and Client components separate
Complete Guide: resources/file-organization.md
Loading & Error States
App Router Conventions:
Loading:
// loading.tsx (route-level) import { Skeleton } from '@/components/ui/skeleton'; export default function Loading() { return ( <div className="space-y-4"> <Skeleton className="h-8 w-48" /> <div className="grid grid-cols-1 lg:grid-cols-4 gap-4"> {[...Array(4)].map((_, i) => ( <Skeleton key={i} className="h-24" /> ))} </div> </div> ); }
Error Handling (Korean):
// error.tsx (route-level) 'use client'; import { Button } from '@/components/ui/button'; import { AlertCircle } from 'lucide-react'; export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div className="flex flex-col items-center justify-center py-12"> <AlertCircle className="h-12 w-12 text-destructive mb-4" /> <h2 className="text-xl font-bold mb-2">오류가 발생했습니다</h2> <p className="text-muted-foreground mb-4">페이지를 불러오는 중 문제가 발생했습니다.</p> <Button onClick={reset}>다시 시도</Button> </div> ); }
Complete Guide: resources/loading-and-error-states.md
Performance
Next.js 15 Optimizations:
- Server Components by default (zero JS to client)
- Dynamic imports:
const Heavy = dynamic(() => import('./Heavy')) - Image optimization:
componentnext/image - Font optimization: Built-in font loading
- Turbopack: Faster dev builds (already enabled)
React 19 Patterns:
: Expensive computationsuseMemo
: Event handlers passed to childrenuseCallback
: Prevent unnecessary re-rendersReact.memo
Complete Guide: resources/performance.md
TypeScript
Standards:
- Strict mode enabled
- Explicit return types on functions
- Type imports:
import type { User } from '@/types' - Component prop interfaces with JSDoc
- No
type (useany
if needed)unknown
YGS Type Patterns:
// types/admin.ts interface AdminMember { id: string; name: string; gender: "male" | "female"; phone: string; status: string; birth_year: number | null; created_at: string; } interface MemberDetail extends AdminMember { profile: UserProfile | null; lifestyle: UserLifestyle | null; preference: UserPreference | null; subscription: UserSubscription | null; documents: UserDocument[]; photos: UserPhoto[]; } // Update request types (partial) interface BasicInfoUpdateRequest { name?: string; status?: string; is_admin?: boolean; birth_year?: number; gender?: "male" | "female"; }
Complete Guide: resources/typescript-standards.md
Navigation Guide
| Need to... | Read this resource |
|---|---|
| Create a component | component-patterns.md |
| Fetch data | data-fetching.md |
| Organize files/folders | file-organization.md |
| Style components | styling-guide.md |
| Set up routing | routing-guide.md |
| Handle loading/errors | loading-and-error-states.md |
| Optimize performance | performance.md |
| TypeScript types | typescript-standards.md |
| Forms/Auth/API Routes | common-patterns.md |
| See full examples | complete-examples.md |
Core Principles
- Server Components First: Use Server Components by default, Client Components only for interactivity
- Async Data Fetching: Fetch data directly in Server Components
- Minimize Client JS: Less JavaScript sent to the browser = better performance
- App Router Conventions: Use loading.tsx, error.tsx, layout.tsx appropriately
- shadcn/ui Components: Use pre-built accessible components from
@/components/ui/ - cn() for Classes: Always use
for conditional/merged class namescn() - Import with @/ alias: Consistent import paths across the project
- Type Safety: Strict TypeScript with explicit types
- Korean Localization: All user-facing text in Korean
- AuthProvider: Use
hook for client-side auth stateuseAuth()
Quick Reference: Templates
Server Component Template
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { getDashboardStats } from '@/lib/adminApi'; import type { DashboardStats } from '@/types/admin'; import type { Metadata } from 'next'; export const metadata: Metadata = { title: '대시보드 | YGS 관리자', }; export default async function DashboardPage() { const stats: DashboardStats = await getDashboardStats(); return ( <div className="container py-8"> <h1 className="text-2xl font-bold mb-6">대시보드</h1> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <Card> <CardHeader> <CardTitle>전체 회원</CardTitle> </CardHeader> <CardContent> <p className="text-3xl font-bold">{stats.total_members}</p> </CardContent> </Card> </div> </div> ); }
Client Component Template
'use client'; import { useState, useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Skeleton } from '@/components/ui/skeleton'; import { getMembers } from '@/lib/adminApi'; import { cn } from '@/lib/utils'; import { Loader2 } from 'lucide-react'; import type { AdminMember } from '@/types/admin'; interface MemberListProps { className?: string; } export function MemberList({ className }: MemberListProps) { const [members, setMembers] = useState<AdminMember[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const fetchMembers = useCallback(async () => { try { setLoading(true); const response = await getMembers({ skip: 0, limit: 20 }); setMembers(response.items); } catch (err) { setError(err instanceof Error ? err.message : '데이터를 불러오는데 실패했습니다.'); } finally { setLoading(false); } }, []); useEffect(() => { fetchMembers(); }, [fetchMembers]); if (loading) { return ( <div className="space-y-4"> {[...Array(5)].map((_, i) => ( <Skeleton key={i} className="h-16 w-full" /> ))} </div> ); } if (error) { return ( <div className="text-center py-8"> <p className="text-red-500 mb-4">{error}</p> <Button onClick={fetchMembers}>다시 시도</Button> </div> ); } return ( <div className={cn("space-y-4", className)}> {members.map(member => ( <div key={member.id} className="p-4 border rounded-lg"> <p className="font-medium">{member.name}</p> <p className="text-sm text-muted-foreground">{member.phone}</p> </div> ))} </div> ); }
For complete examples, see resources/complete-examples.md
Related Skills
- error-tracking: Error tracking with Sentry (applies to frontend too)
- fastapi-backend-guidelines: Backend API patterns that frontend consumes
Skill Status: Updated for YGS project with comprehensive coverage of actual codebase patterns