-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: 동아리 소속 목록 조회 api, 동아리 목록 조회 api, 동아리 상세 정보 조회 api 구현 #347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
e1d43dc
4c42e72
7090b05
d4d83cb
a2be583
9c44e7d
d818cac
840644f
26f1195
2b65645
8cec711
c67ff24
e4e7276
9d613f4
e0a0241
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| package com.kustacks.kuring.club.adapter.in.web; | ||
|
|
||
| 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.ClubDetailResponse; | ||
| import com.kustacks.kuring.club.adapter.in.web.dto.ClubDivisionListResponse; | ||
| import com.kustacks.kuring.club.adapter.in.web.dto.ClubDivisionResponse; | ||
| import com.kustacks.kuring.club.adapter.in.web.dto.ClubListResponse; | ||
| import com.kustacks.kuring.club.application.port.in.ClubQueryUseCase; | ||
| import com.kustacks.kuring.club.application.port.in.dto.ClubDetailResult; | ||
| import com.kustacks.kuring.club.application.port.in.dto.ClubListCommand; | ||
| import com.kustacks.kuring.club.application.port.in.dto.ClubListResult; | ||
| import com.kustacks.kuring.common.annotation.RestWebAdapter; | ||
| import com.kustacks.kuring.common.data.Cursor; | ||
| import com.kustacks.kuring.common.dto.BaseResponse; | ||
| import com.kustacks.kuring.common.exception.InvalidStateException; | ||
| import com.kustacks.kuring.common.exception.code.ErrorCode; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.constraints.Max; | ||
| import jakarta.validation.constraints.Min; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.validation.annotation.Validated; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| import static com.kustacks.kuring.auth.authentication.AuthorizationExtractor.extractAuthorizationValue; | ||
| import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CLUB_DETAIL_SEARCH_SUCCESS; | ||
| import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CLUB_DIVISION_SEARCH_SUCCESS; | ||
| import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CLUB_LIST_SEARCH_SUCCESS; | ||
|
|
||
| @Tag(name = "Club-Query", description = "동아리 정보 조회") | ||
| @Validated | ||
| @RequiredArgsConstructor | ||
| @RestWebAdapter(path = "/api/v2/clubs") | ||
| public class ClubQueryApiV2 { | ||
|
|
||
| private static final String FCM_TOKEN_HEADER_KEY = "User-Token"; | ||
| private static final String JWT_TOKEN_HEADER_KEY = "JWT"; | ||
|
|
||
| private final JwtTokenProvider jwtTokenProvider; | ||
| private final ClubQueryUseCase clubQueryUseCase; | ||
|
|
||
| @Operation(summary = "동아리 소속 목록 조회", description = "서버가 지원하는 동아리 소속 목록을 조회합니다") | ||
| @GetMapping("/divisions") | ||
| public ResponseEntity<BaseResponse<ClubDivisionListResponse>> getSupportedClubDivisions() { | ||
|
|
||
| List<ClubDivisionResponse> divisions = clubQueryUseCase.getClubDivisions() | ||
| .stream() | ||
| .map(ClubDivisionResponse::from) | ||
| .toList(); | ||
|
|
||
| ClubDivisionListResponse response = new ClubDivisionListResponse(divisions); | ||
|
|
||
| return ResponseEntity.ok().body(new BaseResponse<>(CLUB_DIVISION_SEARCH_SUCCESS, response)); | ||
| } | ||
|
|
||
| @Operation(summary = "동아리 목록 조회", description = "필터 조건에 맞는 동아리 목록을 커서 페이징으로 조회합니다") | ||
| @SecurityRequirement(name = JWT_TOKEN_HEADER_KEY) | ||
| @GetMapping | ||
| public ResponseEntity<BaseResponse<ClubListResponse>> getClubs( | ||
| @RequestParam(required = false) String category, | ||
| @RequestParam(required = false) String division, | ||
| @RequestParam(required = false) String cursor, | ||
| @RequestParam(defaultValue = "20") @Min(1) @Max(30) int size, | ||
| @RequestParam(defaultValue = "name") String sortBy, | ||
| @RequestHeader(value = AuthorizationExtractor.AUTHORIZATION, required = false) String bearerToken | ||
| ) { | ||
| String email = resolveLoginEmail(bearerToken); | ||
|
|
||
| ClubListCommand command = new ClubListCommand(category, division, Cursor.from(cursor), size, sortBy); | ||
|
|
||
| ClubListResult result = clubQueryUseCase.getClubs(command, email); | ||
|
|
||
| ClubListResponse response = ClubListResponse.from(result); | ||
|
|
||
| return ResponseEntity.ok().body(new BaseResponse<>(CLUB_LIST_SEARCH_SUCCESS, response)); | ||
| } | ||
|
|
||
| @Operation(summary = "동아리 상세 조회", description = "특정 동아리의 상세 정보를 조회합니다.") | ||
| @SecurityRequirement(name = FCM_TOKEN_HEADER_KEY) | ||
| @SecurityRequirement(name = JWT_TOKEN_HEADER_KEY) | ||
| @GetMapping("/{id}") | ||
| public ResponseEntity<BaseResponse<ClubDetailResponse>> getClubDetail( | ||
| @PathVariable Long id, | ||
| @RequestHeader(value = FCM_TOKEN_HEADER_KEY, required = false) String userToken, | ||
| @RequestHeader(value = AuthorizationExtractor.AUTHORIZATION, required = false) String bearerToken | ||
| ) { | ||
| String email = resolveLoginEmail(bearerToken); | ||
|
|
||
| ClubDetailResult result = clubQueryUseCase.getClubDetail(id, email); | ||
jiyun921 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ClubDetailResponse response = ClubDetailResponse.from(result); | ||
|
|
||
| return ResponseEntity.ok().body(new BaseResponse<>(CLUB_DETAIL_SEARCH_SUCCESS, response)); | ||
| } | ||
|
|
||
| private String resolveLoginEmail(String bearerToken) { | ||
| return Optional.ofNullable(bearerToken) | ||
| .map(token -> extractAuthorizationValue(token, AuthorizationType.BEARER)) | ||
| .map(this::validateJwtAndGetEmail) | ||
| .orElse(null); | ||
| } | ||
|
|
||
| private String validateJwtAndGetEmail(String jwtToken) { | ||
| if (!jwtTokenProvider.validateToken(jwtToken)) { | ||
| throw new InvalidStateException(ErrorCode.JWT_INVALID_TOKEN); | ||
| } | ||
| return jwtTokenProvider.getPrincipal(jwtToken); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package com.kustacks.kuring.club.adapter.in.web.dto; | ||
|
|
||
| import com.kustacks.kuring.club.application.port.in.dto.ClubDetailResult; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public record ClubDetailResponse( | ||
| Long id, | ||
| String name, | ||
| String summary, | ||
| String category, | ||
| String division, | ||
| int subscriberCount, | ||
| boolean isSubscribed, | ||
| String instagramUrl, | ||
| String youtubeUrl, | ||
| String etcUrl, | ||
| String description, | ||
| String qualifications, | ||
| String recruitmentStatus, | ||
| LocalDateTime recruitStartAt, | ||
| LocalDateTime recruitEndAt, | ||
| String applyUrl, | ||
| String posterImageUrl, | ||
| Location location | ||
| ) { | ||
|
|
||
| public static ClubDetailResponse from(ClubDetailResult result) { | ||
|
|
||
| Location location = result.location() == null ? | ||
| null | ||
| : new Location( | ||
| result.location().building(), | ||
| result.location().room(), | ||
| result.location().lon(), | ||
| result.location().lat() | ||
| ); | ||
|
|
||
|
Comment on lines
+30
to
+38
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 새로운 객체를 만드는 이유가 있는지요??
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ClubDetailResult의 location을 그대로 반환하면 web 계층에서 application에 의존하게 된다고 생각해서 Response 전용 location으로 새로 만들었습니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아아 그런거라면 괜찮은거 같아유 //AS-IS
ClubDetailResult.Location location = hasLocation(dto) ?
new ClubDetailResult.Location(
dto.getBuilding(),
dto.getRoom(),
dto.getLon(),
dto.getLat()
)
: null;
return new ClubDetailResult(
dto.getId(),
dto.getName(),
dto.getSummary(),
dto.getCategory(),
dto.getDivision(),
subscriberCount,
isSubscribed,
dto.getInstagramUrl(),
dto.getYoutubeUrl(),
dto.getEtcUrl(),
dto.getDescription(),
dto.getQualifications(),
dto.getRecruitmentStatus(),
dto.getRecruitStartAt(),
dto.getRecruitEndAt(),
dto.getApplyUrl(),
dto.getPosterImagePath(),
location
);
//TO-BE
ClubDetailResult.from(dto); |
||
| return new ClubDetailResponse( | ||
| result.id(), | ||
| result.name(), | ||
| result.summary(), | ||
| result.category().getName(), | ||
| result.division().getName(), | ||
| result.subscriberCount(), | ||
| result.isSubscribed(), | ||
| result.instagramUrl(), | ||
| result.youtubeUrl(), | ||
| result.etcUrl(), | ||
| result.description(), | ||
| result.qualifications(), | ||
| result.recruitmentStatus().getValue(), | ||
| result.recruitStartAt(), | ||
| result.recruitEndAt(), | ||
| result.applyUrl(), | ||
| result.posterImageUrl(), | ||
| location | ||
| ); | ||
jiyun921 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public record Location( | ||
| String building, | ||
| String room, | ||
| Double lon, | ||
| Double lat | ||
| ) { | ||
| } | ||
| } | ||
rlagkswn00 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.kustacks.kuring.club.adapter.in.web.dto; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record ClubDivisionListResponse( | ||
| List<ClubDivisionResponse> divisions | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Result배열로 만들어도 될거같슴니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 별도 Response단일 객체를 만들지 않아도!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앗 저도 첨에 그렇게 생각해봤는데 명세서에 응답 구조가 { divisions : [] } 이렇게 divisions 필드로 감싸는 형식으로 돼있어서 따로 response 만들었습니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아아 제 말은 |
||
| ) { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.kustacks.kuring.club.adapter.in.web.dto; | ||
|
|
||
| import com.kustacks.kuring.club.application.port.in.dto.ClubDivisionResult; | ||
|
|
||
| public record ClubDivisionResponse( | ||
| String code, | ||
| String koreanName | ||
| ) { | ||
| public static ClubDivisionResponse from(ClubDivisionResult result) { | ||
| return new ClubDivisionResponse( | ||
| result.code(), | ||
| result.koreanName() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.kustacks.kuring.club.adapter.in.web.dto; | ||
|
|
||
| import com.kustacks.kuring.club.application.port.in.dto.ClubItemResult; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public record ClubItemResponse( | ||
| Long id, | ||
| String name, | ||
| String summary, | ||
| String iconImageUrl, | ||
| String category, | ||
| String division, | ||
| boolean isSubscribed, | ||
| int subscriberCount, | ||
| LocalDateTime recruitStartDate, | ||
| LocalDateTime recruitEndDate | ||
| ) { | ||
| public static ClubItemResponse from(ClubItemResult result) { | ||
| return new ClubItemResponse( | ||
| result.id(), | ||
| result.name(), | ||
| result.summary(), | ||
| result.iconImageUrl(), | ||
| result.category(), | ||
| result.division(), | ||
| result.isSubscribed(), | ||
| result.subscriberCount(), | ||
| result.recruitStartDate(), | ||
| result.recruitEndDate() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.kustacks.kuring.club.adapter.in.web.dto; | ||
|
|
||
| import com.kustacks.kuring.club.application.port.in.dto.ClubListResult; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record ClubListResponse( | ||
| List<ClubItemResponse> clubs, | ||
| String cursor, | ||
| boolean hasNext, | ||
| int totalCount | ||
| ) { | ||
|
|
||
| public static ClubListResponse from(ClubListResult result) { | ||
| List<ClubItemResponse> clubs = result.clubs().stream() | ||
| .map(ClubItemResponse::from) | ||
| .toList(); | ||
|
|
||
| return new ClubListResponse( | ||
| clubs, | ||
| result.cursor(), | ||
| result.hasNext(), | ||
| result.totalCount() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| package com.kustacks.kuring.club.adapter.out.persistence; | ||
|
|
||
| import com.kustacks.kuring.club.application.port.out.ClubQueryPort; | ||
| import com.kustacks.kuring.club.application.port.out.dto.ClubDetailDto; | ||
| import com.kustacks.kuring.club.application.port.out.dto.ClubReadModel; | ||
| import com.kustacks.kuring.club.domain.ClubSubscribe; | ||
| import com.kustacks.kuring.common.annotation.PersistenceAdapter; | ||
| import com.kustacks.kuring.common.data.Cursor; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @PersistenceAdapter | ||
| @RequiredArgsConstructor | ||
| public class ClubPersistenceAdapter implements ClubQueryPort { | ||
|
|
||
| private final ClubRepository clubRepository; | ||
| private final ClubSubscribeRepository clubSubscribeRepository; | ||
|
|
||
| @Override | ||
| public List<ClubReadModel> searchClubs( | ||
| String category, | ||
| List<String> divisions, | ||
| Cursor cursor, | ||
| int size, | ||
| String sortBy, | ||
| LocalDateTime now | ||
| ) { | ||
| return clubRepository.searchClubs( | ||
| category, | ||
| divisions, | ||
| cursor == null ? null : cursor.getStringCursor(), | ||
| size, | ||
| sortBy, | ||
| now | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public int countClubs(String category, List<String> divisions) { | ||
| return clubRepository.countClubs(category, divisions); | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<ClubDetailDto> findClubDetailById(Long id) { | ||
| return clubRepository.findClubDetailById(id); | ||
| } | ||
|
|
||
| @Override | ||
| public int countSubscribers(Long clubId) { | ||
| return clubSubscribeRepository.countByClubId(clubId); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean existsSubscription(Long clubId, Long loginUserId) { | ||
| return clubSubscribeRepository.existsByClubIdAndUser_LoginUserId(clubId, loginUserId); | ||
| } | ||
|
|
||
| @Override | ||
| public Map<Long, Integer> countSubscribersByClubIds(List<Long> clubIds) { | ||
|
|
||
| if (clubIds == null || clubIds.isEmpty()) { | ||
| return Map.of(); | ||
| } | ||
|
|
||
| List<ClubSubscribe> subscriptions = clubSubscribeRepository.findByClubIdIn(clubIds); | ||
|
|
||
| return subscriptions.stream() | ||
| .collect(Collectors.groupingBy( | ||
| sub -> sub.getClub().getId(), | ||
| Collectors.collectingAndThen( | ||
| Collectors.counting(), | ||
| Long::intValue | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희 서비스에 당연히 구독자 수 21억을 넘지는 않겠지만 서도..? 약간 조심은 해야할 내용이지 않나 이런 생각이 듭니닷 |
||
| ) | ||
| )); | ||
| } | ||
|
Comment on lines
+63
to
+80
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. groupby 절로 작성해서 DB단에서 처리하는게 더 좋아보이는데 어떻게 생각하세유? |
||
|
|
||
| @Override | ||
| public Map<Long, Boolean> findSubscribedClubIds( | ||
| List<Long> clubIds, | ||
| Long loginUserId | ||
| ) { | ||
|
|
||
| if (clubIds == null || clubIds.isEmpty() || loginUserId == null) { | ||
| return Map.of(); | ||
| } | ||
|
|
||
| List<ClubSubscribe> subscriptions = clubSubscribeRepository.findByClubIdInAndUser_LoginUserId(clubIds, loginUserId); | ||
|
|
||
| return subscriptions.stream() | ||
| .collect(Collectors.toMap( | ||
| sub -> sub.getClub().getId(), | ||
| sub -> true | ||
| )); | ||
| } | ||
|
Comment on lines
+64
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "ClubSubscribe.java" | head -5Repository: ku-ring/ku-ring-backend-web Length of output: 137 🏁 Script executed: find . -type f -name "ClubSubscribeRepository.java" | head -5Repository: ku-ring/ku-ring-backend-web Length of output: 164 🏁 Script executed: find . -type f -name "*ClubSubscribe*" -type f | grep -E "\.(java|kt)$"Repository: ku-ring/ku-ring-backend-web Length of output: 231 🏁 Script executed: cat -n ./src/main/java/com/kustacks/kuring/club/domain/ClubSubscribe.javaRepository: ku-ring/ku-ring-backend-web Length of output: 1428 🏁 Script executed: cat -n ./src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubSubscribeRepository.javaRepository: ku-ring/ku-ring-backend-web Length of output: 752 데이터베이스 수준의 집계 쿼리 사용 권장
🤖 Prompt for AI Agents
jiyun921 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.kustacks.kuring.club.adapter.out.persistence; | ||
|
|
||
| import com.kustacks.kuring.club.application.port.out.dto.ClubDetailDto; | ||
| import com.kustacks.kuring.club.application.port.out.dto.ClubReadModel; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface ClubQueryRepository { | ||
|
|
||
| List<ClubReadModel> searchClubs(String category, List<String> divisions, String cursor, int size, String sortBy, LocalDateTime now); | ||
|
|
||
| int countClubs(String category, List<String> divisions); | ||
jiyun921 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Optional<ClubDetailDto> findClubDetailById(Long id); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.