Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
55 changes: 51 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,53 @@ 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);
}

/**
* 가이드 확인 여부 삭제 API.
*
* 사용자의 가이드 확인 여부를 삭제합니다.
*
* @return 성공 상태 응답 객체
*/
@DeleteMapping("/guides/view-status")
public ResponseEntity<ApiResponse<GetGuideViewStatusResponse>> deleteGuideViewStatus(){

userService.deleteGuideViewStatus();
return ApiResponse.onSuccess(SuccessStatus._DELETE_GUIDE_VIEW_STATUS);
}
}
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,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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_STATUS(HttpStatus.CREATED, "201", "유저 가이드 확인 여부 저장에 성공했습니다."),
_GET_GUIDE_VIEW_STATUS(HttpStatus.OK, "200", "유저 가이드 확인 여부 조회에 성공했습니다."),
_DELETE_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,15 @@
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;

import java.util.Optional;

public interface GuideViewStatusRepository extends JpaRepository<GuideViewStatus, Long> {

boolean existsByUserAndGuideType(User user, GuideType guideType);

Optional<GuideViewStatus> findByUserAndIsViewedTrue(User user);
}
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
69 changes: 65 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,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 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);
}

/**
* 가이드 확인 여부 삭제 메서드.
*
* 사용자의 가이드 확인 여부를 삭제합니다.
*/
@Transactional
public void deleteGuideViewStatus() {
User user = userRepository.findById(UserAuthorizationUtil.getLoginUserId())
.orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_USER));

GuideViewStatus guideViewStatus = guideViewStatusRepository.findByUserAndIsViewedTrue(user)
.orElseThrow(() -> new CustomException(UserErrorStatus._NOT_FOUND_GUIDE));

guideViewStatusRepository.delete(guideViewStatus);
}
}
Loading
Loading