Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions src/main/java/side/onetime/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -156,4 +154,39 @@ public ResponseEntity<ApiResponse<SuccessStatus>> logoutUser(
userService.logoutUser(logoutUserRequest);
return ApiResponse.onSuccess(SuccessStatus._LOGOUT_USER);
}

/**
* 가이드 확인 여부 저장 API.
*
* GuideType에 정의된 가이드에 대해 사용자의 확인 여부를 저장합니다.
* 이미 확인한 상태일 경우, Conflict 에러를 반환합니다.
*
* @param request 확인 여부를 저장할 가이드 타입 객체
* @return 성공 상태 응답 객체
*/
@PostMapping("/guides/view-status")
public ResponseEntity<ApiResponse<SuccessStatus>> createGuideViewStatus(
@Valid @RequestBody CreateGuideViewStatusRequest request
) {

userService.createGuideViewStatus(request);
return ApiResponse.onSuccess(SuccessStatus._CREATE_GUIDE_VIEW_STATUS);
}

/**
* 가이드 확인 여부 조회 API.
*
* GuideType에 정의된 가이드에 대해 사용자의 확인 여부를 조회합니다.
*
* @param guideType 조회할 가이드 타입
* @return 가이드 확인 여부 응답 객체
*/
@GetMapping("/guides/view-status")
public ResponseEntity<ApiResponse<GetGuideViewStatusResponse>> getGuideViewStatus(
@RequestParam("guide_type") GuideType guideType
){

GetGuideViewStatusResponse response = userService.getGuideViewStatus(guideType);
return ApiResponse.onSuccess(SuccessStatus._GET_GUIDE_VIEW_STATUS, response);
}
}
49 changes: 49 additions & 0 deletions src/main/java/side/onetime/domain/GuideViewStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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_status",
uniqueConstraints = {
@UniqueConstraint(name = "unique_user_guide_type", columnNames = {"users_id", "guide_type"})
}
)
public class GuideViewStatus {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "guide_view_status_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "users_id", foreignKey = @ForeignKey(name = "guide_view_status_fk_users_id"), nullable = false)
private User user;

@Enumerated(EnumType.STRING)
@Column(name = "guide_type", nullable = false)
private GuideType guideType;

@Column(name = "is_viewed", nullable = false)
private Boolean isViewed;

@Column(name = "viewed_at", nullable = false)
private LocalDateTime viewedAt;

@Builder
public GuideViewStatus(User user, GuideType guideType) {
this.user = user;
this.guideType = guideType;
this.isViewed = true;
this.viewedAt = LocalDateTime.now();
}
}
7 changes: 7 additions & 0 deletions src/main/java/side/onetime/domain/enums/GuideType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package side.onetime.domain.enums;

