diff --git a/src/main/java/com/soptie/server/memberroutine/adapter/MemberRoutineFinder.java b/src/main/java/com/soptie/server/memberroutine/adapter/MemberRoutineFinder.java index 33ff6d4e..eeddbcbc 100644 --- a/src/main/java/com/soptie/server/memberroutine/adapter/MemberRoutineFinder.java +++ b/src/main/java/com/soptie/server/memberroutine/adapter/MemberRoutineFinder.java @@ -13,6 +13,7 @@ import com.soptie.server.memberroutine.repository.dto.MemberChallengeResponse; import com.soptie.server.memberroutine.repository.dto.MemberRoutineResponse; import com.soptie.server.routine.entity.Routine; +import com.soptie.server.routine.entity.RoutineType; import com.soptie.server.routine.exception.RoutineException; import lombok.RequiredArgsConstructor; @@ -47,4 +48,8 @@ public boolean existMemberChallenge(Member member) { public Optional findChallengeByMember(Member member) { return memberRoutineRepository.findChallengeByMember(member); } + + public List findAllByMemberAndType(Member member, RoutineType type) { + return memberRoutineRepository.findByMemberAndType(member, type); + } } diff --git a/src/main/java/com/soptie/server/memberroutine/repository/MemberRoutineRepository.java b/src/main/java/com/soptie/server/memberroutine/repository/MemberRoutineRepository.java index acfd62de..67c526c4 100644 --- a/src/main/java/com/soptie/server/memberroutine/repository/MemberRoutineRepository.java +++ b/src/main/java/com/soptie/server/memberroutine/repository/MemberRoutineRepository.java @@ -20,4 +20,6 @@ public interface MemberRoutineRepository extends JpaRepository findByMemberAndType(Member member, RoutineType type); } diff --git a/src/main/java/com/soptie/server/routine/adapter/RoutineFinder.java b/src/main/java/com/soptie/server/routine/adapter/RoutineFinder.java index e0beec6c..00b1e747 100644 --- a/src/main/java/com/soptie/server/routine/adapter/RoutineFinder.java +++ b/src/main/java/com/soptie/server/routine/adapter/RoutineFinder.java @@ -37,7 +37,7 @@ public List findChallengeRoutinesByTheme(Long themeId) { return routineRepository.findByTypeAndThemeId(RoutineType.CHALLENGE, themeId); } - public List findAllByTypeAndThemeId(RoutineType type, Long themeId) { - return routineRepository.findByTypeAndThemeId(type, themeId).stream().map(RoutineVO::from).toList(); + public List findAllByTypeAndThemeId(RoutineType type, Long themeId) { + return routineRepository.findByTypeAndThemeId(type, themeId); } } diff --git a/src/main/java/com/soptie/server/routine/controller/v1/dto/response/HappinessRoutineListAcquireResponse.java b/src/main/java/com/soptie/server/routine/controller/v1/dto/response/HappinessRoutineListAcquireResponse.java index 8764dfc3..e625e746 100644 --- a/src/main/java/com/soptie/server/routine/controller/v1/dto/response/HappinessRoutineListAcquireResponse.java +++ b/src/main/java/com/soptie/server/routine/controller/v1/dto/response/HappinessRoutineListAcquireResponse.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Objects; -import com.soptie.server.routine.service.vo.RoutineVO; +import com.soptie.server.routine.entity.Routine; import lombok.AccessLevel; import lombok.Builder; @@ -14,7 +14,7 @@ public record HappinessRoutineListAcquireResponse( List routines ) { - public static HappinessRoutineListAcquireResponse from(List routines) { + public static HappinessRoutineListAcquireResponse from(List routines) { return HappinessRoutineListAcquireResponse.builder() .routines(routines.stream().map(HappinessRoutineResponse::from).toList()) .build(); @@ -29,13 +29,13 @@ private record HappinessRoutineResponse( @NonNull String iconImageUrl ) { - private static HappinessRoutineResponse from(RoutineVO routine) { + private static HappinessRoutineResponse from(Routine routine) { return HappinessRoutineResponse.builder() - .routineId(routine.routineId()) - .name(Objects.nonNull(routine.theme()) ? routine.theme().name() : "알 수 없는 테마") - .nameColor(Objects.nonNull(routine.theme()) ? routine.theme().color() : "#FFFFFF") - .title(routine.content()) - .iconImageUrl(Objects.nonNull(routine.theme()) ? routine.theme().imageLinks().iconImageUrl() : "") + .routineId(routine.getId()) + .name(Objects.nonNull(routine.getTheme()) ? routine.getTheme().getName() : "알 수 없는 테마") + .nameColor(Objects.nonNull(routine.getTheme()) ? routine.getTheme().getColor() : "#FFFFFF") + .title(routine.getContent()) + .iconImageUrl(routine.getTheme() != null ? routine.getTheme().getImageLinks().getIconImageUrl() : "") .build(); } } diff --git a/src/main/java/com/soptie/server/routine/controller/v2/DailyRoutineControllerV2.java b/src/main/java/com/soptie/server/routine/controller/v2/DailyRoutineControllerV2.java index 53a6ab32..3c271a55 100644 --- a/src/main/java/com/soptie/server/routine/controller/v2/DailyRoutineControllerV2.java +++ b/src/main/java/com/soptie/server/routine/controller/v2/DailyRoutineControllerV2.java @@ -1,16 +1,21 @@ package com.soptie.server.routine.controller.v2; +import java.security.Principal; import java.util.LinkedHashSet; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.soptie.server.common.dto.SuccessResponse; import com.soptie.server.routine.controller.v2.docs.DailyRoutineControllerV2Docs; import com.soptie.server.routine.controller.v2.dto.response.DailyRoutineListAcquireResponseV2; +import com.soptie.server.routine.controller.v2.dto.response.DailyRoutinesAcquireResponseV2; import com.soptie.server.routine.message.RoutineSuccessMessage; import com.soptie.server.routine.service.RoutineService; @@ -34,4 +39,16 @@ public ResponseEntity> acquir DailyRoutineListAcquireResponseV2.from(response))); } + @ResponseStatus(HttpStatus.OK) + @GetMapping("/theme/{themeId}") + public SuccessResponse acquireAllByThemeAndMember( + Principal principal, + @PathVariable long themeId + ) { + val memberId = Long.parseLong(principal.getName()); + val response = routineService.acquireAllInDailyByThemeAndMember(memberId, themeId); + return SuccessResponse.success( + RoutineSuccessMessage.SUCCESS_GET_ROUTINE.getMessage(), + DailyRoutinesAcquireResponseV2.from(response)); + } } diff --git a/src/main/java/com/soptie/server/routine/controller/v2/docs/DailyRoutineControllerV2Docs.java b/src/main/java/com/soptie/server/routine/controller/v2/docs/DailyRoutineControllerV2Docs.java index c6e45b94..80056734 100644 --- a/src/main/java/com/soptie/server/routine/controller/v2/docs/DailyRoutineControllerV2Docs.java +++ b/src/main/java/com/soptie/server/routine/controller/v2/docs/DailyRoutineControllerV2Docs.java @@ -1,17 +1,60 @@ package com.soptie.server.routine.controller.v2.docs; +import java.security.Principal; import java.util.LinkedHashSet; import org.springframework.http.ResponseEntity; import com.soptie.server.common.dto.SuccessResponse; import com.soptie.server.routine.controller.v2.dto.response.DailyRoutineListAcquireResponseV2; +import com.soptie.server.routine.controller.v2.dto.response.DailyRoutinesAcquireResponseV2; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +@Tag(name = "DailyRoutineApiV2", description = "데일리 루틴 API (version2)") public interface DailyRoutineControllerV2Docs { + @Operation( + summary = "데일리 루틴 목록 조회", + description = "특정 테마 id 목록에 속하는 데일리 루틴 목록을 조회합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK Success", + content = @Content(schema = @Schema(implementation = DailyRoutineListAcquireResponseV2.class)))}) ResponseEntity> acquireAllByThemes( - @Parameter(name = "themeIds", description = "조회할 테마 id 목록") LinkedHashSet themeIds + @Parameter( + name = "themeIds", + description = "조회할 테마 id 목록", + required = true, + in = ParameterIn.QUERY + ) LinkedHashSet themeIds + ); + + @Operation( + summary = "데일리 루틴 목록 조회", + description = "특정 테마에 속하는 데일리 루틴 목록을 조회합니다. 회원이 가진 유무를 함께 확인할 수 있습니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OK Success", + content = @Content(schema = @Schema(implementation = DailyRoutinesAcquireResponseV2.class)))}) + SuccessResponse acquireAllByThemeAndMember( + @Parameter(hidden = true) Principal principal, + @Parameter( + name = "themeId", + description = "조회하려는 루틴의 테마 id", + required = true, + in = ParameterIn.PATH + ) long themeId ); } diff --git a/src/main/java/com/soptie/server/routine/controller/v2/dto/response/DailyRoutineListAcquireResponseV2.java b/src/main/java/com/soptie/server/routine/controller/v2/dto/response/DailyRoutineListAcquireResponseV2.java index b870fb56..2a11a7d9 100644 --- a/src/main/java/com/soptie/server/routine/controller/v2/dto/response/DailyRoutineListAcquireResponseV2.java +++ b/src/main/java/com/soptie/server/routine/controller/v2/dto/response/DailyRoutineListAcquireResponseV2.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.Map; -import com.soptie.server.routine.service.vo.RoutineVO; +import com.soptie.server.routine.entity.Routine; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; @@ -14,7 +14,7 @@ public record DailyRoutineListAcquireResponseV2( List themes ) { - public static DailyRoutineListAcquireResponseV2 from(Map> routinesMap) { + public static DailyRoutineListAcquireResponseV2 from(Map> routinesMap) { return DailyRoutineListAcquireResponseV2.builder() .themes(routinesMap.keySet().stream().map(key -> ThemeResponse.from(key, routinesMap.get(key))).toList()) .build(); @@ -26,7 +26,7 @@ private record ThemeResponse( List routines ) { - private static ThemeResponse from(long themeId, List routines) { + private static ThemeResponse from(long themeId, List routines) { return ThemeResponse.builder() .themeId(themeId) .routines(routines.stream().map(RoutineResponse::from).toList()) @@ -40,10 +40,10 @@ private record RoutineResponse( @NotNull String content ) { - private static RoutineResponse from(RoutineVO routine) { + private static RoutineResponse from(Routine routine) { return RoutineResponse.builder() - .routineId(routine.routineId()) - .content(routine.content()) + .routineId(routine.getId()) + .content(routine.getContent()) .build(); } } diff --git a/src/main/java/com/soptie/server/routine/controller/v2/dto/response/DailyRoutinesAcquireResponseV2.java b/src/main/java/com/soptie/server/routine/controller/v2/dto/response/DailyRoutinesAcquireResponseV2.java new file mode 100644 index 00000000..0c3c6508 --- /dev/null +++ b/src/main/java/com/soptie/server/routine/controller/v2/dto/response/DailyRoutinesAcquireResponseV2.java @@ -0,0 +1,53 @@ +package com.soptie.server.routine.controller.v2.dto.response; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.soptie.server.routine.entity.Routine; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.val; + +@Builder(access = AccessLevel.PRIVATE) +public record DailyRoutinesAcquireResponseV2( + @Schema(description = "조회하려는 루틴 목록") + @NotNull + List routines +) { + + public static DailyRoutinesAcquireResponseV2 from(Map> routineToMember) { + val routines = new ArrayList(); + for (val key : routineToMember.keySet()) { + routines.addAll(routineToMember.get(key).stream() + .map(routine -> DailyRoutineResponse.of(routine, key)) + .toList()); + } + return DailyRoutinesAcquireResponseV2.builder() + .routines(routines) + .build(); + } + + @Builder(access = AccessLevel.PRIVATE) + private record DailyRoutineResponse( + @Schema(description = "루틴 id") + long id, + @Schema(description = "루틴 내용") + @NotNull + String content, + @Schema(description = "회원이 가지고 있는 루틴 유무") + boolean existedInMember + ) { + + private static DailyRoutineResponse of(Routine routine, boolean existedInMember) { + return DailyRoutineResponse.builder() + .id(routine.getId()) + .content(routine.getContent()) + .existedInMember(existedInMember) + .build(); + } + } +} diff --git a/src/main/java/com/soptie/server/routine/service/RoutineService.java b/src/main/java/com/soptie/server/routine/service/RoutineService.java index 225015a1..19afb813 100644 --- a/src/main/java/com/soptie/server/routine/service/RoutineService.java +++ b/src/main/java/com/soptie/server/routine/service/RoutineService.java @@ -2,6 +2,8 @@ import static com.soptie.server.common.config.ValueConfig.*; +import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -13,9 +15,11 @@ import com.soptie.server.member.adapter.MemberFinder; import com.soptie.server.member.entity.Member; import com.soptie.server.memberroutine.adapter.MemberRoutineFinder; +import com.soptie.server.memberroutine.entity.MemberRoutine; import com.soptie.server.memberroutine.repository.dto.MemberChallengeResponse; import com.soptie.server.routine.adapter.ChallengeFinder; import com.soptie.server.routine.adapter.RoutineFinder; +import com.soptie.server.routine.entity.Routine; import com.soptie.server.routine.entity.RoutineType; import com.soptie.server.routine.service.dto.request.HappinessSubRoutineListGetServiceRequest; import com.soptie.server.routine.service.dto.response.ChallengeRoutineListAcquireServiceResponse; @@ -45,7 +49,7 @@ public List acquireAllInDailyNotInMemberByThemeId(long memberId, long return routineFinder.findAllNotInMemberByTypeAndThemeId(memberId, RoutineType.DAILY, themeId); } - public List acquireAllInHappinessByThemeId(Long themeId) { + public List acquireAllInHappinessByThemeId(Long themeId) { return routineFinder.findAllByTypeAndThemeId(RoutineType.CHALLENGE, themeId); } @@ -57,8 +61,8 @@ public HappinessSubRoutineListGetServiceResponse getHappinessSubRoutines( return HappinessSubRoutineListGetServiceResponse.of(routine, subRoutines); } - public Map> acquireAllInDailyWithThemeId(Set themeIds) { - val themeToRoutine = new LinkedHashMap>(); + public Map> acquireAllInDailyWithThemeId(Set themeIds) { + val themeToRoutine = new LinkedHashMap>(); for (val themeId : themeIds) { val routines = routineFinder.findAllByTypeAndThemeId(RoutineType.DAILY, themeId); themeToRoutine.put(themeId, routines); @@ -83,6 +87,26 @@ public Map acquireAllInChall return themeToChallenge; } + public Map> acquireAllInDailyByThemeAndMember(long memberId, long themeId) { + val routines = routineFinder.findAllByTypeAndThemeId(RoutineType.DAILY, themeId); + val member = memberFinder.findById(memberId); + val memberRoutineIds = memberRoutineFinder.findAllByMemberAndType(member, RoutineType.DAILY).stream() + .map(MemberRoutine::getRoutineId) + .toList(); + return getRoutineToMember(routines, memberRoutineIds); + } + + private Map> getRoutineToMember(List routines, List memberRoutineIds) { + val routineToMember = new HashMap>(); + routineToMember.put(true, new ArrayList<>()); + routineToMember.put(false, new ArrayList<>()); + for (val routine : routines) { + val isMemberRoutine = memberRoutineIds.contains(routine.getId()); + routineToMember.get(isMemberRoutine).add(routine); + } + return routineToMember; + } + private long getChallengeIdByMember(Member member) { val challengeByMember = memberRoutineFinder.findChallengeByMember(member); return challengeByMember.map(MemberChallengeResponse::challengeId).orElse(MEMBER_HAS_NOT_CHALLENGE); diff --git a/src/test/java/com/soptie/server/routine/service/DailyRoutineServiceTest.java b/src/test/java/com/soptie/server/routine/service/DailyRoutineServiceTest.java new file mode 100644 index 00000000..e2ca5d07 --- /dev/null +++ b/src/test/java/com/soptie/server/routine/service/DailyRoutineServiceTest.java @@ -0,0 +1,73 @@ +package com.soptie.server.routine.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.soptie.server.member.adapter.MemberFinder; +import com.soptie.server.member.entity.Member; +import com.soptie.server.memberroutine.adapter.MemberRoutineFinder; +import com.soptie.server.memberroutine.entity.MemberRoutine; +import com.soptie.server.routine.adapter.RoutineFinder; +import com.soptie.server.routine.entity.Routine; +import com.soptie.server.routine.entity.RoutineType; +import com.soptie.server.support.fixture.MemberFixture; +import com.soptie.server.support.fixture.MemberRoutineFixture; +import com.soptie.server.support.fixture.RoutineFixture; + +@ExtendWith(MockitoExtension.class) +class DailyRoutineServiceTest { + + @InjectMocks + RoutineService routineService; + + @Mock + RoutineFinder routineFinder; + + @Mock + MemberRoutineFinder memberRoutineFinder; + + @Mock + MemberFinder memberFinder; + + @Test + @DisplayName("[성공] 회원 루틴 유무에 따라 Map 타입으로 루틴 정보를 반환한다.") + void acquireRoutineToMember() { + // given + long themeId = 0L; + long memberId = 0L; + Member member = MemberFixture.member().build(); + List routines = List.of( + RoutineFixture.routine().id(1L).build(), + RoutineFixture.routine().id(2L).build(), + RoutineFixture.routine().id(3L).build(), + RoutineFixture.routine().id(4L).build(), + RoutineFixture.routine().id(5L).build() + ); + List memberRoutines = List.of( + MemberRoutineFixture.memberRoutine().routineId(1L).build(), + MemberRoutineFixture.memberRoutine().routineId(2L).build(), + MemberRoutineFixture.memberRoutine().routineId(3L).build() + ); + + doReturn(routines).when(routineFinder).findAllByTypeAndThemeId(RoutineType.DAILY, themeId); + doReturn(member).when(memberFinder).findById(memberId); + doReturn(memberRoutines).when(memberRoutineFinder).findAllByMemberAndType(member, RoutineType.DAILY); + + // when + Map> result = routineService.acquireAllInDailyByThemeAndMember(memberId, themeId); + + // then + assertThat(result.get(true)).hasSize(3); + assertThat(result.get(false)).hasSize(2); + } +} diff --git a/src/test/java/com/soptie/server/routine/service/integration/RoutineServiceIntegrationTest.java b/src/test/java/com/soptie/server/routine/service/integration/RoutineServiceIntegrationTest.java index b6ea59d6..df5ef10e 100644 --- a/src/test/java/com/soptie/server/routine/service/integration/RoutineServiceIntegrationTest.java +++ b/src/test/java/com/soptie/server/routine/service/integration/RoutineServiceIntegrationTest.java @@ -114,15 +114,15 @@ void acquireAllWithThemeIds() { themeIds.add(theme1.getId()); // when - final Map> actual = routineService.acquireAllInDailyWithThemeId(themeIds); + final Map> actual = routineService.acquireAllInDailyWithThemeId(themeIds); // then Assertions.assertThat(actual.keySet()).containsExactly(theme2.getId(), theme1.getId()); - List routineIdsForTheme1 = actual.get(theme1.getId()).stream().map(RoutineVO::routineId).toList(); + List routineIdsForTheme1 = actual.get(theme1.getId()).stream().map(Routine::getId).toList(); Assertions.assertThat(routineIdsForTheme1).containsExactlyInAnyOrder(routineOfTheme1.getId()); - List routineIdsForTheme2 = actual.get(theme2.getId()).stream().map(RoutineVO::routineId).toList(); + List routineIdsForTheme2 = actual.get(theme2.getId()).stream().map(Routine::getId).toList(); Assertions.assertThat(routineIdsForTheme2).containsExactlyInAnyOrder(routineOfTheme2.getId()); } @@ -178,11 +178,11 @@ void setUp() { @DisplayName("[성공] 테마에 포함된 행복 루틴 목록을 조회한다.") void getHappinessRoutinesByTheme() { // when - final List actual = routineService.acquireAllInHappinessByThemeId(theme1.getId()); + final List actual = routineService.acquireAllInHappinessByThemeId(theme1.getId()); // then Assertions.assertThat(actual).hasSize(2); - List routineIds = actual.stream().map(RoutineVO::routineId).toList(); + List routineIds = actual.stream().map(Routine::getId).toList(); Assertions.assertThat(routineIds).containsExactlyInAnyOrder(routine1.getId(), routine2.getId()); } }