From 17bb5cf219ebd1bb417c0f504cf8341eeb46fc1b Mon Sep 17 00:00:00 2001 From: Eugene Shin Date: Sun, 17 May 2026 23:40:23 +0900 Subject: [PATCH 1/2] mission/#07 --- .../dto/response/MissionResponseDto.java | 22 ++++++- .../dto/response/ReviewResponseDto.java | 17 +++++ .../review/repository/ReviewRepository.java | 28 ++++++++ .../user/controller/UserAuthController.java | 3 +- .../user/controller/UserController.java | 33 ++++++++-- .../domain/user/converter/UserConverter.java | 57 ++++++++++++++++ .../user/dto/request/UserRequestDto.java | 7 ++ .../UserDoingMissionRepository.java | 6 ++ .../domain/user/service/UserService.java | 66 +++++++++++++++++++ .../handler/GeneralExceptionAdvice.java | 15 +++-- 10 files changed, 238 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java b/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java index 87f30c7..877a1e9 100644 --- a/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java +++ b/src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; -import java.time.LocalDate; import java.util.List; public class MissionResponseDto { @@ -22,4 +21,25 @@ public record GetMission ( public record CountMissions( int count ){} + + @Builder + public record GetMissionsPaged( + List missions, + Integer pageNumber, + Integer pageSize, + Integer totalPages, + Long totalElements, + Boolean first, + Boolean last + ) { + @Builder + public record GetMission( + Long missionId, + String storeName, + String title, + String content, + Integer reward, + String status + ) {} + } } diff --git a/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java b/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java index ea186de..dd81a0a 100644 --- a/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java +++ b/src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java @@ -19,4 +19,21 @@ public record ReviewItem ( LocalDateTime createdAt ){} } + + @Builder + public record GetMyReviewsPaged( + List reviews, + Boolean hasNext, + String nextCursor, + Integer pageSize + ) { + @Builder + public record ReviewItem( + Long reviewId, + String nickname, + String stars, + String content, + LocalDateTime createdAt + ) {} + } } diff --git a/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java b/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java index e03e344..5e75d27 100644 --- a/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java @@ -2,8 +2,10 @@ import com.umc.umc10th.domain.review.entity.Review; +import com.umc.umc10th.domain.review.enums.Stars; 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; @@ -29,4 +31,30 @@ boolean existsByStoreAndUser( @Param("storeId") Long storeId, @Param("userId") Long userId ); + + Slice findReviewsByUser_IdOrderByIdDesc( + Long userId, + Pageable pageable + ); + + Slice findReviewsByUser_IdAndIdLessThanOrderByIdDesc( + Long userId, + Long idCursor, + Pageable pageable + ); + + Slice findReviewsByUser_IdOrderByStarsDescIdDesc( + Long userId, + Pageable pageable + ); + + @Query("SELECT r FROM Review r WHERE r.user.id = :userId " + + "AND (r.stars < :starsCursor OR (r.stars = :starsCursor AND r.id < :idCursor)) " + + "ORDER BY r.stars DESC, r.id DESC") + Slice findReviewsByUser_IdOrderByStarsDesc( + @Param("userId") Long userId, + @Param("starsCursor") Stars starsCursor, + @Param("idCursor") Long idCursor, + Pageable pageable + ); } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java b/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java index e7693b5..648e7a7 100644 --- a/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java +++ b/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java @@ -5,6 +5,7 @@ import com.umc.umc10th.domain.user.service.UserService; import com.umc.umc10th.global.apipayload.ApiResponse; import com.umc.umc10th.global.apipayload.code.BaseSuccessCode; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -16,7 +17,7 @@ public class UserAuthController { private final UserService userService; // @PostMapping("/signup") -// public ApiResponse getInfo(@RequestBody UserRequestDto.CreateUser dto) { +// public ApiResponse getInfo(@Valid @RequestBody UserRequestDto.CreateUser dto) { // BaseSuccessCode code = UserSuccessCode.OK; // userService.createUser(dto); // return ApiResponse.onSuccess(code); diff --git a/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java b/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java index 021b8d3..bdb17fd 100644 --- a/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java +++ b/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java @@ -1,16 +1,16 @@ package com.umc.umc10th.domain.user.controller; import com.umc.umc10th.domain.mission.dto.response.MissionResponseDto; +import com.umc.umc10th.domain.review.dto.response.ReviewResponseDto; import com.umc.umc10th.domain.user.apipayload.code.UserSuccessCode; +import com.umc.umc10th.domain.user.dto.request.UserRequestDto; import com.umc.umc10th.domain.user.dto.response.UserResponseDto; import com.umc.umc10th.domain.user.service.UserService; import com.umc.umc10th.global.apipayload.ApiResponse; import com.umc.umc10th.global.apipayload.code.BaseSuccessCode; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/users") @@ -26,11 +26,30 @@ public ApiResponse getInfo() { } @GetMapping("/me/missions") - public ApiResponse getMissions(@RequestParam Long locationId, @RequestParam String status) { - BaseSuccessCode code = UserSuccessCode.OK; - return ApiResponse.onSuccess(code, userService.getMissions(locationId, status)); + public ApiResponse getMissions( + @RequestParam Long locationId, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(defaultValue = "0") Integer pageNumber, + @RequestBody @Valid UserRequestDto.GetMissionsRequest request) { + + return ApiResponse.onSuccess( + UserSuccessCode.OK, + userService.getMissions(locationId, pageSize, pageNumber, request) + ); } + @GetMapping("/me/reviews") + public ApiResponse getMyReviews( + @RequestParam Long userId, + @RequestParam(defaultValue = "10") Integer pageSize, + @RequestParam(defaultValue = "-1") String cursor, + @RequestParam(defaultValue = "ID") String query) { + + return ApiResponse.onSuccess( + UserSuccessCode.OK, + userService.getMyReviews(userId, pageSize, cursor, query) + ); + } // @GetMapping("/me/missions/count") // public ApiResponse countMissions(@RequestParam Long locationId) { // BaseSuccessCode code = UserSuccessCode.OK; diff --git a/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java b/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java index 1ca67d2..0675689 100644 --- a/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java +++ b/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java @@ -1,4 +1,61 @@ package com.umc.umc10th.domain.user.converter; +import com.umc.umc10th.domain.mission.dto.response.MissionResponseDto; +import com.umc.umc10th.domain.review.dto.response.ReviewResponseDto; +import com.umc.umc10th.domain.review.entity.Review; +import com.umc.umc10th.domain.user.entity.UserDoingMission; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; + +import java.util.List; + public class UserConverter { + + public static MissionResponseDto.GetMissionsPaged toGetMissionsPaged( + Page page) { + + List missions = + page.getContent().stream() + .map(udm -> MissionResponseDto.GetMissionsPaged.GetMission.builder() + .missionId(udm.getMission().getId()) + .storeName(udm.getMission().getStore().getStoreName()) + .title(udm.getMission().getTitle()) + .content(udm.getMission().getContent()) + .reward(udm.getMission().getReward()) + .status(udm.getIsCompleted() ? "COMPLETED" : "IN_PROGRESS") + .build()) + .toList(); + + return MissionResponseDto.GetMissionsPaged.builder() + .missions(missions) + .pageNumber(page.getNumber()) + .pageSize(page.getSize()) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .first(page.isFirst()) + .last(page.isLast()) + .build(); + } + + public static ReviewResponseDto.GetMyReviewsPaged toGetMyReviewsPaged( + Slice slice, String nextCursor) { + + List reviews = + slice.getContent().stream() + .map(r -> ReviewResponseDto.GetMyReviewsPaged.ReviewItem.builder() + .reviewId(r.getId()) + .nickname(r.getUser().getName()) + .stars(r.getStars().name()) + .content(r.getContent()) + .createdAt(r.getCreatedAt()) + .build()) + .toList(); + + return ReviewResponseDto.GetMyReviewsPaged.builder() + .reviews(reviews) + .hasNext(slice.hasNext()) + .nextCursor(nextCursor) + .pageSize(slice.getSize()) + .build(); + } } diff --git a/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java index 22109e0..19715cf 100644 --- a/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java @@ -3,6 +3,7 @@ import com.umc.umc10th.domain.user.enums.Provider; import com.umc.umc10th.domain.user.enums.ServiceRole; import com.umc.umc10th.domain.user.enums.Sex; +import jakarta.validation.constraints.*; import lombok.Builder; import java.time.LocalDate; @@ -20,4 +21,10 @@ public record CreateUser ( String address, String phone ){} + + @Builder + public record GetMissionsRequest( + @NotNull(message = "사용자 ID는 필수입니다") + Long userId + ) {} } diff --git a/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java b/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java index cee16aa..5616ef8 100644 --- a/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java +++ b/src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java @@ -21,4 +21,10 @@ Page findMyMissions( @Param("status") String status, Pageable pageable ); + + Page findAllByUser_IdAndMission_Location_Id( + Long userId, + Long locationId, + Pageable pageable + ); } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/domain/user/service/UserService.java b/src/main/java/com/umc/umc10th/domain/user/service/UserService.java index 15098ae..f883641 100644 --- a/src/main/java/com/umc/umc10th/domain/user/service/UserService.java +++ b/src/main/java/com/umc/umc10th/domain/user/service/UserService.java @@ -3,7 +3,10 @@ import com.umc.umc10th.domain.mission.dto.response.MissionResponseDto; import com.umc.umc10th.domain.review.dto.response.ReviewResponseDto; import com.umc.umc10th.domain.review.entity.Review; +import com.umc.umc10th.domain.review.enums.Stars; import com.umc.umc10th.domain.review.repository.ReviewRepository; +import com.umc.umc10th.domain.user.converter.UserConverter; +import com.umc.umc10th.domain.user.dto.request.UserRequestDto; import com.umc.umc10th.domain.user.dto.response.UserResponseDto; import com.umc.umc10th.domain.user.entity.User; import com.umc.umc10th.domain.user.entity.UserDoingMission; @@ -13,6 +16,7 @@ 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -78,4 +82,66 @@ public ReviewResponseDto.GetMyReviews getMyReviews() { .reviews(items) .build(); } + + public MissionResponseDto.GetMissionsPaged getMissions( + Long locationId, + Integer pageSize, + Integer pageNumber, + UserRequestDto.GetMissionsRequest request + ) { + + PageRequest pageRequest = PageRequest.of(pageNumber, pageSize); + + Page page = + userDoingMissionRepository.findAllByUser_IdAndMission_Location_Id( + request.userId(), locationId, pageRequest + ); + + return UserConverter.toGetMissionsPaged(page); + } + + public ReviewResponseDto.GetMyReviewsPaged getMyReviews( + Long userId, Integer pageSize, String cursor, String query) { // ← 파라미터 분리 + + PageRequest pageRequest = PageRequest.of(0, pageSize); + Slice slice; + + if (!cursor.equals("-1")) { + String[] split = cursor.split(":"); + switch (query.toUpperCase()) { + case "ID" -> { + Long idCursor = Long.parseLong(split[1]); + slice = reviewRepository.findReviewsByUser_IdAndIdLessThanOrderByIdDesc( + userId, idCursor, pageRequest + ); + } + case "STARS" -> { + Stars starsCursor = Stars.valueOf(split[0]); + Long idCursor = Long.parseLong(split[1]); + slice = reviewRepository.findReviewsByUser_IdOrderByStarsDesc( + userId, starsCursor, idCursor, pageRequest + ); + } + default -> throw new IllegalArgumentException("유효하지 않은 정렬 기준입니다: " + query); + } + } else { + slice = switch (query.toUpperCase()) { + case "STARS" -> reviewRepository.findReviewsByUser_IdOrderByStarsDescIdDesc( + userId, pageRequest + ); + default -> reviewRepository.findReviewsByUser_IdOrderByIdDesc( + userId, pageRequest + ); + }; + } + + String nextCursor = null; + if (slice.hasNext() && !slice.getContent().isEmpty()) { + List content = slice.getContent(); + Review last = content.get(content.size() - 1); + nextCursor = last.getStars().name() + ":" + last.getId(); + } + + return UserConverter.toGetMyReviewsPaged(slice, nextCursor); + } } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java index 365ca2a..2040958 100644 --- a/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java @@ -11,7 +11,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import java.net.BindException; -import java.util.stream.Collectors; +import java.util.HashMap; +import java.util.Map; @Slf4j @RestControllerAdvice @@ -28,16 +29,16 @@ public ResponseEntity> handleProjectException( } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleMethodArgumentValidation( - MethodArgumentNotValidException e) { + public ResponseEntity>> handleMethodArgumentValidation( + MethodArgumentNotValidException e + ) { - String message = e.getBindingResult().getFieldErrors().stream() - .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) - .collect(Collectors.joining(", ")); + 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, null)); + .body(ApiResponse.onFailure(code, errors)); } @ExceptionHandler(BindException.class) From 3b121ed69cfa81a5f4899756caff8aadf5fad458 Mon Sep 17 00:00:00 2001 From: Eugene Shin Date: Sun, 24 May 2026 14:58:57 +0900 Subject: [PATCH 2/2] mission/#8 --- .omc/project-memory.json | 345 ++++++++++++++++++ .../703ef31d-bc8c-48fd-bbf5-151f4e400a5e.json | 8 + .../e01a8ae3-80eb-464d-aa34-b036d2c801d6.json | 8 + .../hud-state.json | 6 + .../hud-state.json | 6 + .../user/controller/UserAuthController.java | 12 +- .../user/controller/UserController.java | 2 +- .../domain/user/converter/UserConverter.java | 22 ++ .../user/dto/request/UserRequestDto.java | 9 +- .../umc/umc10th/domain/user/entity/User.java | 12 +- .../umc10th/domain/user/enums/Provider.java | 1 + .../domain/user/enums/ServiceRole.java | 2 + .../umc10th/domain/user/enums/SystemRole.java | 2 + .../domain/user/exception/UserException.java | 9 +- .../user/exception/code/UserErrorCode.java | 21 +- .../user/repository/UserRepository.java | 4 + .../domain/user/service/UserService.java | 44 ++- .../apipayload/code/GeneralErrorCode.java | 2 +- .../handler/GeneralExceptionAdvice.java | 2 +- .../umc10th/global/config/SecurityConfig.java | 59 +++ .../umc/umc10th/global/security/AuthUser.java | 35 ++ .../security/CustomUserDetailsService.java | 23 ++ .../security/handler/CustomAccessDenied.java | 33 ++ .../security/handler/CustomEntryPoint.java | 33 ++ 24 files changed, 671 insertions(+), 29 deletions(-) create mode 100644 .omc/project-memory.json create mode 100644 .omc/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e.json create mode 100644 .omc/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6.json create mode 100644 .omc/state/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e/hud-state.json create mode 100644 .omc/state/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6/hud-state.json create mode 100644 src/main/java/com/umc/umc10th/global/config/SecurityConfig.java create mode 100644 src/main/java/com/umc/umc10th/global/security/AuthUser.java create mode 100644 src/main/java/com/umc/umc10th/global/security/CustomUserDetailsService.java create mode 100644 src/main/java/com/umc/umc10th/global/security/handler/CustomAccessDenied.java create mode 100644 src/main/java/com/umc/umc10th/global/security/handler/CustomEntryPoint.java diff --git a/.omc/project-memory.json b/.omc/project-memory.json new file mode 100644 index 0000000..730370a --- /dev/null +++ b/.omc/project-memory.json @@ -0,0 +1,345 @@ +{ + "version": "1.0.0", + "lastScanned": 1779538463568, + "projectRoot": "/Users/shin-yujin/Desktop/UMC/umc10th", + "techStack": { + "languages": [ + { + "name": "Java/Kotlin", + "version": null, + "confidence": "high", + "markers": [ + "build.gradle" + ] + } + ], + "frameworks": [], + "packageManager": "gradle", + "runtime": null + }, + "build": { + "buildCommand": null, + "testCommand": null, + "lintCommand": null, + "devCommand": null, + "scripts": {} + }, + "conventions": { + "namingStyle": null, + "importStyle": null, + "testPattern": null, + "fileOrganization": null + }, + "structure": { + "isMonorepo": false, + "workspaces": [], + "mainDirectories": [ + "bin", + "src" + ], + "gitBranches": { + "defaultBranch": "main", + "branchingStrategy": null + } + }, + "customNotes": [], + "directoryMap": { + "bin": { + "path": "bin", + "purpose": "Executable scripts", + "fileCount": 0, + "lastAccessed": 1779538463562, + "keyFiles": [] + }, + "build": { + "path": "build", + "purpose": "Build output", + "fileCount": 0, + "lastAccessed": 1779538463563, + "keyFiles": [] + }, + "gradle": { + "path": "gradle", + "purpose": null, + "fileCount": 0, + "lastAccessed": 1779538463563, + "keyFiles": [] + }, + "src": { + "path": "src", + "purpose": "Source code", + "fileCount": 0, + "lastAccessed": 1779538463564, + "keyFiles": [] + }, + "bin/test": { + "path": "bin/test", + "purpose": "Test files", + "fileCount": 0, + "lastAccessed": 1779538463564, + "keyFiles": [] + }, + "src/test": { + "path": "src/test", + "purpose": "Test files", + "fileCount": 0, + "lastAccessed": 1779538463565, + "keyFiles": [] + } + }, + "hotPaths": [ + { + "path": "src/main/java/com/umc/umc10th/domain/user/controller/UserController.java", + "accessCount": 6, + "lastAccessed": 1779601161395, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/service/UserService.java", + "accessCount": 6, + "lastAccessed": 1779602172842, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/entity/User.java", + "accessCount": 6, + "lastAccessed": 1779602314072, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java", + "accessCount": 5, + "lastAccessed": 1779602172343, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java", + "accessCount": 4, + "lastAccessed": 1779602171841, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java", + "accessCount": 4, + "lastAccessed": 1779602222913, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/apipayload/code/GeneralErrorCode.java", + "accessCount": 3, + "lastAccessed": 1779601453594, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java", + "accessCount": 3, + "lastAccessed": 1779601563249, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/repository/UserRepository.java", + "accessCount": 3, + "lastAccessed": 1779601792705, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/enums/ServiceRole.java", + "accessCount": 3, + "lastAccessed": 1779601826203, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/exception/code/UserErrorCode.java", + "accessCount": 3, + "lastAccessed": 1779602223874, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/exception/UserException.java", + "accessCount": 3, + "lastAccessed": 1779602224176, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/enums/SystemRole.java", + "accessCount": 2, + "lastAccessed": 1779601670600, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/enums/Provider.java", + "accessCount": 2, + "lastAccessed": 1779601694834, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/security/AuthUser.java", + "accessCount": 2, + "lastAccessed": 1779602219039, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/security/CustomUserDetailsService.java", + "accessCount": 2, + "lastAccessed": 1779602220184, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/security/handler/CustomAccessDenied.java", + "accessCount": 2, + "lastAccessed": 1779602220683, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/security/handler/CustomEntryPoint.java", + "accessCount": 2, + "lastAccessed": 1779602221198, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/config/SecurityConfig.java", + "accessCount": 2, + "lastAccessed": 1779602222290, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/mission/controller/MissionController.java", + "accessCount": 1, + "lastAccessed": 1779538514621, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/notification/controller/NotificationController.java", + "accessCount": 1, + "lastAccessed": 1779538515847, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/question/controller/QuestionController.java", + "accessCount": 1, + "lastAccessed": 1779538515878, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/review/controller/ReviewController.java", + "accessCount": 1, + "lastAccessed": 1779538516444, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/store/controller/StoreController.java", + "accessCount": 1, + "lastAccessed": 1779538516917, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/mission/service/MissionService.java", + "accessCount": 1, + "lastAccessed": 1779538525590, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/review/service/ReviewService.java", + "accessCount": 1, + "lastAccessed": 1779538525840, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/mission/dto/request/MissionRequestDto.java", + "accessCount": 1, + "lastAccessed": 1779538526955, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/mission/dto/response/MissionResponseDto.java", + "accessCount": 1, + "lastAccessed": 1779538527940, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/review/dto/request/ReviewRequestDto.java", + "accessCount": 1, + "lastAccessed": 1779538528485, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/review/dto/response/ReviewResponseDto.java", + "accessCount": 1, + "lastAccessed": 1779538528875, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/dto/response/UserResponseDto.java", + "accessCount": 1, + "lastAccessed": 1779538530192, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/review/repository/ReviewRepository.java", + "accessCount": 1, + "lastAccessed": 1779538548764, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/repository/UserDoingMissionRepository.java", + "accessCount": 1, + "lastAccessed": 1779538549664, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/mission/repository/MissionRepository.java", + "accessCount": 1, + "lastAccessed": 1779538549829, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/store/repository/StoreRepository.java", + "accessCount": 1, + "lastAccessed": 1779538551139, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/user/entity/UserDoingMission.java", + "accessCount": 1, + "lastAccessed": 1779538553197, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/review/entity/Review.java", + "accessCount": 1, + "lastAccessed": 1779538553367, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/domain/review/enums/Stars.java", + "accessCount": 1, + "lastAccessed": 1779538701526, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/apipayload/ApiResponse.java", + "accessCount": 1, + "lastAccessed": 1779601454603, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/config/SwaggerConfig.java", + "accessCount": 1, + "lastAccessed": 1779601522913, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/apipayload/code/BaseErrorCode.java", + "accessCount": 1, + "lastAccessed": 1779601524321, + "type": "file" + }, + { + "path": "src/main/java/com/umc/umc10th/global/apipayload/exception/ProjectException.java", + "accessCount": 1, + "lastAccessed": 1779601572760, + "type": "file" + } + ], + "userDirectives": [] +} \ No newline at end of file diff --git a/.omc/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e.json b/.omc/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e.json new file mode 100644 index 0000000..50bdf33 --- /dev/null +++ b/.omc/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e.json @@ -0,0 +1,8 @@ +{ + "session_id": "703ef31d-bc8c-48fd-bbf5-151f4e400a5e", + "ended_at": "2026-05-24T05:58:41.810Z", + "reason": "prompt_input_exit", + "agents_spawned": 0, + "agents_completed": 0, + "modes_used": [] +} \ No newline at end of file diff --git a/.omc/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6.json b/.omc/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6.json new file mode 100644 index 0000000..2e38613 --- /dev/null +++ b/.omc/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6.json @@ -0,0 +1,8 @@ +{ + "session_id": "e01a8ae3-80eb-464d-aa34-b036d2c801d6", + "ended_at": "2026-05-23T12:23:16.611Z", + "reason": "other", + "agents_spawned": 0, + "agents_completed": 0, + "modes_used": [] +} \ No newline at end of file diff --git a/.omc/state/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e/hud-state.json b/.omc/state/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e/hud-state.json new file mode 100644 index 0000000..c4fee72 --- /dev/null +++ b/.omc/state/sessions/703ef31d-bc8c-48fd-bbf5-151f4e400a5e/hud-state.json @@ -0,0 +1,6 @@ +{ + "timestamp": "2026-05-24T05:39:20.447Z", + "backgroundTasks": [], + "sessionStartTimestamp": "2026-05-24T05:37:57.022Z", + "sessionId": "703ef31d-bc8c-48fd-bbf5-151f4e400a5e" +} \ No newline at end of file diff --git a/.omc/state/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6/hud-state.json b/.omc/state/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6/hud-state.json new file mode 100644 index 0000000..cb89699 --- /dev/null +++ b/.omc/state/sessions/e01a8ae3-80eb-464d-aa34-b036d2c801d6/hud-state.json @@ -0,0 +1,6 @@ +{ + "timestamp": "2026-05-23T12:14:51.390Z", + "backgroundTasks": [], + "sessionStartTimestamp": "2026-05-23T12:14:23.560Z", + "sessionId": "e01a8ae3-80eb-464d-aa34-b036d2c801d6" +} \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java b/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java index 648e7a7..da81ee3 100644 --- a/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java +++ b/src/main/java/com/umc/umc10th/domain/user/controller/UserAuthController.java @@ -16,10 +16,10 @@ public class UserAuthController { private final UserService userService; -// @PostMapping("/signup") -// public ApiResponse getInfo(@Valid @RequestBody UserRequestDto.CreateUser dto) { -// BaseSuccessCode code = UserSuccessCode.OK; -// userService.createUser(dto); -// return ApiResponse.onSuccess(code); -// } + @PostMapping("/signup") + public ApiResponse signup(@Valid @RequestBody UserRequestDto.CreateUser dto) { + BaseSuccessCode code = UserSuccessCode.OK; + userService.createUser(dto); + return ApiResponse.onSuccess(code); + } } diff --git a/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java b/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java index bdb17fd..0856896 100644 --- a/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java +++ b/src/main/java/com/umc/umc10th/domain/user/controller/UserController.java @@ -25,7 +25,7 @@ public ApiResponse getInfo() { return ApiResponse.onSuccess(code, userService.getInfo()); } - @GetMapping("/me/missions") + @PostMapping("/me/missions") public ApiResponse getMissions( @RequestParam Long locationId, @RequestParam(defaultValue = "10") Integer pageSize, diff --git a/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java b/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java index 0675689..4509cc1 100644 --- a/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java +++ b/src/main/java/com/umc/umc10th/domain/user/converter/UserConverter.java @@ -3,14 +3,36 @@ import com.umc.umc10th.domain.mission.dto.response.MissionResponseDto; import com.umc.umc10th.domain.review.dto.response.ReviewResponseDto; import com.umc.umc10th.domain.review.entity.Review; +import com.umc.umc10th.domain.user.dto.request.UserRequestDto; +import com.umc.umc10th.domain.user.entity.User; import com.umc.umc10th.domain.user.entity.UserDoingMission; +import com.umc.umc10th.domain.user.enums.Provider; +import com.umc.umc10th.domain.user.enums.ServiceRole; +import com.umc.umc10th.domain.user.enums.SystemRole; import org.springframework.data.domain.Page; import org.springframework.data.domain.Slice; import java.util.List; +import java.util.UUID; public class UserConverter { + public static User toUser(UserRequestDto.CreateUser dto, String encodedPassword) { + return User.builder() + .loginId(dto.email()) + .password(encodedPassword) + .uuid(UUID.randomUUID().toString()) + .provider(dto.provider() != null ? dto.provider() : Provider.LOCAL) + .serviceRole(dto.serviceRole() != null ? dto.serviceRole() : ServiceRole.CUSTOMER) + .systemRole(SystemRole.ROLE_USER) + .name(dto.name()) + .sex(dto.sex()) + .birthday(dto.birthday()) + .address(dto.address()) + .phone(dto.phone()) + .build(); + } + public static MissionResponseDto.GetMissionsPaged toGetMissionsPaged( Page page) { diff --git a/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java b/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java index 19715cf..b9fc6bf 100644 --- a/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java +++ b/src/main/java/com/umc/umc10th/domain/user/dto/request/UserRequestDto.java @@ -10,8 +10,11 @@ public class UserRequestDto { @Builder - public record CreateUser ( - String id, + public record CreateUser( + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "올바른 이메일 형식이 아닙니다.") + String email, + @NotBlank(message = "비밀번호는 필수입니다.") String password, Provider provider, ServiceRole serviceRole, @@ -20,7 +23,7 @@ public record CreateUser ( LocalDate birthday, String address, String phone - ){} + ) {} @Builder public record GetMissionsRequest( diff --git a/src/main/java/com/umc/umc10th/domain/user/entity/User.java b/src/main/java/com/umc/umc10th/domain/user/entity/User.java index a2a45fc..ce9b3f7 100644 --- a/src/main/java/com/umc/umc10th/domain/user/entity/User.java +++ b/src/main/java/com/umc/umc10th/domain/user/entity/User.java @@ -16,7 +16,9 @@ @Entity @Table(name = "users") @Getter +@Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class User { @Id @@ -24,13 +26,13 @@ public class User { @Column(name = "user_id") private Long id; - @Column(name = "id", nullable = false, length = 30) + @Column(name = "id", nullable = false, unique = true, length = 100) private String loginId; - @Column(name = "uuid", nullable = false, length = 30) + @Column(name = "uuid", nullable = false, length = 36) private String uuid; - @Column(name = "password", length = 20) + @Column(name = "password", length = 255) private String password; @Enumerated(EnumType.STRING) @@ -61,6 +63,7 @@ public class User { @Column(name = "address", length = 100) private String address; + @Builder.Default @Column(name = "points", nullable = false) private Integer points = 0; @@ -76,9 +79,11 @@ public class User { @Column(name = "deleted_at") private LocalDateTime deletedAt; + @Builder.Default @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List doingMissions = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List reviews = new ArrayList<>(); @@ -88,6 +93,7 @@ public class User { @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private NotificationSetting notificationSettings; + @Builder.Default @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List preferences = new ArrayList<>(); diff --git a/src/main/java/com/umc/umc10th/domain/user/enums/Provider.java b/src/main/java/com/umc/umc10th/domain/user/enums/Provider.java index 2c45014..8c541d8 100644 --- a/src/main/java/com/umc/umc10th/domain/user/enums/Provider.java +++ b/src/main/java/com/umc/umc10th/domain/user/enums/Provider.java @@ -1,4 +1,5 @@ package com.umc.umc10th.domain.user.enums; public enum Provider { + LOCAL } diff --git a/src/main/java/com/umc/umc10th/domain/user/enums/ServiceRole.java b/src/main/java/com/umc/umc10th/domain/user/enums/ServiceRole.java index a8f2840..d9e53b5 100644 --- a/src/main/java/com/umc/umc10th/domain/user/enums/ServiceRole.java +++ b/src/main/java/com/umc/umc10th/domain/user/enums/ServiceRole.java @@ -1,4 +1,6 @@ package com.umc.umc10th.domain.user.enums; public enum ServiceRole { + SELLER, + CUSTOMER } diff --git a/src/main/java/com/umc/umc10th/domain/user/enums/SystemRole.java b/src/main/java/com/umc/umc10th/domain/user/enums/SystemRole.java index 05fecd0..4bbbb41 100644 --- a/src/main/java/com/umc/umc10th/domain/user/enums/SystemRole.java +++ b/src/main/java/com/umc/umc10th/domain/user/enums/SystemRole.java @@ -1,4 +1,6 @@ package com.umc.umc10th.domain.user.enums; public enum SystemRole { + ROLE_USER, + ROLE_ADMIN } diff --git a/src/main/java/com/umc/umc10th/domain/user/exception/UserException.java b/src/main/java/com/umc/umc10th/domain/user/exception/UserException.java index 0d20cea..cea4020 100644 --- a/src/main/java/com/umc/umc10th/domain/user/exception/UserException.java +++ b/src/main/java/com/umc/umc10th/domain/user/exception/UserException.java @@ -1,7 +1,10 @@ package com.umc.umc10th.domain.user.exception; -public class UserException extends RuntimeException { - public UserException(String message) { - super(message); +import com.umc.umc10th.global.apipayload.code.BaseErrorCode; +import com.umc.umc10th.global.apipayload.exception.ProjectException; + +public class UserException extends ProjectException { + public UserException(BaseErrorCode errorCode) { + super(errorCode); } } diff --git a/src/main/java/com/umc/umc10th/domain/user/exception/code/UserErrorCode.java b/src/main/java/com/umc/umc10th/domain/user/exception/code/UserErrorCode.java index c6fbeee..6678189 100644 --- a/src/main/java/com/umc/umc10th/domain/user/exception/code/UserErrorCode.java +++ b/src/main/java/com/umc/umc10th/domain/user/exception/code/UserErrorCode.java @@ -1,4 +1,23 @@ package com.umc.umc10th.domain.user.exception.code; -public enum UserErrorCode { +import com.umc.umc10th.global.apipayload.code.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum UserErrorCode implements BaseErrorCode { + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "USER409_1", "이미 사용 중인 이메일입니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404_1", "존재하지 않는 유저입니다."), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public HttpStatus getStatus() { + return httpStatus; + } } diff --git a/src/main/java/com/umc/umc10th/domain/user/repository/UserRepository.java b/src/main/java/com/umc/umc10th/domain/user/repository/UserRepository.java index 0895f3f..0701b2d 100644 --- a/src/main/java/com/umc/umc10th/domain/user/repository/UserRepository.java +++ b/src/main/java/com/umc/umc10th/domain/user/repository/UserRepository.java @@ -15,4 +15,8 @@ public interface UserRepository extends JpaRepository { WHERE u.id = :userId """) Optional findUserInfo(@Param("userId") Long userId); + + Optional findByLoginId(String loginId); + + boolean existsByLoginId(String loginId); } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/domain/user/service/UserService.java b/src/main/java/com/umc/umc10th/domain/user/service/UserService.java index f883641..2fe50fa 100644 --- a/src/main/java/com/umc/umc10th/domain/user/service/UserService.java +++ b/src/main/java/com/umc/umc10th/domain/user/service/UserService.java @@ -10,6 +10,8 @@ import com.umc.umc10th.domain.user.dto.response.UserResponseDto; import com.umc.umc10th.domain.user.entity.User; import com.umc.umc10th.domain.user.entity.UserDoingMission; +import com.umc.umc10th.domain.user.exception.UserException; +import com.umc.umc10th.domain.user.exception.code.UserErrorCode; import com.umc.umc10th.domain.user.repository.UserDoingMissionRepository; import com.umc.umc10th.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -17,6 +19,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,11 +33,21 @@ public class UserService { private final UserRepository userRepository; private final UserDoingMissionRepository userDoingMissionRepository; private final ReviewRepository reviewRepository; + private final PasswordEncoder passwordEncoder; // 임시 인증 유저 ID - 실제로는 SecurityContextHolder 등에서 추출 private static final Long TEMP_USER_ID = 1L; private static final int PAGE_SIZE = 10; + @Transactional + public void createUser(UserRequestDto.CreateUser dto) { + if (userRepository.existsByLoginId(dto.email())) { + throw new UserException(UserErrorCode.DUPLICATE_EMAIL); + } + String encodedPassword = passwordEncoder.encode(dto.password()); + userRepository.save(UserConverter.toUser(dto, encodedPassword)); + } + public UserResponseDto.GetInfo getInfo() { User user = userRepository.findUserInfo(TEMP_USER_ID) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다.")); @@ -107,20 +120,31 @@ public ReviewResponseDto.GetMyReviewsPaged getMyReviews( Slice slice; if (!cursor.equals("-1")) { - String[] split = cursor.split(":"); + String[] split = cursor.split(":", 2); + if (split.length < 2) { + throw new IllegalArgumentException("유효하지 않은 커서 형식입니다: " + cursor); + } switch (query.toUpperCase()) { case "ID" -> { - Long idCursor = Long.parseLong(split[1]); - slice = reviewRepository.findReviewsByUser_IdAndIdLessThanOrderByIdDesc( - userId, idCursor, pageRequest - ); + try { + Long idCursor = Long.parseLong(split[1]); + slice = reviewRepository.findReviewsByUser_IdAndIdLessThanOrderByIdDesc( + userId, idCursor, pageRequest + ); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("유효하지 않은 커서 형식입니다: " + cursor); + } } case "STARS" -> { - Stars starsCursor = Stars.valueOf(split[0]); - Long idCursor = Long.parseLong(split[1]); - slice = reviewRepository.findReviewsByUser_IdOrderByStarsDesc( - userId, starsCursor, idCursor, pageRequest - ); + try { + Stars starsCursor = Stars.valueOf(split[0]); + Long idCursor = Long.parseLong(split[1]); + slice = reviewRepository.findReviewsByUser_IdOrderByStarsDesc( + userId, starsCursor, idCursor, pageRequest + ); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("유효하지 않은 커서 형식입니다: " + cursor); + } } default -> throw new IllegalArgumentException("유효하지 않은 정렬 기준입니다: " + query); } diff --git a/src/main/java/com/umc/umc10th/global/apipayload/code/GeneralErrorCode.java b/src/main/java/com/umc/umc10th/global/apipayload/code/GeneralErrorCode.java index 31096e7..69ba512 100644 --- a/src/main/java/com/umc/umc10th/global/apipayload/code/GeneralErrorCode.java +++ b/src/main/java/com/umc/umc10th/global/apipayload/code/GeneralErrorCode.java @@ -20,6 +20,6 @@ public enum GeneralErrorCode implements BaseErrorCode { @Override public HttpStatus getStatus() { - return null; + return httpStatus; } } \ No newline at end of file diff --git a/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java index 2040958..84b1613 100644 --- a/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/umc/umc10th/global/apipayload/handler/GeneralExceptionAdvice.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.net.BindException; +import org.springframework.validation.BindException; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/com/umc/umc10th/global/config/SecurityConfig.java b/src/main/java/com/umc/umc10th/global/config/SecurityConfig.java new file mode 100644 index 0000000..58b4bff --- /dev/null +++ b/src/main/java/com/umc/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,59 @@ +package com.umc.umc10th.global.config; + +import com.umc.umc10th.global.security.handler.CustomAccessDenied; +import com.umc.umc10th.global.security.handler.CustomEntryPoint; +import lombok.RequiredArgsConstructor; +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 +@RequiredArgsConstructor +public class SecurityConfig { + + private final CustomAccessDenied customAccessDenied; + private final CustomEntryPoint customEntryPoint; + + private final String[] allowUris = { + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/auth/**" + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ) + .exceptionHandling(e -> e + .accessDeniedHandler(customAccessDenied) + .authenticationEntryPoint(customEntryPoint) + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/umc/umc10th/global/security/AuthUser.java b/src/main/java/com/umc/umc10th/global/security/AuthUser.java new file mode 100644 index 0000000..77c0ce9 --- /dev/null +++ b/src/main/java/com/umc/umc10th/global/security/AuthUser.java @@ -0,0 +1,35 @@ +package com.umc.umc10th.global.security; + +import com.umc.umc10th.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class AuthUser implements UserDetails { + + private final User user; + + public User getUser() { + return user; + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(user.getSystemRole().name())); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getLoginId(); + } +} diff --git a/src/main/java/com/umc/umc10th/global/security/CustomUserDetailsService.java b/src/main/java/com/umc/umc10th/global/security/CustomUserDetailsService.java new file mode 100644 index 0000000..9b65059 --- /dev/null +++ b/src/main/java/com/umc/umc10th/global/security/CustomUserDetailsService.java @@ -0,0 +1,23 @@ +package com.umc.umc10th.global.security; + +import com.umc.umc10th.domain.user.entity.User; +import com.umc.umc10th.domain.user.repository.UserRepository; +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 email) throws UsernameNotFoundException { + User user = userRepository.findByLoginId(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email)); + return new AuthUser(user); + } +} diff --git a/src/main/java/com/umc/umc10th/global/security/handler/CustomAccessDenied.java b/src/main/java/com/umc/umc10th/global/security/handler/CustomAccessDenied.java new file mode 100644 index 0000000..788672b --- /dev/null +++ b/src/main/java/com/umc/umc10th/global/security/handler/CustomAccessDenied.java @@ -0,0 +1,33 @@ +package com.umc.umc10th.global.security.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.umc10th.global.apipayload.ApiResponse; +import com.umc.umc10th.global.apipayload.code.BaseErrorCode; +import com.umc.umc10th.global.apipayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.FORBIDDEN; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/src/main/java/com/umc/umc10th/global/security/handler/CustomEntryPoint.java b/src/main/java/com/umc/umc10th/global/security/handler/CustomEntryPoint.java new file mode 100644 index 0000000..2d9aa3d --- /dev/null +++ b/src/main/java/com/umc/umc10th/global/security/handler/CustomEntryPoint.java @@ -0,0 +1,33 @@ +package com.umc.umc10th.global.security.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.umc.umc10th.global.apipayload.ApiResponse; +import com.umc.umc10th.global.apipayload.code.BaseErrorCode; +import com.umc.umc10th.global.apipayload.code.GeneralErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +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; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +}