Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
15 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
@@ -0,0 +1,123 @@
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 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
) {
Long loginUserId = null;

if (bearerToken != null) {
String jwt = extractAuthorizationValue(bearerToken, AuthorizationType.BEARER);

if (jwtTokenProvider.validateToken(jwt)) {
loginUserId = Long.parseLong(jwtTokenProvider.getPrincipal(jwt));
}
}

ClubListCommand command = new ClubListCommand(category, division, Cursor.from(cursor), size, sortBy);

ClubListResult result = clubQueryUseCase.getClubs(command, loginUserId);

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
) {
Long loginUserId = null;

if (bearerToken != null) {
String jwt = extractAuthorizationValue(bearerToken, AuthorizationType.BEARER);

if (!jwtTokenProvider.validateToken(jwt)) {
throw new InvalidStateException(ErrorCode.JWT_INVALID_TOKEN);
}

loginUserId = Long.parseLong(jwtTokenProvider.getPrincipal(jwt));
}


ClubDetailResult result = clubQueryUseCase.getClubDetail(id, userToken, loginUserId);

ClubDetailResponse response = ClubDetailResponse.from(result);

return ResponseEntity.ok().body(new BaseResponse<>(CLUB_DETAIL_SEARCH_SUCCESS, response));
}
}
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 = null;
if (result.location() != null) {
location = new Location(
result.location().building(),
result.location().room(),
result.location().lon(),
result.location().lat()
);
}

Comment on lines +30 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

새로운 객체를 만드는 이유가 있는지요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClubDetailResult의 location을 그대로 반환하면 web 계층에서 application에 의존하게 된다고 생각해서 Response 전용 location으로 새로 만들었습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아 그런거라면 괜찮은거 같아유
그러면 ClubDetailDto가 일종의 ReadModel인거 같은데 아래처럼 표시하고 생성은 Result로 넘기는게 낫지 않을지용? 서비스로직에 신경 분산되는 큰 요인인거 같슴니다

//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
);
}

public record Location(
String building,
String room,
Double lon,
Double lat
) {
}
}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Result배열로 만들어도 될거같슴니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별도 Response단일 객체를 만들지 않아도!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 저도 첨에 그렇게 생각해봤는데 명세서에 응답 구조가 { divisions : [] } 이렇게 divisions 필드로 감싸는 형식으로 돼있어서 따로 response 만들었습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아 제 말은 ClubDivisionResult를 그대로 사용해도 되지 않나라는 이야기 였슴니다

) {
}
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,56 @@
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.common.annotation.PersistenceAdapter;
import com.kustacks.kuring.common.data.Cursor;
import lombok.RequiredArgsConstructor;

import java.util.List;
import java.util.Optional;

@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
) {
return clubRepository.searchClubs(
category,
divisions,
cursor == null ? null : cursor.getStringCursor(),
size,
sortBy
);
}

@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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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.util.List;
import java.util.Optional;

public interface ClubQueryRepository {

List<ClubReadModel> searchClubs(String category, List<String> divisions, String cursor, int size, String sortBy);

int countClubs(String category, List<String> divisions);

Optional<ClubDetailDto> findClubDetailById(Long id);
}
Loading
Loading