From 0a7a1c51f6367f4fd5403249261bfe0de8bbfdf3 Mon Sep 17 00:00:00 2001 From: seohyunk09 <2022112400@dgu.ac.kr> Date: Thu, 4 Dec 2025 11:58:40 +0900 Subject: [PATCH 1/4] mission/#9 --- .../mission/controller/MissionController.java | 13 ++++++++++ .../mission/converter/MissionConverter.java | 13 ++++++++++ .../domain/mission/dto/res/MissionResDTO.java | 8 ++++++ .../domain/mission/entity/Mission.java | 1 + .../mission/repository/MissionRepository.java | 3 +++ .../review/controller/ReviewController.java | 12 +++++++++ .../review/converter/ReviewConverter.java | 15 +++++++++++ .../domain/review/dto/res/ReviewResDTO.java | 8 ++++++ .../review/repository/ReviewRepository.java | 3 +++ .../review/service/ReviewQueryService.java | 5 ++++ .../controller/UserMissionController.java | 12 +++++++++ .../umc9th2/domain/user/dto/MyMissionDTO.java | 4 +-- .../repository/UserMissionRepository.java | 22 ++++++++++++++++ .../user/service/UserMissionService.java | 5 ++++ .../global/annotation/PageValidator.java | 26 +++++++++++++++++++ .../umc9th2/global/annotation/ValidPage.java | 17 ++++++++++++ .../apiPayload/code/GeneralErrorCode.java | 4 +++ 17 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/umc9th2/global/annotation/PageValidator.java create mode 100644 src/main/java/com/example/umc9th2/global/annotation/ValidPage.java diff --git a/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java index f1ac845..1fa9e24 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java +++ b/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java @@ -30,4 +30,17 @@ public ApiResponse createMission( ) ); } + + //특정가게의미션을 조회 + @GetMapping("/stores/{storeId}") + @Operation(summary = "특정 가게의 미션 목록 조회", description = "특정 가게에 등록된 미션 목록을 페이징하여 조회합니다.") + public ApiResponse> getStoreMissions( + @PathVariable Long storeId, + @ValidPage @RequestParam Integer page + ) { + Pageable pageable = PageRequest.of(page - 1, 10); + Page missionPage = missionService.getStoreMissions(storeId, pageable); + List result = MissionConverter.toMissionListDTO(missionPage.getContent()); + return ApiResponse.onSuccess(GeneralSuccessCode.OK, result); + } } diff --git a/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java index 41fdf75..74b4834 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java +++ b/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java @@ -29,4 +29,17 @@ public static MissionResDTO.CreateMission toCreateMissionDTO(Mission mission) { mission.getStore().getStoreName() ); } + +// 미션목록반환 +public static List toMissionListDTO(List missionList) { + return missionList.stream() + .map(mission -> new MissionResDTO.MissionList( + mission.getMissionId(), + mission.getTitle(), + mission.getDescription(), + mission.getRewardPoints(), + mission.getIsActive() + )) + .toList(); +} } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/mission/dto/res/MissionResDTO.java b/src/main/java/com/example/umc9th2/domain/mission/dto/res/MissionResDTO.java index feb74ce..0f6e6ef 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/dto/res/MissionResDTO.java +++ b/src/main/java/com/example/umc9th2/domain/mission/dto/res/MissionResDTO.java @@ -10,4 +10,12 @@ public record CreateMission( Integer rewardPoints, String storeName //store 에서 ) {} + //미션 week 9 + public record MissionList( + Long missionId, + String title, + String description, + Integer rewardPoints, + Boolean isActive +) {} } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java b/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java index 155c184..834b18b 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java +++ b/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java @@ -11,6 +11,7 @@ @Getter @Setter @NoArgsConstructor +@Builder public class Mission { @Id diff --git a/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java b/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java index ca79ae5..dd719ea 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java @@ -4,4 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface MissionRepository extends JpaRepository { + //특정 가게의 미션 목록 + Page findByStore_StoreId(Long storeId, Pageable pageable); + } diff --git a/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java index f14aeca..6f6ef7e 100644 --- a/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java @@ -45,4 +45,16 @@ public ApiResponse createReview( ReviewResDTO.CreateReview result = reviewCommandService.createReview(request, userId); return ApiResponse.onSuccess(code, result); } + //작성한 리뷰 목록조회 + @GetMapping("/my-reviews") + @Operation(summary = "내가 작성한 리뷰 목록 조회", description = "특정 사용자가 작성한 리뷰 목록을 페이징하여 조회합니다.") + public ApiResponse> getMyReviews( + @RequestParam Long userId, + @ValidPage @RequestParam Integer page + ) { + Pageable pageable = PageRequest.of(page - 1, 10); + Page reviewPage = reviewQueryService.getMyReviews(userId, pageable); + List result = ReviewConverter.toMyReviewListDTO(reviewPage.getContent()); + return ApiResponse.onSuccess(GeneralSuccessCode.OK, result); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc9th2/domain/review/converter/ReviewConverter.java index bd4510c..399043e 100644 --- a/src/main/java/com/example/umc9th2/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/umc9th2/domain/review/converter/ReviewConverter.java @@ -59,4 +59,19 @@ public static ReviewResDTO.CreateReview toCreateReviewResultDto(Review review) { LocalDateTime.now() ); } + + //내가 작성한 리뷰 DTO 반환해 + public static List toMyReviewListDTO(List reviewList) { + return reviewList.stream() + .map(review -> new ReviewResDTO.MyReviewList( + review.getReviewId(), + review.getStore() != null ? review.getStore().getStoreName() : null, + review.getRating(), + review.getContent(), + review.getCreatedAt() + )) + .toList(); + } + + } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/review/dto/res/ReviewResDTO.java b/src/main/java/com/example/umc9th2/domain/review/dto/res/ReviewResDTO.java index fa4587c..9a72898 100644 --- a/src/main/java/com/example/umc9th2/domain/review/dto/res/ReviewResDTO.java +++ b/src/main/java/com/example/umc9th2/domain/review/dto/res/ReviewResDTO.java @@ -12,4 +12,12 @@ public record CreateReview( String storeName, LocalDateTime createdAt ) {} + //내가 작성한 review 목ㄹ곻 + public record MyReviewList( + Long reviewId, + String storeName, + Integer rating, + String content, + LocalDateTime createdAt +) {} } diff --git a/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java index 577ae83..2bed3b7 100644 --- a/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java @@ -4,4 +4,7 @@ import org.springframework.data.jpa.repository.*; public interface ReviewRepository extends JpaRepository, ReviewQueryDsl { +//내가 작성한 리뷰 목록 + Page findByUser_UserId(Long userId, Pageable pageable); + } diff --git a/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java b/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java index 36ceed8..9ee0bb0 100644 --- a/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java +++ b/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java @@ -43,5 +43,10 @@ public List searchReview(String type, String query) { List reviewList = reviewRepository.searchReview(builder); return reviewList; + + + } + public Page getMyReviews(Long userId, Pageable pageable) { + return reviewRepository.findByUser_UserId(userId, pageable); } } diff --git a/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java b/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java index 85eb11d..f7babff 100644 --- a/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java +++ b/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java @@ -36,4 +36,16 @@ public ApiResponse challengeMission( UserMissionResDTO.ChallengeMission result = userMissionService.challengeMission(missionId); return ApiResponse.onSuccess(code, result); } + //진행중인 미션목록죄회 + @GetMapping("/in-progress") + @Operation(summary = "내가 진행중인 미션 목록 조회", description = "특정 사용자가 진행중인 미션 목록을 페이징하여 조회합니다.") + public ApiResponse> getInProgressMissions( + @RequestParam Long userId, + @ValidPage @RequestParam Integer page + ) { + Pageable pageable = PageRequest.of(page - 1, 10); + Page missionPage = userMissionService.getInProgressMissions(userId, pageable); + return ApiResponse.onSuccess(GeneralSuccessCode.OK, missionPage.getContent()); + } + } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java b/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java index 252a0aa..aa64d55 100644 --- a/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java +++ b/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java @@ -7,8 +7,8 @@ public record MyMissionDTO( String title, String description, Integer rewardPoints, - String status, - LocalDateTime clearedAt, + boolean status, // String에서 바꿈 + LocalDateTime clearedAt, LocalDateTime clearedAt, String storeName, String regionName ) {} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java b/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java index be7c94f..3c51afe 100644 --- a/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java +++ b/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java @@ -29,4 +29,26 @@ public interface UserMissionRepository extends JpaRepository order by um.status asc, um.clearedAt desc """) List findMyMissions(@Param("userId") Long userId, Pageable pageable); + @Query(""" + select new com.example.umc9th2.domain.user.dto.MyMissionDTO( + m.missionId, + m.title, + m.description, + m.rewardPoints, + um.status, + um.clearedAt, + s.storeName, + rg.regionName + ) + from UserMission um + join um.mission m + join m.store s + join s.region rg + where um.user.userId = :userId + and um.status = false + order by um.clearedAt desc + """) + Page findInProgressMissions(@Param("userId") Long userId, Pageable pageable); + + } diff --git a/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java b/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java index d04b9dd..a25b7b4 100644 --- a/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java +++ b/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java @@ -27,6 +27,11 @@ public class UserMissionService { public List getMyMissions(Long userId, Pageable pageable) { return userMissionRepository.findMyMissions(userId, pageable); } + + public Page getInProgressMissions(Long userId, Pageable pageable) { + return userMissionRepository.findInProgressMissions(userId, pageable); + } + //미션 도전 @Transactional diff --git a/src/main/java/com/example/umc9th2/global/annotation/PageValidator.java b/src/main/java/com/example/umc9th2/global/annotation/PageValidator.java new file mode 100644 index 0000000..eefba7e --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/annotation/PageValidator.java @@ -0,0 +1,26 @@ +package com.example.umc9th2.global.annotation; + +import com.example.umc9th2.global.apiPayload.code.GeneralErrorCode; +import com.example.umc9th2.global.apiPayload.exception.GeneralException; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tnwjdwnd + +public class PageValidator implements ConstraintValidator { + + @Override + public void initialize(ValidPage constraintAnnotation) { + } + + @Override + public boolean isValid(Integer page, ConstraintValidatorContext context) { + if (page == null) { + throw new GeneralException(GeneralErrorCode.INVALID_PAGE); + } + if (page <= 0) { + throw new GeneralException(GeneralErrorCode.INVALID_PAGE); + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/global/annotation/ValidPage.java b/src/main/java/com/example/umc9th2/global/annotation/ValidPage.java new file mode 100644 index 0000000..06be912 --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/annotation/ValidPage.java @@ -0,0 +1,17 @@ +package com.example.umc9th2.global.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PageValidator.class) +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidPage { + String message() default "페이지 번호는 1 이상이어야 합니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} + diff --git a/src/main/java/com/example/umc9th2/global/apiPayload/code/GeneralErrorCode.java b/src/main/java/com/example/umc9th2/global/apiPayload/code/GeneralErrorCode.java index 8552161..3bd1b33 100644 --- a/src/main/java/com/example/umc9th2/global/apiPayload/code/GeneralErrorCode.java +++ b/src/main/java/com/example/umc9th2/global/apiPayload/code/GeneralErrorCode.java @@ -20,6 +20,10 @@ public enum GeneralErrorCode implements BaseErrorCode{ NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404_1", "요청한 리소스를 찾을 수 없습니다."), + //추가함 + INVALID_PAGE(HttpStatus.BAD_REQUEST, + "COMMON400_2", + "페이지 번호는 1 이상이어야 합니다."), //에러 핸들러 추가된 enum INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500_1", From 710a84093e874d59bff0798c678eec9764726b9c Mon Sep 17 00:00:00 2001 From: seohyunk09 <2022112400@dgu.ac.kr> Date: Sat, 20 Dec 2025 21:10:43 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=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/umc9th2/domain/review/entity/Review.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/umc9th2/domain/review/entity/Review.java b/src/main/java/com/example/umc9th2/domain/review/entity/Review.java index 525ecbc..86e59ee 100644 --- a/src/main/java/com/example/umc9th2/domain/review/entity/Review.java +++ b/src/main/java/com/example/umc9th2/domain/review/entity/Review.java @@ -2,7 +2,9 @@ import com.example.umc9th2.domain.user.entity.User; import com.example.umc9th2.domain.store.entity.Store; +import com.example.umc9th2.global.BaseEntity; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,8 +15,9 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor @Builder -public class Review { +public class Review extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From cbe74879e19102a67bac0425e9964613172f142e Mon Sep 17 00:00:00 2001 From: seohyunk09 <2022112400@dgu.ac.kr> Date: Sat, 20 Dec 2025 23:57:28 +0900 Subject: [PATCH 3/4] =?UTF-8?q?mission/#10=20:=20=EC=8B=A4=EC=8A=B51=20Ses?= =?UTF-8?q?sion=20=EB=B0=A9=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../mission/controller/MissionController.java | 11 ++-- .../mission/converter/MissionConverter.java | 2 + .../domain/mission/entity/Mission.java | 3 ++ .../mission/repository/MissionRepository.java | 2 + .../mission/service/MissionService.java | 6 +++ .../review/controller/ReviewController.java | 5 ++ .../review/repository/ReviewRepository.java | 4 +- .../review/service/ReviewQueryService.java | 13 +++-- .../store/exception/code/StoreErrorCode.java | 2 +- .../user/controller/UserController.java | 33 ++++++++++++ .../controller/UserMissionController.java | 3 ++ .../domain/user/converter/UserConverter.java | 26 +++++++++ .../umc9th2/domain/user/dto/MyMissionDTO.java | 2 +- .../user/dto/req/UserMissionReqDTO.java | 2 +- .../domain/user/dto/req/UserReqDTO.java | 32 +++++++++++ .../domain/user/dto/res/UserResDTO.java | 11 ++++ .../umc9th2/domain/user/entity/User.java | 12 ++++- .../repository/UserMissionRepository.java | 4 +- .../user/repository/UserRepository.java | 2 + .../user/service/UserMissionService.java | 1 + .../domain/user/service/UserService.java | 22 +++++++- .../global/auth/CustomUserDetails.java | 51 ++++++++++++++++++ .../global/auth/CustomUserDetailsService.java | 27 ++++++++++ .../umc9th2/global/auth/enums/Role.java | 5 ++ .../umc9th2/global/auth/enums/SocialType.java | 4 ++ .../umc9th2/global/config/SecurityConfig.java | 54 +++++++++++++++++++ 27 files changed, 325 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/example/umc9th2/domain/user/controller/UserController.java create mode 100644 src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java create mode 100644 src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java create mode 100644 src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java create mode 100644 src/main/java/com/example/umc9th2/global/auth/CustomUserDetails.java create mode 100644 src/main/java/com/example/umc9th2/global/auth/CustomUserDetailsService.java create mode 100644 src/main/java/com/example/umc9th2/global/auth/enums/Role.java create mode 100644 src/main/java/com/example/umc9th2/global/auth/enums/SocialType.java create mode 100644 src/main/java/com/example/umc9th2/global/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index f4a06c5..9fbd516 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,9 @@ dependencies { // Validation implementation 'org.springframework.boot:spring-boot-starter-validation' + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java b/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java index 1fa9e24..407dfa6 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java +++ b/src/main/java/com/example/umc9th2/domain/mission/controller/MissionController.java @@ -4,11 +4,18 @@ import com.example.umc9th2.domain.mission.dto.req.MissionReqDTO; import com.example.umc9th2.domain.mission.dto.res.MissionResDTO; import com.example.umc9th2.domain.mission.service.MissionService; +import com.example.umc9th2.global.annotation.ValidPage; import com.example.umc9th2.global.apiPayload.ApiResponse; import com.example.umc9th2.global.apiPayload.code.GeneralSuccessCode; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequiredArgsConstructor @RequestMapping("/api/missions") @@ -16,9 +23,7 @@ public class MissionController { private final MissionService missionService; - /** - * 미션 생성 - */ + @PostMapping public ApiResponse createMission( @RequestBody MissionReqDTO request diff --git a/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java b/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java index 74b4834..91ad9c6 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java +++ b/src/main/java/com/example/umc9th2/domain/mission/converter/MissionConverter.java @@ -5,6 +5,8 @@ import com.example.umc9th2.domain.mission.entity.Mission; import com.example.umc9th2.domain.store.entity.Store; +import java.util.List; + public class MissionConverter { // DTO -> Entity 변환 (미션 생성) diff --git a/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java b/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java index 834b18b..f60f6e6 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java +++ b/src/main/java/com/example/umc9th2/domain/mission/entity/Mission.java @@ -2,6 +2,8 @@ import com.example.umc9th2.domain.store.entity.Store; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -11,6 +13,7 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor @Builder public class Mission { diff --git a/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java b/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java index dd719ea..e251719 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/example/umc9th2/domain/mission/repository/MissionRepository.java @@ -1,6 +1,8 @@ package com.example.umc9th2.domain.mission.repository; import com.example.umc9th2.domain.mission.entity.Mission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface MissionRepository extends JpaRepository { diff --git a/src/main/java/com/example/umc9th2/domain/mission/service/MissionService.java b/src/main/java/com/example/umc9th2/domain/mission/service/MissionService.java index 3558a3c..59886f1 100644 --- a/src/main/java/com/example/umc9th2/domain/mission/service/MissionService.java +++ b/src/main/java/com/example/umc9th2/domain/mission/service/MissionService.java @@ -7,6 +7,8 @@ import com.example.umc9th2.domain.store.entity.Store; import com.example.umc9th2.domain.store.repository.StoreRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,4 +28,8 @@ public Mission createMission(MissionReqDTO request) { return missionRepository.save(mission); } + + public Page getStoreMissions(Long storeId, Pageable pageable) { + return missionRepository.findByStore_StoreId(storeId, pageable); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java index 6f6ef7e..a182e26 100644 --- a/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th2/domain/review/controller/ReviewController.java @@ -7,9 +7,14 @@ import com.example.umc9th2.domain.review.entity.Review; import com.example.umc9th2.domain.review.service.ReviewCommandService; import com.example.umc9th2.domain.review.service.ReviewQueryService; +import com.example.umc9th2.global.annotation.ValidPage; import com.example.umc9th2.global.apiPayload.ApiResponse; import com.example.umc9th2.global.apiPayload.code.GeneralSuccessCode; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; import java.util.List; diff --git a/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java index 2bed3b7..8306f76 100644 --- a/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/umc9th2/domain/review/repository/ReviewRepository.java @@ -1,7 +1,9 @@ package com.example.umc9th2.domain.review.repository; import com.example.umc9th2.domain.review.entity.Review; -import org.springframework.data.jpa.repository.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; public interface ReviewRepository extends JpaRepository, ReviewQueryDsl { //내가 작성한 리뷰 목록 diff --git a/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java b/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java index 9ee0bb0..d8691e2 100644 --- a/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java +++ b/src/main/java/com/example/umc9th2/domain/review/service/ReviewQueryService.java @@ -1,12 +1,14 @@ package com.example.umc9th2.domain.review.service; import com.example.umc9th2.domain.review.dto.res.ReviewSearchDTO; -import com.example.umc9th2.domain.review.entity.QReview; import com.example.umc9th2.domain.review.entity.Review; +import com.example.umc9th2.domain.review.entity.QReview; import com.example.umc9th2.domain.review.repository.ReviewRepository; import com.example.umc9th2.domain.store.entity.QStore; import com.querydsl.core.BooleanBuilder; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,7 +25,7 @@ public List searchReview(String type, String query) { QReview review = QReview.review; BooleanBuilder builder = new BooleanBuilder(); - // 동적 쿼리 조건 + if (type.equals("store")) { builder.and(review.store.storeName.contains(query)); } @@ -34,17 +36,14 @@ public List searchReview(String type, String query) { if (type.equals("both")) { String[] queries = query.split("&"); - String firstQuery = queries[0]; // storeName - String secondQuery = queries[1]; // content - + String firstQuery = queries[0]; + String secondQuery = queries[1]; builder.and(review.store.storeName.contains(firstQuery)); builder.and(review.content.contains(secondQuery)); } List reviewList = reviewRepository.searchReview(builder); return reviewList; - - } public Page getMyReviews(Long userId, Pageable pageable) { return reviewRepository.findByUser_UserId(userId, pageable); diff --git a/src/main/java/com/example/umc9th2/domain/store/exception/code/StoreErrorCode.java b/src/main/java/com/example/umc9th2/domain/store/exception/code/StoreErrorCode.java index ed1fe84..b5ea701 100644 --- a/src/main/java/com/example/umc9th2/domain/store/exception/code/StoreErrorCode.java +++ b/src/main/java/com/example/umc9th2/domain/store/exception/code/StoreErrorCode.java @@ -14,7 +14,7 @@ public enum StoreErrorCode implements BaseErrorCode { "가게를 찾을 수 없습니다."), ; - private final org.springframework.http.HttpStatus status; + private final HttpStatus status; private final String code; private final String message; } diff --git a/src/main/java/com/example/umc9th2/domain/user/controller/UserController.java b/src/main/java/com/example/umc9th2/domain/user/controller/UserController.java new file mode 100644 index 0000000..e88308f --- /dev/null +++ b/src/main/java/com/example/umc9th2/domain/user/controller/UserController.java @@ -0,0 +1,33 @@ +package com.example.umc9th2.domain.user.controller; + +import com.example.umc9th2.domain.user.dto.req.UserReqDTO; +import com.example.umc9th2.domain.user.dto.res.UserResDTO; +import com.example.umc9th2.domain.user.service.UserService; +import com.example.umc9th2.global.apiPayload.ApiResponse; +import com.example.umc9th2.global.apiPayload.code.GeneralSuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @PostMapping("/sign-up") + @Operation(summary = "회원가입", description = "새로운 사용자를 등록합니다.") + public ApiResponse signup( + @RequestBody UserReqDTO.JoinDTO request + ) { + UserResDTO.JoinDTO result = userService.signup(request); + return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, result); + } +//로그아웃 + @PostMapping("/logout") + @Operation(summary = "로그아웃", description = "사용자 로그아웃을 처리합니다.") + public ApiResponse logout() { + return ApiResponse.onSuccess(GeneralSuccessCode.OK, "로그아웃되었습니다."); + } +} + diff --git a/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java b/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java index f7babff..9c2c54c 100644 --- a/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java +++ b/src/main/java/com/example/umc9th2/domain/user/controller/UserMissionController.java @@ -3,9 +3,12 @@ import com.example.umc9th2.domain.user.dto.MyMissionDTO; import com.example.umc9th2.domain.user.dto.res.UserMissionResDTO; import com.example.umc9th2.domain.user.service.UserMissionService; +import com.example.umc9th2.global.annotation.ValidPage; import com.example.umc9th2.global.apiPayload.ApiResponse; import com.example.umc9th2.global.apiPayload.code.GeneralSuccessCode; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java b/src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java new file mode 100644 index 0000000..1483a5c --- /dev/null +++ b/src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java @@ -0,0 +1,26 @@ +package com.example.umc9th2.domain.user.converter; + +import com.example.umc9th2.domain.user.dto.req.UserReqDTO; +import com.example.umc9th2.domain.user.entity.User; +import com.example.umc9th2.global.auth.enums.Role; + +public class UserConverter { + + public static User toUser( + UserReqDTO.JoinDTO dto, + String passwordHash, + Role role + ) { + return User.builder() + .username(dto.name()) + .email(dto.email()) + .passwordHash(passwordHash) + .role(role) + .birthday(dto.birth()) + .address(dto.address() + " " + dto.specAddress()) // address와 specAddress 합치기 + .gender(dto.gender()) + .totalPoints(0) + .build(); + } +} + diff --git a/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java b/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java index aa64d55..c2f8fdf 100644 --- a/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java +++ b/src/main/java/com/example/umc9th2/domain/user/dto/MyMissionDTO.java @@ -8,7 +8,7 @@ public record MyMissionDTO( String description, Integer rewardPoints, boolean status, // String에서 바꿈 - LocalDateTime clearedAt, LocalDateTime clearedAt, + LocalDateTime clearedAt, String storeName, String regionName ) {} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/domain/user/dto/req/UserMissionReqDTO.java b/src/main/java/com/example/umc9th2/domain/user/dto/req/UserMissionReqDTO.java index 629de7b..e362909 100644 --- a/src/main/java/com/example/umc9th2/domain/user/dto/req/UserMissionReqDTO.java +++ b/src/main/java/com/example/umc9th2/domain/user/dto/req/UserMissionReqDTO.java @@ -6,6 +6,6 @@ @Getter public class UserMissionReqDTO { - // 필요할지도..? + } diff --git a/src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java b/src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java new file mode 100644 index 0000000..f1b8b84 --- /dev/null +++ b/src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java @@ -0,0 +1,32 @@ +package com.example.umc9th2.domain.user.dto.req; + +import com.example.umc9th2.domain.user.entity.User; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; +import java.util.List; + +public class UserReqDTO { + + public record JoinDTO( + @NotBlank + String name, + @Email + String email, + @NotBlank + String password, + @NotNull + User.Gender gender, + @NotNull + LocalDate birth, + @NotNull + String address, + @NotNull + String specAddress, + + List preferCategory + ) {} +} + diff --git a/src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java b/src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java new file mode 100644 index 0000000..ecc27eb --- /dev/null +++ b/src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java @@ -0,0 +1,11 @@ +package com.example.umc9th2.domain.user.dto.res; + +public class UserResDTO { + + public record JoinDTO( + Long userId, + String username, + String email + ) {} +} + diff --git a/src/main/java/com/example/umc9th2/domain/user/entity/User.java b/src/main/java/com/example/umc9th2/domain/user/entity/User.java index 436277a..369633f 100644 --- a/src/main/java/com/example/umc9th2/domain/user/entity/User.java +++ b/src/main/java/com/example/umc9th2/domain/user/entity/User.java @@ -6,7 +6,10 @@ import com.example.umc9th2.domain.user.entity.mapping.UserTerm; import com.example.umc9th2.domain.user.entity.mapping.UserFood; +import com.example.umc9th2.global.auth.enums.Role; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -19,13 +22,15 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor +@Builder public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; - @Column(nullable = false, length = 100) + @Column(nullable = false, unique = true, length = 100) private String email; @Column(nullable = false, length = 50) @@ -37,6 +42,10 @@ public class User extends BaseEntity { @Column(nullable = false) private Integer totalPoints = 0; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + @Enumerated(EnumType.STRING) @Column(nullable = true, length = 10) private Gender gender; @@ -63,4 +72,5 @@ public class User extends BaseEntity { public enum Gender { 남, 여 } + } diff --git a/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java b/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java index 3c51afe..691afb5 100644 --- a/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java +++ b/src/main/java/com/example/umc9th2/domain/user/repository/UserMissionRepository.java @@ -3,8 +3,10 @@ import com.example.umc9th2.domain.user.dto.MyMissionDTO; import com.example.umc9th2.domain.user.entity.mapping.UserMission; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.*; +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; diff --git a/src/main/java/com/example/umc9th2/domain/user/repository/UserRepository.java b/src/main/java/com/example/umc9th2/domain/user/repository/UserRepository.java index 7efdf66..46df618 100644 --- a/src/main/java/com/example/umc9th2/domain/user/repository/UserRepository.java +++ b/src/main/java/com/example/umc9th2/domain/user/repository/UserRepository.java @@ -10,6 +10,8 @@ public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + @Query(""" select new com.example.umc9th2.domain.user.dto.MyPageDTO( u.userId, diff --git a/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java b/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java index a25b7b4..0dc5e27 100644 --- a/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java +++ b/src/main/java/com/example/umc9th2/domain/user/service/UserMissionService.java @@ -10,6 +10,7 @@ import com.example.umc9th2.domain.mission.entity.Mission; import com.example.umc9th2.domain.mission.repository.MissionRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/com/example/umc9th2/domain/user/service/UserService.java b/src/main/java/com/example/umc9th2/domain/user/service/UserService.java index 569eafe..7cc5afa 100644 --- a/src/main/java/com/example/umc9th2/domain/user/service/UserService.java +++ b/src/main/java/com/example/umc9th2/domain/user/service/UserService.java @@ -1,19 +1,39 @@ package com.example.umc9th2.domain.user.service; import com.example.umc9th2.domain.user.dto.MyPageDTO; +import com.example.umc9th2.domain.user.dto.req.UserReqDTO; +import com.example.umc9th2.domain.user.dto.res.UserResDTO; +import com.example.umc9th2.domain.user.entity.User; import com.example.umc9th2.domain.user.repository.UserRepository; +import com.example.umc9th2.domain.user.converter.UserConverter; +import com.example.umc9th2.global.auth.enums.Role; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -@Transactional(readOnly = true) @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + @Transactional(readOnly = true) public MyPageDTO getMyPage(Long userId) { return userRepository.findMyPageSummary(userId) .orElseThrow(() -> new IllegalArgumentException("User not found")); } + + // 회원가입 + @Transactional + public UserResDTO.JoinDTO signup(UserReqDTO.JoinDTO dto) { + String passwordHash = passwordEncoder.encode(dto.password()); + User user = UserConverter.toUser(dto, passwordHash, Role.ROLE_USER); + User savedUser = userRepository.save(user); + return new UserResDTO.JoinDTO( + savedUser.getUserId(), + savedUser.getUsername(), + savedUser.getEmail() + ); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/global/auth/CustomUserDetails.java b/src/main/java/com/example/umc9th2/global/auth/CustomUserDetails.java new file mode 100644 index 0000000..b1cd490 --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/auth/CustomUserDetails.java @@ -0,0 +1,51 @@ +package com.example.umc9th2.global.auth; + +import com.example.umc9th2.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + + private final User user; + + @Override + public Collection getAuthorities() { + return List.of(() -> user.getRole().toString()); + } + + @Override + public String getPassword() { + return user.getPasswordHash(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} + diff --git a/src/main/java/com/example/umc9th2/global/auth/CustomUserDetailsService.java b/src/main/java/com/example/umc9th2/global/auth/CustomUserDetailsService.java new file mode 100644 index 0000000..95d3de6 --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/auth/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.umc9th2.global.auth; + +import com.example.umc9th2.domain.user.entity.User; +import com.example.umc9th2.domain.user.exception.code.UserErrorCode; +import com.example.umc9th2.domain.user.repository.UserRepository; +import com.example.umc9th2.global.apiPayload.exception.GeneralException; +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 +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByEmail(username) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + return new CustomUserDetails(user); + } +} + diff --git a/src/main/java/com/example/umc9th2/global/auth/enums/Role.java b/src/main/java/com/example/umc9th2/global/auth/enums/Role.java new file mode 100644 index 0000000..fc234d0 --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/auth/enums/Role.java @@ -0,0 +1,5 @@ +package com.example.umc9th2.global.auth.enums; + +public enum Role { + ROLE_ADMIN, ROLE_USER +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/global/auth/enums/SocialType.java b/src/main/java/com/example/umc9th2/global/auth/enums/SocialType.java new file mode 100644 index 0000000..12808cf --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/auth/enums/SocialType.java @@ -0,0 +1,4 @@ +package com.example.umc9th2.global.auth.enums; + +public enum SocialType { +} diff --git a/src/main/java/com/example/umc9th2/global/config/SecurityConfig.java b/src/main/java/com/example/umc9th2/global/config/SecurityConfig.java new file mode 100644 index 0000000..925e1d3 --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/config/SecurityConfig.java @@ -0,0 +1,54 @@ +package com.example.umc9th2.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 +@Configuration +public class SecurityConfig { + + private final String[] allowUris = { + "/", + "/sign-up", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/v3/api-docs/**", + "/v3/api-docs", + "/api-docs/**", + "/api-docs", + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) + .permitAll() + ) + .csrf(AbstractHttpConfigurer::disable) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file From 27b8be98c3caa168518ee228e1056c40c229cae6 Mon Sep 17 00:00:00 2001 From: seohyunk09 <2022112400@dgu.ac.kr> Date: Sun, 21 Dec 2025 01:11:10 +0900 Subject: [PATCH 4/4] =?UTF-8?q?mission/#10=20=EC=8B=A4=EC=8A=B5=202:=20JWT?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + build.gradle | 7 ++ .../user/controller/UserController.java | 17 +++- .../domain/user/converter/UserConverter.java | 8 ++ .../domain/user/dto/req/UserReqDTO.java | 8 ++ .../domain/user/dto/res/UserResDTO.java | 6 ++ .../user/exception/code/UserErrorCode.java | 3 + .../domain/user/service/UserService.java | 21 +++++ .../umc9th2/global/auth/JwtAuthFilter.java | 68 +++++++++++++++ .../example/umc9th2/global/auth/JwtUtil.java | 84 +++++++++++++++++++ .../umc9th2/global/config/SecurityConfig.java | 36 ++++---- src/main/resources/application.yml | 8 +- 12 files changed, 251 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/example/umc9th2/global/auth/JwtAuthFilter.java create mode 100644 src/main/java/com/example/umc9th2/global/auth/JwtUtil.java diff --git a/.gitignore b/.gitignore index 8e71103..909475e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ out/ # QueryDSL 자동 생성 Q 클래스 무시 /build/generated/querydsl/ + +# 로컬 환경 설정 파일 +src/main/resources/application-local.yml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 9fbd516..16e5beb 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,13 @@ dependencies { // Security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + + // Jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + } tasks.named('test') { diff --git a/src/main/java/com/example/umc9th2/domain/user/controller/UserController.java b/src/main/java/com/example/umc9th2/domain/user/controller/UserController.java index e88308f..bd6045a 100644 --- a/src/main/java/com/example/umc9th2/domain/user/controller/UserController.java +++ b/src/main/java/com/example/umc9th2/domain/user/controller/UserController.java @@ -6,6 +6,7 @@ import com.example.umc9th2.global.apiPayload.ApiResponse; import com.example.umc9th2.global.apiPayload.code.GeneralSuccessCode; import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -15,15 +16,27 @@ public class UserController { private final UserService userService; + // 회원가입 @PostMapping("/sign-up") @Operation(summary = "회원가입", description = "새로운 사용자를 등록합니다.") public ApiResponse signup( - @RequestBody UserReqDTO.JoinDTO request + @RequestBody @Valid UserReqDTO.JoinDTO request ) { UserResDTO.JoinDTO result = userService.signup(request); return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, result); } -//로그아웃 + + // 로그인 + @PostMapping("/login") + @Operation(summary = "로그인", description = "사용자 로그인을 처리합니다.") + public ApiResponse login( + @RequestBody @Valid UserReqDTO.LoginDTO dto + ) { + UserResDTO.LoginDTO result = userService.login(dto); + return ApiResponse.onSuccess(GeneralSuccessCode.OK, result); + } + + // 로그아웃 @PostMapping("/logout") @Operation(summary = "로그아웃", description = "사용자 로그아웃을 처리합니다.") public ApiResponse logout() { diff --git a/src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java b/src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java index 1483a5c..e01937d 100644 --- a/src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java +++ b/src/main/java/com/example/umc9th2/domain/user/converter/UserConverter.java @@ -1,6 +1,7 @@ package com.example.umc9th2.domain.user.converter; import com.example.umc9th2.domain.user.dto.req.UserReqDTO; +import com.example.umc9th2.domain.user.dto.res.UserResDTO; import com.example.umc9th2.domain.user.entity.User; import com.example.umc9th2.global.auth.enums.Role; @@ -22,5 +23,12 @@ public static User toUser( .totalPoints(0) .build(); } + + public static UserResDTO.LoginDTO toLoginDTO(User user, String accessToken) { + return new UserResDTO.LoginDTO( + user.getUserId(), + accessToken + ); + } } diff --git a/src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java b/src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java index f1b8b84..7a41263 100644 --- a/src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java +++ b/src/main/java/com/example/umc9th2/domain/user/dto/req/UserReqDTO.java @@ -28,5 +28,13 @@ public record JoinDTO( List preferCategory ) {} + + // 로그인 + public record LoginDTO( + @NotBlank + String email, + @NotBlank + String password + ) {} } diff --git a/src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java b/src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java index ecc27eb..335e09e 100644 --- a/src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java +++ b/src/main/java/com/example/umc9th2/domain/user/dto/res/UserResDTO.java @@ -7,5 +7,11 @@ public record JoinDTO( String username, String email ) {} + + // 로그인 + public record LoginDTO( + Long userId, + String accessToken + ) {} } diff --git a/src/main/java/com/example/umc9th2/domain/user/exception/code/UserErrorCode.java b/src/main/java/com/example/umc9th2/domain/user/exception/code/UserErrorCode.java index 4a2a247..7be15eb 100644 --- a/src/main/java/com/example/umc9th2/domain/user/exception/code/UserErrorCode.java +++ b/src/main/java/com/example/umc9th2/domain/user/exception/code/UserErrorCode.java @@ -12,6 +12,9 @@ public enum UserErrorCode implements BaseErrorCode { USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404_1", "사용자를 찾을 수 없습니다."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, + "USER401_1", + "비밀번호가 일치하지 않습니다."), ; private final org.springframework.http.HttpStatus status; diff --git a/src/main/java/com/example/umc9th2/domain/user/service/UserService.java b/src/main/java/com/example/umc9th2/domain/user/service/UserService.java index 7cc5afa..6514ba9 100644 --- a/src/main/java/com/example/umc9th2/domain/user/service/UserService.java +++ b/src/main/java/com/example/umc9th2/domain/user/service/UserService.java @@ -4,8 +4,12 @@ import com.example.umc9th2.domain.user.dto.req.UserReqDTO; import com.example.umc9th2.domain.user.dto.res.UserResDTO; import com.example.umc9th2.domain.user.entity.User; +import com.example.umc9th2.domain.user.exception.code.UserErrorCode; import com.example.umc9th2.domain.user.repository.UserRepository; import com.example.umc9th2.domain.user.converter.UserConverter; +import com.example.umc9th2.global.apiPayload.exception.GeneralException; +import com.example.umc9th2.global.auth.CustomUserDetails; +import com.example.umc9th2.global.auth.JwtUtil; import com.example.umc9th2.global.auth.enums.Role; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @@ -17,6 +21,7 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; @Transactional(readOnly = true) public MyPageDTO getMyPage(Long userId) { @@ -36,4 +41,20 @@ public UserResDTO.JoinDTO signup(UserReqDTO.JoinDTO dto) { savedUser.getEmail() ); } + + // 로그인 + @Transactional(readOnly = true) + public UserResDTO.LoginDTO login(UserReqDTO.LoginDTO dto) { + // User 조회 + User user = userRepository.findByEmail(dto.email()) + .orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND)); + + // 비밀번호 검증 + if (!passwordEncoder.matches(dto.password(), user.getPasswordHash())) { + throw new GeneralException(UserErrorCode.INVALID_PASSWORD); + } + CustomUserDetails userDetails = new CustomUserDetails(user); + String accessToken = jwtUtil.createAccessToken(userDetails); + return UserConverter.toLoginDTO(user, accessToken); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th2/global/auth/JwtAuthFilter.java b/src/main/java/com/example/umc9th2/global/auth/JwtAuthFilter.java new file mode 100644 index 0000000..43239a2 --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/auth/JwtAuthFilter.java @@ -0,0 +1,68 @@ +package com.example.umc9th2.global.auth; + +import com.example.umc9th2.global.apiPayload.ApiResponse; +import com.example.umc9th2.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + try { + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + token = token.replace("Bearer ", ""); + // AccessToken 검증 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 + String email = jwtUtil.getEmail(token); + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} + diff --git a/src/main/java/com/example/umc9th2/global/auth/JwtUtil.java b/src/main/java/com/example/umc9th2/global/auth/JwtUtil.java new file mode 100644 index 0000000..3df8f30 --- /dev/null +++ b/src/main/java/com/example/umc9th2/global/auth/JwtUtil.java @@ -0,0 +1,84 @@ +package com.example.umc9th2.global.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(CustomUserDetails user) { + return createToken(user, accessExpiration); + } + + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(CustomUserDetails user, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(user.getUsername()) + .claim("role", authorities) + .claim("email", user.getUsername()) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plus(expiration))) + .signWith(secretKey) + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} + diff --git a/src/main/java/com/example/umc9th2/global/config/SecurityConfig.java b/src/main/java/com/example/umc9th2/global/config/SecurityConfig.java index 925e1d3..ca88da4 100644 --- a/src/main/java/com/example/umc9th2/global/config/SecurityConfig.java +++ b/src/main/java/com/example/umc9th2/global/config/SecurityConfig.java @@ -1,5 +1,9 @@ package com.example.umc9th2.global.config; +import com.example.umc9th2.global.auth.CustomUserDetailsService; +import com.example.umc9th2.global.auth.JwtAuthFilter; +import com.example.umc9th2.global.auth.JwtUtil; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -8,21 +12,23 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + private final String[] allowUris = { - "/", "/sign-up", + "/login", + "/logout", "/swagger-ui/**", - "/swagger-ui.html", "/swagger-resources/**", "/v3/api-docs/**", - "/v3/api-docs", - "/api-docs/**", - "/api-docs", }; @Bean @@ -33,20 +39,20 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) - .csrf(AbstractHttpConfigurer::disable) - .logout(logout -> logout - .logoutUrl("/logout") - .logoutSuccessUrl("/login?logout") - .permitAll() - ); + + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) + .csrf(AbstractHttpConfigurer::disable); return http.build(); } + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e31568d..0a85109 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,10 @@ spring: ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? properties: hibernate: - format_sql: true # ???? SQL ??? ?? ?? ??? \ No newline at end of file + format_sql: true # ???? SQL ??? ?? ?? ??? + +jwt: + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 14400000 \ No newline at end of file