This commit is contained in:
mindol1004
2024-09-20 18:45:29 +09:00
parent 4bfda23a47
commit 9d1079fcbd
77 changed files with 1116 additions and 1035 deletions

View File

@@ -16,7 +16,10 @@
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>11</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>

View File

@@ -17,7 +17,8 @@ public enum ExceptionRule implements ErrorRule {
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 오류가 발생했습니다."),
CONFLICT(HttpStatus.CONFLICT, "데이터 충돌이 발생했습니다."),
UNPROCESSABLE_ENTITY(HttpStatus.UNPROCESSABLE_ENTITY, "요청을 처리할 수 없습니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다.");
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "스케쥴 정보를 찾을 수 없습니다.");
private final HttpStatus status;
private String message;

View File

@@ -1,30 +0,0 @@
package com.spring.domain.batch.entity;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import lombok.Getter;
// @Entity
// @Table(name = "BATCH_JOB_EXECUTION_CONTEXT")
@Getter
public class BatchJobExecutionContext {
@Id
@Column(name = "JOB_EXECUTION_ID")
private Long jobExecutionId;
@Column(name = "SHORT_CONTEXT", length = 2500, nullable = false)
private String shortContext;
@Column(name = "SERIALIZED_CONTEXT")
private String serializedContext;
@OneToOne
@JoinColumn(name = "JOB_EXECUTION_ID", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "JOB_EXEC_CTX_FK"))
private BatchJobExecution batchJobExecution;
}

View File

@@ -1,47 +0,0 @@
package com.spring.domain.batch.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "BATCH_JOB_EXECUTION_PARAMS")
@Getter
public class BatchJobExecutionParams {
@EmbeddedId
private BatchJobExecutionParamsId id;
@ManyToOne
@JoinColumn(name = "JOB_EXECUTION_ID", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "JOB_EXEC_PARAMS_FK"))
private BatchJobExecution jobExecution;
@Column(name = "PARAMETER_TYPE", nullable = false)
private String parameterType;
@Column(name = "PARAMETER_VALUE", length = 2500)
private String parameterValue;
@Column(name = "IDENTIFYING", nullable = false)
private Character identifying;
@Embeddable
@Getter
@EqualsAndHashCode
public static class BatchJobExecutionParamsId implements Serializable {
@Column(name = "JOB_EXECUTION_ID")
private Long jobExecutionId;
@Column(name = "PARAMETER_NAME")
private String parameterName;
}
}

View File

@@ -1,80 +0,0 @@
package com.spring.domain.batch.entity;
import java.sql.Timestamp;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.Getter;
// @Entity
// @Table(name = "BATCH_STEP_EXECUTION")
@Getter
public class BatchStepExecution {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "STEP_EXECUTION_ID")
private Long stepExecutionId;
@Column(name = "VERSION", nullable = false)
private Long version;
@Column(name = "STEP_NAME", length = 100, nullable = false)
private String stepName;
@ManyToOne
@JoinColumn(name = "JOB_EXECUTION_ID", nullable = false, foreignKey = @ForeignKey(name = "JOB_EXEC_STEP_FK"))
private BatchJobExecution batchJobExecution;
@Column(name = "CREATE_TIME", nullable = false)
private Timestamp createTime;
@Column(name = "START_TIME")
private Timestamp startTime;
@Column(name = "END_TIME")
private Timestamp endTime;
@Column(name = "STATUS", length = 10)
private String status;
@Column(name = "COMMIT_COUNT")
private Long commitCount;
@Column(name = "READ_COUNT")
private Long readCount;
@Column(name = "FILTER_COUNT")
private Long filterCount;
@Column(name = "WRITE_COUNT")
private Long writeCount;
@Column(name = "READ_SKIP_COUNT")
private Long readSkipCount;
@Column(name = "WRITE_SKIP_COUNT")
private Long writeSkipCount;
@Column(name = "PROCESS_SKIP_COUNT")
private Long processSkipCount;
@Column(name = "ROLLBACK_COUNT")
private Long rollbackCount;
@Column(name = "EXIT_CODE", length = 2500)
private String exitCode;
@Column(name = "EXIT_MESSAGE", length = 2500)
private String exitMessage;
@Column(name = "LAST_UPDATED")
private Timestamp lastUpdated;
}

View File

@@ -1,30 +0,0 @@
package com.spring.domain.batch.entity;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import lombok.Getter;
// @Entity
// @Table(name = "BATCH_STEP_EXECUTION_CONTEXT")
@Getter
public class BatchStepExecutionContext {
@Id
@Column(name = "STEP_EXECUTION_ID")
private Long stepExecutionId;
@Column(name = "SHORT_CONTEXT", length = 2500, nullable = false)
private String shortContext;
@Column(name = "SERIALIZED_CONTEXT")
private String serializedContext;
@OneToOne
@JoinColumn(name = "STEP_EXECUTION_ID", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "STEP_EXEC_CTX_FK"))
private BatchStepExecution batchStepExecution;
}

View File

@@ -17,31 +17,25 @@ import lombok.extern.slf4j.Slf4j;
@BatchJobInfo(
group = "EMAIL",
jobName = "emailSendJob",
cronExpression = "*/5 * * * * ?"
cronExpression = "*/10 * * * * ?"
)
public class EmailSendBatch extends AbstractBatchTask {
@Override
protected List<Step> createSteps() {
return List.of(
addStep("emailSendJobStep1111", createTasklet()),
addStep("emailSendJobStep2222", createSendTasklet())
addStep("emailSendJobStep1", createTasklet()),
addStep("emailSendJobStep2", createSendTasklet())
);
}
@Override
protected Tasklet createTasklet() {
return ((contribution, chunkContext) -> {
log.info(">>>>> emailSendTasklet1111111");
return RepeatStatus.FINISHED;
});
return ((contribution, chunkContext) -> RepeatStatus.FINISHED);
}
private Tasklet createSendTasklet() {
return ((contribution, chunkContext) -> {
log.info(">>>>> emailSendTasklet2222222");
return RepeatStatus.FINISHED;
});
return ((contribution, chunkContext) -> RepeatStatus.FINISHED);
}
}

View File

