From 8d08099980199dd9efe359cf9f7364ccd42d3d6a Mon Sep 17 00:00:00 2001 From: thkim7 Date: Mon, 22 Sep 2025 18:54:20 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20s3=20upload=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=201.=20bodybuilder=20=EC=B6=94=EA=B0=80=202.=20=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=EB=A7=88=20=EC=9E=84=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B3=B5=EC=88=98=EC=9D=98=20product=20->?= =?UTF-8?q?=20=EB=8B=A8=EC=9D=BC=20product=203.=20s3=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=ED=9B=84=20rag=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EA=B0=88=20=EC=9E=90=EB=A6=AC=EC=97=90=20?= =?UTF-8?q?=EC=9E=84=EC=9D=98=EB=A1=9C=201=EA=B0=9C=20=EB=BD=91=EB=8A=94?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=204.=20workflow?= =?UTF-8?q?=EC=97=90=20s3=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B4=80=EB=A0=A8=20=EC=BF=BC=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=205.=20=EC=A0=84=EC=B2=B4=EC=A0=81=EC=9D=B8=20bodybui?= =?UTF-8?q?lder=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/model/schemas.py | 6 ++ .../app/service/blog/blog_publish_service.py | 26 +++--- .../app/service/s3_upload_service.py | 92 ++++++++++++++++++- .../fastapi/body/BlogPublishBodyBuilder.java | 6 +- .../fastapi/body/BlogRagBodyBuilder.java | 62 ++++++------- .../fastapi/body/ProductCrawlBodyBuilder.java | 59 +++++++----- .../fastapi/body/S3UploadBodyBuilder.java | 49 ++++++++++ .../main/resources/sql/03-insert-workflow.sql | 34 ++++--- 8 files changed, 251 insertions(+), 83 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java diff --git a/apps/pre-processing-service/app/model/schemas.py b/apps/pre-processing-service/app/model/schemas.py index 549ba7b5..301e3f9b 100644 --- a/apps/pre-processing-service/app/model/schemas.py +++ b/apps/pre-processing-service/app/model/schemas.py @@ -227,6 +227,12 @@ class S3UploadData(BaseModel): uploaded_at: str = Field( ..., title="업로드 완료 시간", description="S3 업로드 완료 시간" ) + # 🆕 임시: 콘텐츠 생성용 단일 상품만 추가 (나중에 삭제 예정) + selected_product_for_content: Optional[Dict] = Field( + None, + title="콘텐츠 생성용 선택 상품", + description="임시: 블로그 콘텐츠 생성을 위해 선택된 단일 상품 정보" + ) # 최종 응답 모델 diff --git a/apps/pre-processing-service/app/service/blog/blog_publish_service.py b/apps/pre-processing-service/app/service/blog/blog_publish_service.py index 0848f123..c88e8cc5 100644 --- a/apps/pre-processing-service/app/service/blog/blog_publish_service.py +++ b/apps/pre-processing-service/app/service/blog/blog_publish_service.py @@ -1,4 +1,5 @@ -from typing import Dict, Optional +# app/service/blog/blog_publish_service.py +from typing import Dict from app.errors.CustomException import CustomException from app.model.schemas import RequestBlogPublish from app.service.blog.blog_service_factory import BlogServiceFactory @@ -10,20 +11,15 @@ class BlogPublishService: def __init__(self): self.factory = BlogServiceFactory() - def publish_content( - self, - request: RequestBlogPublish, - ) -> Dict: + def publish_content(self, request: RequestBlogPublish) -> Dict: """ 생성된 블로그 콘텐츠를 배포합니다. Args: - request: 블로그 발행 요청 데이터 - blog_id: 블로그 아이디 - blog_password: 블로그 비밀번호 + request: RequestBlogPublish 객체 """ try: - # 팩토리를 통해 적절한 서비스 생성 + # 블로그 서비스 생성 (네이버, 티스토리, 블로거 등) blog_service = self.factory.create_service( request.tag, blog_id=request.blog_id, @@ -31,7 +27,7 @@ def publish_content( blog_name=request.blog_name, ) - # 공통 인터페이스로 포스팅 실행 + # 콘텐츠 포스팅 response_data = blog_service.post_content( title=request.post_title, content=request.post_content, @@ -40,16 +36,20 @@ def publish_content( if not response_data: raise CustomException( - 500, f"{request.tag} 블로그 포스팅에 실패했습니다.", "POSTING_FAIL" + detail=f"{request.tag} 블로그 포스팅에 실패했습니다.", + status_code=500, + code="POSTING_FAIL" ) return response_data except CustomException: - # 이미 처리된 예외는 그대로 전달 + # 이미 CustomException이면 그대로 전달 raise except Exception as e: # 예상치 못한 예외 처리 raise CustomException( - 500, f"블로그 포스팅 중 오류가 발생했습니다: {str(e)}", "ERROR" + detail=f"블로그 포스팅 중 오류가 발생했습니다: {str(e)}", + status_code=500, + code="ERROR" ) diff --git a/apps/pre-processing-service/app/service/s3_upload_service.py b/apps/pre-processing-service/app/service/s3_upload_service.py index 1c024a63..fa3674b9 100644 --- a/apps/pre-processing-service/app/service/s3_upload_service.py +++ b/apps/pre-processing-service/app/service/s3_upload_service.py @@ -22,7 +22,7 @@ async def upload_crawled_products_to_s3(self, request: RequestS3Upload) -> dict: keyword = request.keyword # 키워드 추가 crawled_products = request.crawled_products base_folder = ( - request.base_folder or "product" + request.base_folder or "product" ) # 🔸 기본값 변경: product-images → product logger.info( @@ -102,11 +102,16 @@ async def upload_crawled_products_to_s3(self, request: RequestS3Upload) -> dict: if product_index < len(crawled_products): await asyncio.sleep(1) + # 🆕 임시: 콘텐츠 생성용 단일 상품 선택 로직 + selected_product_for_content = self._select_single_product_for_content( + crawled_products, upload_results + ) + logger.success( f"S3 업로드 서비스 완료: 총 성공 이미지 {total_success_images}개, 총 실패 이미지 {total_fail_images}개" ) - # 간소화된 응답 데이터 구성 + # 기존 응답 데이터 구성 data = { "upload_results": upload_results, "summary": { @@ -115,6 +120,8 @@ async def upload_crawled_products_to_s3(self, request: RequestS3Upload) -> dict: "total_fail_images": total_fail_images, }, "uploaded_at": time.strftime("%Y-%m-%d %H:%M:%S"), + # 🆕 임시: 콘텐츠 생성용 단일 상품만 추가 (나중에 삭제 예정) + "selected_product_for_content": selected_product_for_content, } message = f"S3 업로드 완료: {total_success_images}개 이미지 업로드 성공, 상품 데이터 JSON 파일 포함" @@ -123,3 +130,84 @@ async def upload_crawled_products_to_s3(self, request: RequestS3Upload) -> dict: except Exception as e: logger.error(f"S3 업로드 서비스 전체 오류: {e}") raise InvalidItemDataException() + + def _select_single_product_for_content( + self, crawled_products: List[Dict], upload_results: List[Dict] + ) -> Dict: + """ + 🆕 임시: 콘텐츠 생성을 위한 단일 상품 선택 로직 + 우선순위: 1) S3 업로드 성공한 상품 중 이미지 개수가 많은 것 + 2) 없다면 크롤링 성공한 첫 번째 상품 + """ + try: + # 1순위: S3 업로드 성공하고 이미지가 있는 상품들 + successful_uploads = [ + result for result in upload_results + if result.get("status") == "completed" and result.get("success_count", 0) > 0 + ] + + if successful_uploads: + # 이미지 개수가 가장 많은 상품 선택 + best_upload = max(successful_uploads, key=lambda x: x.get("success_count", 0)) + selected_index = best_upload["product_index"] + + # 원본 크롤링 데이터에서 해당 상품 찾기 + for product_info in crawled_products: + if product_info.get("index") == selected_index: + logger.info( + f"콘텐츠 생성용 상품 선택: index={selected_index}, " + f"title='{product_info.get('product_detail', {}).get('title', 'Unknown')[:30]}', " + f"images={best_upload.get('success_count', 0)}개" + ) + return { + "selection_reason": "s3_upload_success_with_most_images", + "product_info": product_info, + "s3_upload_info": best_upload, + } + + # 2순위: 크롤링 성공한 첫 번째 상품 (S3 업로드 실패해도) + for product_info in crawled_products: + if (product_info.get("status") == "success" and + product_info.get("product_detail")): + + # 해당 상품의 S3 업로드 정보 찾기 + upload_info = None + for result in upload_results: + if result.get("product_index") == product_info.get("index"): + upload_info = result + break + + logger.info( + f"콘텐츠 생성용 상품 선택 (fallback): index={product_info.get('index')}, " + f"title='{product_info.get('product_detail', {}).get('title', 'Unknown')[:30]}'" + ) + return { + "selection_reason": "first_crawl_success", + "product_info": product_info, + "s3_upload_info": upload_info, + } + + # 3순위: 아무거나 (모든 상품이 실패한 경우) + if crawled_products: + logger.warning("모든 상품이 크롤링 실패 - 첫 번째 상품으로 fallback") + return { + "selection_reason": "fallback_first_product", + "product_info": crawled_products[0], + "s3_upload_info": upload_results[0] if upload_results else None, + } + + logger.error("선택할 상품이 없습니다") + return { + "selection_reason": "no_products_available", + "product_info": None, + "s3_upload_info": None, + } + + except Exception as e: + logger.error(f"단일 상품 선택 오류: {e}") + return { + "selection_reason": "selection_error", + "product_info": crawled_products[0] if crawled_products else None, + "s3_upload_info": upload_results[0] if upload_results else None, + "error": str(e), + } \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java index 94613f64..4b62cd60 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java @@ -50,9 +50,9 @@ public ObjectNode build(Task task, Map workflowContext) { .ifPresent(tagsNode -> body.set("post_tags", tagsNode)); }); - body.put("tag", "tistory"); - body.put("blog_id", "fair_05@nate.com"); - body.put("blog_pw", "kdyn2641*"); + body.put("tag", "NAVER_BLOG"); + body.put("blog_id", "wtecho331"); + body.put("blog_pw", "wt505033@#"); return body; } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java index ad22a58d..66551c70 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java @@ -17,34 +17,34 @@ @RequiredArgsConstructor public class BlogRagBodyBuilder implements TaskBodyBuilder { - private final ObjectMapper objectMapper; - private static final String TASK_NAME = "블로그 RAG 생성 태스크"; - private static final String KEYWORD_SOURCE_TASK = "키워드 검색 태스크"; - private static final String CRAWL_SOURCE_TASK = "상품 정보 크롤링 태스크"; - - @Override - public boolean supports(String taskName) { - return TASK_NAME.equals(taskName); - } - - @Override - public ObjectNode build(Task task, Map workflowContext) { - ObjectNode body = objectMapper.createObjectNode(); - - // 키워드 정보 가져오기 - Optional.ofNullable(workflowContext.get(KEYWORD_SOURCE_TASK)) - .map(node -> node.path("data").path("keyword")) - .ifPresent(keywordNode -> body.set("keyword", keywordNode)); - - // 크롤링된 상품 정보 가져오기 - Optional.ofNullable(workflowContext.get(CRAWL_SOURCE_TASK)) - .map(node -> node.path("data").path("product_detail")) - .ifPresent(productNode -> body.set("product_info", productNode)); - - // 기본 콘텐츠 설정 - body.put("content_type", "review_blog"); - body.put("target_length", 1000); - - return body; - } -} + private final ObjectMapper objectMapper; + private static final String TASK_NAME = "블로그 RAG 생성 태스크"; + private static final String KEYWORD_SOURCE_TASK = "키워드 검색 태스크"; + private static final String S3_UPLOAD_SOURCE_TASK = "S3 업로드 태스크"; // 변경: 크롤링 → S3 업로드 + + @Override + public boolean supports(String taskName) { + return TASK_NAME.equals(taskName); + } + + @Override + public ObjectNode build(Task task, Map workflowContext) { + ObjectNode body = objectMapper.createObjectNode(); + + // 키워드 정보 가져오기 + Optional.ofNullable(workflowContext.get(KEYWORD_SOURCE_TASK)) + .map(node -> node.path("data").path("keyword")) + .ifPresent(keywordNode -> body.set("keyword", keywordNode)); + + // S3 업로드에서 선택된 상품 정보 가져오기 (변경된 부분) + Optional.ofNullable(workflowContext.get(S3_UPLOAD_SOURCE_TASK)) + .map(node -> node.path("data").path("selected_product_for_content").path("product_info").path("product_detail")) + .ifPresent(productNode -> body.set("product_info", productNode)); + + // 기본 콘텐츠 설정 + body.put("content_type", "review_blog"); + body.put("target_length", 1000); + + return body; + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java index 138e95d0..db6f5444 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; +import com.fasterxml.jackson.databind.node.ArrayNode; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.JsonNode; @@ -17,25 +18,39 @@ @RequiredArgsConstructor public class ProductCrawlBodyBuilder implements TaskBodyBuilder { - private final ObjectMapper objectMapper; - private static final String TASK_NAME = "상품 정보 크롤링 태스크"; - private static final String SIMILARITY_SOURCE_TASK = "상품 유사도 분석 태스크"; - - @Override - public boolean supports(String taskName) { - return TASK_NAME.equals(taskName); - } - - @Override - public ObjectNode build(Task task, Map workflowContext) { - ObjectNode body = objectMapper.createObjectNode(); - - // 유사도 분석에서 선택된 상품의 URL 가져오기 - Optional.ofNullable(workflowContext.get(SIMILARITY_SOURCE_TASK)) - .map(node -> node.path("data").path("selected_product").path("url")) - .filter(urlNode -> !urlNode.isMissingNode() && !urlNode.asText().isEmpty()) - .ifPresent(urlNode -> body.set("product_url", urlNode)); - - return body; - } -} + private final ObjectMapper objectMapper; + private static final String TASK_NAME = "상품 정보 크롤링 태스크"; + private static final String SIMILARITY_SOURCE_TASK = "상품 유사도 분석 태스크"; + + @Override + public boolean supports(String taskName) { + return TASK_NAME.equals(taskName); + } + + @Override + public ObjectNode build(Task task, Map workflowContext) { + ObjectNode body = objectMapper.createObjectNode(); + + // ArrayNode 준비 (product_urls 배열로 변경) + ArrayNode productUrls = objectMapper.createArrayNode(); + + // 유사도 분석에서 선택된 상품들의 URL 가져오기 (복수로 변경) + Optional.ofNullable(workflowContext.get(SIMILARITY_SOURCE_TASK)) + .ifPresent(node -> { + JsonNode topProducts = node.path("data").path("top_products"); + if (topProducts.isArray()) { + // top_products 배열에서 각 상품의 URL 추출 + topProducts.forEach(product -> { + JsonNode urlNode = product.path("url"); + if (!urlNode.isMissingNode() && !urlNode.asText().isEmpty()) { + productUrls.add(urlNode.asText()); + } + }); + } + }); + + body.set("product_urls", productUrls); + + return body; + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java new file mode 100644 index 00000000..1af32aab --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java @@ -0,0 +1,49 @@ +package site.icebang.domain.workflow.runner.fastapi.body; + +import java.util.Map; +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import lombok.RequiredArgsConstructor; + +import site.icebang.domain.workflow.model.Task; + +@Component +@RequiredArgsConstructor +public class S3UploadBodyBuilder implements TaskBodyBuilder { + + private final ObjectMapper objectMapper; + private static final String TASK_NAME = "S3 업로드 태스크"; + private static final String KEYWORD_SOURCE_TASK = "키워드 검색 태스크"; + private static final String CRAWL_SOURCE_TASK = "상품 정보 크롤링 태스크"; + + @Override + public boolean supports(String taskName) { + return TASK_NAME.equals(taskName); + } + + @Override + public ObjectNode build(Task task, Map workflowContext) { + ObjectNode body = objectMapper.createObjectNode(); + + // 키워드 정보 가져오기 + Optional.ofNullable(workflowContext.get(KEYWORD_SOURCE_TASK)) + .map(node -> node.path("data").path("keyword")) + .ifPresent(keywordNode -> body.set("keyword", keywordNode)); + + // 크롤링된 상품 데이터 가져오기 + Optional.ofNullable(workflowContext.get(CRAWL_SOURCE_TASK)) + .map(node -> node.path("data").path("crawled_products")) + .ifPresent(crawledProductsNode -> body.set("crawled_products", crawledProductsNode)); + + // 기본 폴더 설정 + body.put("base_folder", "product"); + + return body; + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/sql/03-insert-workflow.sql b/apps/user-service/src/main/resources/sql/03-insert-workflow.sql index 9b6db4c0..4eb45b6c 100644 --- a/apps/user-service/src/main/resources/sql/03-insert-workflow.sql +++ b/apps/user-service/src/main/resources/sql/03-insert-workflow.sql @@ -51,20 +51,29 @@ INSERT INTO `task` (`id`, `name`, `type`, `parameters`) VALUES )), (5, '상품 정보 크롤링 태스크', 'FastAPI', JSON_OBJECT( 'endpoint', '/products/crawl', 'method', 'POST', - 'body', JSON_OBJECT('product_url', 'String') -- { "product_url": str } + 'body', JSON_OBJECT('product_urls', 'List') -- { "product_urls": List[str] } 수정됨 )), + -- 🆕 S3 업로드 태스크 추가 + (6, 'S3 업로드 태스크', 'FastAPI', JSON_OBJECT( + 'endpoint', '/products/s3-upload', 'method', 'POST', + 'body', JSON_OBJECT( -- { keyword: str, crawled_products: List, base_folder: str } + 'keyword', 'String', + 'crawled_products', 'List', + 'base_folder', 'String' + ) + )), -- RAG관련 request body는 추후에 결정될 예정 - (6, '블로그 RAG 생성 태스크', 'FastAPI', JSON_OBJECT('endpoint', '/blogs/rag/create', 'method', 'POST')), - (7, '블로그 발행 태스크', 'FastAPI', JSON_OBJECT( + (7, '블로그 RAG 생성 태스크', 'FastAPI', JSON_OBJECT('endpoint', '/blogs/rag/create', 'method', 'POST')), + (8, '블로그 발행 태스크', 'FastAPI', JSON_OBJECT( 'endpoint', '/blogs/publish', 'method', 'POST', 'body', JSON_OBJECT( -- { tag: str, blog_id: str, ... } - 'tag', 'String', - 'blog_id', 'String', - 'blog_pw', 'String', - 'blog_name', 'String', - 'post_title', 'String', - 'post_content', 'String', - 'post_tags', 'List' + 'tag', 'NAVER_BLOG', + 'blog_id', 'wtecho331', + 'blog_pw', 'wt505033@#', + 'blog_name', '박스박스dasdsafs.', + 'post_title', '박스박스dasdsafs.', + 'post_content', '퉁퉁퉁퉁퉁퉁퉁사후르', + 'post_tags', '[]' ) )) ON DUPLICATE KEY UPDATE name = VALUES(name), type = VALUES(type), parameters = VALUES(parameters), updated_at = NOW(); @@ -80,8 +89,9 @@ INSERT INTO `workflow_job` (`workflow_id`, `job_id`, `execution_order`) VALUES -- Job-Task 연결 INSERT INTO `job_task` (`job_id`, `task_id`, `execution_order`) VALUES - (1, 1, 1), (1, 2, 2), (1, 3, 3), (1, 4, 4), (1, 5, 5), - (2, 6, 1), (2, 7, 2) + -- Job 1: 상품 분석 (키워드검색 → 상품검색 → 매칭 → 유사도 → 크롤링 → S3업로드) + (1, 1, 1), (1, 2, 2), (1, 3, 3), (1, 4, 4), (1, 5, 5), (1, 6, 6), + (2, 7, 1), (2, 8, 2) ON DUPLICATE KEY UPDATE execution_order = VALUES(execution_order); -- 스케줄 설정 (매일 오전 8시) From 977a5e75be62cafa275749cb71485192b6de8f06 Mon Sep 17 00:00:00 2001 From: JiHoon Date: Mon, 22 Sep 2025 18:55:46 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat=20:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=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 --- .../controller/WorkflowController.java | 10 +++++++ .../workflow/dto/WorkflowHistoryDTO.java | 20 ++++++++++++++ .../workflow/mapper/WorkflowMapper.java | 5 ++++ .../service/WorkflowHistoryService.java | 27 +++++++++++++++++++ .../mybatis/mapper/WorkflowMapper.xml | 27 +++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.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/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index 9cd5933b..0d91f71b 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 @@ -9,7 +9,9 @@ import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; import site.icebang.domain.workflow.dto.WorkflowCardDto; +import site.icebang.domain.workflow.dto.WorkflowHistoryDTO; import site.icebang.domain.workflow.service.WorkflowExecutionService; +import site.icebang.domain.workflow.service.WorkflowHistoryService; import site.icebang.domain.workflow.service.WorkflowService; @RestController @@ -18,6 +20,7 @@ public class WorkflowController { private final WorkflowService workflowService; private final WorkflowExecutionService workflowExecutionService; + private final WorkflowHistoryService workflowHistoryService; @GetMapping("") public ApiResponse> getWorkflowList( @@ -26,6 +29,13 @@ public ApiResponse> getWorkflowList( return ApiResponse.success(result); } + @GetMapping("/history") + public ApiResponse> getWorkflowHistoryList( + @ModelAttribute PageParams pageParams) { + PageResult result = workflowHistoryService.getPagedResult(pageParams); + return ApiResponse.success(result); + } + @PostMapping("/{workflowId}/run") public ResponseEntity runWorkflow(@PathVariable Long workflowId) { // HTTP 요청/응답 스레드를 블로킹하지 않도록 비동기 실행 diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.java new file mode 100644 index 00000000..946393e5 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.java @@ -0,0 +1,20 @@ +package site.icebang.domain.workflow.dto; + +import lombok.Data; + +import java.math.BigInteger; +import java.time.LocalDateTime; + +@Data +public class WorkflowHistoryDTO { + + private BigInteger id; + private BigInteger workflowId; + private String traceId; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private BigInteger createdBy; + private String triggerType; + private String runNumber; + private String status; +} 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 00afbebc..f1f128ff 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 @@ -5,6 +5,7 @@ import site.icebang.common.dto.PageParams; import site.icebang.domain.workflow.dto.WorkflowCardDto; +import site.icebang.domain.workflow.dto.WorkflowHistoryDTO; public interface WorkflowMapper { List selectWorkflowList(PageParams pageParams); @@ -12,4 +13,8 @@ public interface WorkflowMapper { int selectWorkflowCount(PageParams pageParams); WorkflowCardDto selectWorkflowById(BigInteger id); + + List selectWorkflowHistoryList(PageParams pageParams); + + int selectWorkflowHistoryCount(PageParams pageParams); } 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..092b80d0 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java @@ -0,0 +1,27 @@ +package site.icebang.domain.workflow.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import site.icebang.common.dto.PageParams; +import site.icebang.common.dto.PageResult; +import site.icebang.common.service.PageableService; +import site.icebang.domain.workflow.dto.WorkflowHistoryDTO; +import site.icebang.domain.workflow.mapper.WorkflowMapper; + +@Service +@RequiredArgsConstructor +public class WorkflowHistoryService implements PageableService { + + private final WorkflowMapper workflowMapper; + + @Override + @Transactional(readOnly = true) + public PageResult getPagedResult(PageParams pageParams) { + + return PageResult.from( + pageParams, + () -> workflowMapper.selectWorkflowHistoryList(pageParams), + () -> workflowMapper.selectWorkflowHistoryCount(pageParams)); + } +} 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 dacade96..5968c395 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml @@ -39,4 +39,31 @@ LEFT JOIN user u ON w.created_by = u.id WHERE w.id = #{id} + + + + + \ No newline at end of file From 4d84232b67fcdd40b113218b01cad7f96a11b0bd Mon Sep 17 00:00:00 2001 From: thkim7 Date: Mon, 22 Sep 2025 18:56:11 +0900 Subject: [PATCH 03/12] chore: spotlessApply --- .../fastapi/body/BlogPublishBodyBuilder.java | 6 +- .../fastapi/body/BlogRagBodyBuilder.java | 67 ++++++++-------- .../fastapi/body/ProductCrawlBodyBuilder.java | 76 ++++++++++--------- .../fastapi/body/S3UploadBodyBuilder.java | 60 +++++++-------- 4 files changed, 108 insertions(+), 101 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java index 4b62cd60..6b19de0b 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java @@ -50,9 +50,9 @@ public ObjectNode build(Task task, Map workflowContext) { .ifPresent(tagsNode -> body.set("post_tags", tagsNode)); }); - body.put("tag", "NAVER_BLOG"); - body.put("blog_id", "wtecho331"); - body.put("blog_pw", "wt505033@#"); + body.put("tag", "NAVER_BLOG"); + body.put("blog_id", "wtecho331"); + body.put("blog_pw", "wt505033@#"); return body; } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java index 66551c70..419a23a4 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogRagBodyBuilder.java @@ -17,34 +17,39 @@ @RequiredArgsConstructor public class BlogRagBodyBuilder implements TaskBodyBuilder { - private final ObjectMapper objectMapper; - private static final String TASK_NAME = "블로그 RAG 생성 태스크"; - private static final String KEYWORD_SOURCE_TASK = "키워드 검색 태스크"; - private static final String S3_UPLOAD_SOURCE_TASK = "S3 업로드 태스크"; // 변경: 크롤링 → S3 업로드 - - @Override - public boolean supports(String taskName) { - return TASK_NAME.equals(taskName); - } - - @Override - public ObjectNode build(Task task, Map workflowContext) { - ObjectNode body = objectMapper.createObjectNode(); - - // 키워드 정보 가져오기 - Optional.ofNullable(workflowContext.get(KEYWORD_SOURCE_TASK)) - .map(node -> node.path("data").path("keyword")) - .ifPresent(keywordNode -> body.set("keyword", keywordNode)); - - // S3 업로드에서 선택된 상품 정보 가져오기 (변경된 부분) - Optional.ofNullable(workflowContext.get(S3_UPLOAD_SOURCE_TASK)) - .map(node -> node.path("data").path("selected_product_for_content").path("product_info").path("product_detail")) - .ifPresent(productNode -> body.set("product_info", productNode)); - - // 기본 콘텐츠 설정 - body.put("content_type", "review_blog"); - body.put("target_length", 1000); - - return body; - } -} \ No newline at end of file + private final ObjectMapper objectMapper; + private static final String TASK_NAME = "블로그 RAG 생성 태스크"; + private static final String KEYWORD_SOURCE_TASK = "키워드 검색 태스크"; + private static final String S3_UPLOAD_SOURCE_TASK = "S3 업로드 태스크"; // 변경: 크롤링 → S3 업로드 + + @Override + public boolean supports(String taskName) { + return TASK_NAME.equals(taskName); + } + + @Override + public ObjectNode build(Task task, Map workflowContext) { + ObjectNode body = objectMapper.createObjectNode(); + + // 키워드 정보 가져오기 + Optional.ofNullable(workflowContext.get(KEYWORD_SOURCE_TASK)) + .map(node -> node.path("data").path("keyword")) + .ifPresent(keywordNode -> body.set("keyword", keywordNode)); + + // S3 업로드에서 선택된 상품 정보 가져오기 (변경된 부분) + Optional.ofNullable(workflowContext.get(S3_UPLOAD_SOURCE_TASK)) + .map( + node -> + node.path("data") + .path("selected_product_for_content") + .path("product_info") + .path("product_detail")) + .ifPresent(productNode -> body.set("product_info", productNode)); + + // 기본 콘텐츠 설정 + body.put("content_type", "review_blog"); + body.put("target_length", 1000); + + return body; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java index db6f5444..4c90e31a 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/ProductCrawlBodyBuilder.java @@ -3,11 +3,11 @@ import java.util.Map; import java.util.Optional; -import com.fasterxml.jackson.databind.node.ArrayNode; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; @@ -18,39 +18,41 @@ @RequiredArgsConstructor public class ProductCrawlBodyBuilder implements TaskBodyBuilder { - private final ObjectMapper objectMapper; - private static final String TASK_NAME = "상품 정보 크롤링 태스크"; - private static final String SIMILARITY_SOURCE_TASK = "상품 유사도 분석 태스크"; - - @Override - public boolean supports(String taskName) { - return TASK_NAME.equals(taskName); - } - - @Override - public ObjectNode build(Task task, Map workflowContext) { - ObjectNode body = objectMapper.createObjectNode(); - - // ArrayNode 준비 (product_urls 배열로 변경) - ArrayNode productUrls = objectMapper.createArrayNode(); - - // 유사도 분석에서 선택된 상품들의 URL 가져오기 (복수로 변경) - Optional.ofNullable(workflowContext.get(SIMILARITY_SOURCE_TASK)) - .ifPresent(node -> { - JsonNode topProducts = node.path("data").path("top_products"); - if (topProducts.isArray()) { - // top_products 배열에서 각 상품의 URL 추출 - topProducts.forEach(product -> { - JsonNode urlNode = product.path("url"); - if (!urlNode.isMissingNode() && !urlNode.asText().isEmpty()) { - productUrls.add(urlNode.asText()); - } - }); - } - }); - - body.set("product_urls", productUrls); - - return body; - } -} \ No newline at end of file + private final ObjectMapper objectMapper; + private static final String TASK_NAME = "상품 정보 크롤링 태스크"; + private static final String SIMILARITY_SOURCE_TASK = "상품 유사도 분석 태스크"; + + @Override + public boolean supports(String taskName) { + return TASK_NAME.equals(taskName); + } + + @Override + public ObjectNode build(Task task, Map workflowContext) { + ObjectNode body = objectMapper.createObjectNode(); + + // ArrayNode 준비 (product_urls 배열로 변경) + ArrayNode productUrls = objectMapper.createArrayNode(); + + // 유사도 분석에서 선택된 상품들의 URL 가져오기 (복수로 변경) + Optional.ofNullable(workflowContext.get(SIMILARITY_SOURCE_TASK)) + .ifPresent( + node -> { + JsonNode topProducts = node.path("data").path("top_products"); + if (topProducts.isArray()) { + // top_products 배열에서 각 상품의 URL 추출 + topProducts.forEach( + product -> { + JsonNode urlNode = product.path("url"); + if (!urlNode.isMissingNode() && !urlNode.asText().isEmpty()) { + productUrls.add(urlNode.asText()); + } + }); + } + }); + + body.set("product_urls", productUrls); + + return body; + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java index 1af32aab..bd0f823e 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/S3UploadBodyBuilder.java @@ -17,33 +17,33 @@ @RequiredArgsConstructor public class S3UploadBodyBuilder implements TaskBodyBuilder { - private final ObjectMapper objectMapper; - private static final String TASK_NAME = "S3 업로드 태스크"; - private static final String KEYWORD_SOURCE_TASK = "키워드 검색 태스크"; - private static final String CRAWL_SOURCE_TASK = "상품 정보 크롤링 태스크"; - - @Override - public boolean supports(String taskName) { - return TASK_NAME.equals(taskName); - } - - @Override - public ObjectNode build(Task task, Map workflowContext) { - ObjectNode body = objectMapper.createObjectNode(); - - // 키워드 정보 가져오기 - Optional.ofNullable(workflowContext.get(KEYWORD_SOURCE_TASK)) - .map(node -> node.path("data").path("keyword")) - .ifPresent(keywordNode -> body.set("keyword", keywordNode)); - - // 크롤링된 상품 데이터 가져오기 - Optional.ofNullable(workflowContext.get(CRAWL_SOURCE_TASK)) - .map(node -> node.path("data").path("crawled_products")) - .ifPresent(crawledProductsNode -> body.set("crawled_products", crawledProductsNode)); - - // 기본 폴더 설정 - body.put("base_folder", "product"); - - return body; - } -} \ No newline at end of file + private final ObjectMapper objectMapper; + private static final String TASK_NAME = "S3 업로드 태스크"; + private static final String KEYWORD_SOURCE_TASK = "키워드 검색 태스크"; + private static final String CRAWL_SOURCE_TASK = "상품 정보 크롤링 태스크"; + + @Override + public boolean supports(String taskName) { + return TASK_NAME.equals(taskName); + } + + @Override + public ObjectNode build(Task task, Map workflowContext) { + ObjectNode body = objectMapper.createObjectNode(); + + // 키워드 정보 가져오기 + Optional.ofNullable(workflowContext.get(KEYWORD_SOURCE_TASK)) + .map(node -> node.path("data").path("keyword")) + .ifPresent(keywordNode -> body.set("keyword", keywordNode)); + + // 크롤링된 상품 데이터 가져오기 + Optional.ofNullable(workflowContext.get(CRAWL_SOURCE_TASK)) + .map(node -> node.path("data").path("crawled_products")) + .ifPresent(crawledProductsNode -> body.set("crawled_products", crawledProductsNode)); + + // 기본 폴더 설정 + body.put("base_folder", "product"); + + return body; + } +} From bc3fa6a0807ffe4e35b4db5c1cc6a42798d76321 Mon Sep 17 00:00:00 2001 From: thkim7 Date: Mon, 22 Sep 2025 18:56:54 +0900 Subject: [PATCH 04/12] chore: poetry run black . --- .../app/model/schemas.py | 2 +- .../app/service/blog/blog_publish_service.py | 4 ++-- .../app/service/s3_upload_service.py | 21 ++++++++++++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/pre-processing-service/app/model/schemas.py b/apps/pre-processing-service/app/model/schemas.py index 301e3f9b..dd49cf44 100644 --- a/apps/pre-processing-service/app/model/schemas.py +++ b/apps/pre-processing-service/app/model/schemas.py @@ -231,7 +231,7 @@ class S3UploadData(BaseModel): selected_product_for_content: Optional[Dict] = Field( None, title="콘텐츠 생성용 선택 상품", - description="임시: 블로그 콘텐츠 생성을 위해 선택된 단일 상품 정보" + description="임시: 블로그 콘텐츠 생성을 위해 선택된 단일 상품 정보", ) diff --git a/apps/pre-processing-service/app/service/blog/blog_publish_service.py b/apps/pre-processing-service/app/service/blog/blog_publish_service.py index c88e8cc5..59014222 100644 --- a/apps/pre-processing-service/app/service/blog/blog_publish_service.py +++ b/apps/pre-processing-service/app/service/blog/blog_publish_service.py @@ -38,7 +38,7 @@ def publish_content(self, request: RequestBlogPublish) -> Dict: raise CustomException( detail=f"{request.tag} 블로그 포스팅에 실패했습니다.", status_code=500, - code="POSTING_FAIL" + code="POSTING_FAIL", ) return response_data @@ -51,5 +51,5 @@ def publish_content(self, request: RequestBlogPublish) -> Dict: raise CustomException( detail=f"블로그 포스팅 중 오류가 발생했습니다: {str(e)}", status_code=500, - code="ERROR" + code="ERROR", ) diff --git a/apps/pre-processing-service/app/service/s3_upload_service.py b/apps/pre-processing-service/app/service/s3_upload_service.py index fa3674b9..48c84d35 100644 --- a/apps/pre-processing-service/app/service/s3_upload_service.py +++ b/apps/pre-processing-service/app/service/s3_upload_service.py @@ -22,7 +22,7 @@ async def upload_crawled_products_to_s3(self, request: RequestS3Upload) -> dict: keyword = request.keyword # 키워드 추가 crawled_products = request.crawled_products base_folder = ( - request.base_folder or "product" + request.base_folder or "product" ) # 🔸 기본값 변경: product-images → product logger.info( @@ -132,7 +132,7 @@ async def upload_crawled_products_to_s3(self, request: RequestS3Upload) -> dict: raise InvalidItemDataException() def _select_single_product_for_content( - self, crawled_products: List[Dict], upload_results: List[Dict] + self, crawled_products: List[Dict], upload_results: List[Dict] ) -> Dict: """ 🆕 임시: 콘텐츠 생성을 위한 단일 상품 선택 로직 @@ -142,13 +142,17 @@ def _select_single_product_for_content( try: # 1순위: S3 업로드 성공하고 이미지가 있는 상품들 successful_uploads = [ - result for result in upload_results - if result.get("status") == "completed" and result.get("success_count", 0) > 0 + result + for result in upload_results + if result.get("status") == "completed" + and result.get("success_count", 0) > 0 ] if successful_uploads: # 이미지 개수가 가장 많은 상품 선택 - best_upload = max(successful_uploads, key=lambda x: x.get("success_count", 0)) + best_upload = max( + successful_uploads, key=lambda x: x.get("success_count", 0) + ) selected_index = best_upload["product_index"] # 원본 크롤링 데이터에서 해당 상품 찾기 @@ -167,8 +171,9 @@ def _select_single_product_for_content( # 2순위: 크롤링 성공한 첫 번째 상품 (S3 업로드 실패해도) for product_info in crawled_products: - if (product_info.get("status") == "success" and - product_info.get("product_detail")): + if product_info.get("status") == "success" and product_info.get( + "product_detail" + ): # 해당 상품의 S3 업로드 정보 찾기 upload_info = None @@ -210,4 +215,4 @@ def _select_single_product_for_content( "product_info": crawled_products[0] if crawled_products else None, "s3_upload_info": upload_results[0] if upload_results else None, "error": str(e), - } \ No newline at end of file + } From 69003b0b187b9dba6922bb1f3cfc81e2e848875e Mon Sep 17 00:00:00 2001 From: Yousung Jung Date: Mon, 22 Sep 2025 19:11:12 +0900 Subject: [PATCH 05/12] =?UTF-8?q?Workflow=20history=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20api=20(#169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Workflow history detail controller (mock data) * chore: Workflow history detail에 필요한 dto 및 service method 정의 * feat: Workflow run detail api --- .../controller/WorkflowHistoryController.java | 31 +++ .../domain/workflow/dto/ExecutionLogDto.java | 22 ++ .../domain/workflow/dto/JobRunDto.java | 26 ++ .../domain/workflow/dto/TaskRunDto.java | 24 ++ .../dto/WorkflowRunDetailResponse.java | 18 ++ .../domain/workflow/dto/WorkflowRunDto.java | 25 ++ .../workflow/dto/WorkflowRunLogsResponse.java | 17 ++ .../mapper/WorkflowHistoryMapper.java | 44 ++++ .../service/WorkflowHistoryService.java | 73 ++++++ .../mybatis/mapper/WorkflowHistoryMapper.xml | 71 ++++++ .../sql/04-insert-workflow-history.sql | 76 ++++++ .../WorkflowHistoryApiIntegrationTest.java | 228 ++++++++++++++++++ 12 files changed, 655 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java 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/WorkflowRunDetailResponse.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/mapper/WorkflowHistoryMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.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 new file mode 100644 index 00000000..8ca79726 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowHistoryController.java @@ -0,0 +1,31 @@ +package site.icebang.domain.workflow.controller; + +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; +import site.icebang.domain.workflow.dto.WorkflowRunDetailResponse; +import site.icebang.domain.workflow.service.WorkflowHistoryService; + +@RestController +@RequestMapping("/v0/workflow-runs") +@RequiredArgsConstructor +public class WorkflowHistoryController { + private final WorkflowHistoryService workflowHistoryService; + + /** + * 워크플로우 실행 상세 조회 + * + * @param runId workflow_run.id + * @return WorkflowRunDetailResponse + */ + @GetMapping("/{runId}") + public ApiResponse getWorkflowRunDetail(@PathVariable Long runId) { + WorkflowRunDetailResponse response = workflowHistoryService.getWorkflowRunDetail(runId); + return ApiResponse.success(response); + } +} 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..618a6214 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/JobRunDto.java @@ -0,0 +1,26 @@ +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 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/WorkflowRunDetailResponse.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDetailResponse.java new file mode 100644 index 00000000..194e8583 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowRunDetailResponse.java @@ -0,0 +1,18 @@ +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 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/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 new file mode 100644 index 00000000..583e2b20 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowHistoryService.java @@ -0,0 +1,73 @@ +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.*; +import site.icebang.domain.workflow.mapper.WorkflowHistoryMapper; + +@Service +@RequiredArgsConstructor +public class WorkflowHistoryService { + private final WorkflowHistoryMapper workflowHistoryMapper; + + /** + * 워크플로우 실행 상세 조회 + * + * @param runId workflow_run.id + * @return WorkflowRunDetailResponse + */ + @Transactional(readOnly = true) + public WorkflowRunDetailResponse getWorkflowRunDetail(Long runId) { + // 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(); + } + + /** + * 워크플로우 실행 로그 조회 + * + * @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; + } +} 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()))); + } +} From bf67513cd6a3b14191b5da11b3728e97794106e1 Mon Sep 17 00:00:00 2001 From: Yousung Jung Date: Mon, 22 Sep 2025 19:29:47 +0900 Subject: [PATCH 06/12] =?UTF-8?q?chore:=20Email=20env=20=EC=82=BD=EC=9E=85?= =?UTF-8?q?=20(#170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-java.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy-java.yml b/.github/workflows/deploy-java.yml index 8d472520..bb4483dd 100644 --- a/.github/workflows/deploy-java.yml +++ b/.github/workflows/deploy-java.yml @@ -34,6 +34,8 @@ jobs: echo "GRAFANA_CLOUD_PROMETHEUS_URL=${{ secrets.GRAFANA_CLOUD_PROMETHEUS_URL }}" >> .env.prod echo "GRAFANA_CLOUD_PROMETHEUS_USER=${{ secrets.GRAFANA_CLOUD_PROMETHEUS_USER }}" >> .env.prod echo "GRAFANA_CLOUD_API_KEY=${{ secrets.GRAFANA_CLOUD_API_KEY }}" >> .env.prod + echo "MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}" >> .env.prod + echo "MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}" >> .env.prod - name: Set repo lowercase run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV From 9a9a980870d9c20c751dff424b1463f8f939b4fa Mon Sep 17 00:00:00 2001 From: jeonghyeokSim Date: Mon, 22 Sep 2025 19:39:48 +0900 Subject: [PATCH 07/12] =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=84=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B5=AC=ED=98=84=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영환경용 실제 Gmail 발송 서비스 구성 - 개발환경용 Mock 이메일 서비스 분리 - 프로필 기반 이메일 서비스 자동 선택 기능 추가 - 이메일 발송 테스트 API 엔드포인트 구현 --- .../email/controller/EmailTestController.java | 37 ++++++ .../email/service/EmailServiceImpl.java | 106 +++++++++++++++--- .../email/service/MockEmailService.java | 3 +- .../config/security/SecurityConfig.java | 2 + .../main/resources/application-production.yml | 16 +++ 5 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/email/controller/EmailTestController.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/email/controller/EmailTestController.java b/apps/user-service/src/main/java/site/icebang/domain/email/controller/EmailTestController.java new file mode 100644 index 00000000..633e3ee7 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/email/controller/EmailTestController.java @@ -0,0 +1,37 @@ +package site.icebang.domain.email.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import lombok.RequiredArgsConstructor; + +import site.icebang.domain.email.dto.EmailRequest; +import site.icebang.domain.email.service.EmailService; + +@RestController +@RequestMapping("/api/v1/email") +@RequiredArgsConstructor +public class EmailTestController { + private final EmailService emailService; + + @GetMapping("/test") + @PreAuthorize("permitAll()") + public ResponseEntity sendTestEmail(@RequestParam String to) { + try { + EmailRequest emailRequest = + EmailRequest.builder() + .to(to) + .subject("IceBang 실제 테스트 이메일") + .body("안녕하세요!\n\nIceBang에서 보내는 실제 Gmail 테스트 이메일입니다.\n\n성공적으로 연동되었습니다!") + .isHtml(false) + .build(); + + emailService.send(emailRequest); + return ResponseEntity.ok("실제 Gmail 테스트 이메일 전송 완료! 받은편지함을 확인하세요!"); + + } catch (Exception e) { + return ResponseEntity.badRequest().body("이메일 전송 실패: " + e.getMessage()); + } + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/email/service/EmailServiceImpl.java b/apps/user-service/src/main/java/site/icebang/domain/email/service/EmailServiceImpl.java index cd8ba706..1bf7454b 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/email/service/EmailServiceImpl.java +++ b/apps/user-service/src/main/java/site/icebang/domain/email/service/EmailServiceImpl.java @@ -1,16 +1,92 @@ -// package site.icebang.domain.email.service; -// -// import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -// import org.springframework.stereotype.Service; -// -// import lombok.RequiredArgsConstructor; -// -// import site.icebang.domain.email.dto.EmailRequest; -// -// @Service -// @RequiredArgsConstructor +package site.icebang.domain.email.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import site.icebang.domain.email.dto.EmailRequest; + +@Slf4j +@Service +@Profile({"production"}) +@RequiredArgsConstructor // @ConditionalOnMissingBean(EmailService.class) -// public class EmailServiceImpl implements EmailService { -// @Override -// public void send(EmailRequest emailRequest) {} -// } +public class EmailServiceImpl implements EmailService { + + private final JavaMailSender mailSender; + + @Value("${spring.mail.username}") + private String defaultSender; + + @Override + public void send(EmailRequest request) { + try { + if (request.isHtml()) { + sendHtmlEmail(request); + } else { + sendSimpleEmail(request); + } + } catch (Exception e) { + log.error("❌ 이메일 전송 실패 - To: {}", request.getTo(), e); + throw new RuntimeException("이메일 전송 실패: " + e.getMessage(), e); + } + } + + private void sendSimpleEmail(EmailRequest request) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(request.getTo()); + message.setSubject(request.getSubject()); + message.setText(request.getBody()); + message.setFrom(defaultSender); + + // CC 설정 + if (request.getCc() != null && !request.getCc().isEmpty()) { + message.setCc(request.getCc().toArray(new String[0])); + } + + // BCC 설정 + if (request.getBcc() != null && !request.getBcc().isEmpty()) { + message.setBcc(request.getBcc().toArray(new String[0])); + } + + mailSender.send(message); + log.info("✅ 실제 Gmail 전송 성공! To: {}, Subject: {}", request.getTo(), request.getSubject()); + + } catch (Exception e) { + log.error("❌ Gmail 텍스트 전송 실패", e); + throw e; + } + } + + private void sendHtmlEmail(EmailRequest request) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setTo(request.getTo()); + helper.setSubject(request.getSubject()); + helper.setText(request.getBody(), true); // HTML 모드 + helper.setFrom(defaultSender); + + // CC 설정 + if (request.getCc() != null && !request.getCc().isEmpty()) { + helper.setCc(request.getCc().toArray(new String[0])); + } + + // BCC 설정 + if (request.getBcc() != null && !request.getBcc().isEmpty()) { + helper.setBcc(request.getBcc().toArray(new String[0])); + } + + mailSender.send(message); + log.info("✅ 실제 Gmail HTML 전송 성공! To: {}, Subject: {}", request.getTo(), request.getSubject()); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/email/service/MockEmailService.java b/apps/user-service/src/main/java/site/icebang/domain/email/service/MockEmailService.java index 7530667d..ee84e8ea 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/email/service/MockEmailService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/email/service/MockEmailService.java @@ -1,5 +1,6 @@ package site.icebang.domain.email.service; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import lombok.extern.slf4j.Slf4j; @@ -7,7 +8,7 @@ import site.icebang.domain.email.dto.EmailRequest; @Service -// @Profile({"test-unit", "test-e2e", "test-integration", "local", "develop", "production"}) +@Profile({"develop", "test-e2e", "test-integration", "test-unit"}) @Slf4j public class MockEmailService implements EmailService { diff --git a/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java index 8b5dd63c..3543a8dd 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java @@ -78,6 +78,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .permitAll() .requestMatchers("/v0/workflows/**") .permitAll() + .requestMatchers("/api/v1/email/**") // 이메일 API 경로 허용 + .permitAll() .requestMatchers("/v0/auth/check-session") .authenticated() .requestMatchers(SecurityEndpoints.DATA_ADMIN.getMatchers()) diff --git a/apps/user-service/src/main/resources/application-production.yml b/apps/user-service/src/main/resources/application-production.yml index 032954ad..c3645e13 100644 --- a/apps/user-service/src/main/resources/application-production.yml +++ b/apps/user-service/src/main/resources/application-production.yml @@ -17,6 +17,22 @@ spring: minimum-idle: 5 pool-name: HikariCP-MyBatis + # Gmail 연동 설정 + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + connectiontimeout: 5000 + timeout: 5000 + writetimeout: 5000 + debug: true # quartz: # jdbc: # initialize-schema: never From f9f1ae735992b8d6311f8969e253da72f4c76526 Mon Sep 17 00:00:00 2001 From: JiHoon Date: Mon, 22 Sep 2025 19:42:16 +0900 Subject: [PATCH 08/12] =?UTF-8?q?fix=20:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WorkflowController.java | 7 ------- .../controller/WorkflowHistoryController.java | 15 +++++++++---- .../mapper/WorkflowHistoryMapper.java | 18 ++++++++++++++++ .../workflow/mapper/WorkflowMapper.java | 3 --- .../service/WorkflowHistoryService.java | 21 ++++++++++++++++++- 5 files changed, 49 insertions(+), 15 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 0d91f71b..85ca445c 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 @@ -29,13 +29,6 @@ public ApiResponse> getWorkflowList( return ApiResponse.success(result); } - @GetMapping("/history") - public ApiResponse> getWorkflowHistoryList( - @ModelAttribute PageParams pageParams) { - PageResult result = workflowHistoryService.getPagedResult(pageParams); - return ApiResponse.success(result); - } - @PostMapping("/{workflowId}/run") public ResponseEntity runWorkflow(@PathVariable Long workflowId) { // HTTP 요청/응답 스레드를 블로킹하지 않도록 비동기 실행 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 8ca79726..4267c8ff 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,13 +1,13 @@ package site.icebang.domain.workflow.controller; -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 org.springframework.web.bind.annotation.*; 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.workflow.dto.WorkflowHistoryDTO; import site.icebang.domain.workflow.dto.WorkflowRunDetailResponse; import site.icebang.domain.workflow.service.WorkflowHistoryService; @@ -17,6 +17,13 @@ public class WorkflowHistoryController { private final WorkflowHistoryService workflowHistoryService; + @GetMapping("") + public ApiResponse> getWorkflowHistoryList( + @ModelAttribute PageParams pageParams) { + PageResult response = workflowHistoryService.getPagedResult(pageParams); + return ApiResponse.success(response); + } + /** * 워크플로우 실행 상세 조회 * 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 index 6833a6d7..27406e19 100644 --- 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 @@ -4,8 +4,10 @@ import org.apache.ibatis.annotations.Mapper; +import site.icebang.common.dto.PageParams; import site.icebang.domain.workflow.dto.JobRunDto; import site.icebang.domain.workflow.dto.TaskRunDto; +import site.icebang.domain.workflow.dto.WorkflowHistoryDTO; import site.icebang.domain.workflow.dto.WorkflowRunDto; @Mapper @@ -41,4 +43,20 @@ public interface WorkflowHistoryMapper { * @return String traceId */ String selectTraceIdByRunId(Long runId); + + /** + * 워크플로우 런 페이지네이션 + * + * @param pageParams pageParams + * @return List + * */ + List selectWorkflowHistoryList(PageParams pageParams); + + /** + * 워크플로우 런 인스턴스 개수 조회 + * @param pageParams pageParams + * @return 결과 개수 + * */ + int selectWorkflowHistoryCount(PageParams pageParams); + } 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 f1f128ff..f5ec2a9a 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 @@ -14,7 +14,4 @@ public interface WorkflowMapper { WorkflowCardDto selectWorkflowById(BigInteger id); - List selectWorkflowHistoryList(PageParams pageParams); - - int selectWorkflowHistoryCount(PageParams pageParams); } 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 583e2b20..ab0827e9 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 @@ -7,14 +7,33 @@ import lombok.RequiredArgsConstructor; +import site.icebang.common.dto.PageParams; +import site.icebang.common.dto.PageResult; +import site.icebang.common.service.PageableService; import site.icebang.domain.workflow.dto.*; import site.icebang.domain.workflow.mapper.WorkflowHistoryMapper; +import site.icebang.domain.workflow.mapper.WorkflowMapper; @Service @RequiredArgsConstructor -public class WorkflowHistoryService { +public class WorkflowHistoryService implements PageableService { private final WorkflowHistoryMapper workflowHistoryMapper; + /** + * 워크플로우 런 조회 + * @param pageParams pageParams + * @return PageResult + * */ + @Override + @Transactional(readOnly = true) + public PageResult getPagedResult(PageParams pageParams) { + + return PageResult.from( + pageParams, + () -> workflowHistoryMapper.selectWorkflowHistoryList(pageParams), + () -> workflowHistoryMapper.selectWorkflowHistoryCount(pageParams)); + } + /** * 워크플로우 실행 상세 조회 * From 854799d2fd0c5fad2d68077ea389ac51a6a3bad6 Mon Sep 17 00:00:00 2001 From: thkim7 Date: Mon, 22 Sep 2025 19:47:03 +0900 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20blog=5Fservice=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=BD=94=EB=93=9C=20200=20->=20400?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/service/blog/blog_service_factory.py | 2 +- .../fastapi/body/BlogPublishBodyBuilder.java | 7 ++++--- .../src/main/resources/sql/03-insert-workflow.sql | 14 +++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/pre-processing-service/app/service/blog/blog_service_factory.py b/apps/pre-processing-service/app/service/blog/blog_service_factory.py index 4759b5ab..27eef797 100644 --- a/apps/pre-processing-service/app/service/blog/blog_service_factory.py +++ b/apps/pre-processing-service/app/service/blog/blog_service_factory.py @@ -45,7 +45,7 @@ def create_service( if platform.lower() == "tistory_blog": if not blog_name: raise CustomException( - 200, + 400, "티스토리 블로그가 존재하지않습니다.", "NOT_FOUND_BLOG", ) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java index 6b19de0b..f8a10ee0 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java @@ -50,9 +50,10 @@ public ObjectNode build(Task task, Map workflowContext) { .ifPresent(tagsNode -> body.set("post_tags", tagsNode)); }); - body.put("tag", "NAVER_BLOG"); - body.put("blog_id", "wtecho331"); - body.put("blog_pw", "wt505033@#"); + body.put("tag", "TISTORY_BLOG"); + body.put("blog_name", "hoons2641"); + body.put("blog_id", "fair_05@nate.com"); + body.put("blog_pw", "kdyn2641*"); return body; } diff --git a/apps/user-service/src/main/resources/sql/03-insert-workflow.sql b/apps/user-service/src/main/resources/sql/03-insert-workflow.sql index 4eb45b6c..fc9f39c2 100644 --- a/apps/user-service/src/main/resources/sql/03-insert-workflow.sql +++ b/apps/user-service/src/main/resources/sql/03-insert-workflow.sql @@ -67,13 +67,13 @@ INSERT INTO `task` (`id`, `name`, `type`, `parameters`) VALUES (8, '블로그 발행 태스크', 'FastAPI', JSON_OBJECT( 'endpoint', '/blogs/publish', 'method', 'POST', 'body', JSON_OBJECT( -- { tag: str, blog_id: str, ... } - 'tag', 'NAVER_BLOG', - 'blog_id', 'wtecho331', - 'blog_pw', 'wt505033@#', - 'blog_name', '박스박스dasdsafs.', - 'post_title', '박스박스dasdsafs.', - 'post_content', '퉁퉁퉁퉁퉁퉁퉁사후르', - 'post_tags', '[]' + 'tag', 'String', + 'blog_id', 'String', + 'blog_pw', 'String', + 'blog_name', 'String', + 'post_title', 'String', + 'post_content', 'String', + 'post_tags', 'List' ) )) ON DUPLICATE KEY UPDATE name = VALUES(name), type = VALUES(type), parameters = VALUES(parameters), updated_at = NOW(); From ba9b854e612ab1c0898d251d019f8c2f6d6bc27d Mon Sep 17 00:00:00 2001 From: JiHoon Date: Mon, 22 Sep 2025 19:48:00 +0900 Subject: [PATCH 10/12] chore: fix lint --- .../controller/WorkflowController.java | 1 - .../controller/WorkflowHistoryController.java | 2 +- .../workflow/dto/WorkflowHistoryDTO.java | 22 +++++++++---------- .../mapper/WorkflowHistoryMapper.java | 6 ++--- .../workflow/mapper/WorkflowMapper.java | 2 -- .../service/WorkflowHistoryService.java | 10 ++++----- 6 files changed, 20 insertions(+), 23 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 85ca445c..650a2bcf 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 @@ -9,7 +9,6 @@ import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; import site.icebang.domain.workflow.dto.WorkflowCardDto; -import site.icebang.domain.workflow.dto.WorkflowHistoryDTO; import site.icebang.domain.workflow.service.WorkflowExecutionService; import site.icebang.domain.workflow.service.WorkflowHistoryService; import site.icebang.domain.workflow.service.WorkflowService; 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 4267c8ff..07d4f20e 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 @@ -19,7 +19,7 @@ public class WorkflowHistoryController { @GetMapping("") public ApiResponse> getWorkflowHistoryList( - @ModelAttribute PageParams pageParams) { + @ModelAttribute PageParams pageParams) { PageResult response = workflowHistoryService.getPagedResult(pageParams); return ApiResponse.success(response); } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.java index 946393e5..18a25b7e 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowHistoryDTO.java @@ -1,20 +1,20 @@ package site.icebang.domain.workflow.dto; -import lombok.Data; - import java.math.BigInteger; import java.time.LocalDateTime; +import lombok.Data; + @Data public class WorkflowHistoryDTO { - private BigInteger id; - private BigInteger workflowId; - private String traceId; - private LocalDateTime startedAt; - private LocalDateTime finishedAt; - private BigInteger createdBy; - private String triggerType; - private String runNumber; - private String status; + private BigInteger id; + private BigInteger workflowId; + private String traceId; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private BigInteger createdBy; + private String triggerType; + private String runNumber; + private String status; } 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 index 27406e19..d22e2a68 100644 --- 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 @@ -49,14 +49,14 @@ public interface WorkflowHistoryMapper { * * @param pageParams pageParams * @return List - * */ + */ List selectWorkflowHistoryList(PageParams pageParams); /** * 워크플로우 런 인스턴스 개수 조회 + * * @param pageParams pageParams * @return 결과 개수 - * */ + */ int selectWorkflowHistoryCount(PageParams pageParams); - } 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 f5ec2a9a..00afbebc 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 @@ -5,7 +5,6 @@ import site.icebang.common.dto.PageParams; import site.icebang.domain.workflow.dto.WorkflowCardDto; -import site.icebang.domain.workflow.dto.WorkflowHistoryDTO; public interface WorkflowMapper { List selectWorkflowList(PageParams pageParams); @@ -13,5 +12,4 @@ public interface WorkflowMapper { int selectWorkflowCount(PageParams pageParams); WorkflowCardDto selectWorkflowById(BigInteger id); - } 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 ab0827e9..d04e238f 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 @@ -12,7 +12,6 @@ import site.icebang.common.service.PageableService; import site.icebang.domain.workflow.dto.*; import site.icebang.domain.workflow.mapper.WorkflowHistoryMapper; -import site.icebang.domain.workflow.mapper.WorkflowMapper; @Service @RequiredArgsConstructor @@ -21,17 +20,18 @@ public class WorkflowHistoryService implements PageableService getPagedResult(PageParams pageParams) { return PageResult.from( - pageParams, - () -> workflowHistoryMapper.selectWorkflowHistoryList(pageParams), - () -> workflowHistoryMapper.selectWorkflowHistoryCount(pageParams)); + pageParams, + () -> workflowHistoryMapper.selectWorkflowHistoryList(pageParams), + () -> workflowHistoryMapper.selectWorkflowHistoryCount(pageParams)); } /** From f1ed27dde946758375457ea007ce0025937c0c0c Mon Sep 17 00:00:00 2001 From: thkim7 Date: Mon, 22 Sep 2025 20:01:39 +0900 Subject: [PATCH 11/12] =?UTF-8?q?chore:=20blog=5Fpw=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/runner/fastapi/body/BlogPublishBodyBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java index f8a10ee0..1bf9561f 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/body/BlogPublishBodyBuilder.java @@ -53,7 +53,7 @@ public ObjectNode build(Task task, Map workflowContext) { body.put("tag", "TISTORY_BLOG"); body.put("blog_name", "hoons2641"); body.put("blog_id", "fair_05@nate.com"); - body.put("blog_pw", "kdyn2641*"); + body.put("blog_pw", "kdyn26*"); return body; } From 357da6d5e2fcb000b8faa815a74a7c2f0ac54a2d Mon Sep 17 00:00:00 2001 From: Yousung Jung Date: Mon, 22 Sep 2025 21:25:44 +0900 Subject: [PATCH 12/12] Workflow history list xml mapping (#175) --- .../mybatis/mapper/WorkflowHistoryMapper.xml | 26 ++++++++++++++++++ .../mybatis/mapper/WorkflowMapper.xml | 27 ------------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml index 3ad7aa65..290f53e4 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowHistoryMapper.xml @@ -68,4 +68,30 @@ WHERE id = #{runId} + + + + \ No newline at end of file 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 5968c395..dacade96 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml @@ -39,31 +39,4 @@ LEFT JOIN user u ON w.created_by = u.id WHERE w.id = #{id} - - - - - \ No newline at end of file