public enum GuideType {

SCHEDULE_GUIDE_MODAL_001,
;
}
Original file line number Diff line number Diff line change
@@ -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 CreateGuideViewStatusRequest(
@NotNull(message = "가이드 타입은 필수 값입니다.")
GuideType guideType
) {
}
Original file line number Diff line number Diff line change
@@ -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 GetGuideViewStatusResponse(
boolean isViewed
) {
public static GetGuideViewStatusResponse from(boolean isViewed) {
return new GetGuideViewStatusResponse(isViewed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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", "이미 조회한 가이드입니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ 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_STATUS(HttpStatus.CREATED, "201", "유저 가이드 확인 여부 저장에 성공했습니다."),
_GET_GUIDE_VIEW_STATUS(HttpStatus.OK, "200", "유저 가이드 확인 여부 조회에 성공했습니다."),
// Fixed
_GET_USER_FIXED_SCHEDULE(HttpStatus.OK, "200", "유저 고정 스케줄 조회에 성공했습니다."),
_UPDATE_USER_FIXED_SCHEDULE(HttpStatus.OK, "200", "유저 고정 스케줄 수정에 성공했습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package side.onetime.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import side.onetime.domain.GuideViewStatus;
import side.onetime.domain.User;
import side.onetime.domain.enums.GuideType;

public interface GuideViewStatusRepository extends JpaRepository<GuideViewStatus, Long> {

boolean existsByUserAndGuideType(User user, GuideType guideType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.QGuideViewStatus.guideViewStatus;
import static side.onetime.domain.QMember.member;
import static side.onetime.domain.QSchedule.schedule;
import static side.onetime.domain.QSelection.selection;
Expand Down Expand Up @@ -96,6 +97,10 @@ public void withdraw(User activeUser) {
.where(eventParticipation.user.eq(activeUser))
.execute();

queryFactory.delete(guideViewStatus)
.where(guideViewStatus.user.eq(activeUser))
.execute();

queryFactory.update(user)
.set(user.providerId, Expressions.nullExpression())
.set(user.status, Status.DELETED)
Expand Down
53 changes: 49 additions & 4 deletions src/main/java/side/onetime/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import side.onetime.domain.GuideViewStatus;
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.GuideViewStatusRepository;
import side.onetime.repository.RefreshTokenRepository;
import side.onetime.repository.UserRepository;
import side.onetime.util.JwtUtil;
Expand All @@ -26,6 +26,7 @@ public class UserService {
private final RefreshTokenRepository refreshTokenRepository;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final GuideViewStatusRepository guideViewStatusRepository;

/**
* 유저 온보딩 처리 메서드.
Expand Down Expand Up @@ -201,4 +202,48 @@ public void logoutUser(LogoutUserRequest request) {
String browserId = jwtUtil.getClaimFromToken(refreshToken, "browserId", String.class);
refreshTokenRepository.deleteRefreshToken(userId, browserId);
}

/**
* 가이드 확인 여부 저장 메서드.
*
* GuideType에 정의된 가이드에 대해 사용자의 확인 여부를 저장합니다.
* 이미 확인한 상태일 경우, Conflict 에러를 반환합니다.
*
* @param request 확인 여부를 저장할 가이드 타입 객체
*/
@Transactional
public void createGuideViewStatus(CreateGuideViewStatusRequest request) {
User user = userRepository.findById(UserAuthorizationUtil.getLoginUserId())
.orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER));
GuideType guideType = request.guideType();

boolean isViewed = guideViewStatusRepository.existsByUserAndGuideType(user, guideType);
if (isViewed) {
throw new CustomException(UserErrorStatus._IS_ALREADY_VIEWED_GUIDE);
}

GuideViewStatus guideViewStatus = GuideViewStatus.builder()
.user(user)
.guideType(guideType)
.build();

guideViewStatusRepository.save(guideViewStatus);
}

/**
* 가이드 확인 여부 조회 메서드.
*
* GuideType에 정의된 가이드에 대해 사용자의 확인 여부를 조회합니다.
*
* @param guideType 조회할 가이드 타입
* @return 가이드 확인 여부 응답 데이터
*/
@Transactional(readOnly = true)
public GetGuideViewStatusResponse getGuideViewStatus(GuideType guideType) {
User user = userRepository.findById(UserAuthorizationUtil.getLoginUserId())
.orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER));

boolean isViewed = guideViewStatusRepository.existsByUserAndGuideType(user, guideType);
return GetGuideViewStatusResponse.from(isViewed);
}
}
93 changes: 85 additions & 8 deletions src/test/java/side/onetime/user/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,14 @@
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.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.*;
Expand Down Expand Up @@ -460,4 +456,85 @@ public void logoutUser() throws Exception {
)
));
}

@Test
@DisplayName("가이드 확인 여부를 저장한다.")
public void createGuideViewStatus() throws Exception {
// given
CreateGuideViewStatusRequest request = new CreateGuideViewStatusRequest(GuideType.SCHEDULE_GUIDE_MODAL_001);
String requestContent = objectMapper.writeValueAsString(request);

// when
Mockito.doNothing().when(userService).createGuideViewStatus(any(CreateGuideViewStatusRequest.class));

// then
mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/users/guides/view-status")
.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-status",
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("CreateGuideViewStatusRequestSchema"))
.build()
)
));
}

@Test
@DisplayName("가이드 확인 여부를 조회한다.")
public void getGuideViewStatus() throws Exception {
// given
GuideType guideType = GuideType.SCHEDULE_GUIDE_MODAL_001;
GetGuideViewStatusResponse response = GetGuideViewStatusResponse.from(true);

// when
Mockito.when(userService.getGuideViewStatus(any(GuideType.class))).thenReturn(response);

// then
mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/users/guides/view-status")
.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-status",
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("GetGuideViewStatusResponseSchema"))
.build()
)
));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에 400 409 예외 처리 테스트 코드도 추가해주시면 좋을 것 같아요~!
제가 이번에 에타 기능 개선하면서 예외 케이스도 테스트 코드로 추가해봤는데 가능하더라구요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

400

{
  "code": "E_BAD_REQUEST",
  "message": "올바르지 않은 enum 값입니다. 허용되지 않은 값: SCHEDULE_GUIDE_MODAL_0013232",
  "is_success": false
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

409

{
  "code": "USER-005",
  "message": "이미 조회한 가이드입니다.",
  "is_success": false
}

}
Loading