@@ -21,24 +21,21 @@ import lombok.extern.slf4j.Slf4j;
@BatchJobInfo(
group = "POST",
jobName = "postCreateJob",
cronExpression = "0/2 * * * * ?"
cronExpression = "0/20 * * * * ?"
)
@RequiredArgsConstructor
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() {
log.info(">>>>> PostCreateBatch-createTasklet");
return ((contribution, chunkContext) -> {
postMapper.save(Post.builder().title("testTitle").content("testPost").build());
return RepeatStatus.FINISHED;

View File

@@ -42,14 +42,14 @@ import lombok.extern.slf4j.Slf4j;
public class PostCreateBatchChunk {
private final JobRepository jobRepository;
private final @Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager;
private final @Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY) EntityManagerFactory entityManagerFactory;
private final PlatformTransactionManager transactionManager;
private final EntityManagerFactory entityManagerFactory;
private final PostRepository postRepository;
private final PostBackUpRepository postBackUpRepository;
private List<Post> list = new ArrayList<>();
@QuartzJob(group = "POST", jobName = "testPostJob", cronExpression = "0/5 * * * * ?")
@QuartzJob(group = "POST", jobName = "testPostJob", cronExpression = "0/30 * * * * ?")
@Bean
Job testPostJob() {
return new JobBuilder("testPostJob")
@@ -70,7 +70,6 @@ public class PostCreateBatchChunk {
}
private Step readListStep() {
log.info(">>>>> readListStep");
return new StepBuilder("readListStep")
.repository(jobRepository)
.transactionManager(transactionManager)
@@ -79,7 +78,6 @@ public class PostCreateBatchChunk {
}
private Tasklet readListTasklet() {
log.info(">>>>> readListTasklet");
return (contribution, chunkContext) -> {
list = postRepository.findAll();
return RepeatStatus.FINISHED;
@@ -87,7 +85,6 @@ public class PostCreateBatchChunk {
}
private Step processStep() {
log.info(">>>>> processStep");
return new StepBuilder("processStep")
.repository(jobRepository)
.transactionManager(transactionManager)
@@ -99,7 +96,6 @@ public class PostCreateBatchChunk {
}
private JpaPagingItemReader<Post> testReader() {
log.info(">>>>> JpaPagingItemReader");
return new JpaPagingItemReaderBuilder<Post>()
.name("testReader")
.entityManagerFactory(entityManagerFactory)
@@ -114,12 +110,10 @@ public class PostCreateBatchChunk {
}
private ItemWriter<PostBackUp> testWriter() {
log.info(">>>>> testWriter");
return postBackUpRepository::saveAll;
}
private Step terminateStep() {
log.info(">>>>> terminateStep");
return new StepBuilder("terminateStep")
.repository(jobRepository)
.transactionManager(transactionManager)
@@ -128,7 +122,6 @@ public class PostCreateBatchChunk {
}
private Tasklet terminateTasklet() {
log.info(">>>>> terminateTasklet");
return (contribution, chunkContext) -> {
log.error("List Read Error : List is null");
return RepeatStatus.FINISHED;

View File

@@ -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

View File

@@ -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

View File

@@ -5,9 +5,10 @@ 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.SecondaryMapper;
import com.spring.infra.db.orm.mybatis.annotation.PrimaryMapper;
// import com.spring.infra.db.orm.mybatis.annotation.SecondaryMapper;
@SecondaryMapper
@PrimaryMapper
public interface PostMapper {
List<Post> findAll();
void save(@Param("post") Post post);

View File

@@ -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<PostBackUp, Long> {
}

View File

@@ -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<Post, Long> {
}

View File

@@ -1,45 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_BLOB_TRIGGERS")
@Getter
public class QrtzBlobTriggers {
@EmbeddedId
private QrtzBlobTriggersId id;
@Column(name = "BLOB_DATA")
private byte[] blobData;
@ManyToOne
@JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false)
@JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false)
@JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false)
private QrtzTriggers qrtzTriggers;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzBlobTriggersId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "TRIGGER_NAME", length = 200, nullable = false)
private String triggerName;
@Column(name = "TRIGGER_GROUP", length = 200, nullable = false)
private String triggerGroup;
}
}

View File

@@ -1,34 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_CALENDARS")
@Getter
public class QrtzCalendars {
@EmbeddedId
private QrtzCalendarsId id;
@Column(name = "CALENDAR", nullable = false)
private byte[] calendar;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzCalendarsId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "CALENDAR_NAME", length = 200, nullable = false)
private String calendarName;
}
}

View File

@@ -1,52 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_CRON_TRIGGERS")
@Getter
public class QrtzCronTriggers {
@EmbeddedId
private QrtzCronTriggersId id;
@Column(name = "CRON_EXPRESSION", length = 120, nullable = false)
private String cronExpression;
@Column(name = "TIME_ZONE_ID", length = 80)
private String timeZoneId;
@ManyToOne
@JoinColumns(value = {
@JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false),
@JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false),
@JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false)
}, foreignKey = @ForeignKey(name = "FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS"))
private QrtzTriggers qrtzTriggers;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzCronTriggersId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "TRIGGER_NAME", length = 200, nullable = false)
private String triggerName;
@Column(name = "TRIGGER_GROUP", length = 200, nullable = false)
private String triggerGroup;
}
}

View File

@@ -1,64 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_FIRED_TRIGGERS")
@Getter
public class QrtzFiredTriggers {
@EmbeddedId
private QrtzFiredTriggersId id;
@Column(name = "TRIGGER_NAME", length = 200, nullable = false)
private String triggerName;
@Column(name = "TRIGGER_GROUP", length = 200, nullable = false)
private String triggerGroup;
@Column(name = "INSTANCE_NAME", length = 200, nullable = false)
private String instanceName;
@Column(name = "FIRED_TIME", nullable = false)
private long firedTime;
@Column(name = "SCHED_TIME", nullable = false)
private long schedTime;
@Column(name = "PRIORITY", nullable = false)
private int priority;
@Column(name = "STATE", length = 16, nullable = false)
private String state;
@Column(name = "JOB_NAME", length = 200)
private String jobName;
@Column(name = "JOB_GROUP", length = 200)
private String jobGroup;
@Column(name = "IS_NONCONCURRENT")
private Boolean isNonConcurrent;
@Column(name = "REQUESTS_RECOVERY")
private Boolean requestsRecovery;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzFiredTriggersId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "ENTRY_ID", length = 95, nullable = false)
private String entryId;
}
}

View File

@@ -1,55 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_JOB_DETAILS")
@Getter
public class QrtzJobDetails {
@EmbeddedId
private QrtzJobDetailsId id;
@Column(name = "DESCRIPTION", length = 250)
private String description;
@Column(name = "JOB_CLASS_NAME", length = 250, nullable = false)
private String jobClassName;
@Column(name = "IS_DURABLE", nullable = false)
private boolean isDurable;
@Column(name = "IS_NONCONCURRENT", nullable = false)
private boolean isNonConcurrent;
@Column(name = "IS_UPDATE_DATA", nullable = false)
private boolean isUpdateData;
@Column(name = "REQUESTS_RECOVERY", nullable = false)
private boolean requestsRecovery;
@Column(name = "JOB_DATA")
private byte[] jobData;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzJobDetailsId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "JOB_NAME", length = 200, nullable = false)
private String jobName;
@Column(name = "JOB_GROUP", length = 200, nullable = false)
private String jobGroup;
}
}

View File

@@ -1,31 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_LOCKS")
@Getter
public class QrtzLocks {
@EmbeddedId
private QrtzLocksId id;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzLocksId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "LOCK_NAME", length = 40, nullable = false)
private String lockName;
}
}

View File

@@ -1,31 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_PAUSED_TRIGGER_GRPS")
@Getter
public class QrtzPausedTriggerGrps {
@EmbeddedId
private QrtzPausedTriggerGrpsId id;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzPausedTriggerGrpsId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "TRIGGER_GROUP", length = 200, nullable = false)
private String triggerGroup;
}
}

View File

@@ -1,37 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_SCHEDULER_STATE")
@Getter
public class QrtzSchedulerState {
@EmbeddedId
private QrtzSchedulerStateId id;
@Column(name = "LAST_CHECKIN_TIME", nullable = false)
private long lastCheckinTime;
@Column(name = "CHECKIN_INTERVAL", nullable = false)
private long checkinInterval;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzSchedulerStateId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "INSTANCE_NAME", length = 200, nullable = false)
private String instanceName;
}
}

View File

@@ -1,55 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_SIMPLE_TRIGGERS")
@Getter
public class QrtzSimpleTriggers {
@EmbeddedId
private QrtzSimpleTriggersId id;
@Column(name = "REPEAT_COUNT", nullable = false)
private long repeatCount;
@Column(name = "REPEAT_INTERVAL", nullable = false)
private long repeatInterval;
@Column(name = "TIMES_TRIGGERED", nullable = false)
private long timesTriggered;
@ManyToOne
@JoinColumns(value = {
@JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false),
@JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false),
@JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false)
}, foreignKey = @ForeignKey(name = "FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS"))
private QrtzTriggers qrtzTriggers;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzSimpleTriggersId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "TRIGGER_NAME", length = 200, nullable = false)
private String triggerName;
@Column(name = "TRIGGER_GROUP", length = 200, nullable = false)
private String triggerGroup;
}
}

View File

