From 5a547bfa3f228fdd8666b5f3640433cc86a48f78 Mon Sep 17 00:00:00 2001 From: kddhhh23 Date: Sun, 23 Nov 2025 16:04:53 +0900 Subject: [PATCH 1/3] =?UTF-8?q?practice/=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++ .../com/example/umc9th/Umc9thApplication.java | 2 + .../member/controller/MemberController.java | 28 +++++++++ .../member/converter/MemberConverter.java | 30 ++++++++++ .../domain/member/dto/MemberReqDTO.java | 17 ++++++ .../domain/member/dto/MemberResDTO.java | 13 ++++ .../umc9th/domain/member/entity/Member.java | 4 +- .../member/exception/FoodException.java | 11 ++++ .../member/exception/MemberException.java | 10 ++++ .../member/exception/code/FoodErrorCode.java | 16 +++++ .../exception/code/MemberErrorCode.java | 21 +++++++ .../exception/code/MemberSuccessCode.java | 20 +++++++ .../member/repository/FoodRepository.java | 7 +++ .../repository/MemberFoodRepository.java | 7 +++ .../member/service/MemberCommandService.java | 11 ++++ .../service/MemberCommandServiceImpl.java | 59 +++++++++++++++++++ .../review/controller/ReviewController.java | 10 ++-- .../umc9th/global/apiPayload/ApiResponse.java | 5 +- .../umc9th/global/config/SwaggerConfig.java | 40 +++++++++++++ 19 files changed, 307 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/example/umc9th/domain/member/controller/MemberController.java create mode 100644 src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java create mode 100644 src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java create mode 100644 src/main/java/com/example/umc9th/domain/member/dto/MemberResDTO.java create mode 100644 src/main/java/com/example/umc9th/domain/member/exception/FoodException.java create mode 100644 src/main/java/com/example/umc9th/domain/member/exception/MemberException.java create mode 100644 src/main/java/com/example/umc9th/domain/member/exception/code/FoodErrorCode.java create mode 100644 src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java create mode 100644 src/main/java/com/example/umc9th/domain/member/exception/code/MemberSuccessCode.java create mode 100644 src/main/java/com/example/umc9th/domain/member/repository/FoodRepository.java create mode 100644 src/main/java/com/example/umc9th/domain/member/repository/MemberFoodRepository.java create mode 100644 src/main/java/com/example/umc9th/domain/member/service/MemberCommandService.java create mode 100644 src/main/java/com/example/umc9th/domain/member/service/MemberCommandServiceImpl.java create mode 100644 src/main/java/com/example/umc9th/global/config/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 84c7bb5..4b7cd17 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,10 @@ dependencies { annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa" annotationProcessor "jakarta.persistence:jakarta.persistence-api" annotationProcessor "jakarta.annotation:jakarta.annotation-api" + + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13' } tasks.named('test') { diff --git a/src/main/java/com/example/umc9th/Umc9thApplication.java b/src/main/java/com/example/umc9th/Umc9thApplication.java index cf0063f..9c66a1c 100644 --- a/src/main/java/com/example/umc9th/Umc9thApplication.java +++ b/src/main/java/com/example/umc9th/Umc9thApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing // Baseentity 사용를 위한 어노테이션 public class Umc9thApplication { public static void main(String[] args) { diff --git a/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java b/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java new file mode 100644 index 0000000..0c78564 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java @@ -0,0 +1,28 @@ +package com.example.umc9th.domain.member.controller; + +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; +import com.example.umc9th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc9th.domain.member.service.MemberCommandService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import lombok.RequiredArgsConstructor; +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; + +@RequestMapping("/member") +@RestController +@RequiredArgsConstructor +public class MemberController { + + private final MemberCommandService memberCommandService; + + // 회원가입 + @PostMapping("/sign-up") + public ApiResponse signUp( + @RequestBody MemberReqDTO.JoinDTO dto + ){ + return ApiResponse.onSuccess(memberCommandService.signup(dto)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java b/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java new file mode 100644 index 0000000..aae4f40 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java @@ -0,0 +1,30 @@ +package com.example.umc9th.domain.member.converter; + +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; +import com.example.umc9th.domain.member.entity.Member; + +public class MemberConverter { + + // Entity -> DTO + public static MemberResDTO.JoinDTO toJoinDTO( + Member member + ){ + return MemberResDTO.JoinDTO.builder() + .memberId(member.getId()) + .createAt(member.getCreatedAt()) + .build(); + } + + // DTO -> Entity + public static Member toMember( + MemberReqDTO.JoinDTO dto + ){ + return Member.builder() + .name(dto.name()) + .birthday(dto.birth()) + .address(dto.address()) + .gender(dto.gender()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java b/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java new file mode 100644 index 0000000..101508d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java @@ -0,0 +1,17 @@ +package com.example.umc9th.domain.member.dto; + +import com.example.umc9th.domain.member.enums.Gender; +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; + +import java.time.LocalDate; +import java.util.List; + +public class MemberReqDTO { + public record JoinDTO( + String name, + Gender gender, + LocalDate birth, + String address, + List favoriteFood + ){} +} diff --git a/src/main/java/com/example/umc9th/domain/member/dto/MemberResDTO.java b/src/main/java/com/example/umc9th/domain/member/dto/MemberResDTO.java new file mode 100644 index 0000000..007591d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/dto/MemberResDTO.java @@ -0,0 +1,13 @@ +package com.example.umc9th.domain.member.dto; + +import lombok.Builder; + +import java.time.LocalDateTime; + +public class MemberResDTO { + @Builder + public record JoinDTO( + Long memberId, + LocalDateTime createAt + ){} +} diff --git a/src/main/java/com/example/umc9th/domain/member/entity/Member.java b/src/main/java/com/example/umc9th/domain/member/entity/Member.java index c729dfd..5e24830 100644 --- a/src/main/java/com/example/umc9th/domain/member/entity/Member.java +++ b/src/main/java/com/example/umc9th/domain/member/entity/Member.java @@ -47,10 +47,10 @@ public class Member extends BaseEntity { @Column(name = "inactive_date") private LocalDateTime inactiveDate; - @Column(name = "email", nullable = false) + @Column(name = "email", nullable = true) private String email; - @Column(name = "phonenumber", nullable = false) + @Column(name = "phonenumber", nullable = true) private String phonenumber; @Column(name = "point") diff --git a/src/main/java/com/example/umc9th/domain/member/exception/FoodException.java b/src/main/java/com/example/umc9th/domain/member/exception/FoodException.java new file mode 100644 index 0000000..d3cd6c4 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/FoodException.java @@ -0,0 +1,11 @@ +package com.example.umc9th.domain.member.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class FoodException extends GeneralException { + public FoodException(BaseErrorCode code) { + + super(code); + } +} diff --git a/src/main/java/com/example/umc9th/domain/member/exception/MemberException.java b/src/main/java/com/example/umc9th/domain/member/exception/MemberException.java new file mode 100644 index 0000000..1e4d7a1 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/MemberException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.member.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class MemberException extends GeneralException { + public MemberException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc9th/domain/member/exception/code/FoodErrorCode.java b/src/main/java/com/example/umc9th/domain/member/exception/code/FoodErrorCode.java new file mode 100644 index 0000000..3d37c49 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/code/FoodErrorCode.java @@ -0,0 +1,16 @@ +package com.example.umc9th.domain.member.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum FoodErrorCode implements BaseErrorCode { + FOOD_NOT_FOUND(HttpStatus.NOT_FOUND, "FOOD404", "해당하는 음식이 존재하지 않습니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java new file mode 100644 index 0000000..2fecd52 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java @@ -0,0 +1,21 @@ +package com.example.umc9th.domain.member.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MemberErrorCode implements BaseErrorCode { + + NOT_FOUND(HttpStatus.NOT_FOUND, + "MEMBER404_1", + "해당 사용자를 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} + diff --git a/src/main/java/com/example/umc9th/domain/member/exception/code/MemberSuccessCode.java b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberSuccessCode.java new file mode 100644 index 0000000..5b1e0e0 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberSuccessCode.java @@ -0,0 +1,20 @@ +package com.example.umc9th.domain.member.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum MemberSuccessCode implements BaseSuccessCode { + + FOUND(HttpStatus.OK, + "MEMBER200_1", + "성공적으로 사용자를 조회했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/member/repository/FoodRepository.java b/src/main/java/com/example/umc9th/domain/member/repository/FoodRepository.java new file mode 100644 index 0000000..8407493 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/repository/FoodRepository.java @@ -0,0 +1,7 @@ +package com.example.umc9th.domain.member.repository; + +import com.example.umc9th.domain.member.entity.Food; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FoodRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/umc9th/domain/member/repository/MemberFoodRepository.java b/src/main/java/com/example/umc9th/domain/member/repository/MemberFoodRepository.java new file mode 100644 index 0000000..0a6bebc --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/repository/MemberFoodRepository.java @@ -0,0 +1,7 @@ +package com.example.umc9th.domain.member.repository; + +import com.example.umc9th.domain.member.entity.mapping.MemberFood; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberFoodRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/umc9th/domain/member/service/MemberCommandService.java b/src/main/java/com/example/umc9th/domain/member/service/MemberCommandService.java new file mode 100644 index 0000000..cd80b23 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/service/MemberCommandService.java @@ -0,0 +1,11 @@ +package com.example.umc9th.domain.member.service; + +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; + +public interface MemberCommandService { + // 회원가입 + MemberResDTO.JoinDTO signup( + MemberReqDTO.JoinDTO dto + ); +} diff --git a/src/main/java/com/example/umc9th/domain/member/service/MemberCommandServiceImpl.java b/src/main/java/com/example/umc9th/domain/member/service/MemberCommandServiceImpl.java new file mode 100644 index 0000000..534e75a --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/service/MemberCommandServiceImpl.java @@ -0,0 +1,59 @@ +package com.example.umc9th.domain.member.service; + +import com.example.umc9th.domain.member.converter.MemberConverter; +import com.example.umc9th.domain.member.dto.MemberReqDTO; +import com.example.umc9th.domain.member.dto.MemberResDTO; +import com.example.umc9th.domain.member.entity.Food; +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.entity.mapping.MemberFood; +import com.example.umc9th.domain.member.exception.FoodException; +import com.example.umc9th.domain.member.exception.code.FoodErrorCode; +import com.example.umc9th.domain.member.repository.FoodRepository; +import com.example.umc9th.domain.member.repository.MemberFoodRepository; +import com.example.umc9th.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class MemberCommandServiceImpl implements MemberCommandService{ + + private final MemberRepository memberRepository; + private final MemberFoodRepository memberFoodRepository; + private final FoodRepository foodRepository; + + // 회원가입 + @Override + @Transactional + public MemberResDTO.JoinDTO signup( + MemberReqDTO.JoinDTO dto + ){ + // 사용자 생성 + Member member = MemberConverter.toMember(dto); + // DB 적용 + memberRepository.save(member); + + // 선호 음식 존재 여부 확인 + if (dto.favoriteFood().size() > 1){ + List memberFood = dto.favoriteFood().stream() + .map(id -> MemberFood.builder() + .member(member) + .food(foodRepository.findById(id) + .orElseThrow(() -> new FoodException(FoodErrorCode.FOOD_NOT_FOUND))) + .build() + ) + .collect(Collectors.toList()); + + memberFoodRepository.saveAll(memberFood); + } + + + // 응답 DTO 생성 + return MemberConverter.toJoinDTO(member); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java index d11517e..db5c599 100644 --- a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -8,18 +8,20 @@ import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; +@RequestMapping("/reviews") @RestController @RequiredArgsConstructor public class ReviewController { private final ReviewService reviewQueryService; // 워크북 예시 - @GetMapping("/reviews/search") + @GetMapping("/search") public ApiResponse> searchReview( @RequestParam String query, @RequestParam String type @@ -27,11 +29,11 @@ public ApiResponse> searchReview( List reviewList = reviewQueryService.searchReview(query, type); GeneralSuccessCode code = GeneralSuccessCode.OK; List dtoList = ReviewConverter.toReviewListDto(reviewList); - return ApiResponse.onSuccess(code, dtoList); + return ApiResponse.onSuccess(dtoList); } // 미션 - @GetMapping("/reviews/my-reviews") + @GetMapping("/my-reviews") public ApiResponse> getMyReviews( @RequestParam Long memberId, @RequestParam(required = false) String query, @@ -40,6 +42,6 @@ public ApiResponse> getMyReviews( List reviewList = reviewQueryService.getMyReviews(memberId, query, type); GeneralSuccessCode code = GeneralSuccessCode.OK; List dtoList = ReviewConverter.toReviewListDto(reviewList); - return ApiResponse.onSuccess(code, dtoList); + return ApiResponse.onSuccess(dtoList); } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java b/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java index f542779..88093f5 100644 --- a/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java +++ b/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java @@ -2,6 +2,7 @@ import com.example.umc9th.global.apiPayload.code.BaseErrorCode; import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.AllArgsConstructor; @@ -25,8 +26,8 @@ public class ApiResponse { private T result; // 성공한 경우 (result 포함) - public static ApiResponse onSuccess(BaseSuccessCode code, T result) { - return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); + public static ApiResponse onSuccess(T result){ + return new ApiResponse<>(true, GeneralSuccessCode.OK.getCode() , GeneralSuccessCode.OK.getMessage(), result); } // 실패한 경우 (result 포함) diff --git a/src/main/java/com/example/umc9th/global/config/SwaggerConfig.java b/src/main/java/com/example/umc9th/global/config/SwaggerConfig.java new file mode 100644 index 0000000..bf5dea4 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/SwaggerConfig.java @@ -0,0 +1,40 @@ +package com.example.umc9th.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI swagger() { + Info info = new Info() + .title("UMC 9th Project") + .description("UMC 9th Project API 명세서") + .version("0.0.1"); + + // JWT 토큰 설정 (로그인 기능 쓸 때 필요) + String securityScheme = "JWT TOKEN"; + SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityScheme); + + Components components = new Components() + .addSecuritySchemes(securityScheme, new SecurityScheme() + .name(securityScheme) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT")); + + return new OpenAPI() + .info(info) + .addServersItem(new Server().url("/")) // 기본 서버 URL 설정 + .addSecurityItem(securityRequirement) + .components(components); + } +} \ No newline at end of file From 7b0137bcb520ea8c9fafbe40e66cdc149e5e6531 Mon Sep 17 00:00:00 2001 From: kddhhh23 Date: Sun, 23 Nov 2025 18:24:32 +0900 Subject: [PATCH 2/3] =?UTF-8?q?mission/=20=EA=B0=80=EA=B2=8C=EC=97=90=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../member/controller/MemberController.java | 3 +- .../domain/member/dto/MemberReqDTO.java | 2 + .../review/controller/ReviewController.java | 19 ++++++-- .../review/converter/ReviewConverter.java | 19 ++++++++ .../domain/review/dto/ReviewReqDto.java | 15 +++++++ .../domain/review/dto/ReviewResDto.java | 11 +++++ .../umc9th/domain/review/entity/Review.java | 3 +- .../review/service/ReviewCreateService.java | 44 +++++++++++++++++++ .../domain/review/service/ReviewService.java | 2 +- .../test/controller/TestController.java | 3 +- .../umc9th/global/annotation/ExistFoods.java | 18 ++++++++ .../apiPayload/code/GeneralErrorCode.java | 1 - .../handler/GeneralExceptionAdvice.java | 22 ++++++++++ .../global/validator/FoodExistValidator.java | 33 ++++++++++++++ 15 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/example/umc9th/domain/review/dto/ReviewReqDto.java create mode 100644 src/main/java/com/example/umc9th/domain/review/service/ReviewCreateService.java create mode 100644 src/main/java/com/example/umc9th/global/annotation/ExistFoods.java create mode 100644 src/main/java/com/example/umc9th/global/validator/FoodExistValidator.java diff --git a/build.gradle b/build.gradle index 4b7cd17..a0fa333 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,9 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13' + + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java b/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java index 0c78564..5240f0e 100644 --- a/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java @@ -5,6 +5,7 @@ import com.example.umc9th.domain.member.exception.code.MemberSuccessCode; import com.example.umc9th.domain.member.service.MemberCommandService; import com.example.umc9th.global.apiPayload.ApiResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -21,7 +22,7 @@ public class MemberController { // 회원가입 @PostMapping("/sign-up") public ApiResponse signUp( - @RequestBody MemberReqDTO.JoinDTO dto + @RequestBody @Valid MemberReqDTO.JoinDTO dto ){ return ApiResponse.onSuccess(memberCommandService.signup(dto)); } diff --git a/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java b/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java index 101508d..6e8e064 100644 --- a/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java +++ b/src/main/java/com/example/umc9th/domain/member/dto/MemberReqDTO.java @@ -1,6 +1,7 @@ package com.example.umc9th.domain.member.dto; import com.example.umc9th.domain.member.enums.Gender; +import com.example.umc9th.global.annotation.ExistFoods; import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; import java.time.LocalDate; @@ -12,6 +13,7 @@ public record JoinDTO( Gender gender, LocalDate birth, String address, + @ExistFoods List favoriteFood ){} } diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java index db5c599..c4b1807 100644 --- a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -1,16 +1,16 @@ package com.example.umc9th.domain.review.controller; import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.ReviewReqDto; import com.example.umc9th.domain.review.dto.ReviewResDto; import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.service.ReviewCreateService; import com.example.umc9th.domain.review.service.ReviewService; import com.example.umc9th.global.apiPayload.ApiResponse; import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -19,6 +19,7 @@ @RequiredArgsConstructor public class ReviewController { private final ReviewService reviewQueryService; + private final ReviewCreateService reviewCreateService; // 워크북 예시 @GetMapping("/search") @@ -44,4 +45,14 @@ public ApiResponse> getMyReviews( List dtoList = ReviewConverter.toReviewListDto(reviewList); return ApiResponse.onSuccess(dtoList); } + + // 가게에 리뷰 추가 + @PostMapping("/{storeId}") + public ApiResponse createReview( + @PathVariable Long storeId, + @RequestBody @Valid ReviewReqDto.JoinDto request + ) { + Review review = reviewCreateService.createReview(storeId, request); + return ApiResponse.onSuccess(ReviewConverter.toCreateReviewResultDto(review)); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java index 4a6d2c2..3baa15d 100644 --- a/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java @@ -2,7 +2,9 @@ import com.example.umc9th.domain.review.dto.ReviewResDto; import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.dto.ReviewReqDto; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -22,4 +24,21 @@ public static List toReviewListDto(List reviewList) { .map(ReviewConverter::toReviewDto) .collect(Collectors.toList()); } + + + // 리뷰 생성 (DTO -> Entity) + public static Review toEntity(ReviewReqDto.JoinDto request) { + return Review.builder() + .content(request.getContent()) + .rating(request.getScore()) + .build(); + } + + // 리뷰 생성 응답 (Entity -> DTO) + public static ReviewResDto.CreateReviewResultDto toCreateReviewResultDto(Review review) { + return ReviewResDto.CreateReviewResultDto.builder() + .reviewId(review.getId()) + .createdAt(LocalDateTime.now()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/review/dto/ReviewReqDto.java b/src/main/java/com/example/umc9th/domain/review/dto/ReviewReqDto.java new file mode 100644 index 0000000..30a9a74 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/dto/ReviewReqDto.java @@ -0,0 +1,15 @@ +package com.example.umc9th.domain.review.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +public class ReviewReqDto { + @Getter + public static class JoinDto { + @NotBlank + private String content; + @NotNull + private Float score; + } +} diff --git a/src/main/java/com/example/umc9th/domain/review/dto/ReviewResDto.java b/src/main/java/com/example/umc9th/domain/review/dto/ReviewResDto.java index 7a2f6df..873bc16 100644 --- a/src/main/java/com/example/umc9th/domain/review/dto/ReviewResDto.java +++ b/src/main/java/com/example/umc9th/domain/review/dto/ReviewResDto.java @@ -5,6 +5,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Getter @Builder @NoArgsConstructor @@ -14,4 +16,13 @@ public class ReviewResDto { private String content; private Float rating; + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class CreateReviewResultDto { + private Long reviewId; + private LocalDateTime createdAt; + } + } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/review/entity/Review.java b/src/main/java/com/example/umc9th/domain/review/entity/Review.java index 26a13a8..c8278d7 100644 --- a/src/main/java/com/example/umc9th/domain/review/entity/Review.java +++ b/src/main/java/com/example/umc9th/domain/review/entity/Review.java @@ -14,6 +14,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter +@Setter @Table(name = "review") public class Review extends BaseEntity { @@ -26,7 +27,7 @@ public class Review extends BaseEntity { private String content; @Column(name = "rating", nullable = false) - private Integer rating; + private Float rating; // Member(1) : Review(N) 관계 @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/example/umc9th/domain/review/service/ReviewCreateService.java b/src/main/java/com/example/umc9th/domain/review/service/ReviewCreateService.java new file mode 100644 index 0000000..5ee8dd2 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewCreateService.java @@ -0,0 +1,44 @@ +package com.example.umc9th.domain.review.service; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.ReviewReqDto; +import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.repository.ReviewRepository; +import com.example.umc9th.domain.store.entity.Store; +import com.example.umc9th.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ReviewCreateService { + + private final ReviewRepository reviewRepository; + private final MemberRepository memberRepository; + private final StoreRepository storeRepository; + + @Transactional + public Review createReview(Long storeId, ReviewReqDto.JoinDto request) { + + // DTO -> Entity 변환 + Review review = ReviewConverter.toEntity(request); + + // 1번 유저 조회 + Member member = memberRepository.findById(1L) // ID가 1인 멤버 고정 + .orElseThrow(() -> new RuntimeException("멤버를 찾을 수 없습니다.")); + + // 가게 조회 + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new RuntimeException("가게를 찾을 수 없습니다.")); + + // 연관관계 매핑 + review.setMember(member); + review.setStore(store); + + // 저장 + return reviewRepository.save(review); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java index 7b4ecfd..7b4e92a 100644 --- a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java @@ -26,7 +26,7 @@ public class ReviewService { private final StoreRepository storeRepository; // Store repository // 리뷰 생성 메서드 - public Review createReview(Long memberId, Long storeId, String content, Integer rating) { + public Review createReview(Long memberId, Long storeId, String content, Float rating) { // 외래키로 사용할 엔티티 조회 Member member = memberRepository.findById(memberId) diff --git a/src/main/java/com/example/umc9th/domain/test/controller/TestController.java b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java index 851510f..3f9542a 100644 --- a/src/main/java/com/example/umc9th/domain/test/controller/TestController.java +++ b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java @@ -24,7 +24,6 @@ public ApiResponse test() { GeneralSuccessCode code = GeneralSuccessCode.OK; return ApiResponse.onSuccess( - code, TestConverter.toTestingDTO("This is Test!") ); } @@ -39,6 +38,6 @@ public ApiResponse exception( // 응답 코드 정의 GeneralSuccessCode code = GeneralSuccessCode.OK; - return ApiResponse.onSuccess(code, TestConverter.toExceptionDTO("This is Test!")); + return ApiResponse.onSuccess(TestConverter.toExceptionDTO("This is Test!")); } } diff --git a/src/main/java/com/example/umc9th/global/annotation/ExistFoods.java b/src/main/java/com/example/umc9th/global/annotation/ExistFoods.java new file mode 100644 index 0000000..3846dd7 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/annotation/ExistFoods.java @@ -0,0 +1,18 @@ +package com.example.umc9th.global.annotation; + +import com.example.umc9th.global.validator.FoodExistValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = FoodExistValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistFoods { + //여기서 디폴트 메시지를 설정합니다. + String message() default "해당 음식이 존재하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java index c7c90bd..6e70c7b 100644 --- a/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java +++ b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java @@ -23,7 +23,6 @@ public enum GeneralErrorCode implements BaseErrorCode{ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500_1", "예기치 않은 서버 에러가 발생했습니다." ); - private final HttpStatus status; private final String code; private final String message; diff --git a/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java index 195e36c..e7d951e 100644 --- a/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -5,9 +5,13 @@ import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; import com.example.umc9th.global.apiPayload.exception.GeneralException; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.util.HashMap; +import java.util.Map; + @RestControllerAdvice public class GeneralExceptionAdvice { @@ -39,4 +43,22 @@ public ResponseEntity> handleException( ) ); } + + // 컨트롤러 메서드에서 @Valid 어노테이션을 사용하여 DTO의 유효성 검사를 수행 + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity>> handleMethodArgumentNotValidException( + MethodArgumentNotValidException ex + ) { + // 검사에 실패한 필드와 그에 대한 메시지를 저장하는 Map + Map errors = new HashMap<>(); + ex.getBindingResult().getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage()) + ); + + GeneralErrorCode code = GeneralErrorCode.BAD_REQUEST; + ApiResponse> errorResponse = ApiResponse.onFailure(code, errors); + + // 에러 코드, 메시지와 함께 errors를 반환 + return ResponseEntity.status(code.getStatus()).body(errorResponse); + } } diff --git a/src/main/java/com/example/umc9th/global/validator/FoodExistValidator.java b/src/main/java/com/example/umc9th/global/validator/FoodExistValidator.java new file mode 100644 index 0000000..c27d47f --- /dev/null +++ b/src/main/java/com/example/umc9th/global/validator/FoodExistValidator.java @@ -0,0 +1,33 @@ +package com.example.umc9th.global.validator; + +import com.example.umc9th.domain.member.repository.FoodRepository; +import com.example.umc9th.global.annotation.ExistFoods; +import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class FoodExistValidator implements ConstraintValidator> { + + private final FoodRepository foodRepository; + + @Override + public boolean isValid(List values, ConstraintValidatorContext context) { + boolean isValid = values.stream() + .allMatch(value -> foodRepository.existsById(value)); + + if (!isValid) { + // 이 부분에서 아까 디폴트 메시지를 초기화 시키고, 새로운 메시지로 덮어씌우게 됩니다. + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(GeneralErrorCode.NOT_FOUND.getMessage()).addConstraintViolation(); + } + + return isValid; + + } +} From 20c63e005df953134f6251f7e54569bb1ffbb995 Mon Sep 17 00:00:00 2001 From: kddhhh23 Date: Sun, 23 Nov 2025 19:47:03 +0900 Subject: [PATCH 3/3] =?UTF-8?q?mission/=20=EB=AF=B8=EC=85=98=20=EB=8F=84?= =?UTF-8?q?=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/entity/mapping/MemberMission.java | 1 + .../mission/controller/MissionController.java | 28 +++++++++++ .../converter/MemberMissionConverter.java | 23 +++++++++ .../mission/dto/MemberMissionResDto.java | 21 ++++++++ .../repository/MemberMissionRepository.java | 2 + .../service/MissionCommandService.java | 49 +++++++++++++++++++ 6 files changed, 124 insertions(+) create mode 100644 src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java create mode 100644 src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java create mode 100644 src/main/java/com/example/umc9th/domain/mission/dto/MemberMissionResDto.java create mode 100644 src/main/java/com/example/umc9th/domain/mission/service/MissionCommandService.java diff --git a/src/main/java/com/example/umc9th/domain/member/entity/mapping/MemberMission.java b/src/main/java/com/example/umc9th/domain/member/entity/mapping/MemberMission.java index 0bbe6ef..e9f7cdc 100644 --- a/src/main/java/com/example/umc9th/domain/member/entity/mapping/MemberMission.java +++ b/src/main/java/com/example/umc9th/domain/member/entity/mapping/MemberMission.java @@ -11,6 +11,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter +@Setter @Table(name = "member_mission") public class MemberMission extends BaseEntity { diff --git a/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java new file mode 100644 index 0000000..dd6b14a --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/controller/MissionController.java @@ -0,0 +1,28 @@ +package com.example.umc9th.domain.mission.controller; + +import com.example.umc9th.domain.member.entity.mapping.MemberMission; +import com.example.umc9th.domain.mission.converter.MemberMissionConverter; +import com.example.umc9th.domain.mission.dto.MemberMissionResDto; +import com.example.umc9th.domain.mission.service.MissionCommandService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/missions") +public class MissionController { + + private final MissionCommandService missionCommandService; + + // API: 가게의 미션을 도전 중인 미션에 추가 + // URL: POST /missions/{missionId}/challenge + @PostMapping("/{missionId}/challenge") + public ApiResponse challengeMission( + @PathVariable Long missionId + ) { + + MemberMission memberMission = missionCommandService.challengeMission(missionId); + return ApiResponse.onSuccess(MemberMissionConverter.toCreateResultDto(memberMission)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java b/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java new file mode 100644 index 0000000..bfb67fc --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java @@ -0,0 +1,23 @@ +package com.example.umc9th.domain.mission.converter; + +import com.example.umc9th.domain.member.entity.mapping.MemberMission; +import com.example.umc9th.domain.mission.dto.MemberMissionResDto; +import java.time.LocalDateTime; + +public class MemberMissionConverter { + + // 도전 시작 (DTO -> Entity) + public static MemberMission toEntity() { + return MemberMission.builder() + .isComplete(false) // 초기 상태는 '진행 중' + .build(); + } + + // 도전 성공 응답 (Entity -> DTO) + public static MemberMissionResDto.CreateMemberMissionResultDto toCreateResultDto(MemberMission memberMission) { + return MemberMissionResDto.CreateMemberMissionResultDto.builder() + .memberMissionId(memberMission.getId()) + .createdAt(LocalDateTime.now()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/mission/dto/MemberMissionResDto.java b/src/main/java/com/example/umc9th/domain/mission/dto/MemberMissionResDto.java new file mode 100644 index 0000000..956f77c --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/dto/MemberMissionResDto.java @@ -0,0 +1,21 @@ +package com.example.umc9th.domain.mission.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +public class MemberMissionResDto { + + // 미션 도전 성공 시 응답 DTO + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class CreateMemberMissionResultDto { + private Long memberMissionId; // 생성된 도전 기록의 ID + private LocalDateTime createdAt; // 도전 시작 시간 + } +} diff --git a/src/main/java/com/example/umc9th/domain/mission/repository/MemberMissionRepository.java b/src/main/java/com/example/umc9th/domain/mission/repository/MemberMissionRepository.java index aa4b32a..dc78294 100644 --- a/src/main/java/com/example/umc9th/domain/mission/repository/MemberMissionRepository.java +++ b/src/main/java/com/example/umc9th/domain/mission/repository/MemberMissionRepository.java @@ -35,4 +35,6 @@ List findMyMissionsByCursor( @Param("memberId") Long memberId, @Param("lastId") Long lastId ); + + boolean existsByMemberIdAndMissionId(Long memberId, Long missionId); // 해당 미션을 이미 도전 중인지 확인하기 위함 } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/mission/service/MissionCommandService.java b/src/main/java/com/example/umc9th/domain/mission/service/MissionCommandService.java new file mode 100644 index 0000000..5135dfb --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/service/MissionCommandService.java @@ -0,0 +1,49 @@ +package com.example.umc9th.domain.mission.service; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.member.entity.mapping.MemberMission; +import com.example.umc9th.domain.mission.converter.MemberMissionConverter; +import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.mission.repository.MemberMissionRepository; +import com.example.umc9th.domain.mission.repository.MissionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class MissionCommandService { + + private final MemberMissionRepository memberMissionRepository; + private final MemberRepository memberRepository; + private final MissionRepository missionRepository; + + public MemberMission challengeMission(Long missionId) { + + // 회원 조회 + Member member = memberRepository.findById(1L) // 1번 멤버 + .orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다.")); + + // 미션 조회 + Mission mission = missionRepository.findById(missionId) + .orElseThrow(() -> new RuntimeException("미션을 찾을 수 없습니다.")); + + // 이미 도전 중인지 확인 (중복 방지) + if (memberMissionRepository.existsByMemberIdAndMissionId(member.getId(), mission.getId())) { + // 이미 도전 중이면 에러 처리 + throw new RuntimeException("이미 도전 중인 미션입니다."); + } + + // 매핑 엔티티 생성 + MemberMission memberMission = MemberMissionConverter.toEntity(); + + // 연관관계 설정 + memberMission.setMember(member); + memberMission.setMission(mission); + + // 저장 + return memberMissionRepository.save(memberMission); + } +} \ No newline at end of file