Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

import java.math.BigInteger;

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.model.AuthCredential;
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;
Expand All @@ -29,6 +34,23 @@ public ApiResponse<PageResult<WorkflowCardDto>> getWorkflowList(
return ApiResponse.success(result);
}

@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<Void> createWorkflow(
@Valid @RequestBody WorkflowCreateDto workflowCreateDto,
@AuthenticationPrincipal AuthCredential authCredential) {
// 인증 체크
if (authCredential == null) {
throw new IllegalArgumentException("로그인이 필요합니다");
}
Comment on lines +43 to +45
Copy link
Collaborator

@can019 can019 Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨트롤러에서 AuthCredential을 @Authenticationprincipal 어노테이션을 통해 주입 받는 다는 것은 AuthCredential 생성에 실패 (잘못된 인증 정보 or 인증 정보 없는 경우)등을 security 단에서 봐주기 때문에 필요없는 코드입니다.

여담으로 인증관련은 Security 관련 exception을 내주시면 됩니다. IlligalArugmentException은 잘못된 값이 메서드에 전달되었을 때 발생하는 이슈 입니다.


// AuthCredential에서 userId 추출
BigInteger userId = authCredential.getId();

workflowService.createWorkflow(workflowCreateDto, userId);
return ApiResponse.success(null);
}

@PostMapping("/{workflowId}/run")
public ResponseEntity<Void> runWorkflow(@PathVariable Long workflowId) {
// HTTP 요청/응답 스레드를 블로킹하지 않도록 비동기 실행
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +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;

/**
* 워크플로우 생성 요청 DTO
*
* <p>프론트엔드에서 워크플로우 생성 시 필요한 모든 정보를 담는 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();
}

// 포스팅 설정 완성도 체크 (상태 확인 유틸)
public boolean hasPostingConfig() {
return postingPlatform != null
&& !postingPlatform.isBlank()
&& postingAccountId != null
&& !postingAccountId.isBlank()
&& postingAccountPassword != null
&& !postingAccountPassword.isBlank();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ public interface WorkflowMapper {

int selectWorkflowCount(PageParams pageParams);

int insertWorkflow(Map<String, Object> params); // insert workflow

// Job 생성 관련 메서드
void insertJobs(Map<String, Object> params); // 여러 Job을 동적으로 생성

void insertWorkflowJobs(Map<String, Object> params); // Workflow-Job 연결

void insertJobTasks(Map<String, Object> params); // Job-Task 연결

boolean existsByName(String name);

WorkflowCardDto selectWorkflowById(BigInteger id);

WorkflowDetailCardDto selectWorkflowDetailById(BigInteger workflowId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package site.icebang.domain.workflow.service;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

Expand All @@ -32,6 +35,7 @@
* @author [email protected]
* @since v0.1.0
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WorkflowService implements PageableService<WorkflowCardDto> {
Expand Down Expand Up @@ -86,4 +90,72 @@ public WorkflowDetailCardDto getWorkflowDetail(BigInteger workflowId) {

return workflow;
}

/** 워크플로우 생성 */
@Transactional
public void createWorkflow(WorkflowCreateDto dto, BigInteger createdBy) {
// 1. 기본 검증
validateBasicInput(dto, createdBy);

// 2. 비즈니스 검증
validateBusinessRules(dto);

// 3. 중복체크
if (workflowMapper.existsByName(dto.getName())) {
throw new IllegalArgumentException("이미 존재하는 워크플로우 이름입니다 : " + dto.getName());
}

Comment on lines +103 to +107
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DuplicateDataException 사용해주세요

// 4. 워크플로우 생성
try {
// JSON 설정 생성
String defaultConfigJson = dto.genertateDefaultConfigJson();
dto.setDefaultConfigJson(defaultConfigJson);

// DB 삽입 파라미터 구성
Map<String, Object> 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, BigInteger 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("티스토리 블로그 선택 시 블로그 이름은 필수입니다");
}
}
}
}
Comment on lines +143 to +160
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO에서 valid를 통해 검증하고 있는데 중복 코드 같아요

}
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,61 @@
wj.execution_order, j.id, j.name, j.description, j.is_enabled
ORDER BY wj.execution_order
</select>

<insert id="insertWorkflow" parameterType="map" useGeneratedKeys="true" keyProperty="id">
INSERT INTO workflow (
name,
description,
is_enabled,
created_by,
created_at,
default_config
) VALUES (
#{dto.name},
#{dto.description},
#{dto.isEnabled},
#{createdBy},
NOW(),
#{dto.defaultConfigJson}
)
</insert>

<!-- 워크플로우 이름 중복 체크 -->
<select id="existsByName" parameterType="string" resultType="boolean">
SELECT COUNT(*) > 0
FROM workflow
WHERE name = #{name}
</select>

<!-- Job 생성 -->
<insert id="insertDefaultJobs" parameterType="map" useGeneratedKeys="true" keyProperty="jobIds">
<selectKey keyProperty="jobIds" resultType="java.util.List" order="AFTER">
SELECT LAST_INSERT_ID() as id
</selectKey>
INSERT INTO job (name, description, created_by, created_at) VALUES
('상품 분석', '키워드 검색, 상품 크롤링 및 유사도 분석 작업', #{createdBy}, NOW()),
('블로그 콘텐츠 생성', '분석 데이터를 기반으로 RAG 콘텐츠 생성 및 발행 작업', #{createdBy}, NOW())
</insert>

<!-- Workflow-Job 연결 -->
<insert id="insertWorkflowJob" parameterType="map">
INSERT INTO workflow_job (workflow_id, job_id, execution_order) VALUES
(#{workflowId}, #{job1Id}, 1),
(#{workflowId}, #{job2Id}, 2)
</insert>

<!-- Job-Task 연결 -->
<insert id="insertJobTask" parameterType="map">
INSERT INTO job_task (job_id, task_id, execution_order) VALUES
<!-- Job 1: 상품 분석 (Task 1~6) -->
(#{job1Id}, 1, 1),
(#{job1Id}, 2, 2),
(#{job1Id}, 3, 3),
(#{job1Id}, 4, 4),
(#{job1Id}, 5, 5),
(#{job1Id}, 6, 6),
<!-- Job 2: 블로그 콘텐츠 생성 (Task 7~8) -->
(#{job2Id}, 7, 1),
(#{job2Id}, 8, 2)
</insert>
</mapper>
Loading
Loading