[Schedule]
- Created schedule config. Call workers only based on db schedule setting - Created schedule log - Enabled scheduling
This commit is contained in:
parent
62d453b926
commit
aef400e67f
|
|
@ -5,11 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication(scanBasePackages = {"com.goi.erp"})
|
@SpringBootApplication(scanBasePackages = {"com.goi.erp"})
|
||||||
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
|
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
|
||||||
@EntityScan(basePackages = {"com.goi.erp.entity"})
|
@EntityScan(basePackages = {"com.goi.erp.entity"})
|
||||||
@EnableJpaRepositories(basePackages = {"com.goi.erp.repository"})
|
@EnableJpaRepositories(basePackages = {"com.goi.erp.repository"})
|
||||||
|
@EnableScheduling
|
||||||
public class SecurityApplication {
|
public class SecurityApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,31 @@ package com.goi.erp.common.permission;
|
||||||
|
|
||||||
public class PermissionChecker {
|
public class PermissionChecker {
|
||||||
|
|
||||||
public static boolean canCreateCRM(PermissionSet set) {
|
public static boolean canCreateSYS(PermissionSet set) {
|
||||||
if (set.hasAll()) return true;
|
if (set.hasAll()) return true;
|
||||||
return set.has(PermissionEnums.Module.C, PermissionEnums.Action.C);
|
return set.has(PermissionEnums.Module.S, PermissionEnums.Action.C);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean canReadCRM(PermissionSet set) {
|
public static boolean canReadSYS(PermissionSet set) {
|
||||||
if (set.hasAll()) return true;
|
if (set.hasAll()) return true;
|
||||||
return set.has(PermissionEnums.Module.C, PermissionEnums.Action.R);
|
return set.has(PermissionEnums.Module.S, PermissionEnums.Action.R);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean canUpdateCRM(PermissionSet set) {
|
public static boolean canUpdateSYS(PermissionSet set) {
|
||||||
if (set.hasAll()) return true;
|
if (set.hasAll()) return true;
|
||||||
return set.has(PermissionEnums.Module.C, PermissionEnums.Action.U);
|
return set.has(PermissionEnums.Module.S, PermissionEnums.Action.U);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean canDeleteCRM(PermissionSet set) {
|
public static boolean canDeleteSYS(PermissionSet set) {
|
||||||
if (set.hasAll()) return true;
|
if (set.hasAll()) return true;
|
||||||
return set.has(PermissionEnums.Module.C, PermissionEnums.Action.D);
|
return set.has(PermissionEnums.Module.S, PermissionEnums.Action.D);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 범위까지 체크
|
// 범위까지 체크
|
||||||
public static boolean canReadCRMAll(PermissionSet set) {
|
public static boolean canReadSYSAll(PermissionSet set) {
|
||||||
if (set.hasAll()) return true;
|
if (set.hasAll()) return true;
|
||||||
return set.hasFull(
|
return set.hasFull(
|
||||||
PermissionEnums.Module.C,
|
PermissionEnums.Module.S,
|
||||||
PermissionEnums.Action.R,
|
PermissionEnums.Action.R,
|
||||||
PermissionEnums.Scope.A
|
PermissionEnums.Scope.A
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
package com.goi.erp.controller;
|
package com.goi.erp.controller;
|
||||||
|
|
||||||
|
import com.goi.erp.common.permission.PermissionChecker;
|
||||||
|
import com.goi.erp.common.permission.PermissionSet;
|
||||||
import com.goi.erp.dto.ConfigRequestDto;
|
import com.goi.erp.dto.ConfigRequestDto;
|
||||||
import com.goi.erp.dto.ConfigResponseDto;
|
import com.goi.erp.dto.ConfigResponseDto;
|
||||||
import com.goi.erp.entity.ConfigChangeLog;
|
import com.goi.erp.entity.ConfigChangeLog;
|
||||||
import com.goi.erp.service.ConfigService;
|
|
||||||
import com.goi.erp.repository.ConfigChangeLogRepository;
|
import com.goi.erp.repository.ConfigChangeLogRepository;
|
||||||
|
import com.goi.erp.service.ConfigService;
|
||||||
|
import com.goi.erp.token.PermissionAuthenticationToken;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -27,13 +32,18 @@ public class ConfigController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 단일 config 조회
|
* 단일 config 조회
|
||||||
* GET /configs/{module}/{key}
|
* GET /config/{module}/{key}
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{module}/{key}")
|
@GetMapping("/{module}/{key}")
|
||||||
public ResponseEntity<ConfigResponseDto> getConfig(
|
public ResponseEntity<ConfigResponseDto> getConfig(
|
||||||
@PathVariable String module,
|
@PathVariable String module,
|
||||||
@PathVariable String key
|
@PathVariable String key
|
||||||
) {
|
) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canReadSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to read SYS config");
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
configService.getOne(module, key)
|
configService.getOne(module, key)
|
||||||
);
|
);
|
||||||
|
|
@ -41,12 +51,17 @@ public class ConfigController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모듈별 config 목록
|
* 모듈별 config 목록
|
||||||
* GET /configs/{module}
|
* GET /config/{module}
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{module}")
|
@GetMapping("/{module}")
|
||||||
public ResponseEntity<List<ConfigResponseDto>> getConfigsByModule(
|
public ResponseEntity<List<ConfigResponseDto>> getConfigsByModule(
|
||||||
@PathVariable String module
|
@PathVariable String module
|
||||||
) {
|
) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canReadSYSAll(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to read all SYS configs");
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
configService.getAllByModule(module)
|
configService.getAllByModule(module)
|
||||||
);
|
);
|
||||||
|
|
@ -60,25 +75,34 @@ public class ConfigController {
|
||||||
public ResponseEntity<ConfigResponseDto> save(
|
public ResponseEntity<ConfigResponseDto> save(
|
||||||
@RequestBody ConfigRequestDto dto
|
@RequestBody ConfigRequestDto dto
|
||||||
) {
|
) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canCreateSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to create or update SYS config");
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
configService.save(dto)
|
configService.save(dto)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
CHANGE HISTORY
|
CHANGE HISTORY
|
||||||
=============================== */
|
=============================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* config 변경 이력
|
* config 변경 이력
|
||||||
* GET /configs/{module}/{key}/history
|
* GET /config/{module}/{key}/history
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{module}/{key}/history")
|
@GetMapping("/{module}/{key}/history")
|
||||||
public ResponseEntity<List<ConfigChangeLog>> getHistory(
|
public ResponseEntity<List<ConfigChangeLog>> getHistory(
|
||||||
@PathVariable String module,
|
@PathVariable String module,
|
||||||
@PathVariable String key
|
@PathVariable String key
|
||||||
) {
|
) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canReadSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to read SYS config history");
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
configChangeLogRepository
|
configChangeLogRepository
|
||||||
.findAllByCclModuleAndCclKeyOrderByCclChangedAtDesc(
|
.findAllByCclModuleAndCclKeyOrderByCclChangedAtDesc(
|
||||||
|
|
@ -86,4 +110,21 @@ public class ConfigController {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
permission helper
|
||||||
|
=============================== */
|
||||||
|
|
||||||
|
private PermissionSet getPermissionSet() {
|
||||||
|
PermissionAuthenticationToken auth =
|
||||||
|
(PermissionAuthenticationToken) SecurityContextHolder
|
||||||
|
.getContext()
|
||||||
|
.getAuthentication();
|
||||||
|
|
||||||
|
if (auth == null || auth.getPermissionSet() == null) {
|
||||||
|
throw new AccessDeniedException("Permission information is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.getPermissionSet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
package com.goi.erp.controller;
|
||||||
|
|
||||||
|
import com.goi.erp.common.permission.PermissionChecker;
|
||||||
|
import com.goi.erp.common.permission.PermissionSet;
|
||||||
|
import com.goi.erp.dto.ScheduleJobConfigRequestDto;
|
||||||
|
import com.goi.erp.dto.ScheduleJobConfigResponseDto;
|
||||||
|
import com.goi.erp.service.ScheduleJobConfigService;
|
||||||
|
import com.goi.erp.token.PermissionAuthenticationToken;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/schedule/jobs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleJobConfigController {
|
||||||
|
|
||||||
|
private final ScheduleJobConfigService service;
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* CREATE
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<ScheduleJobConfigResponseDto> create(
|
||||||
|
@RequestBody ScheduleJobConfigRequestDto request
|
||||||
|
) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canCreateSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to create SYS schedule job");
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleJobConfigResponseDto response = service.create(request);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* READ
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<ScheduleJobConfigResponseDto> get(@PathVariable Long id) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canReadSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to read SYS schedule job");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(service.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/code/{jobCode}")
|
||||||
|
public ResponseEntity<ScheduleJobConfigResponseDto> getByJobCode(
|
||||||
|
@PathVariable String jobCode
|
||||||
|
) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canReadSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to read SYS schedule job");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(service.getByJobCode(jobCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<ScheduleJobConfigResponseDto>> listAll() {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canReadSYSAll(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to read all SYS schedule jobs");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(service.listAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* UPDATE
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<ScheduleJobConfigResponseDto> update(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody ScheduleJobConfigRequestDto request
|
||||||
|
) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canUpdateSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to update SYS schedule job");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(service.update(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{id}/enable")
|
||||||
|
public ResponseEntity<Void> enable(@PathVariable Long id) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canUpdateSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to enable SYS schedule job");
|
||||||
|
}
|
||||||
|
|
||||||
|
service.setEnabled(id, true);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{id}/disable")
|
||||||
|
public ResponseEntity<Void> disable(@PathVariable Long id) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canUpdateSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to disable SYS schedule job");
|
||||||
|
}
|
||||||
|
|
||||||
|
service.setEnabled(id, false);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* DELETE
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
PermissionSet permissionSet = getPermissionSet();
|
||||||
|
if (!PermissionChecker.canDeleteSYS(permissionSet)) {
|
||||||
|
throw new AccessDeniedException("You do not have permission to delete SYS schedule job");
|
||||||
|
}
|
||||||
|
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* permission helper
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
private PermissionSet getPermissionSet() {
|
||||||
|
PermissionAuthenticationToken auth =
|
||||||
|
(PermissionAuthenticationToken) SecurityContextHolder
|
||||||
|
.getContext()
|
||||||
|
.getAuthentication();
|
||||||
|
|
||||||
|
if (auth == null || auth.getPermissionSet() == null) {
|
||||||
|
throw new AccessDeniedException("Permission information is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.getPermissionSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.goi.erp.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleJobConfigRequestDto {
|
||||||
|
|
||||||
|
private String sjcJobCode; // 예: SAMSARA_DVIR
|
||||||
|
private String sjcJobName; // 예: Samsara DVIR Ingest
|
||||||
|
private String sjcJobGroup; // 예: SAMSARA
|
||||||
|
|
||||||
|
private Boolean sjcEnabled;
|
||||||
|
|
||||||
|
private String sjcCronExpression; // 예: 0 */10 * * * *
|
||||||
|
private String sjcTimezone; // 예: UTC, Asia/Seoul
|
||||||
|
|
||||||
|
private String sjcWorkerMethod; // POST
|
||||||
|
private String sjcWorkerUrl; // worker endpoint
|
||||||
|
private Integer sjcWorkerTimeoutSeconds; // seconds
|
||||||
|
|
||||||
|
// execution parameters
|
||||||
|
private Integer sjcLookbackHours;
|
||||||
|
private Integer sjcOverlapMinutes;
|
||||||
|
private Integer sjcMaxRecords;
|
||||||
|
|
||||||
|
private String sjcDescription;
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* required config references
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 예:
|
||||||
|
* {
|
||||||
|
* "SYS": ["HOME_LATITUDE", "HOME_LONGITUDE"],
|
||||||
|
* "OPR": ["HOME_RADIUS_METERS", "PAUSED_CLOSE_MINUTES"]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private Map<String, List<String>> sjcRequiredConfigs;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.goi.erp.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleJobConfigResponseDto {
|
||||||
|
|
||||||
|
private Long sjcId;
|
||||||
|
|
||||||
|
private String sjcJobCode;
|
||||||
|
private String sjcJobName;
|
||||||
|
private String sjcJobGroup;
|
||||||
|
|
||||||
|
private Boolean sjcEnabled;
|
||||||
|
|
||||||
|
private String sjcCronExpression;
|
||||||
|
private String sjcTimezone;
|
||||||
|
|
||||||
|
private String sjcWorkerMethod;
|
||||||
|
private String sjcWorkerUrl;
|
||||||
|
private Integer sjcWorkerTimeoutSeconds;
|
||||||
|
|
||||||
|
// execution parameters
|
||||||
|
private Integer sjcLookbackHours;
|
||||||
|
private Integer sjcOverlapMinutes;
|
||||||
|
private Integer sjcMaxRecords;
|
||||||
|
|
||||||
|
private String sjcDescription;
|
||||||
|
|
||||||
|
private LocalDateTime sjcCreatedAt;
|
||||||
|
private LocalDateTime sjcUpdatedAt;
|
||||||
|
|
||||||
|
private String sjcCreatedBy;
|
||||||
|
private String sjcUpdatedBy;
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* required config references
|
||||||
|
* ========================= */
|
||||||
|
private Map<String, List<String>> sjcRequiredConfigs;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.goi.erp.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleWorkerRequestDto {
|
||||||
|
|
||||||
|
private String jobCode;
|
||||||
|
private LocalDateTime from;
|
||||||
|
private LocalDateTime to;
|
||||||
|
private Integer maxRecords;
|
||||||
|
private Map<String, Map<String, String>> config;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.goi.erp.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleWorkerResponseDto {
|
||||||
|
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
private int processedCount;
|
||||||
|
private int successCount;
|
||||||
|
private int failCount;
|
||||||
|
|
||||||
|
private String errorCode;
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
package com.goi.erp.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EntityListeners;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
import org.springframework.data.annotation.CreatedBy;
|
||||||
|
import org.springframework.data.annotation.LastModifiedBy;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(
|
||||||
|
name = "schedule_job_config",
|
||||||
|
uniqueConstraints = {
|
||||||
|
@UniqueConstraint(
|
||||||
|
name = "uk_schedule_job_config_job_code",
|
||||||
|
columnNames = {"sjc_job_code"}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class ScheduleJobConfig {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "sjc_id")
|
||||||
|
private Long sjcId;
|
||||||
|
|
||||||
|
@Column(name = "sjc_job_code", nullable = false, length = 50)
|
||||||
|
private String sjcJobCode;
|
||||||
|
|
||||||
|
@Column(name = "sjc_job_name", nullable = false, length = 100)
|
||||||
|
private String sjcJobName;
|
||||||
|
|
||||||
|
@Column(name = "sjc_job_group", length = 50)
|
||||||
|
private String sjcJobGroup;
|
||||||
|
|
||||||
|
@Column(name = "sjc_enabled", nullable = false)
|
||||||
|
private Boolean sjcEnabled;
|
||||||
|
|
||||||
|
@Column(name = "sjc_cron_expression", nullable = false, length = 50)
|
||||||
|
private String sjcCronExpression;
|
||||||
|
|
||||||
|
@Column(name = "sjc_timezone", length = 50)
|
||||||
|
private String sjcTimezone;
|
||||||
|
|
||||||
|
@Column(name = "sjc_worker_method", length = 10)
|
||||||
|
private String sjcWorkerMethod;
|
||||||
|
|
||||||
|
@Column(name = "sjc_worker_url", length = 255, nullable = false)
|
||||||
|
private String sjcWorkerUrl;
|
||||||
|
|
||||||
|
@Column(name = "sjc_worker_timeout_seconds")
|
||||||
|
private Integer sjcWorkerTimeoutSeconds;
|
||||||
|
|
||||||
|
// execution parameters
|
||||||
|
@Column(name = "sjc_lookback_hours")
|
||||||
|
private Integer sjcLookbackHours;
|
||||||
|
|
||||||
|
@Column(name = "sjc_overlap_minutes")
|
||||||
|
private Integer sjcOverlapMinutes;
|
||||||
|
|
||||||
|
@Column(name = "sjc_max_records")
|
||||||
|
private Integer sjcMaxRecords;
|
||||||
|
|
||||||
|
@Column(name = "sjc_description", columnDefinition = "TEXT")
|
||||||
|
private String sjcDescription;
|
||||||
|
|
||||||
|
@Column(name = "sjc_created_at")
|
||||||
|
private LocalDateTime sjcCreatedAt;
|
||||||
|
|
||||||
|
@CreatedBy
|
||||||
|
@Column(name = "sjc_created_by", length = 50)
|
||||||
|
private String sjcCreatedBy;
|
||||||
|
|
||||||
|
@Column(name = "sjc_updated_at")
|
||||||
|
private LocalDateTime sjcUpdatedAt;
|
||||||
|
|
||||||
|
@LastModifiedBy
|
||||||
|
@Column(name = "sjc_updated_by", length = 50)
|
||||||
|
private String sjcUpdatedBy;
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* required config references
|
||||||
|
* ========================= */
|
||||||
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
|
@Column(name = "sjc_required_configs", columnDefinition = "jsonb")
|
||||||
|
private Map<String, List<String>> sjcRequiredConfigs;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.goi.erp.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "schedule_job_log")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleJobLog {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "sjl_id")
|
||||||
|
private Long sjlId;
|
||||||
|
|
||||||
|
@Column(name = "sjl_job_id", nullable = false)
|
||||||
|
private Long sjlJobId;
|
||||||
|
|
||||||
|
@Column(name = "sjl_status", nullable = false, length = 20)
|
||||||
|
private String sjlStatus;
|
||||||
|
|
||||||
|
@Column(name = "sjl_scheduled_at")
|
||||||
|
private LocalDateTime sjlScheduledAt;
|
||||||
|
|
||||||
|
@Column(name = "sjl_started_at", nullable = false)
|
||||||
|
private LocalDateTime sjlStartedAt;
|
||||||
|
|
||||||
|
@Column(name = "sjl_finished_at")
|
||||||
|
private LocalDateTime sjlFinishedAt;
|
||||||
|
|
||||||
|
@Column(name = "sjl_lookback_hours")
|
||||||
|
private Integer sjlLookbackHours;
|
||||||
|
|
||||||
|
@Column(name = "sjl_overlap_minutes")
|
||||||
|
private Integer sjlOverlapMinutes;
|
||||||
|
|
||||||
|
@Column(name = "sjl_max_records")
|
||||||
|
private Integer sjlMaxRecords;
|
||||||
|
|
||||||
|
@Column(name = "sjl_processed_count")
|
||||||
|
private Integer sjlProcessedCount;
|
||||||
|
|
||||||
|
@Column(name = "sjl_success_count")
|
||||||
|
private Integer sjlSuccessCount;
|
||||||
|
|
||||||
|
@Column(name = "sjl_fail_count")
|
||||||
|
private Integer sjlFailCount;
|
||||||
|
|
||||||
|
@Column(name = "sjl_error_code", length = 100)
|
||||||
|
private String sjlErrorCode;
|
||||||
|
|
||||||
|
@Column(name = "sjl_error_message", columnDefinition = "TEXT")
|
||||||
|
private String sjlErrorMessage;
|
||||||
|
|
||||||
|
@Column(name = "sjl_executed_by", length = 50)
|
||||||
|
private String sjlExecutedBy;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.goi.erp.repository;
|
||||||
|
|
||||||
|
import com.goi.erp.entity.ScheduleJobConfig;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ScheduleJobConfigRepository
|
||||||
|
extends JpaRepository<ScheduleJobConfig, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* job_code 로 단건 조회
|
||||||
|
*/
|
||||||
|
Optional<ScheduleJobConfig> findBySjcJobCode(String sjcJobCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* job_code 존재 여부 확인
|
||||||
|
*/
|
||||||
|
boolean existsBySjcJobCode(String sjcJobCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활성화된 스케줄 목록 조회
|
||||||
|
* (Scheduler Core에서 주기적으로 로딩)
|
||||||
|
*/
|
||||||
|
List<ScheduleJobConfig> findBySjcEnabledTrue();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.goi.erp.repository;
|
||||||
|
|
||||||
|
import com.goi.erp.entity.ScheduleJobLog;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ScheduleJobLogRepository
|
||||||
|
extends JpaRepository<ScheduleJobLog, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 job의 가장 최근 성공 실행 시작 시간
|
||||||
|
* (Scheduler Core에서 lookback 기준 계산용)
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT l.sjlStartedAt
|
||||||
|
FROM ScheduleJobLog l
|
||||||
|
WHERE l.sjlJobId = :jobId
|
||||||
|
AND l.sjlStatus = 'SUCCESS'
|
||||||
|
ORDER BY l.sjlStartedAt DESC
|
||||||
|
""")
|
||||||
|
Optional<LocalDateTime> findLastSuccessStartedAt(
|
||||||
|
@Param("jobId") Long jobId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 실행 중인 로그 존재 여부
|
||||||
|
* (중복 실행 방지용)
|
||||||
|
*/
|
||||||
|
boolean existsBySjlJobIdAndSjlStatus(Long sjlJobId, String sjlStatus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 job의 최근 실행 로그 목록
|
||||||
|
* (Admin UI / 모니터링용)
|
||||||
|
*/
|
||||||
|
List<ScheduleJobLog> findTop50BySjlJobIdOrderBySjlStartedAtDesc(
|
||||||
|
Long sjlJobId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 기간 동안 실행된 로그 조회
|
||||||
|
* (재처리 / 분석용)
|
||||||
|
*/
|
||||||
|
List<ScheduleJobLog> findBySjlJobIdAndSjlStartedAtBetween(
|
||||||
|
Long sjlJobId,
|
||||||
|
LocalDateTime from,
|
||||||
|
LocalDateTime to
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -198,4 +198,12 @@ public class ConfigService {
|
||||||
.cfgUpdatedBy(entity.getCfgUpdatedBy())
|
.cfgUpdatedBy(entity.getCfgUpdatedBy())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===============================
|
||||||
|
Scheduler
|
||||||
|
=============================== */
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public String getRawValue(String module, String key) {
|
||||||
|
return getConfig(module, key).getCfgValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
package com.goi.erp.service;
|
||||||
|
|
||||||
|
import com.goi.erp.dto.ScheduleJobConfigRequestDto;
|
||||||
|
import com.goi.erp.dto.ScheduleJobConfigResponseDto;
|
||||||
|
import com.goi.erp.entity.ScheduleJobConfig;
|
||||||
|
import com.goi.erp.repository.ScheduleJobConfigRepository;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleJobConfigService {
|
||||||
|
|
||||||
|
private final ScheduleJobConfigRepository repository;
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* CREATE
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ScheduleJobConfigResponseDto create(ScheduleJobConfigRequestDto request) {
|
||||||
|
|
||||||
|
if (repository.existsBySjcJobCode(request.getSjcJobCode())) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Schedule job already exists. jobCode=" + request.getSjcJobCode()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getSjcWorkerUrl() == null || request.getSjcWorkerUrl().isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Worker URL must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleJobConfig entity = ScheduleJobConfig.builder()
|
||||||
|
.sjcJobCode(request.getSjcJobCode())
|
||||||
|
.sjcJobName(request.getSjcJobName())
|
||||||
|
.sjcJobGroup(request.getSjcJobGroup())
|
||||||
|
.sjcEnabled(
|
||||||
|
request.getSjcEnabled() != null ? request.getSjcEnabled() : Boolean.TRUE
|
||||||
|
)
|
||||||
|
.sjcCronExpression(request.getSjcCronExpression())
|
||||||
|
.sjcTimezone(
|
||||||
|
request.getSjcTimezone() != null ? request.getSjcTimezone() : "UTC"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* execution parameters */
|
||||||
|
.sjcLookbackHours(request.getSjcLookbackHours())
|
||||||
|
.sjcOverlapMinutes(
|
||||||
|
request.getSjcOverlapMinutes() != null ? request.getSjcOverlapMinutes() : 0
|
||||||
|
)
|
||||||
|
.sjcMaxRecords(request.getSjcMaxRecords())
|
||||||
|
|
||||||
|
/* worker execution info */
|
||||||
|
.sjcWorkerMethod(
|
||||||
|
request.getSjcWorkerMethod() != null ? request.getSjcWorkerMethod() : "POST"
|
||||||
|
)
|
||||||
|
.sjcWorkerUrl(request.getSjcWorkerUrl())
|
||||||
|
.sjcWorkerTimeoutSeconds(
|
||||||
|
request.getSjcWorkerTimeoutSeconds() != null
|
||||||
|
? request.getSjcWorkerTimeoutSeconds()
|
||||||
|
: 300
|
||||||
|
)
|
||||||
|
|
||||||
|
.sjcDescription(request.getSjcDescription())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ScheduleJobConfig saved = repository.save(entity);
|
||||||
|
return toResponseDto(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* READ
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ScheduleJobConfigResponseDto get(Long sjcId) {
|
||||||
|
return repository.findById(sjcId)
|
||||||
|
.map(this::toResponseDto)
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new EntityNotFoundException("Schedule job not found. id=" + sjcId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ScheduleJobConfigResponseDto getByJobCode(String jobCode) {
|
||||||
|
return repository.findBySjcJobCode(jobCode)
|
||||||
|
.map(this::toResponseDto)
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new EntityNotFoundException("Schedule job not found. jobCode=" + jobCode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<ScheduleJobConfigResponseDto> listAll() {
|
||||||
|
return repository.findAll()
|
||||||
|
.stream()
|
||||||
|
.map(this::toResponseDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<ScheduleJobConfigResponseDto> listEnabled() {
|
||||||
|
return repository.findBySjcEnabledTrue()
|
||||||
|
.stream()
|
||||||
|
.map(this::toResponseDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* UPDATE
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ScheduleJobConfigResponseDto update(
|
||||||
|
Long sjcId,
|
||||||
|
ScheduleJobConfigRequestDto request
|
||||||
|
) {
|
||||||
|
ScheduleJobConfig entity = repository.findById(sjcId)
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new EntityNotFoundException("Schedule job not found. id=" + sjcId)
|
||||||
|
);
|
||||||
|
|
||||||
|
entity.setSjcJobName(request.getSjcJobName());
|
||||||
|
entity.setSjcJobGroup(request.getSjcJobGroup());
|
||||||
|
entity.setSjcEnabled(request.getSjcEnabled());
|
||||||
|
entity.setSjcCronExpression(request.getSjcCronExpression());
|
||||||
|
entity.setSjcTimezone(request.getSjcTimezone());
|
||||||
|
|
||||||
|
/* execution parameters */
|
||||||
|
entity.setSjcLookbackHours(request.getSjcLookbackHours());
|
||||||
|
entity.setSjcOverlapMinutes(request.getSjcOverlapMinutes());
|
||||||
|
entity.setSjcMaxRecords(request.getSjcMaxRecords());
|
||||||
|
|
||||||
|
/* worker execution info */
|
||||||
|
if (request.getSjcWorkerMethod() != null) {
|
||||||
|
entity.setSjcWorkerMethod(request.getSjcWorkerMethod());
|
||||||
|
}
|
||||||
|
if (request.getSjcWorkerUrl() != null) {
|
||||||
|
entity.setSjcWorkerUrl(request.getSjcWorkerUrl());
|
||||||
|
}
|
||||||
|
if (request.getSjcWorkerTimeoutSeconds() != null) {
|
||||||
|
entity.setSjcWorkerTimeoutSeconds(request.getSjcWorkerTimeoutSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.setSjcDescription(request.getSjcDescription());
|
||||||
|
|
||||||
|
return toResponseDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* ENABLE / DISABLE
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void setEnabled(Long sjcId, boolean enabled) {
|
||||||
|
ScheduleJobConfig entity = repository.findById(sjcId)
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new EntityNotFoundException("Schedule job not found. id=" + sjcId)
|
||||||
|
);
|
||||||
|
|
||||||
|
entity.setSjcEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* DELETE
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long sjcId) {
|
||||||
|
if (!repository.existsById(sjcId)) {
|
||||||
|
throw new EntityNotFoundException("Schedule job not found. id=" + sjcId);
|
||||||
|
}
|
||||||
|
repository.deleteById(sjcId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* private mapper
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
private ScheduleJobConfigResponseDto toResponseDto(ScheduleJobConfig entity) {
|
||||||
|
return ScheduleJobConfigResponseDto.builder()
|
||||||
|
.sjcId(entity.getSjcId())
|
||||||
|
.sjcJobCode(entity.getSjcJobCode())
|
||||||
|
.sjcJobName(entity.getSjcJobName())
|
||||||
|
.sjcJobGroup(entity.getSjcJobGroup())
|
||||||
|
.sjcEnabled(entity.getSjcEnabled())
|
||||||
|
.sjcCronExpression(entity.getSjcCronExpression())
|
||||||
|
.sjcTimezone(entity.getSjcTimezone())
|
||||||
|
|
||||||
|
/* execution parameters */
|
||||||
|
.sjcLookbackHours(entity.getSjcLookbackHours())
|
||||||
|
.sjcOverlapMinutes(entity.getSjcOverlapMinutes())
|
||||||
|
.sjcMaxRecords(entity.getSjcMaxRecords())
|
||||||
|
|
||||||
|
/* worker execution info */
|
||||||
|
.sjcWorkerMethod(entity.getSjcWorkerMethod())
|
||||||
|
.sjcWorkerUrl(entity.getSjcWorkerUrl())
|
||||||
|
.sjcWorkerTimeoutSeconds(entity.getSjcWorkerTimeoutSeconds())
|
||||||
|
|
||||||
|
.sjcDescription(entity.getSjcDescription())
|
||||||
|
.sjcCreatedAt(entity.getSjcCreatedAt())
|
||||||
|
.sjcUpdatedAt(entity.getSjcUpdatedAt())
|
||||||
|
.sjcCreatedBy(entity.getSjcCreatedBy())
|
||||||
|
.sjcUpdatedBy(entity.getSjcUpdatedBy())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
package com.goi.erp.service;
|
||||||
|
|
||||||
|
import com.goi.erp.entity.ScheduleJobLog;
|
||||||
|
import com.goi.erp.repository.ScheduleJobLogRepository;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleJobLogService {
|
||||||
|
|
||||||
|
private final ScheduleJobLogRepository repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최근 성공 실행 시작 시각 조회
|
||||||
|
* (없으면 Optional.empty)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Optional<LocalDateTime> getLastSuccessStartedAt(Long jobId) {
|
||||||
|
return repository.findLastSuccessStartedAt(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 실행 중인지 여부
|
||||||
|
* (중복 실행 방지)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public boolean isRunning(Long jobId) {
|
||||||
|
return repository.existsBySjlJobIdAndSjlStatus(jobId, "RUNNING");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행 시작 로그 생성
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ScheduleJobLog createRunningLog(
|
||||||
|
Long jobId,
|
||||||
|
LocalDateTime scheduledAt,
|
||||||
|
Integer lookbackHours,
|
||||||
|
Integer overlapMinutes,
|
||||||
|
Integer maxRecords,
|
||||||
|
String executedBy
|
||||||
|
) {
|
||||||
|
ScheduleJobLog log = ScheduleJobLog.builder()
|
||||||
|
.sjlJobId(jobId)
|
||||||
|
.sjlStatus("RUNNING")
|
||||||
|
.sjlScheduledAt(scheduledAt)
|
||||||
|
.sjlStartedAt(LocalDateTime.now())
|
||||||
|
.sjlLookbackHours(lookbackHours)
|
||||||
|
.sjlOverlapMinutes(overlapMinutes)
|
||||||
|
.sjlMaxRecords(maxRecords)
|
||||||
|
.sjlExecutedBy(executedBy)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return repository.save(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행 성공 처리
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void markSuccess(
|
||||||
|
Long logId,
|
||||||
|
int processedCount,
|
||||||
|
int successCount,
|
||||||
|
int failCount
|
||||||
|
) {
|
||||||
|
ScheduleJobLog log = repository.findById(logId)
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new IllegalStateException("ScheduleJobLog not found. id=" + logId)
|
||||||
|
);
|
||||||
|
|
||||||
|
log.setSjlStatus("SUCCESS");
|
||||||
|
log.setSjlFinishedAt(LocalDateTime.now());
|
||||||
|
log.setSjlProcessedCount(processedCount);
|
||||||
|
log.setSjlSuccessCount(successCount);
|
||||||
|
log.setSjlFailCount(failCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행 실패 처리
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void markFailed(
|
||||||
|
Long logId,
|
||||||
|
String errorCode,
|
||||||
|
String errorMessage
|
||||||
|
) {
|
||||||
|
ScheduleJobLog log = repository.findById(logId)
|
||||||
|
.orElseThrow(() ->
|
||||||
|
new IllegalStateException("ScheduleJobLog not found. id=" + logId)
|
||||||
|
);
|
||||||
|
|
||||||
|
log.setSjlStatus("FAILED");
|
||||||
|
log.setSjlFinishedAt(LocalDateTime.now());
|
||||||
|
log.setSjlErrorCode(errorCode);
|
||||||
|
log.setSjlErrorMessage(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실행 스킵 처리 (중복 실행, 비활성화 등)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ScheduleJobLog markSkipped(
|
||||||
|
Long jobId,
|
||||||
|
LocalDateTime scheduledAt,
|
||||||
|
String reason,
|
||||||
|
String executedBy
|
||||||
|
) {
|
||||||
|
ScheduleJobLog log = ScheduleJobLog.builder()
|
||||||
|
.sjlJobId(jobId)
|
||||||
|
.sjlStatus("SKIPPED")
|
||||||
|
.sjlScheduledAt(scheduledAt)
|
||||||
|
.sjlStartedAt(LocalDateTime.now())
|
||||||
|
.sjlFinishedAt(LocalDateTime.now())
|
||||||
|
.sjlErrorMessage(reason)
|
||||||
|
.sjlExecutedBy(executedBy)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return repository.save(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
package com.goi.erp.service;
|
||||||
|
|
||||||
|
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||||
|
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||||
|
import com.goi.erp.entity.ScheduleJobConfig;
|
||||||
|
import com.goi.erp.entity.ScheduleJobLog;
|
||||||
|
import com.goi.erp.repository.ScheduleJobConfigRepository;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.scheduling.support.CronExpression;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class SchedulerCoreService {
|
||||||
|
|
||||||
|
private final ScheduleJobConfigRepository configRepository;
|
||||||
|
private final ScheduleJobLogService logService;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final ConfigService configService;
|
||||||
|
|
||||||
|
private final Map<Long, LocalDateTime> lastTickMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheduler Tick
|
||||||
|
* - 1분마다 실행
|
||||||
|
* - 실제 cron 판단은 내부에서 수행
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 60_000)
|
||||||
|
public void tick() {
|
||||||
|
|
||||||
|
List<ScheduleJobConfig> jobs = configRepository.findBySjcEnabledTrue();
|
||||||
|
|
||||||
|
for (ScheduleJobConfig job : jobs) {
|
||||||
|
try {
|
||||||
|
evaluateAndRun(job);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Scheduler error. jobCode={}", job.getSjcJobCode(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* core logic
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
private void evaluateAndRun(ScheduleJobConfig job) {
|
||||||
|
|
||||||
|
ZoneId zoneId = ZoneId.of(
|
||||||
|
job.getSjcTimezone() != null ? job.getSjcTimezone() : "UTC"
|
||||||
|
);
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now(zoneId);
|
||||||
|
|
||||||
|
// 1. cron 판단
|
||||||
|
if (!shouldRun(job.getSjcId(), job.getSjcCronExpression(), now)) {
|
||||||
|
System.out.println(job.getSjcJobCode() + " shouldRun is false");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 중복 실행 방지
|
||||||
|
if (Boolean.TRUE.equals(job.getSjcEnabled())
|
||||||
|
&& logService.isRunning(job.getSjcId())) {
|
||||||
|
log.info("Job already running. jobCode={}", job.getSjcJobCode());
|
||||||
|
System.out.println("Job already running. jobCode=" + job.getSjcJobCode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 실행 범위 계산
|
||||||
|
LocalDateTime from = now
|
||||||
|
.minusHours(job.getSjcLookbackHours() != null ? job.getSjcLookbackHours() : 0)
|
||||||
|
.minusMinutes(job.getSjcOverlapMinutes() != null ? job.getSjcOverlapMinutes() : 0);
|
||||||
|
|
||||||
|
LocalDateTime to = now;
|
||||||
|
|
||||||
|
// 4. RUNNING 로그 생성
|
||||||
|
ScheduleJobLog runningLog = logService.createRunningLog(
|
||||||
|
job.getSjcId(),
|
||||||
|
now,
|
||||||
|
job.getSjcLookbackHours(),
|
||||||
|
job.getSjcOverlapMinutes(),
|
||||||
|
job.getSjcMaxRecords(),
|
||||||
|
"scheduler"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. worker 실행
|
||||||
|
try {
|
||||||
|
System.out.println(job.getSjcJobCode() + " worker start");
|
||||||
|
executeWorker(job, from, to, runningLog);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logService.markFailed(
|
||||||
|
runningLog.getSjlId(),
|
||||||
|
"WORKER_ERROR",
|
||||||
|
e.getMessage()
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* worker execution
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
private void executeWorker(
|
||||||
|
ScheduleJobConfig job,
|
||||||
|
LocalDateTime from,
|
||||||
|
LocalDateTime to,
|
||||||
|
ScheduleJobLog log
|
||||||
|
) {
|
||||||
|
|
||||||
|
Map<String, Map<String, String>> configSnapshot = buildConfigSnapshot(job);
|
||||||
|
|
||||||
|
ScheduleWorkerRequestDto request = ScheduleWorkerRequestDto.builder()
|
||||||
|
.jobCode(job.getSjcJobCode())
|
||||||
|
.from(from)
|
||||||
|
.to(to)
|
||||||
|
.maxRecords(job.getSjcMaxRecords())
|
||||||
|
.config(configSnapshot)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpMethod method = HttpMethod.valueOf(
|
||||||
|
job.getSjcWorkerMethod() != null
|
||||||
|
? job.getSjcWorkerMethod()
|
||||||
|
: "POST"
|
||||||
|
);
|
||||||
|
|
||||||
|
HttpEntity<ScheduleWorkerRequestDto> entity =
|
||||||
|
new HttpEntity<>(request);
|
||||||
|
|
||||||
|
ResponseEntity<ScheduleWorkerResponseDto> response =
|
||||||
|
restTemplate.exchange(
|
||||||
|
job.getSjcWorkerUrl(),
|
||||||
|
method,
|
||||||
|
entity,
|
||||||
|
ScheduleWorkerResponseDto.class
|
||||||
|
);
|
||||||
|
|
||||||
|
ScheduleWorkerResponseDto body = response.getBody();
|
||||||
|
System.out.println("response: "+body);
|
||||||
|
if (body == null) {
|
||||||
|
throw new IllegalStateException("Worker response is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.isSuccess()) {
|
||||||
|
logService.markSuccess(
|
||||||
|
log.getSjlId(),
|
||||||
|
body.getProcessedCount(),
|
||||||
|
body.getSuccessCount(),
|
||||||
|
body.getFailCount()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logService.markFailed(
|
||||||
|
log.getSjlId(),
|
||||||
|
body.getErrorCode(),
|
||||||
|
body.getErrorMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* cron 판단
|
||||||
|
* ========================= */
|
||||||
|
|
||||||
|
private boolean shouldRun(Long jobId, String cronExpression, LocalDateTime now) {
|
||||||
|
CronExpression cron = CronExpression.parse(cronExpression);
|
||||||
|
|
||||||
|
LocalDateTime last = lastTickMap.get(jobId);
|
||||||
|
if (last == null) {
|
||||||
|
lastTickMap.put(jobId, now);
|
||||||
|
return true; // 최초 1회 허용 (원하면 false로 바꿔도 됨)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime next = cron.next(last);
|
||||||
|
boolean shouldRun = next != null && !next.isAfter(now);
|
||||||
|
|
||||||
|
lastTickMap.put(jobId, now);
|
||||||
|
return shouldRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Map<String, String>> buildConfigSnapshot(
|
||||||
|
ScheduleJobConfig job
|
||||||
|
) {
|
||||||
|
if (job.getSjcRequiredConfigs() == null ||
|
||||||
|
job.getSjcRequiredConfigs().isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Map<String, String>> snapshot = new HashMap<>();
|
||||||
|
|
||||||
|
job.getSjcRequiredConfigs().forEach((module, keys) -> {
|
||||||
|
Map<String, String> values = new HashMap<>();
|
||||||
|
for (String key : keys) {
|
||||||
|
values.put(
|
||||||
|
key,
|
||||||
|
configService.getRawValue(module, key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
snapshot.put(module, values);
|
||||||
|
});
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue