diff --git a/batch-quartz/src/main/java/com/spring/common/validation/CronExpressionValidator.java b/batch-quartz/src/main/java/com/spring/common/validation/CronExpressionValidator.java new file mode 100644 index 0000000..d9303c7 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/common/validation/CronExpressionValidator.java @@ -0,0 +1,32 @@ +package com.spring.common.validation; + +import java.text.ParseException; +import java.util.Date; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.quartz.CronExpression; + +public class CronExpressionValidator implements ConstraintValidator { + + @Override + public boolean isValid(String cronExpression, ConstraintValidatorContext context) { + if (cronExpression == null || cronExpression.isEmpty()) { + return false; + } + boolean result = false; + if(CronExpression.isValidExpression(cronExpression)){ + try { + CronExpression targetExpression = new CronExpression(cronExpression); + if (targetExpression.getNextValidTimeAfter(new Date(System.currentTimeMillis())) != null) { + result = true; + } + } catch (ParseException e) { + result = false; + } + } + return result; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/common/validation/ValidCronExpression.java b/batch-quartz/src/main/java/com/spring/common/validation/ValidCronExpression.java new file mode 100644 index 0000000..c74baa4 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/common/validation/ValidCronExpression.java @@ -0,0 +1,20 @@ +package com.spring.common.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Documented +@Constraint(validatedBy = CronExpressionValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidCronExpression { + String message() default "Invalid cron expression"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java b/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java index 618af64..2797941 100644 --- a/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java +++ b/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java @@ -10,19 +10,27 @@ import org.springframework.stereotype.Component; import com.spring.infra.batch.AbstractBatchTask; import com.spring.infra.batch.BatchJobInfo; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import com.spring.domain.post.repository.PostRepository; + @Slf4j @Component @BatchJobInfo( group = "EMAIL", jobName = "emailSendJob", - cronExpression = "*/10 * * * * ?" + cronExpression = "*/10 * * * * ?", + description = "이메일배치작업" ) +@RequiredArgsConstructor public class EmailSendBatch extends AbstractBatchTask { + private final PostRepository postRepository; + @Override protected List createSteps() { + log.info("EmailSendBatch -> createSteps"); return List.of( addStep("emailSendJobStep1", createTasklet()), addStep("emailSendJobStep2", createSendTasklet()) @@ -31,10 +39,15 @@ public class EmailSendBatch extends AbstractBatchTask { @Override protected Tasklet createTasklet() { - return ((contribution, chunkContext) -> RepeatStatus.FINISHED); + log.info("EmailSendBatch -> createTasklet"); + return ((contribution, chunkContext) -> { + postRepository.findAll(); + return RepeatStatus.FINISHED; + }); } private Tasklet createSendTasklet() { + log.info("EmailSendBatch -> createSendTasklet"); return ((contribution, chunkContext) -> RepeatStatus.FINISHED); } diff --git a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java index 4f73b30..e29d90f 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java @@ -28,11 +28,11 @@ public class PostCreateBatch extends AbstractBatchTask { private final PostMapper postMapper; - // @Autowired - // @Override - // public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) { - // super.setTransactionManager(transactionManager); - // } + @Autowired + @Override + public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) { + super.setTransactionManager(transactionManager); + } @Override protected Tasklet createTasklet() { diff --git a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java index 5b4363b..c4b5955 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java @@ -42,15 +42,17 @@ import lombok.extern.slf4j.Slf4j; public class PostCreateBatchChunk { private final JobRepository jobRepository; + @Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) private final PlatformTransactionManager transactionManager; + @Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY) private final EntityManagerFactory entityManagerFactory; private final PostRepository postRepository; private final PostBackUpRepository postBackUpRepository; private List list = new ArrayList<>(); - @QuartzJob(group = "POST", jobName = "testPostJob", cronExpression = "0/30 * * * * ?") @Bean + @QuartzJob(group = "POST", jobName = "testPostJob", cronExpression = "0/50 * * * * ?", description = "테스트배치작업") Job testPostJob() { return new JobBuilder("testPostJob") .repository(jobRepository) @@ -63,8 +65,7 @@ public class PostCreateBatchChunk { .build(); } - @Bean - JobExecutionDecider decider() { + private JobExecutionDecider decider() { return (JobExecution jobExecution, StepExecution stepExecution) -> !list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE"); } diff --git a/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java b/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java index e09871e..614a44b 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java @@ -14,7 +14,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -// @DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) @Entity @Table(name = "APP_POST") @Getter diff --git a/batch-quartz/src/main/java/com/spring/domain/post/entity/PostBackUp.java b/batch-quartz/src/main/java/com/spring/domain/post/entity/PostBackUp.java index 8b90e6c..989a5b0 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/entity/PostBackUp.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/entity/PostBackUp.java @@ -12,7 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -// @DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) @Entity @Table(name = "APP_POST_BACKUP") @Getter diff --git a/batch-quartz/src/main/java/com/spring/domain/post/mapper/PostMapper.java b/batch-quartz/src/main/java/com/spring/domain/post/mapper/PostMapper.java index 489d1c3..40699ca 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/mapper/PostMapper.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/mapper/PostMapper.java @@ -5,10 +5,9 @@ import java.util.List; import org.apache.ibatis.annotations.Param; import com.spring.domain.post.entity.Post; -import com.spring.infra.db.orm.mybatis.annotation.PrimaryMapper; -// import com.spring.infra.db.orm.mybatis.annotation.SecondaryMapper; +import com.spring.infra.db.orm.mybatis.annotation.SecondaryMapper; -@PrimaryMapper +@SecondaryMapper public interface PostMapper { List findAll(); void save(@Param("post") Post post); diff --git a/batch-quartz/src/main/java/com/spring/domain/post/repository/PostBackUpRepository.java b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostBackUpRepository.java index 8d9cabb..b2189b3 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/repository/PostBackUpRepository.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostBackUpRepository.java @@ -6,7 +6,7 @@ import com.spring.domain.post.entity.PostBackUp; import com.spring.infra.db.SecondaryDataSourceConfig; import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector; -// @DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) public interface PostBackUpRepository extends JpaRepository { } diff --git a/batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java index 72c4cc3..e1e19d4 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostRepository.java @@ -6,7 +6,7 @@ import com.spring.domain.post.entity.Post; import com.spring.infra.db.SecondaryDataSourceConfig; import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector; -// @DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) public interface PostRepository extends JpaRepository { } diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/api/DashBoardApi.java b/batch-quartz/src/main/java/com/spring/domain/schedule/api/DashBoardApi.java index 370d439..1b6e72e 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/api/DashBoardApi.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/api/DashBoardApi.java @@ -4,6 +4,7 @@ import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.spring.domain.schedule.dto.BatchChartResponse; @@ -21,9 +22,9 @@ public class DashBoardApi { private final BatchChartService batchChartService; private final DashBoardJobService dashBoardJobService; - @GetMapping("/chart/batch") - public BatchChartResponse getBatchJobExecutionData() { - return batchChartService.getBatchJobExecutionData(); + @GetMapping("/chart") + public BatchChartResponse getBatchJobExecutionData(@RequestParam int year, @RequestParam int month) { + return batchChartService.getBatchJobExecutionData(year, month); } @GetMapping("/recent-job") diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java b/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java index d079101..0b49dde 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/api/ScheduleJobApi.java @@ -2,6 +2,8 @@ package com.spring.domain.schedule.api; import java.util.List; +import javax.validation.Valid; + import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -53,7 +55,7 @@ public class ScheduleJobApi { } @PostMapping("/reschedule") - public boolean rescheduleJob(@RequestBody ReScheduleJobRequest request) { + public boolean rescheduleJob(@Valid @RequestBody ReScheduleJobRequest request) { return reScheduleJobService.rescheduleJob(request); } diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchChartResponse.java b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchChartResponse.java index a5ac246..4a3ccf9 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchChartResponse.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchChartResponse.java @@ -1,9 +1,6 @@ package com.spring.domain.schedule.dto; import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.BatchStatus; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -13,7 +10,7 @@ import lombok.RequiredArgsConstructor; public class BatchChartResponse { private final List jobAvgSummary; - private final Map statusCounts; + private final List statusCounts; private final List jobHourSummary; private final List jobExecutionSummary; diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobStatusCountProjection.java b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobStatusCountProjection.java index cc53469..29dc20e 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobStatusCountProjection.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/BatchJobStatusCountProjection.java @@ -3,6 +3,7 @@ package com.spring.domain.schedule.dto; import org.springframework.batch.core.BatchStatus; public interface BatchJobStatusCountProjection { + String getJobName(); BatchStatus getStatus(); Long getCount(); } diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/dto/ReScheduleJobRequest.java b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/ReScheduleJobRequest.java index e8504fb..d635e80 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/dto/ReScheduleJobRequest.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/dto/ReScheduleJobRequest.java @@ -1,5 +1,9 @@ package com.spring.domain.schedule.dto; +import javax.validation.constraints.NotBlank; + +import com.spring.common.validation.ValidCronExpression; + import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -7,8 +11,11 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class ReScheduleJobRequest { + @NotBlank(message = "잡 그룹을 입력해 주십시오.") private final String jobGroup; + @NotBlank(message = "잡 이름을 입력해 주십시오.") private final String jobName; + @ValidCronExpression(message = "잘못된 크론 표현식 입니다.") private final String cronExpression; } diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java b/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java index 5e974ba..bee1364 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/repository/BatchJobExecutionRepository.java @@ -1,6 +1,5 @@ package com.spring.domain.schedule.repository; -import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -14,39 +13,47 @@ import com.spring.domain.schedule.dto.BatchJobStatusCountProjection; import com.spring.domain.schedule.entity.BatchJobExecution; public interface BatchJobExecutionRepository extends JpaRepository { - - @Query("SELECT bji.jobName AS jobName, " + - "bje.startTime AS startTime, " + - "bje.endTime AS endTime " + - "FROM BatchJobExecution bje " + - "JOIN bje.jobInstance bji " + - "WHERE bje.status = 'COMPLETED' AND bje.startTime >= :startDate " + - "ORDER BY bje.startTime") - List findJobAverageDurations(@Param("startDate") LocalDateTime startDate); + + @Query("SELECT bji.jobName AS jobName, " + + "bje.startTime AS startTime, " + + "bje.endTime AS endTime " + + "FROM BatchJobExecution bje " + + "JOIN bje.jobInstance bji " + + "WHERE bje.status = 'COMPLETED' " + + "AND FUNCTION('YEAR', bje.startTime) = :year " + + "AND FUNCTION('MONTH', bje.startTime) = :month " + + "ORDER BY bje.startTime") + List findJobAverageDurations(@Param("year") int year, @Param("month") int month); - @Query("SELECT bje.status AS status, COUNT(bje) AS count " + - "FROM BatchJobExecution bje " + - "GROUP BY bje.status") - List findJobStatusCounts(); + @Query("SELECT bji.jobName AS jobName, bje.status AS status, COUNT(bje) AS count " + + "FROM BatchJobExecution bje " + + "JOIN bje.jobInstance bji " + + "WHERE FUNCTION('YEAR', bje.startTime) = :year " + + "AND FUNCTION('MONTH', bje.startTime) = :month " + + "GROUP BY bji.jobName, bje.status " + + "ORDER BY bji.jobName, bje.status") + List findJobStatusCounts(@Param("year") int year, @Param("month") int month); - @Query("SELECT bje.startTime AS executionDate, " + - "bji.jobName AS jobName, " + - "COUNT(bje) AS executionCount " + - "FROM BatchJobExecution bje " + - "JOIN bje.jobInstance bji " + - "WHERE bje.startTime >= :startDate " + - "GROUP BY bje.startTime, bji.jobName " + - "ORDER BY bje.startTime, bji.jobName") - List findJobExecutionSummaryDays(@Param("startDate") LocalDateTime startDate); + @Query("SELECT bje.startTime AS executionDate, " + + "bji.jobName AS jobName, " + + "COUNT(bje) AS executionCount " + + "FROM BatchJobExecution bje " + + "JOIN bje.jobInstance bji " + + "WHERE FUNCTION('YEAR', bje.startTime) = :year " + + "AND FUNCTION('MONTH', bje.startTime) = :month " + + "GROUP BY bje.startTime, bji.jobName " + + "ORDER BY bje.startTime, bji.jobName") + List findJobExecutionSummaryDays(@Param("year") int year, @Param("month") int month); - @Query("SELECT bji.jobName AS jobName, " + - "FUNCTION('HOUR', bje.startTime) AS hour, " + - "COUNT(bje) AS count " + - "FROM BatchJobExecution bje " + - "JOIN bje.jobInstance bji " + - "WHERE bje.startTime >= :startDate " + - "GROUP BY bji.jobName, FUNCTION('HOUR', bje.startTime) " + - "ORDER BY bji.jobName, hour") - List findHourlyJobExecutionDistribution(@Param("startDate") LocalDateTime startDate); + @Query("SELECT bji.jobName AS jobName, " + + "FUNCTION('HOUR', bje.startTime) AS hour, " + + "COUNT(bje) AS count " + + "FROM BatchJobExecution bje " + + "JOIN bje.jobInstance bji " + + "WHERE FUNCTION('YEAR', bje.startTime) = :year " + + "AND FUNCTION('MONTH', bje.startTime) = :month " + + "GROUP BY bji.jobName, FUNCTION('HOUR', bje.startTime) " + + "ORDER BY bji.jobName, hour") + List findHourlyJobExecutionDistribution(@Param("year") int year, @Param("month") int month); } diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/service/BatchChartService.java b/batch-quartz/src/main/java/com/spring/domain/schedule/service/BatchChartService.java index 317a0a9..dd15ed0 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/service/BatchChartService.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/service/BatchChartService.java @@ -1,12 +1,9 @@ package com.spring.domain.schedule.service; -import java.time.LocalDateTime; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.springframework.batch.core.BatchStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.spring.domain.schedule.dto.BatchChartResponse; import com.spring.domain.schedule.dto.BatchJobAverageDurationProjection; @@ -23,36 +20,30 @@ public class BatchChartService { private final BatchJobExecutionRepository batchJobExecutionRepository; - public BatchChartResponse getBatchJobExecutionData() { + @Transactional(readOnly = true) + public BatchChartResponse getBatchJobExecutionData(int year, int month) { return new BatchChartResponse( - getJobAverageDurations(), - getJobStatusCounts(), - getHourlyJobExecutionDistribution(), - getJobExecutionSummary() + getJobAverageDurations(year, month), + getJobStatusCounts(year, month), + getHourlyJobExecutionDistribution(year, month), + getJobExecutionSummary(year, month) ); } - private List getJobAverageDurations() { - LocalDateTime startDate = LocalDateTime.now().minusDays(30); - return batchJobExecutionRepository.findJobAverageDurations(startDate); + private List getJobAverageDurations(int year, int month) { + return batchJobExecutionRepository.findJobAverageDurations(year, month); } - private Map getJobStatusCounts() { - return batchJobExecutionRepository.findJobStatusCounts().stream() - .collect(Collectors.toMap( - BatchJobStatusCountProjection::getStatus, - BatchJobStatusCountProjection::getCount - )); + private List getJobStatusCounts(int year, int month) { + return batchJobExecutionRepository.findJobStatusCounts(year, month); } - private List getHourlyJobExecutionDistribution() { - LocalDateTime startDate = LocalDateTime.now().minusDays(30); - return batchJobExecutionRepository.findHourlyJobExecutionDistribution(startDate); + private List getHourlyJobExecutionDistribution(int year, int month) { + return batchJobExecutionRepository.findHourlyJobExecutionDistribution(year, month); } - private List getJobExecutionSummary() { - LocalDateTime startDate = LocalDateTime.now().minusDays(30); - return batchJobExecutionRepository.findJobExecutionSummaryDays(startDate); + private List getJobExecutionSummary(int year, int month) { + return batchJobExecutionRepository.findJobExecutionSummaryDays(year, month); } } diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/service/FindScheduleJobService.java b/batch-quartz/src/main/java/com/spring/domain/schedule/service/FindScheduleJobService.java index 3be527c..f839006 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/service/FindScheduleJobService.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/service/FindScheduleJobService.java @@ -62,7 +62,8 @@ public class FindScheduleJobService { } private boolean isGroupMatched(final String group, final String groupName) { - return groupName == null || groupName.isEmpty() || group.equals(groupName); + return groupName == null || groupName.isEmpty() || + group.toLowerCase().contains(groupName.toLowerCase()); } private Stream getJobKeysForGroup(final String group) { @@ -74,7 +75,8 @@ public class FindScheduleJobService { } private boolean isJobMatched(final JobKey jobKey, final String jobName) { - return jobName == null || jobName.isEmpty() || jobKey.getName().equals(jobName); + return jobName == null || jobName.isEmpty() || + jobKey.getName().toLowerCase().contains(jobName.toLowerCase()); } private ScheduleJobResponse createScheduleSafely(final JobKey jobKey) { @@ -105,11 +107,12 @@ public class FindScheduleJobService { private String getTriggerState(final Optional trigger) { return trigger.map(t -> { try { - return scheduler.getTriggerState(t.getKey()).name(); + Trigger.TriggerState state = scheduler.getTriggerState(t.getKey()); + return state.name(); } catch (SchedulerException e) { - return ""; + return Trigger.TriggerState.ERROR.name(); } - }).orElse(""); + }).orElse(Trigger.TriggerState.NONE.name()); } private String getCronExpression(final Optional trigger) { diff --git a/batch-quartz/src/main/java/com/spring/domain/schedule/service/ReScheduleJobService.java b/batch-quartz/src/main/java/com/spring/domain/schedule/service/ReScheduleJobService.java index 28ccf49..24b418b 100644 --- a/batch-quartz/src/main/java/com/spring/domain/schedule/service/ReScheduleJobService.java +++ b/batch-quartz/src/main/java/com/spring/domain/schedule/service/ReScheduleJobService.java @@ -1,6 +1,7 @@ package com.spring.domain.schedule.service; import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; @@ -20,13 +21,17 @@ public class ReScheduleJobService { public boolean rescheduleJob(ReScheduleJobRequest request) { try { - TriggerKey triggerKey = TriggerKey.triggerKey(request.getJobName(), request.getJobGroup()); + TriggerKey triggerKey = TriggerKey.triggerKey(request.getJobName() + "Trigger", request.getJobGroup()); Trigger oldTrigger = scheduler.getTrigger(triggerKey); if (oldTrigger != null) { - Trigger newTrigger = TriggerBuilder.newTrigger() - .withIdentity(triggerKey) - .withSchedule(CronScheduleBuilder.cronSchedule(request.getCronExpression())) - .build(); + CronTrigger newTrigger = TriggerBuilder.newTrigger() + .withIdentity(oldTrigger.getKey()) + .withDescription(oldTrigger.getDescription()) + .withPriority(oldTrigger.getPriority()) + .forJob(oldTrigger.getJobKey()) + .withSchedule(CronScheduleBuilder.cronSchedule(request.getCronExpression().trim()) + .withMisfireHandlingInstructionDoNothing()) + .build(); scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger); return true; } diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java index aacba9a..fcf49c5 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java @@ -175,4 +175,13 @@ public abstract class AbstractBatchTask implements ApplicationContextAware { return batchJobInfo.cronExpression(); } + /** + * 배치 작업의 설명을 반환합니다. + * + * @return 배치 작업의 설명 + */ + public String description() { + return batchJobInfo.description(); + } + } diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java index 93bf674..b80d7c2 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java @@ -43,4 +43,10 @@ public @interface BatchJobInfo { * @return cron 표현식 */ String cronExpression() default ""; + /** + * 배치 작업의 설명을 지정합니다. + * + * @return 작업의 설명 + */ + String description() default ""; } diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java index e5b4a49..a61f3b4 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java @@ -51,4 +51,10 @@ public @interface QuartzJob { * @return Cron 표현식 */ String cronExpression() default ""; + /** + * Quartz 작업의 설명을 지정합니다. + * + * @return 작업의 설명 + */ + String description() default ""; } diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java index aa1d65a..a475d80 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java @@ -1,5 +1,6 @@ package com.spring.infra.quartz; +import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.batch.core.Job; @@ -42,11 +43,6 @@ public class QuartzJobLauncher extends QuartzJobBean { private final JobLauncher jobLauncher; private final JobRegistry jobRegistry; - private String jobName; - - public void setJobName(String jobName) { - this.jobName = jobName; - } /** * Quartz 스케줄러에 의해 호출되는 메소드로, 실제 Job을 실행합니다. @@ -62,6 +58,9 @@ public class QuartzJobLauncher extends QuartzJobBean { */ @Override protected void executeInternal(@NonNull JobExecutionContext context) throws JobExecutionException { + JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); + String jobName = jobDataMap.getString("jobName"); + try { Job job = jobRegistry.getJob(jobName); JobParameters params = new JobParametersBuilder() @@ -70,7 +69,7 @@ public class QuartzJobLauncher extends QuartzJobBean { .toJobParameters(); jobLauncher.run(job, params); } catch (Exception e) { - log.error("job execution exception! - {}", e.getCause()); + log.error("Job execution exception! - {}", e.getMessage(), e); throw new JobExecutionException(); } } diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java index 0f35d30..958b807 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java @@ -10,7 +10,6 @@ import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; -import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; @@ -56,10 +55,19 @@ public class QuartzJobRegistrar implements ApplicationListener batchJobs = applicationContext.getBeansOfType(AbstractBatchTask.class); for (AbstractBatchTask batchJob : batchJobs.values()) { - JobDataMap jobDataMap = new JobDataMap(); - jobDataMap.put("jobName", batchJob.jobName()); - JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) - .withIdentity(batchJob.jobName(), batchJob.group()) - .setJobData(jobDataMap) - .storeDurably(true) - .build(); - CronTrigger trigger = TriggerBuilder.newTrigger() - .forJob(jobDetail) - .withIdentity(batchJob.jobName() + "Trigger", batchJob.group()) - .withSchedule(CronScheduleBuilder.cronSchedule(batchJob.cronExpression())) - .build(); - try { - scheduler.scheduleJob(jobDetail, trigger); - } catch (SchedulerException e) { - throw new IllegalStateException("Error scheduling AbstractBatchJob: " + batchJob.jobName(), e); - } + scheduleJob(batchJob); + } + } + + private void scheduleJob(AbstractBatchTask batchJob) { + JobDataMap jobDataMap = new JobDataMap(); + jobDataMap.put("jobName", batchJob.jobName()); + + JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) + .withIdentity(batchJob.jobName(), batchJob.group()) + .setJobData(jobDataMap) + .storeDurably(true) + .withDescription(batchJob.description()) + .build(); + + CronTrigger trigger = TriggerBuilder.newTrigger() + .forJob(jobDetail) + .withIdentity(batchJob.jobName() + "Trigger", batchJob.group()) + .withSchedule(CronScheduleBuilder.cronSchedule(batchJob.cronExpression())) + .withDescription(batchJob.description()) + .build(); + + try { + scheduler.scheduleJob(jobDetail, trigger); + } catch (SchedulerException e) { + throw new IllegalStateException("Error scheduling AbstractBatchJob: " + batchJob.jobName(), e); } } diff --git a/batch-quartz/src/main/resources/application.yml b/batch-quartz/src/main/resources/application.yml index cbe036a..67a449c 100644 --- a/batch-quartz/src/main/resources/application.yml +++ b/batch-quartz/src/main/resources/application.yml @@ -41,14 +41,16 @@ spring: hibernate: dialect: org.hibernate.dialect.H2Dialect "[format_sql]": true # 쿼리 로그 포맷 (정렬) - "[show_sql]": true # 쿼리 로그 출력 + "[show_sql]": false # 쿼리 로그 출력 + "[highlight_sql]": true # 쿼리 하이라이트 + "[use_sql_comments]": true # SQL 주석 사용 naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: true batch: job: - enabled: true + enabled: false jdbc: initialize-schema: always @@ -100,4 +102,12 @@ jwt: front: base: url: http://localhost:8081 - timeout: 100000 \ No newline at end of file + timeout: 100000 + +logging: + level: + org: + hibernate: + SQL: DEBUG + type: + descriptor: TRACE \ No newline at end of file diff --git a/batch-quartz/src/main/resources/static/js/apis/dashboard-api.js b/batch-quartz/src/main/resources/static/js/apis/dashboard-api.js index 3077f6f..e0fef33 100644 --- a/batch-quartz/src/main/resources/static/js/apis/dashboard-api.js +++ b/batch-quartz/src/main/resources/static/js/apis/dashboard-api.js @@ -1,7 +1,9 @@ import apiClient from '../common/axios-instance.js'; -export const getBatchJobExecutionData = async () => { - const response = await apiClient.get('/api/dashboard/chart/batch'); +export const getBatchJobExecutionData = async (year, month) => { + const response = await apiClient.get('/api/dashboard/chart', { + params: { year, month } + }); return response.data; } diff --git a/batch-quartz/src/main/resources/static/js/apis/schedule-api.js b/batch-quartz/src/main/resources/static/js/apis/schedule-api.js index 801135b..7078544 100644 --- a/batch-quartz/src/main/resources/static/js/apis/schedule-api.js +++ b/batch-quartz/src/main/resources/static/js/apis/schedule-api.js @@ -11,16 +11,25 @@ export const getJobDetail = async (groupName, jobName) => { } export const pauseJob = async (groupName, jobName) => { - const response = await apiClient.post(`/api/schedule/pause/${groupName}/${jobName}`); + const response = await apiClient.get(`/api/schedule/pause/${groupName}/${jobName}`); return response.data; } export const resumeJob = async (groupName, jobName) => { - const response = await apiClient.post(`/api/schedule/resume/${groupName}/${jobName}`); + const response = await apiClient.get(`/api/schedule/resume/${groupName}/${jobName}`); return response.data; } export const triggerJob = async (groupName, jobName) => { - const response = await apiClient.post(`/api/schedule/trigger/${groupName}/${jobName}`); + const response = await apiClient.get(`/api/schedule/trigger/${groupName}/${jobName}`); + return response.data; +}; + +export const rescheduleJob = async (jobGroup, jobName, cronExpression) => { + const response = await apiClient.post('/api/schedule/reschedule', { + jobGroup, + jobName, + cronExpression + }); return response.data; }; \ No newline at end of file diff --git a/batch-quartz/src/main/resources/static/js/common/common.js b/batch-quartz/src/main/resources/static/js/common/common.js index 73e47bd..46469bd 100644 --- a/batch-quartz/src/main/resources/static/js/common/common.js +++ b/batch-quartz/src/main/resources/static/js/common/common.js @@ -1 +1,7 @@ -dayjs.locale('ko'); \ No newline at end of file +dayjs.locale('ko'); + +export const formatDateTime = (dateTimeString) => { + if (!dateTimeString) return '-'; + const date = new Date(dateTimeString); + return dayjs(date).format("YYYY-MM-DD ddd A HH:mm:ss"); +} \ No newline at end of file diff --git a/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js b/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js index 907260a..0f26d9d 100644 --- a/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js +++ b/batch-quartz/src/main/resources/static/js/pages/dashboard/dashboard.js @@ -1,11 +1,15 @@ +import { formatDateTime } from '../../common/common.js'; import { getBatchJobExecutionData, getRecentJobs } from '../../apis/dashboard-api.js'; +let selectedMonth; document.addEventListener('DOMContentLoaded', () => { + initMonthPicker(); fetchDataAndRender(); }); const fetchDataAndRender = async () => { - const batchData = await getBatchJobExecutionData(); + const [year, month] = selectedMonth.split('-'); + const batchData = await getBatchJobExecutionData(year, month); const recentJobs = await getRecentJobs(); renderBatchExecutionTimeChart(batchData.jobAvgSummary); @@ -15,6 +19,23 @@ const fetchDataAndRender = async () => { renderRecentJobsTable(recentJobs); }; +const initMonthPicker = () => { + const monthPicker = document.getElementById('monthPicker'); + const currentDate = dayjs(); + selectedMonth = currentDate.format('YYYY-MM'); + monthPicker.value = selectedMonth; + + monthPicker.addEventListener('click', (event) => { + event.preventDefault(); + monthPicker.showPicker(); + }); + + monthPicker.addEventListener('change', (event) => { + selectedMonth = event.target.value; + if(selectedMonth) fetchDataAndRender(); + }); +}; + const chartOptions = { responsive: true, maintainAspectRatio: false, @@ -32,6 +53,7 @@ const chartOptions = { let batchExecutionTimeChart; const renderBatchExecutionTimeChart = (data) => { + if (batchExecutionTimeChart) batchExecutionTimeChart.destroy(); const jobExecutionTimes = {}; data.forEach(job => { if (job.endTime && job.startTime) { @@ -57,9 +79,9 @@ const renderBatchExecutionTimeChart = (data) => { datasets: [{ label: '평균 실행 시간 (초)', data: Object.values(averageExecutionTimes), - backgroundColor: 'rgba(75, 192, 192, 0.6)', - borderColor: 'rgba(75, 192, 192, 1)', - borderWidth: 1 + backgroundColor: 'rgba(54, 162, 235, 0.6)', + borderColor: 'rgba(54, 162, 235, 3)', + borderWidth: 3 }] }, options: { @@ -91,25 +113,58 @@ const renderBatchExecutionTimeChart = (data) => { let batchStatusChart; const renderBatchStatusChart = (data) => { + if (batchStatusChart) batchStatusChart.destroy(); + const statusTotals = data.reduce((acc, item) => { + if (!acc[item.status]) { + acc[item.status] = 0; + } + acc[item.status] += item.count; + return acc; + }, {}); + + const labels = Object.keys(statusTotals); + const counts = Object.values(statusTotals); + const statusColors = { + 'COMPLETED': 'rgba(75, 192, 192, 0.6)', + 'FAILED': 'rgba(255, 99, 132, 0.6)', + 'STARTING': 'rgba(255, 206, 86, 0.6)', + 'STARTED': 'rgba(54, 162, 235, 0.6)', + 'STOPPING': 'rgba(153, 102, 255, 0.6)', + 'STOPPED': 'rgba(255, 159, 64, 0.6)', + 'ABANDONED': 'rgba(201, 203, 207, 0.6)' + }; + const ctx = document.getElementById('batchStatusChart').getContext('2d'); batchStatusChart = new Chart(ctx, { type: 'pie', data: { - labels: Object.keys(data), + labels: labels, datasets: [{ - data: Object.values(data), - backgroundColor: [ - 'rgba(75, 192, 192, 0.6)', - 'rgba(255, 99, 132, 0.6)', - 'rgba(255, 206, 86, 0.6)', - 'rgba(54, 162, 235, 0.6)' - ] + data: counts, + backgroundColor: Object.values(statusColors) }] }, options: { ...chartOptions, plugins: { - ...chartOptions.plugins + ...chartOptions.plugins, + tooltip: { + callbacks: { + title: () => null, + label: (context) => { + const label = context.label || ''; + const value = context.parsed || 0; + return `${label}: ${value}`; + }, + afterLabel: (context) => { + const status = context.label; + return data + .filter(item => item.status === status) + .map(item => ` ${item.jobName}: ${item.count}`) + .join('\n'); + } + } + } } } }); @@ -117,6 +172,7 @@ const renderBatchStatusChart = (data) => { let hourlyJobExecutionChart; const renderHourlyJobExecutionChart = (data) => { + if (hourlyJobExecutionChart) hourlyJobExecutionChart.destroy(); const ctx = document.getElementById('hourlyJobExecutionChart').getContext('2d'); const hours = Array.from({length: 24}, (_, i) => i); const jobNames = [...new Set(data.map(item => item.jobName))]; @@ -174,12 +230,16 @@ const renderHourlyJobExecutionChart = (data) => { let dailyJobExecutionsChart; const renderDailyJobExecutionsChart = (data) => { + if (dailyJobExecutionsChart) dailyJobExecutionsChart.destroy(); const ctx = document.getElementById('dailyJobExecutionsChart').getContext('2d'); - const now = new Date(); - const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); - const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); + + const [year, month] = selectedMonth.split('-'); + const firstDay = new Date(year, month - 1, 1); + const lastDay = new Date(year, month, 0); + const endDate = new Date(lastDay.getTime()); + const dates = []; - for (let d = new Date(firstDay); d <= lastDay; d.setDate(d.getDate() + 1)) { + for (let d = new Date(firstDay); d <= endDate; d = new Date(d.setDate(d.getDate() + 1))) { dates.push(d.toISOString().split('T')[0]); } @@ -203,7 +263,7 @@ const renderDailyJobExecutionsChart = (data) => { label: jobName, data: dates.map(date => ({ x: luxon.DateTime.fromISO(date).toJSDate(), - y: groupedData[date]?.[jobName] || null + y: groupedData[date]?.[jobName] || 0 })), borderColor: color, backgroundColor: color, @@ -220,7 +280,14 @@ const renderDailyJobExecutionsChart = (data) => { options: { ...chartOptions, plugins: { - ...chartOptions.plugins + ...chartOptions.plugins, + tooltip: { + callbacks: { + title: (context) => { + return luxon.DateTime.fromJSDate(context[0].parsed.x).toFormat('yyyy-MM-dd'); + } + } + } }, scales: { x: { @@ -229,13 +296,14 @@ const renderDailyJobExecutionsChart = (data) => { unit: 'day', displayFormats: { day: 'MM-dd' - }, - tooltipFormat: 'yyyy-MM-dd' + } }, title: { display: true, text: '날짜' - } + }, + min: firstDay, + max: lastDay }, y: { beginAtZero: true, @@ -253,9 +321,9 @@ const renderRecentJobsTable = (recentJobs) => { const tableBody = document.getElementById('recentJobsTable'); tableBody.innerHTML = recentJobs.map(job => ` - ${job.jobName} ${job.jobGroup} - ${new Date(job.firedTime).toLocaleString()} + ${job.jobName} + ${formatDateTime(job.firedTime)} ${job.state} `).join(''); diff --git a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js index 55dd591..e373f27 100644 --- a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js +++ b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js @@ -1,90 +1,169 @@ -import { getAllJobs, getJobDetail, triggerJob, pauseJob, resumeJob } from '../../apis/schedule-api.js'; +import { formatDateTime } from '../../common/common.js'; +import { getAllJobs, getJobDetail, pauseJob, resumeJob, rescheduleJob } from '../../apis/schedule-api.js'; document.addEventListener('DOMContentLoaded', () => { - const searchForm = document.getElementById('searchForm'); - const tableBody = document.querySelector('tbody'); + fetchDataAndRender(); - searchForm.addEventListener('submit', async (e) => { + document.getElementById('searchForm').addEventListener('submit', async (e) => { e.preventDefault(); - const formData = new FormData(searchForm); - const searchParams = new URLSearchParams(formData); - const response = await getAllJobs(searchParams); - updateTable(response); + fetchDataAndRender(); }); +}); - const updateTable = (jobs) => { - tableBody.innerHTML = jobs.map(job => ` - - ${job.group} - ${job.name} - ${job.cronExpression} - ${job.status} - - - - - `).join(''); +const fetchDataAndRender = async () => { + const searchForm = document.getElementById('searchForm'); + const formData = new FormData(searchForm); + const searchParams = new URLSearchParams(formData); + const response = await getAllJobs(searchParams); + updateTable(response); +}; - document.querySelectorAll('.detail-btn').forEach(btn => { - btn.addEventListener('click', showJobDetail); - }); - }; +const updateTable = (jobs) => { + const tableBody = document.querySelector('tbody'); + tableBody.innerHTML = jobs.map(job => ` + + ${job.group} + ${job.name} + ${job.cronExpression} + ${job.status} + + + + + `).join(''); - const showJobDetail = async (e) => { - const { group, name } = e.target.closest('button').dataset; - const jobDetail = await getJobDetail(group, name); - const detailContent = document.getElementById('scheduleDetailContent'); - detailContent.innerHTML = ` -
-
    - ${createDetailItem('그룹', jobDetail.group, 'bi-people')} - ${createDetailItem('잡 이름', jobDetail.name, 'bi-briefcase')} - ${createDetailItem('설명', jobDetail.description || '-', 'bi-card-text')} - ${createDetailItem('스케줄', ``, 'bi-calendar-event')} - ${createDetailItem('다음 실행', formatDateTime(jobDetail.nextFireTime), 'bi-clock')} - ${createDetailItem('이전 실행', formatDateTime(jobDetail.previousFireTime), 'bi-clock-history')} - ${createDetailItem('상태', `${jobDetail.status}`, 'bi-activity')} -
-
- `; + document.querySelectorAll('.detail-btn').forEach(btn => { + btn.addEventListener('click', showJobDetail); + }); +}; - const modal = new bootstrap.Modal(document.getElementById('scheduleDetailModal')); - modal.show(); - - document.getElementById('startJobBtn').onclick = () => triggerJob(group, name); - document.getElementById('pauseJobBtn').onclick = () => pauseJob(group, name); - document.getElementById('resumeJobBtn').onclick = () => resumeJob(group, name); - }; - - const createDetailItem = (label, value, iconClass) => ` -
  • -
    -
    - - ${label} -
    -
    - ${value} -
    -
    -
  • +const showJobDetail = async (e) => { + const { group, name } = e.target.closest('button').dataset; + const jobDetail = await getJobDetail(group, name); + const detailContent = document.getElementById('scheduleDetailContent'); + detailContent.innerHTML = ` +
    +
      + ${createDetailItem('그룹', jobDetail.group, 'bi-people')} + ${createDetailItem('잡 이름', jobDetail.name, 'bi-briefcase')} + ${createDetailItem('설명', jobDetail.description || '-', 'bi-card-text')} + ${createDetailItem('스케줄', ``, 'bi-calendar-event')} + ${createDetailItem('다음 실행', formatDateTime(jobDetail.nextFireTime), 'bi-clock')} + ${createDetailItem('이전 실행', formatDateTime(jobDetail.previousFireTime), 'bi-clock-history')} + ${createDetailItem('상태', `${jobDetail.status}`, 'bi-activity')} +
    +
    `; - const formatDateTime = (dateTimeString) => { - if (!dateTimeString) return '-'; - const date = new Date(dateTimeString); - return dayjs(date).format("YYYY-MM-DD ddd A HH:mm:ss"); + const modal = new bootstrap.Modal(document.getElementById('scheduleDetailModal')); + modal.show(); + + updateJobControlButtons(jobDetail.status); + + document.getElementById('pauseJobBtn').onclick = () => { + pauseJob(group, name).then(() => updateJobStatus(group, name, 'PAUSED')); + }; + document.getElementById('resumeJobBtn').onclick = () => { + resumeJob(group, name).then(() => updateJobStatus(group, name, 'NORMAL')); + }; + document.getElementById('updateCronBtn').onclick = () => { + updateCronExpression(group, name); + }; +}; + +const createDetailItem = (label, value, iconClass) => ` +
  • +
    +
    + + ${label} +
    +
    + ${value} +
    +
    +
  • +`; + +const getStatusBadgeClass = (status) => { + const statusClasses = { + 'NONE': 'bg-secondary', + 'NORMAL': 'bg-success', + 'PAUSED': 'bg-warning', + 'COMPLETE': 'bg-info', + 'ERROR': 'bg-danger', + 'BLOCKED': 'bg-dark' + }; + return statusClasses[status] || 'bg-secondary'; +}; + +const updateCronExpression = async (group, name) => { + const cronExpressionInput = document.getElementById('cronExpression'); + const newCronExpression = cronExpressionInput.value; + const result = await rescheduleJob(group, name, newCronExpression); + if (result) { + alert('스케쥴이 수정 되었습니다.'); + fetchDataAndRender(); + } else { + alert('스케쥴 수정이 실패했습니다.'); + } +}; + +const updateJobControlButtons = (status) => { + const pauseJobBtn = document.getElementById('pauseJobBtn'); + const resumeJobBtn = document.getElementById('resumeJobBtn'); + + const updateButtonState = (button, isEnabled) => { + button.disabled = !isEnabled; + button.classList.toggle(button.dataset.enabledClass, isEnabled); + button.classList.toggle('btn-outline-secondary', !isEnabled); }; - const getStatusBadgeClass = (status) => { - const statusClasses = { - 'NORMAL': 'bg-success', - 'PAUSED': 'bg-warning', - 'COMPLETE': 'bg-info', - 'ERROR': 'bg-danger' - }; - return statusClasses[status] || 'bg-secondary'; + const updateBothButtons = (pauseEnabled, resumeEnabled) => { + updateButtonState(pauseJobBtn, pauseEnabled); + updateButtonState(resumeJobBtn, resumeEnabled); }; -}); \ No newline at end of file + + switch (status) { + case 'PAUSED': + updateBothButtons(false, true); + break; + case 'COMPLETE': + case 'ERROR': + updateBothButtons(false, false); + break; + case 'NORMAL': + case 'BLOCKED': + case 'NONE': + default: + updateBothButtons(true, false); + break; + } +}; + +const updateJobStatus = async (group, name, newStatus) => { + const jobDetail = await getJobDetail(group, name); + jobDetail.status = newStatus; + + const statusElement = document.querySelector('#scheduleDetailContent .badge'); + statusElement.className = `badge ${getStatusBadgeClass(newStatus)}`; + statusElement.textContent = newStatus; + + updateJobControlButtons(newStatus); + updateTableJobStatus(group, name, newStatus); +}; + +const updateTableJobStatus = (group, name, newStatus) => { + const tableRows = document.querySelectorAll('tbody tr'); + for (let row of tableRows) { + const rowGroup = row.cells[0].textContent; + const rowName = row.cells[1].textContent; + if (rowGroup === group && rowName === name) { + const statusCell = row.cells[3]; + statusCell.innerHTML = `${newStatus}`; + break; + } + } +}; \ No newline at end of file diff --git a/batch-quartz/src/main/resources/templates/fragments/config.html b/batch-quartz/src/main/resources/templates/fragments/config.html index bad22ae..6d71463 100644 --- a/batch-quartz/src/main/resources/templates/fragments/config.html +++ b/batch-quartz/src/main/resources/templates/fragments/config.html @@ -17,5 +17,5 @@ - + \ No newline at end of file diff --git a/batch-quartz/src/main/resources/templates/fragments/header.html b/batch-quartz/src/main/resources/templates/fragments/header.html index 1c5f58b..a5f7daf 100644 --- a/batch-quartz/src/main/resources/templates/fragments/header.html +++ b/batch-quartz/src/main/resources/templates/fragments/header.html @@ -3,7 +3,7 @@