PFPlay ์๋น์ค๋ฅผ ์ํ ๊ฐ์ ์ ์ ์๋ฎฌ๋ ์ด์ ๊ด๋ฆฌ ์ฝ์์ ๋๋ค.
- ๊ฐ์ ์ ์ ์์ฑ (๊ฐ๋ณ/๋๋)
- ์ ์ ๋ชฉ๋ก ์กฐํ ๋ฐ ๊ด๋ฆฌ
- ์ ์ ๋ฑ๊ธ ๊ด๋ฆฌ (Free, Premium, VIP)
- ํํฐ๋ฃธ ๋ฐฐ์ ์ํ ์ถ์
- ๋๋ ์์ฑ (์ต๋ 100๋ช )
- ํํฐ๋ฃธ ์์ฑ/์ญ์
- ์ ์ ๋ฐฐ์ ๋ฐ ์ ๊ฑฐ (๋ฉํฐ ์ ๋ ํธ)
- DJ ํ ๊ด๋ฆฌ (ํ๋ ์ด๋ฆฌ์คํธ + ํธ๋ ์ ํ)
- ๋ฃธ ์ํ ๋ชจ๋ํฐ๋ง
- ์ฑํ ์๋๋ฆฌ์ค: ์คํฌ๋ฆฝํธ ๊ธฐ๋ฐ ์ฑํ ๋ฉ์์ง ์๋ ์์ฑ
- ๋ฐ์ ์๋๋ฆฌ์ค: ์ข์์/์ก๊ธฐ ๋ฐ์ ์๋ ์์ฑ
- React 18.3.1 - UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- TypeScript 5 - ํ์ ์์ ์ฑ
- Vite 6.0.11 - ๋น๋ ๋๊ตฌ ๋ฐ ๊ฐ๋ฐ ์๋ฒ (ํฌํธ 3000)
- React Router DOM 7.1.3 - ํด๋ผ์ด์ธํธ ๋ผ์ฐํ
- Tailwind CSS 4.1.9 - ์ ํธ๋ฆฌํฐ CSS ํ๋ ์์ํฌ
- shadcn/ui - Radix UI ๊ธฐ๋ฐ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- Radix UI - ํค๋๋ฆฌ์ค UI ํ๋ฆฌ๋ฏธํฐ๋ธ
- Lucide React 0.454.0 - ์์ด์ฝ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- Class Variance Authority 0.7.1 - ์ปดํฌ๋ํธ variant ๊ด๋ฆฌ
- Zustand 5.0.9 - ์ ์ญ ์ํ ๊ด๋ฆฌ
- Immer - ๋ถ๋ณ ์ํ ์ ๋ฐ์ดํธ
- TanStack Query 5.62.12 - ์๋ฒ ์ํ ๊ด๋ฆฌ
- React Hook Form 7.60.0 - ํผ ์ํ ๊ด๋ฆฌ
- Zod 3.25.76 - TypeScript ์ฐ์ ์คํค๋ง ๊ฒ์ฆ
- date-fns 4.1.0 - ๋ ์ง ์ ํธ๋ฆฌํฐ
- Sonner - ํ ์คํธ ์๋ฆผ
- clsx 2.1.1 - ์กฐ๊ฑด๋ถ className ์ ํธ๋ฆฌํฐ
- tailwind-merge 3.3.1 - CSS ํด๋์ค ๋ณํฉ
ํ๋ก์ ํธ๋ Feature-Sliced Design (FSD) ์ํคํ ์ฒ๋ฅผ ๋ฐ๋ฆ ๋๋ค.
src/
โโโ app/ # ์ ํ๋ฆฌ์ผ์ด์
์ด๊ธฐํ ๋ฐ ๋ ์ด์์
โ โโโ layout.tsx # ์ฌ์ด๋๋ฐ ๋ค๋น๊ฒ์ด์
์ด ํฌํจ๋ ๋ฉ์ธ ๋ ์ด์์
โ
โโโ pages/ # ๋ผ์ฐํธ๋ณ ํ์ด์ง ์ปดํฌ๋ํธ
โ โโโ users-page.tsx # ๊ฐ์ ์ ์ ๊ด๋ฆฌ ํ์ด์ง
โ โโโ rooms-page.tsx # ํํฐ๋ฃธ ๊ด๋ฆฌ ํ์ด์ง
โ โโโ scenarios-page.tsx # ์๋๋ฆฌ์ค ์คํ ํ์ด์ง
โ
โโโ widgets/ # ํ์ด์ง ๋ ๋ฒจ ๋ณตํฉ ์ปดํฌ๋ํธ
โ โโโ users/ui/
โ โ โโโ users-widget.tsx
โ โโโ rooms/ui/
โ โ โโโ rooms-widget.tsx
โ โโโ scenarios/ui/
โ โโโ scenarios-widget.tsx
โ
โโโ features/ # ๋น์ฆ๋์ค ๋ก์ง ๊ธฐ๋ฅ ๋ชจ๋
โ โโโ users/ui/
โ โ โโโ user-create-form.tsx
โ โ โโโ users-list-table.tsx
โ โ โโโ bulk-actions.tsx
โ โโโ rooms/ui/
โ โ โโโ rooms-list-panel.tsx
โ โ โโโ user-assignment-panel.tsx
โ โ โโโ room-selector.tsx
โ โ โโโ dj-queue-panel.tsx
โ โโโ scenarios/ui/
โ โโโ chat-scenario-panel.tsx
โ โโโ reaction-scenario-panel.tsx
โ
โโโ shared/ # ๊ณต์ ๋ฆฌ์์ค
โ โโโ types/ # TypeScript ํ์
์ ์
โ โ โโโ index.ts
โ โโโ store/ # Zustand ์ํ ๊ด๋ฆฌ
โ โ โโโ users-store.ts
โ โ โโโ rooms-store.ts
โ โโโ hooks/ # ์ปค์คํ
React ํ
โ โ โโโ use-chat-scenario.ts
โ โ โโโ use-reaction-scenario.ts
โ โโโ lib/ # ์ ํธ๋ฆฌํฐ ๋ฐ API ํด๋ผ์ด์ธํธ
โ โโโ api-client.ts
โ โโโ constants.ts
โ โโโ utils.ts
โ โโโ index.ts
โ
โโโ components/
โ โโโ ui/ # shadcn/ui ์ปดํฌ๋ํธ
โ โโโ badge.tsx
โ โโโ button.tsx
โ โโโ card.tsx
โ โโโ checkbox.tsx
โ โโโ input.tsx
โ โโโ label.tsx
โ โโโ select.tsx
โ โโโ table.tsx
โ โโโ tabs.tsx
โ โโโ textarea.tsx
โ
โโโ App.tsx # ๋ผ์ฐํฐ ์ค์
โโโ main.tsx # ์ ํ๋ฆฌ์ผ์ด์
์ง์
์
โโโ globals.css # Tailwind๋ฅผ ํฌํจํ ์ ์ญ ์คํ์ผ
App Layer (app/)
โ
Pages Layer (pages/)
โ
Widgets Layer (widgets/)
โ
Features Layer (features/)
โ
Shared Layer (shared/)
๋ ์ด์ด ๊ท์น:
- app: ์ ํ๋ฆฌ์ผ์ด์ ์ด๊ธฐํ, ๋ผ์ฐํ , ์ ์ญ ๋ ์ด์์
- pages: ํ์ด์ง๋ณ ์์ ฏ ์กฐํฉ
- widgets: ๋ ๋ฆฝ์ ์ธ ํ์ด์ง ๋ธ๋ก, ์ฌ๋ฌ feature ์กฐํฉ
- features: ์ฌ์ฉ์ ์๋๋ฆฌ์ค ๊ตฌํ, ๋น์ฆ๋์ค ๋ก์ง
- shared: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ฝ๋, ๋ชจ๋ ๋ ์ด์ด์์ ์ฌ์ฉ ๊ฐ๋ฅ
๊ฐ ๋ ์ด์ด๋ ํ์ ๋ ์ด์ด๋ง ์์กดํ ์ ์์ผ๋ฉฐ, ์ํ ์ฐธ์กฐ๊ฐ ์์ต๋๋ค.
State:
{
users: VirtualUser[]
isLoading: boolean
error: string | null
}Actions:
fetchUsers(): Promise<void> // ์ ์ ๋ชฉ๋ก ์กฐํ
createUser(userData): Promise<void> // ์ ์ ์์ฑ
deleteUser(userId): Promise<void> // ์ ์ ์ญ์
updateUser(userId, updates): void // ์ ์ ์ ๋ณด ์์
getUserById(userId): VirtualUser | undefined
getUsersInRoom(roomId): VirtualUser[] // ํน์ ๋ฃธ์ ์ ์ ์กฐํ
getAvailableUsers(): VirtualUser[] // ๋ฐฐ์ ๊ฐ๋ฅํ ์ ์ ์กฐํState:
{
rooms: PartyRoom[]
selectedRoomId: string | null
isLoading: boolean
error: string | null
}Actions:
fetchRooms(): Promise<void> // ๋ฃธ ๋ชฉ๋ก ์กฐํ
createRoom(roomData): Promise<void> // ๋ฃธ ์์ฑ
deleteRoom(roomId): Promise<void> // ๋ฃธ ์ญ์
selectRoom(roomId): void // ๋ฃธ ์ ํ
getSelectedRoom(): PartyRoom | undefined // ์ ํ๋ ๋ฃธ ์กฐํ
assignUsers(roomId, userIds): Promise<void> // ์ ์ ๋ฐฐ์
removeUser(roomId, userId): Promise<void> // ์ ์ ์ ๊ฑฐ{
id: string
username: string
email: string
profileImage?: string
tier: "free" | "premium" | "vip"
status: "active" | "inactive"
isInRoom: boolean
currentRoomId?: string
createdAt: string
lastActiveAt: string
}{
id: string
name: string
maxCapacity: number
currentUsers: number
createdAt: string
status: "active" | "inactive"
userIds: string[]
}{
id: string
roomId: string
userId: string
playlistId: string
trackId: string
position: number
createdAt: string
}{
userId: string
username: string
message: string
order: number
}{
userId: string
username: string
type: "like" | "grab"
delay: number
}์ฑํ ์คํฌ๋ฆฝํธ ๋ถ์ ๋ฐ ์ ์ ๋ฐฐ์น ๋ก์ง์ ๊ด๋ฆฌํฉ๋๋ค.
๊ธฐ๋ฅ:
- ์คํฌ๋ฆฝํธ๋ฅผ ๋ผ์ธ๋ณ๋ก ํ์ฑ
- ๋ฃธ ๋ด ์ ์ ์๊ฒ ์ํ ๋ฐฐ์
- ๋ฐฐ์น ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ ๊ณต
- ์คํ ์๋ฎฌ๋ ์ด์
์ฌ์ฉ ์:
const {
script,
setScript,
assignments,
analyzeScript
} = useChatScenario()๋ฆฌ์ก์ ์ด๋ฒคํธ ์๋ ์์ฑ ๋ก์ง์ ๊ด๋ฆฌํฉ๋๋ค.
๊ธฐ๋ฅ:
- ์ฐธ์ฌ์จ 70% ์๋ ์ ์ฉ
- ์ข์์/์ก๊ธฐ 50:50 ๋ถ๋ฐฐ
- 1-15์ด ๋๋ค ๋๋ ์ด
- ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ ์คํ
์ฌ์ฉ ์:
const {
assignments,
generateAssignments,
getLikeAssignments,
getGrabAssignments
} = useReactionScenario()- ์ฌ๋ฌ ์ค์ ์ฑํ ์คํฌ๋ฆฝํธ ์ ๋ ฅ
analyzeScript()ํธ์ถํ์ฌ ํ์ฑ- ๋ฃธ ๋ด ์ ์ ์๊ฒ ์ํ ๋ฐฐ์
- ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ก ๋ฐฐ์ ๊ฒฐ๊ณผ ํ์ธ
- ์คํํ์ฌ ์๋ฎฌ๋ ์ด์ ์ํ
- ๋ฃธ๊ณผ ์ ์ ์ ํ
generateAssignments()ํธ์ถ- ์๋์ผ๋ก ์ฐธ์ฌ์จ 70% ์ ์ฉ
- ์ข์์/์ก๊ธฐ 50:50 ๋ถ๋ฐฐ
- ๊ฐ ์ ์ ์๊ฒ 1-15์ด ๋๋ค ๋๋ ์ด
- ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ์ธ ํ ์คํ
- Node.js 18 ์ด์
- npm ๋๋ pnpm
# ์์กด์ฑ ์ค์น
npm install
# ๊ฐ๋ฐ ์๋ฒ ์คํ (ํฌํธ 3000)
npm run dev
# ํ๋ก๋์
๋น๋
npm run build
# ๋น๋ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
npm run preview
# ๋ฆฐํธ ์คํ
npm run lintํ์ฌ๋ Mock ๋ฐ์ดํฐ๋ก ๋์ํฉ๋๋ค:
- ๊ฐ์ ์ ์ 15๋ช ์๋ ์์ฑ
- ํํฐ๋ฃธ 2๊ฐ (ํ๋๋ 12๋ช ํฌํจ, ํ๋๋ ๋น ๋ฃธ)
- ํ๊ฒฝ ๋ณ์ ์ค์
VITE_API_BASE_URL=http://localhost:8080-
API Client ํ์ฑํ
src/shared/lib/api-client.ts์ ์ฃผ์์ ํด์ ํ์ธ์. -
Store ์์ ๊ฐ store(
users-store.ts,rooms-store.ts)์ API ํธ์ถ ์ฃผ์์ ํด์ ํ๊ณ , Mock ๋ฐ์ดํฐ ์ฝ๋๋ฅผ ์ ๊ฑฐํ์ธ์.
- ๊ธฐ๋ณธ: ๋คํฌ ๋ชจ๋ ์ฐ์
- ์๊ฐ: TOSS ๋์์ธ ์์คํ
--primary: oklch(0.61 0.24 264) /* Purple */
--background: oklch(0.11 0 0) /* Near Black */
--foreground: oklch(0.98 0 0) /* Near White */
--accent: oklch(0.89 0 0) /* Gray */
--destructive: oklch(0.61 0.20 28) /* Red */- Font Family: Geist (sans) & Geist Mono
- Scale: Tailwind ๊ธฐ๋ณธ ์ค์ผ์ผ
- Weight: 400 (Regular), 500 (Medium), 600 (Semibold)
- Base Radius: 0.5rem
- Spacing: Tailwind ์ค์ผ์ผ (4, 6, 8, 12, 16, 20, 24...)
TypeScript์ Vite๋ ๋ค์ ๊ฒฝ๋ก ๋ณ์นญ์ ์ง์ํฉ๋๋ค:
// @ = src/
import { cn } from "@/shared/lib/utils"
import { Button } from "@/components/ui/button"
import { useUsersStore } from "@/shared/store/users-store"tsconfig.json ์ค์ :
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}์ค์: @/ ๋ณ์นญ์ ์ฌ์ฉํ๋ ค๋ฉด ํ์ผ์ด src/ ํด๋ ๋ด๋ถ์ ์์นํด์ผ ํฉ๋๋ค.
UserAssignmentPanel (UI)
โ
User selects room & users
โ
assignUsers() action called
โ
useRoomsStore (Zustand)
โ
State updated:
- room.userIds += selected users
- room.currentUsers += count
โ
useUsersStore updated:
- user.isInRoom = true
- user.currentRoomId = roomId
โ
UI re-renders
โ
Toast notification (Sonner)
- ์ปดํฌ๋ํธ:
kebab-case.tsx - ์คํ ์ด:
feature-store.ts - ํ
:
use-feature-name.ts - ํ์
:
index.ts
// 1. React ๋ฐ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
import { useState } from "react"
import { useQuery } from "@tanstack/react-query"
// 2. ๋ด๋ถ ์ปดํฌ๋ํธ
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
// 3. ์์ด์ฝ
import { Plus, Trash2 } from "lucide-react"
// 4. ์คํ ์ด ๋ฐ ํ
import { useUsersStore } from "@/shared/store/users-store"
import { useChatScenario } from "@/shared/hooks/use-chat-scenario"
// 5. ์ ํธ๋ฆฌํฐ
import { cn, formatDate } from "@/shared/lib/utils"
// 6. ํ์
import type { VirtualUser } from "@/shared/types""use client" // ํ์ํ ๊ฒฝ์ฐ๋ง
// Imports
export function ComponentName() {
// 1. Hooks
const store = useStore()
const [state, setState] = useState()
// 2. Derived state
const computed = useMemo(() => ...)
// 3. Handlers
const handleClick = () => { ... }
// 4. Effects
useEffect(() => { ... }, [])
// 5. Render
return (...)
}๊ฐ๋ฐ ํ๊ฒฝ์์ ์๋ ์์ฑ๋๋ Mock ๋ฐ์ดํฐ:
- Free tier: 8๋ช
- Premium tier: 4๋ช
- VIP tier: 3๋ช
- ์ผ๋ถ๋ ๋ฃธ์ ๋ฐฐ์ ๋จ
- "๋ฉ์ธ ํํฐ๋ฃธ": 12๋ช ์ ์ ํฌํจ, ์ต๋ 20๋ช
- "VIP ๋ผ์ด์ง": ๋น ๋ฃธ, ์ต๋ 10๋ช
์ ํ๋ฆฌ์ผ์ด์ ์ ์ญ ์ค์ ๊ณผ ๋ ์ด์์์ ๊ด๋ฆฌํฉ๋๋ค.
- ๋ผ์ฐํ ๊ตฌ์ฑ
- ์ ์ญ ํ๋ก๋ฐ์ด๋
- ๋ฉ์ธ ๋ ์ด์์ (์ฌ์ด๋๋ฐ ๋ค๋น๊ฒ์ด์ )
๋ผ์ฐํธ๋ณ ํ์ด์ง๋ฅผ ์ ์ํฉ๋๋ค. Widget์ ์กฐํฉํ์ฌ ๊ตฌ์ฑ๋ฉ๋๋ค.
- ์์ ๋ํผ ์ญํ
- ๋ผ์ฐํธ๋น ํ๋์ ํ์ด์ง
ํ์ด์ง ์์ค์ ๋ณตํฉ ์ปดํฌ๋ํธ์ ๋๋ค.
- ์ฌ๋ฌ Feature ์กฐํฉ
- ํญ ๊ธฐ๋ฐ ์ธํฐํ์ด์ค
- ๋ ๋ฆฝ์ ์ด๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ธ๋ก
๋น์ฆ๋์ค ๋ก์ง์ด ํฌํจ๋ ๊ธฐ๋ฅ ๋จ์ ์ปดํฌ๋ํธ์ ๋๋ค.
- ์ฌ์ฉ์ ๋๋ฉด ๊ธฐ๋ฅ
- UI๋ง ํฌํจ, ๋๋ฉ์ธ ๋ก์ง์ shared์
- Feature๋ณ๋ก ์กฐ์งํ
๋ชจ๋ ๋ ์ด์ด์์ ์ฌ์ฉ ๊ฐ๋ฅํ ๊ณต์ ๋ฆฌ์์ค์ ๋๋ค.
- types: ๋๋ฉ์ธ ํ์ ์ ์
- store: Zustand ์ํ ๊ด๋ฆฌ
- hooks: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปค์คํ ํ
- lib: ์ ํธ๋ฆฌํฐ ํจ์ ๋ฐ API ํด๋ผ์ด์ธํธ
shadcn/ui ๊ธฐ๋ฐ์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ UI ์ปดํฌ๋ํธ์ ๋๋ค.
- Radix UI ๋ํผ
- Tailwind๋ก ์คํ์ผ๋ง
- Variant ์์คํ
- ๋ช ํํ ์์กด์ฑ ๋ฐฉํฅ: App โ Pages โ Widgets โ Features โ Shared
- ์ํ ์์กด์ฑ ์์
- Feature๋ ๋ค๋ฅธ Feature์ ์์กดํ ์ ์์
/shared/store์ ์ค์ ์ง์คํ- Devtools ๋ฏธ๋ค์จ์ด
- Mock ๋ฐ์ดํฐ๋ก ๊ฐ๋ฐ
- ์ค์ API ์ฐ๋ ์ค๋น ์๋ฃ
- shadcn/ui ๊ธฐ๋ณธ ์ปดํฌ๋ํธ
- Feature ์ปดํฌ๋ํธ๊ฐ UI ์์ ์กฐํฉ
- Widget์ด Feature ์กฐํฉ
- Page๊ฐ Widget ์ฌ์ฉ
- ๋น์ฆ๋์ค ๋ก์ง์ ํ ์ผ๋ก ๋ถ๋ฆฌ
- ์ปดํฌ๋ํธ ๊ฐ ์ฌ์ฌ์ฉ
- TypeScript๋ก ํ์ ์์ ์ฑ
- ํ ์คํธ ๊ฐ๋ฅํ ๋ก์ง ๋ถ๋ฆฌ
MIT
์ด์ ๋ฐ Pull Request๋ฅผ ํ์ํฉ๋๋ค.
Built with โค๏ธ using React + Vite + Tailwind CSS