Skip to content

Commit e67d9c3

Browse files
authored
[T3-176] UserOnboardingInfo 엔티티 생성 및 온보딩 등록 API V2 개발 (#70)
* feat: UserOnboardingInfo 엔티티 생성 및 온보딩 조회 API 개발 * feat: 온보딩 요청 API V2 개발 및 UserOnboardingInfo 객체 저장 * fix: rename
1 parent c94c07c commit e67d9c3

12 files changed

Lines changed: 295 additions & 4 deletions

File tree

src/main/java/bitnagil/bitnagil_backend/global/errorcode/ErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public enum ErrorCode {
7676

7777
// 온보딩 관련 에러 코드
7878
NOT_FOUND_RECOMMENDED_ROUTINE("ON000", HttpStatus.NOT_FOUND, "조건에 맞는 추천 루틴을 찾을 수 없습니다."),
79+
NOT_FOUND_USER_ONBOARDING_INFO("ON001", HttpStatus.NOT_FOUND, "온보딩 정보가 존재하지 않습니다."),
7980

8081
// 감정구슬 관련 에러코드
8182
ALREADY_REGISTERED_EMOTION_MARBLE("EM000", HttpStatus.CONFLICT, "감정구슬은 하루에 한번만 등록할 수 있습니다."),

src/main/java/bitnagil/bitnagil_backend/onboarding/controller/OnboardingController.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
import bitnagil.bitnagil_backend.global.response.CustomResponseDto;
55
import bitnagil.bitnagil_backend.onboarding.controller.spec.OnboardingSpec;
66
import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequest;
7+
import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequestV2;
78
import bitnagil.bitnagil_backend.onboarding.request.RegistrationRoutinesRequest;
89
import bitnagil.bitnagil_backend.onboarding.response.OnboardingResponse;
910
import bitnagil.bitnagil_backend.onboarding.service.OnboardingService;
1011
import bitnagil.bitnagil_backend.user.domain.User;
1112
import lombok.RequiredArgsConstructor;
12-
import org.springframework.web.bind.annotation.PostMapping;
13-
import org.springframework.web.bind.annotation.RequestBody;
14-
import org.springframework.web.bind.annotation.RequestMapping;
15-
import org.springframework.web.bind.annotation.RestController;
13+
import org.springframework.web.bind.annotation.*;
1614

1715
@RestController
1816
@RequiredArgsConstructor
@@ -21,12 +19,19 @@ public class OnboardingController implements OnboardingSpec {
2119

2220
private final OnboardingService onboardingService;
2321

22+
@Deprecated
2423
@PostMapping("/v1/onboardings")
2524
public CustomResponseDto<OnboardingResponse> startOnboarding(@RequestBody OnboardingRequest onboardingRequest,
2625
@CurrentUser User user) {
2726
return onboardingService.startOnboarding(onboardingRequest, user);
2827
}
2928

29+
@PostMapping("/v2/onboardings")
30+
public CustomResponseDto<OnboardingResponse> startOnboardingV2(@RequestBody OnboardingRequestV2 onboardingRequest,
31+
@CurrentUser User user) {
32+
return onboardingService.startOnboardingV2(onboardingRequest, user);
33+
}
34+
3035
// 온보딩 루틴 등록 API (V2)
3136
@PostMapping("/v2/onboardings/routines")
3237
public CustomResponseDto<Object> registrationRoutinesV2(@RequestBody RegistrationRoutinesRequest registrationRoutinesRequest,

src/main/java/bitnagil/bitnagil_backend/onboarding/controller/spec/OnboardingSpec.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import bitnagil.bitnagil_backend.global.swagger.ApiErrorCodeExamples;
66
import bitnagil.bitnagil_backend.global.swagger.ApiTags;
77
import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequest;
8+
import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequestV2;
89
import bitnagil.bitnagil_backend.onboarding.request.RegistrationRoutinesRequest;
910
import bitnagil.bitnagil_backend.onboarding.response.OnboardingResponse;
1011
import bitnagil.bitnagil_backend.user.domain.User;
@@ -15,6 +16,12 @@
1516
@Tag(name = ApiTags.ONBOARDING)
1617
public interface OnboardingSpec {
1718

19+
@Operation(summary = "(v2) 온보딩을 수행하고, 추천 루틴을 응답받습니다. v1과 달리 emotionType 복수선택을 반영하기 위해 배열형태로 받습니다.")
20+
@ApiErrorCodeExamples({
21+
ErrorCode.NOT_FOUND_USER, ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE, ErrorCode.NOT_FOUND_USER_ONBOARDING_INFO
22+
})
23+
public CustomResponseDto<OnboardingResponse> startOnboardingV2(OnboardingRequestV2 onboardingRequestV2, User user);
24+
1825
@Operation(summary = "온보딩을 수행하고, 추천 루틴을 응답받습니다.")
1926
@ApiErrorCodeExamples({
2027
ErrorCode.NOT_FOUND_USER, ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package bitnagil.bitnagil_backend.onboarding.request;
2+
3+
4+
import bitnagil.bitnagil_backend.onboarding.domain.enums.RealOutingFrequency;
5+
import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
import lombok.AccessLevel;
8+
import lombok.AllArgsConstructor;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
import java.time.LocalTime;
13+
import java.util.List;
14+
15+
@Getter
16+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
17+
@Schema(description = "온보딩 요청 DTO")
18+
@AllArgsConstructor
19+
public class OnboardingRequestV2 {
20+
21+
@Schema(description = "어떤 시간대를 더 잘 보내고 싶나요?", required = true, example = "08:00:00")
22+
private LocalTime timeSlot;
23+
@Schema(description = "요즘 어떤 회복이 필요하신가요?", required = true)
24+
private List<String> emotionType;
25+
@Schema(description = "최근 얼마나 자주 바깥바람을 쐬시나요?", required = true)
26+
private RealOutingFrequency realOutingFrequency;
27+
@Schema(description = "일주일에 몇번 외출하고 싶으신가요?", required = true)
28+
private TargetOutingFrequency targetOutingFrequency;
29+
30+
}

src/main/java/bitnagil/bitnagil_backend/onboarding/service/OnboardingService.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import bitnagil.bitnagil_backend.global.exception.CustomException;
1010
import bitnagil.bitnagil_backend.global.response.CustomResponseDto;
1111
import bitnagil.bitnagil_backend.onboarding.domain.Onboarding;
12+
import bitnagil.bitnagil_backend.onboarding.domain.enums.EmotionType;
1213
import bitnagil.bitnagil_backend.onboarding.repository.OnboardingRepository;
1314
import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequest;
15+
import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequestV2;
1416
import bitnagil.bitnagil_backend.onboarding.request.RegistrationRoutinesRequest;
1517
import bitnagil.bitnagil_backend.onboarding.response.OnboardingResponse;
1618
import bitnagil.bitnagil_backend.recommendedRoutine.response.RecommendedRoutineDto;
@@ -27,6 +29,8 @@
2729
import bitnagil.bitnagil_backend.routineV2.service.RoutineV2Factory;
2830
import bitnagil.bitnagil_backend.user.domain.User;
2931
import bitnagil.bitnagil_backend.user.service.UserManager;
32+
import bitnagil.bitnagil_backend.userOnboardingInfo.domain.UserOnboardingInfo;
33+
import bitnagil.bitnagil_backend.userOnboardingInfo.repository.UserOnboardingInfoRepository;
3034
import lombok.RequiredArgsConstructor;
3135

3236
import org.springframework.stereotype.Service;
@@ -56,6 +60,7 @@ public class OnboardingService {
5660
// TODO: v2로 전환 시 Rename
5761
private final RoutineInfoV2Repository routineInfoV2Repository;
5862
private final RoutineV2Repository routineV2Repository;
63+
private final UserOnboardingInfoRepository userOnboardingInfoRepository;
5964

6065
private final RoutineV2Factory routineV2Factory;
6166
private final RoutineInfoV2Factory routineInfoV2Factory;
@@ -94,6 +99,59 @@ public CustomResponseDto<OnboardingResponse> startOnboarding(OnboardingRequest r
9499
return CustomResponseDto.from(response);
95100
}
96101

102+
/**
103+
* 유저와 매칭되는 온보딩 결과를 설정하고, 리턴하는 메서드
104+
* todo: v2로 전환 예정
105+
*/
106+
@Transactional
107+
public CustomResponseDto<OnboardingResponse> startOnboardingV2(OnboardingRequestV2 request, User user) {
108+
// 요청에 알맞는 Onboarding 객체를 찾는다.
109+
Onboarding onboarding = onboardingRepository
110+
.findByTimeSlotAndEmotionTypeAndRealOutingFrequencyAndTargetOutingFrequency(
111+
request.getTimeSlot(),
112+
EmotionType.valueOf(request.getEmotionType().get(0)), // EmotionType은 List로 받지만, 단일값으로 처리
113+
request.getRealOutingFrequency(),
114+
request.getTargetOutingFrequency()
115+
);
116+
117+
if(onboarding == null) {
118+
throw new CustomException(ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE);
119+
}
120+
121+
// 회원은 온보딩과의 연관관계를 설정한다.
122+
User persistedUser = userManager.getPersistedUser(user);
123+
persistedUser.updateOnboarding(onboarding);
124+
125+
// 회원과 연관된 UserOnboardingInfo 객체를 찾고, 기존에 존재하는 경우 update, 없는 경우 생성한다.
126+
UserOnboardingInfo userOnboardingInfo = userOnboardingInfoRepository.findByUser(persistedUser);
127+
if (userOnboardingInfo == null) { // insert
128+
UserOnboardingInfo newUserOnboardingInfo = UserOnboardingInfo.builder()
129+
.user(persistedUser)
130+
.timeSlot(request.getTimeSlot())
131+
.emotionTypes(request.getEmotionType())
132+
.targetOutingFrequency(request.getTargetOutingFrequency())
133+
.build();
134+
userOnboardingInfoRepository.save(newUserOnboardingInfo);
135+
}else{ // update
136+
userOnboardingInfo.updateUserOnboardingInfo(
137+
request.getTimeSlot(),
138+
request.getEmotionType(),
139+
request.getTargetOutingFrequency()
140+
);
141+
}
142+
143+
// 온보딩의 CASE를 통해 추천루틴을 조회한다.
144+
List<RecommendedRoutineDto> recommendedRoutineDtoList =
145+
recommendedRoutineManager.recommendRoutinesByEmotionMarble(onboarding.getResultCase());
146+
147+
OnboardingResponse response = OnboardingResponse.builder()
148+
.recommendedRoutines(recommendedRoutineDtoList)
149+
.build();
150+
151+
return CustomResponseDto.from(response);
152+
}
153+
154+
97155
/**
98156
* 온보딩 시 추천 루틴을 저장하는 메서드
99157
*/
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package bitnagil.bitnagil_backend.userOnboardingInfo.controller;
2+
3+
import bitnagil.bitnagil_backend.global.annotation.CurrentUser;
4+
import bitnagil.bitnagil_backend.global.response.CustomResponseDto;
5+
import bitnagil.bitnagil_backend.user.domain.User;
6+
import bitnagil.bitnagil_backend.userOnboardingInfo.controller.spec.UserOnboardingInfoSpec;
7+
import bitnagil.bitnagil_backend.userOnboardingInfo.response.UserOnboardingInfoSearchResponse;
8+
import bitnagil.bitnagil_backend.userOnboardingInfo.service.UserOnboardingInfoService;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.web.bind.annotation.GetMapping;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
@RequestMapping(value = "/api")
17+
public class UserOnboardingInfoController implements UserOnboardingInfoSpec {
18+
19+
private final UserOnboardingInfoService userOnboardingInfoService;
20+
21+
@GetMapping("/v2/onboardings")
22+
public CustomResponseDto<UserOnboardingInfoSearchResponse> getUserOnboardingInfo(@CurrentUser User user) {
23+
return CustomResponseDto.from(userOnboardingInfoService.getUserOnboardingInfo(user));
24+
}
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package bitnagil.bitnagil_backend.userOnboardingInfo.controller.spec;
2+
3+
import bitnagil.bitnagil_backend.global.errorcode.ErrorCode;
4+
import bitnagil.bitnagil_backend.global.response.CustomResponseDto;
5+
import bitnagil.bitnagil_backend.global.swagger.ApiErrorCodeExamples;
6+
import bitnagil.bitnagil_backend.global.swagger.ApiTags;
7+
import bitnagil.bitnagil_backend.user.domain.User;
8+
import bitnagil.bitnagil_backend.userOnboardingInfo.response.UserOnboardingInfoSearchResponse;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
11+
12+
@Tag(name = ApiTags.ONBOARDING)
13+
public interface UserOnboardingInfoSpec {
14+
15+
@Operation(summary = "유저의 온보딩 정보를 조회합니다.")
16+
@ApiErrorCodeExamples({
17+
ErrorCode.NOT_FOUND_USER_ONBOARDING_INFO
18+
})
19+
public CustomResponseDto<UserOnboardingInfoSearchResponse> getUserOnboardingInfo(User user);
20+
21+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package bitnagil.bitnagil_backend.userOnboardingInfo.domain;
2+
3+
import bitnagil.bitnagil_backend.global.entity.BaseTimeEntity;
4+
import bitnagil.bitnagil_backend.global.utils.StringListConverter;
5+
import bitnagil.bitnagil_backend.onboarding.domain.Onboarding;
6+
import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency;
7+
import bitnagil.bitnagil_backend.user.domain.User;
8+
import jakarta.persistence.*;
9+
import jakarta.validation.constraints.NotNull;
10+
import lombok.AccessLevel;
11+
import lombok.Builder;
12+
import lombok.Getter;
13+
import lombok.NoArgsConstructor;
14+
import org.hibernate.annotations.SQLDelete;
15+
import org.hibernate.annotations.Where;
16+
17+
import java.time.LocalTime;
18+
import java.util.List;
19+
20+
@Getter
21+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
22+
@Entity
23+
@SQLDelete(sql = "UPDATE user_onboarding_info SET role = 'WITHDRAWN', deleted_at = NOW() WHERE user_id = ?")
24+
@Where(clause = "deleted_at IS NULL")
25+
public class UserOnboardingInfo extends BaseTimeEntity {
26+
27+
@Id
28+
@GeneratedValue(strategy = GenerationType.IDENTITY)
29+
private Long userOnboardingInfoId;
30+
31+
@NotNull
32+
private LocalTime timeSlot; // 사용자가 선택한 시간대
33+
34+
@Convert(converter = StringListConverter.class)
35+
@NotNull
36+
private List<String> emotionTypes; // 사용자가 선택한 감정 유형 목록
37+
38+
@Enumerated(EnumType.STRING)
39+
@Column(columnDefinition = "varchar(40)")
40+
@NotNull
41+
private TargetOutingFrequency targetOutingFrequency; // 사용자가 선택한 목표 외출 빈도
42+
43+
@OneToOne(fetch = FetchType.LAZY)
44+
@JoinColumn(name = "user_id")
45+
private User user;
46+
47+
@Builder
48+
public UserOnboardingInfo(LocalTime timeSlot, List<String> emotionTypes, TargetOutingFrequency targetOutingFrequency,
49+
Onboarding onboarding, User user) {
50+
this.timeSlot = timeSlot;
51+
this.emotionTypes = emotionTypes;
52+
this.targetOutingFrequency = targetOutingFrequency;
53+
this.user = user;
54+
}
55+
56+
public void updateUserOnboardingInfo(LocalTime timeSlot, List<String> emotionType, TargetOutingFrequency targetOutingFrequency) {
57+
this.timeSlot = timeSlot;
58+
this.emotionTypes = emotionType;
59+
this.targetOutingFrequency = targetOutingFrequency;
60+
}
61+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package bitnagil.bitnagil_backend.userOnboardingInfo.repository;
2+
3+
import bitnagil.bitnagil_backend.user.domain.User;
4+
import bitnagil.bitnagil_backend.userOnboardingInfo.domain.UserOnboardingInfo;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
public interface UserOnboardingInfoRepository extends JpaRepository<UserOnboardingInfo, Long> {
8+
UserOnboardingInfo findByUser(User user);
9+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package bitnagil.bitnagil_backend.userOnboardingInfo.response;
2+
3+
import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
9+
import java.time.LocalTime;
10+
import java.util.List;
11+
12+
@Getter
13+
@AllArgsConstructor
14+
@Builder
15+
public class UserOnboardingInfoSearchResponse {
16+
17+
@Schema(example = "08:00:00")
18+
private LocalTime timeSlot;
19+
20+
@Schema(example = "[\"GROWTH\", \"VITALITY\"]")
21+
private List<String> emotionTypes;
22+
23+
@Schema(example = "ONE_PER_WEEK")
24+
private TargetOutingFrequency targetOutingFrequency;
25+
26+
}

0 commit comments

Comments
 (0)