binlog
Projects/BandStage

BandStage

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

Role풀스택 개발Year2025
Next.js 15React 19TypeScriptTailwind CSS v4PrismaSupabaseNextAuth.jsVercel
BandStage
01

Problem

공연 정보가 SNS, 포스터, 예매 플랫폼에 분산되어 있어 사용자가 원하는 공연을 한 번에 탐색할 수 없는 문제가 있습니다. 아티스트, 공연장, 팬이 각각 다른 채널에서 움직이며 공연 정보가 하나의 흐름으로 연결되지 않습니다.

02

Limitation

기존 공연 정보는 SNS, 포스터, 예매 플랫폼에 분산되어 있으며, 데이터가 구조화되지 않아 검색, 필터링, 예매까지 하나의 흐름으로 연결되지 않습니다. 사용자는 원하는 공연을 탐색하기 위해 여러 채널을 반복적으로 확인해야 하고, 아티스트 역시 공연 등록과 관리, 홍보를 통합적으로 수행할 수 없는 구조입니다.

03

Solution

분산된 공연 정보를 하나의 흐름으로 연결하기 위해, 공연 생태계를 "Region → Venue → Event → Reservation" 4계층 데이터 모델로 구조화했습니다. 사용자는 지역, 공연장, 공연 단위를 기준으로 탐색할 수 있고, 예매까지 하나의 흐름 안에서 이어지도록 설계했습니다. 또한 아티스트가 공연을 직접 등록하고 관리할 수 있도록 공연 등록 워크플로우를 구성하고, 이를 상태 머신(DRAFT → PENDING → APPROVED → PUBLISHED)으로 정의해 권한별 전환을 코드 레벨에서 통제했습니다. 사용자 역할(Fan, Artist, Venue Manager)에 따라 서로 다른 진입 경로를 가지지만, 탐색, 등록, 관리 기능이 모두 동일한 데이터 구조 위에서 동작하도록 설계해 시스템 전체 흐름을 일관되게 유지했습니다.

04

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 Confirmation
05

Key Implementation

서버 컴포넌트 우선 아키텍처로 클라이언트 번들을 최소화했습니다. 동적 라우팅(/events/[slug])에서 정적 생성과 ISR을 조합해 성능과 데이터 신선도를 동시에 확보했습니다. NextAuth.js v4로 FAN/ARTIST/VENUE_MANAGER/ADMIN 4역할 인증 시스템을 구현하고, 미들웨어 레벨에서 역할별 라우트를 보호했습니다. 실제 서울 공연장 25개 데이터를 구조화해 Supabase PostgreSQL에 시드했습니다.

06

Key Code

01createEvent — RBAC + Zodtypescript
1export 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로 공연 데이터를 생성합니다.

02createReservation — DB Transactiontypescript
1export 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으로 티켓 생성과 잔여 수량 감소를 원자적으로 처리합니다. 상태 검증, 재고 확인, 사용자 구매 한도를 순서대로 통과해야 예매가 완료됩니다.

07

Result & Learnings

권한 검사를 UI가 아닌 서버와 데이터 레이어에서 처리하도록 설계하면서, 역할에 따른 접근 제어를 일관된 기준으로 통제할 수 있는 구조를 만들었습니다. 상태 머신 도입 이후 잘못된 상태 전환이 차단되었고, 데이터 흐름이 명확해지면서 예외 상황과 디버깅 포인트가 크게 줄어들었습니다. 데이터 구조를 먼저 정의하고 그 위에 기능을 쌓는 방식이, UI 설계와 사용자 탐색 흐름까지 자연스럽게 결정된다는 것을 확인했습니다.

Links