Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.festivalapp.api;

import com.festivalapp.dto.PerformanceResponse;
import com.festivalapp.service.PerformanceService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/performances")
@RequiredArgsConstructor
public class PerformanceController {

private final PerformanceService performanceService;

@GetMapping
ResponseEntity<List<PerformanceResponse>> getPerformances() {
return ResponseEntity.ok(performanceService.getPerformances());
}

@GetMapping("/{performanceId}")
ResponseEntity<PerformanceResponse> getPerformance(@PathVariable Long performanceId) {
return ResponseEntity.ok(performanceService.getPerformance(performanceId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.festivalapp.api;

import com.festivalapp.dto.TicketReservationCreateRequest;
import com.festivalapp.dto.TicketReservationResponse;
import com.festivalapp.security.AuthenticatedUser;
import com.festivalapp.service.TicketReservationService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
@RequestMapping("/api/ticket-reservations")
@RequiredArgsConstructor
public class TicketReservationController {

private final TicketReservationService ticketReservationService;

@PostMapping
ResponseEntity<TicketReservationResponse> reserveTicket(
Authentication authentication,
@RequestBody TicketReservationCreateRequest request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(ticketReservationService.reserveTicket(authenticatedUserId(authentication), request));
}

@GetMapping("/me")
ResponseEntity<List<TicketReservationResponse>> getMyReservations(Authentication authentication) {
return ResponseEntity.ok(
ticketReservationService.getMyReservations(authenticatedUserId(authentication)));
}

@GetMapping("/{reservationId}")
ResponseEntity<TicketReservationResponse> getMyReservation(
Authentication authentication,
@PathVariable String reservationId) {
return ResponseEntity.ok(
ticketReservationService.getMyReservation(authenticatedUserId(authentication), reservationId));
}

private String authenticatedUserId(Authentication authentication) {
if (authentication == null
|| !(authentication.getPrincipal() instanceof AuthenticatedUser user)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
}

return user.id();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
"/api/booths/**",
"/api/timeline",
"/api/timeline/**",
"/api/performances",
"/api/performances/**",
"/api/map-locations",
"/api/map-locations/**")
.permitAll()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.festivalapp.domain.performance;

import java.time.LocalDateTime;

public record Performance(
Long id,
String title,
String artist,
LocalDateTime startsAt,
LocalDateTime endsAt,
String location,
String description,
int totalSeats) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.festivalapp.domain.ticket;

import java.time.LocalDateTime;

public class TicketReservation {

private final String id;
private final Long performanceId;
private final String userId;
private TicketReservationStatus status;
private String qrCode;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;

public TicketReservation(
String id,
Long performanceId,
String userId,
TicketReservationStatus status,
String qrCode,
LocalDateTime createdAt,
LocalDateTime updatedAt) {
this.id = id;
this.performanceId = performanceId;
this.userId = userId;
this.status = status;
this.qrCode = qrCode;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}

public void markReservationCreated(LocalDateTime now) {
status = TicketReservationStatus.RESERVATION_CREATED;
updatedAt = now;
}

public void issueQr(String issuedQrCode, LocalDateTime now) {
status = TicketReservationStatus.QR_ISSUED;
qrCode = issuedQrCode;
updatedAt = now;
}

public void complete(LocalDateTime now) {
status = TicketReservationStatus.COMPLETED;
updatedAt = now;
}

public boolean isSeatOccupying() {
return status != TicketReservationStatus.CANCELLED;
}

public String id() {
return id;
}

public Long performanceId() {
return performanceId;
}

public String userId() {
return userId;
}

public TicketReservationStatus status() {
return status;
}

public String qrCode() {
return qrCode;
}

public LocalDateTime createdAt() {
return createdAt;
}

public LocalDateTime updatedAt() {
return updatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.festivalapp.domain.ticket;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum TicketReservationStatus {
SEAT_HELD("좌석 선점"),
RESERVATION_CREATED("예매 생성"),
QR_ISSUED("QR 티켓 발급"),
COMPLETED("예매 완료"),
CANCELLED("예매 취소");

private final String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.festivalapp.dto;

import com.festivalapp.domain.performance.Performance;
import java.time.LocalDateTime;

public record PerformanceResponse(
Long id,
String title,
String artist,
LocalDateTime startsAt,
LocalDateTime endsAt,
String location,
String description,
int totalSeats,
int remainingSeats) {

public static PerformanceResponse from(Performance performance, int remainingSeats) {
return new PerformanceResponse(
performance.id(),
performance.title(),
performance.artist(),
performance.startsAt(),
performance.endsAt(),
performance.location(),
performance.description(),
performance.totalSeats(),
remainingSeats);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.festivalapp.dto;

public record TicketReservationCreateRequest(Long performanceId) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.festivalapp.dto;

import com.festivalapp.domain.ticket.TicketReservation;
import java.time.LocalDateTime;
import java.util.List;

public record TicketReservationResponse(
String id,
Long performanceId,
String userId,
String status,
String statusDescription,
String qrCode,
List<TicketReservationSagaLogResponse> sagaLogs,
LocalDateTime createdAt,
LocalDateTime updatedAt) {

public static TicketReservationResponse from(
TicketReservation reservation,
List<TicketReservationSagaLogResponse> sagaLogs) {
return new TicketReservationResponse(
reservation.id(),
reservation.performanceId(),
reservation.userId(),
reservation.status().name(),
reservation.status().getDescription(),
reservation.qrCode(),
sagaLogs,
reservation.createdAt(),
reservation.updatedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.festivalapp.dto;

import java.time.LocalDateTime;

public record TicketReservationSagaLogResponse(
String step,
String message,
LocalDateTime createdAt) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.festivalapp.repository.performance;

import com.festivalapp.domain.performance.Performance;
import com.festivalapp.repository.performance.datasource.PerformanceDataSource;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class PerformanceRepository {

private final PerformanceDataSource performanceDataSource;

public List<Performance> findAll() {
return performanceDataSource.findAll().stream()
.sorted(Comparator.comparing(Performance::startsAt))
.toList();
}

public Optional<Performance> findById(Long performanceId) {
return performanceDataSource.findAll().stream()
.filter(performance -> performance.id().equals(performanceId))
.findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.festivalapp.repository.performance.datasource;

import com.festivalapp.domain.performance.Performance;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class InMemoryPerformanceDataSource implements PerformanceDataSource {

private final List<Performance> performances =
List.of(
performance(
1L,
"와우 스테이지 헤드라이너",
"헤드라이너 아티스트",
2026,
5,
13,
19,
0,
21,
0,
"대운동장 메인 스테이지",
"축제 첫날 밤을 여는 메인 스테이지 공연입니다.",
500),
performance(
2L,
"동아리 밴드 쇼케이스",
"학생 밴드 연합",
2026,
5,
14,
18,
0,
19,
30,
"학생회관 야외무대",
"교내 밴드 동아리들이 준비한 라이브 쇼케이스입니다.",
220),
performance(
3L,
"WOW DJ Festival",
"WOW DJ Crew",
2026,
5,
15,
20,
0,
23,
30,
"운동장 DJ 스테이지",
"축제 마지막 밤을 채우는 DJ 페스티벌 공연입니다.",
800));

@Override
public List<Performance> findAll() {
return performances;
}

private static Performance performance(
Long id,
String title,
String artist,
int year,
int month,
int day,
int startHour,
int startMinute,
int endHour,
int endMinute,
String location,
String description,
int totalSeats) {
return new Performance(
id,
title,
artist,
LocalDateTime.of(year, month, day, startHour, startMinute),
LocalDateTime.of(year, month, day, endHour, endMinute),
location,
description,
totalSeats);
}
}
Loading