diff --git a/src/main/java/side/onetime/controller/UserController.java b/src/main/java/side/onetime/controller/UserController.java index 3009c870..b4bbbd50 100644 --- a/src/main/java/side/onetime/controller/UserController.java +++ b/src/main/java/side/onetime/controller/UserController.java @@ -4,11 +4,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import side.onetime.domain.enums.GuideType; import side.onetime.dto.user.request.*; -import side.onetime.dto.user.response.GetUserPolicyAgreementResponse; -import side.onetime.dto.user.response.GetUserProfileResponse; -import side.onetime.dto.user.response.GetUserSleepTimeResponse; -import side.onetime.dto.user.response.OnboardUserResponse; +import side.onetime.dto.user.response.*; import side.onetime.global.common.ApiResponse; import side.onetime.global.common.status.SuccessStatus; import side.onetime.service.UserService; @@ -156,4 +154,53 @@ public ResponseEntity> logoutUser( userService.logoutUser(logoutUserRequest); return ApiResponse.onSuccess(SuccessStatus._LOGOUT_USER); } + + /** + * 유저 가이드 조회 로그 저장 API. + * + * GuideType에 정의된 가이드에 대해 사용자의 조회 로그를 저장합니다. + * 이미 조회한 경우, Conflict 에러를 반환합니다. + * + * @param request 확인 여부를 저장할 가이드 타입 객체 + * @return 성공 상태 응답 객체 + */ + @PostMapping("/guides/view-log") + public ResponseEntity> createGuideViewLog( + @Valid @RequestBody CreateGuideViewLogRequest request + ) { + + userService.createGuideViewLog(request); + return ApiResponse.onSuccess(SuccessStatus._CREATE_GUIDE_VIEW_LOG); + } + + /** + * 유저 가이드 조회 로그 조회 API. + * + * GuideType에 정의된 가이드에 대해 사용자의 조회 로그를 조회합니다. + * + * @param guideType 조회할 가이드 타입 + * @return 가이드 조회 로그 응답 객체 + */ + @GetMapping("/guides/view-log") + public ResponseEntity> getGuideViewLog( + @RequestParam("guide_type") GuideType guideType + ){ + + GetGuideViewLogResponse response = userService.getGuideViewLog(guideType); + return ApiResponse.onSuccess(SuccessStatus._GET_GUIDE_VIEW_LOG, response); + } + + /** + * 유저 가이드 조회 로그 삭제 API. + * + * 사용자의 가이드 조회 로그를 삭제합니다. + * + * @return 성공 상태 응답 객체 + */ + @DeleteMapping("/guides/view-log") + public ResponseEntity> deleteGuideViewLog(){ + + userService.deleteGuideViewLog(); + return ApiResponse.onSuccess(SuccessStatus._DELETE_GUIDE_VIEW_LOG); + } } diff --git a/src/main/java/side/onetime/domain/GuideViewLog.java b/src/main/java/side/onetime/domain/GuideViewLog.java new file mode 100644 index 00000000..e994d27b --- /dev/null +++ b/src/main/java/side/onetime/domain/GuideViewLog.java @@ -0,0 +1,45 @@ +package side.onetime.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import side.onetime.domain.enums.GuideType; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table( + name = "guide_view_logs", + uniqueConstraints = { + @UniqueConstraint(name = "unique_user_guide_type", columnNames = {"users_id", "guide_type"}) + } +) +public class GuideViewLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "guide_view_logs_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "users_id", foreignKey = @ForeignKey(name = "guide_view_logs_fk_users_id"), nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(name = "guide_type", nullable = false) + private GuideType guideType; + + @Column(name = "viewed_at", nullable = false) + private LocalDateTime viewedAt; + + @Builder + public GuideViewLog(User user, GuideType guideType) { + this.user = user; + this.guideType = guideType; + this.viewedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/side/onetime/domain/enums/GuideType.java b/src/main/java/side/onetime/domain/enums/GuideType.java new file mode 100644 index 00000000..15409c28 --- /dev/null +++ b/src/main/java/side/onetime/domain/enums/GuideType.java @@ -0,0 +1,7 @@ +package side.onetime.domain.enums; + +public enum GuideType { + + SCHEDULE_GUIDE_MODAL_001, + ; +} diff --git a/src/main/java/side/onetime/dto/user/request/CreateGuideViewLogRequest.java b/src/main/java/side/onetime/dto/user/request/CreateGuideViewLogRequest.java new file mode 100644 index 00000000..7c0050a0 --- /dev/null +++ b/src/main/java/side/onetime/dto/user/request/CreateGuideViewLogRequest.java @@ -0,0 +1,15 @@ +package side.onetime.dto.user.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.constraints.NotNull; +import side.onetime.domain.enums.GuideType; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CreateGuideViewLogRequest( + @NotNull(message = "가이드 타입은 필수 값입니다.") + GuideType guideType +) { +} diff --git a/src/main/java/side/onetime/dto/user/response/GetGuideViewLogResponse.java b/src/main/java/side/onetime/dto/user/response/GetGuideViewLogResponse.java new file mode 100644 index 00000000..abfc5deb --- /dev/null +++ b/src/main/java/side/onetime/dto/user/response/GetGuideViewLogResponse.java @@ -0,0 +1,15 @@ +package side.onetime.dto.user.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record GetGuideViewLogResponse( + boolean isViewed +) { + public static GetGuideViewLogResponse from(boolean isViewed) { + return new GetGuideViewLogResponse(isViewed); + } +} diff --git a/src/main/java/side/onetime/exception/status/UserErrorStatus.java b/src/main/java/side/onetime/exception/status/UserErrorStatus.java index e35794e1..b9385d12 100644 --- a/src/main/java/side/onetime/exception/status/UserErrorStatus.java +++ b/src/main/java/side/onetime/exception/status/UserErrorStatus.java @@ -13,6 +13,8 @@ public enum UserErrorStatus implements BaseErrorCode { _NOT_FOUND_USER_BY_USERNAME(HttpStatus.UNAUTHORIZED, "USER-002", "username으로 user를 찾을 수 없습니다."), _NOT_FOUND_USER_BY_USERID(HttpStatus.UNAUTHORIZED, "USER-003", "userId로 user를 찾을 수 없습니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "USER-004", "인증된 사용자가 아닙니다."), + _IS_ALREADY_VIEWED_GUIDE(HttpStatus.CONFLICT, "USER-005", "이미 조회한 가이드입니다."), + _NOT_FOUND_GUIDE(HttpStatus.NOT_FOUND, "USER-006", "가이드를 찾을 수 없습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/side/onetime/global/common/status/SuccessStatus.java b/src/main/java/side/onetime/global/common/status/SuccessStatus.java index 5096b409..651bdfa1 100644 --- a/src/main/java/side/onetime/global/common/status/SuccessStatus.java +++ b/src/main/java/side/onetime/global/common/status/SuccessStatus.java @@ -53,6 +53,9 @@ public enum SuccessStatus implements BaseCode { _GET_USER_SLEEP_TIME(HttpStatus.OK, "200", "유저 수면 시간 조회에 성공했습니다."), _UPDATE_USER_SLEEP_TIME(HttpStatus.OK, "200", "유저 수면 시간 수정에 성공했습니다."), _LOGOUT_USER(HttpStatus.OK, "200", "유저 로그아웃에 성공했습니다."), + _CREATE_GUIDE_VIEW_LOG(HttpStatus.CREATED, "201", "유저 가이드 조회 로그 저장에 성공했습니다."), + _GET_GUIDE_VIEW_LOG(HttpStatus.OK, "200", "유저 가이드 조회 로그 조회에 성공했습니다."), + _DELETE_GUIDE_VIEW_LOG(HttpStatus.OK, "200", "유저 가이드 조회 로그 삭제에 성공했습니다."), // Fixed _GET_USER_FIXED_SCHEDULE(HttpStatus.OK, "200", "유저 고정 스케줄 조회에 성공했습니다."), _UPDATE_USER_FIXED_SCHEDULE(HttpStatus.OK, "200", "유저 고정 스케줄 수정에 성공했습니다."), diff --git a/src/main/java/side/onetime/repository/GuideViewLogRepository.java b/src/main/java/side/onetime/repository/GuideViewLogRepository.java new file mode 100644 index 00000000..b6f07546 --- /dev/null +++ b/src/main/java/side/onetime/repository/GuideViewLogRepository.java @@ -0,0 +1,15 @@ +package side.onetime.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import side.onetime.domain.GuideViewLog; +import side.onetime.domain.User; +import side.onetime.domain.enums.GuideType; + +import java.util.Optional; + +public interface GuideViewLogRepository extends JpaRepository { + + boolean existsByUserAndGuideType(User user, GuideType guideType); + + Optional findByUser(User user); +} diff --git a/src/main/java/side/onetime/repository/custom/UserRepositoryImpl.java b/src/main/java/side/onetime/repository/custom/UserRepositoryImpl.java index 7d0be5c6..0ee8f48d 100644 --- a/src/main/java/side/onetime/repository/custom/UserRepositoryImpl.java +++ b/src/main/java/side/onetime/repository/custom/UserRepositoryImpl.java @@ -22,6 +22,7 @@ import static side.onetime.domain.QEvent.event; import static side.onetime.domain.QEventParticipation.eventParticipation; import static side.onetime.domain.QFixedSelection.fixedSelection; +import static side.onetime.domain.QGuideViewLog.guideViewLog; import static side.onetime.domain.QMember.member; import static side.onetime.domain.QSchedule.schedule; import static side.onetime.domain.QSelection.selection; @@ -96,6 +97,10 @@ public void withdraw(User activeUser) { .where(eventParticipation.user.eq(activeUser)) .execute(); + queryFactory.delete(guideViewLog) + .where(guideViewLog.user.eq(activeUser)) + .execute(); + queryFactory.update(user) .set(user.providerId, Expressions.nullExpression()) .set(user.status, Status.DELETED) diff --git a/src/main/java/side/onetime/service/UserService.java b/src/main/java/side/onetime/service/UserService.java index ef0a3aac..9cee27d5 100644 --- a/src/main/java/side/onetime/service/UserService.java +++ b/src/main/java/side/onetime/service/UserService.java @@ -3,15 +3,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import side.onetime.domain.GuideViewLog; import side.onetime.domain.RefreshToken; import side.onetime.domain.User; +import side.onetime.domain.enums.GuideType; import side.onetime.dto.user.request.*; -import side.onetime.dto.user.response.GetUserPolicyAgreementResponse; -import side.onetime.dto.user.response.GetUserProfileResponse; -import side.onetime.dto.user.response.GetUserSleepTimeResponse; -import side.onetime.dto.user.response.OnboardUserResponse; +import side.onetime.dto.user.response.*; import side.onetime.exception.CustomException; import side.onetime.exception.status.UserErrorStatus; +import side.onetime.repository.GuideViewLogRepository; import side.onetime.repository.RefreshTokenRepository; import side.onetime.repository.UserRepository; import side.onetime.util.JwtUtil; @@ -26,6 +26,7 @@ public class UserService { private final RefreshTokenRepository refreshTokenRepository; private final UserRepository userRepository; private final JwtUtil jwtUtil; + private final GuideViewLogRepository guideViewLogRepository; /** * 유저 온보딩 처리 메서드. @@ -201,4 +202,64 @@ public void logoutUser(LogoutUserRequest request) { String browserId = jwtUtil.getClaimFromToken(refreshToken, "browserId", String.class); refreshTokenRepository.deleteRefreshToken(userId, browserId); } + + /** + * 유저 가이드 조회 로그 저장 메서드. + * + * GuideType에 정의된 가이드에 대해 사용자의 조회 로그를 저장합니다. + * 이미 조회한 경우, Conflict 에러를 반환합니다. + * + * @param request 확인 여부를 저장할 가이드 타입 객체 + */ + @Transactional + public void createGuideViewLog(CreateGuideViewLogRequest request) { + User user = userRepository.findById(UserAuthorizationUtil.getLoginUserId()) + .orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER)); + GuideType guideType = request.guideType(); + + boolean isViewed = guideViewLogRepository.existsByUserAndGuideType(user, guideType); + if (isViewed) { + throw new CustomException(UserErrorStatus._IS_ALREADY_VIEWED_GUIDE); + } + + GuideViewLog guideViewLog = GuideViewLog.builder() + .user(user) + .guideType(guideType) + .build(); + + guideViewLogRepository.save(guideViewLog); + } + + /** + * 유저 가이드 조회 로그 조회 메서드. + * + * GuideType에 정의된 가이드에 대해 사용자의 조회 로그를 조회합니다. + * + * @param guideType 조회할 가이드 타입 + * @return 가이드 조회 로그 응답 데이터 + */ + @Transactional(readOnly = true) + public GetGuideViewLogResponse getGuideViewLog(GuideType guideType) { + User user = userRepository.findById(UserAuthorizationUtil.getLoginUserId()) + .orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER)); + + boolean isViewed = guideViewLogRepository.existsByUserAndGuideType(user, guideType); + return GetGuideViewLogResponse.from(isViewed); + } + + /** + * 유저 가이드 조회 로그 삭제 메서드. + * + * 사용자의 가이드 조회 로그를 삭제합니다. + */ + @Transactional + public void deleteGuideViewLog() { + User user = userRepository.findById(UserAuthorizationUtil.getLoginUserId()) + .orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER)); + + GuideViewLog guideViewLog = guideViewLogRepository.findByUser(user) + .orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_GUIDE)); + + guideViewLogRepository.delete(guideViewLog); + } } diff --git a/src/test/java/side/onetime/user/UserControllerTest.java b/src/test/java/side/onetime/user/UserControllerTest.java index ff8b9ec2..a1ba22b6 100644 --- a/src/test/java/side/onetime/user/UserControllerTest.java +++ b/src/test/java/side/onetime/user/UserControllerTest.java @@ -15,18 +15,16 @@ import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.UserController; +import side.onetime.domain.enums.GuideType; import side.onetime.domain.enums.Language; -import side.onetime.dto.user.request.OnboardUserRequest; -import side.onetime.dto.user.request.UpdateUserPolicyAgreementRequest; -import side.onetime.dto.user.request.UpdateUserProfileRequest; -import side.onetime.dto.user.request.UpdateUserSleepTimeRequest; -import side.onetime.dto.user.response.GetUserPolicyAgreementResponse; -import side.onetime.dto.user.response.GetUserProfileResponse; -import side.onetime.dto.user.response.GetUserSleepTimeResponse; -import side.onetime.dto.user.response.OnboardUserResponse; +import side.onetime.dto.user.request.*; +import side.onetime.dto.user.response.*; +import side.onetime.exception.CustomException; +import side.onetime.exception.status.UserErrorStatus; import side.onetime.service.UserService; import side.onetime.util.JwtUtil; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; @@ -460,4 +458,203 @@ public void logoutUser() throws Exception { ) )); } + + @Test + @DisplayName("유저 가이드 조회 로그를 저장한다.") + public void createGuideViewLog() throws Exception { + // given + CreateGuideViewLogRequest request = new CreateGuideViewLogRequest(GuideType.SCHEDULE_GUIDE_MODAL_001); + String requestContent = objectMapper.writeValueAsString(request); + + // when + Mockito.doNothing().when(userService).createGuideViewLog(any(CreateGuideViewLogRequest.class)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/users/guides/view-log") + .content(requestContent) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("201")) + .andExpect(jsonPath("$.message").value("유저 가이드 조회 로그 저장에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("user/create-guide-view-log", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("User API") + .description("유저 가이드 조회 로그를 저장한다.") + .requestFields( + fieldWithPath("guide_type").type(JsonFieldType.STRING).description("가이드 타입") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .requestSchema(Schema.schema("CreateGuideViewLogRequestSchema")) + .build() + ) + )); + } + + @Test + @DisplayName("[FAILED] 유저 가이드 조회 로그 저장에 실패한다. (잘못된 GuideType 값)") + public void createGuideViewLog_Fail_Validation() throws Exception { + // given + String invalidGuideType = "wrong-guide-type"; + String requestContent = String.format("{\"guide_type\": \"%s\"}", invalidGuideType); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/users/guides/view-log") + .content(requestContent) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.is_success").value(false)) + .andExpect(jsonPath("$.code").value("E_BAD_REQUEST")) + .andExpect(jsonPath("$.message").value("올바르지 않은 enum 값입니다. 허용되지 않은 값: " + invalidGuideType)) + .andDo(MockMvcRestDocumentationWrapper.document("user/create-guide-view-log-fail-validation", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("User API") + .build() + ) + )); + } + + @Test + @DisplayName("[FAILED] 유저 가이드 조회 로그 저장에 실패한다. (이미 조회한 가이드)") + public void createGuideViewLog_Fail_Already_Viewed() throws Exception { + // given + CreateGuideViewLogRequest request = new CreateGuideViewLogRequest(GuideType.SCHEDULE_GUIDE_MODAL_001); + String requestContent = objectMapper.writeValueAsString(request); + + // when + Mockito.doThrow(new CustomException(UserErrorStatus._IS_ALREADY_VIEWED_GUIDE)) + .when(userService) + .createGuideViewLog(any(CreateGuideViewLogRequest.class)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/users/guides/view-log") + .content(requestContent) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.is_success").value(false)) + .andExpect(jsonPath("$.code").value("USER-005")) + .andExpect(jsonPath("$.message").value("이미 조회한 가이드입니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("user/create-guide-view-log-fail-already-viewed", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("User API") + .build() + ) + )); + } + + @Test + @DisplayName("유저 가이드 조회 로그를 조회한다.") + public void getGuideViewLog() throws Exception { + // given + GuideType guideType = GuideType.SCHEDULE_GUIDE_MODAL_001; + GetGuideViewLogResponse response = GetGuideViewLogResponse.from(true); + + // when + Mockito.when(userService.getGuideViewLog(any(GuideType.class))).thenReturn(response); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/users/guides/view-log") + .queryParam("guide_type", guideType.name()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("유저 가이드 조회 로그 조회에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("user/get-guide-view-log", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("User API") + .description("유저 가이드 조회 로그를 조회한다.") + .queryParameters( + parameterWithName("guide_type").description("가이드 타입") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("payload.is_viewed").type(JsonFieldType.BOOLEAN).description("가이드 조회 여부") + ) + .responseSchema(Schema.schema("GetGuideViewLogResponseSchema")) + .build() + ) + )); + } + + @Test + @DisplayName("유저 가이드 조회 로그를 삭제한다.") + public void deleteGuideViewLog() throws Exception { + // when + Mockito.doNothing().when(userService).deleteGuideViewLog(); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/users/guides/view-log") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("유저 가이드 조회 로그 삭제에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("user/delete-guide-view-log", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("User API") + .description("유저 가이드 조회 로그를 삭제한다.") + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .responseSchema(Schema.schema("CommonSuccessResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("[FAILED] 유저 가이드 조회 로그 삭제에 실패한다. (가이드 조회 로그 없음)") + public void deleteGuideViewLog_Fail_NotFound() throws Exception { + // when + Mockito.doThrow(new CustomException(UserErrorStatus._NOT_FOUND_GUIDE)) + .when(userService) + .deleteGuideViewLog(); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/users/guides/view-log") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.is_success").value(false)) + .andExpect(jsonPath("$.code").value("USER-006")) + .andExpect(jsonPath("$.message").value("가이드를 찾을 수 없습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("user/delete-guide-view-log-fail-not-found", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("User API") + .build() + ) + )); + } }