diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/JobController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/JobController.java new file mode 100644 index 00000000..e8f12f01 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/JobController.java @@ -0,0 +1,36 @@ +package site.icebang.domain.workflow.controller; + +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +import site.icebang.domain.workflow.dto.JobDto; +import site.icebang.domain.workflow.service.WorkflowService; + +@RestController +@RequestMapping("/v0/jobs") +@RequiredArgsConstructor +public class JobController { + + private final WorkflowService workflowService; + + @PostMapping + public ResponseEntity> createJob(@Valid @RequestBody JobDto dto) { + JobDto created = workflowService.createJob(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("success", true, "data", created)); + } + + @GetMapping("/{id}") + public ResponseEntity> getJob(@PathVariable Long id) { + JobDto job = workflowService.findJobById(id); + if (job == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("success", false)); + } + return ResponseEntity.ok(Map.of("success", true, "data", job)); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/TaskController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/TaskController.java new file mode 100644 index 00000000..fd3ce8f4 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/TaskController.java @@ -0,0 +1,36 @@ +package site.icebang.domain.workflow.controller; + +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +import site.icebang.domain.workflow.dto.TaskDto; +import site.icebang.domain.workflow.service.WorkflowService; + +@RestController +@RequestMapping("/v0/tasks") +@RequiredArgsConstructor +public class TaskController { + + private final WorkflowService workflowService; + + @PostMapping + public ResponseEntity> createTask(@Valid @RequestBody TaskDto dto) { + TaskDto created = workflowService.createTask(dto); + return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("success", true, "data", created)); + } + + @GetMapping("/{id}") + public ResponseEntity> getTask(@PathVariable Long id) { + TaskDto task = workflowService.findTaskById(id); + if (task == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("success", false)); + } + return ResponseEntity.ok(Map.of("success", true, "data", task)); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobDto.java index 035d6d17..8911a4e7 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobDto.java @@ -2,12 +2,16 @@ import java.time.Instant; +import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data public class JobDto { private Long id; + + @NotBlank(message = "Job 이름은 필수입니다") private String name; + private String description; private Boolean isEnabled; private Instant createdAt; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskDto.java index 1047d141..8d323f3b 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskDto.java @@ -4,12 +4,16 @@ import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data public class TaskDto { private Long id; + + @NotBlank(message = "Task 이름은 필수입니다") private String name; + private String type; private Integer executionOrder; private JsonNode settings; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java index e03ac06d..0ae89235 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java @@ -12,4 +12,8 @@ public interface JobMapper { List findJobsByWorkflowId(Long workflowId); List findTasksByJobId(Long jobId); + + JobDto findJobById(Long id); + + void insertJob(JobDto job); } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java index 0edb7812..c15d14ad 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java @@ -4,9 +4,14 @@ import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.workflow.dto.TaskDto; import site.icebang.domain.workflow.model.Task; @Mapper public interface TaskMapper { Optional findById(Long id); + + void insertTask(TaskDto task); + + TaskDto findTaskById(Long id); } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java index c363f8de..41cd5fa8 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java @@ -26,7 +26,7 @@ public Job(JobDto dto) { this.id = dto.getId(); this.name = dto.getName(); this.description = dto.getDescription(); - this.isEnabled = dto.getIsEnabled(); + this.isEnabled = Boolean.TRUE.equals(dto.getIsEnabled()); this.createdAt = dto.getCreatedAt(); this.updatedAt = dto.getUpdatedAt(); } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java index 04d577c1..bfa1b96f 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java @@ -36,5 +36,7 @@ public Task(TaskDto taskDto) { this.type = taskDto.getType(); this.settings = taskDto.getSettings(); this.parameters = taskDto.getParameters(); + this.createdAt = taskDto.getCreatedAt(); + this.updatedAt = taskDto.getUpdatedAt(); } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java index 30a00c55..69a55002 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java @@ -1,6 +1,7 @@ package site.icebang.domain.workflow.service; import java.math.BigInteger; +import java.time.Instant; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -21,11 +22,9 @@ import site.icebang.domain.schedule.mapper.ScheduleMapper; import site.icebang.domain.schedule.model.Schedule; import site.icebang.domain.schedule.service.QuartzScheduleService; -import site.icebang.domain.workflow.dto.ScheduleCreateDto; -import site.icebang.domain.workflow.dto.ScheduleDto; -import site.icebang.domain.workflow.dto.WorkflowCardDto; -import site.icebang.domain.workflow.dto.WorkflowCreateDto; -import site.icebang.domain.workflow.dto.WorkflowDetailCardDto; +import site.icebang.domain.workflow.dto.*; +import site.icebang.domain.workflow.mapper.JobMapper; +import site.icebang.domain.workflow.mapper.TaskMapper; import site.icebang.domain.workflow.mapper.WorkflowMapper; /** @@ -51,6 +50,8 @@ public class WorkflowService implements PageableService { private final WorkflowMapper workflowMapper; private final ScheduleMapper scheduleMapper; private final QuartzScheduleService quartzScheduleService; + private final JobMapper jobMapper; + private final TaskMapper taskMapper; /** * 워크플로우 목록을 페이징 처리하여 조회합니다. @@ -166,6 +167,87 @@ public void createWorkflow(WorkflowCreateDto dto, BigInteger createdBy) { } } + /** + * Job 생성 + * + * @param dto Job 생성 정보 + * @return 생성된 Job 정보 + * @throws IllegalArgumentException Job 이름이 필수인데 없거나 빈 값일 경우 + */ + @Transactional + public JobDto createJob(JobDto dto) { + // 1. 유효성 검증 + if (dto.getName() == null || dto.getName().isBlank()) { + throw new IllegalArgumentException("job name is required"); + } + + // 2. 시간 정보 설정 + Instant now = Instant.now(); + dto.setCreatedAt(now); + dto.setUpdatedAt(now); + + // 3. 생성자 정보 설정 (현재 사용자 정보가 없으므로 기본값 또는 추후 개선) + // dto.setCreatedBy(getCurrentUserId()); + // dto.setUpdatedBy(getCurrentUserId()); + + // 4. DB 저장 + jobMapper.insertJob(dto); + + // 5. 저장된 Job 반환 + return jobMapper.findJobById(dto.getId()); + } + + /** + * Job ID로 Job 조회 + * + * @param id Job ID + * @return Job 정보, 없으면 null + */ + @Transactional(readOnly = true) + public JobDto findJobById(Long id) { + return jobMapper.findJobById(id); + } + + /** + * Task 생성 + * + * @param dto Task 생성 정보 + * @return 생성된 Task 정보 + * @throws IllegalArgumentException Task 이름이 필수인데 없거나 빈 값일 경우 + */ + @Transactional + public TaskDto createTask(TaskDto dto) { + // 1. 유효성 검증 + if (dto.getName() == null || dto.getName().isBlank()) { + throw new IllegalArgumentException("task name is required"); + } + // 2. 시간 정보 설정 + Instant now = Instant.now(); + dto.setCreatedAt(now); + dto.setUpdatedAt(now); + + // 3. 생성자 정보 설정 (현재 사용자 정보가 없으므로 기본값 또는 추후 개선) + // dto.setCreatedBy(getCurrentUserId()); + // dto.setUpdatedBy(getCurrentUserId()); + + // 4. DB 저장 + taskMapper.insertTask(dto); + + // 5. 저장된 Task 반환 + return taskMapper.findTaskById(dto.getId()); + } + + /** + * Task ID로 Task 조회 + * + * @param id Task ID + * @return Task 정보, 없으면 null + */ + @Transactional(readOnly = true) + public TaskDto findTaskById(Long id) { + return taskMapper.findTaskById(id); + } + /** 기본 입력값 검증 */ private void validateBasicInput(WorkflowCreateDto dto, BigInteger createdBy) { if (dto == null) { diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java index 485e7e1e..1c064368 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java @@ -118,4 +118,17 @@ public ApiResponse handleDuplicateData(DuplicateDataException ex) { log.warn(ex.getMessage(), ex); return ApiResponse.error("Duplicate: " + ex.getMessage(), HttpStatus.CONFLICT); } + + /** + * IllegalArgumentException을 400 Bad Request로 처리합니다. WorkflowService에서 던지는 검증 오류를 처리하기 위해 추가되었습니다. + * + * @param ex 발생한 {@link IllegalArgumentException} + * @return {@link ApiResponse} - 검증 실패 메시지와 {@link HttpStatus#BAD_REQUEST} + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleIllegalArgument(IllegalArgumentException ex) { + log.warn("Validation failed: {}", ex.getMessage()); + return ApiResponse.error("입력값 검증 실패: " + ex.getMessage(), HttpStatus.BAD_REQUEST); + } } diff --git a/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml index 5b959db3..f5cd2ed0 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml @@ -23,6 +23,33 @@ + + INSERT INTO job ( + name, + description, + is_enabled, + created_at, + created_by, + updated_at, + updated_by + ) VALUES ( + #{name}, + #{description}, + #{isEnabled}, + #{createdAt}, + #{createdBy}, + #{updatedAt}, + #{updatedBy} + ) + + + + + SELECT t.* + FROM task t + WHERE t.id = #{id} + +