Skip to content
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

Feat: 공연 예매/지원자 통계 조회 API #91

Merged
merged 5 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
3 changes: 3 additions & 0 deletions src/main/java/kahlua/KahluaProject/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
Expand Down Expand Up @@ -50,6 +51,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
);
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers( "/api-docs/**", "/swagger-ui/**", "/swagger-ui.html/**", "/v3/api-docs/**", "/swagger-ui/index.html#/**").permitAll()
.requestMatchers(HttpMethod.GET, "/v1/admin/tickets/**").permitAll()
.requestMatchers(HttpMethod.GET, "/v1/admin/apply/**").permitAll()
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 GET 메서드 요청에 대해 permitAll() 설정을 적용하신 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

공연정보/지원정보 조회에 대해서만 permitAll 설정을 해둔다는게 엔드포인트를 잘못작성했네요 수정하겠습니다!

.requestMatchers("/v1/auth/sign-out/**", "v1/auth/recreate/**","/v1/user/**", "/v1/admin/**").authenticated()
.anyRequest().permitAll())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import kahlua.KahluaProject.dto.applyInfo.request.ApplyInfoRequest;
import kahlua.KahluaProject.dto.applyInfo.response.ApplyInfoResponse;
import kahlua.KahluaProject.dto.apply.response.ApplyListResponse;
import kahlua.KahluaProject.dto.apply.response.ApplyStatisticsResponse;
import kahlua.KahluaProject.exception.GeneralException;
import kahlua.KahluaProject.security.AuthDetails;
import kahlua.KahluaProject.service.ApplyService;
Expand Down Expand Up @@ -70,10 +71,22 @@ public ResponseEntity<InputStreamResource> applyListToExcel() throws IOException
.body(new InputStreamResource(in));
}

