diff --git a/build.gradle b/build.gradle index 22ba0da..9f942cc 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,13 @@ 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' + + // 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 new file mode 100644 index 0000000..a605017 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java @@ -0,0 +1,27 @@ +package com.example.umc9th.domain.member.controller; + +import com.example.umc9th.domain.member.dto.req.MemberReqDTO; +import com.example.umc9th.domain.member.dto.res.MemberResDTO; +import com.example.umc9th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc9th.domain.member.service.command.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; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class MemberController { + + private final MemberCommandService memberCommandService; + + // 회원가입 + @PostMapping("/sign-up") + public ApiResponse signUp( + @RequestBody @Valid MemberReqDTO.JoinDTO dto + ){ + return ApiResponse.onSuccess(MemberSuccessCode.FOUND, 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..58490bf --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java @@ -0,0 +1,31 @@ +package com.example.umc9th.domain.member.converter; + +import com.example.umc9th.domain.member.dto.req.MemberReqDTO; +import com.example.umc9th.domain.member.dto.res.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()) + .birth(dto.birth()) + .address(dto.address()) + .detailAddress(dto.specAddress()) + .gender(dto.gender()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/member/dto/req/MemberReqDTO.java b/src/main/java/com/example/umc9th/domain/member/dto/req/MemberReqDTO.java new file mode 100644 index 0000000..bba0e70 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/dto/req/MemberReqDTO.java @@ -0,0 +1,26 @@ +package com.example.umc9th.domain.member.dto.req; + +import com.example.umc9th.domain.member.enums.Gender; +import com.example.umc9th.global.annotation.ExistFoods; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; +import java.util.List; + +public class MemberReqDTO { + public record JoinDTO( + @NotBlank + String name, + @NotNull + Gender gender, + @NotNull + LocalDate birth, + @NotNull + String address, + @NotNull + String specAddress, + @ExistFoods + List preferCategory + ){} +} diff --git a/src/main/java/com/example/umc9th/domain/member/dto/res/MemberResDTO.java b/src/main/java/com/example/umc9th/domain/member/dto/res/MemberResDTO.java new file mode 100644 index 0000000..fd5279a --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/dto/res/MemberResDTO.java @@ -0,0 +1,13 @@ +package com.example.umc9th.domain.member.dto.res; + +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 f0c5404..246f155 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 @@ -6,6 +6,7 @@ import com.example.umc9th.domain.member.enums.Gender; import com.example.umc9th.domain.member.enums.PhoneVerificationStatus; import com.example.umc9th.domain.member.enums.SocialType; +import com.example.umc9th.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -19,7 +20,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @Table(name = "member") -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -41,6 +42,9 @@ public class Member { @Column(name = "address", length = 125, nullable = false) private String address; + @Column(name = "detail_address", length = 125, nullable = false) + private String detailAddress; + @Column(name = "point", nullable = false) @Builder.Default @@ -63,6 +67,7 @@ public class Member { private Boolean isActive = true; @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE) + @Builder.Default private List memberFoodList = new ArrayList<>(); @OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE) diff --git a/src/main/java/com/example/umc9th/domain/member/enums/SocialType.java b/src/main/java/com/example/umc9th/domain/member/enums/SocialType.java index 068cf10..d3d1c3c 100644 --- a/src/main/java/com/example/umc9th/domain/member/enums/SocialType.java +++ b/src/main/java/com/example/umc9th/domain/member/enums/SocialType.java @@ -1,5 +1,5 @@ package com.example.umc9th.domain.member.enums; public enum SocialType { - KAKAO, NAVER, APPLE, GOOGLE + KAKAO, NAVER, APPLE, GOOGLE, LOCAL } 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..71f7432 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/FoodException.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 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..8b8001b --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/code/FoodErrorCode.java @@ -0,0 +1,20 @@ +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 { + + NOT_FOUND(HttpStatus.NOT_FOUND, + "FOOD404_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/FoodSuccessCode.java b/src/main/java/com/example/umc9th/domain/member/exception/code/FoodSuccessCode.java new file mode 100644 index 0000000..80642db --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/code/FoodSuccessCode.java @@ -0,0 +1,20 @@ +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 FoodSuccessCode implements BaseErrorCode { + + FOUND(HttpStatus.OK, + "FOOD200_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/MemberErrorCode.java b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java new file mode 100644 index 0000000..64645ae --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java @@ -0,0 +1,20 @@ +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..c11fd5d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/repository/FoodRepository.java @@ -0,0 +1,11 @@ +package com.example.umc9th.domain.member.repository; + +import com.example.umc9th.domain.member.entity.Food; +import com.example.umc9th.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +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..06bedce --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/repository/MemberFoodRepository.java @@ -0,0 +1,11 @@ +package com.example.umc9th.domain.member.repository; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.entity.mapping.MemberFood; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberFoodRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandService.java b/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandService.java new file mode 100644 index 0000000..eac86e0 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandService.java @@ -0,0 +1,9 @@ +package com.example.umc9th.domain.member.service.command; + +import com.example.umc9th.domain.member.dto.req.MemberReqDTO; +import com.example.umc9th.domain.member.dto.res.MemberResDTO; + +public interface MemberCommandService { + // 회원가입 + MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto); +} diff --git a/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java b/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java new file mode 100644 index 0000000..d92605d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java @@ -0,0 +1,70 @@ +package com.example.umc9th.domain.member.service.command; + +import com.example.umc9th.domain.member.converter.MemberConverter; +import com.example.umc9th.domain.member.dto.req.MemberReqDTO; +import com.example.umc9th.domain.member.dto.res.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 jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +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.preferCategory().size() > 1){ + List memberFoodList = new ArrayList<>(); + + // 선호 음식 ID별 조회 + for (Long id : dto.preferCategory()){ + + // 음식 존재 여부 검증 + Food food = foodRepository.findById(id) + .orElseThrow(() -> new FoodException(FoodErrorCode.NOT_FOUND)); + + // MemberFood 엔티티 생성 (컨버터 사용해야 함) + MemberFood memberFood = MemberFood.builder() + .member(member) + .food(food) + .build(); + + // 사용자 - 음식 (선호 음식) 추가 + memberFoodList.add(memberFood); + } + + // 모든 선호 음식 추가: DB 적용 + memberFoodRepository.saveAll(memberFoodList); + } + + + // 응답 DTO 생성 + return MemberConverter.toJoinDTO(member); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/member/service/query/MemberQuerySercvice.java b/src/main/java/com/example/umc9th/domain/member/service/query/MemberQuerySercvice.java new file mode 100644 index 0000000..f3a3e44 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/service/query/MemberQuerySercvice.java @@ -0,0 +1,4 @@ +package com.example.umc9th.domain.member.service.query; + +public interface MemberQuerySercvice { +} diff --git a/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionController.java b/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionController.java new file mode 100644 index 0000000..c769c01 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/controller/MemberMissionController.java @@ -0,0 +1,30 @@ +package com.example.umc9th.domain.mission.controller; + +import com.example.umc9th.domain.mission.dto.req.MemberMissionReqDTO; +import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; +import com.example.umc9th.domain.mission.exception.code.MissionSuccessCode; +import com.example.umc9th.domain.mission.service.command.MemberMissionCommandService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class MemberMissionController { + private final MemberMissionCommandService memberMissionCommandService; + + @Operation( + summary = "미션 도전하기", + description = "현재 회원(me)에 대해 특정 미션 도전 상태를 추가합니다. (과제에서는 하드코딩 회원 사용)" + ) + @PostMapping("/members/me/missions") + public ApiResponse challengeMission( + @Valid @RequestBody MemberMissionReqDTO.ChallengeDTO dto + ) { + MemberMissionResDTO.ChallengeDTO response = memberMissionCommandService.challengeMission(dto); + return ApiResponse.onSuccess(MissionSuccessCode.MISSION_CHALLENGED, response); + } +} 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..348fe30 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/converter/MemberMissionConverter.java @@ -0,0 +1,29 @@ +package com.example.umc9th.domain.mission.converter; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; +import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.mission.entity.mapping.MemberMission; + +public class MemberMissionConverter { + + // 리뷰 생성 : DTO -> Entity + public static MemberMission toMemberMission(Member member, Mission mission) { + return MemberMission.builder() + .member(member) + .mission(mission) + .isCompleted(false) + .build(); + } + + // 리뷰 생성 응답용 : Entity -> DTO + public static MemberMissionResDTO.ChallengeDTO toChallengeDTO(MemberMission memberMission) { + return MemberMissionResDTO.ChallengeDTO.builder() + .memberMissionId(memberMission.getId()) + .memberId(memberMission.getMember().getId()) + .missionId(memberMission.getMission().getId()) + .isCompleted(memberMission.isCompleted()) + .createdAt(memberMission.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/umc9th/domain/mission/dto/req/MemberMissionReqDTO.java b/src/main/java/com/example/umc9th/domain/mission/dto/req/MemberMissionReqDTO.java new file mode 100644 index 0000000..ae09514 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/dto/req/MemberMissionReqDTO.java @@ -0,0 +1,12 @@ +package com.example.umc9th.domain.mission.dto.req; + +import jakarta.validation.constraints.NotNull; + +public class MemberMissionReqDTO { + + // 미션 도전 요청 DTO + public record ChallengeDTO( + @NotNull(message = "missionId는 필수입니다.") + Long missionId + ) {} +} diff --git a/src/main/java/com/example/umc9th/domain/mission/dto/res/MemberMissionResDTO.java b/src/main/java/com/example/umc9th/domain/mission/dto/res/MemberMissionResDTO.java new file mode 100644 index 0000000..c5b62bf --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/dto/res/MemberMissionResDTO.java @@ -0,0 +1,17 @@ +package com.example.umc9th.domain.mission.dto.res; + +import lombok.Builder; + +import java.time.LocalDateTime; + +public class MemberMissionResDTO { + + @Builder + public record ChallengeDTO( + Long memberMissionId, + Long memberId, + Long missionId, + Boolean isCompleted, + LocalDateTime createdAt + ) {} +} diff --git a/src/main/java/com/example/umc9th/domain/mission/entity/mapping/MemberMission.java b/src/main/java/com/example/umc9th/domain/mission/entity/mapping/MemberMission.java index 94c8ec1..b256d6a 100644 --- a/src/main/java/com/example/umc9th/domain/mission/entity/mapping/MemberMission.java +++ b/src/main/java/com/example/umc9th/domain/mission/entity/mapping/MemberMission.java @@ -2,6 +2,7 @@ import com.example.umc9th.domain.mission.entity.Mission; import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -11,7 +12,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @Table(name = "member_mission") -public class MemberMission { +public class MemberMission extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/example/umc9th/domain/mission/exception/MissionException.java b/src/main/java/com/example/umc9th/domain/mission/exception/MissionException.java new file mode 100644 index 0000000..56fe3ab --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/exception/MissionException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.mission.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class MissionException extends GeneralException { + public MissionException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionErrorCode.java b/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionErrorCode.java new file mode 100644 index 0000000..8b1c3a9 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionErrorCode.java @@ -0,0 +1,23 @@ +package com.example.umc9th.domain.mission.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 MissionErrorCode implements BaseErrorCode { + + NOT_FOUND(HttpStatus.NOT_FOUND, + "MISSION404_1", + "해당 미션을 찾지 못했습니다."), + ALREADY_CHALLENGED(HttpStatus.BAD_REQUEST, + "MISSION400_1", + "이미 도전 중인 미션입니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} + diff --git a/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java b/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java new file mode 100644 index 0000000..6b174dd --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/exception/code/MissionSuccessCode.java @@ -0,0 +1,21 @@ +package com.example.umc9th.domain.mission.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 MissionSuccessCode implements BaseSuccessCode { + + MISSION_CHALLENGED( + HttpStatus.CREATED, + "MISSION201_1", + "미션 도전에 성공했습니다." + ); + + private final HttpStatus status; + private final String code; + private final String message; +} 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 951f301..a06094c 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 @@ -13,4 +13,6 @@ public interface MemberMissionRepository extends JpaRepository findAllByMember_IdAndIsCompletedTrue(Long memberId, Pageable pageable); + + boolean existsByMember_IdAndMission_Id(Long memberId, Long missionId); } diff --git a/src/main/java/com/example/umc9th/domain/mission/service/command/MemberMissionCommandService.java b/src/main/java/com/example/umc9th/domain/mission/service/command/MemberMissionCommandService.java new file mode 100644 index 0000000..8c4999e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/service/command/MemberMissionCommandService.java @@ -0,0 +1,9 @@ +package com.example.umc9th.domain.mission.service.command; + +import com.example.umc9th.domain.mission.dto.req.MemberMissionReqDTO; +import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; + +public interface MemberMissionCommandService { + + MemberMissionResDTO.ChallengeDTO challengeMission(MemberMissionReqDTO.ChallengeDTO dto); +} diff --git a/src/main/java/com/example/umc9th/domain/mission/service/command/MemberMissionCommandServiceImpl.java b/src/main/java/com/example/umc9th/domain/mission/service/command/MemberMissionCommandServiceImpl.java new file mode 100644 index 0000000..629bec6 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/mission/service/command/MemberMissionCommandServiceImpl.java @@ -0,0 +1,52 @@ +package com.example.umc9th.domain.mission.service.command; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.mission.converter.MemberMissionConverter; +import com.example.umc9th.domain.mission.dto.req.MemberMissionReqDTO; +import com.example.umc9th.domain.mission.dto.res.MemberMissionResDTO; +import com.example.umc9th.domain.mission.entity.Mission; +import com.example.umc9th.domain.mission.entity.mapping.MemberMission; +import com.example.umc9th.domain.mission.exception.MissionException; +import com.example.umc9th.domain.mission.exception.code.MissionErrorCode; +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 +public class MemberMissionCommandServiceImpl implements MemberMissionCommandService { + private final MemberMissionRepository memberMissionRepository; + private final MissionRepository missionRepository; + private final MemberRepository memberRepository; + + // 미션용 하드코디 회원 + private static final Long HARDCODED_MEMBER_ID = 1L; + + @Override + @Transactional + public MemberMissionResDTO.ChallengeDTO challengeMission(MemberMissionReqDTO.ChallengeDTO dto) { + // 1. 미션 존재 여부 체크 + Mission mission = missionRepository.findById(dto.missionId()) + .orElseThrow(() -> new MissionException(MissionErrorCode.NOT_FOUND)); + + // 2. 회원 존재 여부 체크 + Member member = memberRepository.findById(HARDCODED_MEMBER_ID) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + // 3. 이미 도전 중인지 검사 (중복 방지) + if (memberMissionRepository.existsByMember_IdAndMission_Id(member.getId(), mission.getId())) { + throw new MissionException(MissionErrorCode.ALREADY_CHALLENGED); + } + + // 4. MemberMission 생성 및 저장 + MemberMission memberMission = MemberMissionConverter.toMemberMission(member, mission); + MemberMission saved = memberMissionRepository.save(memberMission); + + return MemberMissionConverter.toChallengeDTO(saved); + } +} diff --git a/src/main/java/com/example/umc9th/domain/restaurant/exception/RestaurantException.java b/src/main/java/com/example/umc9th/domain/restaurant/exception/RestaurantException.java new file mode 100644 index 0000000..dd450ad --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/restaurant/exception/RestaurantException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.restaurant.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class RestaurantException extends GeneralException { + public RestaurantException(BaseErrorCode code) { + super(code); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/restaurant/exception/code/RestaurantErrorCode.java b/src/main/java/com/example/umc9th/domain/restaurant/exception/code/RestaurantErrorCode.java new file mode 100644 index 0000000..ad81eda --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/restaurant/exception/code/RestaurantErrorCode.java @@ -0,0 +1,15 @@ +package com.example.umc9th.domain.restaurant.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 RestaurantErrorCode implements BaseErrorCode { + NOT_FOUND(HttpStatus.NOT_FOUND, "RESTAURANT404_1", "해당 가게를 찾지 못했습니다."); + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/umc9th/domain/restaurant/repository/RestaurantRepository.java b/src/main/java/com/example/umc9th/domain/restaurant/repository/RestaurantRepository.java new file mode 100644 index 0000000..88d4f80 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/restaurant/repository/RestaurantRepository.java @@ -0,0 +1,8 @@ +package com.example.umc9th.domain.restaurant.repository; + +import com.example.umc9th.domain.member.entity.Food; +import com.example.umc9th.domain.restaurant.entity.Restaurant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RestaurantRepository extends JpaRepository { +} 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 8ded23c..e86b578 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,11 +1,16 @@ package com.example.umc9th.domain.review.controller; import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.req.ReviewReqDTO; import com.example.umc9th.domain.review.dto.res.ReviewResDTO; import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.exception.code.ReviewSuccessCode; +import com.example.umc9th.domain.review.service.command.ReviewCommandService; import com.example.umc9th.domain.review.service.query.ReviewQueryServiceImpl; import com.example.umc9th.global.apiPayload.ApiResponse; import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -13,16 +18,17 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/reviews") +@RequestMapping("/api") public class ReviewController { private final ReviewQueryServiceImpl reviewQueryService; + private final ReviewCommandService reviewCommandService; - + // 내 리뷰 조회 // /api/reviews/me?memberId=7 // /api/reviews/me?memberId=7&type=restaurant&query=반이학생마라탕마라반 // /api/reviews/me?memberId=7&type=rating&query=4 // /api/reviews/me?memberId=7&type=both&query=반이학생마라탕마라반&4 - @GetMapping("/me") + @GetMapping("/reviews/me") public ApiResponse myReviews( @RequestParam Long memberId, @RequestParam(required = false) String type, @@ -30,9 +36,18 @@ public ApiResponse myReviews( ) { List list = reviewQueryService.searchMyReviews(memberId, type, query); ReviewResDTO.MyReviews dto = ReviewConverter.toMyReviews(list); - return ApiResponse.onSuccess(GeneralSuccessCode.OK, dto); + return ApiResponse.onSuccess(ReviewSuccessCode.MY_REVIEWS_FOUND, dto); } - + // 가게에 리뷰 추가하기 + @Operation(summary = "가게에 리뷰 추가하기", description = "특정 가게(storeId)에 대한 리뷰를 작성합니다.") + @PostMapping("/restaurant/{restaurantId}/reviews") + public ApiResponse createReview( + @PathVariable Long restaurantId, + @Valid @RequestBody ReviewReqDTO.CreateReview dto + ) { + ReviewResDTO.CreateReview response = reviewCommandService.createReview(restaurantId, dto); + return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_CREATED, response); + } } 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 c9f1839..dce5ccd 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 @@ -1,5 +1,8 @@ package com.example.umc9th.domain.review.converter; +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.restaurant.entity.Restaurant; +import com.example.umc9th.domain.review.dto.req.ReviewReqDTO; import com.example.umc9th.domain.review.dto.res.ReviewResDTO; import com.example.umc9th.domain.review.entity.Review; @@ -31,4 +34,26 @@ public static ReviewResDTO.MyReviews toMyReviews(List list) { .totalCount(reviews.size()) .build(); } + + // 리뷰 생성 : DTO -> Entity + public static Review toReview(ReviewReqDTO.CreateReview dto, Member member, Restaurant restaurant) { + return Review.builder() + .reviewContent(dto.reviewContent()) + .rating(dto.rating()) + .member(member) + .restaurant(restaurant) + .build(); + } + + // 리뷰 생성 응답용 : Entity -> DTO + public static ReviewResDTO.CreateReview toCreateReview(Review review) { + return ReviewResDTO.CreateReview.builder() + .reviewId(review.getId()) + .restaurantId(review.getRestaurant().getId()) + .memberId(review.getMember().getId()) + .reviewContent(review.getReviewContent()) + .rating(review.getRating()) + .createdAt(review.getCreatedAt()) + .build(); + } } diff --git a/src/main/java/com/example/umc9th/domain/review/dto/req/ReviewReqDTO.java b/src/main/java/com/example/umc9th/domain/review/dto/req/ReviewReqDTO.java new file mode 100644 index 0000000..64da77f --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/dto/req/ReviewReqDTO.java @@ -0,0 +1,19 @@ +package com.example.umc9th.domain.review.dto.req; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class ReviewReqDTO { + // 가게에 리뷰 추가하기 요청 DTO + public record CreateReview( + @NotBlank(message = "리뷰 내용을 입력해주세요.") + String reviewContent, + + @NotNull(message = "별점을 입력해주세요.") + @DecimalMin(value = "0.0", message = "별점은 0.0 이상이어야 합니다.") + @DecimalMax(value = "5.0", message = "별점은 5.0 이하이어야 합니다.") + Double rating + ) {} +} diff --git a/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java b/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java index e3daea4..e3cae2a 100644 --- a/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java +++ b/src/main/java/com/example/umc9th/domain/review/dto/res/ReviewResDTO.java @@ -23,4 +23,15 @@ public static class MyReviews { private List reviews; private Integer totalCount; } + + // 가게에 리뷰 작성 응답 DTO + @Builder + public record CreateReview( + Long reviewId, + Long restaurantId, + Long memberId, + String reviewContent, + Double rating, + LocalDateTime createdAt + ){} } diff --git a/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java b/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java new file mode 100644 index 0000000..7dce9f7 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/exception/code/ReviewSuccessCode.java @@ -0,0 +1,27 @@ +package com.example.umc9th.domain.review.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 ReviewSuccessCode implements BaseSuccessCode { + + REVIEW_CREATED( + HttpStatus.CREATED, + "REVIEW201_1", + "리뷰가 성공적으로 등록되었습니다." + ), + + MY_REVIEWS_FOUND( + HttpStatus.OK, + "REVIEW200_1", + "내 리뷰 목록을 성공적으로 조회했습니다." + ); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/umc9th/domain/review/service/command/ReviewCommandService.java b/src/main/java/com/example/umc9th/domain/review/service/command/ReviewCommandService.java new file mode 100644 index 0000000..d2b013f --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/service/command/ReviewCommandService.java @@ -0,0 +1,9 @@ +package com.example.umc9th.domain.review.service.command; + +import com.example.umc9th.domain.review.dto.req.ReviewReqDTO; +import com.example.umc9th.domain.review.dto.res.ReviewResDTO; + +public interface ReviewCommandService { + // 가게에 리뷰 추가하기 + ReviewResDTO.CreateReview createReview(Long restaurantId, ReviewReqDTO.CreateReview dto); +} diff --git a/src/main/java/com/example/umc9th/domain/review/service/command/ReviewCommandServiceImpl.java b/src/main/java/com/example/umc9th/domain/review/service/command/ReviewCommandServiceImpl.java new file mode 100644 index 0000000..9c495e6 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/service/command/ReviewCommandServiceImpl.java @@ -0,0 +1,47 @@ +package com.example.umc9th.domain.review.service.command; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.domain.restaurant.entity.Restaurant; +import com.example.umc9th.domain.restaurant.exception.RestaurantException; +import com.example.umc9th.domain.restaurant.exception.code.RestaurantErrorCode; +import com.example.umc9th.domain.restaurant.repository.RestaurantRepository; +import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.req.ReviewReqDTO; +import com.example.umc9th.domain.review.dto.res.ReviewResDTO; +import com.example.umc9th.domain.review.entity.Review; +import com.example.umc9th.domain.review.repository.ReviewRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ReviewCommandServiceImpl implements ReviewCommandService { + private final ReviewRepository reviewRepository; + private final RestaurantRepository restaurantRepository; + private final MemberRepository memberRepository; + + // 미션용 하드코딩 유저 ID + private static final Long HARDCODED_MEMBER_ID = 1L; + + @Override + @Transactional + public ReviewResDTO.CreateReview createReview(Long restaurantId, ReviewReqDTO.CreateReview dto) { + // 가게 존재 여부 검증 + Restaurant restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow(() -> new RestaurantException(RestaurantErrorCode.NOT_FOUND)); + + // 회원 존재 여부 검증 + Member member = memberRepository.findById(HARDCODED_MEMBER_ID) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + // 리뷰 엔티티 생성 및 저장 + Review review = ReviewConverter.toReview(dto, member, restaurant); + Review saved = reviewRepository.save(review); + + return ReviewConverter.toCreateReview(saved); + } +} 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 37f2770..19bba69 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 @@ -12,6 +12,7 @@ public enum GeneralErrorCode implements BaseErrorCode { FORBIDDEN(HttpStatus.FORBIDDEN, "AUTH403_1", "요청이 거부되었습니다."), NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404_1", "요청한 리소스를 찾을 수 없습니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500_1", "예기치 않은 서버 에러가 발생했습니다."), + VALID_FAIL(HttpStatus.BAD_REQUEST, "VALID400_1", "검증에 실패했습니다."); ; private final HttpStatus status; private final String code; 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 827cbef..ed663e8 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 @@ -6,9 +6,13 @@ import com.example.umc9th.global.apiPayload.exception.GeneralException; import org.springframework.http.HttpStatus; 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 { // 애플리케이션에서 발생하는 커스텀 예외를 처리 @@ -37,4 +41,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.VALID_FAIL; + ApiResponse> errorResponse = ApiResponse.onFailure(code, errors); + + // 에러 코드, 메시지와 함께 errors를 반환 + return ResponseEntity.status(code.getStatus()).body(errorResponse); + } } diff --git a/src/main/java/com/example/umc9th/global/config/JpaConfig.java b/src/main/java/com/example/umc9th/global/config/JpaConfig.java new file mode 100644 index 0000000..b256807 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/JpaConfig.java @@ -0,0 +1,9 @@ +package com.example.umc9th.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} 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..576ef99 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/SwaggerConfig.java @@ -0,0 +1,36 @@ +package com.example.umc9th.global.config; + +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.servers.Server; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI swagger() { + Info info = new Info().title("Project").description("Project Swagger").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("/")) + .addSecurityItem(securityRequirement) + .components(components); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/global/entity/BaseEntity.java b/src/main/java/com/example/umc9th/global/entity/BaseEntity.java index 963b7cf..28dc7dc 100644 --- a/src/main/java/com/example/umc9th/global/entity/BaseEntity.java +++ b/src/main/java/com/example/umc9th/global/entity/BaseEntity.java @@ -19,6 +19,7 @@ public class BaseEntity { private LocalDateTime createdAt; @LastModifiedDate + @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; 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..dd33a00 --- /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.exception.code.FoodErrorCode; +import com.example.umc9th.domain.member.repository.FoodRepository; +import com.example.umc9th.global.annotation.ExistFoods; +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(FoodErrorCode.NOT_FOUND.getMessage()).addConstraintViolation(); + } + + return isValid; + + } +}