diff --git a/bean/umc10th/.gitignore b/bean/umc10th/.gitignore index ac2055b..5cbf6be 100644 --- a/bean/umc10th/.gitignore +++ b/bean/umc10th/.gitignore @@ -1,7 +1,6 @@ HELP.md .gradle -.gradle-user-home/ -.gradle-user-home-2/ +.gradle-user-home*/ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index a223397..06db769 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -2,31 +2,30 @@ import com.example.umc10th.domain.member.dto.MemberReqDto; import com.example.umc10th.domain.member.dto.MemberResDto; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; import com.example.umc10th.domain.member.service.MemberService; +import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/members") +@RequestMapping("/api/v1/users") @RequiredArgsConstructor public class MemberController { private final MemberService memberService; - @GetMapping("/{memberId}") - public ResponseEntity getMember(@PathVariable Long memberId) { - return ResponseEntity.ok(memberService.getMember(memberId)); - } + @PostMapping("/me") + public ResponseEntity> getMyPage( + @Valid @RequestBody MemberReqDto.GetMyPageRequest request + ) { + MemberResDto.GetMyPageResponse result = memberService.getMyPage(request.id()); - @PostMapping - public ResponseEntity createMember(@Valid @RequestBody MemberReqDto.Create request) { - return ResponseEntity.ok(memberService.createMember(request)); + return ResponseEntity.ok(ApiResponse.of(MemberSuccessCode.GET_MY_PAGE_SUCCESS, result)); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 479a2d4..edc3bf4 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -29,4 +29,14 @@ public static MemberResDto.MemberInfo toMemberInfo(Member member) { member.getNickname() ); } -} \ No newline at end of file + + public static MemberResDto.GetMyPageResponse toMyPageResponse(Member member, Integer point) { + return new MemberResDto.GetMyPageResponse( + member.getName(), + null, + null, + null, + point + ); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java index 71fad6a..adfafa5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDto.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.member.enums.Gender; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import java.time.LocalDate; @@ -16,4 +17,9 @@ public record Create( @NotBlank @Size(max = 20) String nickname ) { } + + public record GetMyPageRequest( + @NotNull @Positive Long id + ) { + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java index 26d1817..8ba3e08 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/dto/MemberResDto.java @@ -14,4 +14,13 @@ public record MemberInfo( String nickname ) { } + + public record GetMyPageResponse( + String name, + String profileUrl, + String email, + String phoneNumber, + Integer point + ) { + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java index be0bbb2..5e0d1f9 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.member.exception; import com.example.umc10th.domain.member.exception.code.MemberErrorCode; -import lombok.Getter; - -@Getter -public class MemberException extends RuntimeException { - - private final MemberErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class MemberException extends GeneralException { public MemberException(MemberErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 8d59239..cc28733 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -1,15 +1,27 @@ package com.example.umc10th.domain.member.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum MemberErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "����ڸ� ã�� �� �����ϴ�."); +public enum MemberErrorCode implements BaseErrorCode { + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java new file mode 100644 index 0000000..11a1c29 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -0,0 +1,27 @@ +package com.example.umc10th.domain.member.exception.code; + +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum MemberSuccessCode implements BaseSuccessCode { + GET_MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER2001", "Successfully retrieved user information."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, true, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 86dbcad..970c6a8 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -7,4 +7,6 @@ public interface MemberService { MemberResDto.MemberInfo getMember(Long memberId); MemberResDto.MemberInfo createMember(MemberReqDto.Create request); -} \ No newline at end of file + + MemberResDto.GetMyPageResponse getMyPage(Long memberId); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java index 76794fe..552b8ea 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/member/service/MemberServiceImpl.java @@ -7,6 +7,8 @@ import com.example.umc10th.domain.member.exception.MemberException; import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; +import com.example.umc10th.domain.mission.repository.MemberMissionRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +19,7 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; + private final MemberMissionRepository memberMissionRepository; @Override public MemberResDto.MemberInfo getMember(Long memberId) { @@ -32,4 +35,17 @@ public MemberResDto.MemberInfo createMember(MemberReqDto.Create request) { Member savedMember = memberRepository.save(MemberConverter.toEntity(request)); return MemberConverter.toMemberInfo(savedMember); } -} \ No newline at end of file + + @Override + public MemberResDto.GetMyPageResponse getMyPage(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Integer totalPoint = memberMissionRepository.sumRewardPointByMemberIdAndStatus( + memberId, + MemberMissionStatus.COMPLETE + ); + + return MemberConverter.toMyPageResponse(member, totalPoint); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index f26e710..3f7649e 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -2,31 +2,63 @@ import com.example.umc10th.domain.mission.dto.MissionReqDto; import com.example.umc10th.domain.mission.dto.MissionResDto; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; import com.example.umc10th.domain.mission.service.MissionService; +import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/missions") +@RequestMapping("/api/v1/missions") @RequiredArgsConstructor +@Validated public class MissionController { private final MissionService missionService; @GetMapping("/{missionId}") - public ResponseEntity getMission(@PathVariable Long missionId) { - return ResponseEntity.ok(missionService.getMission(missionId)); + public ResponseEntity> getMission(@PathVariable Long missionId) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getMission(missionId))); } @PostMapping - public ResponseEntity createMission(@Valid @RequestBody MissionReqDto.Create request) { - return ResponseEntity.ok(missionService.createMission(request)); + public ResponseEntity> createMission(@Valid @RequestBody MissionReqDto.Create request) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.createMission(request))); } -} \ No newline at end of file + + @PostMapping("/my/challenging") + public ResponseEntity> getMyChallengingMissions( + @Valid @RequestBody MissionReqDto.MyChallengingMissionsRequest request + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getMyChallengingMissions(request))); + } + + @GetMapping("/members/{memberId}") + public ResponseEntity> getMemberMissions( + @PathVariable Long memberId, + @RequestParam MemberMissionStatus status, + @RequestParam(defaultValue = "1") @Min(1) Integer page + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getMemberMissions(memberId, status, page))); + } + + @GetMapping("/home") + public ResponseEntity> getHomeMissions( + @RequestParam @NotNull Long regionId, + @RequestParam(required = false) Long memberId, + @RequestParam(defaultValue = "1") @Min(1) Integer page + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(missionService.getHomeMissions(regionId, memberId, page))); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index c3c7f68..4baf8a4 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -2,8 +2,11 @@ import com.example.umc10th.domain.mission.dto.MissionReqDto; import com.example.umc10th.domain.mission.dto.MissionResDto; +import com.example.umc10th.domain.mission.entity.MemberMission; import com.example.umc10th.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; import com.example.umc10th.domain.store.entity.Store; +import java.util.List; public final class MissionConverter { @@ -26,4 +29,59 @@ public static MissionResDto.MissionInfo toMissionInfo(Mission mission) { mission.getRewardPoint() ); } -} \ No newline at end of file + + public static MissionResDto.MemberMissionPageItem toMemberMissionPageItem(MemberMission memberMission) { + Mission mission = memberMission.getMission(); + + return new MissionResDto.MemberMissionPageItem( + memberMission.getId(), + mission.getId(), + mission.getStore().getName(), + mission.getContent(), + mission.getRewardPoint(), + memberMission.getStatus() + ); + } + + public static MissionResDto.HomeMissionPageItem toHomeMissionPageItem(Mission mission) { + return new MissionResDto.HomeMissionPageItem( + mission.getId(), + mission.getStore().getName(), + mission.getContent(), + mission.getRewardPoint(), + mission.getStore().getScore() + ); + } + + public static MissionResDto.MemberMissionPageResponse toMemberMissionPageResponse(Page page) { + List missions = page.getContent() + .stream() + .map(MissionConverter::toMemberMissionPageItem) + .toList(); + + return new MissionResDto.MemberMissionPageResponse( + missions, + missions.size(), + page.getTotalPages(), + page.getTotalElements(), + page.isFirst(), + page.isLast() + ); + } + + public static MissionResDto.HomeMissionPageResponse toHomeMissionPageResponse(Page page) { + List missions = page.getContent() + .stream() + .map(MissionConverter::toHomeMissionPageItem) + .toList(); + + return new MissionResDto.HomeMissionPageResponse( + missions, + missions.size(), + page.getTotalPages(), + page.getTotalElements(), + page.isFirst(), + page.isLast() + ); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java index 5c39322..73c0fd4 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDto.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; public class MissionReqDto { @@ -12,4 +13,11 @@ public record Create( @Min(0) int rewardPoint ) { } -} \ No newline at end of file + + public record MyChallengingMissionsRequest( + @NotNull @Positive Long memberId, + @NotNull @Min(1) Integer page, + @NotNull @Min(1) Integer size + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java index fc37061..dce8553 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDto.java @@ -1,5 +1,8 @@ package com.example.umc10th.domain.mission.dto; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; +import java.util.List; + public class MissionResDto { public record MissionInfo( @@ -9,4 +12,43 @@ public record MissionInfo( int rewardPoint ) { } -} \ No newline at end of file + + public record MemberMissionPageItem( + Long memberMissionId, + Long missionId, + String storeName, + String missionContent, + Integer rewardPoint, + MemberMissionStatus status + ) { + } + + public record HomeMissionPageItem( + Long missionId, + String storeName, + String missionContent, + Integer rewardPoint, + Float storeScore + ) { + } + + public record MemberMissionPageResponse( + List missions, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) { + } + + public record HomeMissionPageResponse( + List missions, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java index 26a61fe..41fdeaa 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.mission.exception; import com.example.umc10th.domain.mission.exception.code.MissionErrorCode; -import lombok.Getter; - -@Getter -public class MissionException extends RuntimeException { - - private final MissionErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class MissionException extends GeneralException { public MissionException(MissionErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java index 4d71999..f3087ba 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java @@ -1,16 +1,28 @@ package com.example.umc10th.domain.mission.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum MissionErrorCode { - MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404", "�̼��� ã�� �� �����ϴ�."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "���Ը� ã�� �� �����ϴ�."); +public enum MissionErrorCode implements BaseErrorCode { + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404", "Mission not found."), + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java index d9d5164..28bd763 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java @@ -1,7 +1,38 @@ package com.example.umc10th.domain.mission.repository; import com.example.umc10th.domain.mission.entity.MemberMission; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MemberMissionRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"mission", "mission.store"}) + @Query(""" + select mm + from MemberMission mm + where mm.member.id = :memberId + and mm.status = :status + order by mm.id desc + """) + Page findMemberMissionsByStatus( + @Param("memberId") Long memberId, + @Param("status") MemberMissionStatus status, + Pageable pageable + ); + + @Query(""" + select coalesce(sum(mm.mission.rewardPoint), 0) + from MemberMission mm + where mm.member.id = :memberId + and mm.status = :status + """) + Integer sumRewardPointByMemberIdAndStatus( + @Param("memberId") Long memberId, + @Param("status") MemberMissionStatus status + ); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index 71f7528..73cdabf 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -1,7 +1,33 @@ package com.example.umc10th.domain.mission.repository; import com.example.umc10th.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MissionRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select m + from Mission m + where m.store.region.id = :regionId + and ( + :memberId is null + or m.id not in ( + select mm.mission.id + from MemberMission mm + where mm.member.id = :memberId + ) + ) + order by m.id desc + """) + Page findChallengeableMissionsByRegion( + @Param("regionId") Long regionId, + @Param("memberId") Long memberId, + Pageable pageable + ); } diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index 594e659..dcf7c7f 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -2,9 +2,16 @@ import com.example.umc10th.domain.mission.dto.MissionReqDto; import com.example.umc10th.domain.mission.dto.MissionResDto; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; public interface MissionService { MissionResDto.MissionInfo getMission(Long missionId); MissionResDto.MissionInfo createMission(MissionReqDto.Create request); -} \ No newline at end of file + + MissionResDto.MemberMissionPageResponse getMemberMissions(Long memberId, MemberMissionStatus status, Integer page); + + MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long memberId, Integer page); + + MissionResDto.MemberMissionPageResponse getMyChallengingMissions(MissionReqDto.MyChallengingMissionsRequest request); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java index 42873ba..a22361f 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/mission/service/MissionServiceImpl.java @@ -3,13 +3,18 @@ import com.example.umc10th.domain.mission.converter.MissionConverter; import com.example.umc10th.domain.mission.dto.MissionReqDto; import com.example.umc10th.domain.mission.dto.MissionResDto; +import com.example.umc10th.domain.mission.entity.MemberMission; import com.example.umc10th.domain.mission.entity.Mission; +import com.example.umc10th.domain.mission.enums.MemberMissionStatus; import com.example.umc10th.domain.store.entity.Store; import com.example.umc10th.domain.mission.exception.MissionException; import com.example.umc10th.domain.mission.exception.code.MissionErrorCode; +import com.example.umc10th.domain.mission.repository.MemberMissionRepository; import com.example.umc10th.domain.mission.repository.MissionRepository; import com.example.umc10th.domain.store.repository.StoreRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +24,7 @@ public class MissionServiceImpl implements MissionService { private final MissionRepository missionRepository; + private final MemberMissionRepository memberMissionRepository; private final StoreRepository storeRepository; @Override @@ -38,4 +44,41 @@ public MissionResDto.MissionInfo createMission(MissionReqDto.Create request) { Mission savedMission = missionRepository.save(MissionConverter.toEntity(request, store)); return MissionConverter.toMissionInfo(savedMission); } -} \ No newline at end of file + + @Override + public MissionResDto.MemberMissionPageResponse getMemberMissions(Long memberId, MemberMissionStatus status, Integer page) { + int pageNumber = Math.max(page, 1) - 1; + PageRequest pageRequest = PageRequest.of(pageNumber, 10); + + Page memberMissionPage = memberMissionRepository.findMemberMissionsByStatus( + memberId, + status, + pageRequest + ); + + return MissionConverter.toMemberMissionPageResponse(memberMissionPage); + } + + @Override + public MissionResDto.HomeMissionPageResponse getHomeMissions(Long regionId, Long memberId, Integer page) { + int pageNumber = Math.max(page, 1) - 1; + PageRequest pageRequest = PageRequest.of(pageNumber, 10); + + Page missionPage = missionRepository.findChallengeableMissionsByRegion(regionId, memberId, pageRequest); + return MissionConverter.toHomeMissionPageResponse(missionPage); + } + + @Override + public MissionResDto.MemberMissionPageResponse getMyChallengingMissions(MissionReqDto.MyChallengingMissionsRequest request) { + int pageNumber = request.page() - 1; + PageRequest pageRequest = PageRequest.of(pageNumber, request.size()); + + Page memberMissionPage = memberMissionRepository.findMemberMissionsByStatus( + request.memberId(), + MemberMissionStatus.CHALLENGING, + pageRequest + ); + + return MissionConverter.toMemberMissionPageResponse(memberMissionPage); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index f3cfdfd..033d245 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -3,6 +3,7 @@ import com.example.umc10th.domain.review.dto.ReviewReqDto; import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.domain.review.service.ReviewService; +import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,12 +22,26 @@ public class ReviewController { private final ReviewService reviewService; @GetMapping("/{reviewId}") - public ResponseEntity getReview(@PathVariable Long reviewId) { - return ResponseEntity.ok(reviewService.getReview(reviewId)); + public ResponseEntity> getReview(@PathVariable Long reviewId) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.getReview(reviewId))); } @PostMapping - public ResponseEntity createReview(@Valid @RequestBody ReviewReqDto.Create request) { - return ResponseEntity.ok(reviewService.createReview(request)); + public ResponseEntity> createReview(@Valid @RequestBody ReviewReqDto.Create request) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.createReview(request))); } -} \ No newline at end of file + + @PostMapping("/my/id-cursor") + public ResponseEntity> getMyReviewsByIdCursor( + @Valid @RequestBody ReviewReqDto.MyReviewsByIdCursorRequest request + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.getMyReviewsByIdCursor(request))); + } + + @PostMapping("/my/score-cursor") + public ResponseEntity> getMyReviewsByScoreCursor( + @Valid @RequestBody ReviewReqDto.MyReviewsByScoreCursorRequest request + ) { + return ResponseEntity.ok(ApiResponse.onSuccess(reviewService.getMyReviewsByScoreCursor(request))); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index 99f6080..528a334 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -5,6 +5,7 @@ import com.example.umc10th.domain.review.dto.ReviewReqDto; import com.example.umc10th.domain.review.dto.ReviewResDto; import com.example.umc10th.domain.review.entity.Review; +import java.util.List; public final class ReviewConverter { @@ -29,4 +30,31 @@ public static ReviewResDto.ReviewInfo toReviewInfo(Review review) { review.getScore() ); } -} \ No newline at end of file + + public static ReviewResDto.MyReviewItem toMyReviewItem(Review review) { + return new ReviewResDto.MyReviewItem( + review.getId(), + review.getStore().getId(), + review.getStore().getName(), + review.getContent(), + review.getScore() + ); + } + + public static ReviewResDto.MyReviewsByIdCursorResponse toMyReviewsByIdCursorResponse( + List reviews, + Long nextCursorId, + boolean hasNext + ) { + return new ReviewResDto.MyReviewsByIdCursorResponse(reviews, nextCursorId, hasNext); + } + + public static ReviewResDto.MyReviewsByScoreCursorResponse toMyReviewsByScoreCursorResponse( + List reviews, + Float nextCursorScore, + Long nextCursorId, + boolean hasNext + ) { + return new ReviewResDto.MyReviewsByScoreCursorResponse(reviews, nextCursorScore, nextCursorId, hasNext); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java index d9b49b7..45754f5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDto.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; public class ReviewReqDto { @@ -14,4 +15,19 @@ public record Create( @Min(0) @Max(5) float score ) { } -} \ No newline at end of file + + public record MyReviewsByIdCursorRequest( + @NotNull @Positive Long memberId, + @Positive Long cursorId, + @NotNull @Min(1) @Max(50) Integer size + ) { + } + + public record MyReviewsByScoreCursorRequest( + @NotNull @Positive Long memberId, + @Min(0) @Max(5) Float cursorScore, + @Positive Long cursorId, + @NotNull @Min(1) @Max(50) Integer size + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java index 5ca1e5d..1832470 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDto.java @@ -1,5 +1,7 @@ package com.example.umc10th.domain.review.dto; +import java.util.List; + public class ReviewResDto { public record ReviewInfo( @@ -10,4 +12,28 @@ public record ReviewInfo( float score ) { } -} \ No newline at end of file + + public record MyReviewItem( + Long reviewId, + Long storeId, + String storeName, + String content, + Float score + ) { + } + + public record MyReviewsByIdCursorResponse( + List reviews, + Long nextCursorId, + Boolean hasNext + ) { + } + + public record MyReviewsByScoreCursorResponse( + List reviews, + Float nextCursorScore, + Long nextCursorId, + Boolean hasNext + ) { + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java index e4d509f..10f3491 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.review.exception; import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; -import lombok.Getter; - -@Getter -public class ReviewException extends RuntimeException { - - private final ReviewErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class ReviewException extends GeneralException { public ReviewException(ReviewErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index 84140b6..0f63a8b 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -1,17 +1,30 @@ package com.example.umc10th.domain.review.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum ReviewErrorCode { - REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "���並 ã�� �� �����ϴ�."), - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "����ڸ� ã�� �� �����ϴ�."), - STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "���Ը� ã�� �� �����ϴ�."); +public enum ReviewErrorCode implements BaseErrorCode { + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "Review not found."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "User not found."), + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."), + INVALID_CURSOR(HttpStatus.BAD_REQUEST, "REVIEW4001", "Invalid cursor request."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; -} \ No newline at end of file + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index 73627b1..7f27ce1 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -1,7 +1,65 @@ package com.example.umc10th.domain.review.repository; import com.example.umc10th.domain.review.entity.Review; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ReviewRepository extends JpaRepository { -} \ No newline at end of file + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select r + from Review r + where r.member.id = :memberId + order by r.id desc + """) + List findMyReviewsByIdCursorFirst( + @Param("memberId") Long memberId, + Pageable pageable + ); + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select r + from Review r + where r.member.id = :memberId + and r.id < :cursorId + order by r.id desc + """) + List findMyReviewsByIdCursorNext( + @Param("memberId") Long memberId, + @Param("cursorId") Long cursorId, + Pageable pageable + ); + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select r + from Review r + where r.member.id = :memberId + order by r.score desc, r.id desc + """) + List findMyReviewsByScoreCursorFirst( + @Param("memberId") Long memberId, + Pageable pageable + ); + + @EntityGraph(attributePaths = {"store"}) + @Query(""" + select r + from Review r + where r.member.id = :memberId + and (r.score < :cursorScore or (r.score = :cursorScore and r.id < :cursorId)) + order by r.score desc, r.id desc + """) + List findMyReviewsByScoreCursorNext( + @Param("memberId") Long memberId, + @Param("cursorScore") Float cursorScore, + @Param("cursorId") Long cursorId, + Pageable pageable + ); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 0d29ce7..aab5d6e 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -7,4 +7,8 @@ public interface ReviewService { ReviewResDto.ReviewInfo getReview(Long reviewId); ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request); -} \ No newline at end of file + + ReviewResDto.MyReviewsByIdCursorResponse getMyReviewsByIdCursor(ReviewReqDto.MyReviewsByIdCursorRequest request); + + ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(ReviewReqDto.MyReviewsByScoreCursorRequest request); +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java index 1cb21bb..60b2b8d 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/review/service/ReviewServiceImpl.java @@ -11,7 +11,9 @@ import com.example.umc10th.domain.review.exception.ReviewException; import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; import com.example.umc10th.domain.review.repository.ReviewRepository; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -44,4 +46,69 @@ public ReviewResDto.ReviewInfo createReview(ReviewReqDto.Create request) { Review savedReview = reviewRepository.save(ReviewConverter.toEntity(request, member, store)); return ReviewConverter.toReviewInfo(savedReview); } -} \ No newline at end of file + + @Override + public ReviewResDto.MyReviewsByIdCursorResponse getMyReviewsByIdCursor(ReviewReqDto.MyReviewsByIdCursorRequest request) { + memberRepository.findById(request.memberId()) + .orElseThrow(() -> new ReviewException(ReviewErrorCode.MEMBER_NOT_FOUND)); + + int fetchSize = request.size() + 1; + PageRequest pageRequest = PageRequest.of(0, fetchSize); + + List reviews = request.cursorId() == null + ? reviewRepository.findMyReviewsByIdCursorFirst(request.memberId(), pageRequest) + : reviewRepository.findMyReviewsByIdCursorNext(request.memberId(), request.cursorId(), pageRequest); + + boolean hasNext = reviews.size() > request.size(); + List content = hasNext ? reviews.subList(0, request.size()) : reviews; + + List items = content.stream() + .map(ReviewConverter::toMyReviewItem) + .toList(); + + Long nextCursorId = hasNext ? content.get(content.size() - 1).getId() : null; + return ReviewConverter.toMyReviewsByIdCursorResponse(items, nextCursorId, hasNext); + } + + @Override + public ReviewResDto.MyReviewsByScoreCursorResponse getMyReviewsByScoreCursor(ReviewReqDto.MyReviewsByScoreCursorRequest request) { + memberRepository.findById(request.memberId()) + .orElseThrow(() -> new ReviewException(ReviewErrorCode.MEMBER_NOT_FOUND)); + + boolean bothCursorProvided = request.cursorScore() != null && request.cursorId() != null; + boolean bothCursorEmpty = request.cursorScore() == null && request.cursorId() == null; + if (!bothCursorProvided && !bothCursorEmpty) { + throw new ReviewException(ReviewErrorCode.INVALID_CURSOR); + } + + int fetchSize = request.size() + 1; + PageRequest pageRequest = PageRequest.of(0, fetchSize); + + List reviews = bothCursorEmpty + ? reviewRepository.findMyReviewsByScoreCursorFirst(request.memberId(), pageRequest) + : reviewRepository.findMyReviewsByScoreCursorNext( + request.memberId(), + request.cursorScore(), + request.cursorId(), + pageRequest + ); + + boolean hasNext = reviews.size() > request.size(); + List content = hasNext ? reviews.subList(0, request.size()) : reviews; + + List items = content.stream() + .map(ReviewConverter::toMyReviewItem) + .toList(); + + Float nextCursorScore = null; + Long nextCursorId = null; + + if (hasNext) { + Review lastReview = content.get(content.size() - 1); + nextCursorScore = lastReview.getScore(); + nextCursorId = lastReview.getId(); + } + + return ReviewConverter.toMyReviewsByScoreCursorResponse(items, nextCursorScore, nextCursorId, hasNext); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java index 22afbfa..ed3fdb5 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java @@ -2,6 +2,7 @@ import com.example.umc10th.domain.store.dto.StoreResDto; import com.example.umc10th.domain.store.service.StoreService; +import com.example.umc10th.global.apiPayload.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,7 +18,7 @@ public class StoreController { private final StoreService storeService; @GetMapping("/{storeId}") - public ResponseEntity getStore(@PathVariable Long storeId) { - return ResponseEntity.ok(storeService.getStore(storeId)); + public ResponseEntity> getStore(@PathVariable Long storeId) { + return ResponseEntity.ok(ApiResponse.onSuccess(storeService.getStore(storeId))); } -} \ No newline at end of file +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java index 3d33f25..6574d25 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java @@ -1,15 +1,10 @@ package com.example.umc10th.domain.store.exception; import com.example.umc10th.domain.store.exception.code.StoreErrorCode; -import lombok.Getter; - -@Getter -public class StoreException extends RuntimeException { - - private final StoreErrorCode errorCode; +import com.example.umc10th.global.apiPayload.exception.GeneralException; +public class StoreException extends GeneralException { public StoreException(StoreErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; + super(errorCode); } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java index dffd006..bcac3ca 100644 --- a/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java @@ -1,15 +1,27 @@ package com.example.umc10th.domain.store.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum StoreErrorCode { +public enum StoreErrorCode implements BaseErrorCode { STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404", "Store not found."); - private final HttpStatus status; + private final HttpStatus httpStatus; private final String code; private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } } \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/test/controller/TestController.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/controller/TestController.java new file mode 100644 index 0000000..7fcc448 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/controller/TestController.java @@ -0,0 +1,20 @@ +package com.example.umc10th.domain.test.controller; + +import com.example.umc10th.domain.test.dto.TestResDto; +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.status.SuccessStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/test") +public class TestController { + + @GetMapping("/health") + public ResponseEntity> health() { + TestResDto.HealthResponse result = new TestResDto.HealthResponse("UP", "Test API is working."); + return ResponseEntity.ok(ApiResponse.of(SuccessStatus.OK, result)); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/domain/test/dto/TestResDto.java b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/dto/TestResDto.java new file mode 100644 index 0000000..bd56486 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/domain/test/dto/TestResDto.java @@ -0,0 +1,10 @@ +package com.example.umc10th.domain.test.dto; + +public class TestResDto { + + public record HealthResponse( + String status, + String message + ) { + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/ApiResponse.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/ApiResponse.java new file mode 100644 index 0000000..7912d32 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/ApiResponse.java @@ -0,0 +1,50 @@ +package com.example.umc10th.global.apiPayload; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.apiPayload.code.status.SuccessStatus; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class ApiResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + private final String code; + private final String message; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private T result; + + public static ApiResponse onSuccess(T result) { + return of(SuccessStatus.OK, result); + } + + public static ApiResponse of(BaseSuccessCode code, T result) { + return new ApiResponse<>( + true, + code.getReason().code(), + code.getReason().message(), + result + ); + } + + public static ApiResponse onFailure(BaseErrorCode code, T result) { + return new ApiResponse<>( + false, + code.getReason().code(), + code.getReason().message(), + result + ); + } + + public static ApiResponse onFailure(String code, String message, T result) { + return new ApiResponse<>(false, code, message, result); + } +} diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseCode.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseCode.java new file mode 100644 index 0000000..07573cd --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseCode.java @@ -0,0 +1,9 @@ +package com.example.umc10th.global.apiPayload.code; + +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; + +public interface BaseCode { + ReasonDto getReason(); + + ReasonDto getReasonHttpStatus(); +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseErrorCode.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseErrorCode.java new file mode 100644 index 0000000..da096a5 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseErrorCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th.global.apiPayload.code; + +public interface BaseErrorCode extends BaseCode { +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseSuccessCode.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseSuccessCode.java new file mode 100644 index 0000000..35b18d3 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/BaseSuccessCode.java @@ -0,0 +1,4 @@ +package com.example.umc10th.global.apiPayload.code; + +public interface BaseSuccessCode extends BaseCode { +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/dto/ReasonDto.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/dto/ReasonDto.java new file mode 100644 index 0000000..92b3568 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/dto/ReasonDto.java @@ -0,0 +1,11 @@ +package com.example.umc10th.global.apiPayload.code.dto; + +import org.springframework.http.HttpStatus; + +public record ReasonDto( + HttpStatus httpStatus, + boolean isSuccess, + String code, + String message +) { +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java new file mode 100644 index 0000000..202e2eb --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/ErrorStatus.java @@ -0,0 +1,29 @@ +package com.example.umc10th.global.apiPayload.code.status; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "Server error."), + BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "Bad request."), + VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "COMMON4001", "Validation failed."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, false, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/SuccessStatus.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/SuccessStatus.java new file mode 100644 index 0000000..75662f9 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/code/status/SuccessStatus.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.apiPayload.code.status; + +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum SuccessStatus implements BaseSuccessCode { + OK(HttpStatus.OK, "COMMON200", "Success"); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDto getReason() { + return new ReasonDto(httpStatus, true, code, message); + } + + @Override + public ReasonDto getReasonHttpStatus() { + return getReason(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralException.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralException.java new file mode 100644 index 0000000..2021214 --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralException.java @@ -0,0 +1,21 @@ +package com.example.umc10th.global.apiPayload.exception; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class GeneralException extends RuntimeException { + + private final BaseErrorCode code; + + public ReasonDto getErrorReason() { + return code.getReason(); + } + + public ReasonDto getErrorReasonHttpStatus() { + return code.getReasonHttpStatus(); + } +} \ No newline at end of file diff --git a/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionAdvice.java b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionAdvice.java new file mode 100644 index 0000000..9ff7c5a --- /dev/null +++ b/bean/umc10th/src/main/java/com/example/umc10th/global/apiPayload/exception/GeneralExceptionAdvice.java @@ -0,0 +1,64 @@ +package com.example.umc10th.global.apiPayload.exception; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.dto.ReasonDto; +import com.example.umc10th.global.apiPayload.code.status.ErrorStatus; +import jakarta.validation.ConstraintViolationException; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GeneralExceptionAdvice { + + @ExceptionHandler(GeneralException.class) + public ResponseEntity> handleGeneralException(GeneralException exception) { + ReasonDto reason = exception.getErrorReasonHttpStatus(); + return ResponseEntity + .status(reason.httpStatus()) + .body(ApiResponse.onFailure(reason.code(), reason.message(), null)); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { + Map errors = new LinkedHashMap<>(); + + for (FieldError fieldError : exception.getBindingResult().getFieldErrors()) { + errors.put(fieldError.getField(), fieldError.getDefaultMessage()); + } + + ErrorStatus status = ErrorStatus.VALIDATION_ERROR; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), errors)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolationException(ConstraintViolationException exception) { + ErrorStatus status = ErrorStatus.BAD_REQUEST; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), exception.getMessage())); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) { + ErrorStatus status = ErrorStatus.BAD_REQUEST; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), "Request body is invalid.")); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception exception) { + ErrorStatus status = ErrorStatus.INTERNAL_SERVER_ERROR; + return ResponseEntity + .status(status.getHttpStatus()) + .body(ApiResponse.onFailure(status.getCode(), status.getMessage(), exception.getMessage())); + } +}