-
Notifications
You must be signed in to change notification settings - Fork 0
[UPLUS-121] feat : Cloud Function API 로직 수정 및 DTO 수정 #38
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
Changes from all commits
9289a8b
5903984
b0ec20c
7e1c022
917e563
2c22171
2f0fb85
5374e5d
6aa96c7
da97a76
6c57eba
30d0921
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,21 @@ | ||
| package com.project.core.controller.dto.response; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| public class BatchScheduleResponse { | ||
|
|
||
| private String jobType; // INVOICE | ||
| private String scheduleName; // projects/.../jobs/... | ||
| private String cron; // 15 12 26 1 * | ||
| private String invMonth; // null 가능 | ||
| private String description; // Auto-created/Updated via Batch Control API | ||
| private String state; // ENABLED / DISABLED | ||
| private String timeZone; // Asia/Seoul | ||
|
|
||
| public boolean isEnabled() { | ||
| return "ENABLED".equalsIgnoreCase(state); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ public List<BatchFailureLogDto> findRecentFailures(int limit) { | |
| JOIN BATCH_JOB_EXECUTION je | ||
| ON ji.JOB_INSTANCE_ID = je.JOB_INSTANCE_ID | ||
| WHERE je.STATUS = 'FAILED' | ||
| AND ji.JOB_NAME IN ('invoiceJob', 'invoiceItemJob') | ||
|
Contributor
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. SQL 쿼리 내에 'invoiceJob', 'invoiceItemJob'과 같은 잡 이름이 하드코딩되어 있습니다. 이는 유지보수를 어렵게 만들 수 있습니다. 예를 들어, 다음과 같이 // 쿼리 수정
String sql =
"""
...
AND ji.JOB_NAME IN (?, ?)
...
LIMIT ?
""";
// jdbcTemplate 호출 수정
jdbcTemplate.query(sql, rowMapper, BatchJobType.INVOICE_JOB.getJobName(), BatchJobType.INVOICE_ITEM_JOB.getJobName(), limit); |
||
| ORDER BY je.END_TIME DESC | ||
| LIMIT ? | ||
| """; | ||
|
|
||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,25 @@ | ||
| package com.project.core.service; | ||
|
|
||
| import java.time.ZoneId; | ||
| import java.time.ZonedDateTime; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
|
|
||
| import org.springframework.scheduling.support.CronExpression; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import com.cronutils.descriptor.CronDescriptor; | ||
| import com.cronutils.model.Cron; | ||
| import com.cronutils.model.CronType; | ||
| import com.cronutils.model.definition.CronDefinitionBuilder; | ||
| import com.cronutils.parser.CronParser; | ||
| import com.project.core.controller.dto.BatchSummaryDto; | ||
| import com.project.core.controller.dto.GrafanaInfoDto; | ||
| import com.project.core.controller.dto.response.BatchScheduleResponse; | ||
| import com.project.core.controller.dto.response.BillingDashboardResponse; | ||
| import com.project.core.infra.repository.invoice.BatchFailureQueryRepository; | ||
| import com.project.core.infra.repository.invoice.BatchJobQueryRepository; | ||
| import com.project.core.util.BatchScheduleClient; | ||
| import com.project.global.config.BillingProperties; | ||
| import com.project.global.exception.code.domain.core.CoreErrorCode; | ||
| import com.project.global.exception.core.EntityNotFoundException; | ||
|
|
@@ -20,13 +34,21 @@ public class InvoiceService { | |
|
|
||
| private final BatchJobQueryRepository batchJobQueryRepository; | ||
| private final BatchFailureQueryRepository batchFailureQueryRepository; | ||
| private final BatchScheduleClient batchScheduleClient; | ||
| private final BillingProperties billingProperties; | ||
|
|
||
| public BillingDashboardResponse getDashboard() { | ||
|
|
||
| try { | ||
| // 1️⃣ 최신 배치 실행 요약 조회 | ||
| List<BatchSummaryDto> jobs = batchJobQueryRepository.findLatestJobSummaries(); | ||
|
|
||
| // 2️⃣ Cloud Function 스케줄 정보 결합 | ||
| List<BatchSummaryDto> enrichedJobs = | ||
| jobs.stream().map(this::enrichWithSchedule).toList(); | ||
|
|
||
| return BillingDashboardResponse.builder() | ||
| .batchJobs(batchJobQueryRepository.findLatestJobSummaries()) | ||
| .batchJobs(enrichedJobs) | ||
| .grafana( | ||
| GrafanaInfoDto.builder() | ||
| .iframeUrl(billingProperties.getIframeUrl()) | ||
|
|
@@ -36,8 +58,62 @@ public BillingDashboardResponse getDashboard() { | |
| .build(); | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("[BillingDashboard] 대시보드 빌딩에 실패했습니다.", e); | ||
| log.error("[BillingDashboard] 대시보드 빌딩 실패", e); | ||
| throw new EntityNotFoundException(CoreErrorCode.DASHBOARD_NOT_FOUND); | ||
| } | ||
| } | ||
|
|
||
| /** 배치 실행 요약 + Cloud Function 스케줄 정보 결합 */ | ||
| private BatchSummaryDto enrichWithSchedule(BatchSummaryDto job) { | ||
|
|
||
| return batchScheduleClient | ||
| .getSchedule(job.getJobName()) | ||
| .map(schedule -> applySchedule(job, schedule)) | ||
| .orElseGet(() -> job.toBuilder().cronDescription("수동 실행").build()); | ||
| } | ||
|
|
||
| /** cron / 설명 / 다음 실행 시각 계산 (최종) */ | ||
| private BatchSummaryDto applySchedule(BatchSummaryDto job, BatchScheduleResponse schedule) { | ||
|
|
||
| // 1️⃣ 비활성화 스케줄 | ||
| if (!schedule.isEnabled() || schedule.getCron() == null) { | ||
| return job.toBuilder().cronDescription("비활성화").build(); | ||
| } | ||
|
|
||
| String cronStr = schedule.getCron(); | ||
| String timeZone = schedule.getTimeZone(); // ex) Asia/Seoul | ||
|
|
||
| try { | ||
| // 2️⃣ 타임존 (Cloud Function 기준) | ||
| ZoneId zoneId = ZoneId.of(timeZone); | ||
|
|
||
| // ✅ Spring용 크론 보정 (6필드) | ||
| String springCronStr = cronStr; | ||
| if (cronStr.split(" ").length == 5) { | ||
| springCronStr = "0 " + cronStr; | ||
| } | ||
|
|
||
| CronExpression springCron = CronExpression.parse(springCronStr); | ||
|
|
||
| ZonedDateTime nextFire = springCron.next(ZonedDateTime.now(zoneId)); | ||
|
Contributor
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.
// In a @Configuration class
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
// In InvoiceService
@RequiredArgsConstructor
public class InvoiceService {
private final Clock clock;
// ...
private BatchSummaryDto applySchedule(...) {
// ...
ZonedDateTime nextFire = springCron.next(ZonedDateTime.now(clock).withZoneSameInstant(zoneId));
// ...
}
}테스트에서는 |
||
|
|
||
| // ✅ cron-utils (UNIX) | ||
| CronParser parser = | ||
| new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX)); | ||
| Cron cron = parser.parse(cronStr); | ||
|
|
||
| String cronDescription = CronDescriptor.instance(Locale.KOREAN).describe(cron); | ||
|
Comment on lines
+101
to
+105
Contributor
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.
public class InvoiceService {
private static final CronParser UNIX_CRON_PARSER = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX));
private static final CronDescriptor CRON_DESCRIPTOR_KOREAN = CronDescriptor.instance(Locale.KOREAN);
// ...
private BatchSummaryDto applySchedule(...) {
// ...
try {
// ...
Cron cron = UNIX_CRON_PARSER.parse(cronStr);
String cronDescription = CRON_DESCRIPTOR_KOREAN.describe(cron);
// ...
} catch (Exception e) {
// ...
}
}
} |
||
|
|
||
| return job.toBuilder() | ||
| .cronExpression(cronStr) | ||
| .cronDescription(cronDescription) | ||
| .nextFireTime(nextFire != null ? nextFire.toLocalDateTime() : null) | ||
| .build(); | ||
|
|
||
| } catch (Exception e) { | ||
| log.warn("[BillingDashboard] cron 처리 실패 job={}, cron={}", job.getJobName(), cronStr, e); | ||
|
|
||
| return job.toBuilder().cronExpression(cronStr).cronDescription("잘못된 크론식").build(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.project.core.util; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.client.HttpStatusCodeException; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
| import com.project.core.controller.dto.response.BatchScheduleResponse; | ||
| import com.project.core.infra.entity.batch.BatchJobType; | ||
| import com.project.global.config.CloudFunctionProperties; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class BatchScheduleClient { | ||
|
|
||
| private final RestTemplate restTemplate; | ||
| private final CloudFunctionProperties properties; | ||
|
|
||
| public Optional<BatchScheduleResponse> getSchedule(String jobName) { | ||
|
|
||
| String title = BatchJobType.getTitleByJobName(jobName); | ||
| String url = properties.getBaseUrl() + "/schedule?job=" + title; | ||
|
Contributor
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. |
||
|
|
||
| try { | ||
| ResponseEntity<BatchScheduleResponse> response = | ||
| restTemplate.getForEntity(url, BatchScheduleResponse.class); | ||
|
|
||
| return Optional.ofNullable(response.getBody()); | ||
|
|
||
| } catch (HttpStatusCodeException e) { | ||
| log.warn( | ||
| "[BatchSchedule] 조회 실패 job={}, status={}, body={}", | ||
| jobName, | ||
| e.getStatusCode(), | ||
| e.getResponseBodyAsString()); | ||
| return Optional.empty(); | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("[BatchSchedule] 조회 중 오류 job={}", jobName, e); | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isEnabled()메서드에서 "ENABLED" 문자열을 직접 사용하고 있습니다. 이런 매직 스트링은 오타를 유발하거나 일관성을 해칠 수 있습니다. 상태 값을 나타내는 상수를 선언하여 사용하는 것이 좋습니다.