Skip to content

Commit

Permalink
Merge pull request #38 from SWM-Flash/integration
Browse files Browse the repository at this point in the history
�문제 단건 조회 응답에 클라이밍장 이름 추가
  • Loading branch information
ChoiWonYu authored Aug 2, 2024
2 parents 0c3108a + f3069a2 commit c4471b7
Show file tree
Hide file tree
Showing 24 changed files with 366 additions and 84 deletions.
61 changes: 31 additions & 30 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,48 +1,49 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.first'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// db
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

// QueryDSL Implementation
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// generate UUID
implementation 'com.fasterxml.uuid:java-uuid-generator:5.1.0'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// db
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

// QueryDSL Implementation
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// generate UUID
implementation 'com.fasterxml.uuid:java-uuid-generator:5.1.0'

// db
runtimeOnly 'com.h2database:h2'
Expand All @@ -53,5 +54,5 @@ dependencies {
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.first.flash.climbing.gym.application;

import com.first.flash.climbing.gym.domian.ClimbingGymIdConfirmRequestedEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
public class ClimbingGymEventHandler {

private final ClimbingGymService gymService;

@EventListener
@Transactional
public void confirmGymId(final ClimbingGymIdConfirmRequestedEvent event) {
gymService.findClimbingGymById(event.getGymId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.first.flash.climbing.gym.domian.ClimbingGymRepository;
import com.first.flash.climbing.gym.domian.vo.Difficulty;
import com.first.flash.climbing.gym.exception.exceptions.ClimbingGymNotFoundException;
import com.first.flash.climbing.gym.exception.exceptions.NoSectorGymException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -48,11 +47,7 @@ public ClimbingGymDetailResponseDto findClimbingGymDetail(final Long id) {
}

private List<String> findSectorNamesById(final Long id) {
List<String> sectorNames = climbingGymRepository.findGymSectorNamesById(id);
if (sectorNames.isEmpty()) {
throw new NoSectorGymException(id);
}
return sectorNames;
return climbingGymRepository.findGymSectorNamesById(id);
}

private List<String> getDifficultyNames(final ClimbingGym climbingGym) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

import com.first.flash.climbing.gym.domian.ClimbingGym;
import com.first.flash.climbing.gym.domian.vo.Difficulty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public record ClimbingGymCreateRequestDto(String gymName, String thumbnailUrl, String mapImageUrl,
List<Difficulty> difficulties) {
public record ClimbingGymCreateRequestDto(
@NotEmpty(message = "클라이밍장 이름은 필수입니다.") String gymName,
@NotEmpty(message = "썸네일 URL은 필수입니다.") String thumbnailUrl,
@NotEmpty(message = "지도 이미지 URL은 필수입니다.") String mapImageUrl,
@NotEmpty(message = "난이도 정보는 최소 하나 이상이어야 합니다.")
List<@Valid @NotNull(message = "난이도 정보는 비어있을 수 없습니다.") Difficulty> difficulties) {

public ClimbingGym toEntity() {
return new ClimbingGym(gymName, thumbnailUrl, mapImageUrl, difficulties);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.first.flash.climbing.gym.domian;

import com.first.flash.global.event.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ClimbingGymIdConfirmRequestedEvent extends Event {

private final Long gymId;

public static ClimbingGymIdConfirmRequestedEvent of(final Long gymId) {
return new ClimbingGymIdConfirmRequestedEvent(gymId);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.first.flash.climbing.gym.domian.vo;

import jakarta.persistence.Embeddable;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -11,7 +13,9 @@
@Getter
public class Difficulty {

@NotEmpty(message = "난이도 이름은 필수입니다.")
private String name;
@NotNull(message = "난이도 레벨은 필수입니다.")
private Integer level;

public Difficulty(final String name, final Integer level) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

import com.first.flash.climbing.gym.exception.exceptions.ClimbingGymNotFoundException;
import com.first.flash.climbing.gym.exception.exceptions.DifficultyNotFoundException;
import com.first.flash.climbing.gym.exception.exceptions.DuplicateDifficultyLevelException;
import com.first.flash.climbing.gym.exception.exceptions.DuplicateDifficultyNameException;
import com.first.flash.climbing.gym.exception.exceptions.NoSectorGymException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

Expand All @@ -29,6 +35,32 @@ public ResponseEntity<String> handleNoSectorGymException(
return getResponseWithStatus(HttpStatus.NOT_FOUND, exception);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
final MethodArgumentNotValidException exception) {
Map<String, String> errors = new HashMap<>();

exception.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(errors);
}

@ExceptionHandler(DuplicateDifficultyLevelException.class)
public ResponseEntity<String> handleDuplicateDifficultyLevelException(
final DuplicateDifficultyLevelException exception) {
return getResponseWithStatus(HttpStatus.BAD_REQUEST, exception);
}

@ExceptionHandler(DuplicateDifficultyNameException.class)
public ResponseEntity<String> handleDuplicateDifficultyNameException(
final DuplicateDifficultyNameException exception) {
return getResponseWithStatus(HttpStatus.BAD_REQUEST, exception);
}

private ResponseEntity<String> getResponseWithStatus(final HttpStatus httpStatus,
final RuntimeException exception) {
return ResponseEntity.status(httpStatus)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.first.flash.climbing.gym.exception.exceptions;

public class DuplicateDifficultyLevelException extends RuntimeException {

public DuplicateDifficultyLevelException(final Integer difficultyLevel) {
super(String.format("난이도 레벨이 중복되었습니다: %d", difficultyLevel));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.first.flash.climbing.gym.exception.exceptions;

public class DuplicateDifficultyNameException extends RuntimeException {

public DuplicateDifficultyNameException(final String difficultyName) {
super(String.format("난이도 이름이 중복되었습니다: %s", difficultyName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
import com.first.flash.climbing.gym.application.ClimbingGymService;
import com.first.flash.climbing.gym.application.dto.ClimbingGymCreateRequestDto;
import com.first.flash.climbing.gym.application.dto.ClimbingGymCreateResponseDto;
import com.first.flash.climbing.gym.application.dto.ClimbingGymResponseDto;
import com.first.flash.climbing.gym.application.dto.ClimbingGymDetailResponseDto;
import com.first.flash.climbing.gym.application.dto.ClimbingGymResponseDto;
import com.first.flash.climbing.gym.domian.vo.Difficulty;
import com.first.flash.climbing.gym.exception.exceptions.DuplicateDifficultyLevelException;
import com.first.flash.climbing.gym.exception.exceptions.DuplicateDifficultyNameException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -30,7 +40,22 @@ public class ClimbingGymController {

@Operation(summary = "모든 클라이밍장 조회", description = "모든 클라이밍장을 리스트로 반환")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "클라이밍장 리스트 조회 성공함")
@ApiResponse(responseCode = "200", description = "클라이밍장 리스트 조회 성공함",
content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ClimbingGymResponseDto.class)),
examples = @ExampleObject(value = """
[
{
"id": 1,
"gymName": "더클라임 논현",
"thumbnailUrl": "example_map_image1.jpeg"
},
{
"id": 2,
"gymName": "더클라임 양재",
"thumbnailUrl": "example_map_image2.jpeg"
}
]
"""))),
})
@GetMapping
public List<ClimbingGymResponseDto> getGyms() {
Expand All @@ -40,20 +65,47 @@ public List<ClimbingGymResponseDto> getGyms() {

@Operation(summary = "클라이밍장 생성", description = "새로운 클라이밍장 생성")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "성공적으로 클라이밍장을 생성함"),
@ApiResponse(responseCode = "201", description = "성공적으로 클라이밍장을 생성함",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ClimbingGymCreateResponseDto.class))),
@ApiResponse(responseCode = "400", description = "유효하지 않은 요청 형식",
content = @Content(mediaType = "application/json", examples = {
@ExampleObject(name = "요청값 누락", value = "{\"gymName\": \"클라이밍장 이름은 필수입니다.\"}"),
@ExampleObject(name = "난이도 이름 중복", value = "{\"error\": \"난이도 이름이 중복되었습니다: 파랑\"}"),
@ExampleObject(name = "난이도 레벨 중복", value = "{\"error\": \"난이도 레벨이 중복되었습니다: 2\"}"),
})),
})
@PostMapping
public ResponseEntity<ClimbingGymCreateResponseDto> createGym(
@RequestBody final ClimbingGymCreateRequestDto gymCreateRequestDto) {
@Valid @RequestBody final ClimbingGymCreateRequestDto gymCreateRequestDto) {

validateDifficulties(gymCreateRequestDto.difficulties());

return ResponseEntity.status(HttpStatus.CREATED)
.body(climbingGymService.save(gymCreateRequestDto));
}

private void validateDifficulties(final List<Difficulty> difficulties) {
Set<String> names = new HashSet<>();
Set<Integer> levels = new HashSet<>();

for (Difficulty difficulty : difficulties) {
if (!names.add(difficulty.getName())) {
throw new DuplicateDifficultyNameException(difficulty.getName());
}
if (!levels.add(difficulty.getLevel())) {
throw new DuplicateDifficultyLevelException(difficulty.getLevel());
}
}
}

@Operation(summary = "클라이밍장 정보 조회", description = "특정 클라이밍장의 정보 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공적으로 클라이밍장 정보를 조회함"),
@ApiResponse(responseCode = "404", description = "클라이밍장을 찾을 수 없음"),
@ApiResponse(responseCode = "404", description = "클라이밍장에 섹터가 없음")
@ApiResponse(responseCode = "200", description = "성공적으로 클라이밍장 정보를 조회함",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ClimbingGymDetailResponseDto.class))),
@ApiResponse(responseCode = "404", description = "클라이밍장을 찾을 수 없음",
content = @Content(mediaType = "application/json", examples = {
@ExampleObject(name = "클라이밍장 없음", value = "{\"error\": \"아이디가 1인 클라이밍장을 찾을 수 없습니다.\"}")
}))
})
@GetMapping("/{gymId}")
public ResponseEntity<ClimbingGymDetailResponseDto> getGymDetails(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.first.flash.climbing.problem.application;

import com.first.flash.climbing.problem.domain.ProblemIdConfirmRequestedEvent;
import com.first.flash.climbing.sector.domain.SectorExpiredEvent;
import com.first.flash.climbing.sector.domain.SectorInfoUpdatedEvent;
import com.first.flash.climbing.sector.domain.SectorRemovalDateUpdatedEvent;
Expand All @@ -14,6 +15,7 @@
public class ProblemEventHandler {

private final ProblemsService problemsService;
private final ProblemReadService problemReadService;

@EventListener
@Transactional
Expand All @@ -39,4 +41,10 @@ public void updateQueryProblemInfo(final SectorInfoUpdatedEvent event) {
problemsService.updateQueryProblemInfo(event.getId(), event.getSectorName(),
event.getSettingDate());
}

@EventListener
@Transactional
public void confirmProblemId(final ProblemIdConfirmRequestedEvent event) {
problemReadService.findProblemById(event.getProblemId());
}
}
Loading

0 comments on commit c4471b7

Please sign in to comment.