Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public Optional<ClubDetailReadModel> findClubDetailById(Long id) {
return clubRepository.findClubDetailById(id);
}

@Override
public List<ClubReadModel> findClubsByIds(List<Long> ids) {
return clubRepository.findClubsByIds(ids);
}
Comment thread
jiyun921 marked this conversation as resolved.
Outdated

@Override
public Optional<Club> findClubById(Long id) {
return clubRepository.findById(id);
Expand All @@ -57,6 +62,10 @@ public List<Long> findSubscribedClubIds(
return clubSubscribeRepository.findByClubIdInAndRootUserId(clubIds, rootUserId);
}

@Override
public List<Long> findAllSubscribedClubIds(Long rootUserId) {
return clubSubscribeRepository.findClubIdsByRootUserId(rootUserId);
}

@Override
public Long countSubscribers(Long clubId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public interface ClubQueryRepository {

List<ClubReadModel> searchClubs(ClubCategory category, List<ClubDivision> divisions);

List<ClubReadModel> findClubsByIds(List<Long> ids);

Optional<ClubDetailReadModel> findClubDetailById(Long id);

List<Club> findClubsBetweenDates(LocalDateTime start, LocalDateTime end);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ public List<ClubReadModel> searchClubs(
.fetch();
}

@Override
@Transactional(readOnly = true)
public List<ClubReadModel> findClubsByIds(List<Long> ids) {
return queryFactory
.select(new QClubReadModel(
club.id,
club.name,
club.summary,
club.iconImagePath,
club.category,
club.division,
club.recruitStartAt,
club.recruitEndAt
))
.from(club)
.where(club.id.in(ids))
.fetch();
}

@Override
@Transactional(readOnly = true)
public Optional<ClubDetailReadModel> findClubDetailById(Long id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ SELECT cs.club.id AS clubId, COUNT(cs) AS subscriberCount
and cs.rootUser.id = :rootUserId
""")
List<Long> findByClubIdInAndRootUserId(List<Long> clubIds, Long rootUserId);

@Query("""
select cs.club.id
from ClubSubscribe cs
where cs.rootUser.id = :rootUserId
""")
List<Long> findClubIdsByRootUserId(Long rootUserId);
}
Comment thread
jiyun921 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.kustacks.kuring.club.application.port.in;

import com.kustacks.kuring.club.application.port.in.dto.ClubListResult;
import com.kustacks.kuring.club.application.port.in.dto.ClubSubscriptionCommand;
import com.kustacks.kuring.club.application.port.in.dto.SubscribedClubListCommand;

public interface ClubSubscriptionUseCase {

long addSubscription(ClubSubscriptionCommand command);

long removeSubscription(ClubSubscriptionCommand command);

ClubListResult getSubscribedClubs(SubscribedClubListCommand command);
Comment thread
jiyun921 marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kustacks.kuring.club.application.port.in.dto;

public record SubscribedClubListCommand(
String email
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface ClubQueryPort {

Optional<ClubDetailReadModel> findClubDetailById(Long id);

List<ClubReadModel> findClubsByIds(List<Long> ids);

List<ClubReadModel> searchClubs(ClubCategory category, List<ClubDivision> divisions);

List<Club> findClubsBetweenDates(LocalDateTime start, LocalDateTime end);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface ClubSubscriptionQueryPort {

List<Long> findSubscribedClubIds(List<Long> clubIds, Long rootUserId);

List<Long> findAllSubscribedClubIds(Long rootUserId);

Comment thread
jiyun921 marked this conversation as resolved.
Outdated
Long countSubscribers(Long clubId);

Map<Long, Long> countSubscribersByClubIds(List<Long> clubIds);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package com.kustacks.kuring.club.application.service;

import com.kustacks.kuring.club.application.port.in.ClubSubscriptionUseCase;
import com.kustacks.kuring.club.application.port.in.dto.ClubItemResult;
import com.kustacks.kuring.club.application.port.in.dto.ClubListResult;
import com.kustacks.kuring.club.application.port.in.dto.ClubSubscriptionCommand;
import com.kustacks.kuring.club.application.port.in.dto.SubscribedClubListCommand;
import com.kustacks.kuring.club.application.port.out.ClubQueryPort;
import com.kustacks.kuring.club.application.port.out.ClubSubscriptionCommandPort;
import com.kustacks.kuring.club.application.port.out.ClubSubscriptionQueryPort;
import com.kustacks.kuring.club.application.port.out.dto.ClubReadModel;
import com.kustacks.kuring.club.domain.Club;
import com.kustacks.kuring.common.annotation.UseCase;
import com.kustacks.kuring.common.exception.InvalidStateException;
import com.kustacks.kuring.common.exception.code.ErrorCode;
import com.kustacks.kuring.common.properties.ServerProperties;
import com.kustacks.kuring.storage.application.port.out.StoragePort;
import com.kustacks.kuring.user.application.port.out.RootUserQueryPort;
import com.kustacks.kuring.user.application.port.out.UserEventPort;
import com.kustacks.kuring.user.application.port.out.UserQueryPort;
Expand All @@ -18,6 +23,9 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

@Slf4j
@UseCase
@Transactional
Expand All @@ -33,6 +41,7 @@ public class ClubCommandService implements ClubSubscriptionUseCase {
private final RootUserQueryPort rootUserQueryPort;
private final UserQueryPort userQueryPort;
private final UserEventPort userEventPort;
private final StoragePort storagePort;
Comment thread
jiyun921 marked this conversation as resolved.
Outdated

@Override
public long addSubscription(ClubSubscriptionCommand command) {
Expand Down Expand Up @@ -63,6 +72,60 @@ public long removeSubscription(ClubSubscriptionCommand command) {
return countSubscriptionsQueryPort.countSubscriptions(rootUser.getId());
}

@Override
@Transactional(readOnly = true)
public ClubListResult getSubscribedClubs(SubscribedClubListCommand command) {

RootUser rootUser = findRootUserByEmail(command.email());

List<Long> subscribedClubIds = countSubscriptionsQueryPort.findAllSubscribedClubIds(rootUser.getId());

if (subscribedClubIds.isEmpty()) {
return new ClubListResult(List.of());
}

List<ClubReadModel> clubReadModels = clubQueryPort.findClubsByIds(subscribedClubIds);

Map<Long, Long> subscriberCountMap = countSubscriptionsQueryPort.countSubscribersByClubIds(subscribedClubIds);
Comment thread
rlagkswn00 marked this conversation as resolved.
Outdated

List<ClubItemResult> clubItemResults = clubReadModels.stream()
.map(r -> convertClubItemResult(
r,
true,
subscriberCountMap.getOrDefault(r.getId(), 0L)
))
.toList();
Comment thread
rlagkswn00 marked this conversation as resolved.
Outdated

return new ClubListResult(clubItemResults);
}

private ClubItemResult convertClubItemResult(
ClubReadModel clubReadModel,
boolean isSubscribed,
Long subscriberCount
) {

String iconImageUrl = null;
String iconImagePath = clubReadModel.getIconImagePath();

if (iconImagePath != null && !iconImagePath.isBlank()) {
iconImageUrl = storagePort.getPresignedUrl(iconImagePath);
}

return new ClubItemResult(
clubReadModel.getId(),
clubReadModel.getName(),
clubReadModel.getSummary(),
iconImageUrl,
clubReadModel.getCategory().getName(),
clubReadModel.getDivision().getName(),
isSubscribed,
subscriberCount,
clubReadModel.getRecruitStartDate(),
clubReadModel.getRecruitEndDate()
);
}

private boolean isAlreadySubscription(RootUser rootUser, Club club) {
return countSubscriptionsQueryPort.existsSubscription(rootUser.getId(), club.getId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public enum ResponseCodeAndMessages {
CLUB_DETAIL_SEARCH_SUCCESS(HttpStatus.OK.value(), "동아리 상세 조회에 성공하였습니다"),
CLUB_SUBSCRIPTION_ADD_SUCCESS(HttpStatus.OK.value(), "구독에 성공했습니다."),
CLUB_SUBSCRIPTION_DELETE_SUCCESS(HttpStatus.OK.value(), "구독이 취소되었습니다."),
CLUB_SUBSCRIPTION_LIST_SEARCH_SUCCESS(HttpStatus.OK.value(), "구독한 동아리 목록 조회에 성공하였습니다"),

/**
* ErrorCodes about auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import com.kustacks.kuring.auth.authentication.AuthorizationExtractor;
import com.kustacks.kuring.auth.authentication.AuthorizationType;
import com.kustacks.kuring.auth.token.JwtTokenProvider;
import com.kustacks.kuring.club.adapter.in.web.dto.ClubListResponse;
import com.kustacks.kuring.club.application.port.in.ClubSubscriptionUseCase;
import com.kustacks.kuring.club.application.port.in.dto.ClubListResult;
import com.kustacks.kuring.club.application.port.in.dto.ClubSubscriptionCommand;
import com.kustacks.kuring.club.application.port.in.dto.SubscribedClubListCommand;
import com.kustacks.kuring.common.annotation.RestWebAdapter;
import com.kustacks.kuring.common.dto.BaseResponse;
import com.kustacks.kuring.common.exception.InvalidStateException;
Expand All @@ -19,6 +22,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -27,6 +31,7 @@
import static com.kustacks.kuring.auth.authentication.AuthorizationExtractor.extractAuthorizationValue;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CLUB_SUBSCRIPTION_ADD_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CLUB_SUBSCRIPTION_DELETE_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CLUB_SUBSCRIPTION_LIST_SEARCH_SUCCESS;

Comment thread
jiyun921 marked this conversation as resolved.
@Tag(name = "User-Club-Subscription", description = "동아리 구독")
@Validated
Expand Down Expand Up @@ -71,6 +76,26 @@ public ResponseEntity<BaseResponse<UserClubSubscriptionCountResponse>> deleteSub
return ResponseEntity.ok(new BaseResponse<>(CLUB_SUBSCRIPTION_DELETE_SUCCESS, new UserClubSubscriptionCountResponse(subscriptionCount)));
}

@Operation(summary = "구독한 동아리 목록 조회")
@SecurityRequirement(name = FCM_TOKEN_HEADER_KEY)
@SecurityRequirement(name = JWT_TOKEN_HEADER_KEY)
@GetMapping
public ResponseEntity<BaseResponse<ClubListResponse>> getMySubscriptions(
@RequestHeader(FCM_TOKEN_HEADER_KEY) String userToken,
@RequestHeader(AuthorizationExtractor.AUTHORIZATION) String bearerToken
Comment thread
jiyun921 marked this conversation as resolved.
) {
String email = validateJwtAndGetEmail(extractAuthorizationValue(bearerToken, AuthorizationType.BEARER));

SubscribedClubListCommand command = new SubscribedClubListCommand(email);

ClubListResult result = clubSubscriptionUseCase.getSubscribedClubs(command);

ClubListResponse response = ClubListResponse.from(result);

return ResponseEntity.ok(new BaseResponse<>(CLUB_SUBSCRIPTION_LIST_SEARCH_SUCCESS, response));
}


private String validateJwtAndGetEmail(String jwtToken) {
if (!jwtTokenProvider.validateToken(jwtToken)) {
throw new InvalidStateException(ErrorCode.JWT_INVALID_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,46 @@
import static com.kustacks.kuring.acceptance.CommonStep.실패_응답_확인;
import static com.kustacks.kuring.acceptance.EmailStep.인증코드_인증_요청;
import static com.kustacks.kuring.acceptance.EmailStep.회원가입_인증코드_이메일_전송_요청;
import static com.kustacks.kuring.acceptance.UserStep.*;
import static com.kustacks.kuring.acceptance.UserStep.구독한_동아리_목록_조회_요청;
import static com.kustacks.kuring.acceptance.UserStep.구독한_동아리_목록_조회_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.구독한_학과_목록_조회_요청;
import static com.kustacks.kuring.acceptance.UserStep.남은_질문_횟수_조회;
import static com.kustacks.kuring.acceptance.UserStep.동아리_구독_제거_성공_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.동아리_구독_제거_요청;
import static com.kustacks.kuring.acceptance.UserStep.동아리_구독_추가_성공_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.동아리_구독_추가_요청;
import static com.kustacks.kuring.acceptance.UserStep.로그아웃_요청;
import static com.kustacks.kuring.acceptance.UserStep.로그아웃_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.로그인_요청;
import static com.kustacks.kuring.acceptance.UserStep.로그인_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.루트유저_남은_질문_횟수_조회;
import static com.kustacks.kuring.acceptance.UserStep.북마크_생성_요청;
import static com.kustacks.kuring.acceptance.UserStep.북마크_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.북마크_조회_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.북마크한_공지_조회_요청;
import static com.kustacks.kuring.acceptance.UserStep.비밀번호_변경_요청;
import static com.kustacks.kuring.acceptance.UserStep.비밀번호_변경_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.사용자_로그인_되어_있음;
import static com.kustacks.kuring.acceptance.UserStep.사용자_정보_조회_요청;
import static com.kustacks.kuring.acceptance.UserStep.사용자_정보_조회_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.사용자_카테고리_구독_목록_조회_요청;
import static com.kustacks.kuring.acceptance.UserStep.사용자_학과_조회_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.사용자_회원가입_요청;
import static com.kustacks.kuring.acceptance.UserStep.액세스_토큰으로_비밀번호_변경_요청;
import static com.kustacks.kuring.acceptance.UserStep.질문_횟수_응답_검증;
import static com.kustacks.kuring.acceptance.UserStep.카테고리_구독_목록_조회_요청_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.카테고리_구독_요청;
import static com.kustacks.kuring.acceptance.UserStep.카테고리_구독_요청_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.피드백_요청_v2;
import static com.kustacks.kuring.acceptance.UserStep.피드백_요청_응답_확인_v2;
import static com.kustacks.kuring.acceptance.UserStep.학과_구독_요청;
import static com.kustacks.kuring.acceptance.UserStep.학과_구독_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.학사일정_알림_토글_요청;
import static com.kustacks.kuring.acceptance.UserStep.학사일정_알림_토글_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.회원_가입_요청;
import static com.kustacks.kuring.acceptance.UserStep.회원_탈퇴_요청;
import static com.kustacks.kuring.acceptance.UserStep.회원_탈퇴_응답_확인;
import static com.kustacks.kuring.acceptance.UserStep.회원가입_응답_확인;
Comment thread
jiyun921 marked this conversation as resolved.
Outdated
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
Expand Down Expand Up @@ -543,4 +582,19 @@ void remove_club_subscription_fail_when_not_subscribed() {

실패_응답_확인(response, HttpStatus.BAD_REQUEST);
}

@DisplayName("[v2] 사용자는 구독한 동아리 목록을 조회할 수 있다")
@Test
void lookup_subscribed_clubs() {
// given
String accessToken = 사용자_로그인_되어_있음(USER_FCM_TOKEN, USER_EMAIL, USER_PASSWORD);

동아리_구독_추가_요청(USER_FCM_TOKEN, accessToken, TEST_CLUB_ID);
Comment thread
jiyun921 marked this conversation as resolved.

// when
var response = 구독한_동아리_목록_조회_요청(USER_FCM_TOKEN, accessToken);

// then
구독한_동아리_목록_조회_응답_확인(response, List.of(TEST_CLUB_ID));
}
}
22 changes: 22 additions & 0 deletions src/test/java/com/kustacks/kuring/acceptance/UserStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,26 @@ public class UserStep {
);
}

public static ExtractableResponse<Response> 구독한_동아리_목록_조회_요청(String userToken, String accessToken) {
return RestAssured.given().log().all()
.accept(MediaType.APPLICATION_JSON_VALUE)
.header("User-Token", userToken)
.header("Authorization", "Bearer " + accessToken)
.when().get("/api/v2/users/subscriptions/clubs")
.then().log().all()
.extract();
}


public static void 구독한_동아리_목록_조회_응답_확인(ExtractableResponse<Response> response, List<Long> clubIds) {
assertAll(
() -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()),
() -> assertThat(response.jsonPath().getInt("code")).isEqualTo(200),
() -> assertThat(response.jsonPath().getList("data.clubs.id", Long.class)).containsAll(clubIds),
() -> assertThat(response.jsonPath().getList("data.clubs.isSubscribed")).containsOnly(true)
);
}

public static void 사용자_학과_조회_응답_확인(ExtractableResponse<Response> response, List<String> departments) {
assertAll(
() -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()),
Expand Down Expand Up @@ -326,6 +346,7 @@ public class UserStep {
.then().log().all()
.extract();
}

public static ExtractableResponse<Response> 비밀번호_변경_요청(String email, String newPassword) {
return RestAssured.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
Expand All @@ -343,6 +364,7 @@ public class UserStep {
() -> assertThat(response.jsonPath().getString("data.email")).isEqualTo(email)
);
}

public static ExtractableResponse<Response> 액세스_토큰으로_비밀번호_변경_요청(String jwtToken, String newPassword) {
return RestAssured.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
Expand Down
Loading
Loading