@@ -1,80 +0,0 @@
package com.spring.domain.quartz.entity;
import java.io.Serializable;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_SIMPROP_TRIGGERS")
@Getter
public class QrtzSimpropTriggers {
@EmbeddedId
private QrtzSimpropTriggersId id;
@Column(name = "STR_PROP_1", length = 512)
private String strProp1;
@Column(name = "STR_PROP_2", length = 512)
private String strProp2;
@Column(name = "STR_PROP_3", length = 512)
private String strProp3;
@Column(name = "INT_PROP_1")
private Integer intProp1;
@Column(name = "INT_PROP_2")
private Integer intProp2;
@Column(name = "LONG_PROP_1")
private Long longProp1;
@Column(name = "LONG_PROP_2")
private Long longProp2;
@Column(name = "DEC_PROP_1", precision = 13, scale = 4)
private BigDecimal decProp1;
@Column(name = "DEC_PROP_2", precision = 13, scale = 4)
private BigDecimal decProp2;
@Column(name = "BOOL_PROP_1")
private Boolean boolProp1;
@Column(name = "BOOL_PROP_2")
private Boolean boolProp2;
@ManyToOne
@JoinColumns(value = {
@JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false),
@JoinColumn(name = "TRIGGER_NAME", referencedColumnName = "TRIGGER_NAME", insertable = false, updatable = false),
@JoinColumn(name = "TRIGGER_GROUP", referencedColumnName = "TRIGGER_GROUP", insertable = false, updatable = false)
}, foreignKey = @ForeignKey(name = "FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS"))
private QrtzTriggers qrtzTriggers;
@Embeddable
@Getter
@EqualsAndHashCode
public static class QrtzSimpropTriggersId implements Serializable {
@Column(name = "SCHED_NAME", length = 120, nullable = false)
private String schedName;
@Column(name = "TRIGGER_NAME", length = 200, nullable = false)
private String triggerName;
@Column(name = "TRIGGER_GROUP", length = 200, nullable = false)
private String triggerGroup;
}
}

View File

@@ -0,0 +1,34 @@
package com.spring.domain.schedule.api;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.spring.domain.schedule.dto.BatchChartResponse;
import com.spring.domain.schedule.dto.RecentJobResponse;
import com.spring.domain.schedule.service.BatchChartService;
import com.spring.domain.schedule.service.DashBoardJobService;
import lombok.RequiredArgsConstructor;
@RequestMapping("/api/dashboard")
@RestController
@RequiredArgsConstructor
public class DashBoardApi {
private final BatchChartService batchChartService;
private final DashBoardJobService dashBoardJobService;
@GetMapping("/chart/batch")
public BatchChartResponse getBatchJobExecutionData() {
return batchChartService.getBatchJobExecutionData();
}
@GetMapping("/recent-job")
public List<RecentJobResponse> getRecentJobs() {
return dashBoardJobService.getRecentJobs();
}
}

View File

