diff --git "a/.github/ISSUE_TEMPLATE/\354\275\224\353\223\234 \354\225\210\354\240\225\354\204\261.md" "b/.github/ISSUE_TEMPLATE/\354\275\224\353\223\234 \354\225\210\354\240\225\354\204\261.md" new file mode 100644 index 00000000..6b174b79 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\354\275\224\353\223\234 \354\225\210\354\240\225\354\204\261.md" @@ -0,0 +1,64 @@ +--- +name: 코드 안정성 +about: 코드 안정성 대한 요청 +title: "[🛠 코드 안정성] " +labels: 코드 안정성 +assignees: "" +--- + +## ✨ 문제 요약 + +현재 발생한 문제나 개선이 필요한 부분에 대해 한 줄로 명확하게 요약해주세요. + +---- + +- + +## 🐛 어떤 문제가 발생했나요? + +자세한 문제 상황을 설명해주세요. 버그, 성능 저하, 코드의 문제점(Code Smell) 등 어떤 종류의 안정성 문제인지 구체적으로 서술합니다. + +---- + +- + +## 🕹️ 어떻게 재현할 수 있나요? + +문제를 재현할 수 있는 구체적인 순서를 단계별로 작성해주세요. +만약 재현이 어렵다면, 어떤 상황에서 주로 발생하는지 설명해주세요. (ex. 특정 API에 요청이 몰릴 때) + +---- + +1. `이 단계에서는...` +2. `그 다음에는...` +3. `그러면 문제가 발생합니다.` + +## 🤔 기대 결과와 실제 결과는 무엇이었나요? + +- **기대 결과**: 원래라면 어떻게 동작해야 했는지 설명해주세요. +- **실제 결과**: 실제로 어떻게 동작했는지, 어떤 오류가 발생했는지 설명해주세요. + +---- + +- **기대 결과**: +- **실제 결과**: + +## 📎 근거 자료 + +문제 상황을 파악하는 데 도움이 되는 모든 자료를 첨부해주세요. + +- **에러 로그**: `(에러 로그를 여기에 붙여넣어 주세요)` +- **스크린샷**: `(스크린샷 이미지 첨부)` +- **관련 코드**: 문제가 발생한 코드의 일부 또는 파일 경로 + +---- + +- + +## 📝 추가 사항 + +이슈 해결에 도움이 될 만한 추가 정보나 고려해야 할 조건이 있다면 자유롭게 기입해주세요. + +---- + +- \ No newline at end of file diff --git a/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java b/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java index a173041d..413da653 100644 --- a/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java +++ b/src/main/java/life/mosu/mosuserver/application/application/ApplicationService.java @@ -19,6 +19,7 @@ import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse; import life.mosu.mosuserver.presentation.application.dto.CreateApplicationResponse; import life.mosu.mosuserver.presentation.application.dto.ExamApplicationRequest; +import life.mosu.mosuserver.presentation.application.dto.SchoolApplicationCountResponse; import life.mosu.mosuserver.presentation.common.FileRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -103,4 +104,9 @@ private CreateApplicationResponse handleApplication( public List getApplications(Long userId) { return getApplicationsStepProcessor.process(userId); } + + @Transactional(readOnly = true) + public List getPaidApplicationCountBySchool() { + return applicationJpaRepository.findPaidApplicationCountBySchool(); + } } diff --git a/src/main/java/life/mosu/mosuserver/application/application/cron/ApplicationFailureLogDomainArchiveExecutor.java b/src/main/java/life/mosu/mosuserver/application/application/cron/ApplicationFailureLogDomainArchiveExecutor.java index a219a7a0..52c99abe 100644 --- a/src/main/java/life/mosu/mosuserver/application/application/cron/ApplicationFailureLogDomainArchiveExecutor.java +++ b/src/main/java/life/mosu/mosuserver/application/application/cron/ApplicationFailureLogDomainArchiveExecutor.java @@ -22,7 +22,7 @@ @RequiredArgsConstructor public class ApplicationFailureLogDomainArchiveExecutor implements DomainArchiveExecutor { - private final static Duration DURATION_HOURS_STANDARD = Duration.ofHours(1); + private final static Duration DURATION_HOURS_STANDARD = Duration.ofHours(48); private final static int BATCH_SIZE = 500; private final ApplicationFailureLogFactory applicationFailureLogFactory; @@ -63,7 +63,7 @@ public String getName() { } private List findFailedApplications() { - Instant threshold = Instant.now().minus(DURATION_HOURS_STANDARD); // 3 hours ago + Instant threshold = Instant.now().minus(DURATION_HOURS_STANDARD); LocalDateTime time = LocalDateTime.ofInstant(threshold, ZoneId.systemDefault()); return applicationJpaRepository.findFailedApplications(time); } diff --git a/src/main/java/life/mosu/mosuserver/domain/application/repository/ApplicationJpaRepository.java b/src/main/java/life/mosu/mosuserver/domain/application/repository/ApplicationJpaRepository.java index 38613467..09c419b8 100644 --- a/src/main/java/life/mosu/mosuserver/domain/application/repository/ApplicationJpaRepository.java +++ b/src/main/java/life/mosu/mosuserver/domain/application/repository/ApplicationJpaRepository.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.List; import life.mosu.mosuserver.domain.application.entity.ApplicationJpaEntity; +import life.mosu.mosuserver.presentation.application.dto.SchoolApplicationCountResponse; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -29,6 +30,21 @@ AND a.status IN ('PENDING', 'ABORT') """) List findAllByUserId(@Param("userId") Long userId); + @Query(""" + SELECT new life.mosu.mosuserver.presentation.application.dto.SchoolApplicationCountResponse( + e.schoolName, + COUNT(a.id) + ) + FROM ApplicationJpaEntity a + JOIN ExamApplicationJpaEntity ea ON ea.applicationId = a.id + JOIN ExamJpaEntity e ON e.id = ea.examId + WHERE a.deleted = false + AND a.status = 'APPROVED' + GROUP BY e.schoolName + ORDER BY e.schoolName + """) + List findPaidApplicationCountBySchool(); + @Modifying @Query(value = """ UPDATE application a diff --git a/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java b/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java index f205c74e..c061a7bd 100644 --- a/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java +++ b/src/main/java/life/mosu/mosuserver/global/filter/Whitelist.java @@ -50,8 +50,8 @@ public enum Whitelist { //USER find-password USER_FIND_PASSWORD("/api/v1/user/me/find-password", WhitelistMethod.POST), - APPLICATION_GUEST("/api/v1/applications/guest", WhitelistMethod.ALL); - + APPLICATION_GUEST("/api/v1/applications/guest", WhitelistMethod.ALL), + APPLICATION_PAID("/api/v1/applications/schools/paid-count",WhitelistMethod.ALL); private static final List AUTH_REQUIRED_EXCEPTIONS = List.of( new ExceptionRule("/api/v1/exam-application", WhitelistMethod.GET) ); diff --git a/src/main/java/life/mosu/mosuserver/infra/cron/job/LogCleanupJob.java b/src/main/java/life/mosu/mosuserver/infra/cron/job/LogCleanupJob.java index 99f34460..f980e643 100644 --- a/src/main/java/life/mosu/mosuserver/infra/cron/job/LogCleanupJob.java +++ b/src/main/java/life/mosu/mosuserver/infra/cron/job/LogCleanupJob.java @@ -1,11 +1,7 @@ package life.mosu.mosuserver.infra.cron.job; -import java.time.LocalDateTime; -import java.util.List; -import life.mosu.mosuserver.global.support.cron.LogCleanupExecutor; import life.mosu.mosuserver.infra.cron.annotation.CronJob; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; @@ -17,22 +13,22 @@ name = "logCleanupJob" ) @DisallowConcurrentExecution -@RequiredArgsConstructor +//@RequiredArgsConstructor public class LogCleanupJob implements Job { - private final List cleanups; +// private final List cleanups; @Override public void execute(JobExecutionContext context) { - LocalDateTime threshold = LocalDateTime.now().minusMonths(3); - for (LogCleanupExecutor cleanup : cleanups) { - try { - int deleted = cleanup.deleteLogsBefore(threshold); - log.info("[LogCleanupJob] Deleted total {} logs older than {}", deleted, threshold); - } catch (Exception e) { - log.error("[LogCleanupJob] Error during log cleanup: {}", - cleanup.getClass().getSimpleName(), e); - } - } +// LocalDateTime threshold = LocalDateTime.now().minusMonths(3); +// for (LogCleanupExecutor cleanup : cleanups) { +// try { +// int deleted = cleanup.deleteLogsBefore(threshold); +// log.info("[LogCleanupJob] Deleted total {} logs older than {}", deleted, threshold); +// } catch (Exception e) { +// log.error("[LogCleanupJob] Error during log cleanup: {}", +// cleanup.getClass().getSimpleName(), e); +// } +// } } } diff --git a/src/main/java/life/mosu/mosuserver/presentation/application/ApplicationController.java b/src/main/java/life/mosu/mosuserver/presentation/application/ApplicationController.java index 3bf89b45..6e73b868 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/application/ApplicationController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/application/ApplicationController.java @@ -10,6 +10,7 @@ import life.mosu.mosuserver.presentation.application.dto.ApplicationRequest; import life.mosu.mosuserver.presentation.application.dto.ApplicationResponse; import life.mosu.mosuserver.presentation.application.dto.CreateApplicationResponse; +import life.mosu.mosuserver.presentation.application.dto.SchoolApplicationCountResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -74,4 +75,12 @@ public ResponseEntity>> getApplicat return ResponseEntity.ok( ApiResponseWrapper.success(HttpStatus.OK, "신청 내역 조회 성공", responses)); } + + //학교별 결제된 신청 수 조회 + @GetMapping("/schools/paid-count") + public ResponseEntity>> getPaidApplicationCountBySchool() { + List responses = applicationService.getPaidApplicationCountBySchool(); + return ResponseEntity.ok( + ApiResponseWrapper.success(HttpStatus.OK, "학교별 결제된 신청 수 조회 성공", responses)); + } } diff --git a/src/main/java/life/mosu/mosuserver/presentation/application/dto/SchoolApplicationCountResponse.java b/src/main/java/life/mosu/mosuserver/presentation/application/dto/SchoolApplicationCountResponse.java new file mode 100644 index 00000000..ea5799d2 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/presentation/application/dto/SchoolApplicationCountResponse.java @@ -0,0 +1,13 @@ +package life.mosu.mosuserver.presentation.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "학교별 결제된 신청 수 응답") +public record SchoolApplicationCountResponse( + @Schema(description = "학교명", example = "서울고등학교") + String schoolName, + + @Schema(description = "결제된 신청 수", example = "25") + Long paidApplicationCount +) { +} diff --git a/src/test/java/life/mosu/mosuserver/infra/cron/job/LogCleanupJobTest.java b/src/test/java/life/mosu/mosuserver/infra/cron/job/LogCleanupJobTest.java deleted file mode 100644 index 2a1abc93..00000000 --- a/src/test/java/life/mosu/mosuserver/infra/cron/job/LogCleanupJobTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package life.mosu.mosuserver.infra.cron.job; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.times; - -import java.time.LocalDateTime; -import java.util.List; -import life.mosu.mosuserver.global.support.cron.LogCleanupExecutor; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.quartz.JobExecutionContext; - -@ExtendWith(MockitoExtension.class) -class LogCleanupJobTest { - - @Mock - private LogCleanupExecutor logCleanupExecutor1; - - @Mock - private LogCleanupExecutor logCleanupExecutor2; - - @Mock - private JobExecutionContext jobExecutionContext; - - @InjectMocks - private LogCleanupJob logCleanupJob; - - @Test - @DisplayName("로그 정리 작업 - 성공") - void execute_Success() { - // given - List cleanups = List.of(logCleanupExecutor1, logCleanupExecutor2); - LogCleanupJob job = new LogCleanupJob(cleanups); - - given(logCleanupExecutor1.deleteLogsBefore(any(LocalDateTime.class))).willReturn(100); - given(logCleanupExecutor2.deleteLogsBefore(any(LocalDateTime.class))).willReturn(50); - - // when - job.execute(jobExecutionContext); - - // then - then(logCleanupExecutor1).should(times(1)).deleteLogsBefore(any(LocalDateTime.class)); - then(logCleanupExecutor2).should(times(1)).deleteLogsBefore(any(LocalDateTime.class)); - } - - @Test - @DisplayName("로그 정리 작업 - 일부 실패해도 계속 진행") - void execute_PartialFailure() { - // given - List cleanups = List.of(logCleanupExecutor1, logCleanupExecutor2); - LogCleanupJob job = new LogCleanupJob(cleanups); - - doThrow(new RuntimeException("첫 번째 정리 실패")) - .when(logCleanupExecutor1).deleteLogsBefore(any(LocalDateTime.class)); - given(logCleanupExecutor2.deleteLogsBefore(any(LocalDateTime.class))).willReturn(50); - - // when - job.execute(jobExecutionContext); - - // then - then(logCleanupExecutor1).should(times(1)).deleteLogsBefore(any(LocalDateTime.class)); - then(logCleanupExecutor2).should(times(1)).deleteLogsBefore(any(LocalDateTime.class)); - } - - @Test - @DisplayName("로그 정리 작업 - 빈 목록") - void execute_EmptyList() { - // given - List cleanups = List.of(); - LogCleanupJob job = new LogCleanupJob(cleanups); - - // when - job.execute(jobExecutionContext); - - // then - // 예외가 발생하지 않음을 확인 - } -}