From 9b52122c3fa1c14c123d6f1af3df6da880a529e2 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Fri, 8 May 2026 13:42:29 +0900 Subject: [PATCH 01/71] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20API=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20REST=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 23 ++++++--------- .../review/converter/ReviewConverter.java | 21 +++++--------- .../domain/review/service/ReviewService.java | 29 +++++++++---------- 3 files changed, 30 insertions(+), 43 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index bb7ae77d..0e147ba8 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -3,8 +3,10 @@ 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 com.example.umc10th.domain.review.exception.code.ReviewSuccessCode; import com.example.umc10th.domain.review.service.ReviewService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -13,26 +15,19 @@ import java.util.List; @RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/reviews") +@RequiredArgsConstructor // final이 붙은거 생성자 대신 작성 +@RequestMapping("/api/v1") public class ReviewController { private final ReviewService reviewService; // 특정 가게에 리뷰 등록 - @PostMapping("/stores/{storeId}") + @PostMapping("/stores/{storeId}/reviews") public ApiResponse createReview( - @PathVariable Long storeId, - @RequestBody ReviewReqDTO.CreateReview request + @PathVariable Long storeId, // {storeId}에 적힌 숫자를 storeId라는 변수에 담음 + @RequestBody ReviewReqDTO.CreateReview dto // 사용자가 작성한 리뷰 정보 가져옴 ) { - return ApiResponse.onSuccess(MemberSuccessCode.OK, reviewService.createReview(storeId, request)); + BaseSuccessCode code=ReviewSuccessCode.OK; + return ApiResponse.onSuccess(code, reviewService.createReviewResult(storeId,dto)); } - // 특정 가게의 리뷰 목록 조회(페이징 포함) - @GetMapping("/stores/{storeId}") - public ApiResponse>getStoreReviews( - @PathVariable Long storeId, - @RequestParam(name="page",defaultValue = "0")Integer page - ){ - return ApiResponse.onSuccess(MemberSuccessCode.OK, reviewService.getReviewList(storeId, page)); - } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index 63a5f1dc..3c1e6353 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -7,29 +7,22 @@ import com.example.umc10th.domain.store.entity.Store; public class ReviewConverter { - public static Review toReview(ReviewReqDTO.CreateReview request, Member member, Store store) { + + // 사용자가 보낸 DTO를 DB에 저장할 엔티티로 (조회가 아닌 생성(작성)이라 추가됨) + public static Review toReview(ReviewReqDTO.CreateReview request,Member member,Store store){ return Review.builder() .rating(request.rating()) .content(request.content()) - .member(member) - .store(store) + .member(member) // 연관관계 매핑 + .store(store) // 연관관계 매핑 .build(); } + // 저장된 결과를 DTO로 포장 public static ReviewResDTO.CreateReviewResult toCreateReviewResult(Review review) { return ReviewResDTO.CreateReviewResult.builder() - .reviewId(review.getId()) + .id(review.getId()) .createdAt(review.getCreatedAt()) .build(); } - - public static ReviewResDTO.ReviewDetail toReviewSummaryDTO(Review review) { - return ReviewResDTO.ReviewDetail.builder() - .reviewId(review.getId()) - .writerName(review.getMember().getName()) // 엔티티 그래프에서 이름만 추출 - .rating(review.getRating()) - .content(review.getContent()) - .createdAt(review.getCreatedAt().toLocalDate()) // 날짜 형식 정제 - .build(); - } } \ No newline at end of file diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 33a910ed..a833141b 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -1,11 +1,18 @@ package com.example.umc10th.domain.review.service; +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.dto.MemberReqDTO; +import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; +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.review.converter.ReviewConverter; 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 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 com.example.umc10th.domain.store.entity.Store; import com.example.umc10th.domain.store.repository.StoreRepository; @@ -25,22 +32,14 @@ public class ReviewService { private final MemberRepository memberRepository; private final StoreRepository storeRepository; + // 리뷰 작성 @Transactional - public ReviewResDTO.CreateReviewResult createReview(Long storeId, ReviewReqDTO.CreateReview request) { - Member member = memberRepository.findById(request.memberId()).orElseThrow(); - Store store = storeRepository.findById(storeId).orElseThrow(); - - Review review = ReviewConverter.toReview(request, member, store); - return ReviewConverter.toCreateReviewResult(reviewRepository.save(review)); + public ReviewResDTO.CreateReviewResult createReviewResult(Long storeId, ReviewReqDTO.CreateReview dto) { + // DB에서 해당 가게 ID로 데이터 조회 + Review review=reviewRepository.findById(storeId) + .orElseThrow(()-> new ReviewException(ReviewErrorCode.REVIEW_NOT_CREATED)); + // 컨버터를 이용해서 응답 DTO 생성 & return + return ReviewConverter.toCreateReviewResult(review); } - // 특정 가게의 리뷰 목록 조회(페이징) - public List getReviewList(Long storeId, Integer page) { - Store store = storeRepository.findById(storeId).orElseThrow(); - - // 1. 일단 페이지로 가져옵니다. - Page reviewPage = reviewRepository.findAllByStoreId(storeId, PageRequest.of(page, 10)); - // 2. DTO로 변환한 뒤, .toList()를 써서 리스트로만 뽑아냅니다. - return reviewPage.map(ReviewConverter::toReviewSummaryDTO).getContent(); - } } From 077b7611231da561e6e01de8a16feb616827f822 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Fri, 8 May 2026 13:42:47 +0900 Subject: [PATCH 02/71] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=A0=84=EC=9A=A9=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/exception/ReviewException.java | 12 ++++++++++-- .../review/exception/code/ReviewErrorCode.java | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java index 1664f68a..2560e32c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/ReviewException.java @@ -1,7 +1,15 @@ 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 { - public ReviewException(String message) { - super(message); + // 에러 정보를 저장할 필드 추가 + private final ReviewErrorCode errorCode; + public ReviewException(ReviewErrorCode errorCode) { + // 부모 RuntimeException에 에러 메세지 전달 + super(errorCode.getMessage()); + this.errorCode=errorCode; } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index 84ca45f9..f92775e5 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -7,7 +7,7 @@ @Getter @RequiredArgsConstructor public enum ReviewErrorCode { - REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 리뷰를 찾을 수 없습니다."); + REVIEW_NOT_CREATED(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 리뷰를 찾을 수 없습니다."); private final HttpStatus status; private final String code; From 7a988ceca8b423ba6131cb732a8fe9d1295cc3ea Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Fri, 8 May 2026 13:43:05 +0900 Subject: [PATCH 03/71] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20DTO=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/review/dto/ReviewReqDTO.java | 6 ++++-- .../umc10th/domain/review/dto/ReviewResDTO.java | 11 +---------- .../domain/review/repository/ReviewRepository.java | 3 --- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDTO.java index af04ced1..178c6b17 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewReqDTO.java @@ -1,12 +1,14 @@ package com.example.umc10th.domain.review.dto; +import lombok.Builder; + import java.util.List; public class ReviewReqDTO { + @Builder public record CreateReview( Long memberId, Float rating, - String content, - List imageUrls + String content ){} } \ No newline at end of file diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java index 4d949b26..898ec4af 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java @@ -8,16 +8,7 @@ public class ReviewResDTO { @Builder public record CreateReviewResult( - Long reviewId, + Long id, LocalDateTime createdAt ){} - - @Builder - public record ReviewDetail( - Long reviewId, - String writerName, - Float rating, - String content, - LocalDate createdAt - ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index c6a8bdaa..5ebb9c45 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -7,7 +7,4 @@ import org.springframework.data.jpa.repository.Query; public interface ReviewRepository extends JpaRepository { - // 특정 가게의 리뷰 목록 조회 - @Query("SELECT r FROM Review r WHERE r.store.id = :storeId") - Page findAllByStoreId(Long storeId, Pageable pageable); } From e6eab26f83313abb1efcfa95d1a664305d73a90f Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:44:27 +0900 Subject: [PATCH 04/71] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20URI=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95=20(/u?= =?UTF-8?q?sers=20->=20/members)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/controller/MemberController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 27f8def2..eaa23a2c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -20,7 +20,7 @@ public class MemberController { private final MemberService memberService; // 마이페이지 - @PostMapping("/v1/users/me") + @PostMapping("/v1/members/me") public ApiResponse getInfo( // 받은 JSON 데이터를 자바 객체(dto)로 변환해서 씀 @RequestBody MemberReqDTO.GetInfo dto From 49e76eab8e4143870c454521d191361eee7a020a Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:45:08 +0900 Subject: [PATCH 05/71] =?UTF-8?q?feat:=20MemberMission=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20(CHALLENG?= =?UTF-8?q?ING=20->=20READY)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/entity/mapping/MemberMission.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/mapping/MemberMission.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/mapping/MemberMission.java index e9ab0d03..86e2bc97 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/mapping/MemberMission.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/mapping/MemberMission.java @@ -18,7 +18,7 @@ public class MemberMission extends BaseEntity { private Long id; @Enumerated(EnumType.STRING) - @Column(columnDefinition = "VARCHAR(15) DEFAULT 'CHALLENGING'") + @Column(columnDefinition = "VARCHAR(15) DEFAULT 'READY'") private MissionStatus status; @ManyToOne(fetch = FetchType.LAZY) From 0dbea2d73e299723200b32c976e627f3c84aff2e Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:45:54 +0900 Subject: [PATCH 06/71] =?UTF-8?q?feat:=20MemberSuccessCode=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/code/MissionSuccessCode.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java index a30e4ea2..4283e290 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java @@ -1,4 +1,19 @@ package com.example.umc10th.domain.mission.exception.code; -public enum MissionSuccessCode { -} +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum MissionSuccessCode implements BaseSuccessCode { + + OK(HttpStatus.OK, + "MISSION200_1", + "성공적으로 미션이 조회되었습니다."), + ; + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file From 96d6a9d47499501b71e41dd0717c8d90a194fe23 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:46:52 +0900 Subject: [PATCH 07/71] =?UTF-8?q?refactor:=20Fetch=20Join=EC=9D=84=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EB=AF=B8=EC=85=98=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/repository/MemberMissionRepository.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java index 5674ec57..0a0f15e3 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MemberMissionRepository.java @@ -1,14 +1,22 @@ package com.example.umc10th.domain.mission.repository; +import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.enums.MissionStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; 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 { // 특정 유저의 상태별 미션 목록 조회 (페이징 포함) - @Query("SELECT mm FROM MemberMission mm JOIN FETCH mm.mission WHERE mm.member.id = :memberId AND mm.status = :status") - Page findAllByMemberIdAndStatus(Long memberId, MissionStatus status, Pageable pageable); + @Query("SELECT mm FROM MemberMission mm " + + "JOIN FETCH mm.mission m "+ + "JOIN FETCH m.store s "+ + "WHERE mm.member=:member AND mm.status = :status") + Page findAllByMemberIdAndStatus( + @Param("member")Member member, + @Param("status")MissionStatus status, + Pageable pageable); } From f77f6f3f42efae248b182cc5b92f3205973706cd Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:47:35 +0900 Subject: [PATCH 08/71] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95?= =?UTF-8?q?=EB=B9=84=20=EB=B0=8F=20DTO=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/controller/MissionController.java | 30 ++++------------ .../domain/mission/dto/MissionReqDTO.java | 14 ++++++-- .../domain/mission/dto/MissionResDTO.java | 35 +++++++++++++------ 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 7323f9be..2ed45099 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -5,6 +5,7 @@ import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.enums.MissionStatus; +import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; @@ -16,35 +17,18 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/missions") +@RequestMapping("/api/v1/members") public class MissionController { private final MissionService missionService; - // 미션 도전하기 API - @PostMapping("/{missionId}/challenge") - public ApiResponse challengeMission( - @PathVariable Long missionId, - @RequestBody MissionReqDTO.ChallengeDTO request - ){ - return ApiResponse.onSuccess(MemberSuccessCode.OK,missionService.challengeMission(missionId, request)); - } - // 내가 진행중이거나 완료한 미션 목록 조회(페이징 포함) - @GetMapping("/members/{memberId}") - public ApiResponse> getMemberMissions( + @GetMapping("/{memberId}/missions") + public ApiResponse getMemberMissions( @PathVariable Long memberId, - @RequestParam(name="status") MissionStatus status, - @RequestParam(name="page",defaultValue="0")Integer page + @ModelAttribute MissionReqDTO.MissionListReqDTO dto ){ - return ApiResponse.onSuccess(MemberSuccessCode.OK, missionService.getMemberMissionList(memberId, status, page)); + // 서비스에서 데이터 가져오기 + return ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(memberId, dto)); } - // 특정 지역의 도전 가능한 미션 목록 조회(페이징 포함) - @GetMapping("/regions/{regionId}") - public ApiResponse> getRegionMissions( - @PathVariable Long regionId, - @RequestParam(name = "page", defaultValue = "0") Integer page - ) { - return ApiResponse.onSuccess(MemberSuccessCode.OK, missionService.getMissionListByRegion(regionId, page)); - } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index 0f593309..3316d66e 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -1,8 +1,16 @@ package com.example.umc10th.domain.mission.dto; +import com.example.umc10th.domain.mission.enums.MissionStatus; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + + public class MissionReqDTO { - // 미션 도전하기 요청 - public record ChallengeDTO( - Long memberId + // 미션 목록 조회 + @Builder + public record MissionListReqDTO( + MissionStatus status, + Integer page ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index a7f80454..2f2813f7 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -1,22 +1,37 @@ package com.example.umc10th.domain.mission.dto; +import com.example.umc10th.domain.mission.enums.MissionStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; public class MissionResDTO { + // 미션 목록 @Builder - public record MissionChallengeResult( - Long memberMissionId, - LocalDateTime createdAt + public record MissionListResDTO( + List missionList, + Integer listSize, // 데이터 몇개 불러오는지 + Integer totalPage, // 전체 페이지 + Long totalElements, // 전체 미션 개수 + Boolean isFirst, // 첫 페이지인지 + Boolean isLast // 마지막 페이지인지 ){} - + // 미션 정보 @Builder - public record MissionSummaryDTO( - Long missionId, - String storeName, // 어떤 가게의 미션인지 - Integer rewardPoint, // 보상 포인트 - String content, // 미션 내용 - String status // 현재 상태 (CHALLENGING 등) + public record MissionDetailDTO( + @Schema(description = "가게 이름", example = "요아정") + String storeName, // 가게 이름 + @Schema(description = "미션 보상 포인트", example = "500") + Integer reward, // 보상 + @Schema(description = "미션 내용", example = "요거트 아이스크림 1개 먹기") + String missionSpec, // 미션 내용 + @Schema(description = "미션 상태", example = "CHALLENGING") + MissionStatus status // 미션 상태 ){} } From c46a603adeeddfa8bd6b8ff19ce15a31ddbeb416 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:48:11 +0900 Subject: [PATCH 09/71] =?UTF-8?q?refactor:=20MissionConverter=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/converter/MissionConverter.java | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 33997715..3ffe560a 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -5,41 +5,38 @@ import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.enums.MissionStatus; +import org.springframework.data.domain.Page; + +import java.util.List; +import java.util.stream.Collectors; public class MissionConverter { - public static MemberMission toMemberMission(Member member, Mission mission) { - return MemberMission.builder() - .member(member) - .mission(mission) - .status(MissionStatus.CHALLENGING) - .build(); - } - public static MissionResDTO.MissionChallengeResult toMissionChallengeResult(MemberMission memberMission) { - return MissionResDTO.MissionChallengeResult.builder() - .memberMissionId(memberMission.getId()) - .createdAt(memberMission.getCreatedAt()) + // 개별 미션 엔티티 -> 상세 DTO + public static MissionResDTO.MissionDetailDTO toMissionDetailDTO(MemberMission memberMission){ + return MissionResDTO.MissionDetailDTO.builder() + .storeName(memberMission.getMission().getStore().getName()) + .reward(memberMission.getMission().getRewardPoint()) + .missionSpec(memberMission.getMission().getContent()) + .status(memberMission.getStatus()) .build(); } - // 내가 진행 중인 미션 변환 (MemberMission -> DTO) - public static MissionResDTO.MissionSummaryDTO toMissionSummaryDTO(MemberMission mm) { - return MissionResDTO.MissionSummaryDTO.builder() - .missionId(mm.getMission().getId()) - .storeName(mm.getMission().getStore().getName()) - .rewardPoint(mm.getMission().getRewardPoint()) - .content(mm.getMission().getContent()) - .status(mm.getStatus().name()) - .build(); - } - // 지역별 도전 가능 미션 변환 (Mission -> DTO) - public static MissionResDTO.MissionSummaryDTO toMissionSummaryDTO(Mission mission) { - return MissionResDTO.MissionSummaryDTO.builder() - .missionId(mission.getId()) - .storeName(mission.getStore().getName()) - .rewardPoint(mission.getRewardPoint()) - .content(mission.getContent()) - .status("CHALLENGE_AVAILABLE") // 도전 가능 상태를 나타내는 임의의 문자열 + // Page 전체 목록 DTO + public static MissionResDTO.MissionListResDTO toMissionListDTO(Page memberMissionPage) { + // 상세 리스트로 변환 + List missionDetailDTOList= memberMissionPage.getContent().stream() + .map(MissionConverter::toMissionDetailDTO) + .collect(Collectors.toList()); + + // 페이징 정보와 함께 응답 객체 생성 + return MissionResDTO.MissionListResDTO.builder() + .missionList(missionDetailDTOList) + .listSize(missionDetailDTOList.size()) + .totalPage(memberMissionPage.getTotalPages()) + .totalElements(memberMissionPage.getTotalElements()) + .isFirst(memberMissionPage.isFirst()) + .isLast(memberMissionPage.isLast()) .build(); } } \ No newline at end of file From 35f9829c801297538b8b109c2691e4b87030fa13 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:48:27 +0900 Subject: [PATCH 10/71] =?UTF-8?q?refactor:=20MissionRepository=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/repository/MissionRepository.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index b49e0b6a..11dcd234 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -7,7 +7,5 @@ import org.springframework.data.jpa.repository.Query; public interface MissionRepository extends JpaRepository { - // 특정 지역(Region)의 가게들에 걸린 미션 목록 조회 - @Query("SELECT m FROM Mission m JOIN m.store s WHERE s.region.id = :regionId") - Page findAllByRegionId(Long regionId, Pageable pageable); + } \ No newline at end of file From 3f8255e1fcc003952dc79d057f2550f311a0d1a0 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:49:14 +0900 Subject: [PATCH 11/71] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20DTO=20=EB=A7=A4=ED=95=91=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/service/MissionService.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index c56dfdef..da02312b 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -1,6 +1,9 @@ package com.example.umc10th.domain.mission.service; +import com.example.umc10th.domain.member.converter.MemberConverter; import com.example.umc10th.domain.member.entity.Member; +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.converter.MissionConverter; import com.example.umc10th.domain.mission.dto.MissionReqDTO; @@ -20,27 +23,25 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class MissionService { - private final MissionRepository missionRepository; private final MemberMissionRepository memberMissionRepository; private final MemberRepository memberRepository; - @Transactional - public MissionResDTO.MissionChallengeResult challengeMission(Long missionId, MissionReqDTO.ChallengeDTO request) { - Member member = memberRepository.findById(request.memberId()).orElseThrow(); - Mission mission = missionRepository.findById(missionId).orElseThrow(); + public MissionResDTO.MissionListResDTO getMemberMissionList(Long memberId, MissionReqDTO.MissionListReqDTO dto) { - // 도전 테이블 데이터 생성 - MemberMission memberMission = MissionConverter.toMemberMission(member, mission); - return MissionConverter.toMissionChallengeResult(memberMissionRepository.save(memberMission)); - } + // DB에서 해당 유저 ID로 데이터 조회 + Member member=memberRepository.findById(memberId) + .orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + // 몇 번재 페이지를, 몇 개씩 가져올지 + PageRequest pageRequest=PageRequest.of(dto.page(),10); - public Page getMemberMissionList(Long memberId, MissionStatus status, Integer page) { - Page memberMissions = memberMissionRepository.findAllByMemberIdAndStatus(memberId, status, PageRequest.of(page, 10)); - return memberMissions.map(MissionConverter::toMissionSummaryDTO); // 변환 적용 + // DB에서 해당 멤버와 상태가 일치하는 데이터를 페이징해서 가져옴 + Page memberMissionPage=memberMissionRepository.findAllByMemberIdAndStatus( + member, + dto.status(), // 진행중/완료 선택한 데이터 + pageRequest + ); + // 컨버터를 이용해서 응답 DTO 생성 & return + return MissionConverter.toMissionListDTO(memberMissionPage); } - - public Page getMissionListByRegion(Long regionId, Integer page) { - Page missions = missionRepository.findAllByRegionId(regionId, PageRequest.of(page, 10)); - return missions.map(MissionConverter::toMissionSummaryDTO); // 변환 적용 } -} + From 7f56b9bbab5e56a0970333e3e1353f66b13ec3e2 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 19:49:35 +0900 Subject: [PATCH 12/71] =?UTF-8?q?feat:=20READY=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/umc10th/domain/mission/enums/MissionStatus.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/enums/MissionStatus.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/enums/MissionStatus.java index 35678025..ab8c2515 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/enums/MissionStatus.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/enums/MissionStatus.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.mission.enums; public enum MissionStatus { + READY, // 시작 전 CHALLENGING, // 도전 중 COMPLETE, // 완료 FAILED // 실패 From 2d78e66f9696d732338f8980d7f140936a543d59 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:26:07 +0900 Subject: [PATCH 13/71] =?UTF-8?q?feat:=20status=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/umc10th/domain/mission/entity/Mission.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java index c76560bb..5bbea620 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.mission.entity; import com.example.umc10th.domain.BaseEntity; +import com.example.umc10th.domain.mission.enums.MissionStatus; import com.example.umc10th.domain.store.entity.Store; import jakarta.persistence.*; import lombok.*; @@ -24,4 +25,8 @@ public class Mission extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "store_id") private Store store; + + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "VARCHAR(15) DEFAULT 'READY'") + private MissionStatus status; } From ddc4cb2ea965b2f2e0a92ee6c0b11e76f96bfc50 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:26:24 +0900 Subject: [PATCH 14/71] =?UTF-8?q?refactor:=20uri=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/controller/MissionController.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 2ed45099..c2fea6b8 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -17,18 +17,28 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/members") +@RequestMapping("/api/v1") public class MissionController { private final MissionService missionService; // 내가 진행중이거나 완료한 미션 목록 조회(페이징 포함) - @GetMapping("/{memberId}/missions") + @GetMapping("/members/{memberId}/missions") public ApiResponse getMemberMissions( @PathVariable Long memberId, - @ModelAttribute MissionReqDTO.MissionListReqDTO dto + @ModelAttribute MissionReqDTO.MissionListReqDTO dto // 데이터가 많을때 ){ // 서비스에서 데이터 가져오기 return ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(memberId, dto)); } + // 현재 선택된 지역에서 도전 가능한 미션 목록(페이징 포함) + @GetMapping("/regions/{regionId}/missions") + public ApiResponsegetRegionMissions( + @PathVariable Long regionId, + @ModelAttribute MissionReqDTO.MyMissionReqDTO dto + ){ + return ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getRegionMissionList(regionId,dto)); + } + + } From fec22c235adce869d05b2ded2f84965225fd6aa8 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:26:50 +0900 Subject: [PATCH 15/71] =?UTF-8?q?refactor:=20=EC=A7=80=EC=97=AD=EB=B3=84?= =?UTF-8?q?=20=EB=AF=B8=EC=85=98=20=EC=BB=A8=EB=B2=84=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/converter/MissionConverter.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 3ffe560a..457c74da 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -39,4 +39,30 @@ public static MissionResDTO.MissionListResDTO toMissionListDTO(Page DTO + public static MissionResDTO.MissionDetailDTO toMissionDetailDTO(Mission mission){ + return MissionResDTO.MissionDetailDTO.builder() + .storeName(mission.getStore().getName()) + .reward(mission.getRewardPoint()) + .missionSpec(mission.getContent()) + .status(mission.getStatus()) + .build(); + } + + // 지역별 미션 목록 변환 + public static MissionResDTO.RegionMissionListDTO toRegionMissionListDTO(Page missionPage) { + List missionDetailDTOList=missionPage.getContent().stream() + .map(MissionConverter::toMissionDetailDTO) + .collect(Collectors.toList()); + + return MissionResDTO.RegionMissionListDTO.builder() + .missionList(missionDetailDTOList) + .listSize(missionDetailDTOList.size()) + .totalPage(missionPage.getTotalPages()) + .totalElements(missionPage.getTotalElements()) + .isFirst(missionPage.isFirst()) + .isLast(missionPage.isLast()) + .build(); + } } \ No newline at end of file From 18d357d42211c4f3b324a52f2670dc8c80b173ee Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:27:07 +0900 Subject: [PATCH 16/71] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/code/MissionErrorCode.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java index ee791914..ebc858cb 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java @@ -1,4 +1,21 @@ package com.example.umc10th.domain.mission.exception.code; -public enum MissionErrorCode { -} +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + + +@Getter +@RequiredArgsConstructor +public enum MissionErrorCode implements BaseErrorCode { + + MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, + "MISSION404_1", + "미션 조회에 실패했습니다."), + ; + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file From 7cd0b0ea96d6e6120b44bc1bcfb029c70c1b4b6e Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:27:27 +0900 Subject: [PATCH 17/71] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20String=EC=9D=B4=20=EC=95=84=EB=8B=8C=20code?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/exception/MissionException.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java index d01099f5..b1e6b504 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/MissionException.java @@ -1,7 +1,19 @@ package com.example.umc10th.domain.mission.exception; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.mission.exception.code.MissionErrorCode; +import lombok.Getter; + +@Getter public class MissionException extends RuntimeException { - public MissionException(String message) { - super(message); + + // 에러 정보를 저장할 필드 추가 + private final MissionErrorCode errorCode; + + // MemberError를 인자로 받는 생성자 추가 + public MissionException(MissionErrorCode errorCode) { + // 부모 클래스(RuntimeException)에게 에러 메시지를 전달 + super(errorCode.getMessage()); + this.errorCode=errorCode; } } From ccaab387214a2eb20e30dda4693cc4d6580de098 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:27:44 +0900 Subject: [PATCH 18/71] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/repository/MissionRepository.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index 11dcd234..2eab2611 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -5,7 +5,14 @@ import org.springframework.data.domain.Pageable; 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 { + @Query("SELECT m From Mission m "+ + "JOIN FETCH m.store s "+ + "JOIN s.region r "+ + "WHERE r.id=:regionId "+ + "AND m.status='READY'") + Page findAllByRegionId(@Param("regionId")Long regionId, Pageable pageable); } \ No newline at end of file From 860afbf6192e0ac7ae42b04334d7bc2a4b719a65 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:28:18 +0900 Subject: [PATCH 19/71] =?UTF-8?q?refactor:=20dto=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/dto/MissionReqDTO.java | 7 +++++++ .../umc10th/domain/mission/dto/MissionResDTO.java | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index 3316d66e..499a2ba0 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -13,4 +13,11 @@ public record MissionListReqDTO( MissionStatus status, Integer page ){} + + // 홈 화면 가능 미션 목록 + @Builder + public record MyMissionReqDTO( + Long regionId, + Integer page + ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index 2f2813f7..1788f2ea 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -34,4 +34,14 @@ public record MissionDetailDTO( @Schema(description = "미션 상태", example = "CHALLENGING") MissionStatus status // 미션 상태 ){} + + @Builder + public record RegionMissionListDTO( + List missionList, + Integer listSize, + Integer totalPage, + Long totalElements, + Boolean isFirst, + Boolean isLast + ){} } From ffec1bc6fd8214aa17c12bebe471e7ee95acd8b4 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:28:34 +0900 Subject: [PATCH 20/71] =?UTF-8?q?feat:=20region=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/store/repository/RegionRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/domain/store/repository/RegionRepository.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/repository/RegionRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/repository/RegionRepository.java new file mode 100644 index 00000000..83f12fbc --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/repository/RegionRepository.java @@ -0,0 +1,9 @@ +package com.example.umc10th.domain.store.repository; + +import com.example.umc10th.domain.mission.entity.Mission; +import com.example.umc10th.domain.store.entity.Region; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RegionRepository extends JpaRepository { + +} \ No newline at end of file From b66bd8f3ac4b32e23901671e5e5a6548676545d6 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Sun, 10 May 2026 22:29:01 +0900 Subject: [PATCH 21/71] =?UTF-8?q?feat:=20=EC=A7=80=EC=97=AD=EB=B3=84=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/service/MissionService.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index da02312b..02e447fc 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -11,8 +11,14 @@ import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.enums.MissionStatus; +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.entity.Region; +import com.example.umc10th.domain.store.exception.StoreException; +import com.example.umc10th.domain.store.exception.code.StoreErrorCode; +import com.example.umc10th.domain.store.repository.RegionRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -25,14 +31,15 @@ public class MissionService { private final MemberMissionRepository memberMissionRepository; private final MemberRepository memberRepository; - + private final RegionRepository regionRepository; + private final MissionRepository missionRepository; public MissionResDTO.MissionListResDTO getMemberMissionList(Long memberId, MissionReqDTO.MissionListReqDTO dto) { // DB에서 해당 유저 ID로 데이터 조회 Member member=memberRepository.findById(memberId) .orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); // 몇 번재 페이지를, 몇 개씩 가져올지 - PageRequest pageRequest=PageRequest.of(dto.page(),10); + PageRequest pageRequest=PageRequest.of(dto.page(),3); // DB에서 해당 멤버와 상태가 일치하는 데이터를 페이징해서 가져옴 Page memberMissionPage=memberMissionRepository.findAllByMemberIdAndStatus( @@ -43,5 +50,21 @@ public MissionResDTO.MissionListResDTO getMemberMissionList(Long memberId, Missi // 컨버터를 이용해서 응답 DTO 생성 & return return MissionConverter.toMissionListDTO(memberMissionPage); } + + public MissionResDTO.RegionMissionListDTO getRegionMissionList(Long regionId, MissionReqDTO.MyMissionReqDTO dto) { + + // 지역 존재 확인 + Region region=regionRepository.findById(regionId) + .orElseThrow(()->new StoreException(StoreErrorCode.REGION_NOT_FOUND)); + + // 페이징 + PageRequest pageRequest=PageRequest.of(dto.page(),3); + + // 리포지토리에서 지역 id에 해당하는 미션 조회 + Page missionPage=missionRepository.findAllByRegionId(regionId,pageRequest); + + // 컨버터를 이용해서 응답 dto로 변환 후 반환 + return MissionConverter.toRegionMissionListDTO(missionPage); } +} From d3b01d6442c0c80bd2ad68c445db07219ddb45ae Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 15:15:33 +0900 Subject: [PATCH 22/71] =?UTF-8?q?feat:=20Enum=20=EC=9E=84=EC=9D=98=20?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/enums/Address.java | 3 +++ .../com/example/umc10th/domain/member/enums/FoodName.java | 4 ++++ .../java/com/example/umc10th/domain/member/enums/Gender.java | 3 +++ 3 files changed, 10 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Address.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Address.java index cba8a4a5..ae9d6dac 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Address.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Address.java @@ -1,4 +1,7 @@ package com.example.umc10th.domain.member.enums; public enum Address { + SEOUL, + GYEONGGI, + INCHEON } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/FoodName.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/FoodName.java index 3f347f58..316a4468 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/FoodName.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/FoodName.java @@ -1,4 +1,8 @@ package com.example.umc10th.domain.member.enums; public enum FoodName { + KOREAN, + JAPANESE, + CHINESE, + WESTERN } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Gender.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Gender.java index 729751df..a719d9f2 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Gender.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/Gender.java @@ -1,4 +1,7 @@ package com.example.umc10th.domain.member.enums; public enum Gender { + MALE, + FEMALE, + NONE } From ad67d1fa2889cdefcf09bf9462d4ca36b42e97eb Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 17:37:35 +0900 Subject: [PATCH 23/71] =?UTF-8?q?refactor:=20GeneratedValue=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/entity/Member.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java index c50dc9cc..063d5939 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -21,7 +21,7 @@ public class Member extends BaseEntity { @Id - @GeneratedValue(strategy= GenerationType.SEQUENCE) + @GeneratedValue(strategy= GenerationType.IDENTITY) private Long id; @Column(name="name", nullable=false) From 71877908f36a087acd6d116284adb730b24e08a8 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 17:38:07 +0900 Subject: [PATCH 24/71] =?UTF-8?q?feat:=20@Valid=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EA=B2=80=EC=A6=9D=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GeneralExceptionAdvice.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java b/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java index 3de86d0d..1eeb0942 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -5,9 +5,13 @@ import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; import com.example.umc10th.global.apiPayload.exception.ProjectException; 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 { @@ -33,4 +37,19 @@ public ResponseEntity> handleException( ex.getMessage() )); } + // @Valid 어노테이션 검증 실패 예외 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleMethodArgumentNotValidException( + MethodArgumentNotValidException e + ){ + // 검증 실패한 변수명과 실패 이유를 담을 Map + Map errors = new HashMap<>(); + e.getBindingResult().getFieldErrors().forEach(error -> { + errors.put(error.getField(), error.getDefaultMessage()); + }); + + BaseErrorCode code = GeneralErrorCode.BAD_REQUEST; + return ResponseEntity.status(code.getStatus()) + .body(ApiResponse.onFailure(code, errors)); + } } From 89605d35d29736cfc87deedcdb67853c556d2bad Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 17:38:28 +0900 Subject: [PATCH 25/71] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=9F=BC=EB=AA=85?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/umc10th/domain/mission/entity/Mission.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java index 5bbea620..24a9784f 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/entity/Mission.java @@ -6,6 +6,8 @@ import jakarta.persistence.*; import lombok.*; +import java.time.LocalDate; + @Entity @Getter @Builder @@ -19,7 +21,7 @@ public class Mission extends BaseEntity { @Column(name="rewardPoint",nullable = false) private Integer rewardPoint; - @Column(name="name",nullable = false) + @Column(name="content",nullable = false) private String content; @ManyToOne(fetch = FetchType.LAZY) @@ -29,4 +31,7 @@ public class Mission extends BaseEntity { @Enumerated(EnumType.STRING) @Column(columnDefinition = "VARCHAR(15) DEFAULT 'READY'") private MissionStatus status; + + @Column(name="deadline",nullable = false) + private LocalDate deadline; } From f66ee1f9c9856da69c53f5ef1871fc1f44353eb4 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:50:35 +0900 Subject: [PATCH 26/71] =?UTF-8?q?feat:=20MissionController=20=EA=B0=80?= =?UTF-8?q?=EA=B2=8C=20=EB=AF=B8=EC=85=98=20=EC=83=9D=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/controller/MissionController.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index c2fea6b8..a37da68b 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -8,7 +8,9 @@ import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; @@ -40,5 +42,25 @@ public ApiResponse getMemberMissions( return ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getRegionMissionList(regionId,dto)); } + // 가게 미션 생성 + @PostMapping("/stores/{storeId}/missions") + public ApiResponse createMission( + @PathVariable Long storeId, + @RequestBody @Valid MissionReqDTO.CreateMission dto // Valid 검증 어노테이션떄매 사용 + ){ + BaseSuccessCode code=MissionSuccessCode.CREATED; + return ApiResponse.onSuccess(code,missionService.createMission(storeId,dto)); + } + // 가게 내 미션들 조회 + @GetMapping("/stores/{storeId}/missions") + public ApiResponse>getMissions( + @PathVariable Long storeId, + @RequestParam Integer pageSize, // 한 페이지에 몇개 보여줄지 + @RequestParam String cursor, + @RequestParam String query + ){ + BaseSuccessCode code=MissionSuccessCode.OK; + return ApiResponse.onSuccess(code,missionService.getMissions(storeId,pageSize,cursor,query)); + } } From 056a363d1908daf13f8843d40bdf14ae1d62da70 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:50:56 +0900 Subject: [PATCH 27/71] =?UTF-8?q?refactor:=20reward=20->=20rewardPoint?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/converter/MissionConverter.java | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 457c74da..07017df2 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -1,10 +1,12 @@ package com.example.umc10th.domain.mission.converter; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.mission.dto.MissionReqDTO; import com.example.umc10th.domain.mission.dto.MissionResDTO; import com.example.umc10th.domain.mission.entity.Mission; import com.example.umc10th.domain.mission.entity.mapping.MemberMission; import com.example.umc10th.domain.mission.enums.MissionStatus; +import com.example.umc10th.domain.store.entity.Store; import org.springframework.data.domain.Page; import java.util.List; @@ -16,8 +18,8 @@ public class MissionConverter { public static MissionResDTO.MissionDetailDTO toMissionDetailDTO(MemberMission memberMission){ return MissionResDTO.MissionDetailDTO.builder() .storeName(memberMission.getMission().getStore().getName()) - .reward(memberMission.getMission().getRewardPoint()) - .missionSpec(memberMission.getMission().getContent()) + .rewardPoint(memberMission.getMission().getRewardPoint()) + .content(memberMission.getMission().getContent()) .status(memberMission.getStatus()) .build(); } @@ -44,8 +46,8 @@ public static MissionResDTO.MissionListResDTO toMissionListDTO(Page MissionResDTO.PaginationtoPagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + + ){ + return MissionResDTO.Pagination.builder() + .data(data) + .hasNext(hasNext) + .nextCursor(nextCursor) + .pageSize(pageSize) + .build(); + } } \ No newline at end of file From 9cfab48c7e1fa73ee1219fb9901baf769fe44845 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:51:11 +0900 Subject: [PATCH 28/71] =?UTF-8?q?feat:=20QUERY=5FNOT=5FVALID=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/exception/code/MissionErrorCode.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java index ebc858cb..1362257c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionErrorCode.java @@ -14,6 +14,9 @@ public enum MissionErrorCode implements BaseErrorCode { MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "MISSION404_1", "미션 조회에 실패했습니다."), + QUERY_NOT_VALID(HttpStatus.BAD_REQUEST, + "MISSION400_2", + "유효하지 않은 쿼리 파라미터입니다.") ; private final HttpStatus status; private final String code; From f229fa8a8506b7f266b773fe08cbc8bde6c9d90d Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:52:01 +0900 Subject: [PATCH 29/71] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=BB=A4=EC=84=9C=20=EA=B8=B0=EB=B0=98=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/repository/MissionRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java index 2eab2611..91c9487d 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/repository/MissionRepository.java @@ -2,11 +2,15 @@ import com.example.umc10th.domain.mission.entity.Mission; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; + public interface MissionRepository extends JpaRepository { @Query("SELECT m From Mission m "+ @@ -15,4 +19,8 @@ public interface MissionRepository extends JpaRepository { "WHERE r.id=:regionId "+ "AND m.status='READY'") Page findAllByRegionId(@Param("regionId")Long regionId, Pageable pageable); + + Slice findMissionByStore_IdOrderByIdDesc(Long storeId, PageRequest pageRequest); + + Slice findMissionByStore_IdAndIdLessThanOrderByIdDesc(Long storeId, long idCursor, PageRequest pageRequest); } \ No newline at end of file From 491abd8ff57c169f22f613977040e5541807ff5e Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:52:21 +0900 Subject: [PATCH 30/71] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=BB=A4=EC=84=9C=20=EA=B8=B0=EB=B0=98=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98,=20=EA=B0=80?= =?UTF-8?q?=EA=B2=8C=20=EB=82=B4=20=EB=AF=B8=EC=85=98=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/dto/MissionReqDTO.java | 17 ++++++++++++ .../domain/mission/dto/MissionResDTO.java | 26 ++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index 499a2ba0..9a1902c4 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -1,10 +1,15 @@ package com.example.umc10th.domain.mission.dto; import com.example.umc10th.domain.mission.enums.MissionStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; import lombok.Setter; +import java.time.LocalDate; +import java.util.List; + public class MissionReqDTO { // 미션 목록 조회 @@ -20,4 +25,16 @@ public record MyMissionReqDTO( Long regionId, Integer page ){} + + // 가게 미션 생성 + @Builder + public record CreateMission( + @NotBlank(message="조건은 빈칸일 수 없습니다.") + String content, + @NotNull(message="마감기한은 필수입니다.") + LocalDate deadline, + @NotNull(message="미션 성공 포인트는 필수입니다.") + Integer rewardPoint, + MissionStatus status + ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index 1788f2ea..7f3d4b25 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -25,13 +25,9 @@ public record MissionListResDTO( // 미션 정보 @Builder public record MissionDetailDTO( - @Schema(description = "가게 이름", example = "요아정") String storeName, // 가게 이름 - @Schema(description = "미션 보상 포인트", example = "500") - Integer reward, // 보상 - @Schema(description = "미션 내용", example = "요거트 아이스크림 1개 먹기") - String missionSpec, // 미션 내용 - @Schema(description = "미션 상태", example = "CHALLENGING") + Integer rewardPoint, // 보상 + String content, // 미션 내용 MissionStatus status // 미션 상태 ){} @@ -44,4 +40,22 @@ public record RegionMissionListDTO( Boolean isFirst, Boolean isLast ){} + + // 가게 내 매션 조회 + @Builder + public record GetMission( + Long missionId, + Integer rewardPoint, + MissionStatus status, + String content + ){} + + // 페이지네이션 틀 + @Builder + public record Pagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + ){} } From 198bb66cbdc0cfbf685f45c64b0cb0e03f44885b Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:52:48 +0900 Subject: [PATCH 31/71] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=8C=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/service/MissionService.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index 02e447fc..ddc610bb 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -16,15 +16,23 @@ import com.example.umc10th.domain.mission.repository.MemberMissionRepository; import com.example.umc10th.domain.mission.repository.MissionRepository; import com.example.umc10th.domain.store.entity.Region; +import com.example.umc10th.domain.store.entity.Store; import com.example.umc10th.domain.store.exception.StoreException; import com.example.umc10th.domain.store.exception.code.StoreErrorCode; import com.example.umc10th.domain.store.repository.RegionRepository; +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.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +import static org.springframework.data.jpa.domain.AbstractPersistable_.id; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -33,11 +41,14 @@ public class MissionService { private final MemberRepository memberRepository; private final RegionRepository regionRepository; private final MissionRepository missionRepository; + private final StoreRepository storeRepository; + public MissionResDTO.MissionListResDTO getMemberMissionList(Long memberId, MissionReqDTO.MissionListReqDTO dto) { // DB에서 해당 유저 ID로 데이터 조회 Member member=memberRepository.findById(memberId) .orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + // 몇 번재 페이지를, 몇 개씩 가져올지 PageRequest pageRequest=PageRequest.of(dto.page(),3); @@ -66,5 +77,72 @@ public MissionResDTO.RegionMissionListDTO getRegionMissionList(Long regionId, Mi // 컨버터를 이용해서 응답 dto로 변환 후 반환 return MissionConverter.toRegionMissionListDTO(missionPage); } + + @Transactional + public Void createMission(Long storeId, MissionReqDTO.CreateMission dto) { + + // 가게 찾기 + Store store=storeRepository.findById(storeId) + .orElseThrow(()->new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + // 미션 생성 + Mission mission= MissionConverter.toMission(store,dto); + + // 미션 DB 저장 + missionRepository.save(mission); + return null; + } + + // 가게 내 미션들 조회 + public MissionResDTO.Pagination getMissions( + Long storeId, + Integer pageSize, + String cursor, + String query + ) { + // 페이지 정보들을 PageRequest로 만들기 + PageRequest pageRequest=PageRequest.of(0,pageSize); + + long idCursor; + Slice missionList; + String nextCursor; + + // 커서가 있는 경우 + if(!cursor.equals("-1")) { + + // 커서 분리 + String[] cursorSplit = cursor.split(":"); + switch (query.toLowerCase()) { + case "id": + + // 커서 타입 변환 + Long prevCursor = Long.parseLong(cursorSplit[0]); + idCursor = Long.parseLong(cursorSplit[1]); + + // 가게 내 미션들 조회& where절에 커서값 기입 + missionList = missionRepository.findMissionByStore_IdAndIdLessThanOrderByIdDesc( + storeId, + idCursor, + pageRequest + ); + break; + default: + throw new MissionException(MissionErrorCode.QUERY_NOT_VALID); + } + }else{ + // 커서 없이 조회 + missionList=missionRepository.findMissionByStore_IdOrderByIdDesc(storeId,pageRequest); + } + // 다음 커서 계산 + nextCursor=missionList.getContent().getLast().getId()+":"+missionList.getContent().getLast().getId(); + + // 미션들 응답 DTO로 포장하기 + return MissionConverter.toPagination( + missionList.map(MissionConverter::toGetMission).toList(), + missionList.hasNext(), + nextCursor, + missionList.getSize() + ); + } } From 1c62e4d60fb5e37e18a57e3ea8ef5c993afdf85b Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:53:01 +0900 Subject: [PATCH 32/71] =?UTF-8?q?feat:=20CREATED=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/exception/code/MissionSuccessCode.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java index 4283e290..4a2e60d0 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java @@ -9,8 +9,11 @@ @RequiredArgsConstructor public enum MissionSuccessCode implements BaseSuccessCode { - OK(HttpStatus.OK, + CREATED(HttpStatus.OK, "MISSION200_1", + "성공적으로 미션을 생성했습니다."), + OK(HttpStatus.OK, + "MISSION200_2", "성공적으로 미션이 조회되었습니다."), ; private final HttpStatus status; From db85075a483e08a9bc73487a87f563c6547045a5 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:53:36 +0900 Subject: [PATCH 33/71] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20db=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/service/ReviewService.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index a833141b..6c2f26fe 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -15,6 +15,8 @@ import com.example.umc10th.domain.review.exception.code.ReviewErrorCode; import com.example.umc10th.domain.review.repository.ReviewRepository; import com.example.umc10th.domain.store.entity.Store; +import com.example.umc10th.domain.store.exception.StoreException; +import com.example.umc10th.domain.store.exception.code.StoreErrorCode; import com.example.umc10th.domain.store.repository.StoreRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -35,11 +37,21 @@ public class ReviewService { // 리뷰 작성 @Transactional public ReviewResDTO.CreateReviewResult createReviewResult(Long storeId, ReviewReqDTO.CreateReview dto) { - // DB에서 해당 가게 ID로 데이터 조회 - Review review=reviewRepository.findById(storeId) - .orElseThrow(()-> new ReviewException(ReviewErrorCode.REVIEW_NOT_CREATED)); - // 컨버터를 이용해서 응답 DTO 생성 & return - return ReviewConverter.toCreateReviewResult(review); + // 리뷰를 달 가게가 있는지 확인 + Store store=storeRepository.findById(storeId) + .orElseThrow(()->new StoreException(StoreErrorCode.STORE_NOT_FOUND)); + + // 리뷰를 쓰는 멤버가 있는지 확인 + Member member=memberRepository.findById(dto.memberId()) + .orElseThrow(()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + // 찾아온 가게와 멤버 정보를 엮어서 리뷰 객체 생성 + Review review=ReviewConverter.toReview(dto,member,store); + + // db저장 + Review savedReview=reviewRepository.save(review); + + return ReviewConverter.toCreateReviewResult(savedReview); } } From 49e1ebc388310057eca6d330e99058292254d36c Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:53:55 +0900 Subject: [PATCH 34/71] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20enum=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/enums/TermName.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/TermName.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/TermName.java index 5fca61ac..a9bea686 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/TermName.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/enums/TermName.java @@ -1,4 +1,7 @@ package com.example.umc10th.domain.member.enums; public enum TermName { + SERVICE_AGREEMENT, + PRIVACY_POLICY, + LOCATION_TERMS } From 8ad505196450844a8c757acacf26adf9ace7bd3b Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:54:13 +0900 Subject: [PATCH 35/71] =?UTF-8?q?feat:=20BaseEntity=EB=8F=99=EC=9E=91?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9CEnableJpaAuditing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/example/umc10th/Umc10thApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/Umc10thApplication.java b/Hyeonu/src/main/java/com/example/umc10th/Umc10thApplication.java index 9983f43a..6e64fac9 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/Umc10thApplication.java +++ b/Hyeonu/src/main/java/com/example/umc10th/Umc10thApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing // BaseEntity 동작 @SpringBootApplication public class Umc10thApplication { From 27995fa0c32d1ed58b8706aff57c0e28bc61bfdb Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:54:41 +0900 Subject: [PATCH 36/71] =?UTF-8?q?feat:=20BaseErrorCode=20=EC=83=81?= =?UTF-8?q?=EC=86=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/store/exception/code/StoreErrorCode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java index 60d8c64a..c2373704 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/code/StoreErrorCode.java @@ -1,12 +1,14 @@ package com.example.umc10th.domain.store.exception.code; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; 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_1", "해당 가게를 찾을 수 없습니다."), REGION_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE404_2", "해당 지역이 존재하지 않습니다."); From 1ff410efbac622a81c930af757f50ee9700e4cfa Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:54:54 +0900 Subject: [PATCH 37/71] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/store/exception/StoreException.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java index d5143b74..35101c67 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/exception/StoreException.java @@ -1,14 +1,15 @@ package com.example.umc10th.domain.store.exception; import com.example.umc10th.domain.store.exception.code.StoreErrorCode; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.exception.ProjectException; import lombok.Getter; @Getter -public class StoreException extends RuntimeException { - private final StoreErrorCode errorCode; +public class StoreException extends ProjectException { + + public StoreException(BaseErrorCode code) { + super(code); - public StoreException(StoreErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; } } From 977dc5c73eb591ccf338162afb700cdc524e3f50 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 11 May 2026 18:55:53 +0900 Subject: [PATCH 38/71] =?UTF-8?q?refactor:=20Store=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/store/controller/StoreController.java | 5 ----- .../domain/store/converter/StoreConverter.java | 14 -------------- .../umc10th/domain/store/dto/StoreReqDTO.java | 7 +------ .../umc10th/domain/store/dto/StoreResDTO.java | 12 ------------ .../umc10th/domain/store/service/StoreService.java | 6 ------ 5 files changed, 1 insertion(+), 43 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java index 742adc0d..2561b353 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java @@ -13,10 +13,5 @@ @RequestMapping("/api/v1/stores") public class StoreController { - private final StoreService storeService; - @GetMapping("/{storeId}") - public ApiResponse getStoreInfo(@PathVariable Long storeId) { - return ApiResponse.onSuccess(MemberSuccessCode.OK, storeService.getStoreInfo(storeId)); - } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/converter/StoreConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/converter/StoreConverter.java index fd360437..18054bd9 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/converter/StoreConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/converter/StoreConverter.java @@ -4,19 +4,5 @@ import com.example.umc10th.domain.store.entity.Store; public class StoreConverter { - public static StoreResDTO.StoreInfo toStoreInfo(Store store) { - return StoreResDTO.StoreInfo.builder() - .id(store.getId()) - .name(store.getName()) - .address(store.getAddress()) - .regionName(store.getRegion().getName()) - .build(); - } - public static StoreResDTO.RegisterResult toRegisterResult(Store store) { - return StoreResDTO.RegisterResult.builder() - .storeId(store.getId()) - .createdAt(store.getCreatedAt()) - .build(); - } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreReqDTO.java index 40d91800..8f91b230 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreReqDTO.java @@ -3,10 +3,5 @@ public class StoreReqDTO { - // 가게 등록 요청 - public record JoinDTO( - String name, // 가게 이름 - String address, // 각 주소 - Long regionId // 지역ID - ){} + } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreResDTO.java index 794d9e1a..f599da6c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/dto/StoreResDTO.java @@ -4,17 +4,5 @@ import java.time.LocalDateTime; public class StoreResDTO { - @Builder - public record StoreInfo( - Long id, - String name, - String address, - String regionName - ){} - @Builder - public record RegisterResult( - Long storeId, - LocalDateTime createdAt - ){} } \ No newline at end of file diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/service/StoreService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/service/StoreService.java index 263997e6..016f7a50 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/service/StoreService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/service/StoreService.java @@ -14,11 +14,5 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class StoreService { - private final StoreRepository storeRepository; - public StoreResDTO.StoreInfo getStoreInfo(Long storeId) { - Store store = storeRepository.findById(storeId) - .orElseThrow(() -> new StoreException(StoreErrorCode.STORE_NOT_FOUND)); - return StoreConverter.toStoreInfo(store); - } } \ No newline at end of file From 66ef595f00bd762267b0c0d77a0a7442be161157 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 13:48:35 +0900 Subject: [PATCH 39/71] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/umc10th/domain/mission/dto/MissionResDTO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java index 7f3d4b25..6dae5271 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionResDTO.java @@ -41,7 +41,7 @@ public record RegionMissionListDTO( Boolean isLast ){} - // 가게 내 매션 조회 + // 가게 내 미션 조회 @Builder public record GetMission( Long missionId, From 9f09aa6ea9df145a33f968d44a612778f6687b4e Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 13:49:13 +0900 Subject: [PATCH 40/71] =?UTF-8?q?feat:=20=EB=82=B4=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EB=A6=AC=EB=B7=B0=EB=93=A4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20getReviews=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/controller/ReviewController.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 0e147ba8..9759217c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -26,8 +26,20 @@ public ApiResponse createReview( @PathVariable Long storeId, // {storeId}에 적힌 숫자를 storeId라는 변수에 담음 @RequestBody ReviewReqDTO.CreateReview dto // 사용자가 작성한 리뷰 정보 가져옴 ) { - BaseSuccessCode code=ReviewSuccessCode.OK; + BaseSuccessCode code=ReviewSuccessCode.CREATED; return ApiResponse.onSuccess(code, reviewService.createReviewResult(storeId,dto)); } + + // 내가 작성한 리뷰들 조회 + @GetMapping("/members/{memberId}/reviews") + public ApiResponse> getReviews( + @PathVariable Long memberId, + @RequestParam Integer pageSize, + @RequestParam String cursor, + @RequestParam String query + ){ + BaseSuccessCode code= ReviewSuccessCode.OK; + return ApiResponse.onSuccess(code,reviewService.getReviews(memberId,pageSize,cursor,query)); + } } From 604c69a87c57950928724451db6dbf70daca5f29 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 13:50:06 +0900 Subject: [PATCH 41/71] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=ED=8B=80,=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=EC=A0=95=EB=B3=B4=20ReviewResDTO=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/dto/ReviewResDTO.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java index 898ec4af..64081eff 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/dto/ReviewResDTO.java @@ -4,6 +4,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; public class ReviewResDTO { @Builder @@ -11,4 +12,24 @@ public record CreateReviewResult( Long id, LocalDateTime createdAt ){} + + // 전체 목록 응답 + @Builder + public record MyReviewDetailDTO( + Long reviewId, + String storeName, + String memberName, + Float rating, + String content, + LocalDate createdAt + ){} + + // 페이지네이션 틀 + @Builder + public record Pagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + ){} } From 3948ecbad6732004906c5768399ca21e04ed3e70 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 13:50:31 +0900 Subject: [PATCH 42/71] =?UTF-8?q?feat:=20=EC=BB=A4=EC=84=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=82=B4=EA=B0=80=20=EC=9E=91=EC=84=B1=ED=95=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/service/ReviewService.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 6c2f26fe..c04cf2c4 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -54,4 +55,102 @@ public ReviewResDTO.CreateReviewResult createReviewResult(Long storeId, ReviewRe return ReviewConverter.toCreateReviewResult(savedReview); } + + // 리뷰 조회 + public ReviewResDTO.Pagination getReviews( + Long memberId, + Integer pageSize, + String cursor, + String query + ) { + PageRequest pageRequest = PageRequest.of(0, pageSize); + + Slice reviewList; + String nextCursor = null; + + // 커서가 존재하는 경우 (첫 페이지가 아닌 경우) + if (cursor != null && !cursor.equals("-1")) { + + // 커서 파싱: "firstId:lastId" 또는 "firstRating:lastId" 형태 + String[] cursorSplit = cursor.split(":"); + + switch (query.toLowerCase()) { + + // ── ID 순 정렬 ────────────────────────────────────────────────── + case "id": + // cursorSplit[1]: 이전 페이지 마지막 리뷰의 ID + long idCursor = Long.parseLong(cursorSplit[1]); + + // 이전 페이지 마지막 ID보다 작은 리뷰들을 ID 내림차순으로 조회 + reviewList = reviewRepository + .findReviewsByMember_IdAndIdLessThanOrderByIdDesc( + memberId, idCursor, pageRequest); + break; + + // ── RATING 순 정렬 ─────────────────────────────────────────────── + case "rating": + // cursorSplit[0]: 이전 페이지 마지막 리뷰의 Rating + // cursorSplit[1]: 이전 페이지 마지막 리뷰의 ID (동일 rating 내 순서 보장용) + float ratingCursor = Float.parseFloat(cursorSplit[0]); + long ratingIdCursor = Long.parseLong(cursorSplit[1]); + + // rating 내림차순, 같은 rating이면 ID 내림차순으로 조회 + // WHERE (rating < ratingCursor) OR (rating = ratingCursor AND id < ratingIdCursor) + reviewList = reviewRepository + .findReviewsByMember_IdWithRatingCursor( + memberId, ratingCursor, ratingIdCursor, pageRequest); + break; + + default: + throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID); + } + + } else { + // 커서가 없는 경우 (첫 페이지) + switch (query.toLowerCase()) { + + // ── ID 순 첫 페이지 ────────────────────────────────────────────── + case "id": + reviewList = reviewRepository + .findReviewsByMember_IdOrderByIdDesc(memberId, pageRequest); + break; + + // ── RATING 순 첫 페이지 ────────────────────────────────────────── + case "rating": + reviewList = reviewRepository + .findReviewsByMember_IdOrderByRatingDescIdDesc(memberId, pageRequest); + break; + + default: + throw new ReviewException(ReviewErrorCode.QUERY_NOT_VALID); + } + } + + // 다음 페이지가 존재하고 현재 페이지 데이터가 있을 때만 커서 생성 + if (reviewList.hasNext() && !reviewList.getContent().isEmpty()) { + Review lastReview = reviewList.getContent().getLast(); + + switch (query.toLowerCase()) { + + // ID 순: "firstId:lastId" + case "id": + nextCursor = reviewList.getContent().getFirst().getId() + + ":" + lastReview.getId(); + break; + + // RATING 순: "lastRating:lastId" + case "rating": + nextCursor = lastReview.getRating() + + ":" + lastReview.getId(); + break; + } + } + + return ReviewConverter.toPagination( + reviewList.map(ReviewConverter::toGetReview).toList(), + reviewList.hasNext(), + nextCursor, + reviewList.getSize() + ); + } } From 7418d9c065eb0d3b2a80a6e964df480f430f9670 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 13:50:46 +0900 Subject: [PATCH 43/71] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=97=90=EB=9F=AC=EC=99=80=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/exception/code/ReviewErrorCode.java | 3 ++- .../review/exception/code/ReviewSuccessCode.java | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java index f92775e5..25398ff9 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewErrorCode.java @@ -7,7 +7,8 @@ @Getter @RequiredArgsConstructor public enum ReviewErrorCode { - REVIEW_NOT_CREATED(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 리뷰를 찾을 수 없습니다."); + REVIEW_NOT_CREATED(HttpStatus.NOT_FOUND, "REVIEW404_1", "해당 리뷰를 찾을 수 없습니다."), + QUERY_NOT_VALID(HttpStatus.BAD_REQUEST, "REVIEW400_1", "유효하지 않은 쿼리 파라미터입니다."); private final HttpStatus status; private final String code; diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java index 4dd5f9a2..e37d742b 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/exception/code/ReviewSuccessCode.java @@ -8,11 +8,13 @@ @Getter @RequiredArgsConstructor public enum ReviewSuccessCode implements BaseSuccessCode { - - OK(HttpStatus.OK, + CREATED(HttpStatus.CREATED, "REVIEW200_1", - "성공적으로 리뷰가 작성되었습니다."), - ; + "성공적으로 리뷰가 생성되었습니다."), + OK(HttpStatus.OK, + "REVIEW200_2", + "성공적으로 리뷰가 조회되었습니다."), + ; private final HttpStatus status; private final String code; private final String message; From b24996c360858c968cff7043b1f60f98deeccb5d Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 13:51:19 +0900 Subject: [PATCH 44/71] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=ED=8B=80=EA=B3=BC=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EA=B4=80=EB=A0=A8=20toGetReview=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/converter/ReviewConverter.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java index 3c1e6353..56d2b79c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/converter/ReviewConverter.java @@ -1,11 +1,14 @@ package com.example.umc10th.domain.review.converter; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.mission.dto.MissionResDTO; 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 com.example.umc10th.domain.store.entity.Store; +import java.util.List; + public class ReviewConverter { // 사용자가 보낸 DTO를 DB에 저장할 엔티티로 (조회가 아닌 생성(작성)이라 추가됨) @@ -25,4 +28,31 @@ public static ReviewResDTO.CreateReviewResult toCreateReviewResult(Review review .createdAt(review.getCreatedAt()) .build(); } + + // 페이지네이션 틀 생성 + public static ReviewResDTO.PaginationtoPagination( + List data, + Boolean hasNext, + String nextCursor, + Integer pageSize + ){ + return ReviewResDTO.Pagination.builder() + .data(data) + .hasNext(hasNext) + .nextCursor(nextCursor) + .pageSize(pageSize) + .build(); + } + + + public static ReviewResDTO.MyReviewDetailDTO toGetReview(Review review) { + return ReviewResDTO.MyReviewDetailDTO.builder() + .reviewId(review.getId()) + .storeName(review.getStore().getName()) + .memberName(review.getMember().getName()) + .rating(review.getRating()) + .content(review.getContent()) + .createdAt(review.getCreatedAt().toLocalDate()) + .build(); + } } \ No newline at end of file From 0f6005389b3d33f465fb447faeebbbb92c5e2930 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 13:51:40 +0900 Subject: [PATCH 45/71] =?UTF-8?q?feat:=20id=EC=88=9C=20=EB=82=B4=EB=A6=BC?= =?UTF-8?q?=EC=B0=A8=EC=88=9C=20=EC=BF=BC=EB=A6=AC=EC=99=80=20rating?= =?UTF-8?q?=EC=88=9C=20=EC=BF=BC=EB=A6=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/repository/ReviewRepository.java | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java index 5ebb9c45..4bb85b25 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/repository/ReviewRepository.java @@ -1,10 +1,63 @@ package com.example.umc10th.domain.review.repository; import com.example.umc10th.domain.review.entity.Review; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; 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 { -} + + // ── ID 순 ──────────────────────────────────────────────────────────────── + + // 첫 페이지: ID 내림차순 전체 조회 + @Query(""" + SELECT r FROM Review r + WHERE r.member.id = :memberId + ORDER BY r.id DESC + """) + Slice findReviewsByMember_IdOrderByIdDesc( + @Param("memberId") Long memberId, + Pageable pageable); + + // 이후 페이지: 커서(ID)보다 작은 것만 조회 + @Query(""" + SELECT r FROM Review r + WHERE r.member.id = :memberId + AND r.id < :idCursor + ORDER BY r.id DESC + """) + Slice findReviewsByMember_IdAndIdLessThanOrderByIdDesc( + @Param("memberId") Long memberId, + @Param("idCursor") Long idCursor, + Pageable pageable); + + // ── RATING 순 ──────────────────────────────────────────────────────────── + + // 첫 페이지: rating 내림차순, 동률이면 ID 내림차순 + @Query(""" + SELECT r FROM Review r + WHERE r.member.id = :memberId + ORDER BY r.rating DESC, r.id DESC + """) + Slice findReviewsByMember_IdOrderByRatingDescIdDesc( + @Param("memberId") Long memberId, + Pageable pageable); + + // 이후 페이지: (rating < cursor) OR (rating = cursor AND id < idCursor) + @Query(""" + SELECT r FROM Review r + WHERE r.member.id = :memberId + AND ( + r.rating < :ratingCursor + OR (r.rating = :ratingCursor AND r.id < :idCursor) + ) + ORDER BY r.rating DESC, r.id DESC + """) + Slice findReviewsByMember_IdWithRatingCursor( + @Param("memberId") Long memberId, + @Param("ratingCursor") Float ratingCursor, + @Param("idCursor") Long idCursor, + Pageable pageable); +} \ No newline at end of file From 8f5ca73551e2a2df3c4024830cb36e5e0e77d55d Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 14:06:36 +0900 Subject: [PATCH 46/71] =?UTF-8?q?feat:=20PostMapping=EC=9C=BC=EB=A1=9C=20R?= =?UTF-8?q?equestBody=EB=A1=9C=20=EB=B0=9B=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/controller/MissionController.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index a37da68b..408bcd54 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -24,13 +24,12 @@ public class MissionController { private final MissionService missionService; // 내가 진행중이거나 완료한 미션 목록 조회(페이징 포함) - @GetMapping("/members/{memberId}/missions") + @PostMapping("/members/missions") public ApiResponse getMemberMissions( - @PathVariable Long memberId, - @ModelAttribute MissionReqDTO.MissionListReqDTO dto // 데이터가 많을때 + @RequestBody MissionReqDTO.MissionListReqDTO dto ){ // 서비스에서 데이터 가져오기 - return ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(memberId, dto)); + return ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(dto)); } // 현재 선택된 지역에서 도전 가능한 미션 목록(페이징 포함) From a2e4c262910927212c6fb5ecc05d3659ca4f2396 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 14:06:50 +0900 Subject: [PATCH 47/71] =?UTF-8?q?feat:=20RequestBody=EB=A1=9C=20=EB=B0=9B?= =?UTF-8?q?=EC=9D=84=20memberId=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/dto/MissionReqDTO.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index 9a1902c4..54799596 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -15,26 +15,30 @@ public class MissionReqDTO { // 미션 목록 조회 @Builder public record MissionListReqDTO( - MissionStatus status, - Integer page - ){} + Long memberId, + MissionStatus status, + Integer page + ) { + } // 홈 화면 가능 미션 목록 @Builder public record MyMissionReqDTO( Long regionId, Integer page - ){} + ) { + } // 가게 미션 생성 @Builder - public record CreateMission( - @NotBlank(message="조건은 빈칸일 수 없습니다.") + public record CreateMission( + @NotBlank(message = "조건은 빈칸일 수 없습니다.") String content, - @NotNull(message="마감기한은 필수입니다.") + @NotNull(message = "마감기한은 필수입니다.") LocalDate deadline, - @NotNull(message="미션 성공 포인트는 필수입니다.") + @NotNull(message = "미션 성공 포인트는 필수입니다.") Integer rewardPoint, MissionStatus status - ){} + ) { + } } From 26d28ccf2721bba04a41098cb61bad20120cfe36 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 14:07:29 +0900 Subject: [PATCH 48/71] =?UTF-8?q?feat:=20=EB=A7=A4=EA=B0=9C=EB=B3=80?= =?UTF-8?q?=EC=88=98=20memberId=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/service/MissionService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index ddc610bb..6bb8d79f 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -43,10 +43,10 @@ public class MissionService { private final MissionRepository missionRepository; private final StoreRepository storeRepository; - public MissionResDTO.MissionListResDTO getMemberMissionList(Long memberId, MissionReqDTO.MissionListReqDTO dto) { + public MissionResDTO.MissionListResDTO getMemberMissionList(MissionReqDTO.MissionListReqDTO dto) { // DB에서 해당 유저 ID로 데이터 조회 - Member member=memberRepository.findById(memberId) + Member member=memberRepository.findById(dto.memberId()) .orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); // 몇 번재 페이지를, 몇 개씩 가져올지 From b9c56cefa647f7f6948f0d1e05111e097fd1512a Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Tue, 12 May 2026 14:25:43 +0900 Subject: [PATCH 49/71] =?UTF-8?q?docs:=207=EC=A3=BC=EC=B0=A8=20=ED=95=B5?= =?UTF-8?q?=EC=8B=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/keyword_summary/ch07.md | 152 +++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 Hyeonu/keyword_summary/ch07.md diff --git a/Hyeonu/keyword_summary/ch07.md b/Hyeonu/keyword_summary/ch07.md new file mode 100644 index 00000000..906d4da9 --- /dev/null +++ b/Hyeonu/keyword_summary/ch07.md @@ -0,0 +1,152 @@ +- Page와 Slice + + **Page 란?** + + Spring Data JPA에서 제공하는 페이지네이션 객체 중 하나로, 전체 페이지 정보까지 포함하는 페이징 방식이다. + + 예를 들어 게시글이 총 100개 있고, 한 페이지에 10개씩 보여준다면 + + - 현재 페이지 번호 + - 전체 데이터 개수 + - 전체 페이지 수 + - 다음 페이지 여부 + + 이런 정보들을 모두 알 수 있다. + + ```java + Page missions=missionRepository.findByStoreId(storeId,pageable); + ``` + + **장점** + + - 프론트엔드에서 페이지 UI 만들기 편함 ("총 15페이지 중 현재 3페이지”) + - 전체 데이터 규모 파악 가능 + + **단점** + + - COUNT 쿼리 비용 발생 (데이터가 많아질수록 성능 부담 발생 가능) + - Offset 기반의 한계 (뒤 페이지로 갈수록 느려질 수 있다. + + **Slice 란?** + + Page보다 가벼운 페이지네이션 방식이다. + + 전체 개수는 계산하지 않음 + + ```java + Slice missions = missionRepository.findByStoreId(storeId, pageable); + ``` + + Spring 공식 문서에서도 `Slice`는 COUNT 쿼리 없이 **다음 페이지 존재 여부만 확인**한다고 설명한다 + **장점** + + - 성능이 더 좋음 (COUNT 쿼리가 없음) + - 무한스크롤 구현에 적합 + + **단점** + + - 전체 페이지 수를 알 수 없다. + + | 항목 | Page | Slice | + | --- | --- | --- | + | 전체 개수 | O | X | + | Count 쿼리 | O | X | + | 성능 | 상대적으로 느림 | 상대적으로 빠름 | + | 사용처 | 게시판, 관리자 페이지 | 무한스크롤, 커서 페이징 | +- Java stream API + + Java 8부터 추가된 기능으로, 컬렉션 데이터를 함수형 방식으로 처리할 수 있게 해주는 API이다. + + 예: + + ```java + List dtoList = + missions.stream() + .map(MissionConverter::toDTO) + .toList(); + ``` + + **주요 메서드** + + map() + + 데이터 변환 + + `mission -> dto` + + filter() + + 조건 필터림 + + `point>100` + + forEach() + + 반복 처리 + + collect()/toList() + + 결과 수집 + + **장점** + + - 코드가 간결해짐 + - 가독성이 좋음 (데이터 흐름이 잘 보임) + + **단점** + + - 익숙하지 않으면 오히려 어려움 (람다식) + - 디버깅 불편 (중간 상태 확인 어려움) +- 객체 그래프 탐색 + + JPA에서 객체 간 연관관계를 통해 다른 엔티티에 접근하는 방식 + + 예: + + ```java + mission.getStore().getName(); + ``` + + **장점** + + - SQL Join을 직접 안 써도 됨 + - 코드가 직관적 + + **단점** + + - Lazy Loading 문제 (필요할 때 추가 쿼리가 발생할 수 있음 (N+1문제) +- @Valid vs @Validated + + **@Valid** + + Java Bean Validation 표준 어노테이션 + + 주로 `Request Body` 검증 + + ```java + @PostMapping + public void create( + @RequestBody @Valid MissionRequest request + ) + ``` + + 검증 실패 시 `MethodArgumentNotValidException` 발생 + + **@Validated** + + Spring에서 제공하는 확장 검증 기능 + + 예: + + ```java + @Validated + @RestController + ``` + + **차이점** + + | 항목 | @Valid | @Validated | + | --- | --- | --- | + | 제공 | Java 표준 | Spring | + | 그룹 검증 | X | O | + | 메서드 파라미터 검증 | 제한적 | O | \ No newline at end of file From a8ba722c77918523983d9d2bf15f75205691714f Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Wed, 13 May 2026 19:33:12 +0900 Subject: [PATCH 50/71] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20status=20READY=EC=83=81=ED=83=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/mission/converter/MissionConverter.java | 2 +- .../com/example/umc10th/domain/mission/dto/MissionReqDTO.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java index 07017df2..fbab0a4d 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/converter/MissionConverter.java @@ -75,7 +75,7 @@ public static Mission toMission( ){ return Mission.builder() .store(store) - .status(dto.status()) + .status(MissionStatus.READY) .rewardPoint(dto.rewardPoint()) .deadline(dto.deadline()) .content(dto.content()) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java index 54799596..5ab9e76c 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/dto/MissionReqDTO.java @@ -37,8 +37,7 @@ public record CreateMission( @NotNull(message = "마감기한은 필수입니다.") LocalDate deadline, @NotNull(message = "미션 성공 포인트는 필수입니다.") - Integer rewardPoint, - MissionStatus status + Integer rewardPoint ) { } } From 9833d04b80c537a59267e269a33462ea5da19283 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Fri, 15 May 2026 12:48:38 +0900 Subject: [PATCH 51/71] =?UTF-8?q?feat:=20ResponseEntity=EB=A1=9C=20HTTP=20?= =?UTF-8?q?status=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 8 +++--- .../mission/controller/MissionController.java | 27 ++++++++++++------- .../review/controller/ReviewController.java | 15 ++++++----- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index eaa23a2c..7c422bad 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -7,6 +7,7 @@ import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -21,12 +22,13 @@ public class MemberController { // 마이페이지 @PostMapping("/v1/members/me") - public ApiResponse getInfo( + public ResponseEntity> getInfo( // 받은 JSON 데이터를 자바 객체(dto)로 변환해서 씀 @RequestBody MemberReqDTO.GetInfo dto ){ - BaseSuccessCode code= MemberSuccessCode.OK; - return ApiResponse.onSuccess(code,memberService.getInfo(dto)); + return ResponseEntity + .status(MemberSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(dto))); } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 408bcd54..634e55ff 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -13,6 +13,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -25,41 +26,47 @@ public class MissionController { // 내가 진행중이거나 완료한 미션 목록 조회(페이징 포함) @PostMapping("/members/missions") - public ApiResponse getMemberMissions( + public ResponseEntity> getMemberMissions( @RequestBody MissionReqDTO.MissionListReqDTO dto ){ // 서비스에서 데이터 가져오기 - return ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(dto)); + return ResponseEntity + .status(MissionSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.OK, missionService.getMemberMissionList(dto))); } // 현재 선택된 지역에서 도전 가능한 미션 목록(페이징 포함) @GetMapping("/regions/{regionId}/missions") - public ApiResponsegetRegionMissions( + public ResponseEntity>getRegionMissions( @PathVariable Long regionId, @ModelAttribute MissionReqDTO.MyMissionReqDTO dto ){ - return ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getRegionMissionList(regionId,dto)); + return ResponseEntity + .status(MissionSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getRegionMissionList(regionId,dto))); } // 가게 미션 생성 @PostMapping("/stores/{storeId}/missions") - public ApiResponse createMission( + public ResponseEntity> createMission( @PathVariable Long storeId, @RequestBody @Valid MissionReqDTO.CreateMission dto // Valid 검증 어노테이션떄매 사용 ){ - BaseSuccessCode code=MissionSuccessCode.CREATED; - return ApiResponse.onSuccess(code,missionService.createMission(storeId,dto)); + return ResponseEntity + .status(MissionSuccessCode.CREATED.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.CREATED,missionService.createMission(storeId,dto))); } // 가게 내 미션들 조회 @GetMapping("/stores/{storeId}/missions") - public ApiResponse>getMissions( + public ResponseEntity>>getMissions( @PathVariable Long storeId, @RequestParam Integer pageSize, // 한 페이지에 몇개 보여줄지 @RequestParam String cursor, @RequestParam String query ){ - BaseSuccessCode code=MissionSuccessCode.OK; - return ApiResponse.onSuccess(code,missionService.getMissions(storeId,pageSize,cursor,query)); + return ResponseEntity + .status(MissionSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(MissionSuccessCode.OK,missionService.getMissions(storeId,pageSize,cursor,query))); } } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index 9759217c..f4aa6be7 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -10,6 +10,7 @@ import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,24 +23,26 @@ public class ReviewController { // 특정 가게에 리뷰 등록 @PostMapping("/stores/{storeId}/reviews") - public ApiResponse createReview( + public ResponseEntity> createReview( @PathVariable Long storeId, // {storeId}에 적힌 숫자를 storeId라는 변수에 담음 @RequestBody ReviewReqDTO.CreateReview dto // 사용자가 작성한 리뷰 정보 가져옴 ) { - BaseSuccessCode code=ReviewSuccessCode.CREATED; - return ApiResponse.onSuccess(code, reviewService.createReviewResult(storeId,dto)); + return ResponseEntity + .status(ReviewSuccessCode.CREATED.getStatus()) + .body(ApiResponse.onSuccess(ReviewSuccessCode.CREATED, reviewService.createReviewResult(storeId,dto))); } // 내가 작성한 리뷰들 조회 @GetMapping("/members/{memberId}/reviews") - public ApiResponse> getReviews( + public ResponseEntity>> getReviews( @PathVariable Long memberId, @RequestParam Integer pageSize, @RequestParam String cursor, @RequestParam String query ){ - BaseSuccessCode code= ReviewSuccessCode.OK; - return ApiResponse.onSuccess(code,reviewService.getReviews(memberId,pageSize,cursor,query)); + return ResponseEntity + .status(ReviewSuccessCode.OK.getStatus()) + .body(ApiResponse.onSuccess(ReviewSuccessCode.OK,reviewService.getReviews(memberId,pageSize,cursor,query))); } } From 57f8f2bfa3c39ca90c109145b19f9957e36337f1 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Fri, 15 May 2026 12:49:09 +0900 Subject: [PATCH 52/71] =?UTF-8?q?fix:=20MissionSuccessCode=20CREATED=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=BD=94=EB=93=9C=20201=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/exception/code/MissionSuccessCode.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java index 4a2e60d0..e6289484 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/exception/code/MissionSuccessCode.java @@ -9,12 +9,12 @@ @RequiredArgsConstructor public enum MissionSuccessCode implements BaseSuccessCode { - CREATED(HttpStatus.OK, - "MISSION200_1", - "성공적으로 미션을 생성했습니다."), OK(HttpStatus.OK, - "MISSION200_2", + "MISSION200_1", "성공적으로 미션이 조회되었습니다."), + CREATED(HttpStatus.CREATED, + "MISSION201_1", + "성공적으로 미션을 생성했습니다.") ; private final HttpStatus status; private final String code; From 82021c94f74bfddbd9beaf4de0047f3985cfd689 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:41:27 +0900 Subject: [PATCH 53/71] =?UTF-8?q?feat:=20Spring=20Security=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Hyeonu/build.gradle b/Hyeonu/build.gradle index 4c422380..2b6a1a72 100644 --- a/Hyeonu/build.gradle +++ b/Hyeonu/build.gradle @@ -33,6 +33,10 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { From e9dc6089008e75b75da901ab06c635d1712e24aa Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:41:52 +0900 Subject: [PATCH 54/71] =?UTF-8?q?feat:=20password=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/entity/Member.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 063d5939..0f390e54 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -47,6 +47,9 @@ public class Member extends BaseEntity { @Column(name="email", nullable=false) private String email; + @Column(name="password") + private String password; + @Column(name="point", nullable=false) @Builder.Default private int point=0; From 15f7d9f5a45b867257cd86a4071e8fc0203b07cf Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:42:06 +0900 Subject: [PATCH 55/71] =?UTF-8?q?feat:=20findByEmail=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/repository/MemberRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index aa14436c..8d9fa19f 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -9,4 +9,6 @@ public interface MemberRepository extends JpaRepository { @Query("SELECT m FROM Member m WHERE m.name=:name AND m.deletedAt IS NULL") Optional findActiveMember(String name); + + Optional findByEmail(String username); } From b527af55f75f31184a145d244cd771c4ebc00de4 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:42:48 +0900 Subject: [PATCH 56/71] =?UTF-8?q?feat:=20Spring=20Security=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 00000000..11ed4763 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.example.umc10th.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity // Spring Security 활성화 +@Configuration // 스프링 설정 파일임을 선언 +public class SecurityConfig { + + // 허용 URI 목록 + private final String[] allowUris = { + // Swagger 허용 + "/swagger-ui/**", // API 문서 UI + "/swagger-resources/**", // Swagger 리소스 + "/v3/api-docs/**", // Openapi 스펙 문서 + "/auth/**" // 인증 관련 엔드 포인트(로그인, 회원가입 등) + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // RestAPI는 보통 CSRF 공격에 덜 취약하므로 비활성화 + .csrf(AbstractHttpConfigurer::disable) + // 요청 권한 설정 + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() // allowUris는 누구나 접근 가능 + .anyRequest().authenticated() // 그 외 모든 요청은 로그인 필요 + ) + // 폼 로그인 설정 + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) // 로그인 성공 시 이동 + .permitAll() // 로그인 페이지는 모든 사용자가 접근 가능 + ) + // 로그아웃 설정 + .logout(logout -> logout + .logoutUrl("/logout") // 이 URL로 POST 요청 시 로그아웃 + .logoutSuccessUrl("/login?logout") // 로그아웃 후 로그인 페이지로 이동 + .permitAll() + ); + + return http.build(); + } + + // PasswordEncoder- 비밀번호 암호화 + // BCrypt 알고리즘으로 해시 암호화(같은 비밀번호도 매번 다른 해시값이 생성되어 암호화) + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} From 1e404af6990d0f0718f01bdeecde31703138d398 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:42:56 +0900 Subject: [PATCH 57/71] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomUserDetailsService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java new file mode 100644 index 00000000..dc1306d2 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,32 @@ +package com.example.umc10th.global.security.service; + +import com.example.umc10th.domain.member.entity.Member; +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.global.security.entity.AuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service // 스프링 서비스 빈 등록 +@RequiredArgsConstructor // final 필드 생성자 자동 생성 +// Spring Security가 "이 인터페이스를 구현한 클래스로 사용자를 조회해라" +public class CustomUserDetailsService implements UserDetailsService { + + // DB에서 회원을 조회하기 위한 Repository + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername( + String username + )throws UsernameNotFoundException{ + // 이메일로 DB에서 회원 조회 + Member member=memberRepository.findByEmail(username) + .orElseThrow(()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + // AuthMember로 감싸서 반환 + return new AuthMember(member); + } +} From f6997a77cfce87080b52834756d23f46a365704f Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 12:43:02 +0900 Subject: [PATCH 58/71] =?UTF-8?q?feat:=20Spring=20Security=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/entity/AuthMember.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java new file mode 100644 index 00000000..1e3d3da4 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -0,0 +1,36 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter // 모든 필드의 getter 자동 생성 +@RequiredArgsConstructor // final 필드를 받는 생성자 자동 생성 +// Member를 Security용으로 포장한 Wrapper 역할 +public class AuthMember implements UserDetails { + + private final Member member; + + // 자신의 역할 반환(유저, 관리자) + @Override + public Collection getAuthorities(){ + return List.of(); // 권한 구분 미구현 상태(빈 리스트 반환) + } + + @Override + public @Nullable String getPassword(){ // @Nullable은 비밀번호가 없을 수도 있음(소셜 로그인 등) + return member.getPassword(); // Member 엔티티의 비밀번호 반환 + } + + @Override + public String getUsername(){ + return member.getEmail(); // 이메일을 username으로 사용(식별자로) + } +} From 4a4abc6a6ffb0bcf643e4912460441b8e3bdb15a Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 13:03:08 +0900 Subject: [PATCH 59/71] =?UTF-8?q?feat:=20=EA=B6=8C=ED=95=9C=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=ED=95=A0=20=EB=95=8C=20JSON=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=91=EB=8B=B5=ED=95=B4=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/handler/CustomAccessDenied.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java new file mode 100644 index 00000000..929cdce2 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDenied.java @@ -0,0 +1,39 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +// 권한이 없는 사용자가 접근할 떄 JSON으로 응답해주는 핸들러 +// AccessDeniedHandler는 로그인은 했지만 권한이 없을 때 어떻게 처리할지 +// 401 Unauthorized 로그인 자체를 안함/ 403 Forbidden 로그인은 했지만 권한 없음 (이 핸들러가 처리) +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + )throws IOException { + // Java 객체->JSON문자열로 변환해주는 도구 + ObjectMapper objectMapper=new ObjectMapper(); + BaseErrorCode code= GeneralErrorCode.FORBIDDEN; // 403 Forbidden 에러코드 가져옴 + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); // JSON 형식으로 응답 + response.setStatus(code.getStatus().value()); // HTTP 상태코드 403 설정 + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse=ApiResponse.onFailure(code,null); + + // 실제 Response로 덮어쓰기 (만든 객체를 실제 HTTP 응답 바디에 JSON으로 작성) + objectMapper.writeValue(response.getOutputStream(),errorResponse); + } +} From 211f883012011aed2d8671909654903c94c853f9 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 13:03:23 +0900 Subject: [PATCH 60/71] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=95=88=ED=96=88=EC=9D=84=EB=96=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EC=A0=91=EA=B7=BC=ED=95=A0=20=EB=95=8C=20?= =?UTF-8?q?JSON=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5=ED=95=B4=EC=A3=BC?= =?UTF-8?q?=EB=8A=94=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/handler/CustomEntryPoint.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java new file mode 100644 index 00000000..bfe138b2 --- /dev/null +++ b/Hyeonu/src/main/java/com/example/umc10th/global/security/handler/CustomEntryPoint.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; +// 로그인 안했을때 +public class CustomEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + )throws IOException { + ObjectMapper objectMapper=new ObjectMapper(); + BaseErrorCode code= GeneralErrorCode.UNAUTHORIZED; + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse= ApiResponse.onFailure(code,null); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(),errorResponse); + } +} From a49ffa38c1ae8c178aefcacc1dcfc2fe802a3407 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 13:03:39 +0900 Subject: [PATCH 61/71] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=EC=83=81?= =?UTF-8?q?=ED=99=A9=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 11ed4763..009289f6 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.handler.CustomAccessDenied; +import com.example.umc10th.global.security.handler.CustomEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -42,7 +44,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logoutUrl("/logout") // 이 URL로 POST 요청 시 로그아웃 .logoutSuccessUrl("/login?logout") // 로그아웃 후 로그인 페이지로 이동 .permitAll() - ); + ) + // 예외 상황 핸들러 + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDenied()) // 403 발생 + .authenticationEntryPoint(customEntryPoint())) //401 발생 + ; + return http.build(); } @@ -53,4 +61,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + // + @Bean + public CustomAccessDenied customAccessDenied(){ + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint(){ + return new CustomEntryPoint(); + } } From 2f823adfec05f7fc7be117650b4a80686a3c3a61 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:54:25 +0900 Subject: [PATCH 62/71] =?UTF-8?q?refactor:=20=EC=A0=84=EC=97=AD=20MemberSu?= =?UTF-8?q?ccessCode=EB=A5=BC=20Member=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/code/MemberSuccessCode.java | 21 ++++++++++++++++++- .../apiPayload/code/MemberSuccessCode.java | 19 ----------------- 2 files changed, 20 insertions(+), 20 deletions(-) delete mode 100644 Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index f42e80f8..5720f48a 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -1,4 +1,23 @@ package com.example.umc10th.domain.member.exception.code; -public enum MemberSuccessCode { +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum MemberSuccessCode implements BaseSuccessCode { + + OK(HttpStatus.OK, + "MEMBER200_1", + "성공적으로 유저를 조회했습니다."), + SIGN_UP(HttpStatus.OK, + "MEMBER200_2", + "회원가입에 성공했습니다."); + + + private final HttpStatus status; + private final String code; + private final String message; } diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java b/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java deleted file mode 100644 index 0d337cca..00000000 --- a/Hyeonu/src/main/java/com/example/umc10th/global/apiPayload/code/MemberSuccessCode.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.umc10th.global.apiPayload.code; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@Getter -@RequiredArgsConstructor -public enum MemberSuccessCode implements BaseSuccessCode{ - - OK(HttpStatus.OK, - "MEMBER200_1", - "성공적으로 유저를 조회했습니다."), - ; - - private final HttpStatus status; - private final String code; - private final String message; -} From 5e1ece28994cdcbb5f7e7d36e76c32bd363291b7 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:55:41 +0900 Subject: [PATCH 63/71] =?UTF-8?q?chore:=20StoreController=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/store/controller/StoreController.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java index 2561b353..c7906d26 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/store/controller/StoreController.java @@ -1,10 +1,5 @@ package com.example.umc10th.domain.store.controller; -import com.example.umc10th.domain.store.dto.StoreResDTO; -import com.example.umc10th.domain.store.service.StoreService; -import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; From 684e89fa8b840166f0464683b298e161265a2493 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:56:02 +0900 Subject: [PATCH 64/71] =?UTF-8?q?refactor:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 009289f6..16900ba4 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Hyeonu/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -21,7 +21,7 @@ public class SecurityConfig { "/swagger-ui/**", // API 문서 UI "/swagger-resources/**", // Swagger 리소스 "/v3/api-docs/**", // Openapi 스펙 문서 - "/auth/**" // 인증 관련 엔드 포인트(로그인, 회원가입 등) + "/api/auth/**" // 인증 관련 엔드 포인트(로그인, 회원가입 등) }; @Bean From b8ddca3fbfaf15d47cbed0d5ec0d7bde5c24967f Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:56:30 +0900 Subject: [PATCH 65/71] =?UTF-8?q?chore:=20ReviewController=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/review/controller/ReviewController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java index f4aa6be7..c12932b5 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/review/controller/ReviewController.java @@ -2,19 +2,13 @@ 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 com.example.umc10th.domain.review.exception.code.ReviewSuccessCode; import com.example.umc10th.domain.review.service.ReviewService; import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequiredArgsConstructor // final이 붙은거 생성자 대신 작성 @RequestMapping("/api/v1") From af9280e71e0f3e4d885f01f36050af9f5f1456e7 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:56:37 +0900 Subject: [PATCH 66/71] =?UTF-8?q?chore:=20MissionController=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/controller/MissionController.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java index 634e55ff..b0b5d9eb 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/mission/controller/MissionController.java @@ -2,22 +2,14 @@ import com.example.umc10th.domain.mission.dto.MissionReqDTO; import com.example.umc10th.domain.mission.dto.MissionResDTO; -import com.example.umc10th.domain.mission.entity.Mission; -import com.example.umc10th.domain.mission.entity.mapping.MemberMission; -import com.example.umc10th.domain.mission.enums.MissionStatus; import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode; import com.example.umc10th.domain.mission.service.MissionService; import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequiredArgsConstructor @RequestMapping("/api/v1") From 2bb127f6a7ac67cc0935131b838157b196b40354 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:57:16 +0900 Subject: [PATCH 67/71] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A9=94=EC=84=9C=EB=93=9C=20signUp=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/controller/MemberController.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index 7c422bad..1a4135b9 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -4,8 +4,7 @@ import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; -import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; -import com.example.umc10th.global.apiPayload.code.MemberSuccessCode; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -31,4 +30,13 @@ public ResponseEntity> getInfo( .body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(dto))); } + // 회원가입 + @PostMapping("/auth/sign-up") + public ResponseEntity> signUp( + @RequestBody MemberReqDTO.SignUpReqDTO dto + ){ + return ResponseEntity + .status(MemberSuccessCode.SIGN_UP.getStatus()) + .body(ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP,memberService.signUp(dto))); + } } From e42cd53ad64abc435318b1e16eb25de9c4cc5bbb Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:57:44 +0900 Subject: [PATCH 68/71] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20SignUpReqDTO,=20SignUpResDTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/dto/MemberReqDTO.java | 17 +++++++++++++++++ .../umc10th/domain/member/dto/MemberResDTO.java | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index e937272d..d28130cf 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -1,9 +1,26 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.enums.Address; +import com.example.umc10th.domain.member.enums.Gender; + +import java.time.LocalDate; + public class MemberReqDTO { // 마이페이지 public record GetInfo( Long id ){} + + // 회원가입 + public record SignUpReqDTO( + String name, + Gender gender, + LocalDate birth, + Address address, + String detailAddress, + String phoneNumber, + String email, + String password + ){} } diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java index e6334334..88bf982d 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.service.MemberService; import lombok.Builder; import java.time.LocalDate; @@ -18,4 +19,10 @@ public record GetInfo( Integer point, Enum status ){} + + // 회원가입 + @Builder + public record SignUpResDTO( + Long id + ){} } From 6e6f1e72a6786072a2ed9c0586d8a2f2923cc456 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:58:06 +0900 Subject: [PATCH 69/71] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20signUp=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberService.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 93e9be65..76ea6d08 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -8,12 +8,14 @@ import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; // 마이 페이지 public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) { // DTO에서 유저 ID를 추출 @@ -24,4 +26,21 @@ public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) { // 컨버터를 이용해서 응답 DTO 생성 & return return MemberConverter.toGetInfo(member); } + + // 회원가입 + public MemberResDTO.SignUpResDTO signUp(MemberReqDTO.SignUpReqDTO dto) { + // 비밀번호 BCrypt 암호화 + String encodedPassword=passwordEncoder.encode(dto.password()); + + // DTO -> Entity 변환 + Member member=MemberConverter.toMember(dto, encodedPassword); + + // DB저장 + Member savedMember=memberRepository.save(member); + + // Entity -> ResponseDTO 변환 + return MemberConverter.toSignUpResDTO(savedMember); + + + } } From 358e0da73d25ea2eddb08ad838f14cfc9b38ebf8 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:58:31 +0900 Subject: [PATCH 70/71] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20toMember,=20toSignUp=20=EC=BB=A8=EB=B2=84=ED=84=B0?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/converter/MemberConverter.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 96452757..25ab43da 100644 --- a/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Hyeonu/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.converter; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; @@ -17,4 +18,22 @@ public static MemberResDTO.GetInfo toGetInfo(Member member) { .status(member.getStatus()) .build(); } + + // DTO -> Entity 변환 + public static Member toMember(MemberReqDTO.SignUpReqDTO dto, String encodedPassword) { + return Member.builder() + .name(dto.name()) + .gender(dto.gender()) + .birth(dto.birth()) + .address(dto.address()) + .detailAddress(dto.detailAddress()) + .phoneNumber(dto.phoneNumber()) + .email(dto.email()) + .password(encodedPassword) // BCrypt 암호화된 비밀번호 + .build(); + } + + public static MemberResDTO.SignUpResDTO toSignUpResDTO(Member member) { + return new MemberResDTO.SignUpResDTO(member.getId()); + } } From fd05d4650bb70c20f1ada1d36a301f8ac9662962 Mon Sep 17 00:00:00 2001 From: cha-hyunwoo Date: Mon, 18 May 2026 15:58:43 +0900 Subject: [PATCH 71/71] =?UTF-8?q?docs:=208=EC=A3=BC=EC=B0=A8=20=ED=95=B5?= =?UTF-8?q?=EC=8B=AC=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hyeonu/keyword_summary/ch08.md | 115 +++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 Hyeonu/keyword_summary/ch08.md diff --git a/Hyeonu/keyword_summary/ch08.md b/Hyeonu/keyword_summary/ch08.md new file mode 100644 index 00000000..7c18d5eb --- /dev/null +++ b/Hyeonu/keyword_summary/ch08.md @@ -0,0 +1,115 @@ +- Spring Security가 무엇인가? + + 자바 기반 웹 애플리케이션의 보안을 담당하는 프레임워크이다. + + 개발자가 보안 로직을 직접 구현하지않고 몇가지 설정만으로 아래 3가지를 처리할 수 있음 + + - 인증(Authentication)- 이 사용자가 누구인지 식별(로그인) + - 인가(Authorization)- 이 사용자가 해당 리소스에 접근할 권한이 있는지 식별(권한 확인) + - 보안 위협 방어- CSRF, XSS 등 각종 공격으로부터 보호 + + Filter Chain을 통해 HTTP요청을 순차적으로 필터링하며, SecurityConfig를 통해 커스텀 보안 설정을 적용할 수 있다. + + + +- 인증(Authentication)vs 인가(Authorization) + + + + **인증(Authentication)** + + 목적 + + 사용자 신원 확인 + + 실패 시 + + 401 Unauthorized + + 예시 + + 로그인, 회원가입 + + Spring Security + + `AuthenticationEntryPoint` + + + + **인가(Authorization)** + + 목적 + + 리소스 접근 권한 확인 + + 실패 시 + + 403 Forbidden + + 예시 + + 관리자 페이지 접근 + + Spring Security + + `AccessDeniedHandler` + + 인증 먼저 그 다음 인가 + +- Stateful vs Stateless + + + + **Stateful** + + 상태 저장 + + 서버가 저장 + + 방식 + + 세션 + + 동작 + + 로그인 시 서버에 세션 저장 → 요청마다 세션 확인 + + 장점 + + 구현 간단 + 즉시 로그아웃 가능 + + 단점 + + 서버 부하 + 확장성 낮음 + + 예시 + + 폼 로그인 + + + + **Stateless** + + 상태 저장 + + 서버가 저장 안함 + + 방식 + + JWT 토큰 + + 동작 + + 토큰에 사용자 정보 담아서 → 요청마다 토큰 검증 + + 장점 + + 토큰 탈취 시 만료 전까지 막기 어려움 + + 예시 + + JWT + + 이번 주차는 Stateful 방식 \ No newline at end of file