@@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.RestController;
import com.spring.domain.schedule.dto.ReScheduleJobRequest;
import com.spring.domain.schedule.dto.ScheduleJobResponse;
import com.spring.domain.schedule.service.FindJobGroupService;
import com.spring.domain.schedule.service.FindScheduleJobService;
import com.spring.domain.schedule.service.ReScheduleJobService;
import com.spring.domain.schedule.service.ScheduleControlService;
@@ -25,26 +24,17 @@ import lombok.RequiredArgsConstructor;
public class ScheduleJobApi {
private final FindScheduleJobService findScheduleJobService;
private final FindJobGroupService findJobGroupService;
private final ReScheduleJobService reScheduleJobService;
private final ScheduleControlService scheduleControlService;
@GetMapping
public List<ScheduleJobResponse> getAllJobs(
@RequestParam(required = false) String groupName,
@RequestParam(required = false) String jobName
) {
public List<ScheduleJobResponse> getAllJobs(@RequestParam(required = false) String groupName, @RequestParam(required = false) String jobName) {
return findScheduleJobService.getAllJobs(groupName, jobName);
}
@GetMapping("/groups")
public List<String> getJobGroups() {
return findJobGroupService.getJobGroups();
}
@GetMapping("/group/{groupName}/names")
public List<String> getJobNamesByGroup(@PathVariable String groupName) {
return findJobGroupService.getJobNamesByGroup(groupName);
@GetMapping("/{groupName}/{jobName}")
public ScheduleJobResponse getJobDetail(@PathVariable String groupName, @PathVariable String jobName) {
return findScheduleJobService.getJobDetail(groupName, jobName);
}
@GetMapping("/pause/{groupName}/{jobName}")

View File

@@ -0,0 +1,20 @@
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;
@Getter
@RequiredArgsConstructor
public class BatchChartResponse {
private final List<BatchJobAverageDurationProjection> jobAvgSummary;
private final Map<BatchStatus, Long> statusCounts;
private final List<BatchJobHourProjection> jobHourSummary;
private final List<BatchJobExecutionProjection> jobExecutionSummary;
}

View File

@@ -0,0 +1,9 @@
package com.spring.domain.schedule.dto;
import java.time.LocalDateTime;
public interface BatchJobAverageDurationProjection {
String getJobName();
LocalDateTime getStartTime();
LocalDateTime getEndTime();
}

View File

@@ -0,0 +1,9 @@
package com.spring.domain.schedule.dto;
import java.time.LocalDateTime;
public interface BatchJobExecutionProjection {
LocalDateTime getExecutionDate();
String getJobName();
Long getExecutionCount();
}

View File

@@ -0,0 +1,9 @@
package com.spring.domain.schedule.dto;
public interface BatchJobHourProjection {
String getJobName();
Integer getHour();
Long getCount();
}

View File

@@ -0,0 +1,8 @@
package com.spring.domain.schedule.dto;
import org.springframework.batch.core.BatchStatus;
public interface BatchJobStatusCountProjection {
BatchStatus getStatus();
Long getCount();
}

View File

@@ -0,0 +1,14 @@
package com.spring.domain.schedule.dto;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class QuartzChartResponse {
private final List<QuartzJobFrequencyProjection> jobFrequencys;
}

View File

@@ -0,0 +1,6 @@
package com.spring.domain.schedule.dto;
public interface QuartzJobFrequencyProjection {
String getJobName();
Long getCount();
}

View File

@@ -0,0 +1,15 @@
package com.spring.domain.schedule.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class RecentJobResponse {
private final String jobName;
private final String jobGroup;
private final Long firedTime;
private final String state;
}

View File

@@ -1,22 +1,23 @@
package com.spring.domain.batch.entity;
package com.spring.domain.schedule.entity;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.ForeignKey;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Getter;
// @Entity
// @Table(name = "BATCH_JOB_EXECUTION")
@Entity
@Table(name = "BATCH_JOB_EXECUTION")
@Getter
public class BatchJobExecution {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "JOB_EXECUTION_ID")
@@ -26,17 +27,17 @@ public class BatchJobExecution {
private Long version;
@ManyToOne
@JoinColumn(name = "JOB_INSTANCE_ID", nullable = false, foreignKey = @ForeignKey(name = "JOB_INST_EXEC_FK"))
private BatchJobInstance batchJobInstance;
@JoinColumn(name = "JOB_INSTANCE_ID", nullable = false)
private BatchJobInstance jobInstance;
@Column(name = "CREATE_TIME", nullable = false)
private Timestamp createTime;
private LocalDateTime createTime;
@Column(name = "START_TIME")
private Timestamp startTime;
private LocalDateTime startTime;
@Column(name = "END_TIME")
private Timestamp endTime;
private LocalDateTime endTime;
@Column(name = "STATUS", length = 10)
private String status;
@@ -48,6 +49,9 @@ public class BatchJobExecution {
private String exitMessage;
@Column(name = "LAST_UPDATED")
private Timestamp lastUpdated;
private LocalDateTime lastUpdated;
@Column(name = "JOB_CONFIGURATION_LOCATION", length = 2500)
private String jobConfigurationLocation;
}

View File

@@ -1,18 +1,19 @@
package com.spring.domain.batch.entity;
package com.spring.domain.schedule.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Getter;
// @Entity
// @Table(name = "BATCH_JOB_INSTANCE",
// uniqueConstraints = @UniqueConstraint(name = "JOB_INST_UN", columnNames = {"JOB_NAME", "JOB_KEY"}))
@Entity
@Table(name = "BATCH_JOB_INSTANCE")
@Getter
public class BatchJobInstance {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "JOB_INSTANCE_ID")
@@ -21,10 +22,10 @@ public class BatchJobInstance {
@Column(name = "VERSION")
private Long version;
@Column(name = "JOB_NAME", length = 100, nullable = false)
@Column(name = "JOB_NAME", nullable = false, length = 100)
private String jobName;
@Column(name = "JOB_KEY", length = 32, nullable = false)
@Column(name = "JOB_KEY", nullable = false, length = 32)
private String jobKey;
}

View File

@@ -1,26 +1,31 @@
package com.spring.domain.quartz.entity;
package com.spring.domain.schedule.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.ForeignKey;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.Table;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// @Entity
// @Table(name = "QRTZ_TRIGGERS")
@Entity
@Table(name = "QRTZ_TRIGGERS")
@Getter
public class QrtzTriggers {
@EmbeddedId
private QrtzTriggersId id;
@Column(name = "JOB_NAME", nullable = false, length = 200)
private String jobName;
@Column(name = "JOB_GROUP", nullable = false, length = 200)
private String jobGroup;
@Column(name = "DESCRIPTION", length = 250)
private String description;
@@ -28,15 +33,15 @@ public class QrtzTriggers {
private Long nextFireTime;
@Column(name = "PREV_FIRE_TIME")
private Long prevFireTime;
private Long previousFireTime;
@Column(name = "PRIORITY")
private Integer priority;
@Column(name = "TRIGGER_STATE", length = 16, nullable = false)
@Column(name = "TRIGGER_STATE", nullable = false, length = 16)
private String triggerState;
@Column(name = "TRIGGER_TYPE", length = 8, nullable = false)
@Column(name = "TRIGGER_TYPE", nullable = false, length = 8)
private String triggerType;
@Column(name = "START_TIME", nullable = false)
@@ -49,19 +54,12 @@ public class QrtzTriggers {
private String calendarName;
@Column(name = "MISFIRE_INSTR")
private Short misfireInstr;
private Short misfireInstruction;
@Lob
@Column(name = "JOB_DATA")
private byte[] jobData;
@ManyToOne
@JoinColumns(value = {
@JoinColumn(name = "SCHED_NAME", referencedColumnName = "SCHED_NAME", insertable = false, updatable = false),
@JoinColumn(name = "JOB_NAME", referencedColumnName = "JOB_NAME", insertable = false, updatable = false),
@JoinColumn(name = "JOB_GROUP", referencedColumnName = "JOB_GROUP", insertable = false, updatable = false)
}, foreignKey = @ForeignKey(name = "FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS"))
private QrtzJobDetails qrtzJobDetails;
@Embeddable
@Getter
@EqualsAndHashCode

View File

@@ -0,0 +1,16 @@
package com.spring.domain.schedule.error;
import com.spring.common.error.BizBaseException;
import com.spring.common.error.ExceptionRule;
public class ScheduleNotFoundException extends BizBaseException {
public ScheduleNotFoundException() {
super(ExceptionRule.SCHEDULE_NOT_FOUND);
}
public ScheduleNotFoundException(ExceptionRule exceptionRule) {
super(exceptionRule);
}
}

View File

@@ -0,0 +1,52 @@
package com.spring.domain.schedule.repository;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.spring.domain.schedule.dto.BatchJobAverageDurationProjection;
import com.spring.domain.schedule.dto.BatchJobExecutionProjection;
import com.spring.domain.schedule.dto.BatchJobHourProjection;
import com.spring.domain.schedule.dto.BatchJobStatusCountProjection;
import com.spring.domain.schedule.entity.BatchJobExecution;
public interface BatchJobExecutionRepository extends JpaRepository<BatchJobExecution, Long> {
@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<BatchJobAverageDurationProjection> findJobAverageDurations(@Param("startDate") LocalDateTime startDate);
@Query("SELECT bje.status AS status, COUNT(bje) AS count " +
"FROM BatchJobExecution bje " +
"GROUP BY bje.status")
List<BatchJobStatusCountProjection> findJobStatusCounts();
@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<BatchJobExecutionProjection> findJobExecutionSummaryDays(@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 bje.startTime >= :startDate " +
"GROUP BY bji.jobName, FUNCTION('HOUR', bje.startTime) " +
"ORDER BY bji.jobName, hour")
List<BatchJobHourProjection> findHourlyJobExecutionDistribution(@Param("startDate") LocalDateTime startDate);
}

View File

@@ -0,0 +1,58 @@
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 com.spring.domain.schedule.dto.BatchChartResponse;
import com.spring.domain.schedule.dto.BatchJobAverageDurationProjection;
import com.spring.domain.schedule.dto.BatchJobExecutionProjection;
import com.spring.domain.schedule.dto.BatchJobHourProjection;
import com.spring.domain.schedule.dto.BatchJobStatusCountProjection;
import com.spring.domain.schedule.repository.BatchJobExecutionRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class BatchChartService {
private final BatchJobExecutionRepository batchJobExecutionRepository;
public BatchChartResponse getBatchJobExecutionData() {
return new BatchChartResponse(
getJobAverageDurations(),
getJobStatusCounts(),
getHourlyJobExecutionDistribution(),
getJobExecutionSummary()
);
}
private List<BatchJobAverageDurationProjection> getJobAverageDurations() {
LocalDateTime startDate = LocalDateTime.now().minusDays(30);
return batchJobExecutionRepository.findJobAverageDurations(startDate);
}
private Map<BatchStatus, Long> getJobStatusCounts() {
return batchJobExecutionRepository.findJobStatusCounts().stream()
.collect(Collectors.toMap(
BatchJobStatusCountProjection::getStatus,
BatchJobStatusCountProjection::getCount
));
}
private List<BatchJobHourProjection> getHourlyJobExecutionDistribution() {
LocalDateTime startDate = LocalDateTime.now().minusDays(30);
return batchJobExecutionRepository.findHourlyJobExecutionDistribution(startDate);
}
private List<BatchJobExecutionProjection> getJobExecutionSummary() {
LocalDateTime startDate = LocalDateTime.now().minusDays(30);
return batchJobExecutionRepository.findJobExecutionSummaryDays(startDate);
}
}

View File

@@ -0,0 +1,51 @@
package com.spring.domain.schedule.service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.stereotype.Service;
import com.spring.common.error.BizBaseException;
import com.spring.domain.schedule.dto.RecentJobResponse;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class DashBoardJobService {
private final Scheduler scheduler;
public List<RecentJobResponse> getRecentJobs() {
List<RecentJobResponse> recentJobs = new ArrayList<>();
try {
for (String groupName : scheduler.getJobGroupNames()) {
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
for (Trigger trigger : triggers) {
recentJobs.add(new RecentJobResponse(
jobKey.getName(),
jobKey.getGroup(),
trigger.getPreviousFireTime() != null ? trigger.getPreviousFireTime().getTime() : null,
scheduler.getTriggerState(trigger.getKey()).name()
));
}
}
}
} catch (SchedulerException e) {
throw new BizBaseException();
}
return recentJobs.stream()
.filter(job -> job.getFiredTime() != null)
.sorted((j1, j2) -> Long.compare(j2.getFiredTime(), j1.getFiredTime()))
.limit(10)
.collect(Collectors.toList());
}
}

View File

@@ -1,40 +0,0 @@
package com.spring.domain.schedule.service;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class FindJobGroupService {
private final Scheduler scheduler;
public List<String> getJobGroups() {
try {
return scheduler.getJobGroupNames();
} catch (SchedulerException e) {
return Collections.emptyList();
}
}
public List<String> getJobNamesByGroup(String groupName) {
try {
return scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))
.stream()
.map(JobKey::getName)
.collect(Collectors.toList());
} catch (SchedulerException e) {
return Collections.emptyList();
}
}
}

View File

@@ -19,7 +19,9 @@ import org.quartz.Trigger;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.stereotype.Service;
import com.spring.common.error.BizBaseException;
import com.spring.domain.schedule.dto.ScheduleJobResponse;
import com.spring.domain.schedule.error.ScheduleNotFoundException;
import lombok.RequiredArgsConstructor;
@@ -29,7 +31,7 @@ public class FindScheduleJobService {
private final Scheduler scheduler;
public List<ScheduleJobResponse> getAllJobs(String groupName, String jobName) {
public List<ScheduleJobResponse> getAllJobs(final String groupName, final String jobName) {
try {
return getFilteredJobKeys(groupName, jobName)
.map(this::createScheduleSafely)
@@ -39,19 +41,31 @@ public class FindScheduleJobService {
return Collections.emptyList();
}
}
public ScheduleJobResponse getJobDetail(final String groupName, final String jobName) {
try {
JobKey jobKey = new JobKey(jobName, groupName);
if (scheduler.checkExists(jobKey)) {
return createSchedule(jobKey);
}
throw new ScheduleNotFoundException();
} catch (SchedulerException e) {
throw new BizBaseException();
}
}
private Stream<JobKey> getFilteredJobKeys(String groupName, String jobName) throws SchedulerException {
private Stream<JobKey> getFilteredJobKeys(final String groupName, final String jobName) throws SchedulerException {
return scheduler.getJobGroupNames().stream()
.filter(group -> isGroupMatched(group, groupName))
.flatMap(this::getJobKeysForGroup)
.filter(jobKey -> isJobMatched(jobKey, jobName));
}
private boolean isGroupMatched(String group, String groupName) {
private boolean isGroupMatched(final String group, final String groupName) {
return groupName == null || groupName.isEmpty() || group.equals(groupName);
}
private Stream<JobKey> getJobKeysForGroup(String group) {
private Stream<JobKey> getJobKeysForGroup(final String group) {
try {
return scheduler.getJobKeys(GroupMatcher.jobGroupEquals(group)).stream();
} catch (SchedulerException e) {
@@ -59,11 +73,11 @@ public class FindScheduleJobService {
}
}
private boolean isJobMatched(JobKey jobKey, String jobName) {
private boolean isJobMatched(final JobKey jobKey, final String jobName) {
return jobName == null || jobName.isEmpty() || jobKey.getName().equals(jobName);
}
private ScheduleJobResponse createScheduleSafely(JobKey jobKey) {
private ScheduleJobResponse createScheduleSafely(final JobKey jobKey) {
try {
return createSchedule(jobKey);
} catch (SchedulerException e) {
@@ -71,10 +85,9 @@ public class FindScheduleJobService {
}
}
private ScheduleJobResponse createSchedule(JobKey jobKey) throws SchedulerException {
private ScheduleJobResponse createSchedule(final JobKey jobKey) throws SchedulerException {
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
Optional<Trigger> trigger = Optional.ofNullable(scheduler.getTriggersOfJob(jobKey).stream().findFirst().orElse(null));
return new ScheduleJobResponse(
jobKey.getName(),
jobKey.getGroup(),
@@ -83,23 +96,23 @@ public class FindScheduleJobService {
getTriggerState(trigger),
jobDetail.getJobClass().getName(),
jobDetail.getJobDataMap().toString(),
trigger.map(t -> t.getClass().getSimpleName()).orElse("UNKNOWN"),
trigger.map(t -> t.getClass().getSimpleName()).orElse(""),
trigger.map(Trigger::getNextFireTime).map(this::convertToLocalDateTime).orElse(null),
trigger.map(Trigger::getPreviousFireTime).map(this::convertToLocalDateTime).orElse(null)
);
}
private String getTriggerState(Optional<Trigger> trigger) {
private String getTriggerState(final Optional<Trigger> trigger) {
return trigger.map(t -> {
try {
return scheduler.getTriggerState(t.getKey()).name();
} catch (SchedulerException e) {
return "UNKNOWN";
return "";
}
}).orElse("UNKNOWN");
}).orElse("");
}
private String getCronExpression(Optional<Trigger> trigger) {
private String getCronExpression(final Optional<Trigger> trigger) {
return trigger.map(t -> {
if (t instanceof CronTrigger) {
return ((CronTrigger) t).getCronExpression();
@@ -108,7 +121,7 @@ public class FindScheduleJobService {
}).orElse("N/A");
}
private LocalDateTime convertToLocalDateTime(Date date) {
private LocalDateTime convertToLocalDateTime(final Date date) {
if (date == null) return null;
return date.toInstant()
.atZone(ZoneId.systemDefault())

View File

@@ -77,7 +77,7 @@ public class QuartzConfig {
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setJobFactory(jobFactory);
factory.setAutoStartup(false);
factory.setAutoStartup(true);
factory.setWaitForJobsToCompleteOnShutdown(true);
return factory;
}

View File

@@ -38,7 +38,7 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
jwtTokenService.generateAccessToken(response, authentication);
jwtTokenService.generateRefreshToken(response, authentication);
SavedRequest savedRequest = requestCache.getRequest(request, response);
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : "/main";
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : "/dashboard";
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
Map<String, Object> responseBody = Map.of("status", true, "redirectUrl", targetUrl);

View File

@@ -5,12 +5,12 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/main")
public class MainController {
@RequestMapping("/dashboard")
public class DashBoardController {
@GetMapping
public String main() {
return "pages/main/main";
}
public String dashboard() {
return "pages/dashboard/dashboard";
}
}

View File

@@ -10,6 +10,7 @@ public class ScheduleController {
@GetMapping
public String schedule() {
return "pages/schedule/schedule-list";
return "pages/schedule/schedule";
}
}

View File

@@ -23,9 +23,4 @@ public class SignController {
return "pages/sign/sign-in";
}
@GetMapping("/sign-up")
public String signUp() {
return "pages/sign/sign-up";
}
}

View File

@@ -29,7 +29,7 @@ spring:
# init:
# mode: always
# schema-locations:
# - classpath:batch-schema.sql
# - classpath:sql-schema/test-schema.sql
# - classpath:quartz-schema.sql
jpa:
@@ -92,7 +92,7 @@ spring:
jwt:
access-token:
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
expiration: 1
expiration: 15
refresh-token:
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
expiration: 10080

View File

@@ -685,7 +685,6 @@ h6 {
font-weight: 600;
color: #4154f1;
transition: 0.3;
background: #f6f9ff;
padding: 10px 15px;
border-radius: 4px;
}

View File

@@ -0,0 +1,11 @@
import apiClient from '../common/axios-instance.js';
export const getBatchJobExecutionData = async () => {
const response = await apiClient.get('/api/dashboard/chart/batch');
return response.data;
}
export const getRecentJobs = async () => {
const response = await apiClient.get('/api/dashboard/recent-job');
return response.data;
}

View File

@@ -5,13 +5,8 @@ export const getAllJobs = async (searchParams) => {
return response.data;
}
export const getJobGroups = async () => {
const response = await apiClient.get('/api/schedule/groups');
return response.data;
}
export const getJobNamesByGroup = async (groupName) => {
const response = await apiClient.get(`/api/schedule/group/${groupName}/names`);
export const getJobDetail = async (groupName, jobName) => {
const response = await apiClient.get(`/api/schedule/${groupName}/${jobName}`);
return response.data;
}

View File

@@ -2,9 +2,10 @@ import apiClient from '../common/axios-instance.js';
export const signIn = async (username, password) => {
const response = await apiClient.post('/sign-in', {username, password});
return response.data;
return response;
};
export const signUp = async (loginId, password, userName) => {
await apiClient.post('/api/user/sign-up', {loginId, password, userName});
const response = await apiClient.post('/api/user/sign-up', {loginId, password, userName});
return response.data;
};

View File

@@ -20,16 +20,26 @@ apiClient.interceptors.request.use(
// 응답 인터셉터 추가
apiClient.interceptors.response.use(
(response) => {
return response;
return response.data;
},
async (error) => {
if (error.response) {
const status = error.response.status;
const message = error.response.data.message;
if (message) {
alert(message);
const responseData = error.response.data;
let alertMessage = responseData.message;
if (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) {
const firstError = responseData.data[0];
if (firstError.reason) {
alertMessage = firstError.reason;
}
}
if (status == 401) {
if (alertMessage) {
alert(alertMessage);
}
if (status === 401) {
location.href = "/";
}
return new Promise(() => {});

View File

@@ -0,0 +1 @@
dayjs.locale('ko');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
/*!
* chartjs-adapter-luxon v1.3.1
* https://www.chartjs.org
* (c) 2023 chartjs-adapter-luxon Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_ko=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var d=_(e),t={name:"ko",weekdays:"일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"),weekdaysShort:"일_월_화_수_목_금_토".split("_"),weekdaysMin:"일_월_화_수_목_금_토".split("_"),months:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),monthsShort:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),ordinal:function(e){return e+"일"},formats:{LT:"A h:mm",LTS:"A h:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY년 MMMM D일",LLL:"YYYY년 MMMM D일 A h:mm",LLLL:"YYYY년 MMMM D일 dddd A h:mm",l:"YYYY.MM.DD.",ll:"YYYY년 MMMM D일",lll:"YYYY년 MMMM D일 A h:mm",llll:"YYYY년 MMMM D일 dddd A h:mm"},meridiem:function(e){return e<12?"오전":"오후"},relativeTime:{future:"%s 후",past:"%s 전",s:"몇 초",m:"1분",mm:"%d분",h:"한 시간",hh:"%d시간",d:"하루",dd:"%d일",M:"한 달",MM:"%d달",y:"일 년",yy:"%d년"}};return d.default.locale(t,null,!0),t}));

View File

@@ -0,0 +1,269 @@
import { getBatchJobExecutionData, getRecentJobs } from '../../apis/dashboard-api.js';
document.addEventListener('DOMContentLoaded', () => {
fetchDataAndRender();
});
const fetchDataAndRender = async () => {
const batchData = await getBatchJobExecutionData();
const recentJobs = await getRecentJobs();
renderBatchExecutionTimeChart(batchData.jobAvgSummary);
renderBatchStatusChart(batchData.statusCounts);
renderHourlyJobExecutionChart(batchData.jobHourSummary);
renderDailyJobExecutionsChart(batchData.jobExecutionSummary);
renderRecentJobsTable(recentJobs);
};
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
title: {
font: {
size: 16
}
}
}
};
let batchExecutionTimeChart;
const renderBatchExecutionTimeChart = (data) => {
const jobExecutionTimes = {};
data.forEach(job => {
if (job.endTime && job.startTime) {
const duration = (new Date(job.endTime) - new Date(job.startTime)) / 1000; // 초 단위
if (!jobExecutionTimes[job.jobName]) {
jobExecutionTimes[job.jobName] = { total: 0, count: 0 };
}
jobExecutionTimes[job.jobName].total += duration;
jobExecutionTimes[job.jobName].count++;
}
});
const averageExecutionTimes = Object.entries(jobExecutionTimes).reduce((acc, [jobName, job]) => {
acc[jobName] = job.total / job.count;
return acc;
}, {});
const ctx = document.getElementById('batchExecutionTimeChart').getContext('2d');
batchExecutionTimeChart = new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(averageExecutionTimes),
datasets: [{
label: '평균 실행 시간 (초)',
data: Object.values(averageExecutionTimes),
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins,
tooltip: {
callbacks: {
label: (context) => {
const label = context.dataset.label || '';
const value = context.parsed.y.toFixed(2);
return `${label}: ${value}`;
}
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '시간 (초)'
}
}
}
}
});
};
let batchStatusChart;
const renderBatchStatusChart = (data) => {
const ctx = document.getElementById('batchStatusChart').getContext('2d');
batchStatusChart = new Chart(ctx, {
type: 'pie',
data: {
labels: Object.keys(data),
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)'
]
}]
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins
}
}
});
};
let hourlyJobExecutionChart;
const renderHourlyJobExecutionChart = (data) => {
const ctx = document.getElementById('hourlyJobExecutionChart').getContext('2d');
const hours = Array.from({length: 24}, (_, i) => i);
const jobNames = [...new Set(data.map(item => item.jobName))];
const datasets = jobNames.map(jobName => {
const jobData = hours.map(hour => {
const hourData = data.find(item => item.jobName === jobName && item.hour === hour);
return hourData ? hourData.count : 0;
});
return {
label: jobName,
data: jobData,
borderColor: getRandomColor(),
backgroundColor: 'rgba(0, 0, 0, 0.1)',
fill: false
};
});
hourlyJobExecutionChart = new Chart(ctx, {
type: 'line',
data: {
labels: hours.map(hour => `${hour}:00`),
datasets: datasets
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins,
tooltip: {
mode: 'index',
intersect: false,
}
},
scales: {
x: {
display: true,
title: {
display: true,
text: '시간'
}
},
y: {
display: true,
title: {
display: true,
text: '실행 횟수'
},
suggestedMin: 0,
beginAtZero: true
}
}
}
});
};
let dailyJobExecutionsChart;
const renderDailyJobExecutionsChart = (data) => {
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 dates = [];
for (let d = new Date(firstDay); d <= lastDay; d.setDate(d.getDate() + 1)) {
dates.push(d.toISOString().split('T')[0]);
}
const groupedData = data.reduce((acc, item) => {
const date = item.executionDate.split('T')[0];
if (!acc[date]) {
acc[date] = {};
}
if (!acc[date][item.jobName]) {
acc[date][item.jobName] = 0;
}
acc[date][item.jobName] += item.executionCount;
return acc;
}, {});
const jobNames = [...new Set(data.map(item => item.jobName))];
const datasets = jobNames.map((jobName) => {
const color = getRandomColor();
return {
label: jobName,
data: dates.map(date => ({
x: luxon.DateTime.fromISO(date).toJSDate(),
y: groupedData[date]?.[jobName] || null
})),
borderColor: color,
backgroundColor: color,
fill: false,
spanGaps: true
};
});
dailyJobExecutionsChart = new Chart(ctx, {
type: 'line',
data: {
datasets: datasets
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins
},
scales: {
x: {
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MM-dd'
},
tooltipFormat: 'yyyy-MM-dd'
},
title: {
display: true,
text: '날짜'
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: '실행 횟수'
}
}
}
}
});
};
const renderRecentJobsTable = (recentJobs) => {
const tableBody = document.getElementById('recentJobsTable');
tableBody.innerHTML = recentJobs.map(job => `
<tr>
<td>${job.jobName}</td>
<td>${job.jobGroup}</td>
<td>${new Date(job.firedTime).toLocaleString()}</td>
<td>${job.state}</td>
</tr>
`).join('');
};
const getRandomColor = () => {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
return `rgb(${r}, ${g}, ${b})`;
};

View File

@@ -1,65 +0,0 @@
import {getAllJobs} from '../../apis/schedule-api.js';
document.addEventListener('DOMContentLoaded', () => {
const searchForm = document.getElementById('searchForm');
const tableBody = document.querySelector('tbody');
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(searchForm);
const searchParams = new URLSearchParams(formData);
const response = await getAllJobs(searchParams);
updateTable(response.data);
});
function updateTable(jobs) {
tableBody.innerHTML = '';
jobs.forEach(job => {
const row = `
<tr>
<td>${job.group}</td>
<td>${job.name}</td>
<td>${job.description || '-'}</td>
<td>${job.cronExpression}</td>
<td>${formatDateTime(job.nextFireTime)}</td>
<td>${formatDateTime(job.previousFireTime)}</td>
<td class="text-center">
<span class="badge ${getStatusBadgeClass(job.status)} w-100 py-2">${job.status}</span>
</td>
<td class="text-center">
<span class="badge bg-secondary w-100 py-2"><i class="bi bi-eye"></i> 상세</span>
</td>
</tr>
`;
tableBody.insertAdjacentHTML('beforeend', row);
});
}
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '-';
const date = new Date(dateTimeString);
return date.toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function getStatusBadgeClass(status) {
switch (status) {
case 'NORMAL':
return 'bg-success';
case 'PAUSED':
return 'bg-warning';
case 'COMPLETE':
return 'bg-info';
case 'ERROR':
return 'bg-danger';
default:
return 'bg-secondary';
}
}
});

View File

@@ -0,0 +1,90 @@
import { getAllJobs, getJobDetail, triggerJob, pauseJob, resumeJob } from '../../apis/schedule-api.js';
document.addEventListener('DOMContentLoaded', () => {
const searchForm = document.getElementById('searchForm');
const tableBody = document.querySelector('tbody');
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(searchForm);
const searchParams = new URLSearchParams(formData);
const response = await getAllJobs(searchParams);
updateTable(response);
});
const updateTable = (jobs) => {
tableBody.innerHTML = jobs.map(job => `
<tr>
<td>${job.group}</td>
<td>${job.name}</td>
<td>${job.cronExpression}</td>
<td><span class="badge ${getStatusBadgeClass(job.status)}">${job.status}</span></td>
<td>
<button class="badge btn btn-sm btn-secondary detail-btn" data-group="${job.group}" data-name="${job.name}">
<i class="bi bi-eye"></i> 상세
</button>
</td>
</tr>
`).join('');
document.querySelectorAll('.detail-btn').forEach(btn => {
btn.addEventListener('click', showJobDetail);
});
};
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 = `
<div class="card">
<ul class="list-group list-group-flush">
${createDetailItem('그룹', jobDetail.group, 'bi-people')}
${createDetailItem('잡 이름', jobDetail.name, 'bi-briefcase')}
${createDetailItem('설명', jobDetail.description || '-', 'bi-card-text')}
${createDetailItem('스케줄', `<input type="text" class="form-control form-control-sm" id="cronExpression" value="${jobDetail.cronExpression}">`, 'bi-calendar-event')}
${createDetailItem('다음 실행', formatDateTime(jobDetail.nextFireTime), 'bi-clock')}
${createDetailItem('이전 실행', formatDateTime(jobDetail.previousFireTime), 'bi-clock-history')}
${createDetailItem('상태', `<span class="badge ${getStatusBadgeClass(jobDetail.status)}">${jobDetail.status}</span>`, 'bi-activity')}
</ul>
</div>
`;
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) => `
<li class="list-group-item">
<div class="row align-items-center">
<div class="col-4">
<i class="bi ${iconClass} text-primary me-2"></i>
<strong>${label}</strong>
</div>
<div class="col-8">
${value}
</div>
</div>
</li>
`;
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return '-';
const date = new Date(dateTimeString);
return dayjs(date).format("YYYY-MM-DD ddd A HH:mm:ss");
};
const getStatusBadgeClass = (status) => {
const statusClasses = {
'NORMAL': 'bg-success',
'PAUSED': 'bg-warning',
'COMPLETE': 'bg-info',
'ERROR': 'bg-danger'
};
return statusClasses[status] || 'bg-secondary';
};
});

View File

@@ -1,13 +1,37 @@
import {signIn} from '../../apis/sign-api.js';
import {signIn, signUp} from '../../apis/sign-api.js';
document.getElementById('signinForm').addEventListener('submit', function(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
signIn(username, password)
.then(response => {
document.addEventListener('DOMContentLoaded', () => {
const signInButton = document.getElementById('signIn');
const signupButton = document.getElementById('signUp');
const signupModal = new bootstrap.Modal(document.getElementById('signupModal'));
const signupSubmit = document.getElementById('signupSubmit');
signInButton.addEventListener('click', (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
signIn(username, password).then(response => {
if (response.status) {
window.location.href = response.redirectUrl;
}
});
});
});
signupButton.addEventListener('click', (e) => {
e.preventDefault();
signupModal.show();
});
signupSubmit.addEventListener('click', (e) => {
e.preventDefault();
const loginId = document.getElementById('loginId').value;
const password = document.getElementById('loginPassword').value;
const userName = document.getElementById('userName').value;
signUp(loginId, password, userName).then(response => {
alert(`회원가입이 완료 되었습니다.`);
signupModal.hide();
});
});
});

View File

@@ -1,9 +0,0 @@
import {signUp} from '../../apis/sign-api.js';
document.getElementById('signupForm').addEventListener('submit', function(event) {
event.preventDefault();
const loginId = document.getElementById('loginId').value;
const password = document.getElementById('password').value;
const userName = document.getElementById('userName').value;
signUp(loginId, password, userName);
});

View File

@@ -9,4 +9,13 @@
</script>
<script th:src="@{/js/lib/axios/axios.min.js}"></script>
<script th:src="@{/js/lib/cookie/js.cookie.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/popper.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/bootstrap.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/chart.js}"></script>
<script th:src="@{/js/lib/bootstrap/luxon.min.js}"></script>
<script th:src="@{/js/lib/bootstrap/chartjs-adapter-luxon.umd.min.js}"></script>
<script th:src="@{/js/lib/dayjs/dayjs.min.js}"></script>
<script th:src="@{/js/lib/dayjs/locale/ko.js}"></script>
<script th:src="@{/js/common/common.js}"></script>
</html>

View File

@@ -1,6 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="footer" lang="ko" xml:lang="ko">
<footer class="bg-light text-center p-3">
<p>회사 정보: © 2023 회사명. 모든 권리 보유.</p>
</footer>
</html>

View File

@@ -3,42 +3,13 @@
<aside id="sidebar" class="sidebar">
<ul class="sidebar-nav" id="sidebar-nav">
<li class="nav-item">
<a class="nav-link " href="index.html"><i class="bi bi-grid"></i><span>Dashboard</span></a>
<a class="nav-link" href="/dashboard"><i class="bi bi-grid"></i><span>Dashboard</span></a>
</li>
<li class="nav-item">
<a class="nav-link " href="index.html"><i class="bi bi-grid"></i><span>Quartz Job</span></a>
<a class="nav-link" href="/schedule"><i class="bi bi-menu-button-wide"></i><span>Schedule</span></a>
</li>
<li class="nav-item">
<a class="nav-link " href="index.html"><i class="bi bi-grid"></i><span>CronTrigger</span></a>
</li>
<li class="nav-item">
<a class="nav-link " href="index.html"><i class="bi bi-grid"></i><span>SimpleTrigger</span></a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" data-bs-target="#components-nav" data-bs-toggle="collapse" href="#">
<i class="bi bi-menu-button-wide"></i><span>Schedule</span><i class="bi bi-chevron-down ms-auto"></i>
</a>
<ul id="components-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
<li>
<a href="components-alerts.html"><i class="bi bi-circle"></i><span>Alerts</span></a>
</li>
<li>
<a href="components-accordion.html"><i class="bi bi-circle"></i><span>Accordion</span></a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link collapsed" data-bs-target="#charts-nav" data-bs-toggle="collapse" href="#">
<i class="bi bi-bar-chart"></i><span>Charts</span><i class="bi bi-chevron-down ms-auto"></i>
</a>
<ul id="charts-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
<li>
<a href="charts-chartjs.html"><i class="bi bi-circle"></i><span>Chart.js</span></a>
</li>
<li>
<a href="charts-apexcharts.html"><i class="bi bi-circle"></i><span>ApexCharts</span></a>
</li>
</ul>
<a class="nav-link" href="index.html"><i class="bi bi-bar-chart"></i><span>Charts</span></a>
</li>
</ul>
</aside>

View File

@@ -5,6 +5,5 @@
<head th:replace="fragments/config :: config"/>
<body>
<section layout:fragment="content"></section>
<footer th:replace="fragments/footer::footer"></footer>
</body>
</html>

View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout}"
layout:fragment="content" lang="ko" xml:lang="ko">
<head>
<title>DashBoard</title>
</head>
<body>
<main id="main" class="main">
<div class="pagetitle">
<div class="row align-items-center">
<div class="col">
<h1><i class="bi bi-speedometer2"></i> 대시보드</h1>
</div>
<div class="col-auto">
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="index.html"></a></li>
<li class="breadcrumb-item active">대시보드</li>
</ol>
</nav>
</div>
</div>
</div>
<section class="section">
<div class="row">
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<i class="bi bi-bar-chart-line me-2"></i>작업별 평균 실행 시간
</div>
<div class="card-body">
<canvas id="batchExecutionTimeChart" style="height: 250px;"></canvas>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<i class="bi bi-pie-chart-fill me-2"></i>작업 상태 분포
</div>
<div class="card-body">
<canvas id="batchStatusChart" style="height: 250px;"></canvas>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<i class="bi bi-graph-up me-2"></i>시간대별 작업 실행 분포
</div>
<div class="card-body">
<canvas id="hourlyJobExecutionChart" style="height: 250px;"></canvas>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<i class="bi bi-clock-history me-2"></i>일별 작업 실행 횟수
</div>
<div class="card-body">
<canvas id="dailyJobExecutionsChart" style="height: 250px;"></canvas>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-clock-history me-2"></i>최근 실행된 작업
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th><i class="bi bi-briefcase me-2"></i>작업 이름</th>
<th><i class="bi bi-folder me-2"></i>그룹</th>
<th><i class="bi bi-calendar-event me-2"></i>실행 시간</th>
<th><i class="bi bi-flag me-2"></i>상태</th>
</tr>
</thead>
<tbody id="recentJobsTable">
</tbody>
</table>
</div>
</div>
</div>
</section>
<script type="module" th:src="@{/js/pages/dashboard/dashboard.js}" defer></script>
</main>
</body>
</html>

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout}"
layout:fragment="content" lang="ko" xml:lang="ko">
<div>
본문 영역입니다.
</div>
</html>

View File

@@ -11,7 +11,7 @@
<div class="pagetitle">
<div class="row align-items-center">
<div class="col">
<h1><i class="bi bi-calendar3"></i> 스케줄 목록</h1>
<h1><i class="bi bi-calendar3"></i> 스케줄</h1>
</div>
<div class="col-auto">
<nav>
@@ -23,7 +23,6 @@
</div>
</div>
</div>
<section class="section">
<div class="row">
<div class="col-lg-12">
@@ -53,24 +52,20 @@
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-list-ul"></i> 스케줄 목록
</h5>
<div class="table-responsive">
<table class="table table-striped table-hover">
<table class="table table-hover">
<thead>
<tr>
<th class="col-1 text-nowrap"><i class="bi bi-people"></i> 그룹</th>
<th class="col-1 text-nowrap"><i class="bi bi-briefcase"></i> 잡 이름</th>
<th class="col-2 text-nowrap"><i class="bi bi-info-circle"></i> 설명</th>
<th class="col-2 text-nowrap"><i class="bi bi-calendar-event"></i> 스케줄</th>
<th class="col-2 text-nowrap"><i class="bi bi-clock"></i> 다음 실행</th>
<th class="col-2 text-nowrap"><i class="bi bi-clock-history"></i> 이전 실행</th>
<th class="col-1 text-nowrap"><i class="bi bi-calendar-event"></i> 스케줄</th>
<th class="col-1 text-nowrap"><i class="bi bi-activity"></i> 상태</th>
<th class="col-2 text-nowrap"><i class="bi bi-gear"></i> 액션</th>
<th class="col-1 text-nowrap"><i class="bi bi-gear"></i> 액션</th>
</tr>
</thead>
<tbody>
@@ -82,7 +77,45 @@
</div>
</div>
</section>
<script type="module" th:src="@{/js/pages/schedule/schedule-list.js}" defer></script>
<div class="modal fade" id="scheduleDetailModal" tabindex="-1" aria-labelledby="scheduleDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="scheduleDetailModalLabel">
<i class="bi bi-info-circle-fill me-2"></i>스케줄 정보
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="scheduleDetailContent">
</div>
<div class="modal-footer justify-content-center">
<fieldset>
<legend class="visually-hidden">작업 제어 버튼</legend>
<div class="d-flex justify-content-between" style="width: 300px;">
<button type="button" class="btn btn-outline-success rounded-circle" id="startJobBtn" data-bs-toggle="tooltip" data-bs-placement="top" title="시작">
<i class="bi bi-play-fill"></i>
</button>
<button type="button" class="btn btn-outline-danger rounded-circle" id="pauseJobBtn" data-bs-toggle="tooltip" data-bs-placement="top" title="정지">
<i class="bi bi-pause-fill"></i>
</button>
<button type="button" class="btn btn-outline-info rounded-circle" id="resumeJobBtn" data-bs-toggle="tooltip" data-bs-placement="top" title="재시작">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button type="button" class="btn btn-outline-primary rounded-circle" id="updateCronBtn" data-bs-toggle="tooltip" data-bs-placement="top" title="저장">
<i class="bi bi-save"></i>
</button>
<button type="button" class="btn btn-outline-secondary rounded-circle" data-bs-dismiss="modal" data-bs-toggle="tooltip" data-bs-placement="top" title="닫기">
<i class="bi bi-x-lg"></i>
</button>
</div>
</fieldset>
</div>
</div>
</div>
</div>
<script type="module" th:src="@{/js/pages/schedule/schedule.js}" defer></script>
</main>
</body>
</html>

View File

@@ -8,27 +8,86 @@
<body>
<section layout:fragment="content">
<div class="container">
<div class="icon">
<img src="/images/user.png" alt="User Icon">
<div class="row justify-content-center align-items-center min-vh-100">
<div class="col-lg-4 col-md-6">
<div class="card shadow-sm">
<div class="card-body p-5">
<div class="text-center mb-4">
<i class="bi bi-person-circle text-primary" style="font-size: 3rem;"></i>
<h2 class="card-title mt-3">로그인</h2>
</div>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-person"></i>
</span>
<input type="text" id="username" name="username" class="form-control" placeholder="아이디" required>
</div>
</div>
<div class="mb-4">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-lock"></i>
</span>
<input type="password" id="password" name="password" class="form-control" placeholder="비밀번호" required>
</div>
</div>
<div class="d-grid gap-2">
<button id="signIn" type="button" class="btn btn-primary">
<i class="bi bi-box-arrow-in-right me-2"></i>로그인
</button>
<button id="signUp" type="button" class="btn btn-outline-secondary">
<i class="bi bi-person-plus-fill me-2"></i>회원가입
</button>
</div>
</div>
</div>
</div>
</div>
<h1>로그인</h1>
<form id="signinForm" method="post">
<div class="input-group">
<div class="input-icon">
<img src="/images/user-id.png" alt="User Icon">
<input type="text" id="username" name="username" placeholder="아이디">
</div>
</div>
<div class="input-group">
<div class="input-icon">
<img src="/images/user-lock.png" alt="Password Icon">
<input type="password" id="password" name="password" placeholder="비밀번호">
</div>
</div>
<button type="submit">로그인</button>
</form>
<button id="signup" th:onclick="|location.href='@{/sign-up}'|" class="small-button">회원가입</button>
</div>
<!-- 회원가입 모달 -->
<div class="modal fade" id="signupModal" tabindex="-1" aria-labelledby="signupModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="signupModalLabel">회원가입</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="signupForm">
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input type="text" class="form-control" id="loginId" placeholder="아이디" required>
</div>
</div>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="password" class="form-control" id="loginPassword" placeholder="비밀번호" required>
</div>
</div>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<input type="text" class="form-control" id="userName" placeholder="사용자명" required>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-2"></i>취소
</button>
<button type="button" class="btn btn-primary" id="signupSubmit">
<i class="bi bi-check-circle me-2"></i>가입하기
</button>
</div>
</div>
</div>
</div>
<script type="module" th:src="@{/js/pages/sign/sign-in.js}" defer></script>
</section>
</body>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/signin-layout}" lang="ko" xml:lang="ko">
<head>
<title>회원가입 페이지</title>
</head>
<body>
<section layout:fragment="content">
<div class="container">
<h1>회원가입</h1>
<form id="signupForm" method="post">
<div class="input-group">
<input type="text" id="loginId" name="loginId" placeholder="아이디">
</div>
<div class="input-group">
<input type="password" id="password" name="password" placeholder="비밀번호">
</div>
<div class="input-group">
<input type="text" id="userName" name="userName" placeholder="이름">
</div>
<button type="submit">회원가입</button>
</form>
</div>
<script type="module" th:src="@{/js/pages/sign/sign-up.js}" defer></script>
</section>
</body>
</html>