@PutMapping("/info/{apply_id}")
@PutMapping("/info/{apply_info_id}")
@Operation(summary = "지원 정보 수정", description = "지원 정보를 수정합니다")
public ApiResponse<ApplyInfoResponse> updateApplyInfo(@PathVariable("apply_id") Long applyId, @RequestBody ApplyInfoRequest applyInfoRequest, @AuthenticationPrincipal AuthDetails authDetails) {
public ApiResponse<ApplyInfoResponse> updateApplyInfo(@PathVariable("apply_info_id") Long applyId, @RequestBody ApplyInfoRequest applyInfoRequest, @AuthenticationPrincipal AuthDetails authDetails) {
return ApiResponse.onSuccess(applyService.updateApplyInfo(applyId, applyInfoRequest, authDetails.user()));
}

@GetMapping("/info/{apply_info_id}")
@Operation(summary = "지원 정보 조회", description = "지원 정보를 조회합니다")
public ApiResponse<ApplyInfoResponse> getApplyInfo(@PathVariable("apply_info_id") Long applyId) {
return ApiResponse.onSuccess(applyService.getApplyInfo(applyId));
}

@GetMapping("/statistics")
@Operation(summary = "지원자 통계 조회", description = "지원자 통계를 조회합니다")
public ApiResponse<ApplyStatisticsResponse> getApplyStatistics(@AuthenticationPrincipal AuthDetails authDetails) {
return ApiResponse.onSuccess(applyService.getApplyStatistics(authDetails.user()));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.servlet.http.HttpServletResponse;
import kahlua.KahluaProject.apipayload.ApiResponse;
import kahlua.KahluaProject.dto.ticket.response.TicketListResponse;
import kahlua.KahluaProject.dto.ticket.response.TicketStatisticsResponse;
import kahlua.KahluaProject.dto.ticket.response.TicketUpdateResponse;
import kahlua.KahluaProject.dto.ticketInfo.request.TicketInfoRequest;
import kahlua.KahluaProject.dto.ticketInfo.response.TicketInfoResponse;
Expand Down Expand Up @@ -104,4 +105,16 @@ public ResponseEntity<InputStreamResource> participantsListToExcel() throws IOEx
public ApiResponse<TicketInfoResponse> updateTicketInfo(@PathVariable("ticket_info_id") Long ticketInfoId, @RequestBody TicketInfoRequest ticketUpdateRequest, @AuthenticationPrincipal AuthDetails authDetails) {
return ApiResponse.onSuccess(ticketService.updateTicketInfo(ticketInfoId, ticketUpdateRequest, authDetails.user()));
}

@GetMapping("/{ticket_info_id}")
@Operation(summary = "티켓 정보 조회", description = "티켓 정보를 조회합니다")
public ApiResponse<TicketInfoResponse> getTicketInfo(@PathVariable("ticket_info_id") Long ticketInfoId) {
return ApiResponse.onSuccess(ticketService.getTicketInfo(ticketInfoId));
}

@GetMapping("/statistics")
@Operation(summary = "티켓 통계 조회", description = "티켓 통계를 조회합니다")
public ApiResponse<TicketStatisticsResponse> getTicketStatistics(@AuthenticationPrincipal AuthDetails authDetails) {
return ApiResponse.onSuccess(ticketService.getTicketStatistics(authDetails.user()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package kahlua.KahluaProject.dto.apply.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record ApplyStatisticsResponse(
@Schema(description = "총 지원자 수", example = "100")
Long totalApplyCount,

@Schema(description = "보컬 지원자 수", example = "20")
Long vocalCount,

@Schema(description = "보컬 지원자 비율", example = "20")
Long vocalPercent,

@Schema(description = "드럼 지원자 수", example = "20")
Long drumCount,

@Schema(description = "드럼 지원자 비율", example = "20")
Long drumPercent,

@Schema(description = "기타 지원자 수", example = "20")
Long guitarCount,

@Schema(description = "기타 지원자 비율", example = "20")
Long guitarPercent,

@Schema(description = "베이스 지원자 수", example = "20")
Long bassCount,

@Schema(description = "베이스 지원자 비율", example = "20")
Long bassPercent,

@Schema(description = "신디사이저 지원자 수", example = "20")
Long synthesizerCount,

@Schema(description = "신디사이저 지원자 비율", example = "20")
Long synthesizerPercent
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package kahlua.KahluaProject.dto.ticket.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.util.List;

@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record TicketStatisticsResponse(
@Schema(description = "티켓 상태별 수")
TicketStatusResponse ticketStatusCount,
@Schema(description = "그래프 정보")
GraphResponse graph,
@Schema(description = "총 수입", example = "1000000")
Long totalIncome
) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

하나의 클래스로 관련된 DTO를 관리하니까 프로젝트 구조가 간결해지는 것 같습니다
배워갑니다!!

@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record TicketStatusResponse(
@Schema(description = "총 티켓 수", example = "100")
Long totalTicketCount,
@Schema(description = "결제 대기 수", example = "20")
Long waitCount,
@Schema(description = "결제 완료 수", example = "20")
Long finishPaymentCount,
@Schema(description = "환불 요청 수", example = "20")
Long cancelRequestCount
) {
}
@Builder
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record GraphResponse(
@Schema(description = "일반 티켓 수", example = "80")
Long generalCount,
@Schema(description = "일반 티켓 비율", example = "80")
Long generalPercent,
@Schema(description = "신입생 티켓 수", example = "20")
Long freshmanCount,
@Schema(description = "신입생 티켓 비율", example = "20")
Long freshmanPercent
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import org.springframework.data.jpa.repository.JpaRepository;

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

public interface ApplyRepository extends JpaRepository<Apply, Long> {

List<Apply> findAllByFirstPreference(Preference first_preference);
List<Apply> findAllBySecondPreference(Preference second_preference);
Boolean existsByPhoneNum(String phone_num);
Optional<Long> countByDeletedAtIsNull();
Optional<Long> countByFirstPreferenceAndDeletedAtIsNull(Preference first_preference);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kahlua.KahluaProject.repository.ticket;

import kahlua.KahluaProject.domain.ticket.Status;
import kahlua.KahluaProject.domain.ticket.Ticket;
import kahlua.KahluaProject.domain.ticket.Type;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -16,4 +17,9 @@ public interface TicketRepository extends JpaRepository<Ticket, Long>, TicketCus
Optional<Ticket> findByReservationId(String reservationId);
List<Ticket> findAllByOrderByBuyerAscIdDesc();
List<Ticket> findAllByTypeOrderByBuyerAscIdDesc(Type type);

Optional<Long> countByStatusAndDeletedAtIsNull(Status status);
Optional<Long> countByTypeAndDeletedAtIsNull(Type type);
Optional<Long> countAllByDeletedAtIsNull();
Optional<Long> countByTypeAndStatusAndDeletedAtIsNull(Type type, Status status);
}
55 changes: 55 additions & 0 deletions src/main/java/kahlua/KahluaProject/service/ApplyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import kahlua.KahluaProject.dto.apply.response.*;
import kahlua.KahluaProject.dto.applyInfo.request.ApplyInfoRequest;
import kahlua.KahluaProject.dto.applyInfo.response.ApplyInfoResponse;
import kahlua.KahluaProject.dto.apply.response.ApplyStatisticsResponse;
import kahlua.KahluaProject.exception.GeneralException;
import kahlua.KahluaProject.repository.ApplyInfoRepository;
import kahlua.KahluaProject.repository.ApplyRepository;
Expand All @@ -20,7 +21,10 @@
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -168,4 +172,55 @@ public ApplyInfoResponse updateApplyInfo(Long applyInfoId, ApplyInfoRequest appl
//return: ApplyInfoResponse 타입으로 변환 후 반환
return ApplyConverter.toApplyInfoResponse(applyInfo);
}

public ApplyInfoResponse getApplyInfo(Long applyId) {
//business logic: applyInfo 데이터 조회
ApplyInfo applyInfo = applyInfoRepository.findById(applyId)
.orElseThrow(() -> new GeneralException(ErrorStatus.APPLY_INFO_NOT_FOUND));

//return: ApplyInfoResponse 타입으로 변환 후 반환
return ApplyConverter.toApplyInfoResponse(applyInfo);
}

public ApplyStatisticsResponse getApplyStatistics(User user) {
//validation: 관리자 권한 확인
if (user.getUserType() != UserType.ADMIN) {
throw new GeneralException(ErrorStatus.UNAUTHORIZED);
}

//business logic: 전체 지원자 수, 각 세션별 지원자 수 조회
Long totalApplyCount = applyRepository.countByDeletedAtIsNull()
.orElseThrow(() -> new GeneralException(ErrorStatus.APPLICANT_NOT_FOUND));

Map<Preference, Long> counts = Arrays.stream(Preference.values())
.collect(Collectors.toMap(
preference -> preference,
this::getCount
));

//return: ApplyStatisticsResponse 타입으로 변환 후 반환
return ApplyStatisticsResponse.builder()
.totalApplyCount(totalApplyCount)
.vocalCount(counts.get(Preference.VOCAL))
.vocalPercent(calculatePercent(counts.get(Preference.VOCAL), totalApplyCount))
.drumCount(counts.get(Preference.DRUM))
.drumPercent(calculatePercent(counts.get(Preference.DRUM), totalApplyCount))
.guitarCount(counts.get(Preference.GUITAR))
.guitarPercent(calculatePercent(counts.get(Preference.GUITAR), totalApplyCount))
.bassCount(counts.get(Preference.BASS))
.bassPercent(calculatePercent(counts.get(Preference.BASS), totalApplyCount))
.synthesizerCount(counts.get(Preference.SYNTHESIZER))
.synthesizerPercent(calculatePercent(counts.get(Preference.SYNTHESIZER), totalApplyCount))
.build();
}

private Long getCount(Preference preference) {
return applyRepository.countByFirstPreferenceAndDeletedAtIsNull(preference)
.orElseThrow(() -> new GeneralException(ErrorStatus.APPLICANT_NOT_FOUND));
}

private Long calculatePercent(Long count, Long total) {
return total > 0 ? (count * 100) / total : 0;
}

}
Loading