Skip to content

Commit

Permalink
[BE] feat: 축제 상세 정보 조회 Api 구현(#119) (#169)
Browse files Browse the repository at this point in the history
* feat: 축제 상세 정보 조회 응답 dto 정의

* feat: 축제 상세 조회 기능 구현

* feat: 축제 상세 조회 Api 구현

* test: 축제 상세 조회 기능 통합 테스트

* feat: 축제 상세 정보 조회시 무대 시작 시간 기반으로 무대 정보 정렬

* style: 변수명 타입 통일화

* refactor: 무대 상세 조회 fetch join 적용
  • Loading branch information
BGuga authored and seokjin8678 committed Aug 2, 2023
1 parent cd783f1 commit 3f08752
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package com.festago.application;

import static java.util.Comparator.comparing;

import com.festago.domain.Festival;
import com.festago.domain.FestivalRepository;
import com.festago.domain.Stage;
import com.festago.domain.StageRepository;
import com.festago.dto.FestivalCreateRequest;
import com.festago.dto.FestivalDetailResponse;
import com.festago.dto.FestivalResponse;
import com.festago.dto.FestivalsResponse;
import com.festago.exception.ErrorCode;
import com.festago.exception.NotFoundException;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -14,9 +21,11 @@
public class FestivalService {

private final FestivalRepository festivalRepository;
private final StageRepository stageRepository;

public FestivalService(FestivalRepository festivalRepository) {
public FestivalService(FestivalRepository festivalRepository, StageRepository stageRepository) {
this.festivalRepository = festivalRepository;
this.stageRepository = stageRepository;
}

public FestivalResponse create(FestivalCreateRequest request) {
Expand All @@ -29,4 +38,14 @@ public FestivalsResponse findAll() {
List<Festival> festivals = festivalRepository.findAll();
return FestivalsResponse.from(festivals);
}

@Transactional(readOnly = true)
public FestivalDetailResponse findDetail(Long festivalId) {
Festival festival = festivalRepository.findById(festivalId)
.orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND));
List<Stage> stages = stageRepository.findAllDetailByFestivalId(festivalId).stream()
.sorted(comparing(Stage::getStartTime))
.toList();
return FestivalDetailResponse.of(festival, stages);
}
}
5 changes: 1 addition & 4 deletions backend/src/main/java/com/festago/domain/Festival.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ public Festival(String name, LocalDate startDate, LocalDate endDate) {
}

public Festival(String name, LocalDate startDate, LocalDate endDate, String thumbnail) {
this.name = name;
this.startDate = startDate;
this.endDate = endDate;
this.thumbnail = thumbnail;
this(null, name, startDate, endDate, thumbnail);
}

public Festival(Long id, String name, LocalDate startDate, LocalDate endDate, String thumbnail) {
Expand Down
10 changes: 10 additions & 0 deletions backend/src/main/java/com/festago/domain/Stage.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Stage {
Expand All @@ -28,6 +31,9 @@ public class Stage {
@ManyToOne(fetch = FetchType.LAZY)
private Festival festival;

@OneToMany(mappedBy = "stage", fetch = FetchType.LAZY)
private List<Ticket> tickets = new ArrayList<>();

protected Stage() {
}

Expand Down Expand Up @@ -73,4 +79,8 @@ public LocalDateTime getTicketOpenTime() {
public Festival getFestival() {
return festival;
}

public List<Ticket> getTickets() {
return tickets;
}
}
12 changes: 12 additions & 0 deletions backend/src/main/java/com/festago/domain/StageRepository.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
package com.festago.domain;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface StageRepository extends JpaRepository<Stage, Long> {

List<Stage> findAllByFestival(Festival festival);

@Query("""
SELECT s FROM Stage s
JOIN FETCH s.tickets t
JOIN FETCH t.ticketAmount
WHERE s.festival.id = :festivalId
""")
List<Stage> findAllDetailByFestivalId(@Param("festivalId") Long festivalId);
}
4 changes: 4 additions & 0 deletions backend/src/main/java/com/festago/domain/TicketAmount.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public void addTotalAmount(int amount) {
totalAmount += amount;
}

public int calculateRemainAmount() {
return totalAmount - reservedAmount;
}

public Long getId() {
return id;
}
Expand Down
28 changes: 28 additions & 0 deletions backend/src/main/java/com/festago/dto/FestivalDetailResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.festago.dto;

import com.festago.domain.Festival;
import com.festago.domain.Stage;
import java.time.LocalDate;
import java.util.List;

public record FestivalDetailResponse(Long id,
String name,
LocalDate startDate,
LocalDate endDate,
String thumbnail,
List<FestivalDetailStageResponse> stages) {

public static FestivalDetailResponse of(Festival festival, List<Stage> stages) {
List<FestivalDetailStageResponse> stageResponses = stages.stream()
.map(FestivalDetailStageResponse::from)
.toList();
return new FestivalDetailResponse(
festival.getId(),
festival.getName(),
festival.getStartDate(),
festival.getEndDate(),
festival.getThumbnail(),
stageResponses
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.festago.dto;

import com.festago.domain.Stage;
import java.time.LocalDateTime;
import java.util.List;

public record FestivalDetailStageResponse(Long id,
LocalDateTime startDate,
LocalDateTime ticketOpenTime,
String lineUp,
List<FestivalDetailTicketResponse> tickets) {

public static FestivalDetailStageResponse from(Stage stage) {
List<FestivalDetailTicketResponse> tickets = stage.getTickets().stream()
.map(FestivalDetailTicketResponse::from)
.toList();
return new FestivalDetailStageResponse(
stage.getId(),
stage.getStartTime(),
stage.getTicketOpenTime(),
stage.getLineUp(),
tickets
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.festago.dto;

import com.festago.domain.Ticket;
import com.festago.domain.TicketAmount;
import com.festago.domain.TicketType;

public record FestivalDetailTicketResponse(Long id,
TicketType ticketType,
Integer totalAmount,
Integer remainAmount) {

public static FestivalDetailTicketResponse from(Ticket ticket) {
TicketAmount ticketAmount = ticket.getTicketAmount();
return new FestivalDetailTicketResponse(
ticket.getId(),
ticket.getTicketType(),
ticketAmount.getTotalAmount(),
ticketAmount.calculateRemainAmount()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.festago.presentation;

import com.festago.application.FestivalService;
import com.festago.dto.FestivalDetailResponse;
import com.festago.dto.FestivalsResponse;
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;

Expand All @@ -23,4 +25,11 @@ public ResponseEntity<FestivalsResponse> findAll() {
return ResponseEntity.ok()
.body(response);
}

@GetMapping("/{festivalId}")
public ResponseEntity<FestivalDetailResponse> findDetail(@PathVariable Long festivalId) {
FestivalDetailResponse response = festivalService.findDetail(festivalId);
return ResponseEntity.ok()
.body(response);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package com.festago.application;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;

import com.festago.domain.Festival;
import com.festago.domain.FestivalRepository;
import com.festago.domain.Stage;
import com.festago.domain.StageRepository;
import com.festago.dto.FestivalDetailResponse;
import com.festago.dto.FestivalDetailStageResponse;
import com.festago.dto.FestivalResponse;
import com.festago.dto.FestivalsResponse;
import com.festago.exception.NotFoundException;
import com.festago.support.FestivalFixture;
import com.festago.support.StageFixture;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
Expand All @@ -25,6 +35,9 @@ class FestivalServiceTest {
@Mock
FestivalRepository festivalRepository;

@Mock
StageRepository stageRepository;

@InjectMocks
FestivalService festivalService;

Expand All @@ -33,17 +46,49 @@ class FestivalServiceTest {
// given
Festival festival1 = FestivalFixture.festival().id(1L).build();
Festival festival2 = FestivalFixture.festival().id(2L).build();
given(festivalRepository.findAll())
.willReturn(List.of(festival1, festival2));
given(festivalRepository.findAll()).willReturn(List.of(festival1, festival2));

// when
FestivalsResponse response = festivalService.findAll();

// then
List<Long> festivalIds = response.festivals().stream()
.map(FestivalResponse::id)
.toList();
List<Long> festivalIds = response.festivals().stream().map(FestivalResponse::id).toList();

assertThat(festivalIds).containsExactly(1L, 2L);
}

@Nested
class 축제_상세_조회 {

@Test
void 축제가_없다면_예외() {
// given
Long festivalId = 1L;
given(festivalRepository.findById(festivalId)).willReturn(Optional.empty());

// when & then
assertThatThrownBy(() -> festivalService.findDetail(festivalId)).isInstanceOf(NotFoundException.class)
.hasMessage("존재하지 않는 축제입니다.");
}

@Test
void 무대_시작시간순으로_정렬() {
// given
Long festivalId = 1L;
Festival festival = FestivalFixture.festival().id(festivalId).build();
LocalDateTime now = LocalDateTime.now();
Stage stage1 = StageFixture.stage().id(1L).startTime(now).festival(festival).build();
Stage stage2 = StageFixture.stage().id(2L).startTime(now.plusDays(1)).festival(festival).build();

given(festivalRepository.findById(festivalId)).willReturn(Optional.of(festival));
given(stageRepository.findAllDetailByFestivalId(festival.getId())).willReturn(List.of(stage2, stage1));

// when
FestivalDetailResponse response = festivalService.findDetail(festivalId);

// then
List<Long> stageIds = response.stages().stream().map(FestivalDetailStageResponse::id).toList();
assertThat(stageIds).containsExactly(1L, 2L);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,26 @@
import static org.assertj.core.api.Assertions.assertThat;

import com.festago.application.FestivalService;
import com.festago.domain.Festival;
import com.festago.domain.FestivalRepository;
import com.festago.domain.Stage;
import com.festago.domain.StageRepository;
import com.festago.domain.Ticket;
import com.festago.domain.TicketRepository;
import com.festago.domain.TicketType;
import com.festago.dto.FestivalCreateRequest;
import com.festago.dto.FestivalDetailResponse;
import com.festago.dto.FestivalDetailStageResponse;
import com.festago.dto.FestivalDetailTicketResponse;
import com.festago.dto.FestivalResponse;
import com.festago.support.FestivalFixture;
import com.festago.support.StageFixture;
import com.festago.support.TicketFixture;
import jakarta.persistence.EntityManager;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.Test;
Expand All @@ -18,6 +35,18 @@ class FestivalServiceIntegrationTest extends ApplicationIntegrationTest {
@Autowired
FestivalService festivalService;

@Autowired
FestivalRepository festivalRepository;

@Autowired
StageRepository stageRepository;

@Autowired
TicketRepository ticketRepository;

@Autowired
EntityManager entityManager;

@Test
void 축제를_생성한다() {
// given
Expand All @@ -30,4 +59,33 @@ class FestivalServiceIntegrationTest extends ApplicationIntegrationTest {
// then
assertThat(festivalResponse).isNotNull();
}

@Test
void 축제_상세_정보를_조회한다() {
// given
Festival festival = festivalRepository.save(FestivalFixture.festival().build());
Stage stage = stageRepository.save(StageFixture.stage().festival(festival).build());
Ticket ticket1 = ticketRepository.save(
TicketFixture.ticket().stage(stage).ticketType(TicketType.VISITOR).build());
ticket1.addTicketEntryTime(LocalDateTime.now().minusMinutes(10), 100);
Ticket ticket2 = ticketRepository.save(
TicketFixture.ticket().stage(stage).ticketType(TicketType.STUDENT).build());
ticket2.addTicketEntryTime(LocalDateTime.now().minusMinutes(10), 200);

entityManager.flush();
entityManager.clear();

// when
FestivalDetailResponse response = festivalService.findDetail(festival.getId());

// then
SoftAssertions.assertSoftly(softly -> {
List<FestivalDetailStageResponse> stages = response.stages();
softly.assertThat(response.id()).isEqualTo(festival.getId());
softly.assertThat(stages.stream().map(FestivalDetailStageResponse::id).toList())
.containsExactly(stage.getId());
softly.assertThat(stages.get(0).tickets().stream().map(FestivalDetailTicketResponse::id).toList())
.containsExactly(ticket1.getId(), ticket2.getId());
});
}
}
Loading

0 comments on commit 3f08752

Please sign in to comment.