From 49e9706cdc10136f1d735ab49c64a46e53a0617e Mon Sep 17 00:00:00 2001 From: can019 Date: Mon, 22 Sep 2025 17:58:31 +0900 Subject: [PATCH 1/3] chore: Workflow history detail controller (mock data) --- .../controller/WorkflowHistoryController.java | 219 ++++++++++++++++++ .../security/endpoints/SecurityEndpoints.java | 3 +- 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java new file mode 100644 index 00000000..82ffa56d --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java @@ -0,0 +1,219 @@ +package site.icebang.domain.workflow.controller; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +import site.icebang.common.dto.ApiResponse; + +@RestController +@RequestMapping("/v0/workflow-runs") +@RequiredArgsConstructor +public class WorkflowHistoryController { + @GetMapping("/{runId}") + public ApiResponse> getWorkflowRunDetail(@PathVariable Long runId) { + Map response = new HashMap<>(); + + // Workflow Run + Map workflowRun = new HashMap<>(); + workflowRun.put("id", 1L); + workflowRun.put("workflowId", 1L); + workflowRun.put("workflowName", "네이버 블로그 포스팅#1"); + workflowRun.put("workflowDescription", "네이버 트렌드 기반 자동 블로그 포스팅 워크플로"); + workflowRun.put("runNumber", "RUN-2024090100001"); + workflowRun.put("status", "success"); + workflowRun.put("triggerType", "scheduled"); + workflowRun.put("startedAt", "2024-09-01T09:00:00Z"); + workflowRun.put("finishedAt", "2024-09-01T09:08:20Z"); + workflowRun.put("durationMs", 500000); + workflowRun.put("createdBy", 1L); + workflowRun.put("createdAt", "2024-09-01T09:00:00Z"); + + // Task Runs for Job 1 + List> taskRuns1 = + Arrays.asList( + createTaskRun( + 1L, + 1L, + 1L, + "네이버 트렌드 크롤링", + "실시간 네이버 트렌드 키워드를 수집하고 분석합니다", + "FastAPI", + "success", + 1, + "2024-09-01T09:00:00Z", + "2024-09-01T09:02:15Z", + 135000), + createTaskRun( + 2L, + 1L, + 2L, + "싸다구 몰 검색", + "수집된 트렌드 키워드로 싸다구 몰에서 관련 상품을 검색합니다", + "FastAPI", + "success", + 2, + "2024-09-01T09:02:15Z", + "2024-09-01T09:03:45Z", + 90000), + createTaskRun( + 3L, + 1L, + 5L, + "상품 정보 추출", + "검색된 상품들의 상세 정보를 추출하고 검증합니다", + "FastAPI", + "success", + 3, + "2024-09-01T09:03:45Z", + "2024-09-01T09:04:30Z", + 45000)); + + // Task Runs for Job 2 + List> taskRuns2 = + Arrays.asList( + createTaskRun( + 4L, + 2L, + 6L, + "콘텐츠 생성", + "AI를 활용하여 상품 정보 기반의 블로그 콘텐츠를 생성합니다", + "FastAPI", + "success", + 1, + "2024-09-01T09:04:30Z", + "2024-09-01T09:07:50Z", + 200000), + createTaskRun( + 5L, + 2L, + 7L, + "블로그 업로드", + "생성된 콘텐츠를 지정된 블로그 플랫폼에 업로드합니다", + "FastAPI", + "success", + 2, + "2024-09-01T09:07:50Z", + "2024-09-01T09:08:20Z", + 30000)); + + // Job Runs + List> jobRuns = + Arrays.asList( + createJobRun( + 1L, + 1L, + 1L, + "상품 분석", + "키워드 검색, 상품 크롤링 및 유사도 분석 작업", + "success", + 1, + "2024-09-01T09:00:00Z", + "2024-09-01T09:04:30Z", + 270000, + taskRuns1), + createJobRun( + 2L, + 1L, + 2L, + "블로그 콘텐츠 생성", + "분석 데이터를 기반으로 RAG 콘텐츠 생성 및 발행 작업", + "success", + 2, + "2024-09-01T09:04:30Z", + "2024-09-01T09:08:20Z", + 230000, + taskRuns2)); + + response.put("traceId", "550e8400-e29b-41d4-a716-446655440000"); + response.put("workflowRun", workflowRun); + response.put("jobRuns", jobRuns); + + return ApiResponse.success(response); + } + + private Map createTaskRun( + Long id, + Long jobRunId, + Long taskId, + String taskName, + String taskDescription, + String taskType, + String status, + int executionOrder, + String startedAt, + String finishedAt, + Integer durationMs) { + Map taskRun = new HashMap<>(); + taskRun.put("id", id); + taskRun.put("jobRunId", jobRunId); + taskRun.put("taskId", taskId); + taskRun.put("taskName", taskName); + taskRun.put("taskDescription", taskDescription); + taskRun.put("taskType", taskType); + taskRun.put("status", status); + taskRun.put("executionOrder", executionOrder); + taskRun.put("startedAt", startedAt); + taskRun.put("finishedAt", finishedAt); + taskRun.put("durationMs", durationMs); + return taskRun; + } + + private Map createJobRun( + Long id, + Long workflowRunId, + Long jobId, + String jobName, + String jobDescription, + String status, + int executionOrder, + String startedAt, + String finishedAt, + Integer durationMs, + List> taskRuns) { + Map jobRun = new HashMap<>(); + jobRun.put("id", id); + jobRun.put("workflowRunId", workflowRunId); + jobRun.put("jobId", jobId); + jobRun.put("jobName", jobName); + jobRun.put("jobDescription", jobDescription); + jobRun.put("status", status); + jobRun.put("executionOrder", executionOrder); + jobRun.put("startedAt", startedAt); + jobRun.put("finishedAt", finishedAt); + jobRun.put("durationMs", durationMs); + jobRun.put("taskRuns", taskRuns); + return jobRun; + } + + private Map createLog( + Long id, + String executionType, + Long sourceId, + Long runId, + String logLevel, + String status, + String logMessage, + String executedAt, + Integer durationMs) { + Map log = new HashMap<>(); + log.put("id", id); + log.put("executionType", executionType); + log.put("sourceId", sourceId); + log.put("runId", runId); + log.put("logLevel", logLevel); + log.put("status", status); + log.put("logMessage", logMessage); + log.put("executedAt", executedAt); + log.put("durationMs", durationMs); + return log; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java index e6e24243..26b72a73 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java @@ -12,7 +12,8 @@ public enum SecurityEndpoints { "/images/**", "/v0/organizations/**", "/v0/auth/register", - "/v0/check-execution-log-insert"), + "/v0/check-execution-log-insert", + "/v0/workflow-runs/**"), // 데이터 관리 관련 엔드포인트 DATA_ADMIN("/admin/**", "/api/admin/**", "/management/**", "/actuator/**"), From 554f322690ad3b384285559ea2d6c62c4d9bf9ed Mon Sep 17 00:00:00 2001 From: can019 Date: Mon, 22 Sep 2025 18:07:40 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20Workflow=20history=20detail?= =?UTF-8?q?=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20dto=20=EB=B0=8F=20ser?= =?UTF-8?q?vice=20method=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WorkflowHistoryController.java | 3 ++ .../domain/workflow/dto/ExecutionLogDto.java | 22 +++++++++ .../domain/workflow/dto/JobRunDto.java | 24 ++++++++++ .../domain/workflow/dto/TaskRunDto.java | 24 ++++++++++ .../workflow/dto/WorkflowDetailResponse.java | 16 +++++++ .../domain/workflow/dto/WorkflowRunDto.java | 25 ++++++++++ .../workflow/dto/WorkflowRunLogsResponse.java | 17 +++++++ .../service/WorkflowHistoryService.java | 46 +++++++++++++++++++ 8 files changed, 177 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/ExecutionLogDto.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskRunDto.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowDetailResponse.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDto.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunLogsResponse.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java index 82ffa56d..c7d85e62 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java @@ -13,11 +13,14 @@ import lombok.RequiredArgsConstructor; import site.icebang.common.dto.ApiResponse; +import site.icebang.domain.workflow.service.WorkflowHistoryService; @RestController @RequestMapping("/v0/workflow-runs") @RequiredArgsConstructor public class WorkflowHistoryController { + private final WorkflowHistoryService workflowHistoryService; + @GetMapping("/{runId}") public ApiResponse> getWorkflowRunDetail(@PathVariable Long runId) { Map response = new HashMap<>(); diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/ExecutionLogDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/ExecutionLogDto.java new file mode 100644 index 00000000..5dbb5711 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/ExecutionLogDto.java @@ -0,0 +1,22 @@ +package site.icebang.domain.workflow.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ExecutionLogDto { + private Long id; // execution_log.id + private String executionType; // workflow, job, task + private Long sourceId; // 모든 데이터에 대한 ID + private Long runId; // 실행 ID (workflow_run, job_run, task_run) + private String logLevel; // info, success, warning, error + private String status; // running, success, failed, etc + private String logMessage; + private String executedAt; + private Integer durationMs; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java new file mode 100644 index 00000000..c06d907d --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java @@ -0,0 +1,24 @@ +package site.icebang.domain.workflow.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JobRunDto { + private Long id; // job_run.id (Job 실행 ID) + private Long workflowRunId; // workflow_run.id (관계) + private Long jobId; // job.id (Job 설계 ID) + private String jobName; + private String jobDescription; + private String status; + private Integer executionOrder; + private String startedAt; + private String finishedAt; + private Integer durationMs; + private List taskRuns; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskRunDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskRunDto.java new file mode 100644 index 00000000..9005c45a --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/TaskRunDto.java @@ -0,0 +1,24 @@ +package site.icebang.domain.workflow.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskRunDto { + private Long id; // task_run.id (Task 실행 ID) + private Long jobRunId; // job_run.id (관계) + private Long taskId; // task.id (Task 설계 ID) + private String taskName; + private String taskDescription; + private String taskType; + private String status; + private Integer executionOrder; + private String startedAt; + private String finishedAt; + private Integer durationMs; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowDetailResponse.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowDetailResponse.java new file mode 100644 index 00000000..a6ebdf4c --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowDetailResponse.java @@ -0,0 +1,16 @@ +package site.icebang.domain.workflow.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WorkflowRunDetailResponse { + private String traceId; + private WorkflowRunDto workflowRun; + private List jobRuns; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDto.java new file mode 100644 index 00000000..20b8ecd2 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDto.java @@ -0,0 +1,25 @@ +package site.icebang.domain.workflow.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WorkflowRunDto { + private Long id; // workflow_run.id (실행 ID) + private Long workflowId; // workflow.id (설계 ID) + private String workflowName; + private String workflowDescription; + private String runNumber; + private String status; + private String triggerType; + private String startedAt; + private String finishedAt; + private Integer durationMs; + private Long createdBy; + private String createdAt; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunLogsResponse.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunLogsResponse.java new file mode 100644 index 00000000..ff5304f5 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunLogsResponse.java @@ -0,0 +1,17 @@ +package site.icebang.domain.workflow.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WorkflowRunLogsResponse { + private String traceId; + private List logs; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java new file mode 100644 index 00000000..536d5ce1 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java @@ -0,0 +1,46 @@ +package site.icebang.domain.workflow.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import site.icebang.domain.workflow.dto.WorkflowRunDetailResponse; +import site.icebang.domain.workflow.dto.WorkflowRunLogsResponse; + +@Service +@RequiredArgsConstructor +public class WorkflowHistoryService { + + /** + * 워크플로우 실행 상세 조회 + * + * @param runId workflow_run.id + * @return WorkflowRunDetailResponse + */ + public WorkflowRunDetailResponse getWorkflowRunDetail(Long runId) { + // TODO: 구현 예정 + return null; + } + + /** + * 워크플로우 실행 로그 조회 + * + * @param runId workflow_run.id + * @return WorkflowRunLogsResponse + */ + public WorkflowRunLogsResponse getWorkflowRunLogs(Long runId) { + // TODO: 구현 예정 + return null; + } + + /** + * TraceId로 워크플로우 실행 조회 + * + * @param traceId workflow_run.trace_id + * @return WorkflowRunDetailResponse + */ + public WorkflowRunDetailResponse getWorkflowRunByTraceId(String traceId) { + // TODO: 구현 예정 + return null; + } +} From 1e1c1c0a143906411b209b74180f7eacccde0e86 Mon Sep 17 00:00:00 2001 From: can019 Date: Mon, 22 Sep 2025 19:06:21 +0900 Subject: [PATCH 3/3] feat: Workflow run detail api --- .../controller/WorkflowHistoryController.java | 209 +--------------- .../domain/workflow/dto/JobRunDto.java | 2 + ...se.java => WorkflowRunDetailResponse.java} | 2 + .../mapper/WorkflowHistoryMapper.java | 44 ++++ .../service/WorkflowHistoryService.java | 35 ++- .../security/endpoints/SecurityEndpoints.java | 3 +- .../mybatis/mapper/WorkflowHistoryMapper.xml | 71 ++++++ .../sql/04-insert-workflow-history.sql | 76 ++++++ .../WorkflowHistoryApiIntegrationTest.java | 228 ++++++++++++++++++ 9 files changed, 464 insertions(+), 206 deletions(-) rename apps/user-service/src/main/java/site/icebang/domain/workflow/dto/{WorkflowDetailResponse.java => WorkflowRunDetailResponse.java} (93%) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowHistoryMapper.java create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml create mode 100644 apps/user-service/src/main/resources/sql/04-insert-workflow-history.sql create mode 100644 apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowHistoryApiIntegrationTest.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java index c7d85e62..8ca79726 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java @@ -1,10 +1,5 @@ package site.icebang.domain.workflow.controller; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,6 +8,7 @@ import lombok.RequiredArgsConstructor; import site.icebang.common.dto.ApiResponse; +import site.icebang.domain.workflow.dto.WorkflowRunDetailResponse; import site.icebang.domain.workflow.service.WorkflowHistoryService; @RestController @@ -21,202 +17,15 @@ public class WorkflowHistoryController { private final WorkflowHistoryService workflowHistoryService; + /** + * 워크플로우 실행 상세 조회 + * + * @param runId workflow_run.id + * @return WorkflowRunDetailResponse + */ @GetMapping("/{runId}") - public ApiResponse> getWorkflowRunDetail(@PathVariable Long runId) { - Map response = new HashMap<>(); - - // Workflow Run - Map workflowRun = new HashMap<>(); - workflowRun.put("id", 1L); - workflowRun.put("workflowId", 1L); - workflowRun.put("workflowName", "네이버 블로그 포스팅#1"); - workflowRun.put("workflowDescription", "네이버 트렌드 기반 자동 블로그 포스팅 워크플로"); - workflowRun.put("runNumber", "RUN-2024090100001"); - workflowRun.put("status", "success"); - workflowRun.put("triggerType", "scheduled"); - workflowRun.put("startedAt", "2024-09-01T09:00:00Z"); - workflowRun.put("finishedAt", "2024-09-01T09:08:20Z"); - workflowRun.put("durationMs", 500000); - workflowRun.put("createdBy", 1L); - workflowRun.put("createdAt", "2024-09-01T09:00:00Z"); - - // Task Runs for Job 1 - List> taskRuns1 = - Arrays.asList( - createTaskRun( - 1L, - 1L, - 1L, - "네이버 트렌드 크롤링", - "실시간 네이버 트렌드 키워드를 수집하고 분석합니다", - "FastAPI", - "success", - 1, - "2024-09-01T09:00:00Z", - "2024-09-01T09:02:15Z", - 135000), - createTaskRun( - 2L, - 1L, - 2L, - "싸다구 몰 검색", - "수집된 트렌드 키워드로 싸다구 몰에서 관련 상품을 검색합니다", - "FastAPI", - "success", - 2, - "2024-09-01T09:02:15Z", - "2024-09-01T09:03:45Z", - 90000), - createTaskRun( - 3L, - 1L, - 5L, - "상품 정보 추출", - "검색된 상품들의 상세 정보를 추출하고 검증합니다", - "FastAPI", - "success", - 3, - "2024-09-01T09:03:45Z", - "2024-09-01T09:04:30Z", - 45000)); - - // Task Runs for Job 2 - List> taskRuns2 = - Arrays.asList( - createTaskRun( - 4L, - 2L, - 6L, - "콘텐츠 생성", - "AI를 활용하여 상품 정보 기반의 블로그 콘텐츠를 생성합니다", - "FastAPI", - "success", - 1, - "2024-09-01T09:04:30Z", - "2024-09-01T09:07:50Z", - 200000), - createTaskRun( - 5L, - 2L, - 7L, - "블로그 업로드", - "생성된 콘텐츠를 지정된 블로그 플랫폼에 업로드합니다", - "FastAPI", - "success", - 2, - "2024-09-01T09:07:50Z", - "2024-09-01T09:08:20Z", - 30000)); - - // Job Runs - List> jobRuns = - Arrays.asList( - createJobRun( - 1L, - 1L, - 1L, - "상품 분석", - "키워드 검색, 상품 크롤링 및 유사도 분석 작업", - "success", - 1, - "2024-09-01T09:00:00Z", - "2024-09-01T09:04:30Z", - 270000, - taskRuns1), - createJobRun( - 2L, - 1L, - 2L, - "블로그 콘텐츠 생성", - "분석 데이터를 기반으로 RAG 콘텐츠 생성 및 발행 작업", - "success", - 2, - "2024-09-01T09:04:30Z", - "2024-09-01T09:08:20Z", - 230000, - taskRuns2)); - - response.put("traceId", "550e8400-e29b-41d4-a716-446655440000"); - response.put("workflowRun", workflowRun); - response.put("jobRuns", jobRuns); - + public ApiResponse getWorkflowRunDetail(@PathVariable Long runId) { + WorkflowRunDetailResponse response = workflowHistoryService.getWorkflowRunDetail(runId); return ApiResponse.success(response); } - - private Map createTaskRun( - Long id, - Long jobRunId, - Long taskId, - String taskName, - String taskDescription, - String taskType, - String status, - int executionOrder, - String startedAt, - String finishedAt, - Integer durationMs) { - Map taskRun = new HashMap<>(); - taskRun.put("id", id); - taskRun.put("jobRunId", jobRunId); - taskRun.put("taskId", taskId); - taskRun.put("taskName", taskName); - taskRun.put("taskDescription", taskDescription); - taskRun.put("taskType", taskType); - taskRun.put("status", status); - taskRun.put("executionOrder", executionOrder); - taskRun.put("startedAt", startedAt); - taskRun.put("finishedAt", finishedAt); - taskRun.put("durationMs", durationMs); - return taskRun; - } - - private Map createJobRun( - Long id, - Long workflowRunId, - Long jobId, - String jobName, - String jobDescription, - String status, - int executionOrder, - String startedAt, - String finishedAt, - Integer durationMs, - List> taskRuns) { - Map jobRun = new HashMap<>(); - jobRun.put("id", id); - jobRun.put("workflowRunId", workflowRunId); - jobRun.put("jobId", jobId); - jobRun.put("jobName", jobName); - jobRun.put("jobDescription", jobDescription); - jobRun.put("status", status); - jobRun.put("executionOrder", executionOrder); - jobRun.put("startedAt", startedAt); - jobRun.put("finishedAt", finishedAt); - jobRun.put("durationMs", durationMs); - jobRun.put("taskRuns", taskRuns); - return jobRun; - } - - private Map createLog( - Long id, - String executionType, - Long sourceId, - Long runId, - String logLevel, - String status, - String logMessage, - String executedAt, - Integer durationMs) { - Map log = new HashMap<>(); - log.put("id", id); - log.put("executionType", executionType); - log.put("sourceId", sourceId); - log.put("runId", runId); - log.put("logLevel", logLevel); - log.put("status", status); - log.put("logMessage", logMessage); - log.put("executedAt", executedAt); - log.put("durationMs", durationMs); - return log; - } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java index c06d907d..618a6214 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java @@ -1,5 +1,7 @@ package site.icebang.domain.workflow.dto; +import java.util.List; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowDetailResponse.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDetailResponse.java similarity index 93% rename from apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowDetailResponse.java rename to apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDetailResponse.java index a6ebdf4c..194e8583 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowDetailResponse.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDetailResponse.java @@ -1,5 +1,7 @@ package site.icebang.domain.workflow.dto; +import java.util.List; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowHistoryMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowHistoryMapper.java new file mode 100644 index 00000000..6833a6d7 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowHistoryMapper.java @@ -0,0 +1,44 @@ +package site.icebang.domain.workflow.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; + +import site.icebang.domain.workflow.dto.JobRunDto; +import site.icebang.domain.workflow.dto.TaskRunDto; +import site.icebang.domain.workflow.dto.WorkflowRunDto; + +@Mapper +public interface WorkflowHistoryMapper { + /** + * 워크플로우 실행 정보 조회 + * + * @param runId workflow_run.id + * @return WorkflowRunDto + */ + WorkflowRunDto selectWorkflowRun(Long runId); + + /** + * 워크플로우 실행의 Job 목록 조회 + * + * @param workflowRunId workflow_run.id + * @return List + */ + List selectJobRunsByWorkflowRunId(Long workflowRunId); + + /** + * Job 실행의 Task 목록 조회 + * + * @param jobRunId job_run.id + * @return List + */ + List selectTaskRunsByJobRunId(Long jobRunId); + + /** + * 워크플로우 실행 TraceId 조회 + * + * @param runId workflow_run.id + * @return String traceId + */ + String selectTraceIdByRunId(Long runId); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java index 536d5ce1..583e2b20 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java @@ -1,15 +1,19 @@ package site.icebang.domain.workflow.service; +import java.util.List; + import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import site.icebang.domain.workflow.dto.WorkflowRunDetailResponse; -import site.icebang.domain.workflow.dto.WorkflowRunLogsResponse; +import site.icebang.domain.workflow.dto.*; +import site.icebang.domain.workflow.mapper.WorkflowHistoryMapper; @Service @RequiredArgsConstructor public class WorkflowHistoryService { + private final WorkflowHistoryMapper workflowHistoryMapper; /** * 워크플로우 실행 상세 조회 @@ -17,9 +21,32 @@ public class WorkflowHistoryService { * @param runId workflow_run.id * @return WorkflowRunDetailResponse */ + @Transactional(readOnly = true) public WorkflowRunDetailResponse getWorkflowRunDetail(Long runId) { - // TODO: 구현 예정 - return null; + // 1. 워크플로우 실행 정보 조회 + WorkflowRunDto workflowRunDto = workflowHistoryMapper.selectWorkflowRun(runId); + + // 2. Job 실행 목록 조회 + List jobRunDtos = workflowHistoryMapper.selectJobRunsByWorkflowRunId(runId); + + // 3. 각 Job의 Task 실행 목록 조회 + if (jobRunDtos != null) { + jobRunDtos.forEach( + jobRun -> { + List taskRuns = + workflowHistoryMapper.selectTaskRunsByJobRunId(jobRun.getId()); + jobRun.setTaskRuns(taskRuns); + }); + } + + // 4. TraceId 조회 + String traceId = workflowHistoryMapper.selectTraceIdByRunId(runId); + + return WorkflowRunDetailResponse.builder() + .workflowRun(workflowRunDto) + .jobRuns(jobRunDtos) + .traceId(traceId) + .build(); } /** diff --git a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java index 26b72a73..e6e24243 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java @@ -12,8 +12,7 @@ public enum SecurityEndpoints { "/images/**", "/v0/organizations/**", "/v0/auth/register", - "/v0/check-execution-log-insert", - "/v0/workflow-runs/**"), + "/v0/check-execution-log-insert"), // 데이터 관리 관련 엔드포인트 DATA_ADMIN("/admin/**", "/api/admin/**", "/management/**", "/actuator/**"), diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml new file mode 100644 index 00000000..3ad7aa65 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/user-service/src/main/resources/sql/04-insert-workflow-history.sql b/apps/user-service/src/main/resources/sql/04-insert-workflow-history.sql new file mode 100644 index 00000000..814c3b5b --- /dev/null +++ b/apps/user-service/src/main/resources/sql/04-insert-workflow-history.sql @@ -0,0 +1,76 @@ +-- =================================================================== +-- 워크플로우 히스토리 테스트용 데이터 삽입 +-- =================================================================== + +-- 기존 실행 데이터 삭제 (참조 순서 고려) +DELETE FROM `task_run` WHERE id = 1; +DELETE FROM `job_run` WHERE id = 1; +DELETE FROM `workflow_run` WHERE id = 1; + +-- AUTO_INCREMENT 초기화 +ALTER TABLE `task_run` AUTO_INCREMENT = 1; +ALTER TABLE `job_run` AUTO_INCREMENT = 1; +ALTER TABLE `workflow_run` AUTO_INCREMENT = 1; + +-- 워크플로우 실행 데이터 삽입 (workflow_run) +INSERT INTO `workflow_run` ( + `workflow_id`, + `trace_id`, + `run_number`, + `status`, + `trigger_type`, + `started_at`, + `finished_at`, + `created_by` +) VALUES ( + 1, + '3e3c832d-b51f-48ea-95f9-98f0ae6d3413', + NULL, + 'FAILED', + NULL, + '2025-09-22 18:18:43', + '2025-09-22 18:18:44', + NULL + ); + +-- Job 실행 데이터 삽입 (job_run) +INSERT INTO `job_run` ( + `id`, + `workflow_run_id`, + `job_id`, + `status`, + `execution_order`, + `started_at`, + `finished_at`, + `created_at` +) VALUES ( + 1, + 1, + 1, + 'FAILED', + NULL, + '2025-09-22 18:18:44', + '2025-09-22 18:18:44', + NOW() + ); + +-- Task 실행 데이터 삽입 (task_run) +INSERT INTO `task_run` ( + `id`, + `job_run_id`, + `task_id`, + `status`, + `execution_order`, + `started_at`, + `finished_at`, + `created_at` +) VALUES ( + 1, + 1, + 1, + 'FAILED', + NULL, + '2025-09-22 18:18:44', + '2025-09-22 18:18:44', + NOW() + ); \ No newline at end of file diff --git a/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowHistoryApiIntegrationTest.java b/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowHistoryApiIntegrationTest.java new file mode 100644 index 00000000..4703e9f6 --- /dev/null +++ b/apps/user-service/src/test/java/site/icebang/integration/tests/workflow/WorkflowHistoryApiIntegrationTest.java @@ -0,0 +1,228 @@ +package site.icebang.integration.tests.workflow; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; + +import site.icebang.integration.setup.support.IntegrationTestSupport; + +@Sql( + value = { + "classpath:sql/01-insert-internal-users.sql", + "classpath:sql/03-insert-workflow.sql", + "classpath:sql/04-insert-workflow-history.sql" + }, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@Transactional +public class WorkflowHistoryApiIntegrationTest extends IntegrationTestSupport { + @Test + @DisplayName("워크플로우 실행 상세 조회 성공") + @WithUserDetails("admin@icebang.site") + void getWorkflowRunDetail_success() throws Exception { + // given + Long runId = 1L; + + // when & then + mockMvc + .perform( + get(getApiUrlForDocs("/v0/workflow-runs/{runId}"), runId) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + // traceId 확인 + .andExpect(jsonPath("$.data.traceId").value("3e3c832d-b51f-48ea-95f9-98f0ae6d3413")) + // workflowRun 필드 확인 + .andExpect(jsonPath("$.data.workflowRun.id").value(1)) + .andExpect(jsonPath("$.data.workflowRun.workflowId").value(1)) + .andExpect(jsonPath("$.data.workflowRun.workflowName").value("상품 분석 및 블로그 자동 발행")) + .andExpect( + jsonPath("$.data.workflowRun.workflowDescription") + .value("키워드 검색부터 상품 분석 후 블로그 발행까지의 자동화 프로세스")) + .andExpect(jsonPath("$.data.workflowRun.runNumber").isEmpty()) + .andExpect(jsonPath("$.data.workflowRun.status").value("FAILED")) + .andExpect(jsonPath("$.data.workflowRun.triggerType").isEmpty()) + .andExpect(jsonPath("$.data.workflowRun.startedAt").value("2025-09-22 18:18:43")) + .andExpect(jsonPath("$.data.workflowRun.finishedAt").value("2025-09-22 18:18:44")) + .andExpect(jsonPath("$.data.workflowRun.durationMs").value(1000)) + .andExpect(jsonPath("$.data.workflowRun.createdBy").isEmpty()) + .andExpect(jsonPath("$.data.workflowRun.createdAt").exists()) + // jobRuns 배열 확인 + .andExpect(jsonPath("$.data.jobRuns").isArray()) + .andExpect(jsonPath("$.data.jobRuns.length()").value(1)) + // jobRuns[0] 필드 확인 + .andExpect(jsonPath("$.data.jobRuns[0].id").value(1)) + .andExpect(jsonPath("$.data.jobRuns[0].workflowRunId").value(1)) + .andExpect(jsonPath("$.data.jobRuns[0].jobId").value(1)) + .andExpect(jsonPath("$.data.jobRuns[0].jobName").value("상품 분석")) + .andExpect(jsonPath("$.data.jobRuns[0].jobDescription").value("키워드 검색, 상품 크롤링 및 유사도 분석 작업")) + .andExpect(jsonPath("$.data.jobRuns[0].status").value("FAILED")) + .andExpect(jsonPath("$.data.jobRuns[0].executionOrder").isEmpty()) + .andExpect(jsonPath("$.data.jobRuns[0].startedAt").value("2025-09-22 18:18:44")) + .andExpect(jsonPath("$.data.jobRuns[0].finishedAt").value("2025-09-22 18:18:44")) + .andExpect(jsonPath("$.data.jobRuns[0].durationMs").value(0)) + // taskRuns 배열 확인 + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns").isArray()) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns.length()").value(1)) + // taskRuns[0] 필드 확인 + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].id").value(1)) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].jobRunId").value(1)) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].taskId").value(1)) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].taskName").value("키워드 검색 태스크")) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].taskDescription").isEmpty()) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].taskType").value("FastAPI")) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].status").value("FAILED")) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].executionOrder").isEmpty()) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].startedAt").value("2025-09-22 18:18:44")) + .andExpect( + jsonPath("$.data.jobRuns[0].taskRuns[0].finishedAt").value("2025-09-22 18:18:44")) + .andExpect(jsonPath("$.data.jobRuns[0].taskRuns[0].durationMs").value(0)) + .andDo( + document( + "workflow-run-detail", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Workflow History") + .summary("워크플로우 실행 상세 조회") + .description("워크플로우 실행 ID로 상세 정보를 조회합니다") + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), + fieldWithPath("data.traceId") + .type(JsonFieldType.STRING) + .description("워크플로우 실행 추적 ID"), + fieldWithPath("data.workflowRun") + .type(JsonFieldType.OBJECT) + .description("워크플로우 실행 정보"), + fieldWithPath("data.workflowRun.id") + .type(JsonFieldType.NUMBER) + .description("워크플로우 실행 ID"), + fieldWithPath("data.workflowRun.workflowId") + .type(JsonFieldType.NUMBER) + .description("워크플로우 설계 ID"), + fieldWithPath("data.workflowRun.workflowName") + .type(JsonFieldType.STRING) + .description("워크플로우 이름"), + fieldWithPath("data.workflowRun.workflowDescription") + .type(JsonFieldType.STRING) + .description("워크플로우 설명"), + fieldWithPath("data.workflowRun.runNumber") + .type(JsonFieldType.NULL) + .description("실행 번호"), + fieldWithPath("data.workflowRun.status") + .type(JsonFieldType.STRING) + .description("실행 상태"), + fieldWithPath("data.workflowRun.triggerType") + .type(JsonFieldType.NULL) + .description("트리거 유형"), + fieldWithPath("data.workflowRun.startedAt") + .type(JsonFieldType.STRING) + .description("시작 시간"), + fieldWithPath("data.workflowRun.finishedAt") + .type(JsonFieldType.STRING) + .description("완료 시간"), + fieldWithPath("data.workflowRun.durationMs") + .type(JsonFieldType.NUMBER) + .description("실행 시간(ms)"), + fieldWithPath("data.workflowRun.createdBy") + .type(JsonFieldType.NULL) + .description("생성자 ID"), + fieldWithPath("data.workflowRun.createdAt") + .type(JsonFieldType.STRING) + .description("생성 시간"), + fieldWithPath("data.jobRuns") + .type(JsonFieldType.ARRAY) + .description("Job 실행 목록"), + fieldWithPath("data.jobRuns[].id") + .type(JsonFieldType.NUMBER) + .description("Job 실행 ID"), + fieldWithPath("data.jobRuns[].workflowRunId") + .type(JsonFieldType.NUMBER) + .description("워크플로우 실행 ID"), + fieldWithPath("data.jobRuns[].jobId") + .type(JsonFieldType.NUMBER) + .description("Job 설계 ID"), + fieldWithPath("data.jobRuns[].jobName") + .type(JsonFieldType.STRING) + .description("Job 이름"), + fieldWithPath("data.jobRuns[].jobDescription") + .type(JsonFieldType.STRING) + .description("Job 설명"), + fieldWithPath("data.jobRuns[].status") + .type(JsonFieldType.STRING) + .description("Job 실행 상태"), + fieldWithPath("data.jobRuns[].executionOrder") + .type(JsonFieldType.NULL) + .description("실행 순서"), + fieldWithPath("data.jobRuns[].startedAt") + .type(JsonFieldType.STRING) + .description("Job 시작 시간"), + fieldWithPath("data.jobRuns[].finishedAt") + .type(JsonFieldType.STRING) + .description("Job 완료 시간"), + fieldWithPath("data.jobRuns[].durationMs") + .type(JsonFieldType.NUMBER) + .description("Job 실행 시간(ms)"), + fieldWithPath("data.jobRuns[].taskRuns") + .type(JsonFieldType.ARRAY) + .description("Task 실행 목록"), + fieldWithPath("data.jobRuns[].taskRuns[].id") + .type(JsonFieldType.NUMBER) + .description("Task 실행 ID"), + fieldWithPath("data.jobRuns[].taskRuns[].jobRunId") + .type(JsonFieldType.NUMBER) + .description("Job 실행 ID"), + fieldWithPath("data.jobRuns[].taskRuns[].taskId") + .type(JsonFieldType.NUMBER) + .description("Task 설계 ID"), + fieldWithPath("data.jobRuns[].taskRuns[].taskName") + .type(JsonFieldType.STRING) + .description("Task 이름"), + fieldWithPath("data.jobRuns[].taskRuns[].taskDescription") + .type(JsonFieldType.NULL) + .description("Task 설명"), + fieldWithPath("data.jobRuns[].taskRuns[].taskType") + .type(JsonFieldType.STRING) + .description("Task 유형"), + fieldWithPath("data.jobRuns[].taskRuns[].status") + .type(JsonFieldType.STRING) + .description("Task 실행 상태"), + fieldWithPath("data.jobRuns[].taskRuns[].executionOrder") + .type(JsonFieldType.NULL) + .description("Task 실행 순서"), + fieldWithPath("data.jobRuns[].taskRuns[].startedAt") + .type(JsonFieldType.STRING) + .description("Task 시작 시간"), + fieldWithPath("data.jobRuns[].taskRuns[].finishedAt") + .type(JsonFieldType.STRING) + .description("Task 완료 시간"), + fieldWithPath("data.jobRuns[].taskRuns[].durationMs") + .type(JsonFieldType.NUMBER) + .description("Task 실행 시간(ms)"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("응답 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } +}