BandStage
분산된 공연, 아티스트, 예매 정보를 구조화해 하나의 흐름으로 연결한 라이브 음악 플랫폼. 데이터 모델과 탐색 경험을 함께 설계해 공연을 찾는 과정을 시스템으로 정의했습니다.

Problem
공연 정보가 SNS, 포스터, 예매 플랫폼에 분산되어 있어 사용자가 원하는 공연을 한 번에 탐색할 수 없는 문제가 있습니다. 아티스트, 공연장, 팬이 각각 다른 채널에서 움직이며 공연 정보가 하나의 흐름으로 연결되지 않습니다.
Limitation
기존 공연 정보는 SNS, 포스터, 예매 플랫폼에 분산되어 있으며, 데이터가 구조화되지 않아 검색, 필터링, 예매까지 하나의 흐름으로 연결되지 않습니다. 사용자는 원하는 공연을 탐색하기 위해 여러 채널을 반복적으로 확인해야 하고, 아티스트 역시 공연 등록과 관리, 홍보를 통합적으로 수행할 수 없는 구조입니다.
Solution
분산된 공연 정보를 하나의 흐름으로 연결하기 위해, 공연 생태계를 "Region → Venue → Event → Reservation" 4계층 데이터 모델로 구조화했습니다. 사용자는 지역, 공연장, 공연 단위를 기준으로 탐색할 수 있고, 예매까지 하나의 흐름 안에서 이어지도록 설계했습니다. 또한 아티스트가 공연을 직접 등록하고 관리할 수 있도록 공연 등록 워크플로우를 구성하고, 이를 상태 머신(DRAFT → PENDING → APPROVED → PUBLISHED)으로 정의해 권한별 전환을 코드 레벨에서 통제했습니다. 사용자 역할(Fan, Artist, Venue Manager)에 따라 서로 다른 진입 경로를 가지지만, 탐색, 등록, 관리 기능이 모두 동일한 데이터 구조 위에서 동작하도록 설계해 시스템 전체 흐름을 일관되게 유지했습니다.
System Architecture
User (Fan) User (Artist) User (Venue Manager)
| | |
v v v
[Region Filter] [Event Registration] [Venue Management]
| DRAFT → PENDING |
v APPROVED → PUBLISHED |
[Venue List] | |
| v |
v [Event Detail Page] |
[Event Browse] ←────────────────────────── [Venue Profile]
|
v
[Reservation Flow]
├── Ticket Type Selection
├── Quantity Management
└── Booking ConfirmationKey Implementation
서버 컴포넌트 우선 아키텍처로 클라이언트 번들을 최소화했습니다. 동적 라우팅(/events/[slug])에서 정적 생성과 ISR을 조합해 성능과 데이터 신선도를 동시에 확보했습니다. NextAuth.js v4로 FAN/ARTIST/VENUE_MANAGER/ADMIN 4역할 인증 시스템을 구현하고, 미들웨어 레벨에서 역할별 라우트를 보호했습니다. 실제 서울 공연장 25개 데이터를 구조화해 Supabase PostgreSQL에 시드했습니다.
Key Code
| 1 | export async function createEvent(input: EventInput) { |
| 2 | const session = await auth() |
| 3 | if (!session?.user) return { success: false, error: "로그인이 필요합니다." } |
| 4 | if (session.user.role !== "ARTIST" && session.user.role !== "ADMIN") |
| 5 | return { success: false, error: "공연 등록 권한이 없습니다." } |
| 6 | |
| 7 | const parsed = eventSchema.safeParse(input) |
| 8 | if (!parsed.success) return { success: false, error: parsed.error.errors[0].message } |
| 9 | |
| 10 | const slug = await generateUniqueSlug(parsed.data.title, async (s) => { |
| 11 | const exists = await db.event.findUnique({ where: { slug: s } }) |
| 12 | return !!exists |
| 13 | }) |
| 14 | |
| 15 | const event = await db.event.create({ |
| 16 | data: { |
| 17 | ...parsed.data, |
| 18 | slug, |
| 19 | status: "PENDING", |
| 20 | ownerId: session.user.id, |
| 21 | }, |
| 22 | include: { ticketTypes: true }, |
| 23 | }) |
| 24 | |
| 25 | revalidatePath("/events") |
| 26 | return { success: true, slug: event.slug, eventId: event.id } |
| 27 | } |
Next.js Server Action: Zod 검증과 역할 기반 접근 제어(ARTIST/ADMIN)를 서버 레이어에서 처리하고, 중복 없는 slug를 생성한 뒤 Prisma로 공연 데이터를 생성합니다.
| 1 | export async function createReservation(ticketTypeId: string, quantity: number) { |
| 2 | const session = await auth() |
| 3 | if (!session?.user) return { success: false, error: "로그인이 필요합니다." } |
| 4 | |
| 5 | const ticketType = await db.ticketType.findUnique({ |
| 6 | where: { id: ticketTypeId }, |
| 7 | include: { event: { select: { id: true, slug: true, status: true } } }, |
| 8 | }) |
| 9 | |
| 10 | if (ticketType?.event.status !== "PUBLISHED") |
| 11 | return { success: false, error: "예매가 불가능한 공연입니다." } |
| 12 | if (ticketType.remaining < quantity) |
| 13 | return { success: false, error: "잔여 수량이 부족합니다." } |
| 14 | |
| 15 | const ticket = await db.$transaction(async (tx) => { |
| 16 | const created = await tx.ticket.create({ |
| 17 | data: { |
| 18 | ticketTypeId, |
| 19 | eventId: ticketType.eventId, |
| 20 | userId: session.user!.id, |
| 21 | quantity, |
| 22 | totalAmount: Number(ticketType.price) * quantity, |
| 23 | status: "PENDING", |
| 24 | }, |
| 25 | }) |
| 26 | await tx.ticketType.update({ |
| 27 | where: { id: ticketTypeId }, |
| 28 | data: { remaining: { decrement: quantity } }, |
| 29 | }) |
| 30 | return created |
| 31 | }) |
| 32 | |
| 33 | revalidatePath("/events/" + ticketType.event.slug) |
| 34 | return { success: true, ticketId: ticket.id } |
| 35 | } |
Prisma $transaction으로 티켓 생성과 잔여 수량 감소를 원자적으로 처리합니다. 상태 검증, 재고 확인, 사용자 구매 한도를 순서대로 통과해야 예매가 완료됩니다.
Result & Learnings
Links