diff --git a/src/main/java/kahlua/KahluaProject/config/SecurityConfig.java b/src/main/java/kahlua/KahluaProject/config/SecurityConfig.java index a055155..ee51527 100644 --- a/src/main/java/kahlua/KahluaProject/config/SecurityConfig.java +++ b/src/main/java/kahlua/KahluaProject/config/SecurityConfig.java @@ -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; @@ -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/info/**").permitAll() + .requestMatchers(HttpMethod.GET, "/v1/admin/apply/info/**").permitAll() .requestMatchers("/v1/auth/sign-out/**", "v1/auth/recreate/**","/v1/user/**", "/v1/admin/**").authenticated() .requestMatchers("v1/reservation/**").hasAnyAuthority("KAHLUA", "ADMIN") .anyRequest().permitAll()) diff --git a/src/main/java/kahlua/KahluaProject/controller/adminController/AdminApplyController.java b/src/main/java/kahlua/KahluaProject/controller/adminController/AdminApplyController.java index ffd0758..ecef6f2 100644 --- a/src/main/java/kahlua/KahluaProject/controller/adminController/AdminApplyController.java +++ b/src/main/java/kahlua/KahluaProject/controller/adminController/AdminApplyController.java @@ -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; @@ -70,10 +71,22 @@ public ResponseEntity applyListToExcel() throws IOException .body(new InputStreamResource(in)); } - @PutMapping("/info/{apply_id}") + @PutMapping("/info/{apply_info_id}") @Operation(summary = "지원 정보 수정", description = "지원 정보를 수정합니다") - public ApiResponse updateApplyInfo(@PathVariable("apply_id") Long applyId, @RequestBody ApplyInfoRequest applyInfoRequest, @AuthenticationPrincipal AuthDetails authDetails) { + public ApiResponse 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 getApplyInfo(@PathVariable("apply_info_id") Long applyId) { + return ApiResponse.onSuccess(applyService.getApplyInfo(applyId)); + } + + @GetMapping("/statistics") + @Operation(summary = "지원자 통계 조회", description = "지원자 통계를 조회합니다") + public ApiResponse getApplyStatistics(@AuthenticationPrincipal AuthDetails authDetails) { + return ApiResponse.onSuccess(applyService.getApplyStatistics(authDetails.user())); + } } diff --git a/src/main/java/kahlua/KahluaProject/controller/adminController/AdminTicketController.java b/src/main/java/kahlua/KahluaProject/controller/adminController/AdminTicketController.java index 79750f8..f5dad74 100644 --- a/src/main/java/kahlua/KahluaProject/controller/adminController/AdminTicketController.java +++ b/src/main/java/kahlua/KahluaProject/controller/adminController/AdminTicketController.java @@ -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; @@ -104,4 +105,16 @@ public ResponseEntity participantsListToExcel() throws IOEx public ApiResponse 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 getTicketInfo(@PathVariable("ticket_info_id") Long ticketInfoId) { + return ApiResponse.onSuccess(ticketService.getTicketInfo(ticketInfoId)); + } + + @GetMapping("/statistics") + @Operation(summary = "티켓 통계 조회", description = "티켓 통계를 조회합니다") + public ApiResponse getTicketStatistics(@AuthenticationPrincipal AuthDetails authDetails) { + return ApiResponse.onSuccess(ticketService.getTicketStatistics(authDetails.user())); + } } \ No newline at end of file diff --git a/src/main/java/kahlua/KahluaProject/dto/apply/response/ApplyStatisticsResponse.java b/src/main/java/kahlua/KahluaProject/dto/apply/response/ApplyStatisticsResponse.java new file mode 100644 index 0000000..514987e --- /dev/null +++ b/src/main/java/kahlua/KahluaProject/dto/apply/response/ApplyStatisticsResponse.java @@ -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 +) { +} diff --git a/src/main/java/kahlua/KahluaProject/dto/ticket/response/TicketStatisticsResponse.java b/src/main/java/kahlua/KahluaProject/dto/ticket/response/TicketStatisticsResponse.java new file mode 100644 index 0000000..a816740 --- /dev/null +++ b/src/main/java/kahlua/KahluaProject/dto/ticket/response/TicketStatisticsResponse.java @@ -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 +) { + @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 + ) { + } +} diff --git a/src/main/java/kahlua/KahluaProject/repository/ApplyRepository.java b/src/main/java/kahlua/KahluaProject/repository/ApplyRepository.java index 623a945..76491e3 100644 --- a/src/main/java/kahlua/KahluaProject/repository/ApplyRepository.java +++ b/src/main/java/kahlua/KahluaProject/repository/ApplyRepository.java @@ -5,10 +5,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface ApplyRepository extends JpaRepository { List findAllByFirstPreference(Preference first_preference); List findAllBySecondPreference(Preference second_preference); Boolean existsByPhoneNum(String phone_num); + Optional countByDeletedAtIsNull(); + Optional countByFirstPreferenceAndDeletedAtIsNull(Preference first_preference); } diff --git a/src/main/java/kahlua/KahluaProject/repository/ticket/TicketRepository.java b/src/main/java/kahlua/KahluaProject/repository/ticket/TicketRepository.java index 66749b2..bbd955b 100644 --- a/src/main/java/kahlua/KahluaProject/repository/ticket/TicketRepository.java +++ b/src/main/java/kahlua/KahluaProject/repository/ticket/TicketRepository.java @@ -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; @@ -16,4 +17,9 @@ public interface TicketRepository extends JpaRepository, TicketCus Optional findByReservationId(String reservationId); List findAllByOrderByBuyerAscIdDesc(); List findAllByTypeOrderByBuyerAscIdDesc(Type type); + + Optional countByStatusAndDeletedAtIsNull(Status status); + Optional countByTypeAndDeletedAtIsNull(Type type); + Optional countAllByDeletedAtIsNull(); + Optional countByTypeAndStatusAndDeletedAtIsNull(Type type, Status status); } diff --git a/src/main/java/kahlua/KahluaProject/service/ApplyService.java b/src/main/java/kahlua/KahluaProject/service/ApplyService.java index 57e5f4e..1de8856 100644 --- a/src/main/java/kahlua/KahluaProject/service/ApplyService.java +++ b/src/main/java/kahlua/KahluaProject/service/ApplyService.java @@ -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; @@ -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 @@ -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 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; + } + } diff --git a/src/main/java/kahlua/KahluaProject/service/TicketService.java b/src/main/java/kahlua/KahluaProject/service/TicketService.java index e43e27e..d22750b 100644 --- a/src/main/java/kahlua/KahluaProject/service/TicketService.java +++ b/src/main/java/kahlua/KahluaProject/service/TicketService.java @@ -23,10 +23,7 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Random; +import java.util.*; import java.util.stream.Collectors; @Service @@ -112,7 +109,7 @@ public TicketUpdateResponse requestCancelTicket(String reservationId) { @Transactional public TicketUpdateResponse completeCancelTicket(User user, Long ticketId) { - if(user.getUserType() != UserType.ADMIN){ + if (user.getUserType() != UserType.ADMIN) { throw new GeneralException(ErrorStatus.UNAUTHORIZED); } @@ -140,7 +137,7 @@ public TicketGetResponse viewTicket(String reservationId) { // 어드민 페이지 티켓 리스트 조회 public TicketListResponse getTicketList(User user, String sortBy) { - if(user.getUserType() != UserType.ADMIN){ + if (user.getUserType() != UserType.ADMIN) { throw new GeneralException(ErrorStatus.UNAUTHORIZED); } @@ -174,7 +171,7 @@ public TicketListResponse getTicketList(User user, String sortBy) { // 일반티켓 // 전공, 뒷풀이 참석 여부 null - if (ticket.getType() == Type.GENERAL){ + if (ticket.getType() == Type.GENERAL) { TicketItemResponse ticketItemResponse = TicketItemResponse.builder() .id(ticket.getId()) .status(ticket.getStatus()) @@ -217,7 +214,7 @@ else if (ticket.getType() == Type.FRESHMAN) { // 일반 티켓 리스트 조회 public TicketListResponse getGeneralTicketList(User user, String sortBy) { - if(user.getUserType() != UserType.ADMIN){ + if (user.getUserType() != UserType.ADMIN) { throw new GeneralException(ErrorStatus.UNAUTHORIZED); } @@ -271,7 +268,7 @@ public TicketListResponse getGeneralTicketList(User user, String sortBy) { // 신입생 티켓 리스트 조회 public TicketListResponse getFreshmanTicketList(User user, String sortBy) { - if(user.getUserType() != UserType.ADMIN){ + if (user.getUserType() != UserType.ADMIN) { throw new GeneralException(ErrorStatus.UNAUTHORIZED); } @@ -321,7 +318,7 @@ public String uniqueReservationId() { String reservationId; do { reservationId = generateReservationId(); - } while(ticketRepository.existsByReservationId(reservationId)); + } while (ticketRepository.existsByReservationId(reservationId)); return reservationId; } @@ -331,11 +328,11 @@ public String generateReservationId() { Random random = new Random(); List idList = new ArrayList<>(); - for (int i=0; i new GeneralException(ErrorStatus.TICKET_NOT_FOUND)); + + //return: ticket 정보 반환 + return TicketConverter.toTicketInfoResponse(ticketInfo); + } + + public TicketStatisticsResponse getTicketStatistics(User user) { + //validation: user가 admin인지 확인 + if (user.getUserType() != UserType.ADMIN) { + throw new GeneralException(ErrorStatus.UNAUTHORIZED); + } + + //business logic: 티켓 통계 조회 + Long totalTicket = ticketRepository.countAllByDeletedAtIsNull() + .orElseThrow(() -> new GeneralException(ErrorStatus.TICKET_NOT_FOUND)); + + Map statusCounts = Arrays.stream(Status.values()) + .collect(Collectors.toMap( + status -> status, + status -> ticketRepository.countByStatusAndDeletedAtIsNull(status) + .orElseThrow(() -> new GeneralException(ErrorStatus.TICKET_NOT_FOUND)) + )); + + Long generalTicket = ticketRepository.countByTypeAndDeletedAtIsNull(Type.GENERAL) + .orElseThrow(() -> new GeneralException(ErrorStatus.TICKET_NOT_FOUND)); + Long freshmanTicket = ticketRepository.countByTypeAndDeletedAtIsNull(Type.FRESHMAN) + .orElseThrow(() -> new GeneralException(ErrorStatus.TICKET_NOT_FOUND)); + + Long totalIncome = ticketRepository.countByTypeAndStatusAndDeletedAtIsNull(Type.GENERAL, Status.FINISH_PAYMENT) + .orElseThrow(() -> new GeneralException(ErrorStatus.TICKET_NOT_FOUND)) * 5000L; + + //return: 티켓 통계 정보 반환 + return TicketStatisticsResponse.builder() + .ticketStatusCount(TicketStatisticsResponse.TicketStatusResponse.builder() + .totalTicketCount(totalTicket) + .waitCount(statusCounts.get(Status.WAIT)) + .finishPaymentCount(statusCounts.get(Status.FINISH_PAYMENT)) + .cancelRequestCount(statusCounts.get(Status.CANCEL_REQUEST)) + .build()) + .graph(TicketStatisticsResponse.GraphResponse.builder() + .generalCount(generalTicket) + .generalPercent(calculatePercent(generalTicket, totalTicket)) + .freshmanCount(freshmanTicket) + .freshmanPercent(calculatePercent(freshmanTicket, totalTicket)) + .build()) + .totalIncome(totalIncome) + .build(); + } + + private Long calculatePercent(Long count, Long total) { + return total > 0 ? (count * 100) / total : 0; + } }