diff --git a/src/main/java/com/soptie/server/memberroutine/controller/v2/MemberChallengeRoutineControllerV2.java b/src/main/java/com/soptie/server/memberroutine/controller/v2/MemberChallengeRoutineControllerV2.java new file mode 100644 index 00000000..50e24467 --- /dev/null +++ b/src/main/java/com/soptie/server/memberroutine/controller/v2/MemberChallengeRoutineControllerV2.java @@ -0,0 +1,37 @@ +package com.soptie.server.memberroutine.controller.v2; + +import static com.soptie.server.memberroutine.message.MemberRoutineSuccessMassage.*; + +import java.security.Principal; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.soptie.server.common.dto.SuccessResponse; +import com.soptie.server.memberroutine.controller.v2.docs.MemberChallengeRoutineControllerV2Docs; +import com.soptie.server.memberroutine.controller.v2.dto.response.MemberChallengeRoutineAcquireResponseV2; +import com.soptie.server.memberroutine.service.MemberRoutineReadService; +import com.soptie.server.memberroutine.service.dto.request.MemberChallengeRoutineAcquireServiceRequest; + +import lombok.RequiredArgsConstructor; +import lombok.val; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/routines/challenge/member") +public class MemberChallengeRoutineControllerV2 implements MemberChallengeRoutineControllerV2Docs { + + private final MemberRoutineReadService memberRoutineReadService; + + @GetMapping + public ResponseEntity acquire(Principal principal) { + val memberId = Long.parseLong(principal.getName()); + return memberRoutineReadService.acquire(MemberChallengeRoutineAcquireServiceRequest.of(memberId)) + .map(response -> ResponseEntity.ok(SuccessResponse.success( + SUCCESS_GET_ROUTINE.getMessage(), + MemberChallengeRoutineAcquireResponseV2.of(response)))) + .orElseGet(() -> ResponseEntity.noContent().build()); + } +} diff --git a/src/main/java/com/soptie/server/memberroutine/controller/v2/docs/MemberChallengeRoutineControllerV2Docs.java b/src/main/java/com/soptie/server/memberroutine/controller/v2/docs/MemberChallengeRoutineControllerV2Docs.java new file mode 100644 index 00000000..06697b3b --- /dev/null +++ b/src/main/java/com/soptie/server/memberroutine/controller/v2/docs/MemberChallengeRoutineControllerV2Docs.java @@ -0,0 +1,40 @@ +package com.soptie.server.memberroutine.controller.v2.docs; + +import java.security.Principal; + +import org.springframework.http.ResponseEntity; + +import com.soptie.server.common.dto.ErrorResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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.tags.Tag; + +@Tag(name = "member challenge routines V2", description = "회원의 도전 루틴 API Version2") +public interface MemberChallengeRoutineControllerV2Docs { + + @Operation( + summary = "회원의 도전 루틴 목록 조회", + description = "회원의 도전 루틴을 테마와 함께 조회한다.", + responses = { + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse( + responseCode = "401", + description = "유효하지 않은 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse( + responseCode = "4xx", + description = "클라이언트(요청) 오류", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)))} + ) + ResponseEntity acquire( + @Parameter(hidden = true) Principal principal + ); +} diff --git a/src/main/java/com/soptie/server/memberroutine/controller/v2/dto/response/MemberChallengeRoutineAcquireResponseV2.java b/src/main/java/com/soptie/server/memberroutine/controller/v2/dto/response/MemberChallengeRoutineAcquireResponseV2.java new file mode 100644 index 00000000..d83c0888 --- /dev/null +++ b/src/main/java/com/soptie/server/memberroutine/controller/v2/dto/response/MemberChallengeRoutineAcquireResponseV2.java @@ -0,0 +1,34 @@ +package com.soptie.server.memberroutine.controller.v2.dto.response; + +import static lombok.AccessLevel.*; + +import com.soptie.server.memberroutine.service.dto.response.MemberChallengeRoutineAcquireServiceResponse; + +import lombok.Builder; +import lombok.NonNull; + +@Builder(access = PRIVATE) +public record MemberChallengeRoutineAcquireResponseV2( + long routineId, + long themeId, + @NonNull String themeName, + @NonNull String title, + @NonNull String content, + @NonNull String detailContent, + @NonNull String place, + @NonNull String timeTaken +) { + + public static MemberChallengeRoutineAcquireResponseV2 of(MemberChallengeRoutineAcquireServiceResponse response) { + return MemberChallengeRoutineAcquireResponseV2.builder() + .routineId(response.routineId()) + .themeId(response.theme().themeId()) + .themeName(response.theme().name()) + .title(response.routineContent()) + .content(response.content()) + .detailContent(response.description()) + .place(response.place()) + .timeTaken(response.requiredTime()) + .build(); + } +} diff --git a/src/main/java/com/soptie/server/memberroutine/service/MemberRoutineReadService.java b/src/main/java/com/soptie/server/memberroutine/service/MemberRoutineReadService.java index 1adc53fb..abb2a545 100644 --- a/src/main/java/com/soptie/server/memberroutine/service/MemberRoutineReadService.java +++ b/src/main/java/com/soptie/server/memberroutine/service/MemberRoutineReadService.java @@ -10,8 +10,10 @@ import com.soptie.server.member.adapter.MemberFinder; import com.soptie.server.memberroutine.adapter.MemberRoutineFinder; import com.soptie.server.memberroutine.repository.dto.MemberRoutineResponse; +import com.soptie.server.memberroutine.service.dto.request.MemberChallengeRoutineAcquireServiceRequest; import com.soptie.server.memberroutine.service.dto.request.MemberDailyRoutineListAcquireServiceRequest; import com.soptie.server.memberroutine.service.dto.request.MemberHappinessRoutineGetServiceRequest; +import com.soptie.server.memberroutine.service.dto.response.MemberChallengeRoutineAcquireServiceResponse; import com.soptie.server.memberroutine.service.dto.response.MemberDailyRoutineListAcquireServiceResponse; import com.soptie.server.memberroutine.service.dto.response.MemberDailyRoutinesAcquireServiceResponse; import com.soptie.server.memberroutine.service.dto.response.MemberHappinessRoutineGetServiceResponse; @@ -50,6 +52,14 @@ public MemberDailyRoutineListAcquireServiceResponse acquireAll( return MemberDailyRoutineListAcquireServiceResponse.of(routinesWithTheme); } + public Optional acquire( + MemberChallengeRoutineAcquireServiceRequest request + ) { + val member = memberFinder.findById(request.memberId()); + val memberRoutine = memberRoutineFinder.findChallengeByMember(member); + return memberRoutine.map(MemberChallengeRoutineAcquireServiceResponse::of); + } + private List collectByTheme(List routines) { val routinesByTheme = routines.stream().collect(Collectors.groupingBy(MemberRoutineResponse::themeId)); return routinesByTheme.values().stream() diff --git a/src/main/java/com/soptie/server/memberroutine/service/dto/request/MemberChallengeRoutineAcquireServiceRequest.java b/src/main/java/com/soptie/server/memberroutine/service/dto/request/MemberChallengeRoutineAcquireServiceRequest.java new file mode 100644 index 00000000..f3089672 --- /dev/null +++ b/src/main/java/com/soptie/server/memberroutine/service/dto/request/MemberChallengeRoutineAcquireServiceRequest.java @@ -0,0 +1,17 @@ +package com.soptie.server.memberroutine.service.dto.request; + +import static lombok.AccessLevel.*; + +import lombok.Builder; + +@Builder(access = PRIVATE) +public record MemberChallengeRoutineAcquireServiceRequest( + long memberId +) { + + public static MemberChallengeRoutineAcquireServiceRequest of(long memberId) { + return MemberChallengeRoutineAcquireServiceRequest.builder() + .memberId(memberId) + .build(); + } +} diff --git a/src/main/java/com/soptie/server/memberroutine/service/dto/response/MemberChallengeRoutineAcquireServiceResponse.java b/src/main/java/com/soptie/server/memberroutine/service/dto/response/MemberChallengeRoutineAcquireServiceResponse.java new file mode 100644 index 00000000..9725f8b0 --- /dev/null +++ b/src/main/java/com/soptie/server/memberroutine/service/dto/response/MemberChallengeRoutineAcquireServiceResponse.java @@ -0,0 +1,46 @@ +package com.soptie.server.memberroutine.service.dto.response; + +import static lombok.AccessLevel.*; + +import com.soptie.server.memberroutine.repository.dto.MemberChallengeResponse; +import com.soptie.server.theme.entity.Theme; + +import lombok.Builder; + +@Builder(access = PRIVATE) +public record MemberChallengeRoutineAcquireServiceResponse( + long routineId, + ThemeServiceResponse theme, + String routineContent, + String content, + String description, + String place, + String requiredTime +) { + + public static MemberChallengeRoutineAcquireServiceResponse of(MemberChallengeResponse routine) { + return MemberChallengeRoutineAcquireServiceResponse.builder() + .routineId(routine.id()) + .theme(ThemeServiceResponse.of(routine.theme())) + .routineContent(routine.routineContent()) + .content(routine.content()) + .description(routine.description()) + .place(routine.place()) + .requiredTime(routine.requiredTime()) + .build(); + } + + @Builder(access = PRIVATE) + public record ThemeServiceResponse( + long themeId, + String name + ) { + + private static ThemeServiceResponse of(Theme theme) { + return ThemeServiceResponse.builder() + .themeId(theme.getId()) + .name(theme.getName()) + .build(); + } + } +} diff --git a/src/test/java/com/soptie/server/membeerroutine/service/MemberRoutineServiceTest.java b/src/test/java/com/soptie/server/memberroutine/service/MemberRoutineServiceTest.java similarity index 97% rename from src/test/java/com/soptie/server/membeerroutine/service/MemberRoutineServiceTest.java rename to src/test/java/com/soptie/server/memberroutine/service/MemberRoutineServiceTest.java index 5eea98f4..339bbacb 100644 --- a/src/test/java/com/soptie/server/membeerroutine/service/MemberRoutineServiceTest.java +++ b/src/test/java/com/soptie/server/memberroutine/service/MemberRoutineServiceTest.java @@ -1,4 +1,4 @@ -package com.soptie.server.membeerroutine.service; +package com.soptie.server.memberroutine.service; import static com.soptie.server.routine.entity.RoutineType.*; import static org.assertj.core.api.Assertions.*; @@ -20,7 +20,6 @@ import com.soptie.server.memberroutine.adapter.MemberRoutineDeleter; import com.soptie.server.memberroutine.adapter.MemberRoutineFinder; import com.soptie.server.memberroutine.entity.MemberRoutine; -import com.soptie.server.memberroutine.service.MemberRoutineUpdateService; import com.soptie.server.memberroutine.service.dto.request.MemberRoutineAchieveServiceRequest; import com.soptie.server.support.fixture.MemberFixture; import com.soptie.server.support.fixture.MemberRoutineFixture; diff --git a/src/test/java/com/soptie/server/membeerroutine/service/integration/MemberRoutineServiceIntegrationTest.java b/src/test/java/com/soptie/server/memberroutine/service/integration/MemberRoutineServiceIntegrationTest.java similarity index 77% rename from src/test/java/com/soptie/server/membeerroutine/service/integration/MemberRoutineServiceIntegrationTest.java rename to src/test/java/com/soptie/server/memberroutine/service/integration/MemberRoutineServiceIntegrationTest.java index cc5b9351..58df72a1 100644 --- a/src/test/java/com/soptie/server/membeerroutine/service/integration/MemberRoutineServiceIntegrationTest.java +++ b/src/test/java/com/soptie/server/memberroutine/service/integration/MemberRoutineServiceIntegrationTest.java @@ -1,4 +1,4 @@ -package com.soptie.server.membeerroutine.service.integration; +package com.soptie.server.memberroutine.service.integration; import static com.soptie.server.routine.entity.RoutineType.*; import static com.soptie.server.routine.message.RoutineErrorCode.*; @@ -25,11 +25,14 @@ import com.soptie.server.memberroutine.service.MemberRoutineCreateService; import com.soptie.server.memberroutine.service.MemberRoutineDeleteService; import com.soptie.server.memberroutine.service.MemberRoutineReadService; +import com.soptie.server.memberroutine.service.dto.request.MemberChallengeRoutineAcquireServiceRequest; import com.soptie.server.memberroutine.service.dto.request.MemberDailyRoutineCreateServiceRequest; import com.soptie.server.memberroutine.service.dto.request.MemberDailyRoutineListAcquireServiceRequest; import com.soptie.server.memberroutine.service.dto.request.MemberHappinessRoutineCreateServiceRequest; import com.soptie.server.memberroutine.service.dto.request.MemberHappinessRoutineGetServiceRequest; import com.soptie.server.memberroutine.service.dto.request.MemberRoutinesDeleteServiceRequest; +import com.soptie.server.memberroutine.service.dto.response.MemberChallengeRoutineAcquireServiceResponse; +import com.soptie.server.memberroutine.service.dto.response.MemberDailyRoutineListAcquireServiceResponse; import com.soptie.server.memberroutine.service.dto.response.MemberDailyRoutinesAcquireServiceResponse; import com.soptie.server.memberroutine.service.dto.response.MemberDailyRoutinesAcquireServiceResponse.MemberDailyRoutineServiceResponse; import com.soptie.server.memberroutine.service.dto.response.MemberHappinessRoutineGetServiceResponse; @@ -264,7 +267,8 @@ class Acquire { Member member1; Member member2; - Theme theme; + Theme theme1; + Theme theme2; Routine routine1; Routine routine2; Routine routine3; @@ -275,16 +279,17 @@ void setUp() { member1 = memberRepository.save(MemberFixture.member().build()); member2 = memberRepository.save(MemberFixture.member().build()); - theme = themeRepository.save(ThemeFixture.theme().build()); + theme1 = themeRepository.save(ThemeFixture.theme().name("테마 1").build()); + theme2 = themeRepository.save(ThemeFixture.theme().name("테마 2").build()); routine1 = routineRepository.save( - RoutineFixture.routine().theme(theme).type(DAILY).content("새로운 나").build()); + RoutineFixture.routine().theme(theme1).type(DAILY).content("새로운 나").build()); routine2 = routineRepository.save( - RoutineFixture.routine().theme(theme).type(DAILY).content("깨끗한 나").build()); + RoutineFixture.routine().theme(theme1).type(DAILY).content("깨끗한 나").build()); routine3 = routineRepository.save( - RoutineFixture.routine().theme(theme).type(DAILY).content("똑똑한 나").build()); + RoutineFixture.routine().theme(theme2).type(DAILY).content("똑똑한 나").build()); challengeRoutine = routineRepository.save( - RoutineFixture.routine().theme(theme).type(CHALLENGE).content("도전 루틴").build()); + RoutineFixture.routine().theme(theme1).type(CHALLENGE).content("도전 루틴").build()); } @Test @@ -376,6 +381,75 @@ void getEmptyWhenMemberHasNotHappinessRoutine() { // then assertThat(actual).isEmpty(); } - } + @Test + @DisplayName("[성공] 회원이 가진 모든 데일리 루틴을 테마별로 조회한다. 이 때, 루틴은 가나다순으로 정렬된다.") + void acquireAllByMember() { + // given + memberRoutineRepository.save(MemberRoutineFixture.memberRoutine() + .member(member1).routineId(routine1.getId()).type(routine1.getType()).build()); + memberRoutineRepository.save(MemberRoutineFixture.memberRoutine() + .member(member1).routineId(routine2.getId()).type(routine2.getType()).build()); + memberRoutineRepository.save(MemberRoutineFixture.memberRoutine() + .member(member1).routineId(routine3.getId()).type(routine3.getType()).build()); + + MemberDailyRoutineListAcquireServiceRequest request = MemberDailyRoutineListAcquireServiceRequest.of( + member1.getId()); + + // when + final MemberDailyRoutineListAcquireServiceResponse actual = memberRoutineReadService.acquireAll(request); + + // then + int themeCount = actual.routines().size(); + assertThat(themeCount).isEqualTo(2); + List contents = actual.routines().get(0).routines().stream().map( + MemberDailyRoutinesAcquireServiceResponse.MemberDailyRoutineServiceResponse::content).toList(); + assertThat(contents).hasSize(2); + assertThat(contents).containsExactly(routine2.getContent(), routine1.getContent()); + } + + @Test + @DisplayName("[성공] 회원의 도전 루틴이 존재한다면 해당 도전 루틴을 테마와 함께 조회한다.") + void acquireByMember() { + // given + Challenge challenge = challengeRepository.save( + ChallengeFixture.challenge() + .content("무한~ 도전~") + .description("무한으로 즐겨요") + .routine(challengeRoutine) + .build()); + memberRoutineRepository.save(MemberRoutineFixture.memberRoutine() + .member(member1).routineId(challenge.getId()).type(challengeRoutine.getType()).build()); + + MemberChallengeRoutineAcquireServiceRequest request = + MemberChallengeRoutineAcquireServiceRequest.of(member1.getId()); + + // when + final Optional actual = + memberRoutineReadService.acquire(request); + + // then + assertThat(actual).isPresent(); + + final MemberChallengeRoutineAcquireServiceResponse response = actual.get(); + assertThat(response.theme().themeId()).isEqualTo(challenge.getRoutine().getTheme().getId()); + assertThat(response.theme().name()).isEqualTo(challenge.getRoutine().getTheme().getName()); + assertThat(response.content()).isEqualTo(challenge.getContent()); + } + + @Test + @DisplayName("[성공] 회원이 가진 도전 루틴이 없으면 빈 값으로 조회된다.") + void acquireEmptyWhenMemberHasNotChallengeRoutine() { + // given + MemberChallengeRoutineAcquireServiceRequest request = + MemberChallengeRoutineAcquireServiceRequest.of(member1.getId()); + + // when + final Optional actual = + memberRoutineReadService.acquire(request); + + // then + assertThat(actual).isEmpty(); + } + } }