diff --git a/build.gradle b/build.gradle index f82dda3b..39e144a5 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,10 @@ dependencies { // flyway implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + + // cache + implementation 'org.springframework.boot:spring-boot-starter-cache' + } tasks.named('test') { diff --git a/src/main/java/bitnagil/bitnagil_backend/appVersion/Repository/AndroidAppVersionRepository.java b/src/main/java/bitnagil/bitnagil_backend/appVersion/Repository/AndroidAppVersionRepository.java new file mode 100644 index 00000000..2dd94e56 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/appVersion/Repository/AndroidAppVersionRepository.java @@ -0,0 +1,11 @@ +package bitnagil.bitnagil_backend.appVersion.Repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import bitnagil.bitnagil_backend.appVersion.domain.AndroidAppVersion; + +public interface AndroidAppVersionRepository extends JpaRepository { + + // major, minor가 가장 높은 AndroidAppVersion을 조회 + AndroidAppVersion findFirstByOrderByMajorDescMinorDesc(); +} diff --git a/src/main/java/bitnagil/bitnagil_backend/appVersion/controller/AndroidAppVersionController.java b/src/main/java/bitnagil/bitnagil_backend/appVersion/controller/AndroidAppVersionController.java new file mode 100644 index 00000000..004349b2 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/appVersion/controller/AndroidAppVersionController.java @@ -0,0 +1,30 @@ +package bitnagil.bitnagil_backend.appVersion.controller; + +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 bitnagil.bitnagil_backend.appVersion.controller.spec.AndroidAppVersionSpec; +import bitnagil.bitnagil_backend.appVersion.response.ForceUpdateResponse; +import bitnagil.bitnagil_backend.appVersion.service.AndroidAppVersionService; +import bitnagil.bitnagil_backend.global.response.CustomResponseDto; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/api/v1/version") +public class AndroidAppVersionController implements AndroidAppVersionSpec { + + private final AndroidAppVersionService androidAppVersionService; + + @GetMapping("/android/check") + public CustomResponseDto validateForceUpdateRequired( + @RequestParam int major, + @RequestParam int minor, + // 추후에 patch를 최소 버전 기준에 추가될 때 사용하기 위함 + @RequestParam int patch) { + + return CustomResponseDto.from(androidAppVersionService.validateForceUpdateRequired(major, minor)); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/appVersion/controller/spec/AndroidAppVersionSpec.java b/src/main/java/bitnagil/bitnagil_backend/appVersion/controller/spec/AndroidAppVersionSpec.java new file mode 100644 index 00000000..775c8b4f --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/appVersion/controller/spec/AndroidAppVersionSpec.java @@ -0,0 +1,24 @@ +package bitnagil.bitnagil_backend.appVersion.controller.spec; + +import bitnagil.bitnagil_backend.appVersion.response.ForceUpdateResponse; +import bitnagil.bitnagil_backend.global.response.CustomResponseDto; +import bitnagil.bitnagil_backend.global.swagger.ApiTags; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = ApiTags.APP_VERSION) +public interface AndroidAppVersionSpec { + + @Operation( + summary = "강제 업데이트 여부 검증", + description = "사용자의 앱 Major, Minor, Patch 버전을 받아 강제 업데이트 필요 여부를 판단합니다." + ) + @Parameters({ + @Parameter(name = "major", description = "앱 Major 버전", required = true, example = "1"), + @Parameter(name = "minor", description = "앱 Minor 버전", required = true, example = "5"), + @Parameter(name = "patch", description = "앱 Patch 버전 (추후 사용 예정)", required = true, example = "0") + }) + CustomResponseDto validateForceUpdateRequired(int major, int minor, int patch); +} diff --git a/src/main/java/bitnagil/bitnagil_backend/appVersion/domain/AndroidAppVersion.java b/src/main/java/bitnagil/bitnagil_backend/appVersion/domain/AndroidAppVersion.java new file mode 100644 index 00000000..16e5568d --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/appVersion/domain/AndroidAppVersion.java @@ -0,0 +1,31 @@ +package bitnagil.bitnagil_backend.appVersion.domain; + +import bitnagil.bitnagil_backend.global.entity.BaseTimeEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class AndroidAppVersion extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long versionId; // 버전 ID + + @NotNull + private Integer major; // 버전의 가장 좌측 숫자 + + @NotNull + private Integer minor; // 버전의 가장 중앙 숫자 + + @NotNull + private Integer patch; // 버전의 가장 우측 숫자 +} + diff --git a/src/main/java/bitnagil/bitnagil_backend/appVersion/response/ForceUpdateResponse.java b/src/main/java/bitnagil/bitnagil_backend/appVersion/response/ForceUpdateResponse.java new file mode 100644 index 00000000..7ac77ada --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/appVersion/response/ForceUpdateResponse.java @@ -0,0 +1,12 @@ +package bitnagil.bitnagil_backend.appVersion.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class ForceUpdateResponse { + private boolean forceUpdateYn; +} diff --git a/src/main/java/bitnagil/bitnagil_backend/appVersion/service/AndroidAppVersionService.java b/src/main/java/bitnagil/bitnagil_backend/appVersion/service/AndroidAppVersionService.java new file mode 100644 index 00000000..be31715a --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/appVersion/service/AndroidAppVersionService.java @@ -0,0 +1,44 @@ +package bitnagil.bitnagil_backend.appVersion.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import bitnagil.bitnagil_backend.appVersion.Repository.AndroidAppVersionRepository; +import bitnagil.bitnagil_backend.appVersion.domain.AndroidAppVersion; +import bitnagil.bitnagil_backend.appVersion.response.ForceUpdateResponse; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AndroidAppVersionService { + + private final AndroidAppVersionRepository androidAppVersionRepository; + + @Transactional(readOnly = true) + public ForceUpdateResponse validateForceUpdateRequired(Integer clientMajor, Integer clientMinor) { + + AndroidAppVersion latestVersion = androidAppVersionRepository.findFirstByOrderByMajorDescMinorDesc(); + + // major 비교 + if (clientMajor < latestVersion.getMajor()) { + // major 버전이 최소 요구 major 버전보다 낮으면 강제 업데이트 필요 + return ForceUpdateResponse.builder() + .forceUpdateYn(true) + .build(); + } + + if (clientMajor.equals(latestVersion.getMajor())) { + // major 같으면 minor 비교 + if (clientMinor < latestVersion.getMinor()) { + return ForceUpdateResponse.builder() + .forceUpdateYn(true) + .build(); + } + } + + // 강제 업데이트 필요하지 않은 경우 + return ForceUpdateResponse.builder() + .forceUpdateYn(false) + .build(); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/CustomOAuth2User.java b/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/CustomOAuth2User.java index c664efe4..eca9e8a0 100644 --- a/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/CustomOAuth2User.java +++ b/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/CustomOAuth2User.java @@ -6,8 +6,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import bitnagil.bitnagil_backend.enums.Role; -import bitnagil.bitnagil_backend.global.entity.HistoryPk; +import bitnagil.bitnagil_backend.user.domain.enums.Role; import lombok.Getter; /** diff --git a/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/OAuth2Attribute.java b/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/OAuth2Attribute.java index de2fc5ee..e4b1bd3f 100644 --- a/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/OAuth2Attribute.java +++ b/src/main/java/bitnagil/bitnagil_backend/auth/kakao/domain/OAuth2Attribute.java @@ -3,11 +3,11 @@ import java.util.HashMap; import java.util.Map; -import bitnagil.bitnagil_backend.enums.SocialType; +import bitnagil.bitnagil_backend.user.domain.enums.SocialType; import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; import bitnagil.bitnagil_backend.global.exception.CustomException; import bitnagil.bitnagil_backend.user.domain.User; -import bitnagil.bitnagil_backend.enums.Role; +import bitnagil.bitnagil_backend.user.domain.enums.Role; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/bitnagil/bitnagil_backend/auth/kakao/service/CustomOAuth2UserService.java b/src/main/java/bitnagil/bitnagil_backend/auth/kakao/service/CustomOAuth2UserService.java index 1806be33..9ba8d4b8 100644 --- a/src/main/java/bitnagil/bitnagil_backend/auth/kakao/service/CustomOAuth2UserService.java +++ b/src/main/java/bitnagil/bitnagil_backend/auth/kakao/service/CustomOAuth2UserService.java @@ -1,6 +1,5 @@ package bitnagil.bitnagil_backend.auth.kakao.service; -import java.time.LocalDateTime; import java.util.Collections; import java.util.Map; @@ -17,7 +16,7 @@ import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; import bitnagil.bitnagil_backend.global.exception.CustomException; import bitnagil.bitnagil_backend.user.repository.UserRepository; -import bitnagil.bitnagil_backend.enums.SocialType; +import bitnagil.bitnagil_backend.user.domain.enums.SocialType; import bitnagil.bitnagil_backend.user.domain.User; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/EmotionMarbleController.java b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/EmotionMarbleController.java index c3b15134..1e7237bb 100644 --- a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/EmotionMarbleController.java +++ b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/EmotionMarbleController.java @@ -3,6 +3,7 @@ import bitnagil.bitnagil_backend.emotionMarble.controller.spec.EmotionMarbleSpec; import bitnagil.bitnagil_backend.emotionMarble.request.RegisterEmotionMarbleRequest; import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponse; +import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponseV2; import bitnagil.bitnagil_backend.emotionMarble.response.RegisterEmotionMarbleResponse; import bitnagil.bitnagil_backend.emotionMarble.service.EmotionMarbleService; import bitnagil.bitnagil_backend.global.annotation.CurrentUser; @@ -18,18 +19,18 @@ @RestController @RequiredArgsConstructor -@RequestMapping(value = "/api/v1/emotion-marbles") +@RequestMapping(value = "/api") public class EmotionMarbleController implements EmotionMarbleSpec { private final EmotionMarbleService emotionMarbleService; // 감정구슬 조회 API - @GetMapping("") + @GetMapping("/v1/emotion-marbles") public CustomResponseDto> getEmotionMarbles() { return CustomResponseDto.from(emotionMarbleService.getEmotionMarbles()); } // 감정구슬 등록 API - @PostMapping("") + @PostMapping("/v1/emotion-marbles") public CustomResponseDto registryEmotionMarble( @CurrentUser User user, @RequestBody RegisterEmotionMarbleRequest request) { @@ -37,8 +38,19 @@ public CustomResponseDto registryEmotionMarble( return CustomResponseDto.from(emotionMarbleService.registryEmotionMarble(user, request)); } + // todo: 당일의 유저가 선택한 감정 구슬 조회 API V2로 변환 + @GetMapping("/v2/emotion-marbles/{searchDate}") + public CustomResponseDto getEmotionMarbleBySearchDateV2( + @CurrentUser User user, + @PathVariable LocalDate searchDate) { + + return CustomResponseDto.from(emotionMarbleService.getEmotionMarbleBySearchDateV2(user, searchDate)); + } + // 당일의 유저가 선택한 감정 구슬 조회 API - @GetMapping("/{searchDate}") + // TODO: v2로 전환 시 deprecated 처리 + @Deprecated() + @GetMapping("/v1/emotion-marbles/{searchDate}") public CustomResponseDto getEmotionMarbleBySearchDate( @CurrentUser User user, @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate searchDate) { diff --git a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/spec/EmotionMarbleSpec.java b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/spec/EmotionMarbleSpec.java index f1685a57..181019af 100644 --- a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/spec/EmotionMarbleSpec.java +++ b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/controller/spec/EmotionMarbleSpec.java @@ -2,8 +2,8 @@ import bitnagil.bitnagil_backend.emotionMarble.request.RegisterEmotionMarbleRequest; import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponse; +import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponseV2; import bitnagil.bitnagil_backend.emotionMarble.response.RegisterEmotionMarbleResponse; -import bitnagil.bitnagil_backend.global.annotation.CurrentUser; import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; import bitnagil.bitnagil_backend.global.response.CustomResponseDto; import bitnagil.bitnagil_backend.global.swagger.ApiErrorCodeExamples; @@ -14,14 +14,11 @@ import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.List; -import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; @Tag(name = ApiTags.EMOTION_MARBLE) public interface EmotionMarbleSpec { @@ -34,6 +31,14 @@ public interface EmotionMarbleSpec { public CustomResponseDto registryEmotionMarble( User user, RegisterEmotionMarbleRequest request); + @Operation(summary = "(V2) 검색 날짜 기준으로 대한 유저의 감정구슬 정보를 조회합니다.") + @Parameters({ + @Parameter(name = "searchDate", description = "감정 구슬 조회 날짜", required = true, example = "2025-08-15", + in = ParameterIn.PATH) + }) + CustomResponseDto getEmotionMarbleBySearchDateV2( + User user, @PathVariable LocalDate searchDate); + @Operation(summary = "검색 날짜 기준으로 대한 유저의 감정구슬 정보를 조회합니다.") @Parameters({ @Parameter(name = "searchDate", description = "감정 구슬 조회 날짜", required = true, example = "2025-07-01", diff --git a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/domain/enums/EmotionMarbleType.java b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/domain/enums/EmotionMarbleType.java index 097d41b6..364e49db 100644 --- a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/domain/enums/EmotionMarbleType.java +++ b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/domain/enums/EmotionMarbleType.java @@ -7,16 +7,42 @@ @RequiredArgsConstructor @Getter public enum EmotionMarbleType implements EnumType { - CALM("평온함", 5L, "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_calm.png", "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_calm.png"), - VITALITY("활기참", 6L, "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_vitality.png", "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_vitality.png"), - LETHARGY("무기력함", 7L, "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_lethargy.png", "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_lethargy.png"), - ANXIETY("불안함", 8L, "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_anxiety.png", "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_anxiety.png"), - SATISFACTION("만족함", 9L, "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_satisfaction.png", "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_satisfaction.png"), - FATIGUE("피로함", 10L, "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_fatigue.png", "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_fatigue.png") + CALM("평온함", 5L, + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_calm.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_calm_v2.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_calm.png", + "오늘은 평온하군요~"), + VITALITY("활기참", 6L, + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_vitality.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_vitality_v2.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_vitality.png", + "오늘은 활기차군요~"), + LETHARGY("무기력함", 7L, + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_lethargy.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_lethargy_v2.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_lethargy.png", + "오늘은 무기력한가요?"), + ANXIETY("불안함", 8L, + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_anxiety.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_anxiety_v2.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_anxiety.png", + "오늘은 불안한가요?"), + SATISFACTION("만족함", 9L, + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_satisfaction.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_satisfaction_v2.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_satisfaction.png", + "오늘은 만족하는군요~"), + FATIGUE("피로함", 10L, + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_fatigue.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/home_fatigue_v2.png", + "https://bitnagil-s3.s3.ap-northeast-2.amazonaws.com/marble_fatigue.png", + "오늘은 피곤한가요?"), ; private final String description; private final Long caseId; - private final String homeMarbleImageUrl; + private final String homeMarbleImageUrlV1; + private final String homeMarbleImageUrlV2; private final String marbleImageUrl; + private final String homeMessage; } diff --git a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/response/EmotionMarbleTypeResponseV2.java b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/response/EmotionMarbleTypeResponseV2.java new file mode 100644 index 00000000..24dac943 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/response/EmotionMarbleTypeResponseV2.java @@ -0,0 +1,25 @@ +package bitnagil.bitnagil_backend.emotionMarble.response; + +import bitnagil.bitnagil_backend.emotionMarble.domain.enums.EmotionMarbleType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +@Schema(description = "감정 구슬 조회 DTO") +public class EmotionMarbleTypeResponseV2 { + @Schema(description = "감정 구슬 타입", example = "CALM") + private EmotionMarbleType emotionMarbleType; + + @Schema(description = "감정 구슬 명칭", example = "평온함") + private String emotionMarbleName; + + @Schema(description = "감정 구슬 이미지 URL (홈/구슬 선택 화면 이미지 다름)", example = "https://example.com/image/calm.png") + private String imageUrl; + + @Schema(description = "감정 구슬 홈 화면 메시지", example = "오늘은 평온하군요~") + private String emotionMarbleHomeMessage; +} diff --git a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleMapper.java b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleMapper.java index 56783bba..8a18a3ff 100644 --- a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleMapper.java +++ b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleMapper.java @@ -3,10 +3,9 @@ import bitnagil.bitnagil_backend.emotionMarble.domain.EmotionMarble; import bitnagil.bitnagil_backend.emotionMarble.domain.enums.EmotionMarbleType; import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponse; +import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponseV2; import org.springframework.stereotype.Component; -import java.util.List; - /** * 감정구슬에 대한 가공된 데이터를 DTO로 변환하는 Mapper 클래스입니다. */ @@ -21,11 +20,22 @@ public EmotionMarbleTypeResponse toEmotionMarbleTypeResponse(EmotionMarbleType e .build(); } + // todo: v2로 전환 시 deprecated 처리 + @Deprecated public EmotionMarbleTypeResponse toEmotionMarbleTypeResponse(EmotionMarble emotionMarble) { return EmotionMarbleTypeResponse.builder() .emotionMarbleType(emotionMarble == null ? null : emotionMarble.getEmotionMarbleType()) .emotionMarbleName(emotionMarble == null ? null : emotionMarble.getEmotionMarbleType().getDescription()) - .imageUrl(emotionMarble == null ? null :emotionMarble.getEmotionMarbleType().getHomeMarbleImageUrl()) + .imageUrl(emotionMarble == null ? null :emotionMarble.getEmotionMarbleType().getHomeMarbleImageUrlV1()) .build(); } + + public EmotionMarbleTypeResponseV2 toEmotionMarbleTypeResponseV2(EmotionMarble emotionMarble) { + return EmotionMarbleTypeResponseV2.builder() + .emotionMarbleType(emotionMarble == null ? null : emotionMarble.getEmotionMarbleType()) + .emotionMarbleName(emotionMarble == null ? null : emotionMarble.getEmotionMarbleType().getDescription()) + .imageUrl(emotionMarble == null ? null :emotionMarble.getEmotionMarbleType().getHomeMarbleImageUrlV2()) + .emotionMarbleHomeMessage(emotionMarble == null ? null : emotionMarble.getEmotionMarbleType().getHomeMessage()) + .build(); + } } diff --git a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleService.java b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleService.java index 2e505a9e..1c934fde 100644 --- a/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleService.java +++ b/src/main/java/bitnagil/bitnagil_backend/emotionMarble/service/EmotionMarbleService.java @@ -5,6 +5,7 @@ import bitnagil.bitnagil_backend.emotionMarble.repository.EmotionMarbleRepository; import bitnagil.bitnagil_backend.emotionMarble.request.RegisterEmotionMarbleRequest; import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponse; +import bitnagil.bitnagil_backend.emotionMarble.response.EmotionMarbleTypeResponseV2; import bitnagil.bitnagil_backend.recommendedRoutine.response.RecommendedRoutineDto; import bitnagil.bitnagil_backend.emotionMarble.response.RegisterEmotionMarbleResponse; import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; @@ -70,6 +71,13 @@ public RegisterEmotionMarbleResponse registryEmotionMarble(User user, RegisterEm .build(); } + @Transactional(readOnly = true) + public EmotionMarbleTypeResponseV2 getEmotionMarbleBySearchDateV2(User user, LocalDate searchDate) { + EmotionMarble emotionMarble = emotionMarbleRepository.findByUserIdAndDateIs(user.getUserId(), searchDate); + + return emotionMarbleMapper.toEmotionMarbleTypeResponseV2(emotionMarble); + } + @Transactional(readOnly = true) public EmotionMarbleTypeResponse getEmotionMarbleBySearchDate(User user, LocalDate searchDate) { EmotionMarble emotionMarble = emotionMarbleRepository.findByUserIdAndDateIs(user.getUserId(), searchDate); diff --git a/src/main/java/bitnagil/bitnagil_backend/enums/SocialType.java b/src/main/java/bitnagil/bitnagil_backend/enums/SocialType.java deleted file mode 100644 index 0e60ffcf..00000000 --- a/src/main/java/bitnagil/bitnagil_backend/enums/SocialType.java +++ /dev/null @@ -1,6 +0,0 @@ -package bitnagil.bitnagil_backend.enums; - -public enum SocialType { - - KAKAO, APPLE -} diff --git a/src/main/java/bitnagil/bitnagil_backend/global/config/CacheConfig.java b/src/main/java/bitnagil/bitnagil_backend/global/config/CacheConfig.java new file mode 100644 index 00000000..bd809aa3 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/global/config/CacheConfig.java @@ -0,0 +1,91 @@ +package bitnagil.bitnagil_backend.global.config; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class CacheConfig { + + /** + * Jackson 직렬화 문제 해결을 위한 ObjectMapper 커스터마이징 + * Redis Cache 전용 ObjectMapper (전역 빈 아님) + */ + private ObjectMapper createRedisObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new ParameterNamesModule()); + objectMapper.registerModule(new Jdk8Module()); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.activateDefaultTyping( + objectMapper.getPolymorphicTypeValidator(), + ObjectMapper.DefaultTyping.EVERYTHING, + JsonTypeInfo.As.WRAPPER_OBJECT + ); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + // Redis 전용이므로 FEIGN, RestTemplate 등에 영향 없음 + return objectMapper; + } + + /** + * Spring Boot 가 기본적으로 RedisCacheManager 를 자동 설정해줘서 RedisCacheConfiguration 없어도 사용 가능 + * Bean 을 새로 선언하면 직접 설정한 RedisCacheConfiguration 이 적용 + */ + @Bean + public RedisCacheConfiguration redisCacheConfiguration() { + ObjectMapper redisObjectMapper = createRedisObjectMapper(); + return RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(redisObjectMapper)) + ); + } + + /** + * 여러 Redis Cache 에 관한 설정을 하고 싶다면 RedisCacheManagerBuilderCustomizer 를 사용할 수 있음 + */ + @Bean + public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { + ObjectMapper redisObjectMapper = createRedisObjectMapper(); + + return (builder) -> builder + .withCacheConfiguration("recommendedRoutine", + RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(6)) // 6시간 동안 캐시 유지 + .disableCachingNullValues() // null 값 캐싱 비활성화 + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(redisObjectMapper)) + )) + .withCacheConfiguration("categoryRecommendedRoutine", + RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(6)) // 6시간 동안 캐시 유지 + .disableCachingNullValues() // null 값 캐싱 비활성화 + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(redisObjectMapper)) + )); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/global/config/SecurityConfig.java b/src/main/java/bitnagil/bitnagil_backend/global/config/SecurityConfig.java index 086fff9a..db685517 100644 --- a/src/main/java/bitnagil/bitnagil_backend/global/config/SecurityConfig.java +++ b/src/main/java/bitnagil/bitnagil_backend/global/config/SecurityConfig.java @@ -36,7 +36,8 @@ public class SecurityConfig { "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", - "/api/v1/health-check" + "/api/v1/health-check", + "/api/v1/version/**" }; private final JwtAuthenticationFilter jwtAuthenticationFilter; @@ -62,6 +63,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // GUEST 권한으로만 접근 가능한 경로 .requestMatchers("/api/v1/auth/agreements").hasRole("GUEST") + // ONBAORDING 권한으로만 접근 가능한 경로 + .requestMatchers("/api/v1/onboardings", "/api/v2/onboardings", "/api/v1/onboardings/routines", "/api/v2/onboardings/routines", "/api/v1/users/infos").hasAnyRole("USER", "ONBOARDING") // USER 권한으로만 접근 가능한 경로(전체) .requestMatchers("/**").hasRole("USER") .anyRequest().authenticated() diff --git a/src/main/java/bitnagil/bitnagil_backend/global/errorcode/ErrorCode.java b/src/main/java/bitnagil/bitnagil_backend/global/errorcode/ErrorCode.java index 555a0d8b..8e12206a 100644 --- a/src/main/java/bitnagil/bitnagil_backend/global/errorcode/ErrorCode.java +++ b/src/main/java/bitnagil/bitnagil_backend/global/errorcode/ErrorCode.java @@ -56,6 +56,7 @@ public enum ErrorCode { // 루틴 관련 에러 코드 NOT_FOUND_ROUTINE("RT001", HttpStatus.NOT_FOUND, "존재하지 않는 루틴입니다."), ROUTINE_USER_NOT_MATCHED("RT002", HttpStatus.FORBIDDEN, "루틴의 유저 정보와 로그인 유저 정보가 일치하지 않습니다."), + NOT_FOUND_ROUTINE_INFO("RT003", HttpStatus.NOT_FOUND, "존재하지 않는 루틴 정보입니다."), // 서브 루틴 관련 에러 코드 NOT_FOUND_SUB_ROUTINE("SR001", HttpStatus.NOT_FOUND, "해당 복합 키에 맞는 서브 루틴이 존재하지 않습니다."), @@ -75,6 +76,7 @@ public enum ErrorCode { // 온보딩 관련 에러 코드 NOT_FOUND_RECOMMENDED_ROUTINE("ON000", HttpStatus.NOT_FOUND, "조건에 맞는 추천 루틴을 찾을 수 없습니다."), + NOT_FOUND_USER_ONBOARDING_INFO("ON001", HttpStatus.NOT_FOUND, "온보딩 정보가 존재하지 않습니다."), // 감정구슬 관련 에러코드 ALREADY_REGISTERED_EMOTION_MARBLE("EM000", HttpStatus.CONFLICT, "감정구슬은 하루에 한번만 등록할 수 있습니다."), diff --git a/src/main/java/bitnagil/bitnagil_backend/global/interceptor/LoggingInterceptor.java b/src/main/java/bitnagil/bitnagil_backend/global/interceptor/LoggingInterceptor.java index 0a90eb1c..fd01f146 100644 --- a/src/main/java/bitnagil/bitnagil_backend/global/interceptor/LoggingInterceptor.java +++ b/src/main/java/bitnagil/bitnagil_backend/global/interceptor/LoggingInterceptor.java @@ -10,14 +10,27 @@ @Component public class LoggingInterceptor implements HandlerInterceptor { + private static final String START_TIME_ATTR = "startTime"; + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + long startTime = System.currentTimeMillis(); + request.setAttribute(START_TIME_ATTR, startTime); + log.info("️⏹ [REQUEST] {} {}", request.getMethod(), request.getRequestURI()); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { - log.info("⏹ [RESPONSE] {} {} - Status: {}", request.getMethod(), request.getRequestURI(), response.getStatus()); + Long startTime = (Long) request.getAttribute(START_TIME_ATTR); + long endTime = System.currentTimeMillis(); + long duration = (startTime != null) ? (endTime - startTime) : -1; + + log.info("⏹ [RESPONSE] {} {} - Status: {} ({} ms)", + request.getMethod(), + request.getRequestURI(), + response.getStatus(), + duration); } } \ No newline at end of file diff --git a/src/main/java/bitnagil/bitnagil_backend/global/swagger/ApiTags.java b/src/main/java/bitnagil/bitnagil_backend/global/swagger/ApiTags.java index 1097961d..7479056e 100644 --- a/src/main/java/bitnagil/bitnagil_backend/global/swagger/ApiTags.java +++ b/src/main/java/bitnagil/bitnagil_backend/global/swagger/ApiTags.java @@ -8,7 +8,9 @@ public class ApiTags { public static final String USER_AUTH = "유저 인증 API"; public static final String HEALTH_CHECK = "헬스체크 API"; public static final String ROUTINE = "루틴 API"; + public static final String ROUTINEV2 = "[v2] 루틴 API"; public static final String ONBOARDING = "온보딩 API"; public static final String EMOTION_MARBLE = "감정구슬 API"; public static final String RECOMMENDED_ROUTINE = "추천 루틴 API"; + public static final String APP_VERSION = "앱 버전 API"; } diff --git a/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/OnboardingController.java b/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/OnboardingController.java index 9d3a1ffb..d489bec4 100644 --- a/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/OnboardingController.java +++ b/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/OnboardingController.java @@ -4,30 +4,45 @@ import bitnagil.bitnagil_backend.global.response.CustomResponseDto; import bitnagil.bitnagil_backend.onboarding.controller.spec.OnboardingSpec; import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequest; +import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequestV2; import bitnagil.bitnagil_backend.onboarding.request.RegistrationRoutinesRequest; import bitnagil.bitnagil_backend.onboarding.response.OnboardingResponse; import bitnagil.bitnagil_backend.onboarding.service.OnboardingService; import bitnagil.bitnagil_backend.user.domain.User; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor -@RequestMapping(value = "/api/v1/onboardings") +@RequestMapping(value = "/api") public class OnboardingController implements OnboardingSpec { private final OnboardingService onboardingService; - @PostMapping() + @Deprecated + @PostMapping("/v1/onboardings") public CustomResponseDto startOnboarding(@RequestBody OnboardingRequest onboardingRequest, @CurrentUser User user) { return onboardingService.startOnboarding(onboardingRequest, user); } - @PostMapping("/routines") + @PostMapping("/v2/onboardings") + public CustomResponseDto startOnboardingV2(@RequestBody OnboardingRequestV2 onboardingRequest, + @CurrentUser User user) { + return onboardingService.startOnboardingV2(onboardingRequest, user); + } + + // 온보딩 루틴 등록 API (V2) + @PostMapping("/v2/onboardings/routines") + public CustomResponseDto registrationRoutinesV2(@RequestBody RegistrationRoutinesRequest registrationRoutinesRequest, + @CurrentUser User user) { + onboardingService.registrationRoutinesV2(registrationRoutinesRequest, user); + return CustomResponseDto.from(null); + } + + // TODO: v2로 전환 시 deprecated 처리 + @Deprecated() + @PostMapping("/v1/onboardings/routines") public CustomResponseDto registrationRoutines(@RequestBody RegistrationRoutinesRequest registrationRoutinesRequest, @CurrentUser User user) { onboardingService.registrationRoutines(registrationRoutinesRequest, user); diff --git a/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/spec/OnboardingSpec.java b/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/spec/OnboardingSpec.java index 95bb85a6..d7fb47f3 100644 --- a/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/spec/OnboardingSpec.java +++ b/src/main/java/bitnagil/bitnagil_backend/onboarding/controller/spec/OnboardingSpec.java @@ -5,6 +5,7 @@ import bitnagil.bitnagil_backend.global.swagger.ApiErrorCodeExamples; import bitnagil.bitnagil_backend.global.swagger.ApiTags; import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequest; +import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequestV2; import bitnagil.bitnagil_backend.onboarding.request.RegistrationRoutinesRequest; import bitnagil.bitnagil_backend.onboarding.response.OnboardingResponse; import bitnagil.bitnagil_backend.user.domain.User; @@ -15,12 +16,25 @@ @Tag(name = ApiTags.ONBOARDING) public interface OnboardingSpec { + @Operation(summary = "(v2) 온보딩을 수행하고, 추천 루틴을 응답받습니다. v1과 달리 emotionType 복수선택을 반영하기 위해 배열형태로 받습니다.") + @ApiErrorCodeExamples({ + ErrorCode.NOT_FOUND_USER, ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE, ErrorCode.NOT_FOUND_USER_ONBOARDING_INFO + }) + public CustomResponseDto startOnboardingV2(OnboardingRequestV2 onboardingRequestV2, User user); + @Operation(summary = "온보딩을 수행하고, 추천 루틴을 응답받습니다.") @ApiErrorCodeExamples({ ErrorCode.NOT_FOUND_USER, ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE }) public CustomResponseDto startOnboarding(OnboardingRequest onboardingRequest, User user); + @Operation(summary = "(V2) 온보딩 시 추천 루틴을 등록합니다.(복수 선택이 가능합니다.)") + @ApiErrorCodeExamples({ + ErrorCode.NOT_FOUND_USER, ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE + }) + public CustomResponseDto registrationRoutinesV2(RegistrationRoutinesRequest registrationRoutinesRequest, + User user); + @Operation(summary = "온보딩 시 추천 루틴을 등록합니다.(복수 선택이 가능합니다.)") @ApiErrorCodeExamples({ ErrorCode.NOT_FOUND_USER, ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE diff --git a/src/main/java/bitnagil/bitnagil_backend/onboarding/request/OnboardingRequestV2.java b/src/main/java/bitnagil/bitnagil_backend/onboarding/request/OnboardingRequestV2.java new file mode 100644 index 00000000..9a87cb1d --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/onboarding/request/OnboardingRequestV2.java @@ -0,0 +1,30 @@ +package bitnagil.bitnagil_backend.onboarding.request; + + +import bitnagil.bitnagil_backend.onboarding.domain.enums.RealOutingFrequency; +import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalTime; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Schema(description = "온보딩 요청 DTO") +@AllArgsConstructor +public class OnboardingRequestV2 { + + @Schema(description = "어떤 시간대를 더 잘 보내고 싶나요?", required = true, example = "08:00:00") + private LocalTime timeSlot; + @Schema(description = "요즘 어떤 회복이 필요하신가요?", required = true) + private List emotionType; + @Schema(description = "최근 얼마나 자주 바깥바람을 쐬시나요?", required = true) + private RealOutingFrequency realOutingFrequency; + @Schema(description = "일주일에 몇번 외출하고 싶으신가요?", required = true) + private TargetOutingFrequency targetOutingFrequency; + +} diff --git a/src/main/java/bitnagil/bitnagil_backend/onboarding/service/OnboardingService.java b/src/main/java/bitnagil/bitnagil_backend/onboarding/service/OnboardingService.java index 6929e661..7d2239dc 100644 --- a/src/main/java/bitnagil/bitnagil_backend/onboarding/service/OnboardingService.java +++ b/src/main/java/bitnagil/bitnagil_backend/onboarding/service/OnboardingService.java @@ -9,8 +9,10 @@ import bitnagil.bitnagil_backend.global.exception.CustomException; import bitnagil.bitnagil_backend.global.response.CustomResponseDto; import bitnagil.bitnagil_backend.onboarding.domain.Onboarding; +import bitnagil.bitnagil_backend.onboarding.domain.enums.EmotionType; import bitnagil.bitnagil_backend.onboarding.repository.OnboardingRepository; import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequest; +import bitnagil.bitnagil_backend.onboarding.request.OnboardingRequestV2; import bitnagil.bitnagil_backend.onboarding.request.RegistrationRoutinesRequest; import bitnagil.bitnagil_backend.onboarding.response.OnboardingResponse; import bitnagil.bitnagil_backend.recommendedRoutine.response.RecommendedRoutineDto; @@ -19,9 +21,16 @@ import bitnagil.bitnagil_backend.recommendedRoutine.repository.RecommendedRoutineRepository; import bitnagil.bitnagil_backend.recommendedRoutine.repository.RecommendedSubRoutineRepository; import bitnagil.bitnagil_backend.recommendedRoutine.service.RecommendedRoutineManager; +import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; +import bitnagil.bitnagil_backend.routineInfoV2.repository.RoutineInfoV2Repository; +import bitnagil.bitnagil_backend.routineInfoV2.service.RoutineInfoV2Factory; +import bitnagil.bitnagil_backend.routineV2.domain.RoutineV2; +import bitnagil.bitnagil_backend.routineV2.repository.RoutineV2Repository; +import bitnagil.bitnagil_backend.routineV2.service.RoutineV2Factory; import bitnagil.bitnagil_backend.user.domain.User; -import bitnagil.bitnagil_backend.user.repository.UserRepository; import bitnagil.bitnagil_backend.user.service.UserManager; +import bitnagil.bitnagil_backend.userOnboardingInfo.domain.UserOnboardingInfo; +import bitnagil.bitnagil_backend.userOnboardingInfo.repository.UserOnboardingInfoRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -38,17 +47,25 @@ public class OnboardingService { private final OnboardingRepository onboardingRepository; - private final UserRepository userRepository; private final RecommendedRoutineRepository recommendRoutineRepository; private final RecommendedSubRoutineRepository recommendedSubRoutineRepository; private final ChangedRoutineRepository changedRoutineRepository; private final ChangedSubRoutineRepository changedSubRoutineRepository; - private final RecommendedRoutineManager recommendedRoutineManager; private final ChangedRoutineFactory changedRoutineFactory; private final UserManager userManager; + // V2 관련 리포지토리 + // TODO: v2로 전환 시 Rename + private final RoutineInfoV2Repository routineInfoV2Repository; + private final RoutineV2Repository routineV2Repository; + private final UserOnboardingInfoRepository userOnboardingInfoRepository; + + private final RoutineV2Factory routineV2Factory; + private final RoutineInfoV2Factory routineInfoV2Factory; + + /** * 유저와 매칭되는 온보딩 결과를 설정하고, 리턴하는 메서드 */ @@ -82,9 +99,116 @@ public CustomResponseDto startOnboarding(OnboardingRequest r return CustomResponseDto.from(response); } + /** + * 유저와 매칭되는 온보딩 결과를 설정하고, 리턴하는 메서드 + * todo: v2로 전환 예정 + */ + @Transactional + public CustomResponseDto startOnboardingV2(OnboardingRequestV2 request, User user) { + // 요청에 알맞는 Onboarding 객체를 찾는다. + Onboarding onboarding = onboardingRepository + .findByTimeSlotAndEmotionTypeAndRealOutingFrequencyAndTargetOutingFrequency( + request.getTimeSlot(), + EmotionType.valueOf(request.getEmotionType().get(0)), // EmotionType은 List로 받지만, 단일값으로 처리 + request.getRealOutingFrequency(), + request.getTargetOutingFrequency() + ); + + if(onboarding == null) { + throw new CustomException(ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE); + } + + // 회원은 온보딩과의 연관관계를 설정한다. + User persistedUser = userManager.getPersistedUser(user); + persistedUser.updateOnboarding(onboarding); + + // 회원과 연관된 UserOnboardingInfo 객체를 찾고, 기존에 존재하는 경우 update, 없는 경우 생성한다. + UserOnboardingInfo userOnboardingInfo = userOnboardingInfoRepository.findByUser(persistedUser); + if (userOnboardingInfo == null) { // insert + UserOnboardingInfo newUserOnboardingInfo = UserOnboardingInfo.builder() + .user(persistedUser) + .timeSlot(request.getTimeSlot()) + .emotionTypes(request.getEmotionType()) + .targetOutingFrequency(request.getTargetOutingFrequency()) + .build(); + userOnboardingInfoRepository.save(newUserOnboardingInfo); + }else{ // update + userOnboardingInfo.updateUserOnboardingInfo( + request.getTimeSlot(), + request.getEmotionType(), + request.getTargetOutingFrequency() + ); + } + + // 온보딩의 CASE를 통해 추천루틴을 조회한다. + List recommendedRoutineDtoList = + recommendedRoutineManager.recommendRoutinesByEmotionMarble(onboarding.getResultCase()); + + OnboardingResponse response = OnboardingResponse.builder() + .recommendedRoutines(recommendedRoutineDtoList) + .build(); + + return CustomResponseDto.from(response); + } + + + /** + * 온보딩 시 추천 루틴을 저장하는 메서드 + */ + @Transactional + public void registrationRoutinesV2(RegistrationRoutinesRequest request, User user) { + + LocalDate today = LocalDate.now(); + + for (Long recommendedRoutineId : request.getRecommendedRoutineIds()) { + // 추천 루틴을 조회한다 + RecommendedRoutine recommendedRoutine = recommendRoutineRepository.findById(recommendedRoutineId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE)); + + // 온보딩으로 등록한 루틴은 루틴 시작, 종료일자가 당일로 설정된다. + RoutineInfoV2 routineInfo = routineInfoV2Factory.createNewRoutineInfo( + recommendedRoutine.getRecommendedRoutineName(), + List.of(), // 온보딩은 반복일자를 설정하지 않는다. + recommendedRoutine.getExecutionTime(), + today, + today, + recommendedRoutine.getRecommendedRoutineType(), + user + ); + + routineInfoV2Repository.save(routineInfo); + + // 추천 서브 루틴을 조회한다. + List recommendedSubRoutines = + recommendedSubRoutineRepository.findByRecommendedRoutine(recommendedRoutine); + + // 서브 루틴 이름 리스트 생성 + List subRoutineNames = recommendedSubRoutines.stream() + .map(RecommendedSubRoutine::getSubRoutineName) + .toList(); + + // 서브 루틴 완료 여부 리스트 생성 + List subRoutineCompleteYn = recommendedSubRoutines.stream() + .map(completeYn -> false) + .toList(); + + // 루틴 정보에 해당하는 루틴을 생성한다. + RoutineV2 routine = routineV2Factory.createNewRoutine( + today, + false, + subRoutineNames, + subRoutineCompleteYn, + routineInfo); + + routineV2Repository.save(routine); + } + } + /** * 온보딩 시 추천 루틴을 저장하는 메서드 */ + // TODO: v2로 전환 시 deprecated 처리 + @Deprecated @Transactional public void registrationRoutines(RegistrationRoutinesRequest request, User user) { diff --git a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResponse.java b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResponse.java index 1513b67b..f9be8580 100644 --- a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResponse.java +++ b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResponse.java @@ -17,7 +17,7 @@ public class RecommendedRoutineSearchResponse { // 추천 루틴 타입별 루틴, 서브루틴 리스트 @Schema(description = "추천 루틴 타입을 key로 가지는 루틴 목록 Map입니다. Swagger에서는 additionalProp1처럼 보일 수 있습니다.") - Map> recommendedRoutines; + Map> recommendedRoutines; // 감정 구슬 enum 값 @Schema(description = "감정 구슬 타입") EmotionMarbleType emotionMarbleType; diff --git a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResult.java b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResult.java index 3f3cec9f..3d6ead90 100644 --- a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResult.java +++ b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/response/RecommendedRoutineSearchResult.java @@ -1,6 +1,7 @@ package bitnagil.bitnagil_backend.recommendedRoutine.response; import bitnagil.bitnagil_backend.recommendedRoutine.domain.enums.RecommendedRoutineLevel; +import bitnagil.bitnagil_backend.recommendedRoutine.domain.enums.RecommendedRoutineType; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,6 +29,8 @@ public class RecommendedRoutineSearchResult { // 추천 루틴 수행 시간 @Schema(description = "추천 루틴 수행 시간", example = "08:00:00") private LocalTime executionTime; // HH:mm 형식으로 변환된 수행 시간 + @Schema(description = "추천 루틴 타입", example = "WAKE_UP") + private RecommendedRoutineType recommendedRoutineType; // 추천 서브 루틴 리스트 private List recommendedSubRoutineSearchResult; } diff --git a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineFactory.java b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineFactory.java new file mode 100644 index 00000000..82a3810b --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineFactory.java @@ -0,0 +1,116 @@ +package bitnagil.bitnagil_backend.recommendedRoutine.service; + +import bitnagil.bitnagil_backend.emotionMarble.domain.EmotionMarble; +import bitnagil.bitnagil_backend.onboarding.domain.Case; +import bitnagil.bitnagil_backend.onboarding.domain.Onboarding; +import bitnagil.bitnagil_backend.recommendedRoutine.domain.RecommendedRoutine; +import bitnagil.bitnagil_backend.recommendedRoutine.domain.RecommendedSubRoutine; +import bitnagil.bitnagil_backend.recommendedRoutine.domain.enums.RecommendedRoutineType; +import bitnagil.bitnagil_backend.recommendedRoutine.repository.RecommendedRoutineRepository; +import bitnagil.bitnagil_backend.recommendedRoutine.repository.RecommendedSubRoutineRepository; +import bitnagil.bitnagil_backend.recommendedRoutine.response.RecommendedRoutineSearchResult; +import bitnagil.bitnagil_backend.recommendedRoutine.response.RecommendedSubRoutineSearchResult; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 추천 루틴 관련 객체 생성, 초기화 책임을 담당하는 클래스입니다. + */ +@Component +@RequiredArgsConstructor +public class RecommendedRoutineFactory { + + private final RecommendedRoutineRepository recommendedRoutineRepository; + private final RecommendedSubRoutineRepository recommendedSubRoutineRepository; + private final RecommendedRoutineMapper recommendedRoutineMapper; + + /** + * 맞춤추천 루틴을 제외한 카테고리별 추천 루틴, 서브루틴 응답을 생성하는 메서드 + * 캐싱을 위해 서비스 클래스가 아닌 Factory 클래스로 분리 + */ + @Cacheable(cacheNames = "categoryRecommendedRoutine", key = "'categoryRecommendedRoutine'") + public Map> addCategoryRecommendedRoutines() { + Map> response = new HashMap<>(); + RecommendedRoutineType[] values = RecommendedRoutineType.values(); + + for (RecommendedRoutineType value : values) { + // value가 PERSONALIZED가 아닌 경우에만 추천 루틴 조회 + if (value == RecommendedRoutineType.PERSONALIZED) { + continue; + } + // 추천 루틴 조회 + List recommendedRoutines = recommendedRoutineRepository.findByRecommendedRoutineType(value); + List recommendedRoutineResults = buildRecommendedRoutineSearchResult( + recommendedRoutines); + // Map에 값을 저장 + response.put(value.name(), recommendedRoutineResults); + } + + return response; + } + + // 추천 서브루틴 응답 객체를 생성하여 리스트에 추가 + public List buildRecommendedRoutineSearchResult( + List recommendedRoutines) { + List recommendedRoutineResults = new ArrayList<>(); // 추천 루틴 응답 객체 + // 추천 서브루틴 조회 + for (RecommendedRoutine recommendedRoutine : recommendedRoutines) { + List recommendedSubRoutines = recommendedSubRoutineRepository.findByRecommendedRoutine(recommendedRoutine); + List recommendedSubRoutineResults = new ArrayList<>(); + // 추천 서브루틴 응답 객체 생성 + addRecommendedSubRoutineToResponse(recommendedSubRoutines, recommendedSubRoutineResults); + // 추천 루틴 응답 객체 생성 + addRecommendedRoutineToResponse(recommendedRoutine, recommendedSubRoutineResults, recommendedRoutineResults); + } + return recommendedRoutineResults; + } + + // 추천루틴을 응답 객체에 추가하는 메서드 + public void addRecommendedRoutineToResponse(RecommendedRoutine recommendedRoutine, + List recommendedSubRoutineResults, + List recommendedRoutineResults) { + + RecommendedRoutineSearchResult recommendedRoutineResult = + recommendedRoutineMapper.toRecommendedRoutineSearchResult(recommendedRoutine, recommendedSubRoutineResults); + recommendedRoutineResults.add(recommendedRoutineResult); + } + + // 추천 서브루틴을 응답 객체에 추가하는 메서드 + public void addRecommendedSubRoutineToResponse(List recommendedSubRoutines, + List recommendedSubRoutineResults) { + + for (RecommendedSubRoutine recommendedSubRoutine : recommendedSubRoutines) { + RecommendedSubRoutineSearchResult recommendedSubRoutineResult = + recommendedRoutineMapper.toRecommendedSubRoutineSearchResult(recommendedSubRoutine); + recommendedSubRoutineResults.add(recommendedSubRoutineResult); + } + } + + // 감정구슬에 따른 추천 루틴을 생성하는 메서드 + public void makeEmotionMarbleResponse(EmotionMarble emotionMarble, + Map> response) { + Case resultCase = emotionMarble.getResultCase(); + List recommendedRoutines = recommendedRoutineRepository.findByResultCase(resultCase); + List recommendedRoutineResults = buildRecommendedRoutineSearchResult( + recommendedRoutines); + // 감정구슬에 따른 추천 루틴을 Map에 저장 + response.get(RecommendedRoutineType.PERSONALIZED.name()).addAll(recommendedRoutineResults); + } + + // 온보딩에 따른 추천 루틴을 생성하는 메서드 + public void makeOnboardingResponse(Onboarding onboarding, + Map> response) { + Case resultCase = onboarding.getResultCase(); + List recommendedRoutines = recommendedRoutineRepository.findByResultCase(resultCase); + List recommendedRoutineResults = buildRecommendedRoutineSearchResult( + recommendedRoutines); + // 감정구슬에 따른 추천 루틴을 Map에 저장 + response.get(RecommendedRoutineType.PERSONALIZED.name()).addAll(recommendedRoutineResults); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineMapper.java b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineMapper.java index 9b320d7c..9b9a6170 100644 --- a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineMapper.java +++ b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineMapper.java @@ -43,6 +43,7 @@ public RecommendedRoutineSearchResult toRecommendedRoutineSearchResult( .recommendedRoutineLevel(recommendedRoutine.getRecommendedRoutineLevel()) .executionTime(recommendedRoutine.getExecutionTime()) .recommendedSubRoutineSearchResult(recommendedSubRoutineResults) + .recommendedRoutineType(recommendedRoutine.getRecommendedRoutineType()) .build(); } @@ -58,7 +59,7 @@ public RecommendedSubRoutineSearchResult toRecommendedSubRoutineSearchResult( // 추천 카테고리 별 루틴, 서브루틴을 반환하는 DTO로 변환 public RecommendedRoutineSearchResponse toRecommendedRoutineSearchResponse( - Map> response, EmotionMarble emotionMarble) { + Map> response, EmotionMarble emotionMarble) { return RecommendedRoutineSearchResponse.builder() .recommendedRoutines(response) diff --git a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineService.java b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineService.java index 0bd8dd48..70ced5d2 100644 --- a/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineService.java +++ b/src/main/java/bitnagil/bitnagil_backend/recommendedRoutine/service/RecommendedRoutineService.java @@ -4,7 +4,6 @@ import bitnagil.bitnagil_backend.emotionMarble.repository.EmotionMarbleRepository; import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; import bitnagil.bitnagil_backend.global.exception.CustomException; -import bitnagil.bitnagil_backend.onboarding.domain.Case; import bitnagil.bitnagil_backend.onboarding.domain.Onboarding; import bitnagil.bitnagil_backend.recommendedRoutine.domain.RecommendedRoutine; import bitnagil.bitnagil_backend.recommendedRoutine.domain.RecommendedSubRoutine; @@ -20,12 +19,12 @@ import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -40,6 +39,7 @@ public class RecommendedRoutineService { private final EmotionMarbleRepository emotionMarbleRepository; private final RecommendedRoutineMapper recommendedRoutineMapper; + private final RecommendedRoutineFactory recommendedRoutineFactory; private final UserManager userManager; /** @@ -51,9 +51,11 @@ public RecommendedRoutineSearchResponse searchRecommendedRoutines(User user) { LocalDate nowDate = LocalDate.now(); + // 맞춤추천을 제외한 이외의 카테고리에 대한 추천 루틴을 response 추가 + Map> response = recommendedRoutineFactory.addCategoryRecommendedRoutines(); + // 카테고리 별 추천루틴에 대한 response 객체 생성 - Map> response = new HashMap<>(); - response.put(RecommendedRoutineType.PERSONALIZED, new ArrayList<>()); // 맞춤 루틴은 미리 초기화 한다.(감정구슬, 온보딩 결과를 넣기 위해) + response.put(RecommendedRoutineType.PERSONALIZED.name(), new ArrayList<>()); // 맞춤 루틴은 미리 초기화 한다.(감정구슬, 온보딩 결과를 넣기 위해) // 영속성 객체에 user를 저장하기 위해 user를 조회 User persistedUser = userManager.getPersistedUser(user); @@ -61,9 +63,6 @@ public RecommendedRoutineSearchResponse searchRecommendedRoutines(User user) { // 맞춤 추천(감정구슬 + 온보딩)을 조회하고 response에 추가 EmotionMarble emotionMarble = addPersonalizedRecommendedRoutine(persistedUser, nowDate, response); - // 맞춤추천 이외의 카테고리에 대한 추천 루틴을 response 추가 - addCategoryRecommendedRoutines(response); - return recommendedRoutineMapper.toRecommendedRoutineSearchResponse(response, emotionMarble); } @@ -71,6 +70,7 @@ public RecommendedRoutineSearchResponse searchRecommendedRoutines(User user) { * 추천 루틴 단건 조회 */ @Transactional(readOnly = true) + @Cacheable(cacheNames = "recommendedRoutine", key = "#recommendedRoutineId") public RecommendedRoutineSearchResult searchRecommendedRoutine(Long recommendedRoutineId) { RecommendedRoutine recommendedRoutine = recommendedRoutineRepository.findById(recommendedRoutineId) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_RECOMMENDED_ROUTINE)); @@ -86,94 +86,20 @@ public RecommendedRoutineSearchResult searchRecommendedRoutine(Long recommendedR return recommendedRoutineMapper.toRecommendedRoutineSearchResult(recommendedRoutine, recommendedSubRoutineSearchResults); } - private void addCategoryRecommendedRoutines(Map> response) { - RecommendedRoutineType[] values = RecommendedRoutineType.values(); - - for (RecommendedRoutineType value : values) { - // value가 PERSONALIZED가 아닌 경우에만 추천 루틴 조회 - if (value == RecommendedRoutineType.PERSONALIZED) { - continue; - } - // 추천 루틴 조회 - List recommendedRoutines = recommendedRoutineRepository.findByRecommendedRoutineType(value); - List recommendedRoutineResults = buildRecommendedRoutineSearchResult( - recommendedRoutines); - // Map에 값을 저장 - response.put(value, recommendedRoutineResults); - } - } - private EmotionMarble addPersonalizedRecommendedRoutine(User user, LocalDate nowDate, - Map> response) { + Map> response) { // 감정구슬(당일에 감정구슬을 선택한 경우만 조회) EmotionMarble emotionMarble = emotionMarbleRepository.findByUserIdAndDateIs(user.getUserId(), nowDate); if(emotionMarble != null) { // 조회 결과가 존재하는 경우 - makeEmotionMarbleResponse(emotionMarble, response); + recommendedRoutineFactory.makeEmotionMarbleResponse(emotionMarble, response); } // 온보딩 결과에 따른 추천 루틴 조회 Onboarding onboarding = user.getOnboarding(); if (onboarding != null) { // 온보딩을 수행한 유저의 경우(온보딩은 필수지만 방어 로직으로 추가) - makeOnboardingResponse(onboarding, response); + recommendedRoutineFactory.makeOnboardingResponse(onboarding, response); } return emotionMarble; } - - private List buildRecommendedRoutineSearchResult( - List recommendedRoutines) { - List recommendedRoutineResults = new ArrayList<>(); // 추천 루틴 응답 객체 - // 추천 서브루틴 조회 - for (RecommendedRoutine recommendedRoutine : recommendedRoutines) { - List recommendedSubRoutines = recommendedSubRoutineRepository.findByRecommendedRoutine(recommendedRoutine); - List recommendedSubRoutineResults = new ArrayList<>(); - // 추천 서브루틴 응답 객체 생성 - addRecommendedSubRoutineToResponse(recommendedSubRoutines, recommendedSubRoutineResults); - // 추천 루틴 응답 객체 생성 - addRecommendedRoutineToResponse(recommendedRoutine, recommendedSubRoutineResults, recommendedRoutineResults); - } - return recommendedRoutineResults; - } - - // 추천루틴을 응답 객체에 추가하는 메서드 - private void addRecommendedRoutineToResponse(RecommendedRoutine recommendedRoutine, - List recommendedSubRoutineResults, - List recommendedRoutineResults) { - - RecommendedRoutineSearchResult recommendedRoutineResult = - recommendedRoutineMapper.toRecommendedRoutineSearchResult(recommendedRoutine, recommendedSubRoutineResults); - recommendedRoutineResults.add(recommendedRoutineResult); - } - - // 추천 서브루틴을 응답 객체에 추가하는 메서드 - private void addRecommendedSubRoutineToResponse(List recommendedSubRoutines, - List recommendedSubRoutineResults) { - - for (RecommendedSubRoutine recommendedSubRoutine : recommendedSubRoutines) { - RecommendedSubRoutineSearchResult recommendedSubRoutineResult = - recommendedRoutineMapper.toRecommendedSubRoutineSearchResult(recommendedSubRoutine); - recommendedSubRoutineResults.add(recommendedSubRoutineResult); - } - } - - // 감정구슬에 따른 추천 루틴을 생성하는 메서드 - private void makeEmotionMarbleResponse(EmotionMarble emotionMarble, - Map> response) { - Case resultCase = emotionMarble.getResultCase(); - List recommendedRoutines = recommendedRoutineRepository.findByResultCase(resultCase); - List recommendedRoutineResults = buildRecommendedRoutineSearchResult( - recommendedRoutines); - // 감정구슬에 따른 추천 루틴을 Map에 저장 - response.get(RecommendedRoutineType.PERSONALIZED).addAll(recommendedRoutineResults); - } - - // 온보딩에 따른 추천 루틴을 생성하는 메서드 - private void makeOnboardingResponse(Onboarding onboarding, - Map> response) { - Case resultCase = onboarding.getResultCase(); - List recommendedRoutines = recommendedRoutineRepository.findByResultCase(resultCase); - List recommendedRoutineResults = buildRecommendedRoutineSearchResult( - recommendedRoutines); - // 감정구슬에 따른 추천 루틴을 Map에 저장 - response.get(RecommendedRoutineType.PERSONALIZED).addAll(recommendedRoutineResults); - } } + diff --git a/src/main/java/bitnagil/bitnagil_backend/routine/controller/RoutineController.java b/src/main/java/bitnagil/bitnagil_backend/routine/controller/RoutineController.java index 36df8607..83cce02b 100644 --- a/src/main/java/bitnagil/bitnagil_backend/routine/controller/RoutineController.java +++ b/src/main/java/bitnagil/bitnagil_backend/routine/controller/RoutineController.java @@ -3,13 +3,11 @@ import java.time.LocalDate; import java.util.UUID; -import bitnagil.bitnagil_backend.routine.domain.enums.RoutineType; import bitnagil.bitnagil_backend.routine.request.DeleteRoutineByDayRequest; import bitnagil.bitnagil_backend.routine.request.UpdateRoutineCompletionRequest; import bitnagil.bitnagil_backend.routine.response.RoutineSearchResultDto; import jakarta.validation.constraints.NotNull; -import org.springframework.security.core.parameters.P; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -17,19 +15,16 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import bitnagil.bitnagil_backend.routine.request.RoutineSearchRequest; import bitnagil.bitnagil_backend.routine.response.RoutineSearchResponse; import org.springframework.web.bind.annotation.*; import bitnagil.bitnagil_backend.global.annotation.CurrentUser; import bitnagil.bitnagil_backend.global.response.CustomResponseDto; import bitnagil.bitnagil_backend.routine.controller.spec.RoutineSpec; -import bitnagil.bitnagil_backend.routine.domain.Routine; import bitnagil.bitnagil_backend.routine.request.RegisterRoutineRequest; import bitnagil.bitnagil_backend.routine.request.UpdateRoutineRequest; import bitnagil.bitnagil_backend.routine.service.RoutineService; import bitnagil.bitnagil_backend.user.domain.User; -import lombok.Getter; import lombok.RequiredArgsConstructor; @RestController @@ -39,6 +34,7 @@ public class RoutineController implements RoutineSpec { private final RoutineService routineService; + @Deprecated() @PostMapping("") public CustomResponseDto registerRoutine(@CurrentUser User user, @RequestBody RegisterRoutineRequest registerRoutineRequest) { @@ -47,6 +43,7 @@ public CustomResponseDto registerRoutine(@CurrentUser User user, return CustomResponseDto.from(null); } + @Deprecated() @PatchMapping("") public CustomResponseDto updateRoutine(@CurrentUser User user, @RequestBody UpdateRoutineRequest updateRoutineRequest) { @@ -56,7 +53,7 @@ public CustomResponseDto updateRoutine(@CurrentUser User user, } @DeleteMapping("/{routineId}") - public CustomResponseDto deleteRoutine(@CurrentUser User user, @PathVariable UUID routineId) { + public CustomResponseDto deleteRoutine(@CurrentUser User user, @PathVariable String routineId) { routineService.deleteRoutine(user, routineId); return CustomResponseDto.from(null); @@ -65,6 +62,7 @@ public CustomResponseDto deleteRoutine(@CurrentUser User user, @PathVari /* * 유저가 선택한 요일(당일)만 삭제하는 API입니다. */ + @Deprecated() @DeleteMapping("/day") public CustomResponseDto deleteRoutineByDay(@CurrentUser User user, @RequestBody DeleteRoutineByDayRequest deleteRoutineByDayRequest) { @@ -77,6 +75,7 @@ public CustomResponseDto deleteRoutineByDay(@CurrentUser User user, * 회원이 보유한 특정 기간(start_date, end_date)의 루틴을 조회하는 API입니다. */ @GetMapping + @Deprecated() public CustomResponseDto getRoutines(@CurrentUser User user, @RequestParam @NotNull LocalDate startDate, @RequestParam @NotNull LocalDate endDate) { @@ -87,6 +86,7 @@ public CustomResponseDto getRoutines(@CurrentUser User us * 루틴 완료 여부 업데이트 * 새 엔티티를 생성할 수도, 부분 수정할 수도 있기에 PATCH를 쓰지 않고 POST를 씁니다. */ + @Deprecated() @PostMapping("/completions") public CustomResponseDto updateRoutineCompletionStatus(@CurrentUser User user, @RequestBody UpdateRoutineCompletionRequest updateRoutineCompletionRequest) { @@ -96,6 +96,7 @@ public CustomResponseDto updateRoutineCompletionStatus(@CurrentUser User } // 루틴 수정 페이지에서 사용되는 루틴 단건 조회 API + @Deprecated() @GetMapping("{routineId}") public CustomResponseDto getRoutine(@CurrentUser User user, @PathVariable UUID routineId) { diff --git a/src/main/java/bitnagil/bitnagil_backend/routine/controller/spec/RoutineSpec.java b/src/main/java/bitnagil/bitnagil_backend/routine/controller/spec/RoutineSpec.java index e241918f..f925e505 100644 --- a/src/main/java/bitnagil/bitnagil_backend/routine/controller/spec/RoutineSpec.java +++ b/src/main/java/bitnagil/bitnagil_backend/routine/controller/spec/RoutineSpec.java @@ -3,10 +3,6 @@ import java.time.LocalDate; import java.util.UUID; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; - -import bitnagil.bitnagil_backend.global.annotation.CurrentUser; import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; import bitnagil.bitnagil_backend.global.response.CustomResponseDto; import bitnagil.bitnagil_backend.global.swagger.ApiErrorCodeExamples; @@ -43,9 +39,9 @@ public interface RoutineSpec { ErrorCode.ROUTINE_USER_NOT_MATCHED}) CustomResponseDto updateRoutine(User user, UpdateRoutineRequest updateRoutineRequest); - @Operation(summary = "루틴 및 서브 루틴을 모두 삭제합니다.") + @Operation(summary = "(V2) 모든 날짜에서 루틴을 삭제합니다. (루틴 전체 삭제)") @ApiErrorCodeExamples({ErrorCode.NOT_FOUND_ROUTINE, ErrorCode.ROUTINE_USER_NOT_MATCHED}) - CustomResponseDto deleteRoutine(User user, UUID routineId); + CustomResponseDto deleteRoutine(User user, String routineId); @Operation(summary = "여러 루틴의 완료 여부를 갱신합니다.") @ApiErrorCodeExamples({ diff --git a/src/main/java/bitnagil/bitnagil_backend/routine/request/RegisterRoutineRequest.java b/src/main/java/bitnagil/bitnagil_backend/routine/request/RegisterRoutineRequest.java index 976e2c1a..3aab660b 100644 --- a/src/main/java/bitnagil/bitnagil_backend/routine/request/RegisterRoutineRequest.java +++ b/src/main/java/bitnagil/bitnagil_backend/routine/request/RegisterRoutineRequest.java @@ -23,6 +23,7 @@ public class RegisterRoutineRequest { @Schema(description = "반복 요일에 대한 리스트입니다.", example = "[\"MONDAY\", \"FRIDAY\"]", required = true) + @NotNull private List repeatDay; @Schema(description = "루틴 시작 시간입니다.", diff --git a/src/main/java/bitnagil/bitnagil_backend/routine/service/RoutineService.java b/src/main/java/bitnagil/bitnagil_backend/routine/service/RoutineService.java index 6d88fff0..f345501e 100644 --- a/src/main/java/bitnagil/bitnagil_backend/routine/service/RoutineService.java +++ b/src/main/java/bitnagil/bitnagil_backend/routine/service/RoutineService.java @@ -40,6 +40,10 @@ import bitnagil.bitnagil_backend.routine.request.RegisterRoutineRequest; import bitnagil.bitnagil_backend.routine.request.SubRoutineInfo; import bitnagil.bitnagil_backend.routine.request.UpdateRoutineRequest; +import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; +import bitnagil.bitnagil_backend.routineInfoV2.repository.RoutineInfoV2Repository; +import bitnagil.bitnagil_backend.routineV2.domain.RoutineV2; +import bitnagil.bitnagil_backend.routineV2.repository.RoutineV2Repository; import bitnagil.bitnagil_backend.user.domain.User; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -63,6 +67,8 @@ public class RoutineService { private final RoutineFactory routineFactory; private final RoutineMapper routineMapper; private final ChangedRoutineFactory changedRoutineFactory; + private final RoutineV2Repository routineV2Repository; + private final RoutineInfoV2Repository routineInfoV2Repository; // 루틴, 세부루틴을 함께 저장하는 루틴 등록 메서드 @Transactional @@ -157,24 +163,20 @@ public void updateRoutine(User user, UpdateRoutineRequest request) { } } - // 루틴, 세부 루틴을 삭제하는 메서드 + // 루틴을 모든 날짜에서 삭제하는 메서드 @Transactional - public void deleteRoutine(User user, UUID routineId) { + public void deleteRoutine(User user, String routineId) { + LocalDate today = LocalDate.now(); LocalDateTime now = LocalDateTime.now(); - Routine routine = routineValidator.validateRoutineOwnership(routineId, user, now); - - // 기존 루틴, 서브 루틴의 이력 종료일시 및 deleteAt 갱신 - routine.updateHistoryEndDateTime(now); - routine.setDeleteAt(now); - - // 서브 루틴을 순회하면서 이력 종료일시 및 deleteAt 갱신 - subRoutineRepository.findByRoutineId(routineId) - .forEach(subRoutine -> { - subRoutine.updateHistoryEndDateTime(now); - subRoutine.setDeleteAt(now); - }); - + if (routineId.length() == 36) { // (v1) routineId의 타입이 UUID인 경우 + UUID v1RoutineId = UUID.fromString(routineId); + deleteV1Routine(user, v1RoutineId, now); + } + else { // (v2) routineId의 타입이 Long인 경우 + Long v2RoutineId = Long.valueOf(routineId); + deleteV2Routine(user, v2RoutineId, today); + } } // 유저가 선택한 요일(당일)만 루틴, 서브 루틴을 삭제하는 메서드 @@ -273,6 +275,43 @@ public void updateRoutineCompletionStatus(User user, UpdateRoutineCompletionRequ } } + // v2에서 사용하는 루틴 삭제 메서드 + private void deleteV2Routine(User user, Long v2RoutineId, LocalDate today) { + RoutineV2 routineV2 = routineV2Repository.findByUserAndRoutineId(user, v2RoutineId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTINE)); + + RoutineInfoV2 routineInfoV2 = routineV2.getRoutineInfo(); + + // 오늘 이후 루틴 내역 모두 삭제 (Hard Delete) + List routinesV2AfterToday = routineV2Repository + .findByRoutineInfoAndRoutineDateAfter(routineInfoV2, today); + + List routineIds = routinesV2AfterToday.stream() + .map(RoutineV2::getRoutineId) + .toList(); + + routineV2Repository.deleteAllPhysicallyByIds(routineIds); // 물리 삭제 + + routineInfoV2.updateRoutineEndDate(today); // 종료 일자를 삭제 당일로 변경 + routineInfoV2.updateRoutineDeletedYn(true); // 루틴 삭제 여부 갱신 + } + + // v1에서 사용하는 루틴 삭제 메서드 + private void deleteV1Routine(User user, UUID v1RoutineId, LocalDateTime now) { + Routine routine = routineValidator.validateRoutineOwnership(v1RoutineId, user, now); + + // 기존 루틴, 서브 루틴의 이력 종료일시 및 deleteAt 갱신 + routine.updateHistoryEndDateTime(now); + routine.setDeleteAt(now); + + // 서브 루틴을 순회하면서 이력 종료일시 및 deleteAt 갱신 + subRoutineRepository.findByRoutineId(v1RoutineId) + .forEach(subRoutine -> { + subRoutine.updateHistoryEndDateTime(now); + subRoutine.setDeleteAt(now); + }); + } + // routineCompletionId에 해당하는 완료 여부 데이터 삭제 private void deleteRoutineCompletionIfRoutineIdMatches(Long routineCompletionId, UUID routineId) { diff --git a/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/domain/RoutineInfoV2.java b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/domain/RoutineInfoV2.java index 6789445d..279c50df 100644 --- a/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/domain/RoutineInfoV2.java +++ b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/domain/RoutineInfoV2.java @@ -5,16 +5,11 @@ import java.time.LocalTime; import java.util.List; +import bitnagil.bitnagil_backend.global.entity.BaseTimeEntity; import bitnagil.bitnagil_backend.global.utils.DayOfWeekConverter; +import bitnagil.bitnagil_backend.recommendedRoutine.domain.enums.RecommendedRoutineType; import bitnagil.bitnagil_backend.user.domain.User; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @@ -29,9 +24,9 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -@SQLDelete(sql = "UPDATE routine_info_v2 SET deleted_at = NOW() WHERE routine_info_id = ?") +@SQLDelete(sql = "UPDATE routine_infov2 SET deleted_at = NOW() WHERE routine_info_id = ?") @Where(clause = "deleted_at IS NULL") -public class RoutineInfoV2 { +public class RoutineInfoV2 extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long routineInfoId; // 루틴 정보 ID @@ -52,18 +47,36 @@ public class RoutineInfoV2 { @NotNull private LocalDate routineEndDate; // 루틴 종료 일자 + @NotNull + private Boolean routineDeletedYn; // 루틴 삭제 여부 + + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "varchar(40)") + private RecommendedRoutineType recommendedRoutineType; // 추천 루틴 타입 + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; // 루틴의 주체인 유저 @Builder public RoutineInfoV2(String routineName, List routineRepeatDay, LocalTime routineExecutionTime, - LocalDate routineStartDate, LocalDate routineEndDate, User user) { + LocalDate routineStartDate, LocalDate routineEndDate, Boolean routineDeletedYn, User user, + RecommendedRoutineType recommendedRoutineType) { this.routineName = routineName; this.routineRepeatDay = routineRepeatDay; this.routineExecutionTime = routineExecutionTime; this.routineStartDate = routineStartDate; this.routineEndDate = routineEndDate; + this.routineDeletedYn = routineDeletedYn; this.user = user; + this.recommendedRoutineType = recommendedRoutineType; + } + + public void updateRoutineEndDate(LocalDate routineEndDate) { + this.routineEndDate = routineEndDate; + } + + public void updateRoutineDeletedYn(Boolean routineDeletedYn) { + this.routineDeletedYn = routineDeletedYn; } } diff --git a/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/repository/RoutineInfoV2Repository.java b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/repository/RoutineInfoV2Repository.java new file mode 100644 index 00000000..77e09cd4 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/repository/RoutineInfoV2Repository.java @@ -0,0 +1,10 @@ +package bitnagil.bitnagil_backend.routineInfoV2.repository; + +import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RoutineInfoV2Repository extends JpaRepository { + +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/request/RoutineInfoV2UpdateRequest.java b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/request/RoutineInfoV2UpdateRequest.java new file mode 100644 index 00000000..e8f0e03d --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/request/RoutineInfoV2UpdateRequest.java @@ -0,0 +1,60 @@ +package bitnagil.bitnagil_backend.routineInfoV2.request; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import bitnagil.bitnagil_backend.routineV2.domain.enums.UpdateApplyDate; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Schema(description = "루틴 정보 수정 DTO") +public class RoutineInfoV2UpdateRequest { + + @Schema(description = "루틴 ID 값입니다.", + example = "3", + required = true) + @NotNull + private String routineId; + + @Schema(description = "오늘/내일 중 반영 시작할 날짜", + example = "TODAY", + required = true) + @NotNull + private UpdateApplyDate updateApplyDate; + + @Schema(description = "루틴 이름입니다.", + example = "아침 준비", + required = true) + @NotNull + private String routineName; + + @Schema(description = "반복 요일에 대한 리스트입니다. (반복요일이 없으면 당일 루틴입니다.)", + example = "[\"MONDAY\", \"FRIDAY\"]", + required = true) + @NotNull + private List repeatDay; + + @Schema(description = "루틴 시작 일자입니다.", + example = "2025-08-01") + private LocalDate routineStartDate; + + @Schema(description = "루틴 시작 일자입니다.", + example = "2025-08-31") + private LocalDate routineEndDate; + + @Schema(description = "루틴 시작 시간입니다.", + example = "08:15:00", + required = true) + @NotNull + private LocalTime executionTime; + + @Schema(description = "세부 루틴 이름에 대한 리스트입니다.", + example = "[\"손 씻기\", \"세수 하기\", \"양치 하기\"]") + private List subRoutineName; +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/service/RoutineInfoV2Factory.java b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/service/RoutineInfoV2Factory.java new file mode 100644 index 00000000..23ae8b44 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineInfoV2/service/RoutineInfoV2Factory.java @@ -0,0 +1,35 @@ +package bitnagil.bitnagil_backend.routineInfoV2.service; + +import bitnagil.bitnagil_backend.recommendedRoutine.domain.enums.RecommendedRoutineType; +import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; +import bitnagil.bitnagil_backend.user.domain.User; +import org.springframework.stereotype.Component; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +/** + * 루틴 관련 엔티티 생성, 초기화 책임을 담당하는 클래스입니다. + */ +@Component +public class RoutineInfoV2Factory { + + // 신규 RoutineInfo 엔티티 생성 및 초기화 + public RoutineInfoV2 createNewRoutineInfo(String routineName, List routineRepeatDay, + LocalTime routineExecutionTime, LocalDate routineStartDate, + LocalDate routineEndDate, RecommendedRoutineType recommendedRoutineType, + User user) { + return RoutineInfoV2.builder() + .routineName(routineName) + .routineRepeatDay(routineRepeatDay) // 온보딩은 반복일자를 설정하지 않는다. + .routineExecutionTime(routineExecutionTime) + .routineStartDate(routineStartDate) + .routineEndDate(routineEndDate) + .routineDeletedYn(false) + .user(user) + .recommendedRoutineType(recommendedRoutineType) + .build(); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/controller/RoutineV2Controller.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/controller/RoutineV2Controller.java new file mode 100644 index 00000000..127a53b6 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/controller/RoutineV2Controller.java @@ -0,0 +1,78 @@ +package bitnagil.bitnagil_backend.routineV2.controller; + +import bitnagil.bitnagil_backend.routineInfoV2.request.RoutineInfoV2UpdateRequest; +import bitnagil.bitnagil_backend.routineV2.request.RoutineV2UpdateCompletionRequest; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResponse; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResultDto; +import jakarta.validation.constraints.NotNull; +import org.springframework.web.bind.annotation.*; + +import bitnagil.bitnagil_backend.global.annotation.CurrentUser; +import bitnagil.bitnagil_backend.global.response.CustomResponseDto; +import bitnagil.bitnagil_backend.routineV2.controller.spec.RoutineV2Spec; +import bitnagil.bitnagil_backend.routineV2.request.RoutineV2RegisterRequest; +import bitnagil.bitnagil_backend.routineV2.service.RoutineV2Service; +import bitnagil.bitnagil_backend.user.domain.User; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDate; + +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/api/v2/routines") +public class RoutineV2Controller implements RoutineV2Spec { + + private final RoutineV2Service routineV2Service; + + // 회원이 보유한 특정 기간(start_date, end_date)의 루틴을 조회하는 API입니다. + @GetMapping + public CustomResponseDto getRoutines(@CurrentUser User user, + @RequestParam @NotNull LocalDate startDate, + @RequestParam @NotNull LocalDate endDate) { + return CustomResponseDto.from(routineV2Service.getRoutines(user, startDate, endDate)); + } + + // 루틴 단건 조회 + @GetMapping("{routineId}") + public CustomResponseDto getRoutine(@CurrentUser User user, + @PathVariable Long routineId) { + return CustomResponseDto.from(routineV2Service.getRoutine(user, routineId)); + } + + // 루틴을 새롭게 등록하는 API 입니다. + @PostMapping("") + public CustomResponseDto registerRoutine(@CurrentUser User user, @RequestBody RoutineV2RegisterRequest request) { + routineV2Service.registerRoutineV2(user, request); + + return CustomResponseDto.from(null); + } + + // 루틴 당일(오늘)만 삭제하는 API 입니다. + @DeleteMapping("/day/{routineId}") + public CustomResponseDto deleteRoutineByDay(@CurrentUser User user, @PathVariable Long routineId) { + routineV2Service.deleteRoutineByDay(user, routineId); + + return CustomResponseDto.from(null); + } + + @PatchMapping("") + public CustomResponseDto updateRoutineInfo(@CurrentUser User user, @RequestBody RoutineInfoV2UpdateRequest request) { + routineV2Service.updateRoutineInfo(user, request); + + return CustomResponseDto.from(null); + } + + /* + * 루틴 완료 여부를 갱신하는 API 입니다. + * 멱등성이 보장되는 업데이트 API이므로 PUT Method를 사용했습니다. + */ + @PutMapping("/completions") + public CustomResponseDto updateRoutineCompletionStatus( + @CurrentUser User user, @RequestBody RoutineV2UpdateCompletionRequest request) { + + routineV2Service.updateRoutineCompletionStatus(user, request); + + return CustomResponseDto.from(null); + } + +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/controller/spec/RoutineV2Spec.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/controller/spec/RoutineV2Spec.java new file mode 100644 index 00000000..028b351a --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/controller/spec/RoutineV2Spec.java @@ -0,0 +1,56 @@ +package bitnagil.bitnagil_backend.routineV2.controller.spec; + +import bitnagil.bitnagil_backend.global.annotation.CurrentUser; +import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; +import bitnagil.bitnagil_backend.global.response.CustomResponseDto; +import bitnagil.bitnagil_backend.global.swagger.ApiErrorCodeExamples; +import bitnagil.bitnagil_backend.global.swagger.ApiTags; +import bitnagil.bitnagil_backend.routineV2.request.RoutineV2RegisterRequest; +import bitnagil.bitnagil_backend.routineInfoV2.request.RoutineInfoV2UpdateRequest; +import bitnagil.bitnagil_backend.routineV2.request.RoutineV2UpdateCompletionRequest; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResponse; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResultDto; +import bitnagil.bitnagil_backend.user.domain.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = ApiTags.ROUTINEV2) +public interface RoutineV2Spec { + + @Operation(summary = "루틴 조회", + description = "회원이 특정 기간에 보유한 루틴에 대한 정보를 조회합니다.") + @Parameters({ + @Parameter(name = "startDate", description = "조회 시작일", required = true, example = "2025-07-01"), + @Parameter(name = "endDate", description = "조회 종료일", required = true, example = "2025-07-13") + }) + CustomResponseDto getRoutines(User user, @NotNull LocalDate startDate, @NotNull LocalDate endDate); + + @Operation(summary = "회원이 보유한 루틴을 단건 조회합니다.") + @ApiErrorCodeExamples({ErrorCode.NOT_FOUND_ROUTINE}) + CustomResponseDto getRoutine(User user, Long routineId); + + + @Operation(summary = "루틴 정보 등록 및 루틴 시작, 종료일자 사이에서 반복요일에 해당하는 날짜로 루틴 데이터를 생성합니다.") + CustomResponseDto registerRoutine(User user, RoutineV2RegisterRequest request); + + @Operation(summary = "루틴 정보를 업데이트합니다.") + @ApiErrorCodeExamples({ErrorCode.NOT_FOUND_ROUTINE, ErrorCode.NOT_FOUND_ROUTINE_INFO}) + CustomResponseDto updateRoutineInfo(User user, RoutineInfoV2UpdateRequest request); + + @Operation(summary = "여러 루틴의 완료 여부를 갱신합니다. (여러 루틴의 완료 여부를 리스트로 만들어 요청하는 방식입니다.)") + @ApiErrorCodeExamples({ErrorCode.NOT_FOUND_ROUTINE}) + CustomResponseDto updateRoutineCompletionStatus( + @CurrentUser User user, @RequestBody RoutineV2UpdateCompletionRequest request); + + @Operation(summary = "오늘만 루틴을 삭제합니다.") + @ApiErrorCodeExamples({ErrorCode.NOT_FOUND_ROUTINE}) + CustomResponseDto deleteRoutineByDay(User user, Long routineId); +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/domain/RoutineV2.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/domain/RoutineV2.java index dd48de1b..7e1c9208 100644 --- a/src/main/java/bitnagil/bitnagil_backend/routineV2/domain/RoutineV2.java +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/domain/RoutineV2.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.util.List; +import bitnagil.bitnagil_backend.global.entity.BaseTimeEntity; import bitnagil.bitnagil_backend.global.utils.BooleanListConverter; import bitnagil.bitnagil_backend.global.utils.StringListConverter; import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; @@ -30,7 +31,7 @@ @Entity @SQLDelete(sql = "UPDATE routine_v2 SET deleted_at = NOW() WHERE routine_id = ?") @Where(clause = "deleted_at IS NULL") -public class RoutineV2 { +public class RoutineV2 extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long routineId; // 일일 루틴 ID @@ -43,11 +44,11 @@ public class RoutineV2 { @NotNull @Convert(converter = StringListConverter.class) - List subRoutineNames; // 서브 루틴 이름 리스트 + private List subRoutineNames; // 서브 루틴 이름 리스트 @NotNull @Convert(converter = BooleanListConverter.class) - List subRoutineCompleteYn; // 서브 루틴 완료 여부 리스트 + private List subRoutineCompleteYn; // 서브 루틴 완료 여부 리스트 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "routine_info_id") @@ -62,4 +63,10 @@ public RoutineV2(LocalDate routineDate, Boolean routineCompleteYn, List this.subRoutineCompleteYn = subRoutineCompleteYn; this.routineInfo = routineInfo; } + + // 루틴 완료 여부 갱신 + public void updateRoutineCompleteYn(Boolean routineCompleteYn, List subRoutineCompleteYn) { + this.routineCompleteYn = routineCompleteYn; + this.subRoutineCompleteYn = subRoutineCompleteYn; + } } diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/domain/enums/UpdateApplyDate.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/domain/enums/UpdateApplyDate.java new file mode 100644 index 00000000..41d04b14 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/domain/enums/UpdateApplyDate.java @@ -0,0 +1,15 @@ +package bitnagil.bitnagil_backend.routineV2.domain.enums; + +import bitnagil.bitnagil_backend.enums.EnumType; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum UpdateApplyDate implements EnumType { + + TODAY("오늘부터 적용"), + TOMORROW("내일부터 적용"); + + private final String description; +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/repository/RoutineV2Repository.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/repository/RoutineV2Repository.java new file mode 100644 index 00000000..e5632a5a --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/repository/RoutineV2Repository.java @@ -0,0 +1,77 @@ +package bitnagil.bitnagil_backend.routineV2.repository; + +import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; +import bitnagil.bitnagil_backend.routineV2.domain.RoutineV2; +import bitnagil.bitnagil_backend.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public interface RoutineV2Repository extends JpaRepository { + + /** + * startDate와 endDate 사이에 있는 루틴을 조회하는 메서드입니다. + * fetch join을 사용하여 RoutineInfoV2와 함께 조회합니다. + */ + @Query(""" + select r from RoutineV2 r + join fetch r.routineInfo ri + where ri.user = :user + and r.routineDate between :startDate and :endDate + """) + List findByUserAndDateRange( + @Param("user") User user, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); + + /** + * 특정 사용자의 루틴을 ID로 조회하는 메서드입니다. + */ + @Query(""" + select r from RoutineV2 r + join fetch r.routineInfo ri + where ri.user = :user + and r.routineId = :routineId + """) + Optional findByUserAndRoutineId( + @Param("user") User user, + @Param("routineId") Long routineId + ); + + // 오늘 이후의 루틴 정보에 맞는 루틴 내역을 조회 + List findByRoutineInfoAndRoutineDateAfter(RoutineInfoV2 routineInfoV2, LocalDate date); + + /** + * 다수의 루틴을 물리 삭제하기 위한 JPQL + * 메모리 로드 비용 줄이기 위해 routineIds를 파라미터로 채택 + */ + @Modifying + @Query("DELETE FROM RoutineV2 r WHERE r.routineId IN :ids") + void deleteAllPhysicallyByIds(@Param("ids") List routineIds); + + /** + * 단일 루틴을 물리 삭제하기 위한 JPQL + * 메모리 로드 비용 줄이기 위해 routineIds를 파라미터로 채택 + */ + @Modifying + @Query("DELETE FROM RoutineV2 r WHERE r.routineId = :id") + void deletePhysicallyById(@Param("id") Long routineId); + + // startDate부터 endDate까지 routineDate 포함 데이터 모두 삭제 + @Modifying + @Query("DELETE FROM RoutineV2 r WHERE r.routineDate BETWEEN :startDate AND :endDate AND " + + "r.routineInfo.routineInfoId = :routineInfoId") + void deleteByRoutineDateBetweenAndRoutineInfo( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("routineInfoId") Long routineInfoId); + +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2RegisterRequest.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2RegisterRequest.java new file mode 100644 index 00000000..5a6a1106 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2RegisterRequest.java @@ -0,0 +1,55 @@ +package bitnagil.bitnagil_backend.routineV2.request; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import bitnagil.bitnagil_backend.recommendedRoutine.domain.enums.RecommendedRoutineType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "루틴 등록 요청 DTO") +public class RoutineV2RegisterRequest { + + @Schema(description = "루틴 이름입니다.", + example = "아침 준비", + required = true) + @NotNull + private String routineName; + + @Schema(description = "반복 요일에 대한 리스트입니다. (반복요일이 없으면 당일 루틴입니다.)", + example = "[\"MONDAY\", \"FRIDAY\"]", + required = true) + @NotNull + private List repeatDay; + + @Schema(description = "루틴 시작 일자입니다.", + example = "2025-08-01") + private LocalDate routineStartDate; + + @Schema(description = "루틴 시작 일자입니다.", + example = "2025-08-31") + private LocalDate routineEndDate; + + @Schema(description = "루틴 시작 시간입니다.", + example = "08:15:00", + required = true) + @NotNull + private LocalTime executionTime; + + @Schema(description = "세부 루틴 이름에 대한 리스트입니다.", + example = "[\"손 씻기\", \"세수 하기\", \"양치 하기\"]") + private List subRoutineName; + + @Schema(description = "추천 루틴 타입입니다.", example = "WAKE_UP") + private RecommendedRoutineType recommendedRoutineType; +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2UpdateCompletionInfo.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2UpdateCompletionInfo.java new file mode 100644 index 00000000..fd3da4f8 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2UpdateCompletionInfo.java @@ -0,0 +1,31 @@ +package bitnagil.bitnagil_backend.routineV2.request; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class RoutineV2UpdateCompletionInfo { + + @Schema(description = "루틴 완료 여부를 갱신할 루틴 ID 값입니다.", + example = "4", + required = true) + @NotNull + private String routineId; + + @Schema(description = "메인 루틴의 완료 여부입니다.", + example = "true", + required = true) + @NotNull + private Boolean routineCompleteYn; + + @Schema(description = "서브루틴 완료 여부 리스트입니다.", + example = "[true, false, true]", + required = true) + @NotNull + private List subRoutineCompleteYn; +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2UpdateCompletionRequest.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2UpdateCompletionRequest.java new file mode 100644 index 00000000..5ec10951 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/request/RoutineV2UpdateCompletionRequest.java @@ -0,0 +1,19 @@ +package bitnagil.bitnagil_backend.routineV2.request; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Schema(description = "루틴 완료 여부 갱신 DTO") +public class RoutineV2UpdateCompletionRequest { + + @Schema(description = "루틴 완료 여부를 갱신할 루틴 정보 리스트입니다.", + required = true) + @NotNull + List routineCompletionInfos; +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/response/RoutineV2SearchResponse.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/response/RoutineV2SearchResponse.java new file mode 100644 index 00000000..ac10b03c --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/response/RoutineV2SearchResponse.java @@ -0,0 +1,33 @@ +package bitnagil.bitnagil_backend.routineV2.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@Getter +@AllArgsConstructor +@Builder +public class RoutineV2SearchResponse { + @Schema(description = "날짜(LocalDate: 2025-08-01)와 같은 형태를 key로 가지는 루틴 목록 Map입니다. Swagger에서는 additionalProp1처럼 보일 수 있습니다.") + private Map routines; // 날짜별 루틴 목록 + + @Getter + @AllArgsConstructor + @Builder + public static class RoutineData { + @Schema(description = "날짜별 루틴 목록") + private List routineList; + + @Schema(description = "해당 날짜 모든 루틴이 완료되었는지 여부") + private boolean allCompleted; + + public void setAllCompleted(boolean allCompleted) { + this.allCompleted = allCompleted; + } + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/response/RoutineV2SearchResultDto.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/response/RoutineV2SearchResultDto.java new file mode 100644 index 00000000..7ffde4a8 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/response/RoutineV2SearchResultDto.java @@ -0,0 +1,43 @@ +package bitnagil.bitnagil_backend.routineV2.response; + +import bitnagil.bitnagil_backend.recommendedRoutine.domain.enums.RecommendedRoutineType; +import bitnagil.bitnagil_backend.routine.domain.enums.RoutineType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@Getter +@AllArgsConstructor +@Builder +public class RoutineV2SearchResultDto { + @Schema(example = "1") + private String routineId; // RoutineV2의 ID(V2에서는 String으로 파싱해서 전달) + @Schema(example = "물마시기") + private String routineName; // 루틴 이름 + @Schema(example = "[MONDAY, WEDNESDAY, FRIDAY]") + private List repeatDay; // 반복 요일 + @Schema(example = "08:30:00") + private LocalTime executionTime; // 루틴 실행 시간 + @Schema(example = "2025-08-01") + private LocalDate routineDate; // 루틴 일자 + @Schema(example = "true") + private Boolean routineCompleteYn; + @Schema(example = "[물 10초만에 마시기, 물 20초만에 마시기]") + private List subRoutineNames; + @Schema(example = "[true, false]") + private List subRoutineCompleteYn; + @Schema(example = "WAKE_UP") + private RecommendedRoutineType recommendedRoutineType; + @Schema(example = "true") + private Boolean routineDeletedYn; // 루틴 삭제 여부 + @Schema(example = "2025-08-15") + private LocalDate routineStartDate; // 루틴 시작 일자 + @Schema(example = "2025-08-31") + private LocalDate routineEndDate; // 루틴 종료 일자 +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Factory.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Factory.java new file mode 100644 index 00000000..8b16907c --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Factory.java @@ -0,0 +1,28 @@ +package bitnagil.bitnagil_backend.routineV2.service; + +import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; +import bitnagil.bitnagil_backend.routineV2.domain.RoutineV2; + +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +/** + * 루틴 관련 엔티티 생성, 초기화 책임을 담당하는 클래스입니다. + */ +@Component +public class RoutineV2Factory { + + // 신규 Routine 엔티티 생성 및 초기화 + public RoutineV2 createNewRoutine(LocalDate routineDate, Boolean routineCompleteYn, List subRoutineNames, + List subRoutineCompleteYn, RoutineInfoV2 routineInfo) { + return RoutineV2.builder() + .routineDate(routineDate) + .routineCompleteYn(routineCompleteYn) + .subRoutineNames(subRoutineNames) + .subRoutineCompleteYn(subRoutineCompleteYn) + .routineInfo(routineInfo) + .build(); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Mapper.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Mapper.java new file mode 100644 index 00000000..38ee916e --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Mapper.java @@ -0,0 +1,40 @@ +package bitnagil.bitnagil_backend.routineV2.service; + +import bitnagil.bitnagil_backend.routineV2.domain.RoutineV2; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResponse; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResultDto; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.Map; + + +/** + * 루틴 관련해서 DB에서 조회해오거나 가공된 데이터를 DTO로 변환하는 Mapper 클래스입니다. + */ +@Component +public class RoutineV2Mapper { + + public RoutineV2SearchResultDto toRoutineV2SearchResultDto(RoutineV2 routine){ + return RoutineV2SearchResultDto.builder() + .routineId(String.valueOf(routine.getRoutineId())) + .routineName(routine.getRoutineInfo().getRoutineName()) + .repeatDay(routine.getRoutineInfo().getRoutineRepeatDay()) + .executionTime(routine.getRoutineInfo().getRoutineExecutionTime()) + .routineDate(routine.getRoutineDate()) + .routineCompleteYn(routine.getRoutineCompleteYn()) + .subRoutineNames(routine.getSubRoutineNames()) + .subRoutineCompleteYn(routine.getSubRoutineCompleteYn()) + .recommendedRoutineType(routine.getRoutineInfo().getRecommendedRoutineType()) + .routineDeletedYn(routine.getRoutineInfo().getRoutineDeletedYn()) + .routineStartDate(routine.getRoutineInfo().getRoutineStartDate()) + .routineEndDate(routine.getRoutineInfo().getRoutineEndDate()) + .build(); + } + + public RoutineV2SearchResponse toRoutineV2SearchResponse(Map response) { + return RoutineV2SearchResponse.builder() + .routines(response) + .build(); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Service.java b/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Service.java new file mode 100644 index 00000000..6fe99531 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/routineV2/service/RoutineV2Service.java @@ -0,0 +1,234 @@ +package bitnagil.bitnagil_backend.routineV2.service; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.*; + +import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; +import bitnagil.bitnagil_backend.global.exception.CustomException; +import bitnagil.bitnagil_backend.routineV2.domain.enums.UpdateApplyDate; +import bitnagil.bitnagil_backend.routineInfoV2.request.RoutineInfoV2UpdateRequest; +import bitnagil.bitnagil_backend.routineV2.request.RoutineV2UpdateCompletionInfo; +import bitnagil.bitnagil_backend.routineV2.request.RoutineV2UpdateCompletionRequest; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResponse; +import bitnagil.bitnagil_backend.routineV2.response.RoutineV2SearchResultDto; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import bitnagil.bitnagil_backend.routineInfoV2.domain.RoutineInfoV2; +import bitnagil.bitnagil_backend.routineInfoV2.repository.RoutineInfoV2Repository; +import bitnagil.bitnagil_backend.routineInfoV2.service.RoutineInfoV2Factory; +import bitnagil.bitnagil_backend.routineV2.domain.RoutineV2; +import bitnagil.bitnagil_backend.routineV2.repository.RoutineV2Repository; +import bitnagil.bitnagil_backend.routineV2.request.RoutineV2RegisterRequest; +import bitnagil.bitnagil_backend.user.domain.User; +import lombok.RequiredArgsConstructor; + +/** + * [v2] 루틴 관련된 서비스 로직을 담은 클래스입니다. + */ +@Service +@RequiredArgsConstructor +public class RoutineV2Service { + + private final RoutineInfoV2Repository routineInfoV2Repository; + private final RoutineInfoV2Factory routineInfoV2Factory; + private final RoutineV2Factory routineV2Factory; + private final RoutineV2Repository routineV2Repository; + private final RoutineV2Mapper routineV2Mapper; + + /** + * 회원이 보유한 특정 기간(start_date, end_date)의 루틴을 조회하는 메서드입니다. + */ + @Transactional(readOnly = true) + public RoutineV2SearchResponse getRoutines(User user, LocalDate startDate, LocalDate endDate) { + return queryRoutines(user, startDate, endDate); + } + + /** + * 회원이 보유한 루틴 단건 조회 + */ + @Transactional(readOnly = true) + public RoutineV2SearchResultDto getRoutine(User user, Long routineId) { + RoutineV2 routineV2 = routineV2Repository.findByUserAndRoutineId(user, routineId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTINE)); + + return routineV2Mapper.toRoutineV2SearchResultDto(routineV2); + } + + /** + * 루틴 정보를 등록하면서 루틴 시작, 종료일자를 기반으로 루틴 내역을 생성 + */ + @Transactional + public void registerRoutineV2(User user, RoutineV2RegisterRequest request) { + + LocalDate today = LocalDate.now(); + + // repeatDay가 비어 있으면 빈 리스트, 아니면 요청값 사용 + List repeatDays = request.getRepeatDay().isEmpty() ? List.of() : request.getRepeatDay(); + + // 루틴 정보 등록 + RoutineInfoV2 routineInfo = routineInfoV2Factory.createNewRoutineInfo( + request.getRoutineName(), + repeatDays, + request.getExecutionTime(), + request.getRoutineStartDate(), + request.getRoutineEndDate(), + request.getRecommendedRoutineType(), + user); + + routineInfoV2Repository.save(routineInfo); + + // 루틴을 생성할 날짜 목록 생성 + createRoutinesMatchedRepeatDayWithinPeriod(request.getRepeatDay().isEmpty() + ? List.of(today) // 당일 루틴 + : generateRoutineDatesWithinPeriod( + request.getRoutineStartDate(), + request.getRoutineEndDate(), + request.getRepeatDay()), request.getSubRoutineName(), routineInfo); + } + + // 루틴 오늘만 삭제 메서드 + @Transactional + public void deleteRoutineByDay(User user, Long routineId) { + RoutineV2 routineV2 = routineV2Repository.findByUserAndRoutineId(user, routineId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTINE)); + + routineV2Repository.deletePhysicallyById(routineV2.getRoutineId()); // 물리 삭제 + } + + // 루틴 정보 수정 메서드 + @Transactional + public void updateRoutineInfo(User user, RoutineInfoV2UpdateRequest request) { + + LocalDate today = LocalDate.now(); + LocalDate tomorrow = LocalDate.now().plusDays(1); + Long routineId = Long.valueOf(request.getRoutineId()); + + RoutineV2 routineV2 = routineV2Repository.findByUserAndRoutineId(user, routineId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTINE)); + + RoutineInfoV2 routineInfoV2 = routineInfoV2Repository.findById(routineV2.getRoutineInfo().getRoutineInfoId()) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTINE_INFO)); + + // 요첨받은 루틴 정보가 기존 루틴 정보와 동일할 경우 + if (!isChangedRoutineInfo(request, routineInfoV2)) return; + + // 변경사항을 적용할 변경날짜를 설정 + LocalDate changedDate = request.getUpdateApplyDate().equals(UpdateApplyDate.TODAY) ? today : tomorrow; + + // 기존 루틴에서 수정날짜 이후 데이터는 물리 삭제 + routineV2Repository.deleteByRoutineDateBetweenAndRoutineInfo( + changedDate, routineInfoV2.getRoutineEndDate(), routineInfoV2.getRoutineInfoId()); + + // 기존 루틴 정보의 종료일자를 업데이트 + routineInfoV2.updateRoutineEndDate(changedDate.minusDays(1)); + + // 변경날짜부터의 새로운 루틴 등록 request 변환 + RoutineV2RegisterRequest routineV2RegisterRequest = RoutineV2RegisterRequest.builder() + .routineName(request.getRoutineName()) + .repeatDay(request.getRepeatDay()) + .routineStartDate(changedDate) + .routineEndDate(request.getRoutineEndDate()) + .executionTime(request.getExecutionTime()) + .subRoutineName(request.getSubRoutineName()) + .build(); + + // 변경날짜부터의 새로운 루틴 등록 + registerRoutineV2(user, routineV2RegisterRequest); + } + + // 루틴 정보에서 변경된 부분이 있는지 검증 + private boolean isChangedRoutineInfo(RoutineInfoV2UpdateRequest request, RoutineInfoV2 routineInfoV2) { + return !routineInfoV2.getRoutineName().equals(request.getRoutineName()) || + !routineInfoV2.getRoutineRepeatDay().equals(request.getRepeatDay()) || + !routineInfoV2.getRoutineExecutionTime().equals(request.getExecutionTime()) || + !routineInfoV2.getRoutineStartDate().equals(request.getRoutineStartDate()) || + !routineInfoV2.getRoutineEndDate().equals(request.getRoutineEndDate()); + } + + private void createRoutinesMatchedRepeatDayWithinPeriod( + List targetDates, List request, RoutineInfoV2 routineInfoV2) { + + // 서브 루틴 완료 여부 리스트 생성 + List subRoutineCompleteYn = request.stream() + .map(completeYn -> false) + .toList(); + + // 위 날짜 목록을 바탕으로 루틴 생성 + List routinesToRegister = targetDates.stream() + .map(routineDate -> routineV2Factory.createNewRoutine( + routineDate, + false, + request, + subRoutineCompleteYn, + routineInfoV2 + )) + .toList(); + + routineV2Repository.saveAll(routinesToRegister); + } + + // 루틴 완료 여부를 업데이트 하는 메서드 + @Transactional + public void updateRoutineCompletionStatus(User user, RoutineV2UpdateCompletionRequest request) { + for (RoutineV2UpdateCompletionInfo info : request.getRoutineCompletionInfos()) { + Long routineId = Long.valueOf(info.getRoutineId()); + RoutineV2 routineV2 = routineV2Repository.findByUserAndRoutineId(user, routineId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTINE)); + + // 루틴, 서브루틴 완료 여부 갱신 + routineV2.updateRoutineCompleteYn(info.getRoutineCompleteYn(), info.getSubRoutineCompleteYn()); + } + } + + /** + * 날짜 범위에서 주어진 요일(repeatDays)에 해당하는 날짜만 반환 + */ + private List generateRoutineDatesWithinPeriod( + LocalDate startDate, LocalDate endDate, List repeatDays) { + + List routineDatesToRegister = new ArrayList<>(); + for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) { + if (repeatDays.contains(date.getDayOfWeek())) { + routineDatesToRegister.add(date); + } + } + return routineDatesToRegister; + } + + // 특정 기간 보유 루틴 조회 + private RoutineV2SearchResponse queryRoutines(User user, LocalDate startDate, LocalDate endDate) { + Map response = new HashMap<>(); + + List routineList = routineV2Repository.findByUserAndDateRange(user, startDate, endDate); + + for (RoutineV2 routineV2 : routineList) { + LocalDate date = routineV2.getRoutineDate(); + RoutineV2SearchResultDto routineSearchResultDto = routineV2Mapper.toRoutineV2SearchResultDto(routineV2); + + // 날짜별 RoutineData 생성 혹은 가져오기 + response.computeIfAbsent(date, key -> RoutineV2SearchResponse.RoutineData.builder() + .routineList(new ArrayList<>()) + .allCompleted(true) // 초기값 true + .build()); + + RoutineV2SearchResponse.RoutineData routineData = response.get(date); + + // 리스트에 추가 + routineData.getRoutineList().add(routineSearchResultDto); + + // 하나라도 완료 안 된 루틴이 있으면 false로 변경 + if (!routineSearchResultDto.getRoutineCompleteYn()) { + routineData.setAllCompleted(false); + } + } + + // 정렬 처리 + response.values().forEach(data -> + data.getRoutineList().sort(Comparator.comparing(RoutineV2SearchResultDto::getExecutionTime)) + ); + + return routineV2Mapper.toRoutineV2SearchResponse(response); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/user/controller/UserController.java b/src/main/java/bitnagil/bitnagil_backend/user/controller/UserController.java index d87bd571..5ef1bfbd 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/controller/UserController.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/controller/UserController.java @@ -9,6 +9,7 @@ import bitnagil.bitnagil_backend.user.controller.spec.UserSpec; import bitnagil.bitnagil_backend.user.domain.User; import bitnagil.bitnagil_backend.user.response.UserInfoResponse; +import bitnagil.bitnagil_backend.user.response.UserOnboardingResponse; import bitnagil.bitnagil_backend.user.service.UserService; import lombok.RequiredArgsConstructor; @@ -25,4 +26,9 @@ public CustomResponseDto getUserInfo(@CurrentUser User user) { return CustomResponseDto.from(userInfoResponse); } + + @GetMapping("/onboarding") + public CustomResponseDto getUserOnboarding(@CurrentUser User user) { + return CustomResponseDto.from(userService.getUserOnboarding(user)); + } } diff --git a/src/main/java/bitnagil/bitnagil_backend/user/controller/spec/UserSpec.java b/src/main/java/bitnagil/bitnagil_backend/user/controller/spec/UserSpec.java index 5433c685..23985bf6 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/controller/spec/UserSpec.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/controller/spec/UserSpec.java @@ -4,6 +4,7 @@ import bitnagil.bitnagil_backend.global.swagger.ApiTags; import bitnagil.bitnagil_backend.user.domain.User; import bitnagil.bitnagil_backend.user.response.UserInfoResponse; +import bitnagil.bitnagil_backend.user.response.UserOnboardingResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,4 +16,7 @@ public interface UserSpec { @Operation(summary = "유저 정보를 조회합니다.") CustomResponseDto getUserInfo(User user); + + @Operation(summary = "유저의 온보딩 정보를 조회합니다.") + CustomResponseDto getUserOnboarding(User user); } diff --git a/src/main/java/bitnagil/bitnagil_backend/user/domain/User.java b/src/main/java/bitnagil/bitnagil_backend/user/domain/User.java index ce754192..f8f2052b 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/domain/User.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/domain/User.java @@ -1,7 +1,7 @@ package bitnagil.bitnagil_backend.user.domain; -import bitnagil.bitnagil_backend.enums.Role; -import bitnagil.bitnagil_backend.enums.SocialType; +import bitnagil.bitnagil_backend.user.domain.enums.Role; +import bitnagil.bitnagil_backend.user.domain.enums.SocialType; import bitnagil.bitnagil_backend.global.entity.BaseTimeEntity; import bitnagil.bitnagil_backend.onboarding.domain.Onboarding; import jakarta.persistence.*; @@ -68,11 +68,12 @@ public void updateAgreements(Boolean agreedToTermsOfService, Boolean agreedToPri this.agreedToTermsOfService = agreedToTermsOfService; this.agreedToPrivacyPolicy = agreedToPrivacyPolicy; this.isOverFourteen = isOverFourteen; - this.role = Role.USER; // 약관 동의 후 권한을 USER로 변경 + this.role = Role.ONBOARDING; // 약관 동의 후 권한을 임시 USER인 ONBOARDING으로 변경 } public void updateOnboarding(Onboarding onboarding) { this.onboarding = onboarding; + this.role = Role.USER; // 온보딩 완료 후 권한을 USER로 변경 } // todo: 운영 반영 후 이슈가 없으면 제거 diff --git a/src/main/java/bitnagil/bitnagil_backend/enums/Role.java b/src/main/java/bitnagil/bitnagil_backend/user/domain/enums/Role.java similarity index 62% rename from src/main/java/bitnagil/bitnagil_backend/enums/Role.java rename to src/main/java/bitnagil/bitnagil_backend/user/domain/enums/Role.java index 38d52bee..5843df4d 100644 --- a/src/main/java/bitnagil/bitnagil_backend/enums/Role.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/domain/enums/Role.java @@ -1,5 +1,6 @@ -package bitnagil.bitnagil_backend.enums; +package bitnagil.bitnagil_backend.user.domain.enums; +import bitnagil.bitnagil_backend.enums.EnumType; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -7,7 +8,8 @@ public enum Role implements EnumType { GUEST("ROLE_GUEST"), USER("ROLE_USER"), - WITHDRAWN("ROLE_WITHDRAWN"); + WITHDRAWN("ROLE_WITHDRAWN"), + ONBOARDING("ROLE_ONBOARDING"),; private final String description; diff --git a/src/main/java/bitnagil/bitnagil_backend/user/domain/enums/SocialType.java b/src/main/java/bitnagil/bitnagil_backend/user/domain/enums/SocialType.java new file mode 100644 index 00000000..f96082db --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/user/domain/enums/SocialType.java @@ -0,0 +1,6 @@ +package bitnagil.bitnagil_backend.user.domain.enums; + +public enum SocialType { + + KAKAO, APPLE +} diff --git a/src/main/java/bitnagil/bitnagil_backend/user/repository/UserRepository.java b/src/main/java/bitnagil/bitnagil_backend/user/repository/UserRepository.java index c77841a6..c3491312 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/repository/UserRepository.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/repository/UserRepository.java @@ -1,14 +1,11 @@ package bitnagil.bitnagil_backend.user.repository; -import java.time.LocalDateTime; import java.util.Optional; -import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import bitnagil.bitnagil_backend.enums.SocialType; -import bitnagil.bitnagil_backend.global.entity.HistoryPk; +import bitnagil.bitnagil_backend.user.domain.enums.SocialType; import bitnagil.bitnagil_backend.user.domain.User; @Repository diff --git a/src/main/java/bitnagil/bitnagil_backend/user/request/UserLoginRequest.java b/src/main/java/bitnagil/bitnagil_backend/user/request/UserLoginRequest.java index 8a7a7617..da8e1c42 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/request/UserLoginRequest.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/request/UserLoginRequest.java @@ -1,6 +1,6 @@ package bitnagil.bitnagil_backend.user.request; -import bitnagil.bitnagil_backend.enums.SocialType; +import bitnagil.bitnagil_backend.user.domain.enums.SocialType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; diff --git a/src/main/java/bitnagil/bitnagil_backend/user/response/UserOnboardingResponse.java b/src/main/java/bitnagil/bitnagil_backend/user/response/UserOnboardingResponse.java new file mode 100644 index 00000000..7b5089f3 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/user/response/UserOnboardingResponse.java @@ -0,0 +1,31 @@ +package bitnagil.bitnagil_backend.user.response; + +import java.time.LocalTime; + +import bitnagil.bitnagil_backend.onboarding.domain.enums.EmotionType; +import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class UserOnboardingResponse { + + @Schema(description = "잘 보내고 싶은 시간대", + example = "08:00:00", + required = true) + private LocalTime timeSlot; + + @Schema(description = "요즘 필요한 회복 타입", + example = "STABILITY", + required = true) + private EmotionType emotionType; + + @Schema(description = "일주일동안 목표 외출 횟수", + example = "ONE_PER_WEEK", + required = true) + private TargetOutingFrequency targetOutingFrequency; +} \ No newline at end of file diff --git a/src/main/java/bitnagil/bitnagil_backend/user/response/UserTokenResponse.java b/src/main/java/bitnagil/bitnagil_backend/user/response/UserTokenResponse.java index 7b7de160..d36981fe 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/response/UserTokenResponse.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/response/UserTokenResponse.java @@ -1,7 +1,7 @@ package bitnagil.bitnagil_backend.user.response; import bitnagil.bitnagil_backend.auth.jwt.Token; -import bitnagil.bitnagil_backend.enums.Role; +import bitnagil.bitnagil_backend.user.domain.enums.Role; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/bitnagil/bitnagil_backend/user/service/UserAuthService.java b/src/main/java/bitnagil/bitnagil_backend/user/service/UserAuthService.java index 1c16c4ba..89f3ea79 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/service/UserAuthService.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/service/UserAuthService.java @@ -17,10 +17,10 @@ import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; import bitnagil.bitnagil_backend.global.exception.CustomException; import bitnagil.bitnagil_backend.user.repository.UserRepository; -import bitnagil.bitnagil_backend.enums.SocialType; +import bitnagil.bitnagil_backend.user.domain.enums.SocialType; import bitnagil.bitnagil_backend.user.response.UserTokenResponse; import bitnagil.bitnagil_backend.user.domain.User; -import bitnagil.bitnagil_backend.enums.Role; +import bitnagil.bitnagil_backend.user.domain.enums.Role; import bitnagil.bitnagil_backend.user.domain.UserAuthInfo; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/bitnagil/bitnagil_backend/user/service/UserMapper.java b/src/main/java/bitnagil/bitnagil_backend/user/service/UserMapper.java index 21bf1b3d..d5dd803f 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/service/UserMapper.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/service/UserMapper.java @@ -1,9 +1,14 @@ package bitnagil.bitnagil_backend.user.service; +import java.time.LocalTime; + import org.springframework.stereotype.Component; +import bitnagil.bitnagil_backend.onboarding.domain.enums.EmotionType; +import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency; import bitnagil.bitnagil_backend.user.domain.User; import bitnagil.bitnagil_backend.user.response.UserInfoResponse; +import bitnagil.bitnagil_backend.user.response.UserOnboardingResponse; /* * 유저 관련 DTO로 변환하는 클래스입니다. @@ -16,4 +21,14 @@ public UserInfoResponse toUserInfoResponse(User user) { .nickname(user.getNickname()) .build(); } + + public UserOnboardingResponse toUserOnboardingResponse( + LocalTime timeSlot, EmotionType emotionType, TargetOutingFrequency targetOutingFrequency) { + + return UserOnboardingResponse.builder() + .timeSlot(timeSlot) + .emotionType(emotionType) + .targetOutingFrequency(targetOutingFrequency) + .build(); + } } diff --git a/src/main/java/bitnagil/bitnagil_backend/user/service/UserService.java b/src/main/java/bitnagil/bitnagil_backend/user/service/UserService.java index 1e2098bf..6f704d85 100644 --- a/src/main/java/bitnagil/bitnagil_backend/user/service/UserService.java +++ b/src/main/java/bitnagil/bitnagil_backend/user/service/UserService.java @@ -3,8 +3,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import bitnagil.bitnagil_backend.onboarding.domain.Onboarding; import bitnagil.bitnagil_backend.user.domain.User; import bitnagil.bitnagil_backend.user.response.UserInfoResponse; +import bitnagil.bitnagil_backend.user.response.UserOnboardingResponse; import lombok.RequiredArgsConstructor; /** @@ -19,4 +21,14 @@ public class UserService { public UserInfoResponse getUserInfo(User user) { return userMapper.toUserInfoResponse(user); } + + @Transactional(readOnly = true) + public UserOnboardingResponse getUserOnboarding(User user) { + Onboarding onboarding = user.getOnboarding(); + + return userMapper.toUserOnboardingResponse( + onboarding.getTimeSlot(), + onboarding.getEmotionType(), + onboarding.getTargetOutingFrequency()); + } } diff --git a/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/controller/UserOnboardingInfoController.java b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/controller/UserOnboardingInfoController.java new file mode 100644 index 00000000..e96e0968 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/controller/UserOnboardingInfoController.java @@ -0,0 +1,25 @@ +package bitnagil.bitnagil_backend.userOnboardingInfo.controller; + +import bitnagil.bitnagil_backend.global.annotation.CurrentUser; +import bitnagil.bitnagil_backend.global.response.CustomResponseDto; +import bitnagil.bitnagil_backend.user.domain.User; +import bitnagil.bitnagil_backend.userOnboardingInfo.controller.spec.UserOnboardingInfoSpec; +import bitnagil.bitnagil_backend.userOnboardingInfo.response.UserOnboardingInfoSearchResponse; +import bitnagil.bitnagil_backend.userOnboardingInfo.service.UserOnboardingInfoService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/api") +public class UserOnboardingInfoController implements UserOnboardingInfoSpec { + + private final UserOnboardingInfoService userOnboardingInfoService; + + @GetMapping("/v2/onboardings") + public CustomResponseDto getUserOnboardingInfo(@CurrentUser User user) { + return CustomResponseDto.from(userOnboardingInfoService.getUserOnboardingInfo(user)); + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/controller/spec/UserOnboardingInfoSpec.java b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/controller/spec/UserOnboardingInfoSpec.java new file mode 100644 index 00000000..03239ff3 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/controller/spec/UserOnboardingInfoSpec.java @@ -0,0 +1,21 @@ +package bitnagil.bitnagil_backend.userOnboardingInfo.controller.spec; + +import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; +import bitnagil.bitnagil_backend.global.response.CustomResponseDto; +import bitnagil.bitnagil_backend.global.swagger.ApiErrorCodeExamples; +import bitnagil.bitnagil_backend.global.swagger.ApiTags; +import bitnagil.bitnagil_backend.user.domain.User; +import bitnagil.bitnagil_backend.userOnboardingInfo.response.UserOnboardingInfoSearchResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = ApiTags.ONBOARDING) +public interface UserOnboardingInfoSpec { + + @Operation(summary = "유저의 온보딩 정보를 조회합니다.") + @ApiErrorCodeExamples({ + ErrorCode.NOT_FOUND_USER_ONBOARDING_INFO + }) + public CustomResponseDto getUserOnboardingInfo(User user); + +} diff --git a/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/domain/UserOnboardingInfo.java b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/domain/UserOnboardingInfo.java new file mode 100644 index 00000000..e565e600 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/domain/UserOnboardingInfo.java @@ -0,0 +1,61 @@ +package bitnagil.bitnagil_backend.userOnboardingInfo.domain; + +import bitnagil.bitnagil_backend.global.entity.BaseTimeEntity; +import bitnagil.bitnagil_backend.global.utils.StringListConverter; +import bitnagil.bitnagil_backend.onboarding.domain.Onboarding; +import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency; +import bitnagil.bitnagil_backend.user.domain.User; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import java.time.LocalTime; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@SQLDelete(sql = "UPDATE user_onboarding_info SET role = 'WITHDRAWN', deleted_at = NOW() WHERE user_id = ?") +@Where(clause = "deleted_at IS NULL") +public class UserOnboardingInfo extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long userOnboardingInfoId; + + @NotNull + private LocalTime timeSlot; // 사용자가 선택한 시간대 + + @Convert(converter = StringListConverter.class) + @NotNull + private List emotionTypes; // 사용자가 선택한 감정 유형 목록 + + @Enumerated(EnumType.STRING) + @Column(columnDefinition = "varchar(40)") + @NotNull + private TargetOutingFrequency targetOutingFrequency; // 사용자가 선택한 목표 외출 빈도 + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Builder + public UserOnboardingInfo(LocalTime timeSlot, List emotionTypes, TargetOutingFrequency targetOutingFrequency, + Onboarding onboarding, User user) { + this.timeSlot = timeSlot; + this.emotionTypes = emotionTypes; + this.targetOutingFrequency = targetOutingFrequency; + this.user = user; + } + + public void updateUserOnboardingInfo(LocalTime timeSlot, List emotionType, TargetOutingFrequency targetOutingFrequency) { + this.timeSlot = timeSlot; + this.emotionTypes = emotionType; + this.targetOutingFrequency = targetOutingFrequency; + } +} diff --git a/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/repository/UserOnboardingInfoRepository.java b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/repository/UserOnboardingInfoRepository.java new file mode 100644 index 00000000..01aa9ec6 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/repository/UserOnboardingInfoRepository.java @@ -0,0 +1,9 @@ +package bitnagil.bitnagil_backend.userOnboardingInfo.repository; + +import bitnagil.bitnagil_backend.user.domain.User; +import bitnagil.bitnagil_backend.userOnboardingInfo.domain.UserOnboardingInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserOnboardingInfoRepository extends JpaRepository { + UserOnboardingInfo findByUser(User user); +} diff --git a/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/response/UserOnboardingInfoSearchResponse.java b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/response/UserOnboardingInfoSearchResponse.java new file mode 100644 index 00000000..68891427 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/response/UserOnboardingInfoSearchResponse.java @@ -0,0 +1,26 @@ +package bitnagil.bitnagil_backend.userOnboardingInfo.response; + +import bitnagil.bitnagil_backend.onboarding.domain.enums.TargetOutingFrequency; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalTime; +import java.util.List; + +@Getter +@AllArgsConstructor +@Builder +public class UserOnboardingInfoSearchResponse { + + @Schema(example = "08:00:00") + private LocalTime timeSlot; + + @Schema(example = "[\"GROWTH\", \"VITALITY\"]") + private List emotionTypes; + + @Schema(example = "ONE_PER_WEEK") + private TargetOutingFrequency targetOutingFrequency; + +} diff --git a/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/service/UserOnboardingInfoService.java b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/service/UserOnboardingInfoService.java new file mode 100644 index 00000000..5b1794d1 --- /dev/null +++ b/src/main/java/bitnagil/bitnagil_backend/userOnboardingInfo/service/UserOnboardingInfoService.java @@ -0,0 +1,33 @@ +package bitnagil.bitnagil_backend.userOnboardingInfo.service; + +import bitnagil.bitnagil_backend.global.errorcode.ErrorCode; +import bitnagil.bitnagil_backend.global.exception.CustomException; +import bitnagil.bitnagil_backend.user.domain.User; +import bitnagil.bitnagil_backend.userOnboardingInfo.domain.UserOnboardingInfo; +import bitnagil.bitnagil_backend.userOnboardingInfo.repository.UserOnboardingInfoRepository; +import bitnagil.bitnagil_backend.userOnboardingInfo.response.UserOnboardingInfoSearchResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserOnboardingInfoService { + + private final UserOnboardingInfoRepository userOnboardingInfoRepository; + + @Transactional + public UserOnboardingInfoSearchResponse getUserOnboardingInfo (User user) { + UserOnboardingInfo userOnboardingInfo = userOnboardingInfoRepository.findByUser(user); + + if (userOnboardingInfo == null) { + throw new CustomException(ErrorCode.NOT_FOUND_USER_ONBOARDING_INFO); + } + + return UserOnboardingInfoSearchResponse.builder() + .timeSlot(userOnboardingInfo.getTimeSlot()) + .emotionTypes(userOnboardingInfo.getEmotionTypes()) + .targetOutingFrequency(userOnboardingInfo.getTargetOutingFrequency()) + .build(); + } +} diff --git a/src/main/resources/db/migration/V5__add_base_entity_column_to_v2_entity.sql b/src/main/resources/db/migration/V5__add_base_entity_column_to_v2_entity.sql new file mode 100644 index 00000000..6d647761 --- /dev/null +++ b/src/main/resources/db/migration/V5__add_base_entity_column_to_v2_entity.sql @@ -0,0 +1,9 @@ +ALTER TABLE routine_infov2 + ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + ADD COLUMN updated_at TIMESTAMP NULL DEFAULT NULL, + ADD COLUMN deleted_at DATETIME(6) NULL DEFAULT NULL; + +ALTER TABLE routinev2 + ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + ADD COLUMN updated_at TIMESTAMP NULL DEFAULT NULL, + ADD COLUMN deleted_at DATETIME(6) NULL DEFAULT NULL; \ No newline at end of file diff --git a/src/main/resources/db/migration/V6__add_routine_deleted_yn_to_routine_info_v2.sql b/src/main/resources/db/migration/V6__add_routine_deleted_yn_to_routine_info_v2.sql new file mode 100644 index 00000000..a4ba4bc7 --- /dev/null +++ b/src/main/resources/db/migration/V6__add_routine_deleted_yn_to_routine_info_v2.sql @@ -0,0 +1,2 @@ +ALTER TABLE routine_infov2 + ADD COLUMN routine_deleted_yn BIT(1) NOT NULL DEFAULT b'0' \ No newline at end of file diff --git a/src/main/resources/db/migration/V7__add_recommended_routine_type_to_routine_infov2.sql b/src/main/resources/db/migration/V7__add_recommended_routine_type_to_routine_infov2.sql new file mode 100644 index 00000000..b07363db --- /dev/null +++ b/src/main/resources/db/migration/V7__add_recommended_routine_type_to_routine_infov2.sql @@ -0,0 +1,2 @@ +ALTER TABLE routine_infov2 + ADD COLUMN recommended_routine_type VARCHAR(40) NULL; \ No newline at end of file diff --git a/src/main/resources/db/migration/V8__create_android_app_version.sql b/src/main/resources/db/migration/V8__create_android_app_version.sql new file mode 100644 index 00000000..d6aff449 --- /dev/null +++ b/src/main/resources/db/migration/V8__create_android_app_version.sql @@ -0,0 +1,12 @@ +-- AndroidAppVersion 테이블 추가 + +CREATE TABLE android_app_version ( + version_id BIGINT NOT NULL AUTO_INCREMENT, + major INT NOT NULL, + minor INT NOT NULL, + patch INT NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NULL, + deleted_at DATETIME(6), + PRIMARY KEY (version_id) +); \ No newline at end of file diff --git a/src/main/resources/db/migration/V9__create_user_onboarding_info.sql b/src/main/resources/db/migration/V9__create_user_onboarding_info.sql new file mode 100644 index 00000000..2db4b465 --- /dev/null +++ b/src/main/resources/db/migration/V9__create_user_onboarding_info.sql @@ -0,0 +1,15 @@ +CREATE TABLE user_onboarding_info ( + user_onboarding_info_id BIGINT NOT NULL AUTO_INCREMENT, + time_slot TIME(6) NOT NULL, + emotion_types VARCHAR(200) NOT NULL, + target_outing_frequency VARCHAR(40) NOT NULL, + user_id BIGINT, + created_at TIMESTAMP NOT NULL, + deleted_at DATETIME(6), + updated_at TIMESTAMP NULL, + PRIMARY KEY (user_onboarding_info_id), + UNIQUE KEY (user_id), + CONSTRAINT fk_user_onboarding_info_user_id + FOREIGN KEY (user_id) + REFERENCES user (user_id) +); \ No newline at end of file diff --git a/src/test/java/bitnagil/bitnagil_backend/user/service/UserAuthServiceTest.java b/src/test/java/bitnagil/bitnagil_backend/user/service/UserAuthServiceTest.java index 93ffd206..15259710 100644 --- a/src/test/java/bitnagil/bitnagil_backend/user/service/UserAuthServiceTest.java +++ b/src/test/java/bitnagil/bitnagil_backend/user/service/UserAuthServiceTest.java @@ -4,12 +4,8 @@ import bitnagil.bitnagil_backend.auth.jwt.AuthRedisService; import bitnagil.bitnagil_backend.auth.jwt.JwtUtil; import bitnagil.bitnagil_backend.auth.kakao.service.KakaoUserInfoService; -import bitnagil.bitnagil_backend.enums.Role; -import bitnagil.bitnagil_backend.enums.SocialType; -import bitnagil.bitnagil_backend.global.entity.HistoryPk; -import bitnagil.bitnagil_backend.user.domain.User; import bitnagil.bitnagil_backend.user.repository.UserRepository; -import bitnagil.bitnagil_backend.user.request.UserAgreementsRequest; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,13 +13,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Optional; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.when; - @DisplayName("회원 인증 테스트") @ExtendWith(MockitoExtension.class) class UserAuthServiceTest {