From 8ed4c3c8f480c808648d75eab0c1f3d5db0c4f9a Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 15:44:46 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20WorkflowCreateDto=20=EC=B4=88?= =?UTF-8?q?=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/dto/WorkflowCreateDto.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java new file mode 100644 index 00000000..fe7af9f8 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java @@ -0,0 +1,104 @@ +package site.icebang.domain.workflow.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigInteger; + +/** + * 워크플로우 생성 요청 DTO + * + * 프론트엔드에서 워크플로우 생성 시 필요한 모든 정보를 담는 DTO + * - 기본 정보: 이름, 설명 + * - 플랫폼 설정: 검색 플랫폼, 포스팅 플랫폼 + * - 계정 설정: 포스팅 계정 정보 (JSON 형태로 저장) + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class WorkflowCreateDto { + @Null + private BigInteger id; + + @NotBlank(message = "워크플로우 이름은 필수입니다") + @Size(max = 100, message = "워크플로우 이름은 100자를 초과할 수 없습니다") + @Pattern(regexp = "^[가-힣a-zA-Z0-9\\s_-]+$", + message = "워크플로우 이름은 한글, 영문, 숫자, 공백, 언더스코어, 하이픈만 사용 가능합니다") + private String name; + + @Null + @Size(max = 500, message = "설명은 500자를 초과할 수 없습니다") + private String description; + + @Pattern(regexp = "^(naver|naver_store)?$", + message = "검색 플랫폼은 'naver' 또는 'naver_store'만 가능합니다") + @JsonProperty("search_platform") + private String searchPlatform; + + @Pattern(regexp = "^(naver_blog|tstory_blog|blogger)?$", + message = "포스팅 플랫폼은 'naver_blog', 'tstory_blog', 'blogger' 중 하나여야 합니다") + @JsonProperty("posting_platform") + private String postingPlatform; + + @Size(max = 100, message = "포스팅 계정 ID는 100자를 초과할 수 없습니다") + @JsonProperty("posting_account_id") + private String postingAccountId; + + @Size(max = 200, message = "포스팅 계정 비밀번호는 200자를 초과할 수 없습니다") + @JsonProperty("posting_account_password") + private String postingAccountPassword; + + @Size(max = 100, message = "블로그 이름은 100자를 초과할 수 없습니다") + @JsonProperty("blog_name") + private String blogName; + + @Builder.Default + @JsonProperty("is_enabled") + private Boolean isEnabled = true; + + // JSON 변환용 필드 (MyBatis에서 사용) + private String defaultConfigJson; + + public String genertateDefaultConfigJson() { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("{"); + + // 크롤링 플랫폼 설정 (키: "1") + if(searchPlatform != null && !searchPlatform.isBlank()) { + jsonBuilder.append("\"1\": {\"tag\": \"").append(searchPlatform).append("\"}"); + } + + // 포스팅 설정 (키: "8") + if(hasPostingConfig()) { + if(jsonBuilder.length() > 1) { + jsonBuilder.append(", "); + } + jsonBuilder.append("\"8\": {") + .append("\"tag\": \"").append(postingPlatform).append("\", ") + .append("\"blog_id\": \"").append(postingAccountId).append("\", ") + .append("\"blog_pw\": \"").append(postingAccountPassword).append("\""); + + // tstory_blog인 경우 blog_name 추가 + if ("tstory_blog".equals(postingPlatform) && blogName != null && !blogName.isBlank()) { + jsonBuilder.append(", \"blog_name\": \"").append(blogName).append("\""); + } + + jsonBuilder.append("}"); + } + + jsonBuilder.append("}"); + return jsonBuilder.toString(); + } + + // 포스팅 설정 완성도 체크 (상태 확인 유틸) + public boolean hasPostingConfig() { + return postingPlatform != null && !postingPlatform.isBlank() && + postingAccountId != null && !postingAccountId.isBlank() && + postingAccountPassword != null && !postingAccountPassword.isBlank(); + } +} From 98c92eeeada19f6438e891910ca93f26177d6d58 Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 15:45:07 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20Workflow=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C,=20sql=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/mapper/WorkflowMapper.java | 10 +++ .../workflow/service/WorkflowService.java | 79 +++++++++++++++++++ .../mybatis/mapper/WorkflowMapper.xml | 25 ++++++ 3 files changed, 114 insertions(+) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java index 82381737..4227b43b 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java @@ -6,6 +6,7 @@ import site.icebang.common.dto.PageParams; 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; public interface WorkflowMapper { @@ -13,6 +14,15 @@ public interface WorkflowMapper { int selectWorkflowCount(PageParams pageParams); + int insertWorkflow(Map params); // insert workflow + + // Job 생성 관련 메서드 + void insertDefaultJobs(Map params); + void insertJobTask(Map params); + void insertWorkflowJob(Map params); + + boolean existsByName(String name); + WorkflowCardDto selectWorkflowById(BigInteger id); WorkflowDetailCardDto selectWorkflowDetailById(BigInteger workflowId); 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 b994c82e..736d075e 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,9 +1,12 @@ package site.icebang.domain.workflow.service; import java.math.BigInteger; +import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,9 +17,11 @@ import site.icebang.common.service.PageableService; 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.mapper.WorkflowMapper; +@Slf4j @Service @RequiredArgsConstructor public class WorkflowService implements PageableService { @@ -50,4 +55,78 @@ public WorkflowDetailCardDto getWorkflowDetail(BigInteger workflowId) { return workflow; } + + /** + * 워크플로우 생성 + */ + @Transactional + public void createWorkflow(WorkflowCreateDto dto, Long createdBy) { + // 1. 기본 검증 + validateBasicInput(dto, createdBy); + + // 2. 비즈니스 검증 + validateBusinessRules(dto); + + // 3. 중복체크 + if (workflowMapper.existsByName(dto.getName())) { + throw new IllegalArgumentException("이미 존재하는 워크플로우 이름입니다 : " + dto.getName()); + } + + // 4. 워크플로우 생성 + try { + // JSON 설정 생성 + String defaultConfigJson = dto.genertateDefaultConfigJson(); + dto.setDefaultConfigJson(defaultConfigJson); + + //DB 삽입 파라미터 구성 + Map params = new HashMap<>(); + params.put("dto", dto); + params.put("createdBy", createdBy); + + int result = workflowMapper.insertWorkflow(params); + if (result != 1) { + throw new RuntimeException("워크플로우 생성에 실패했습니다"); + } + + log.info("워크플로우 생성 완료: {} (생성자: {})", dto.getName(), createdBy); + + } catch (Exception e) { + log.error("워크플로우 생성 실패: {}", dto.getName(), e); + throw new RuntimeException("워크플로우 생성 중 오류가 발생했습니다", e); + } + } + + /** + * 기본 입력값 검증 + */ + private void validateBasicInput(WorkflowCreateDto dto, Long createdBy) { + if (dto == null) { + throw new IllegalArgumentException("워크플로우 정보가 필요합니다"); + } + if (createdBy == null) { + throw new IllegalArgumentException("생성자 정보가 필요합니다"); + } + } + + /** + * 비즈니스 규칙 검증 + */ + private void validateBusinessRules(WorkflowCreateDto dto) { + // 포스팅 플랫폼 선택 시 계정 정보 필수 검증 + String postingPlatform = dto.getPostingPlatform(); + if (postingPlatform != null && !postingPlatform.isBlank()) { + if (dto.getPostingAccountId() == null || dto.getPostingAccountId().isBlank()) { + throw new IllegalArgumentException("포스팅 플랫폼 선택 시 계정 ID는 필수입니다"); + } + if (dto.getPostingAccountPassword() == null || dto.getPostingAccountPassword().isBlank()) { + throw new IllegalArgumentException("포스팅 플랫폼 선택 시 계정 비밀번호는 필수입니다"); + } + // 티스토리 블로그 추가 검증 + if ("tstory_blog".equals(postingPlatform)) { + if (dto.getBlogName() == null || dto.getBlogName().isBlank()) { + throw new IllegalArgumentException("티스토리 블로그 선택 시 블로그 이름은 필수입니다"); + } + } + } + } } diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml index 63a9f6db..8452312e 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml @@ -129,4 +129,29 @@ wj.execution_order, j.id, j.name, j.description, j.is_enabled ORDER BY wj.execution_order + + + INSERT INTO workflow ( + name, + description, + is_enabled, + created_by, + created_at, + default_config + ) VALUES ( + #{dto.name}, + #{dto.description}, + #{dto.isEnabled}, + #{createdBy}, + NOW(), + #{dto.defaultConfigJson} + ) + + + + \ No newline at end of file From 6a0ed8dbf2fcc0cf35159c4d9f32fc493b0c3e5f Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 15:45:29 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20Workflow=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20post=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/controller/WorkflowController.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index fd42ea13..587b974d 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java @@ -2,6 +2,8 @@ import java.math.BigInteger; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -10,7 +12,9 @@ import site.icebang.common.dto.ApiResponse; import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; +import site.icebang.domain.auth.dto.RegisterDto; 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.service.WorkflowExecutionService; import site.icebang.domain.workflow.service.WorkflowService; @@ -29,6 +33,14 @@ public ApiResponse> getWorkflowList( return ApiResponse.success(result); } +// @PostMapping("") +// @ResponseStatus(HttpStatus.CREATED) +// public ApiResponse createWorkflow(@Valid @RequestBody WorkflowCreateDto workflowCreateDto) { +// Long currentUserId = getCurrentUserId(); +// WorkflowService.createWorkflow(workflowCreateDto, currentuserId); +// return ApiResponse.success(null); +// } + @PostMapping("/{workflowId}/run") public ResponseEntity runWorkflow(@PathVariable Long workflowId) { // HTTP 요청/응답 스레드를 블로킹하지 않도록 비동기 실행 From 0bded963d6f51a9f7bda971cfac991f6ea7580d7 Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 15:45:43 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20WorkflowCreateE2eTest=20=EC=B4=88?= =?UTF-8?q?=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/scenario/WorkflowCreateE2eTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java diff --git a/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java b/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java new file mode 100644 index 00000000..12b6fef6 --- /dev/null +++ b/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java @@ -0,0 +1,65 @@ +package site.icebang.e2e.scenario; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.*; +import org.springframework.test.context.jdbc.Sql; +import site.icebang.e2e.setup.annotation.E2eTest; +import site.icebang.e2e.setup.support.E2eTestSupport; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@Sql( + value = {"classpath:sql/00-truncate.sql", "classpath:sql/01-insert-internal-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@DisplayName("워크플로우 생성 플로우 E2E 테스트") +@E2eTest +class WorkflowCreateE2eTest extends E2eTestSupport { + + @SuppressWarnings("unchecked") + @Test + @DisplayName("사용자가 새 워크플로우를 생성하는 전체 플로우") + void completeWorkflowCreateFloㅈ() throws Exception { + logStep(1, "사용자 로그인"); + + // 1. 사용자 로그인 + performUserLogin(); + + logStep(2, "네이버 블로그 워크플로우 생성"); + + // 2. 네이버 블로그 워크플로우 생성 + + // 3. 티스토리 블로그 워크플로우 생성 (블로그명 필수) + + // 4. 포스팅 없는 검색 전용 워크플로우 (추후 예정) + + } + + /** 사용자 로그인을 수행하는 헬퍼 메서드 */ + private void performUserLogin() { + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Origin", "https://admin.icebang.site"); + headers.set("Referer", "https://admin.icebang.site/"); + + HttpEntity> entity = new HttpEntity<>(loginRequest, headers); + + ResponseEntity response = + restTemplate.postForEntity(getV0ApiUrl("/auth/login"), entity, Map.class); + + if (response.getStatusCode() != HttpStatus.OK) { + logError("사용자 로그인 실패: " + response.getStatusCode()); + throw new RuntimeException("User login failed"); + } + + logSuccess("사용자 로그인 완료"); + } +} From ca37ca7a9fa2d3d90445ddb1e114e60617a391dd Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 18:57:02 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20WorkflowController=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=83=9D=EC=84=B1=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WorkflowController.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index 587b974d..e9f1bfb3 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java @@ -2,9 +2,11 @@ import java.math.BigInteger; +import com.github.dockerjava.api.exception.UnauthorizedException; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import lombok.RequiredArgsConstructor; @@ -13,6 +15,7 @@ import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; import site.icebang.domain.auth.dto.RegisterDto; +import site.icebang.domain.auth.model.AuthCredential; import site.icebang.domain.workflow.dto.WorkflowCardDto; import site.icebang.domain.workflow.dto.WorkflowCreateDto; import site.icebang.domain.workflow.dto.WorkflowDetailCardDto; @@ -33,13 +36,23 @@ public ApiResponse> getWorkflowList( return ApiResponse.success(result); } -// @PostMapping("") -// @ResponseStatus(HttpStatus.CREATED) -// public ApiResponse createWorkflow(@Valid @RequestBody WorkflowCreateDto workflowCreateDto) { -// Long currentUserId = getCurrentUserId(); -// WorkflowService.createWorkflow(workflowCreateDto, currentuserId); -// return ApiResponse.success(null); -// } + @PostMapping("") + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse createWorkflow( + @Valid @RequestBody WorkflowCreateDto workflowCreateDto, + @AuthenticationPrincipal AuthCredential authCredential + ) { + // 인증 체크 + if (authCredential == null) { + throw new IllegalArgumentException("로그인이 필요합니다"); + } + + // AuthCredential에서 userId 추출 + BigInteger userId = authCredential.getId(); + + workflowService.createWorkflow(workflowCreateDto, userId); + return ApiResponse.success(null); + } @PostMapping("/{workflowId}/run") public ResponseEntity runWorkflow(@PathVariable Long workflowId) { From 44c152b5ff049f04aa5a9dd88d25c72aff4ba90d Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 18:57:21 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20description=20@Null=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java index fe7af9f8..e5f13916 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java @@ -31,7 +31,6 @@ public class WorkflowCreateDto { message = "워크플로우 이름은 한글, 영문, 숫자, 공백, 언더스코어, 하이픈만 사용 가능합니다") private String name; - @Null @Size(max = 500, message = "설명은 500자를 초과할 수 없습니다") private String description; From 74a8eb7a7ce950379f55cab472bb5a5d81fe42ca Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 18:57:39 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=83=9D=EC=84=B1=EC=8B=9C=20job,task=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../site/icebang/domain/workflow/mapper/WorkflowMapper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java index 4227b43b..70149f65 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java @@ -17,9 +17,9 @@ public interface WorkflowMapper { int insertWorkflow(Map params); // insert workflow // Job 생성 관련 메서드 - void insertDefaultJobs(Map params); - void insertJobTask(Map params); - void insertWorkflowJob(Map params); + void insertJobs(Map params); // 여러 Job을 동적으로 생성 + void insertWorkflowJobs(Map params); // Workflow-Job 연결 + void insertJobTasks(Map params); // Job-Task 연결 boolean existsByName(String name); From 92c5a323ffc1570bdb3878373f4fc10af0da20f5 Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 18:57:45 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=83=9D=EC=84=B1=EC=8B=9C=20job,task=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mybatis/mapper/WorkflowMapper.xml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml index 8452312e..dda398a9 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml @@ -154,4 +154,36 @@ FROM workflow WHERE name = #{name} + + + + + SELECT LAST_INSERT_ID() as id + + INSERT INTO job (name, description, created_by, created_at) VALUES + ('상품 분석', '키워드 검색, 상품 크롤링 및 유사도 분석 작업', #{createdBy}, NOW()), + ('블로그 콘텐츠 생성', '분석 데이터를 기반으로 RAG 콘텐츠 생성 및 발행 작업', #{createdBy}, NOW()) + + + + + INSERT INTO workflow_job (workflow_id, job_id, execution_order) VALUES + (#{workflowId}, #{job1Id}, 1), + (#{workflowId}, #{job2Id}, 2) + + + + + INSERT INTO job_task (job_id, task_id, execution_order) VALUES + + (#{job1Id}, 1, 1), + (#{job1Id}, 2, 2), + (#{job1Id}, 3, 3), + (#{job1Id}, 4, 4), + (#{job1Id}, 5, 5), + (#{job1Id}, 6, 6), + + (#{job2Id}, 7, 1), + (#{job2Id}, 8, 2) + \ No newline at end of file From 66bb71a28b90503c4db40e7a6229f2bbcabfdd6c Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 18:57:59 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=83=9D=EC=84=B1=EC=8B=9C=20job,task=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/service/WorkflowService.java | 106 ++++++++++++++++-- 1 file changed, 94 insertions(+), 12 deletions(-) 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 76366be3..6917604f 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,9 +1,12 @@ package site.icebang.domain.workflow.service; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,6 +17,7 @@ import site.icebang.common.service.PageableService; 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.mapper.WorkflowMapper; @@ -32,6 +36,7 @@ * @author jihu0210@naver.com * @since v0.1.0 */ +@Slf4j @Service @RequiredArgsConstructor public class WorkflowService implements PageableService { @@ -91,7 +96,7 @@ public WorkflowDetailCardDto getWorkflowDetail(BigInteger workflowId) { * 워크플로우 생성 */ @Transactional - public void createWorkflow(WorkflowCreateDto dto, Long createdBy) { + public void createWorkflow(WorkflowCreateDto dto, BigInteger createdBy) { // 1. 기본 검증 validateBasicInput(dto, createdBy); @@ -103,34 +108,111 @@ public void createWorkflow(WorkflowCreateDto dto, Long createdBy) { throw new IllegalArgumentException("이미 존재하는 워크플로우 이름입니다 : " + dto.getName()); } - // 4. 워크플로우 생성 try { - // JSON 설정 생성 + // 4. JSON 설정 생성 String defaultConfigJson = dto.genertateDefaultConfigJson(); dto.setDefaultConfigJson(defaultConfigJson); - //DB 삽입 파라미터 구성 - Map params = new HashMap<>(); - params.put("dto", dto); - params.put("createdBy", createdBy); + // 5. Workflow 삽입 + Map workflowParams = new HashMap<>(); + workflowParams.put("dto", dto); + workflowParams.put("createdBy", createdBy); - int result = workflowMapper.insertWorkflow(params); + int result = workflowMapper.insertWorkflow(workflowParams); if (result != 1) { throw new RuntimeException("워크플로우 생성에 실패했습니다"); } - log.info("워크플로우 생성 완료: {} (생성자: {})", dto.getName(), createdBy); + BigInteger workflowId = dto.getId(); + log.info("✅ Workflow 생성 완료 - ID: {}, Name: {}", workflowId, dto.getName()); + + // 6. ⭐ 템플릿 기반 Job 생성 + List jobTemplates = templateProvider.getTemplateByPlatform( + dto.getPostingPlatform() + ); + + // 7. ⭐ Job 데이터 준비 (Batch Insert) + List> jobs = new ArrayList<>(); + for (WorkflowJobTemplate template : jobTemplates) { + Map job = new HashMap<>(); + job.put("name", template.getName()); + job.put("description", template.getDescription()); + jobs.add(job); + } + + // 8. ⭐ Job Batch Insert + Map jobParams = new HashMap<>(); + jobParams.put("jobs", jobs); + jobParams.put("createdBy", createdBy); + workflowMapper.insertJobs(jobParams); + + log.info("✅ Job {} 개 Batch Insert 완료", jobs.size()); + + // 9. ⭐ 생성된 Job ID 조회 (안전한 방법) + List createdJobIds = workflowMapper.selectLastInsertedJobIds(createdBy); + + if (createdJobIds.size() != jobTemplates.size()) { + throw new RuntimeException( + String.format("Job 생성 개수 불일치: 예상=%d, 실제=%d", + jobTemplates.size(), createdJobIds.size()) + ); + } + + log.info("✅ 생성된 Job IDs: {}", createdJobIds); + + // 10. ⭐ Workflow-Job 연결 데이터 준비 + List> workflowJobs = new ArrayList<>(); + for (int i = 0; i < jobTemplates.size(); i++) { + Map wj = new HashMap<>(); + wj.put("workflowId", workflowId); + wj.put("jobId", createdJobIds.get(i)); + wj.put("executionOrder", jobTemplates.get(i).getExecutionOrder()); + workflowJobs.add(wj); + } + + // 11. ⭐ Workflow-Job 연결 + Map wjParams = new HashMap<>(); + wjParams.put("workflowJobs", workflowJobs); + workflowMapper.insertWorkflowJobs(wjParams); + + log.info("✅ Workflow-Job 연결 완료 - {} 개", workflowJobs.size()); + + // 12. ⭐ Job-Task 연결 데이터 준비 + List> jobTasks = new ArrayList<>(); + for (int i = 0; i < jobTemplates.size(); i++) { + Long jobId = createdJobIds.get(i); + WorkflowJobTemplate template = jobTemplates.get(i); + + List taskIds = template.getTaskIds(); + for (int j = 0; j < taskIds.size(); j++) { + Map jt = new HashMap<>(); + jt.put("jobId", jobId); + jt.put("taskId", taskIds.get(j)); + jt.put("executionOrder", j + 1); // 1부터 시작 + jobTasks.add(jt); + } + } + + // 13. ⭐ Job-Task 연결 + Map jtParams = new HashMap<>(); + jtParams.put("jobTasks", jobTasks); + workflowMapper.insertJobTasks(jtParams); + + log.info("✅ Job-Task 연결 완료 - {} 개", jobTasks.size()); + + log.info("🎉 워크플로우 전체 생성 완료: {} (ID: {}, Jobs: {}, Tasks: {}, 생성자: {})", + dto.getName(), workflowId, createdJobIds.size(), jobTasks.size(), createdBy); } catch (Exception e) { - log.error("워크플로우 생성 실패: {}", dto.getName(), e); - throw new RuntimeException("워크플로우 생성 중 오류가 발생했습니다", e); + log.error("❌ 워크플로우 생성 실패: {}", dto.getName(), e); + throw new RuntimeException("워크플로우 생성 중 오류가 발생했습니다: " + e.getMessage(), e); } } /** * 기본 입력값 검증 */ - private void validateBasicInput(WorkflowCreateDto dto, Long createdBy) { + private void validateBasicInput(WorkflowCreateDto dto, BigInteger createdBy) { if (dto == null) { throw new IllegalArgumentException("워크플로우 정보가 필요합니다"); } From 9a9d4158dc0332d6a6abcaace3b8f054264ca040 Mon Sep 17 00:00:00 2001 From: bwnfo3 Date: Thu, 25 Sep 2025 18:58:33 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=83=9D=EC=84=B1=20e2eTest=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/scenario/WorkflowCreateE2eTest.java | 65 ------ .../scenario/WorkflowCreateFlowE2eTest.java | 220 ++++++++++++++++++ 2 files changed, 220 insertions(+), 65 deletions(-) delete mode 100644 apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java create mode 100644 apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java diff --git a/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java b/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java deleted file mode 100644 index 12b6fef6..00000000 --- a/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateE2eTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package site.icebang.e2e.scenario; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.*; -import org.springframework.test.context.jdbc.Sql; -import site.icebang.e2e.setup.annotation.E2eTest; -import site.icebang.e2e.setup.support.E2eTestSupport; - -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -@Sql( - value = {"classpath:sql/00-truncate.sql", "classpath:sql/01-insert-internal-users.sql"}, - executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) -@DisplayName("워크플로우 생성 플로우 E2E 테스트") -@E2eTest -class WorkflowCreateE2eTest extends E2eTestSupport { - - @SuppressWarnings("unchecked") - @Test - @DisplayName("사용자가 새 워크플로우를 생성하는 전체 플로우") - void completeWorkflowCreateFloㅈ() throws Exception { - logStep(1, "사용자 로그인"); - - // 1. 사용자 로그인 - performUserLogin(); - - logStep(2, "네이버 블로그 워크플로우 생성"); - - // 2. 네이버 블로그 워크플로우 생성 - - // 3. 티스토리 블로그 워크플로우 생성 (블로그명 필수) - - // 4. 포스팅 없는 검색 전용 워크플로우 (추후 예정) - - } - - /** 사용자 로그인을 수행하는 헬퍼 메서드 */ - private void performUserLogin() { - Map loginRequest = new HashMap<>(); - loginRequest.put("email", "admin@icebang.site"); - loginRequest.put("password", "qwer1234!A"); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("Origin", "https://admin.icebang.site"); - headers.set("Referer", "https://admin.icebang.site/"); - - HttpEntity> entity = new HttpEntity<>(loginRequest, headers); - - ResponseEntity response = - restTemplate.postForEntity(getV0ApiUrl("/auth/login"), entity, Map.class); - - if (response.getStatusCode() != HttpStatus.OK) { - logError("사용자 로그인 실패: " + response.getStatusCode()); - throw new RuntimeException("User login failed"); - } - - logSuccess("사용자 로그인 완료"); - } -} diff --git a/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java b/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java new file mode 100644 index 00000000..c5f872d1 --- /dev/null +++ b/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java @@ -0,0 +1,220 @@ +package site.icebang.e2e.scenario; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.*; +import org.springframework.test.context.jdbc.Sql; +import site.icebang.e2e.setup.annotation.E2eTest; +import site.icebang.e2e.setup.support.E2eTestSupport; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@Sql( + value = {"classpath:sql/00-truncate.sql", "classpath:sql/01-insert-internal-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@DisplayName("워크플로우 생성 플로우 E2E 테스트") +@E2eTest +class WorkflowCreateFlowE2eTest extends E2eTestSupport { + + @SuppressWarnings("unchecked") + @Test + @DisplayName("사용자가 새 워크플로우를 생성하는 전체 플로우") + void completeWorkflowCreateFlow() throws Exception { + logStep(1, "사용자 로그인"); + + // 1. 로그인 (세션에 userId 저장) + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + HttpHeaders loginHeaders = new HttpHeaders(); + loginHeaders.setContentType(MediaType.APPLICATION_JSON); + loginHeaders.set("Origin", "https://admin.icebang.site"); + loginHeaders.set("Referer", "https://admin.icebang.site/"); + + HttpEntity> loginEntity = new HttpEntity<>(loginRequest, loginHeaders); + + ResponseEntity loginResponse = + restTemplate.postForEntity(getV0ApiUrl("/auth/login"), loginEntity, Map.class); + + assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat((Boolean) loginResponse.getBody().get("success")).isTrue(); + + logSuccess("사용자 로그인 성공 - 세션 쿠키 자동 저장됨"); + logDebug("현재 세션 쿠키: " + getSessionCookies()); + + logStep(2, "네이버 블로그 워크플로우 생성"); + + // 2. 네이버 블로그 워크플로우 생성 + Map naverBlogWorkflow = new HashMap<>(); + naverBlogWorkflow.put("name", "상품 분석 및 네이버 블로그 자동 발행"); + naverBlogWorkflow.put("description", "키워드 검색부터 상품 분석 후 네이버 블로그 발행까지의 자동화 프로세스"); + naverBlogWorkflow.put("search_platform", "naver"); + naverBlogWorkflow.put("posting_platform", "naver_blog"); + naverBlogWorkflow.put("posting_account_id", "test_naver_blog"); + naverBlogWorkflow.put("posting_account_password", "naver_password123"); + naverBlogWorkflow.put("is_enabled", true); + + HttpHeaders workflowHeaders = new HttpHeaders(); + workflowHeaders.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> naverEntity = + new HttpEntity<>(naverBlogWorkflow, workflowHeaders); + + ResponseEntity naverResponse = + restTemplate.postForEntity(getV0ApiUrl("/workflows"), naverEntity, Map.class); + + assertThat(naverResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat((Boolean) naverResponse.getBody().get("success")).isTrue(); + + logSuccess("네이버 블로그 워크플로우 생성 성공"); + + logStep(3, "티스토리 블로그 워크플로우 생성 (블로그명 포함)"); + + // 3. 티스토리 블로그 워크플로우 생성 (블로그명 필수) + Map tstoryWorkflow = new HashMap<>(); + tstoryWorkflow.put("name", "티스토리 자동 발행 워크플로우"); + tstoryWorkflow.put("description", "티스토리 블로그 자동 포스팅"); + tstoryWorkflow.put("search_platform", "naver"); + tstoryWorkflow.put("posting_platform", "tstory_blog"); + tstoryWorkflow.put("posting_account_id", "test_tstory"); + tstoryWorkflow.put("posting_account_password", "tstory_password123"); + tstoryWorkflow.put("blog_name", "my-tech-blog"); // 티스토리는 블로그명 필수 + tstoryWorkflow.put("is_enabled", true); + + HttpEntity> tstoryEntity = + new HttpEntity<>(tstoryWorkflow, workflowHeaders); + + ResponseEntity tstoryResponse = + restTemplate.postForEntity(getV0ApiUrl("/workflows"), tstoryEntity, Map.class); + + assertThat(tstoryResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat((Boolean) tstoryResponse.getBody().get("success")).isTrue(); + + logSuccess("티스토리 워크플로우 생성 성공"); + + logStep(4, "검색만 하는 워크플로우 생성 (포스팅 없음)"); + + // 4. 포스팅 없는 검색 전용 워크플로우 (추후 예정) + Map searchOnlyWorkflow = new HashMap<>(); + searchOnlyWorkflow.put("name", "검색 전용 워크플로우"); + searchOnlyWorkflow.put("description", "상품 검색 및 분석만 수행"); + searchOnlyWorkflow.put("search_platform", "naver"); + searchOnlyWorkflow.put("is_enabled", true); + // posting_platform, posting_account_id, posting_account_password는 선택사항 + + HttpEntity> searchOnlyEntity = + new HttpEntity<>(searchOnlyWorkflow, workflowHeaders); + + ResponseEntity searchOnlyResponse = + restTemplate.postForEntity(getV0ApiUrl("/workflows"), searchOnlyEntity, Map.class); + + assertThat(searchOnlyResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat((Boolean) searchOnlyResponse.getBody().get("success")).isTrue(); + + logSuccess("검색 전용 워크플로우 생성 성공"); + + logCompletion("워크플로우 생성 플로우 완료"); + } + + @Test + @DisplayName("중복된 이름으로 워크플로우 생성 시도 시 실패") + void createWorkflow_withDuplicateName_shouldFail() { + // 선행 조건: 로그인 + performUserLogin(); + + logStep(1, "첫 번째 워크플로우 생성"); + + // 첫 번째 워크플로우 생성 + Map firstWorkflow = new HashMap<>(); + firstWorkflow.put("name", "중복테스트워크플로우"); + firstWorkflow.put("search_platform", "naver"); + firstWorkflow.put("is_enabled", true); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> firstEntity = new HttpEntity<>(firstWorkflow, headers); + + ResponseEntity firstResponse = + restTemplate.postForEntity(getV0ApiUrl("/workflows"), firstEntity, Map.class); + + assertThat(firstResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + logSuccess("첫 번째 워크플로우 생성 성공"); + + logStep(2, "동일한 이름으로 두 번째 워크플로우 생성 시도"); + + // 동일한 이름으로 다시 생성 시도 + Map duplicateWorkflow = new HashMap<>(); + duplicateWorkflow.put("name", "중복테스트워크플로우"); // 동일한 이름 + duplicateWorkflow.put("search_platform", "naver_store"); + duplicateWorkflow.put("is_enabled", true); + + HttpEntity> duplicateEntity = + new HttpEntity<>(duplicateWorkflow, headers); + + ResponseEntity duplicateResponse = + restTemplate.postForEntity(getV0ApiUrl("/workflows"), duplicateEntity, Map.class); + + // 중복 이름 처리 확인 (400 또는 409 예상) + assertThat(duplicateResponse.getStatusCode()) + .isIn(HttpStatus.BAD_REQUEST, HttpStatus.CONFLICT, HttpStatus.INTERNAL_SERVER_ERROR); + + logSuccess("중복 이름 워크플로우 생성 차단 확인"); + } + + @Test + @DisplayName("필수 필드 누락 시 워크플로우 생성 실패") + void createWorkflow_withMissingRequiredFields_shouldFail() { + // 선행 조건: 로그인 + performUserLogin(); + + logStep(1, "워크플로우 이름 없이 생성 시도"); + + // 이름 없는 요청 + Map noNameWorkflow = new HashMap<>(); + noNameWorkflow.put("search_platform", "naver"); + noNameWorkflow.put("is_enabled", true); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(noNameWorkflow, headers); + + ResponseEntity response = + restTemplate.postForEntity(getV0ApiUrl("/workflows"), entity, Map.class); + + assertThat(response.getStatusCode()) + .isIn(HttpStatus.BAD_REQUEST, HttpStatus.UNPROCESSABLE_ENTITY); + + logSuccess("필수 필드 검증 확인"); + } + + /** 사용자 로그인을 수행하는 헬퍼 메서드 */ + private void performUserLogin() { + Map loginRequest = new HashMap<>(); + loginRequest.put("email", "admin@icebang.site"); + loginRequest.put("password", "qwer1234!A"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Origin", "https://admin.icebang.site"); + headers.set("Referer", "https://admin.icebang.site/"); + + HttpEntity> entity = new HttpEntity<>(loginRequest, headers); + + ResponseEntity response = + restTemplate.postForEntity(getV0ApiUrl("/auth/login"), entity, Map.class); + + if (response.getStatusCode() != HttpStatus.OK) { + logError("사용자 로그인 실패: " + response.getStatusCode()); + throw new RuntimeException("User login failed"); + } + + logSuccess("사용자 로그인 완료"); + } +} From 7ad0c5d2d43643820723641d99dddbe7a0f7b492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=8B=88=EC=83=88=EC=BB=B4=5Cbwnfo?= Date: Thu, 25 Sep 2025 21:50:55 +0900 Subject: [PATCH 11/12] chore: spotlessApply --- .../controller/WorkflowController.java | 9 +- .../workflow/dto/WorkflowCreateDto.java | 171 +++++++++--------- .../workflow/mapper/WorkflowMapper.java | 9 +- .../scenario/WorkflowCreateFlowE2eTest.java | 49 +++-- 4 files changed, 122 insertions(+), 116 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index e9f1bfb3..c98ece1f 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java @@ -2,19 +2,17 @@ import java.math.BigInteger; -import com.github.dockerjava.api.exception.UnauthorizedException; -import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import site.icebang.common.dto.ApiResponse; import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; -import site.icebang.domain.auth.dto.RegisterDto; import site.icebang.domain.auth.model.AuthCredential; import site.icebang.domain.workflow.dto.WorkflowCardDto; import site.icebang.domain.workflow.dto.WorkflowCreateDto; @@ -39,9 +37,8 @@ public ApiResponse> getWorkflowList( @PostMapping("") @ResponseStatus(HttpStatus.CREATED) public ApiResponse createWorkflow( - @Valid @RequestBody WorkflowCreateDto workflowCreateDto, - @AuthenticationPrincipal AuthCredential authCredential - ) { + @Valid @RequestBody WorkflowCreateDto workflowCreateDto, + @AuthenticationPrincipal AuthCredential authCredential) { // 인증 체크 if (authCredential == null) { throw new IllegalArgumentException("로그인이 필요합니다"); diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java index e5f13916..bcd0cc56 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCreateDto.java @@ -1,103 +1,112 @@ package site.icebang.domain.workflow.dto; +import java.math.BigInteger; + import com.fasterxml.jackson.annotation.JsonProperty; + import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.math.BigInteger; - /** * 워크플로우 생성 요청 DTO * - * 프론트엔드에서 워크플로우 생성 시 필요한 모든 정보를 담는 DTO - * - 기본 정보: 이름, 설명 - * - 플랫폼 설정: 검색 플랫폼, 포스팅 플랫폼 - * - 계정 설정: 포스팅 계정 정보 (JSON 형태로 저장) + *

프론트엔드에서 워크플로우 생성 시 필요한 모든 정보를 담는 DTO - 기본 정보: 이름, 설명 - 플랫폼 설정: 검색 플랫폼, 포스팅 플랫폼 - 계정 설정: 포스팅 계정 + * 정보 (JSON 형태로 저장) */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class WorkflowCreateDto { - @Null - private BigInteger id; - - @NotBlank(message = "워크플로우 이름은 필수입니다") - @Size(max = 100, message = "워크플로우 이름은 100자를 초과할 수 없습니다") - @Pattern(regexp = "^[가-힣a-zA-Z0-9\\s_-]+$", - message = "워크플로우 이름은 한글, 영문, 숫자, 공백, 언더스코어, 하이픈만 사용 가능합니다") - private String name; - - @Size(max = 500, message = "설명은 500자를 초과할 수 없습니다") - private String description; - - @Pattern(regexp = "^(naver|naver_store)?$", - message = "검색 플랫폼은 'naver' 또는 'naver_store'만 가능합니다") - @JsonProperty("search_platform") - private String searchPlatform; - - @Pattern(regexp = "^(naver_blog|tstory_blog|blogger)?$", - message = "포스팅 플랫폼은 'naver_blog', 'tstory_blog', 'blogger' 중 하나여야 합니다") - @JsonProperty("posting_platform") - private String postingPlatform; - - @Size(max = 100, message = "포스팅 계정 ID는 100자를 초과할 수 없습니다") - @JsonProperty("posting_account_id") - private String postingAccountId; - - @Size(max = 200, message = "포스팅 계정 비밀번호는 200자를 초과할 수 없습니다") - @JsonProperty("posting_account_password") - private String postingAccountPassword; - - @Size(max = 100, message = "블로그 이름은 100자를 초과할 수 없습니다") - @JsonProperty("blog_name") - private String blogName; - - @Builder.Default - @JsonProperty("is_enabled") - private Boolean isEnabled = true; - - // JSON 변환용 필드 (MyBatis에서 사용) - private String defaultConfigJson; - - public String genertateDefaultConfigJson() { - StringBuilder jsonBuilder = new StringBuilder(); - jsonBuilder.append("{"); - - // 크롤링 플랫폼 설정 (키: "1") - if(searchPlatform != null && !searchPlatform.isBlank()) { - jsonBuilder.append("\"1\": {\"tag\": \"").append(searchPlatform).append("\"}"); - } - - // 포스팅 설정 (키: "8") - if(hasPostingConfig()) { - if(jsonBuilder.length() > 1) { - jsonBuilder.append(", "); - } - jsonBuilder.append("\"8\": {") - .append("\"tag\": \"").append(postingPlatform).append("\", ") - .append("\"blog_id\": \"").append(postingAccountId).append("\", ") - .append("\"blog_pw\": \"").append(postingAccountPassword).append("\""); - - // tstory_blog인 경우 blog_name 추가 - if ("tstory_blog".equals(postingPlatform) && blogName != null && !blogName.isBlank()) { - jsonBuilder.append(", \"blog_name\": \"").append(blogName).append("\""); - } - - jsonBuilder.append("}"); - } - - jsonBuilder.append("}"); - return jsonBuilder.toString(); + @Null private BigInteger id; + + @NotBlank(message = "워크플로우 이름은 필수입니다") + @Size(max = 100, message = "워크플로우 이름은 100자를 초과할 수 없습니다") + @Pattern( + regexp = "^[가-힣a-zA-Z0-9\\s_-]+$", + message = "워크플로우 이름은 한글, 영문, 숫자, 공백, 언더스코어, 하이픈만 사용 가능합니다") + private String name; + + @Size(max = 500, message = "설명은 500자를 초과할 수 없습니다") + private String description; + + @Pattern(regexp = "^(naver|naver_store)?$", message = "검색 플랫폼은 'naver' 또는 'naver_store'만 가능합니다") + @JsonProperty("search_platform") + private String searchPlatform; + + @Pattern( + regexp = "^(naver_blog|tstory_blog|blogger)?$", + message = "포스팅 플랫폼은 'naver_blog', 'tstory_blog', 'blogger' 중 하나여야 합니다") + @JsonProperty("posting_platform") + private String postingPlatform; + + @Size(max = 100, message = "포스팅 계정 ID는 100자를 초과할 수 없습니다") + @JsonProperty("posting_account_id") + private String postingAccountId; + + @Size(max = 200, message = "포스팅 계정 비밀번호는 200자를 초과할 수 없습니다") + @JsonProperty("posting_account_password") + private String postingAccountPassword; + + @Size(max = 100, message = "블로그 이름은 100자를 초과할 수 없습니다") + @JsonProperty("blog_name") + private String blogName; + + @Builder.Default + @JsonProperty("is_enabled") + private Boolean isEnabled = true; + + // JSON 변환용 필드 (MyBatis에서 사용) + private String defaultConfigJson; + + public String genertateDefaultConfigJson() { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("{"); + + // 크롤링 플랫폼 설정 (키: "1") + if (searchPlatform != null && !searchPlatform.isBlank()) { + jsonBuilder.append("\"1\": {\"tag\": \"").append(searchPlatform).append("\"}"); } - // 포스팅 설정 완성도 체크 (상태 확인 유틸) - public boolean hasPostingConfig() { - return postingPlatform != null && !postingPlatform.isBlank() && - postingAccountId != null && !postingAccountId.isBlank() && - postingAccountPassword != null && !postingAccountPassword.isBlank(); + // 포스팅 설정 (키: "8") + if (hasPostingConfig()) { + if (jsonBuilder.length() > 1) { + jsonBuilder.append(", "); + } + jsonBuilder + .append("\"8\": {") + .append("\"tag\": \"") + .append(postingPlatform) + .append("\", ") + .append("\"blog_id\": \"") + .append(postingAccountId) + .append("\", ") + .append("\"blog_pw\": \"") + .append(postingAccountPassword) + .append("\""); + + // tstory_blog인 경우 blog_name 추가 + if ("tstory_blog".equals(postingPlatform) && blogName != null && !blogName.isBlank()) { + jsonBuilder.append(", \"blog_name\": \"").append(blogName).append("\""); + } + + jsonBuilder.append("}"); } + + jsonBuilder.append("}"); + return jsonBuilder.toString(); + } + + // 포스팅 설정 완성도 체크 (상태 확인 유틸) + public boolean hasPostingConfig() { + return postingPlatform != null + && !postingPlatform.isBlank() + && postingAccountId != null + && !postingAccountId.isBlank() + && postingAccountPassword != null + && !postingAccountPassword.isBlank(); + } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java index 70149f65..417dfd1d 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java @@ -6,7 +6,6 @@ import site.icebang.common.dto.PageParams; 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; public interface WorkflowMapper { @@ -17,9 +16,11 @@ public interface WorkflowMapper { int insertWorkflow(Map params); // insert workflow // Job 생성 관련 메서드 - void insertJobs(Map params); // 여러 Job을 동적으로 생성 - void insertWorkflowJobs(Map params); // Workflow-Job 연결 - void insertJobTasks(Map params); // Job-Task 연결 + void insertJobs(Map params); // 여러 Job을 동적으로 생성 + + void insertWorkflowJobs(Map params); // Workflow-Job 연결 + + void insertJobTasks(Map params); // Job-Task 연결 boolean existsByName(String name); diff --git a/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java b/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java index c5f872d1..115bec64 100644 --- a/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java +++ b/apps/user-service/src/test/java/site/icebang/e2e/scenario/WorkflowCreateFlowE2eTest.java @@ -1,21 +1,21 @@ package site.icebang.e2e.scenario; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.*; import org.springframework.test.context.jdbc.Sql; + import site.icebang.e2e.setup.annotation.E2eTest; import site.icebang.e2e.setup.support.E2eTestSupport; -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - @Sql( - value = {"classpath:sql/00-truncate.sql", "classpath:sql/01-insert-internal-users.sql"}, - executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) + value = {"classpath:sql/00-truncate.sql", "classpath:sql/01-insert-internal-users.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) @DisplayName("워크플로우 생성 플로우 E2E 테스트") @E2eTest class WorkflowCreateFlowE2eTest extends E2eTestSupport { @@ -39,7 +39,7 @@ void completeWorkflowCreateFlow() throws Exception { HttpEntity> loginEntity = new HttpEntity<>(loginRequest, loginHeaders); ResponseEntity loginResponse = - restTemplate.postForEntity(getV0ApiUrl("/auth/login"), loginEntity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/auth/login"), loginEntity, Map.class); assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat((Boolean) loginResponse.getBody().get("success")).isTrue(); @@ -63,10 +63,10 @@ void completeWorkflowCreateFlow() throws Exception { workflowHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity> naverEntity = - new HttpEntity<>(naverBlogWorkflow, workflowHeaders); + new HttpEntity<>(naverBlogWorkflow, workflowHeaders); ResponseEntity naverResponse = - restTemplate.postForEntity(getV0ApiUrl("/workflows"), naverEntity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/workflows"), naverEntity, Map.class); assertThat(naverResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat((Boolean) naverResponse.getBody().get("success")).isTrue(); @@ -83,14 +83,14 @@ void completeWorkflowCreateFlow() throws Exception { tstoryWorkflow.put("posting_platform", "tstory_blog"); tstoryWorkflow.put("posting_account_id", "test_tstory"); tstoryWorkflow.put("posting_account_password", "tstory_password123"); - tstoryWorkflow.put("blog_name", "my-tech-blog"); // 티스토리는 블로그명 필수 + tstoryWorkflow.put("blog_name", "my-tech-blog"); // 티스토리는 블로그명 필수 tstoryWorkflow.put("is_enabled", true); HttpEntity> tstoryEntity = - new HttpEntity<>(tstoryWorkflow, workflowHeaders); + new HttpEntity<>(tstoryWorkflow, workflowHeaders); ResponseEntity tstoryResponse = - restTemplate.postForEntity(getV0ApiUrl("/workflows"), tstoryEntity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/workflows"), tstoryEntity, Map.class); assertThat(tstoryResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat((Boolean) tstoryResponse.getBody().get("success")).isTrue(); @@ -108,10 +108,10 @@ void completeWorkflowCreateFlow() throws Exception { // posting_platform, posting_account_id, posting_account_password는 선택사항 HttpEntity> searchOnlyEntity = - new HttpEntity<>(searchOnlyWorkflow, workflowHeaders); + new HttpEntity<>(searchOnlyWorkflow, workflowHeaders); ResponseEntity searchOnlyResponse = - restTemplate.postForEntity(getV0ApiUrl("/workflows"), searchOnlyEntity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/workflows"), searchOnlyEntity, Map.class); assertThat(searchOnlyResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat((Boolean) searchOnlyResponse.getBody().get("success")).isTrue(); @@ -141,7 +141,7 @@ void createWorkflow_withDuplicateName_shouldFail() { HttpEntity> firstEntity = new HttpEntity<>(firstWorkflow, headers); ResponseEntity firstResponse = - restTemplate.postForEntity(getV0ApiUrl("/workflows"), firstEntity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/workflows"), firstEntity, Map.class); assertThat(firstResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); logSuccess("첫 번째 워크플로우 생성 성공"); @@ -150,19 +150,18 @@ void createWorkflow_withDuplicateName_shouldFail() { // 동일한 이름으로 다시 생성 시도 Map duplicateWorkflow = new HashMap<>(); - duplicateWorkflow.put("name", "중복테스트워크플로우"); // 동일한 이름 + duplicateWorkflow.put("name", "중복테스트워크플로우"); // 동일한 이름 duplicateWorkflow.put("search_platform", "naver_store"); duplicateWorkflow.put("is_enabled", true); - HttpEntity> duplicateEntity = - new HttpEntity<>(duplicateWorkflow, headers); + HttpEntity> duplicateEntity = new HttpEntity<>(duplicateWorkflow, headers); ResponseEntity duplicateResponse = - restTemplate.postForEntity(getV0ApiUrl("/workflows"), duplicateEntity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/workflows"), duplicateEntity, Map.class); // 중복 이름 처리 확인 (400 또는 409 예상) assertThat(duplicateResponse.getStatusCode()) - .isIn(HttpStatus.BAD_REQUEST, HttpStatus.CONFLICT, HttpStatus.INTERNAL_SERVER_ERROR); + .isIn(HttpStatus.BAD_REQUEST, HttpStatus.CONFLICT, HttpStatus.INTERNAL_SERVER_ERROR); logSuccess("중복 이름 워크플로우 생성 차단 확인"); } @@ -186,10 +185,10 @@ void createWorkflow_withMissingRequiredFields_shouldFail() { HttpEntity> entity = new HttpEntity<>(noNameWorkflow, headers); ResponseEntity response = - restTemplate.postForEntity(getV0ApiUrl("/workflows"), entity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/workflows"), entity, Map.class); assertThat(response.getStatusCode()) - .isIn(HttpStatus.BAD_REQUEST, HttpStatus.UNPROCESSABLE_ENTITY); + .isIn(HttpStatus.BAD_REQUEST, HttpStatus.UNPROCESSABLE_ENTITY); logSuccess("필수 필드 검증 확인"); } @@ -208,7 +207,7 @@ private void performUserLogin() { HttpEntity> entity = new HttpEntity<>(loginRequest, headers); ResponseEntity response = - restTemplate.postForEntity(getV0ApiUrl("/auth/login"), entity, Map.class); + restTemplate.postForEntity(getV0ApiUrl("/auth/login"), entity, Map.class); if (response.getStatusCode() != HttpStatus.OK) { logError("사용자 로그인 실패: " + response.getStatusCode()); From 5f4a940865c788a0ad69659628782c22ee621330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=8B=88=EC=83=88=EC=BB=B4=5Cbwnfo?= Date: Thu, 25 Sep 2025 21:51:46 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20job,task=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0=20=EC=83=9D=EC=84=B1=EC=9C=BC=EB=A1=9C=EB=A7=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/service/WorkflowService.java | 112 +++--------------- 1 file changed, 14 insertions(+), 98 deletions(-) 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 6917604f..06a9ee5c 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,16 +1,15 @@ package site.icebang.domain.workflow.service; import java.math.BigInteger; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; @@ -92,9 +91,7 @@ public WorkflowDetailCardDto getWorkflowDetail(BigInteger workflowId) { return workflow; } - /** - * 워크플로우 생성 - */ + /** 워크플로우 생성 */ @Transactional public void createWorkflow(WorkflowCreateDto dto, BigInteger createdBy) { // 1. 기본 검증 @@ -108,110 +105,31 @@ public void createWorkflow(WorkflowCreateDto dto, BigInteger createdBy) { throw new IllegalArgumentException("이미 존재하는 워크플로우 이름입니다 : " + dto.getName()); } + // 4. 워크플로우 생성 try { - // 4. JSON 설정 생성 + // JSON 설정 생성 String defaultConfigJson = dto.genertateDefaultConfigJson(); dto.setDefaultConfigJson(defaultConfigJson); - // 5. Workflow 삽입 - Map workflowParams = new HashMap<>(); - workflowParams.put("dto", dto); - workflowParams.put("createdBy", createdBy); + // DB 삽입 파라미터 구성 + Map params = new HashMap<>(); + params.put("dto", dto); + params.put("createdBy", createdBy); - int result = workflowMapper.insertWorkflow(workflowParams); + int result = workflowMapper.insertWorkflow(params); if (result != 1) { throw new RuntimeException("워크플로우 생성에 실패했습니다"); } - BigInteger workflowId = dto.getId(); - log.info("✅ Workflow 생성 완료 - ID: {}, Name: {}", workflowId, dto.getName()); - - // 6. ⭐ 템플릿 기반 Job 생성 - List jobTemplates = templateProvider.getTemplateByPlatform( - dto.getPostingPlatform() - ); - - // 7. ⭐ Job 데이터 준비 (Batch Insert) - List> jobs = new ArrayList<>(); - for (WorkflowJobTemplate template : jobTemplates) { - Map job = new HashMap<>(); - job.put("name", template.getName()); - job.put("description", template.getDescription()); - jobs.add(job); - } - - // 8. ⭐ Job Batch Insert - Map jobParams = new HashMap<>(); - jobParams.put("jobs", jobs); - jobParams.put("createdBy", createdBy); - workflowMapper.insertJobs(jobParams); - - log.info("✅ Job {} 개 Batch Insert 완료", jobs.size()); - - // 9. ⭐ 생성된 Job ID 조회 (안전한 방법) - List createdJobIds = workflowMapper.selectLastInsertedJobIds(createdBy); - - if (createdJobIds.size() != jobTemplates.size()) { - throw new RuntimeException( - String.format("Job 생성 개수 불일치: 예상=%d, 실제=%d", - jobTemplates.size(), createdJobIds.size()) - ); - } - - log.info("✅ 생성된 Job IDs: {}", createdJobIds); - - // 10. ⭐ Workflow-Job 연결 데이터 준비 - List> workflowJobs = new ArrayList<>(); - for (int i = 0; i < jobTemplates.size(); i++) { - Map wj = new HashMap<>(); - wj.put("workflowId", workflowId); - wj.put("jobId", createdJobIds.get(i)); - wj.put("executionOrder", jobTemplates.get(i).getExecutionOrder()); - workflowJobs.add(wj); - } - - // 11. ⭐ Workflow-Job 연결 - Map wjParams = new HashMap<>(); - wjParams.put("workflowJobs", workflowJobs); - workflowMapper.insertWorkflowJobs(wjParams); - - log.info("✅ Workflow-Job 연결 완료 - {} 개", workflowJobs.size()); - - // 12. ⭐ Job-Task 연결 데이터 준비 - List> jobTasks = new ArrayList<>(); - for (int i = 0; i < jobTemplates.size(); i++) { - Long jobId = createdJobIds.get(i); - WorkflowJobTemplate template = jobTemplates.get(i); - - List taskIds = template.getTaskIds(); - for (int j = 0; j < taskIds.size(); j++) { - Map jt = new HashMap<>(); - jt.put("jobId", jobId); - jt.put("taskId", taskIds.get(j)); - jt.put("executionOrder", j + 1); // 1부터 시작 - jobTasks.add(jt); - } - } - - // 13. ⭐ Job-Task 연결 - Map jtParams = new HashMap<>(); - jtParams.put("jobTasks", jobTasks); - workflowMapper.insertJobTasks(jtParams); - - log.info("✅ Job-Task 연결 완료 - {} 개", jobTasks.size()); - - log.info("🎉 워크플로우 전체 생성 완료: {} (ID: {}, Jobs: {}, Tasks: {}, 생성자: {})", - dto.getName(), workflowId, createdJobIds.size(), jobTasks.size(), createdBy); + log.info("워크플로우 생성 완료: {} (생성자: {})", dto.getName(), createdBy); } catch (Exception e) { - log.error("❌ 워크플로우 생성 실패: {}", dto.getName(), e); - throw new RuntimeException("워크플로우 생성 중 오류가 발생했습니다: " + e.getMessage(), e); + log.error("워크플로우 생성 실패: {}", dto.getName(), e); + throw new RuntimeException("워크플로우 생성 중 오류가 발생했습니다", e); } } - /** - * 기본 입력값 검증 - */ + /** 기본 입력값 검증 */ private void validateBasicInput(WorkflowCreateDto dto, BigInteger createdBy) { if (dto == null) { throw new IllegalArgumentException("워크플로우 정보가 필요합니다"); @@ -221,9 +139,7 @@ private void validateBasicInput(WorkflowCreateDto dto, BigInteger createdBy) { } } - /** - * 비즈니스 규칙 검증 - */ + /** 비즈니스 규칙 검증 */ private void validateBusinessRules(WorkflowCreateDto dto) { // 포스팅 플랫폼 선택 시 계정 정보 필수 검증 String postingPlatform = dto.getPostingPlatform();