Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 주제, 선택지 조회 기능 구현 #36

Merged
merged 23 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
009f49d
feat: RoomQuestion에 생성 일자 추가 #28
leegwichan Jul 19, 2024
5d970fb
feat: RoomQuestion, BalanceOption의 Repository 구현 #28
leegwichan Jul 19, 2024
c4ae03e
feat: 방 질문 조회 서버스 계층 구현 #28
leegwichan Jul 19, 2024
c1867d3
chore: 컨트롤러 테스트를 위해 Rest-Assured 의존성 추가 #28
leegwichan Jul 19, 2024
81b9d14
feat: 방 질문 조회 API 구현 #28
leegwichan Jul 19, 2024
62a14a9
build: rest-assured을 최신 버전(5.5.0)으로 변경 #28
leegwichan Jul 20, 2024
0a4501b
test: 테스트 케이스 일부 변경 및 추가 #28
leegwichan Jul 20, 2024
da4ed13
test: 일부 어노테이션의 기본값 명시한 것 제거, ServiceTest에서 Mock 환경 사용 #28
leegwichan Jul 20, 2024
f57c30c
style: test 파일 개행 및 들여쓰기 맞춤 #28
leegwichan Jul 20, 2024
8566753
refactor: 용도를 나타내기 위해 AuditingEntity 에서 BaseEntity로 파일 이름 변경 #28
leegwichan Jul 20, 2024
935ff1e
refactor: Repository 를 domain 패키지로 이동 및 필요없는 에노테이션 제거 #28
leegwichan Jul 20, 2024
458cd54
refactor: 지연 로딩의 여파를 막기 위해, 엔티티의 @EqualsAndHashCode, @ToString 제거 #28
leegwichan Jul 20, 2024
de80d87
fix: 서버 문제 트래킹을 명확히 하기 위해, ViolateDataException을 error 수준으로 로깅함 #28
leegwichan Jul 20, 2024
31e3543
refactor: 예외를 아키텍쳐 단위의 패키지로 이동 #28
leegwichan Jul 20, 2024
06b3dba
refactor: 구분을 쉽게 하기 위해 일부 테이블 및 컬럼 이름 변경 #28
leegwichan Jul 20, 2024
9e061d6
fix: RoomContentRepository를 room 패키지로 변경 #28
leegwichan Jul 22, 2024
246538a
fix: 서버 에러 시, 에러 스택 트레이스까지 출력하도록 변경, 일부 `@ResponseStatus` 추가 #28
leegwichan Jul 22, 2024
b042859
refactor: balance 패키지 추가 #28
leegwichan Jul 22, 2024
3d5b963
fix: API 명세서를 반영하여 Response 수정 #28
leegwichan Jul 22, 2024
112e91b
feat: BalanceContentController roomId 유효성 검사 추가
leegwichan Jul 22, 2024
39691e1
test: 테스트를 명확하게 설명하기 위해, Nested class 이름 수정
leegwichan Jul 22, 2024
b7ba2f4
test: given, when, then 주석 추가 #28
leegwichan Jul 22, 2024
2cc2472
test: 테스트 클래스 변경 #28
leegwichan Jul 22, 2024
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
1 change: 1 addition & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ddangkong.controller.exception;

public class BusinessLogicException extends RuntimeException {

public BusinessLogicException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
@Slf4j
public class GlobalExceptionHandler {

private static final String SERVER_ERROR_MESSAGE = "서버 오류가 발생했습니다. 관리자에게 문의하세요.";

@ExceptionHandler
public ErrorResponse handleBindingException(BindException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의견) 여기 ResponseStatus가 안되어 있는 것 같네요! 확인 부탁드려요!

log.warn(e.getMessage());
Expand All @@ -26,11 +28,27 @@ public ErrorResponse handleConstraintViolationException(ConstraintViolationExcep
return new ErrorResponse(e.getConstraintViolations());
}

@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleBusinessLogicException(BusinessLogicException e) {
log.warn(e.getMessage());

return new ErrorResponse(e.getMessage());
}

@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleViolateDataException(ViolateDataException e) {
log.warn(e.getMessage());

return new ErrorResponse(SERVER_ERROR_MESSAGE);
}

@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(Exception e) {
log.error(e.getMessage());

return new ErrorResponse("서버 오류가 발생했습니다. 관리자에게 문의하세요.");
return new ErrorResponse(SERVER_ERROR_MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ddangkong.controller.exception;

public class ViolateDataException extends RuntimeException {

public ViolateDataException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ddangkong.controller.option.dto;

import ddangkong.domain.option.BalanceOption;

public record BalanceOptionResponse(
Long optionId,
String content
) {
public static BalanceOptionResponse from(BalanceOption balanceOption) {
return new BalanceOptionResponse(balanceOption.getId(), balanceOption.getContent());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ddangkong.controller.question;

import ddangkong.controller.question.dto.BalanceQuestionResponse;
import ddangkong.service.question.BalanceQuestionService;
import lombok.RequiredArgsConstructor;
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")
@RequiredArgsConstructor
public class BalanceQuestionController {

private final BalanceQuestionService balanceQuestionService;

@GetMapping("/balances/rooms/{roomId}/question")
public BalanceQuestionResponse getBalanceQuestion(@PathVariable Long roomId) {
return balanceQuestionService.findRecentBalanceQuestion(roomId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ddangkong.controller.question.dto;

import ddangkong.controller.option.dto.BalanceOptionResponse;
import ddangkong.domain.option.BalanceOption;
import ddangkong.domain.question.BalanceQuestion;
import ddangkong.domain.question.Category;
import lombok.Builder;

public record BalanceQuestionResponse(
Long questionId,
Category category,
String title,
BalanceOptionResponse firstOption,
BalanceOptionResponse secondOption
) {
@Builder
private BalanceQuestionResponse(BalanceQuestion question, BalanceOption firstOption, BalanceOption secondOption) {
this(question.getId(),
question.getCategory(),
question.getContent(),
BalanceOptionResponse.from(firstOption),
BalanceOptionResponse.from(secondOption));
}
}
19 changes: 19 additions & 0 deletions backend/src/main/java/ddangkong/domain/AuditingEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ddangkong.domain;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class AuditingEntity {

@CreatedDate
@Column(updatable = false, nullable = false)
private LocalDateTime createdAt;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ddangkong.domain.room;

import ddangkong.domain.AuditingEntity;
import ddangkong.domain.question.BalanceQuestion;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -19,7 +20,7 @@
@Getter
@EqualsAndHashCode
@ToString
public class RoomQuestion {
public class RoomQuestion extends AuditingEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ddangkong.repository.option;

import ddangkong.domain.option.BalanceOption;
import ddangkong.domain.question.BalanceQuestion;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BalanceOptionRepository extends JpaRepository<BalanceOption, Long> {

List<BalanceOption> findByBalanceQuestion(BalanceQuestion balanceQuestion);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ddangkong.repository.room;

import ddangkong.domain.room.RoomQuestion;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface RoomQuestionRepository extends JpaRepository<RoomQuestion, Long> {

Optional<RoomQuestion> findTopByRoomIdOrderByCreatedAtDesc(Long roomId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ddangkong.service.question;

import ddangkong.controller.exception.BusinessLogicException;
import ddangkong.controller.exception.ViolateDataException;
import ddangkong.controller.question.dto.BalanceQuestionResponse;
import ddangkong.domain.option.BalanceOption;
import ddangkong.domain.question.BalanceQuestion;
import ddangkong.repository.option.BalanceOptionRepository;
import ddangkong.repository.room.RoomQuestionRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class BalanceQuestionService {

private static final int BALANCE_OPTION_SIZE = 2;

private final RoomQuestionRepository roomQuestionRepository;

private final BalanceOptionRepository balanceOptionRepository;

@Transactional(readOnly = true)
public BalanceQuestionResponse findRecentBalanceQuestion(Long roomId) {
BalanceQuestion balanceQuestion = findRecentQuestion(roomId);
List<BalanceOption> balanceOptions = findBalanceOptions(balanceQuestion);

return BalanceQuestionResponse.builder()
.question(balanceQuestion)
.firstOption(balanceOptions.get(0))
.secondOption(balanceOptions.get(1))
.build();
}

private BalanceQuestion findRecentQuestion(Long roomId) {
return roomQuestionRepository.findTopByRoomIdOrderByCreatedAtDesc(roomId)
.orElseThrow(() -> new BusinessLogicException("해당 방의 질문이 존재하지 않습니다."))
.getBalanceQuestion();
}

private List<BalanceOption> findBalanceOptions(BalanceQuestion balanceQuestion) {
List<BalanceOption> balanceOptions = balanceOptionRepository.findByBalanceQuestion(balanceQuestion);
validateBalanceOptions(balanceOptions);
return balanceOptions;
}

private void validateBalanceOptions(List<BalanceOption> balanceOptions) {
if (balanceOptions.size() != BALANCE_OPTION_SIZE) {
throw new ViolateDataException("밸런스 게임의 선택지가 %d개입니다".formatted(balanceOptions.size()));
}
}
}
21 changes: 21 additions & 0 deletions backend/src/test/java/ddangkong/controller/BaseControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ddangkong.controller;


import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.jdbc.Sql;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Sql(scripts = "/init-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public abstract class BaseControllerTest {

@LocalServerPort
private int port;

@BeforeEach
void setUp() {
RestAssured.port = port;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ddangkong.controller.question;

import static org.assertj.core.api.Assertions.assertThat;

import ddangkong.controller.BaseControllerTest;
import ddangkong.controller.option.dto.BalanceOptionResponse;
import ddangkong.controller.question.dto.BalanceQuestionResponse;
import ddangkong.domain.question.Category;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

class BalanceQuestionControllerTest extends BaseControllerTest {

private static final BalanceQuestionResponse EXPECTED_RESPONSE = new BalanceQuestionResponse(
1L, Category.EXAMPLE, "똥 맛 카레 vs 카레 맛 똥",
new BalanceOptionResponse(1L, "똥 맛 카레"),
new BalanceOptionResponse(2L, "카레 맛 똥"));

@Nested
class 방의_질문_조회 {

@Test
void 현재_방의_질문을_조회할_수_있다() {
BalanceQuestionResponse actual = RestAssured.given().log().all()
.when().get("/api/balances/rooms/1/question")
.then().log().all()
.statusCode(200)
.extract().as(BalanceQuestionResponse.class);

assertThat(actual).isEqualTo(EXPECTED_RESPONSE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ddangkong.repository;

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;

@DataJpaTest
@Sql(scripts = "/init-test.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public abstract class BaseRepositoryTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ddangkong.repository.room;

import static org.assertj.core.api.Assertions.assertThat;

import ddangkong.domain.room.RoomQuestion;
import ddangkong.repository.BaseRepositoryTest;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

class RoomQuestionRepositoryTest extends BaseRepositoryTest {

@Autowired
private RoomQuestionRepository roomQuestionRepository;

@Nested
class 방의_최신_질문_조회 {

@Test
void 방의_가장_최신의_질문을_조회할_수_있다() {
RoomQuestion actual = roomQuestionRepository.findTopByRoomIdOrderByCreatedAtDesc(1L).get();

assertThat(actual.getId()).isEqualTo(1L);
}
}
}
11 changes: 11 additions & 0 deletions backend/src/test/java/ddangkong/service/BaseServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ddangkong.service;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Sql(scripts = "/init-test.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
public abstract class BaseServiceTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package ddangkong.service.question;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import ddangkong.controller.exception.BusinessLogicException;
import ddangkong.controller.option.dto.BalanceOptionResponse;
import ddangkong.controller.question.dto.BalanceQuestionResponse;
import ddangkong.domain.question.Category;
import ddangkong.service.BaseServiceTest;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

class BalanceQuestionServiceTest extends BaseServiceTest {

private static final Long PROGRESS_ROOM_ID = 1L;
private static final Long NOT_EXIST_ROOM_ID = 2L;
private static final BalanceQuestionResponse BALANCE_QUESTION_RESPONSE = new BalanceQuestionResponse(
1L, Category.EXAMPLE, "똥 맛 카레 vs 카레 맛 똥",
new BalanceOptionResponse(1L, "똥 맛 카레"),
new BalanceOptionResponse(2L, "카레 맛 똥"));

@Autowired
private BalanceQuestionService balanceQuestionService;

@Nested
class 방의_최신_질문_조회 {

@Test
void 방의_최신_질문을_조회할_수_있다() {
BalanceQuestionResponse actual = balanceQuestionService.findRecentBalanceQuestion(PROGRESS_ROOM_ID);

assertThat(actual).isEqualTo(BALANCE_QUESTION_RESPONSE);
}

@Test
void 방이_없을_경우_예외를_던진다() {
assertThatThrownBy(() -> balanceQuestionService.findRecentBalanceQuestion(NOT_EXIST_ROOM_ID))
.isInstanceOf(BusinessLogicException.class)
.hasMessage("해당 방의 질문이 존재하지 않습니다.");
}
}
}
Loading