diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml index fd55f05f..e3349be5 100644 --- a/.github/workflows/ci-java.yml +++ b/.github/workflows/ci-java.yml @@ -78,8 +78,10 @@ jobs: run: | ./gradlew unitTest ./gradlew integrationTest + ./gradlew javadoc if [ "${{ github.base_ref }}" = "main" ] || [[ "${{ github.ref }}" == refs/tags/* ]]; then ./gradlew e2eTest + ./gradlew openapi3 fi working-directory: apps/user-service @@ -161,6 +163,12 @@ jobs: - set-image-tag if: startsWith(github.ref, 'refs/tags/user-service-v') + permissions: + contents: read # 리포지토리 읽기 + pages: write # GitHub Pages 쓰기 + id-token: write # OIDC 토큰 + actions: read # Actions 읽기 + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -192,12 +200,13 @@ jobs: with: output: user-service-swagger-ui-${{ needs.set-image-tag.outputs.image-tag }} spec-file: openapi-spec/openapi3.yaml + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy to GitHub Pages if: steps.check-openapi.outputs.openapi_exists == 'true' uses: peaceiris/actions-gh-pages@v3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + personal_token: ${{ secrets.PERSONAL_TOKEN }} publish_dir: ./user-service-swagger-ui-${{ needs.set-image-tag.outputs.image-tag }} destination_dir: user-service/${{ needs.set-image-tag.outputs.image-tag }} diff --git a/apps/pre-processing-service/Dockerfile b/apps/pre-processing-service/Dockerfile index ebc6e465..6ecb09c8 100644 --- a/apps/pre-processing-service/Dockerfile +++ b/apps/pre-processing-service/Dockerfile @@ -64,4 +64,4 @@ COPY . . ENV MECAB_PATH=/usr/lib/mecab/dic/ipadic # (권장 대안) 코드에서 uvicorn import 안 하고 프로세스 매니저로 실행하려면: -ENTRYPOINT ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app.main:app", "-b", "0.0.0.0:8000"] \ No newline at end of file +ENTRYPOINT ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app.main:app", "-b", "0.0.0.0:8000", "--timeout", "120"] \ No newline at end of file diff --git a/apps/pre-processing-service/app/core/config.py b/apps/pre-processing-service/app/core/config.py index a710faba..ad7005ea 100644 --- a/apps/pre-processing-service/app/core/config.py +++ b/apps/pre-processing-service/app/core/config.py @@ -104,7 +104,7 @@ class BaseSettingsConfig(BaseSettings): MAX_IMAGE_SIZE_MB: int = 10 # 테스트/추가용 필드 - openai_api_key: Optional[str] = None # << 이 부분 추가 + OPENAI_API_KEY: Optional[str] = None # << 이 부분 추가 def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/apps/pre-processing-service/app/service/blog/blog_create_service.py b/apps/pre-processing-service/app/service/blog/blog_create_service.py index 29ce12b7..a66fa609 100644 --- a/apps/pre-processing-service/app/service/blog/blog_create_service.py +++ b/apps/pre-processing-service/app/service/blog/blog_create_service.py @@ -1,32 +1,27 @@ import json import logging -import os +from loguru import logger from datetime import datetime from typing import Dict, List, Optional, Any from openai import OpenAI -from dotenv import load_dotenv - +from app.core.config import settings from app.model.schemas import RequestBlogCreate from app.errors.BlogPostingException import * -# 환경변수 로드 -load_dotenv(".env.dev") - class BlogContentService: """RAG를 사용한 블로그 콘텐츠 생성 전용 서비스""" def __init__(self): # OpenAI API 키 설정 - self.openai_api_key = os.getenv("OPENAI_API_KEY") + self.openai_api_key = settings.OPENAI_API_KEY if not self.openai_api_key: raise ValueError("OPENAI_API_KEY가 .env.dev 파일에 설정되지 않았습니다.") # 인스턴스 레벨에서 클라이언트 생성 self.client = OpenAI(api_key=self.openai_api_key) logging.basicConfig(level=logging.INFO) - self.logger = logging.getLogger(__name__) def generate_blog_content(self, request: RequestBlogCreate) -> Dict[str, Any]: """ @@ -39,20 +34,26 @@ def generate_blog_content(self, request: RequestBlogCreate) -> Dict[str, Any]: Dict: {"title": str, "content": str, "tags": List[str]} 형태의 결과 """ try: - # 1. 콘텐츠 정보 정리 + logger.debug("[STEP1] 콘텐츠 컨텍스트 준비 시작") content_context = self._prepare_content_context(request) + logger.debug(f"[STEP1 완료] context length={len(content_context)}") - # 2. 프롬프트 생성 + logger.debug("[STEP2] 프롬프트 생성 시작") prompt = self._create_content_prompt(content_context, request) + logger.debug(f"[STEP2 완료] prompt length={len(prompt)}") - # 3. GPT를 통한 콘텐츠 생성 + logger.debug("[STEP3] OpenAI API 호출 시작") generated_content = self._generate_with_openai(prompt) + logger.debug(f"[STEP3 완료] generated length={len(generated_content)}") + + logger.debug("[STEP4] 콘텐츠 파싱 시작") + result = self._parse_generated_content(generated_content, request) + logger.debug("[STEP4 완료]") - # 4. 콘텐츠 파싱 및 구조화 - return self._parse_generated_content(generated_content, request) + return result except Exception as e: - self.logger.error(f"콘텐츠 생성 실패: {e}") + logger.error(f"콘텐츠 생성 실패: {e}") return self._create_fallback_content(request) def _prepare_content_context(self, request: RequestBlogCreate) -> str: diff --git a/apps/pre-processing-service/app/utils/llm_extractor.py b/apps/pre-processing-service/app/utils/llm_extractor.py index 3fb200a5..7140b184 100644 --- a/apps/pre-processing-service/app/utils/llm_extractor.py +++ b/apps/pre-processing-service/app/utils/llm_extractor.py @@ -1,8 +1,7 @@ import os from openai import OpenAI from dotenv import load_dotenv - -load_dotenv() +from app.core.config import settings class LLMExtractor: @@ -13,7 +12,7 @@ def __init__(self, model="gpt-4o"): :param model: 사용할 LLM 모델 이름 """ - self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + self.client = OpenAI(api_key=settings.OPENAI_API_KEY) self.model = model def login_extraction_prompt(self, target_description: str, html: str): diff --git a/apps/user-service/build.gradle b/apps/user-service/build.gradle index 7d4d7e0a..94750654 100644 --- a/apps/user-service/build.gradle +++ b/apps/user-service/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'site.icebang' -version = '0.0.1-alpha-SNAPSHOT' +version = '0.0.1-beta-STABLE' description = 'Ice bang - fast campus team4' java { @@ -21,7 +21,6 @@ configurations { compileOnly { extendsFrom annotationProcessor } - // Spring Boot의 기본 로깅(Logback) 제외 all { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' } @@ -50,6 +49,7 @@ dependencies { // Retry implementation 'org.springframework.retry:spring-retry' + // logging implementation 'org.springframework.boot:spring-boot-starter-log4j2' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' implementation 'org.apache.logging.log4j:log4j-layout-template-json' @@ -57,11 +57,8 @@ dependencies { implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'org.apache.httpcomponents:httpcore:4.4.16' - // 비동기 로깅 -// implementation 'com.lmax:disruptor:3.4.4' -// implementation 'org.apache.commons:commons-dbcp2' -// implementation 'org.apache.commons:commons-pool2' + // micrometer & actuator implementation "io.micrometer:micrometer-tracing-bridge-brave" implementation "io.micrometer:micrometer-tracing" implementation 'io.micrometer:micrometer-registry-prometheus' diff --git a/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java b/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java index 0f99e59b..8a986b4d 100644 --- a/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java +++ b/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java @@ -4,15 +4,72 @@ import lombok.Data; +/** + * 공통 APi 응답 DTO 클래스입니다. + * + *
REST API의 응답 형식을 표준화하기 위해 사용됩니다. 모든 응답은 성공 여부({@link #success}), 응답 데이터({@link #data}), 응답 + * 메시지({@link #message}), 그리고 HTTP 상태 코드({@link #status})를 포함합니다. + * + *
사용 예시: + * + *
{@code
+ * // 성공 응답 생성
+ * ApiResponse response = ApiResponse.success(userDto);
+ *
+ * // 메시지를 포함한 성공 응답
+ * ApiResponse response = ApiResponse.success(userDto, "회원 조회 성공");
+ *
+ * // 오류 응답 생성
+ * ApiResponse errorResponse = ApiResponse.error("잘못된 요청입니다.", HttpStatus.BAD_REQUEST);
+ * }
+ *
+ * @param true: 요청이 정상적으로 처리됨 false: 요청 처리 중 오류 발생 + */ private boolean success; + + /** + * 실제 응답 데이터(payload). + * + *
요청이 성공적으로 처리되었을 경우 반환되는 데이터이며, 실패 시에는 {@code null}일 수 있습니다. + */ private T data; + + /** + * 응답 메세지. + * + *
성공 또는 오류 상황을 설명하는 메시지를 담습니다. 클라이언트에서 사용자에게 직접 표시할 수도 있습니다. + */ private String message; + + /** + * HTTP 상태 코드. + * + *
Spring의 {@link HttpStatus} 열거형을 사용합니다.
+ */
private HttpStatus status; // HttpStatus로 변경
+ /** 기본 생성자입니다. 모든 필드가 기본값으로 초기화됩니다. */
public ApiResponse() {}
+ /**
+ * 모든 필드를 초기화하는 생성자.
+ *
+ * @param success 요청 성공 여부
+ * @param data 응답 데이터
+ * @param message 응답 메시지
+ * @param status HTTP 상태 코드
+ */
public ApiResponse(boolean success, T data, String message, HttpStatus status) {
this.success = success;
this.data = data;
@@ -20,18 +77,50 @@ public ApiResponse(boolean success, T data, String message, HttpStatus status) {
this.status = status;
}
+ /**
+ * 성공 응답을 생성합니다. (기본 메시지: "OK", 상태: 200 OK)
+ *
+ * @param data 응답 데이터
+ * @param 목록 조회 API에서 공통적으로 사용되는 요청 파라미터를 정의합니다. 현재 페이지 번호({@link #current}), 페이지 크기({@link #pageSize}),
+ * 검색어({@link #search}), 정렬 조건({@link #sorters}), 필터 조건({@link #filters})를 포함합니다.
+ *
+ * 사용 예시:
+ *
+ * 1부터 시작하며, 기본값은 1입니다. 0 이하의 값은 유효하지 않습니다.
+ */
private int current = 1;
+
+ /**
+ * 한 펭지에 표시할 데이터 개수.
+ *
+ * 한 페이지에 표시할 항목의 개수를 지정합니다. 기본값은 10개이며, 일반적으로 10, 20, 50, 100 등의 값을 사용합니다.
+ */
private int pageSize = 10;
+
+ /**
+ * 검색어.
+ *
+ * 목록에서 특정 조건으로 검색할 때 사용되는 키워드입니다. {@code null}이거나 빈 문자열인 경우 검색 조건이 적용되지 않습니다.
+ */
private String search;
+
+ /**
+ * 정렬 조건 배열.
+ *
+ * 예: {@code ["name:asc", "createdAt:desc"]} API 설계에 따라 "필드명:정렬방향" 형식을 권장합니다. {@code null}이거나 빈
+ * 배열인 경우 기본 정렬이 적용됩니다.
+ */
private String[] sorters;
+
+ /**
+ * 필터링 조건 배열.
+ *
+ * 예: {@code ["status:active", "role:admin"]} 각 요소는 특정 필드에 대한 필터링 조건을 나타냅니다. 형태는 구현에 따라 다를 수
+ * 있습니다.
+ */
private String[] filters;
- // 계산된 offset
+ /**
+ * 페이징 처리를 위한 offset(시작 위치)을 계산합니다.
+ *
+ * @return (current - 1) * pageSize
+ */
public int getOffset() {
return (current - 1) * pageSize;
}
+ /**
+ * 검색어가 유효하게 존재하는지 확인합니다.
+ *
+ * @return 검색어가 null이 아니고 공백이 아닌 경우 true
+ */
public boolean hasSearch() {
return search != null && !search.trim().isEmpty();
}
+ /**
+ * 정렬 조건이 존재하는지 확인합니다.
+ *
+ * @return 정렬 조건 배열이 null이 아니고, 1개 이상 있는 경우 true
+ */
public boolean hasSorters() {
return sorters != null && sorters.length > 0;
}
diff --git a/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java b/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java
index 4a2a8bfa..0982be0a 100644
--- a/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java
+++ b/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java
@@ -6,17 +6,67 @@
import lombok.Data;
import lombok.NoArgsConstructor;
+/**
+ * 페이징 처리된 결과 DTO 클래스.
+ *
+ * 목록 조회 API에서 페이징된 데이터를 반환할 때 사용됩니다. 실제 데이터 목록({@link #data}), 전체 개수({@link #total}), 현재 페이지
+ * 번호({@link #current}), 페이지 크기({@link #pageSize}), 전체 페이지 수({@link #totalPages}), 다음/이전 페이지 여부를
+ * 포함합니다.
+ *
+ * 사용 예시:
+ *
+ * totalPages, hasNext, hasPrevious 값을 설정합니다.
+ */
private void calculatePagination() {
this.totalPages = total > 0 ? (int) Math.ceil((double) total / pageSize) : 0;
this.hasNext = current < totalPages;
this.hasPrevious = current > 1;
}
- // 기존 of 메서드
+ /**
+ * PageResult 객체를 생성합니다.
+ *
+ * @param data 현재 페이지 데이터
+ * @param total 전체 데이터 개수
+ * @param current 현재 페이지 번호
+ * @param pageSize 페이지 크기
+ * @param 데이터 조회와 카운트 조회를 별도의 Supplier로 받아 트랜잭션 내에서 실행할 수 있습니다.
+ *
+ * @param pageParams 요청 파라미터 ({@link PageParams})
+ * @param dataSupplier 데이터 조회 함수
+ * @param countSupplier 전체 개수 조회 함수
+ * @param 이 예외는 데이터베이스나 컬렉션에서 이미 존재하는 데이터를 중복해서 생성하거나 저장하려고 할 때 발생합니다. 주로 유니크 제약 조건 위반이나 비즈니스 로직상 중복을
+ * 허용하지 않는 경우에 사용됩니다.
+ *
+ * {@code cause}와 연관된 상세 메시지가 이 예외의 상세 메시지에 자동으로 포함되지는 않습니다.
+ *
+ * @param message 상세 메시지 (나중에 {@link Throwable#getMessage()} 메서드로 검색됨)
+ * @param cause 원인 (나중에 {@link Throwable#getCause()} 메서드로 검색됨). {@code null} 값이 허용되며, 원인이 존재하지 않거나
+ * 알 수 없음을 나타냄
+ * @since v0.0.1-alpha
+ */
public DuplicateDataException(String message, Throwable cause) {
super(message, cause);
}
+ /**
+ * 지정된 원인과 상세 메시지와 함께 새로운 {@code DuplicateDataException}을 생성합니다. 상세 메시지는 {@code (cause==null ?
+ * null : cause.toString())}로 설정됩니다. (일반적으로 {@code cause}의 클래스와 상세 메시지를 포함)
+ *
+ * @param cause 원인 (나중에 {@link Throwable#getCause()} 메서드로 검색됨). {@code null} 값이 허용되며, 원인이 존재하지 않거나
+ * 알 수 없음을 나타냄
+ * @since v0.0.1-alpha
+ */
public DuplicateDataException(Throwable cause) {
super(cause);
}
+ /**
+ * 지정된 상세 메시지, 원인, suppression 활성화 여부, 그리고 writable stack trace 여부와 함께 새로운 {@code
+ * DuplicateDataException}을 생성합니다.
+ *
+ * @param message 상세 메시지
+ * @param cause 원인. ({@code null} 값이 허용되며, 원인이 존재하지 않거나 알 수 없음을 나타냄)
+ * @param enableSuppression suppression이 활성화되는지 또는 비활성화되는지 여부
+ * @param writableStackTrace stack trace가 writable해야 하는지 여부
+ * @since v0.0.1-alpha
+ */
protected DuplicateDataException(
String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
diff --git a/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java b/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java
index 25d41d29..da3ae215 100644
--- a/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java
+++ b/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java
@@ -3,6 +3,42 @@
import site.icebang.common.dto.PageParams;
import site.icebang.common.dto.PageResult;
+/**
+ * 페이징 가능한 서비스 인터페이스.
+ *
+ * 엔티티나 DTO 목록을 페이징 처리하여 반환해야 하는 서비스에서 구현합니다. 공통적으로 {@link PageParams} 요청 파라미터를 받아 {@link
+ * PageResult} 응답을 제공합니다.
+ *
+ * 사용 예시:
+ *
+ * 이 클래스는 소문자, 대문자, 숫자, 특수문자를 포함한 강력한 패스워드를 생성합니다. 생성된 패스워드는 각 문자 유형을 최소 하나씩 포함하도록 보장됩니다.
+ *
+ * 생성되는 패스워드는 다음 규칙을 준수합니다:
+ *
+ * 이는 {@code generate(12)}를 호출하는 것과 동일합니다.
+ *
+ * @return 12자 길이의 랜덤 패스워드
+ * @see #generate(int)
+ * @since v0.0.1-alpha
+ */
public String generate() {
return generate(12); // 기본 길이를 12로 설정
}
diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java
index d8348e7e..667637b1 100644
--- a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java
+++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java
@@ -7,12 +7,39 @@
import site.icebang.domain.schedule.model.Schedule;
import site.icebang.domain.workflow.scheduler.WorkflowTriggerJob;
+/**
+ * Spring Quartz 스케줄러의 Job과 Trigger를 동적으로 관리하는 서비스 클래스입니다.
+ *
+ * 이 서비스는 데이터베이스에 정의된 {@code Schedule} 정보를 바탕으로,
+ * Quartz 엔진에 실제 실행 가능한 작업을 등록, 수정, 삭제하는 역할을 담당합니다.
+ *
+ * 지정된 워크플로우 ID에 해당하는 Job이 이미 존재할 경우, 기존 Job과 Trigger를 삭제하고
+ * 새로운 정보로 다시 생성하여 스케줄을 업데이트합니다. {@code JobDataMap}을 통해
+ * 실행될 Job에게 어떤 워크플로우를 실행해야 하는지 ID를 전달합니다.
+ *
+ * @param schedule Quartz에 등록할 스케줄 정보를 담은 도메인 모델 객체
+ * @since v0.1.0
+ */
public void addOrUpdateSchedule(Schedule schedule) {
try {
JobKey jobKey = JobKey.jobKey("workflow-" + schedule.getWorkflowId());
@@ -40,6 +67,12 @@ public void addOrUpdateSchedule(Schedule schedule) {
}
}
+ /**
+ * 지정된 워크플로우 ID와 연결된 Quartz 스케줄을 삭제합니다.
+ *
+ * @param workflowId 삭제할 스케줄에 연결된 워크플로우의 ID
+ * @since v0.1.0
+ */
public void deleteSchedule(Long workflowId) {
try {
JobKey jobKey = JobKey.jobKey("workflow-" + workflowId);
diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java
index e61faa75..38c1ae38 100644
--- a/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java
+++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/manager/ExecutionMdcManager.java
@@ -7,6 +7,13 @@
public class ExecutionMdcManager {
private static final String SOURCE_ID = "sourceId";
private static final String EXECUTION_TYPE = "executionType";
+ private static final String TRACE_ID = "traceID";
+
+ public void setWorkflowContext(Long workflowId, String traceId) {
+ MDC.put(SOURCE_ID, workflowId.toString());
+ MDC.put(EXECUTION_TYPE, "WORKFLOW");
+ MDC.put(TRACE_ID, traceId);
+ }
public void setWorkflowContext(Long workflowId) {
MDC.put(SOURCE_ID, workflowId.toString());
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 d22e2a68..aec0bb36 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
@@ -10,53 +10,61 @@
import site.icebang.domain.workflow.dto.WorkflowHistoryDTO;
import site.icebang.domain.workflow.dto.WorkflowRunDto;
+/**
+ * 워크플로우 실행 히스토리 관련 데이터베이스 매퍼 인터페이스입니다.
+ *
+ * 워크플로우, 작업(Job), 태스크(Task)의 실행 기록과 관련된 데이터 조회를 담당합니다.
+ *
+ * @author jys01012@gmail.com
+ * @since v0.0.1-beta
+ */
@Mapper
public interface WorkflowHistoryMapper {
/**
- * 워크플로우 실행 정보 조회
+ * 워크플로우 실행 정보를 조회합니다.
*
* @param runId workflow_run.id
- * @return WorkflowRunDto
+ * @return 워크플로우 실행 정보
*/
WorkflowRunDto selectWorkflowRun(Long runId);
/**
- * 워크플로우 실행의 Job 목록 조회
+ * 워크플로우 실행의 작업 목록을 조회합니다.
*
* @param workflowRunId workflow_run.id
- * @return List 이 인터페이스는 전략 패턴(Strategy Pattern)의 '전략(Strategy)' 역할을 수행합니다. {@code WorkflowExecutionService}는
+ * 이 인터페이스에 의존하여, Task의 타입('FastAPI' 등)에 따라 적절한 Runner 구현체를 선택하고 실행 로직을 위임합니다.
+ *
+ * 실행의 성공/실패 여부(status)와 결과 메시지(message)를 표준화된 방식으로 반환합니다.
+ *
+ * @param status 실행 상태 ("SUCCESS" 또는 "FAILED")
+ * @param message 실행 결과 (성공 시 응답 Body, 실패 시 에러 메시지)
+ * @since v0.1.0
+ */
record TaskExecutionResult(String status, String message) {
public static TaskExecutionResult success(String message) {
return new TaskExecutionResult("SUCCESS", message);
}
+ /**
+ * 실패 결과를 생성하는 정적 팩토리 메소드입니다.
+ *
+ * @param message 실패 원인 메시지
+ * @return status가 "FAILED"로 설정된 결과 객체
+ */
public static TaskExecutionResult failure(String message) {
return new TaskExecutionResult("FAILED", message);
}
+ /**
+ * 해당 결과가 실패했는지 여부를 반환합니다.
+ *
+ * @return 실패했다면 true, 아니면 false
+ */
public boolean isFailure() {
return "FAILED".equals(this.status);
}
@@ -26,10 +60,11 @@ public boolean isFailure() {
/**
* 특정 Task를 실행합니다.
*
- * @param task 실행할 Task의 정적 정의
- * @param taskRun 현재 실행에 대한 기록 객체
- * @param requestBody 동적으로 생성된 요청 데이터
- * @return Task 실행 결과
+ * @param task 실행할 Task의 정적 정의 (이름, 타입, 파라미터 등)
+ * @param taskRun 현재 실행에 대한 DB 기록 객체 (ID 추적 등에 사용)
+ * @param requestBody {@code TaskBodyBuilder}에 의해 동적으로 생성된 최종 요청 Body
+ * @return Task 실행 결과를 담은 {@code TaskExecutionResult} 객체
+ * @since v0.1.0
*/
TaskExecutionResult execute(Task task, TaskRun taskRun, ObjectNode requestBody);
}
diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/FastApiTaskRunner.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/FastApiTaskRunner.java
index 136a9d93..95a0e89c 100644
--- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/FastApiTaskRunner.java
+++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/fastapi/FastApiTaskRunner.java
@@ -13,12 +13,43 @@
import site.icebang.domain.workflow.runner.TaskRunner;
import site.icebang.external.fastapi.adapter.FastApiAdapter;
+/**
+ * FastAPI 서버와 통신하는 Task를 실행하는 구체적인 Runner 구현체입니다.
+ *
+ * 이 클래스는 {@code TaskRunner} 인터페이스를 구현하며, Task의 타입이 'FastAPI'일 때 선택됩니다. 실제 HTTP 통신은 {@code
+ * FastApiAdapter}에 위임하고, 이 클래스는 워크플로우의 {@code Task} 객체를 {@code FastApiAdapter}가 이해할 수 있는 호출 형식으로
+ * 변환하는 **어댑터(Adapter)** 역할을 수행합니다.
+ *
+ * Task의 파라미터에서 엔드포인트와 HTTP 메소드를 추출하고, {@code WorkflowExecutionService}로부터 전달받은 동적 Request
+ * Body를 사용하여 {@code FastApiAdapter}를 호출합니다.
+ *
+ * @param task 실행할 Task의 정적 정의
+ * @param taskRun 현재 실행에 대한 기록 객체
+ * @param requestBody {@code TaskBodyBuilder}에 의해 동적으로 생성된 최종 요청 Body
+ * @return {@code FastApiAdapter}의 호출 결과를 담은 {@code TaskExecutionResult} 객체
+ * @since v0.1.0
+ */
@Override
public TaskExecutionResult execute(Task task, TaskRun taskRun, ObjectNode requestBody) {
JsonNode params = task.getParameters();
diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java
index 196c1fa0..a3076d1f 100644
--- a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java
+++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java
@@ -9,12 +9,38 @@
import site.icebang.domain.workflow.service.WorkflowExecutionService;
+/**
+ * Spring Quartz 스케줄러에 의해 실행되는 실제 작업(Job) 클래스입니다.
+ *
+ * 이 클래스는 Quartz의 스케줄링 세계와 애플리케이션의 비즈니스 로직을 연결하는 **브릿지(Bridge)** 역할을 수행합니다. Quartz의 Trigger가 정해진
+ * 시간에 발동하면, Quartz 엔진은 이 Job을 인스턴스화하고 {@code executeInternal} 메소드를 호출합니다.
+ *
+ * 이 메소드는 실행 컨텍스트({@code JobExecutionContext})에서 {@code JobDataMap}을 통해 스케줄 등록 시점에 저장된
+ * 'workflowId'를 추출합니다. 그 후, 해당 ID를 파라미터로 하여 {@code WorkflowExecutionService}의 {@code
+ * executeWorkflow} 메소드를 호출하여 실제 비즈니스 로직의 실행을 시작합니다.
+ *
+ * @param context Quartz가 제공하는 현재 실행에 대한 런타임 정보. JobDetail과 Trigger 정보를 포함합니다.
+ * @since v0.1.0
+ */
@Override
protected void executeInternal(JobExecutionContext context) {
Long workflowId = context.getJobDetail().getJobDataMap().getLong("workflowId");
diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/TaskExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/TaskExecutionService.java
index 80cf44a3..29f28d98 100644
--- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/TaskExecutionService.java
+++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/TaskExecutionService.java
@@ -13,20 +13,50 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import site.icebang.domain.workflow.model.Task;
import site.icebang.domain.workflow.model.TaskRun;
import site.icebang.domain.workflow.runner.TaskRunner;
-@Slf4j
+/**
+ * 워크플로우 내 개별 Task의 실행과 재시도 정책을 전담하는 서비스입니다.
+ *
+ * 이 클래스는 {@code WorkflowExecutionService}로부터 Task 실행 책임을 위임받습니다. Spring AOP의 '자기
+ * 호출(Self-invocation)' 문제를 회피하고, 재시도 로직을 비즈니스 흐름과 분리하기 위해 별도의 서비스로 구현되었습니다.
+ *
+ * HTTP 통신 오류 등 {@code RestClientException} 발생 시, 5초의 고정된 간격({@code Backoff})으로 최대 3회({@code
+ * maxAttempts})까지 실행을 재시도합니다. 지원하지 않는 Task 타입의 경우 재시도 없이 즉시 {@code IllegalArgumentException}을
+ * 발생시킵니다.
+ *
+ * @param task 실행할 Task의 도메인 모델
+ * @param taskRun 현재 실행에 대한 기록 객체
+ * @param requestBody 동적으로 생성된 요청 Body
+ * @return Task 실행 결과
+ * @throws IllegalArgumentException 지원하지 않는 Task 타입일 경우
+ * @since v0.1.0
+ */
@Retryable(
value = {RestClientException.class},
maxAttempts = 3,
@@ -45,7 +75,19 @@ public TaskRunner.TaskExecutionResult executeWithRetry(
return runner.execute(task, taskRun, requestBody);
}
- /** 모든 재시도가 실패했을 때 마지막으로 호출될 복구 메소드입니다. */
+ /**
+ * {@code @Retryable} 재시도가 모두 실패했을 때 호출되는 복구 메소드입니다.
+ *
+ * 이 메소드는 {@code executeWithRetry} 메소드와 동일한 파라미터 시그니처를 가지며, 발생한 예외를 첫 번째 파라미터로 추가로 받습니다. 최종 실패
+ * 상태를 기록하고 실패 결과를 반환하는 역할을 합니다.
+ *
+ * @param e 재시도를 유발한 마지막 예외 객체
+ * @param task 실패한 Task의 도메인 모델
+ * @param taskRun 실패한 실행의 기록 객체
+ * @param requestBody 실패 당시 사용된 요청 Body
+ * @return 최종 실패를 나타내는 Task 실행 결과
+ * @since v0.1.0
+ */
@Recover
public TaskRunner.TaskExecutionResult recover(
RestClientException e, Task task, TaskRun taskRun, ObjectNode requestBody) {
diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java
index a27807ec..3421c043 100644
--- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java
+++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java
@@ -48,11 +48,12 @@ public class WorkflowExecutionService {
@Transactional
@Async("traceExecutor")
public void executeWorkflow(Long workflowId) {
- mdcManager.setWorkflowContext(workflowId);
+ WorkflowRun workflowRun = WorkflowRun.start(workflowId);
+ workflowRunMapper.insert(workflowRun);
+
+ mdcManager.setWorkflowContext(workflowId, workflowRun.getTraceId());
try {
workflowLogger.info("========== 워크플로우 실행 시작: WorkflowId={} ==========", workflowId);
- WorkflowRun workflowRun = WorkflowRun.start(workflowId);
- workflowRunMapper.insert(workflowRun);
Map 이 서비스는 워크플로우 실행 목록의 페이징 처리, 특정 실행 건의 상세 정보 조회 등 읽기 전용(Read-Only) 기능에 집중합니다.
+ *
+ * 이 메소드는 {@code PageableService} 인터페이스를 구현하며, {@code PageResult} 유틸리티를 사용하여 전체 카운트 쿼리와 목록 조회
+ * 쿼리를 실행하고 페이징 결과를 생성합니다.
+ *
+ * @param pageParams 페이징 처리에 필요한 파라미터 (페이지 번호, 페이지 크기 등)
+ * @return 페이징 처리된 워크플로우 실행 이력 목록
+ * @see PageResult
+ * @since v0.1.0
*/
@Override
@Transactional(readOnly = true)
@@ -35,10 +60,14 @@ public PageResult 지정된 실행 ID(`runId`)에 해당하는 워크플로우 실행 정보와, 그에 속한 모든 Job 실행 정보, 그리고 각 Job에 속한 모든 Task 실행 정보를
+ * 계층적으로 조회하여 반환합니다.
*
- * @param runId workflow_run.id
- * @return WorkflowRunDetailResponse
+ * @param runId 조회할 워크플로우 실행의 ID (`workflow_run.id`)
+ * @return 워크플로우, Job, Task 실행 정보를 포함하는 상세 응답 객체
+ * @since v0.1.0
*/
@Transactional(readOnly = true)
public WorkflowRunDetailResponse getWorkflowRunDetail(Long runId) {
@@ -69,10 +98,11 @@ public WorkflowRunDetailResponse getWorkflowRunDetail(Long runId) {
}
/**
- * 워크플로우 실행 로그 조회
+ * 특정 워크플로우 실행과 관련된 모든 로그를 조회합니다.
*
- * @param runId workflow_run.id
- * @return WorkflowRunLogsResponse
+ * @param runId 조회할 워크플로우 실행의 ID (`workflow_run.id`)
+ * @return 워크플로우 실행 로그 응답 객체
+ * @since v0.1.0
*/
public WorkflowRunLogsResponse getWorkflowRunLogs(Long runId) {
// TODO: 구현 예정
@@ -80,10 +110,11 @@ public WorkflowRunLogsResponse getWorkflowRunLogs(Long runId) {
}
/**
- * TraceId로 워크플로우 실행 조회
+ * Trace ID를 사용하여 특정 워크플로우 실행 정보를 조회합니다.
*
- * @param traceId workflow_run.trace_id
- * @return WorkflowRunDetailResponse
+ * @param traceId 조회할 워크플로우 실행의 Trace ID (`workflow_run.trace_id`)
+ * @return 워크플로우 실행 상세 응답 객체
+ * @since v0.1.0
*/
public WorkflowRunDetailResponse getWorkflowRunByTraceId(String traceId) {
// TODO: 구현 예정
diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java
index b994c82e..e8c857f3 100644
--- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java
+++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowService.java
@@ -17,12 +17,38 @@
import site.icebang.domain.workflow.dto.WorkflowDetailCardDto;
import site.icebang.domain.workflow.mapper.WorkflowMapper;
+/**
+ * 워크플로우의 '정의'와 관련된 비즈니스 로직을 처리하는 서비스 클래스입니다.
+ *
+ * 이 서비스는 워크플로우의 실행(Execution)이 아닌, 생성된 워크플로우의 구조를 조회하는 기능에 집중합니다.
+ *
+ * 이 메소드는 {@code PageableService} 인터페이스를 구현하며, {@code PageResult} 유틸리티를 사용하여 전체 카운트 쿼리와 목록 조회
+ * 쿼리를 실행하고 페이징 결과를 생성합니다.
+ *
+ * @param pageParams 페이징 처리에 필요한 파라미터 (페이지 번호, 페이지 크기 등)
+ * @return 페이징 처리된 워크플로우 카드 목록
+ * @see PageResult
+ * @since v0.1.0
+ */
@Override
@Transactional(readOnly = true)
public PageResult 지정된 워크플로우 ID에 해당하는 기본 정보, 연결된 스케줄 목록, 그리고 Job과 Task의 계층 구조를 모두 조회하여 하나의 DTO로 조합하여 반환합니다.
+ *
+ * @param workflowId 조회할 워크플로우의 ID
+ * @return 워크플로우의 전체 구조를 담은 상세 DTO
+ * @throws IllegalArgumentException 주어진 ID에 해당하는 워크플로우가 존재하지 않을 경우
+ * @since v0.1.0
+ */
@Transactional(readOnly = true)
public WorkflowDetailCardDto getWorkflowDetail(BigInteger workflowId) {
diff --git a/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java b/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java
index 2a5bd001..6951bab8 100644
--- a/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java
+++ b/apps/user-service/src/main/java/site/icebang/external/fastapi/adapter/FastApiAdapter.java
@@ -1,5 +1,6 @@
package site.icebang.external.fastapi.adapter;
+import org.slf4j.MDC;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;
@@ -10,6 +11,24 @@
import site.icebang.global.config.properties.FastApiProperties;
+/**
+ * 외부 FastAPI 서버와의 모든 HTTP 통신을 전담하는 어댑터 클래스입니다.
+ *
+ * 이 클래스는 내부 시스템의 다른 부분들이 외부 시스템의 상세한 통신 방법을 알 필요가 없도록 HTTP 요청/응답 로직을 캡슐화합니다. {@code
+ * RestTemplate}을 사용하여 실제 통신을 수행하며, 모든 FastAPI 요청은 이 클래스의 {@code call} 메소드를 통해 이루어져야 합니다.
+ *
+ * 지정된 엔드포인트, HTTP 메소드, 요청 Body를 사용하여 외부 API를 호출합니다. 통신 성공 시 응답 Body를 문자열로 반환하고, 실패 시 에러 로그를
+ * 남기고 null을 반환합니다.
+ *
+ * @param endpoint 호출할 엔드포인트 경로 (예: "/keywords/search")
+ * @param method 사용할 HTTP 메소드 (예: HttpMethod.POST)
+ * @param requestBody 요청에 담을 JSON 문자열
+ * @return 성공 시 API 응답 Body 문자열, 실패 시 null
+ * @see RestTemplate
+ * @since v0.1.0
+ */
public String call(String endpoint, HttpMethod method, String requestBody) {
String fullUrl = properties.getUrl() + endpoint;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
+
+ String traceId = MDC.get("traceId");
+ if (traceId != null) {
+ headers.set("X-Request-ID", traceId);
+ log.debug("TraceID 헤더 추가: {}", traceId);
+ }
+
HttpEntity 이 클래스는 {@code CommandLineRunner}를 구현하여, Spring Boot 애플리케이션이 완전히
+ * 로드된 후 단 한 번 실행됩니다. 데이터베이스의 {@code schedule} 테이블을 'Source of Truth'로 삼아,
+ * 활성화된 모든 스케줄을 읽어와 Quartz 엔진에 동기화하는 매우 중요한 역할을 수행합니다.
+ *
+ * 데이터베이스에서 활성화된 모든 스케줄을 조회하고, 각 스케줄을
+ * {@code QuartzScheduleService}를 통해 Quartz 스케줄러에 등록합니다.
+ *
+ * @param args 애플리케이션 실행 시 전달되는 인자
+ * @since v0.1.0
+ */
@Override
public void run(String... args) {
log.info("Quartz 스케줄러 초기화 시작: DB 스케줄을 등록합니다.");
diff --git a/apps/user-service/src/main/java/site/icebang/global/config/WebConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/WebConfig.java
index 43cfd8b1..7029b7d9 100644
--- a/apps/user-service/src/main/java/site/icebang/global/config/WebConfig.java
+++ b/apps/user-service/src/main/java/site/icebang/global/config/WebConfig.java
@@ -8,9 +8,37 @@
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
+/**
+ * 애플리케이션의 웹 관련 설정을 담당하는 Java 기반 설정 클래스입니다.
+ *
+ * 이 클래스는 애플리케이션 전역에서 사용될 웹 관련 빈(Bean)들을 생성하고 구성합니다. 현재는 외부 API 통신을 위한 {@code RestTemplate} 빈을
+ * 중앙에서 관리하는 역할을 합니다.
+ *
+ * 기본 {@code RestTemplateBuilder}를 사용하되, 커넥션 및 읽기 타임아웃을 각각 30초로 명시적으로 설정하기 위해 {@code
+ * SimpleClientHttpRequestFactory}를 구성하여 주입합니다. 이렇게 생성된 RestTemplate 빈은 애플리케이션의 다른 컴포넌트에서 주입받아 외부
+ * 시스템과의 HTTP 통신에 사용됩니다.
+ *
+ * @param builder Spring Boot가 자동으로 구성해주는 RestTemplateBuilder 객체
+ * @return 타임아웃이 설정된 RestTemplate 인스턴스
+ * @see RestTemplate
+ * @see RestTemplateBuilder
+ * @since v0.1.0
+ */
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// 1. SimpleClientHttpRequestFactory 객체를 직접 생성
diff --git a/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java b/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java
index 4079c9f3..3def3d9d 100644
--- a/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java
+++ b/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java
@@ -13,11 +13,42 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+/**
+ * MyBatis에서 Jackson 라이브러리의 {@code JsonNode} 타입을 데이터베이스의 문자열 타입(예: VARCHAR, JSON)과 매핑하기 위한 커스텀 타입
+ * 핸들러입니다.
+ *
+ * 이 핸들러를 통해, 애플리케이션에서는 JSON 데이터를 편리하게 {@code JsonNode} 객체로 다루고, 데이터베이스에는 해당 객체를 JSON 문자열 형태로
+ * 저장하거나 읽어올 수 있습니다.
+ *
+ * 이 클래스는 {@code @ConfigurationProperties}를 통해 'api.fastapi' 경로의 설정값을 자동으로 주입받습니다.
+ * {@code @Validated}와 {@code @NotBlank}를 사용하여, 필수 설정값(url)이 누락될 경우 애플리케이션 시작 시점에 즉시 에러를 발생시켜 설정 오류를
+ * 방지합니다.
+ *
+ * {@code @NotBlank} 어노테이션이 적용되어 있어, application.yml 파일에 반드시 값이 존재해야 합니다. (예:
+ * "http://host.docker.internal:8000")
+ */
@NotBlank // 값이 비어있을 수 없음을 검증
private String url;
- /** API 호출 시 적용될 타임아웃 (밀리초 단위) */
+ /**
+ * API 호출 시 적용될 타임아웃 시간 (밀리초 단위)입니다.
+ *
+ * 별도로 설정하지 않을 경우 기본값으로 5000ms (5초)가 적용됩니다.
+ */
private int timeout = 5000; // 기본값 5초 설정
}
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 3543a8dd..514998e2 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
@@ -82,6 +82,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.permitAll()
.requestMatchers("/v0/auth/check-session")
.authenticated()
+ .requestMatchers(SecurityEndpoints.SUPER_ADMIN.getMatchers())
+ .hasAnyRole("SUPER_ADMIN")
.requestMatchers(SecurityEndpoints.DATA_ADMIN.getMatchers())
.hasRole("SUPER_ADMIN") // hasAuthority -> hasRole
.requestMatchers(SecurityEndpoints.DATA_ENGINEER.getMatchers())
diff --git a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java
index e6e24243..98129d8b 100644
--- a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java
+++ b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java
@@ -11,7 +11,6 @@ public enum SecurityEndpoints {
"/js/**",
"/images/**",
"/v0/organizations/**",
- "/v0/auth/register",
"/v0/check-execution-log-insert"),
// 데이터 관리 관련 엔드포인트
@@ -27,7 +26,9 @@ public enum SecurityEndpoints {
OPS("/api/scheduler/**", "/api/monitoring/**"),
// 일반 사용자 엔드포인트
- USER("/user/**", "/profile/**", "/v0/auth/check-session", "/v0/workflows/**");
+ USER("/user/**", "/profile/**", "/v0/auth/check-session", "/v0/workflows/**"),
+
+ SUPER_ADMIN("/v0/auth/register");
private final String[] patterns;
diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java
index 8243acde..485e7e1e 100644
--- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java
+++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java
@@ -1,8 +1,11 @@
package site.icebang.global.handler.exception;
+import java.util.stream.Collectors;
+
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
+import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@@ -14,16 +17,52 @@
import site.icebang.common.dto.ApiResponse;
import site.icebang.common.exception.DuplicateDataException;
+/**
+ * 전역 예외 처리기 (Global Exception Handler).
+ *
+ * 이 클래스는 애플리케이션 전역에서 발생하는 예외를 {@link ApiResponse} 형태로 변환하여 클라이언트에게 반환합니다. 예외 유형에 따라 적절한 {@link
+ * HttpStatus} 코드를 설정하며, 공통적인 예외 처리 로직을 중앙화합니다.
+ *
+ * 처리되는 주요 예외는 다음과 같습니다:
+ *
+ * 모든 응답은 {@code ApiResponse.error(...)} 메서드를 통해 생성되며, 에러 메시지와 HTTP 상태 코드가 포함됩니다.
+ */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
+
+ /**
+ * 요청 데이터 유효성 검증 실패 시 발생하는 예외를 처리합니다.
+ *
+ * @param ex 발생한 {@link MethodArgumentNotValidException}
+ * @return {@link ApiResponse} - 검증 실패 메시지와 {@link HttpStatus#BAD_REQUEST}
+ */
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
- public ApiResponse Spring Security에서 인가(Authorization) 실패 시 호출됩니다. 사용자가 필요한 권한 없이 보호된 리소스에 접근하려고 하면 이 핸들러가 실행되어
+ * JSON 형식의 에러 응답을 반환합니다.
+ *
+ * 주요 특징:
+ *
+ * 이 핸들러는 기본 HTML 오류 페이지 대신, REST API 클라이언트에 JSON 기반의 표준 에러 응답을 제공하기 위해 사용됩니다.
+ */
@Component
@RequiredArgsConstructor
public class RestAccessDeniedHandler implements AccessDeniedHandler {
+
+ /** JSON 직렬화를 위한 ObjectMapper */
private final ObjectMapper objectMapper;
+ /**
+ * 인가되지 않은 요청이 들어왔을 때 실행됩니다.
+ *
+ * @param request 현재 요청
+ * @param response 응답 객체
+ * @param ex 발생한 {@link AccessDeniedException}
+ * @throws IOException 응답 스트림 처리 중 오류 발생 시
+ */
@Override
public void handle(
HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java
index b7c50d76..9d3ec7b5 100644
--- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java
+++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java
@@ -15,11 +15,37 @@
import site.icebang.common.dto.ApiResponse;
+/**
+ * 인증 진입점 처리기 (REST 전용 AuthenticationEntryPoint).
+ *
+ * Spring Security에서 인증(Authentication) 실패 시 호출됩니다. 인증되지 않은 사용자가 보호된 리소스에 접근하려고 하면 이 핸들러가 실행되어
+ * JSON 형식의 에러 응답을 반환합니다.
+ *
+ * 주요 특징:
+ *
+ * 이 핸들러는 기본 로그인 페이지 리다이렉트 대신, REST API 클라이언트에 JSON 기반의 표준 에러 응답을 제공하기 위해 사용됩니다.
+ */
@Component
@RequiredArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+ /** JSON 직렬화를 위한 ObjectMapper */
private final ObjectMapper objectMapper;
+ /**
+ * 인증되지 않은 요청이 들어왔을 때 실행됩니다.
+ *
+ * @param request 현재 요청
+ * @param response 응답 객체
+ * @param ex 발생한 {@link AuthenticationException}
+ * @throws IOException 응답 스트림 처리 중 오류 발생 시
+ */
@Override
public void commence(
HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
diff --git a/apps/user-service/src/main/resources/application-production.yml b/apps/user-service/src/main/resources/application-production.yml
index c3645e13..406fed87 100644
--- a/apps/user-service/src/main/resources/application-production.yml
+++ b/apps/user-service/src/main/resources/application-production.yml
@@ -18,21 +18,21 @@ spring:
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
+ 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
diff --git a/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserLogoutFlowE2eTest.java b/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserLogoutFlowE2eTest.java
index 03c5f899..67e6820a 100644
--- a/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserLogoutFlowE2eTest.java
+++ b/apps/user-service/src/test/java/site/icebang/e2e/scenario/UserLogoutFlowE2eTest.java
@@ -6,8 +6,8 @@
import java.util.HashMap;
import java.util.Map;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
import org.springframework.http.*;
import org.springframework.test.context.jdbc.Sql;
@@ -22,9 +22,9 @@
class UserLogoutFlowE2eTest extends E2eTestSupport {
@SuppressWarnings("unchecked")
- @Test
- @DisplayName("정상 로그아웃 전체 플로우 - TDD REd 단계")
- void completeUserRegistrationFlow_shouldFailBecauseApiNotImplemented() throws Exception {
+ @Disabled
+ @DisplayName("정상 로그아웃 전체 플로우 - TDD Red 단계")
+ void completeUserLogoutFlow_shouldFailBecauseApiNotImplemented() throws Exception {
logStep(1, "관리자 로그인 (최우선)");
// 1. 관리자 로그인으로 인증 상태 확립
@@ -45,33 +45,16 @@ void completeUserRegistrationFlow_shouldFailBecauseApiNotImplemented() throws Ex
assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat((Boolean) loginResponse.getBody().get("success")).isTrue();
- logSuccess("관리자 로그인 성공 - 인증 상태 확립 완료");
+ logSuccess("관리자 로그인 성공 - 세션 쿠키 자동 저장됨");
+ logDebug("현재 세션 쿠키: " + getSessionCookies());
logStep(2, "로그인 상태에서 보호된 리소스 접근 확인");
- // 로그인 응답에서 세션 쿠키 추출
- String sessionCookie = null;
- java.util.List{@code
+ * PageParams params = new PageParams();
+ * params.setCurrent(2);
+ * params.setPageSize(20);
+ * params.setSearch("회원");
+ *
+ * int offset = params.getOffset(); // 20
+ * boolean searchable = params.hasSearch(); // true
+ * }
+ *
+ * @author jys01012@gmail.com
+ * @since v0.0.1-alpha
+ * @see lombok.Data
+ */
@Data
public class PageParams {
+ /**
+ * 현재 페이지 번호 (1부터 시작).
+ *
+ * {@code
+ * PageParams params = new PageParams();
+ * params.setCurrent(2);
+ * params.setPageSize(10);
+ *
+ * // Repository나 Mapper에서 데이터를 가져와 PageResult 생성
+ * PageResult
+ *
+ * @param > dataSupplier, Supplier
사용 예제:
+ *
+ * {@code
+ * // 사용자 이메일 중복 체크
+ * if (userRepository.existsByEmail(email)) {
+ * throw new DuplicateDataException("이미 존재하는 이메일입니다: " + email);
+ * }
+ *
+ * // 상품 코드 중복 체크
+ * try {
+ * productService.createProduct(product);
+ * } catch (DataIntegrityViolationException e) {
+ * throw new DuplicateDataException("중복된 상품 코드입니다", e);
+ * }
+ * }
+ *
+ * @author jys01012@gmail.com
+ * @since v0.0.1-alpha
+ */
public class DuplicateDataException extends RuntimeException {
+ /**
+ * 상세 메시지 없이 새로운 {@code DuplicateDataException}을 생성합니다.
+ *
+ * @since v0.0.1-alpha
+ */
public DuplicateDataException() {
super();
}
+ /**
+ * 지정된 상세 메시지와 함께 새로운 {@code DuplicateDataException}을 생성합니다.
+ *
+ * @param message 상세 메시지 (나중에 {@link Throwable#getMessage()} 메서드로 검색됨)
+ * @since v0.0.1-alpha
+ */
public DuplicateDataException(String message) {
super(message);
}
+ /**
+ * 지정된 상세 메시지와 원인과 함께 새로운 {@code DuplicateDataException}을 생성합니다.
+ *
+ * {@code
+ * @Service
+ * public class UserService implements PageableService
+ *
+ * @param 사용 예제:
+ *
+ * {@code
+ * @Autowired
+ * private RandomPasswordGenerator passwordGenerator;
+ *
+ * // 기본 길이(12자)로 패스워드 생성
+ * String password1 = passwordGenerator.generate();
+ *
+ * // 사용자 지정 길이로 패스워드 생성
+ * String password2 = passwordGenerator.generate(16);
+ * }
+ *
+ * @author jys01012@gmail.com
+ * @since v0.0.1-alpha
+ */
@Component
public class RandomPasswordGenerator {
+ /** 소문자 알파벳 문자 집합 */
private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
+
+ /** 대문자 알파벳 문자 집합 */
private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ /** 숫자 문자 집합 */
private static final String DIGITS = "0123456789";
+
+ /** 특수문자 집합 */
private static final String SPECIAL_CHARS = "!@#$%^&*()-_+=<>?";
+ /** 암호학적으로 안전한 난수 생성기 */
private final SecureRandom random = new SecureRandom();
+ /**
+ * 지정된 길이의 랜덤 패스워드를 생성합니다.
+ *
+ *
+ *
+ *
+ * @param length 생성할 패스워드의 길이 (8보다 작으면 8로 조정됩니다)
+ * @return 생성된 랜덤 패스워드
+ * @throws IllegalArgumentException length가 음수인 경우
+ * @since v0.0.1-alpha
+ */
public String generate(int length) {
if (length < 8) {
length = 8;
@@ -49,13 +94,28 @@ public String generate(int length) {
.toString();
}
- // 특정 문자열에서 랜덤으로 한 문자 선택
+ /**
+ * 지정된 문자 집합에서 랜덤으로 한 문자를 선택합니다.
+ *
+ * @param charSet 선택할 문자들이 포함된 문자열
+ * @return 선택된 랜덤 문자
+ * @throws IllegalArgumentException charSet이 null이거나 빈 문자열인 경우
+ * @since v0.0.1-alpha
+ */
private char getRandomChar(String charSet) {
int randomIndex = random.nextInt(charSet.length());
return charSet.charAt(randomIndex);
}
- // 기본 길이로 비밀번호 생성
+ /**
+ * 기본 길이(12자)로 랜덤 패스워드를 생성합니다.
+ *
+ * 주요 기능:
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Slf4j
@Service
@RequiredArgsConstructor
public class QuartzScheduleService {
+
+ /** Quartz 스케줄러의 메인 인스턴스 */
private final Scheduler scheduler;
+ /**
+ * DB에 정의된 Schedule 객체를 기반으로 Quartz에 스케줄을 등록하거나 업데이트합니다.
+ *
+ * 주요 구성 요소:
+ *
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
public interface TaskRunner {
- /** Task 실행 결과를 담는 Record. status: SUCCESS 또는 FAILED message: 실행 결과 또는 에러 메시지 */
+ /**
+ * Task 실행 결과를 담는 불변(Immutable) 데이터 객체(Record)입니다.
+ *
+ * 주요 기능:
+ *
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Component("fastapiTaskRunner")
@RequiredArgsConstructor
public class FastApiTaskRunner implements TaskRunner {
+ /** FastAPI 서버와의 통신을 전담하는 어댑터 */
private final FastApiAdapter fastApiAdapter;
+ /**
+ * FastAPI 타입의 Task를 실행합니다.
+ *
+ * 주요 기능:
+ *
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Slf4j
@Component
@RequiredArgsConstructor
public class WorkflowTriggerJob extends QuartzJobBean {
private final WorkflowExecutionService workflowExecutionService;
+ /**
+ * Quartz 스케줄러에 의해 트리거가 발동될 때 호출되는 메인 실행 메소드입니다.
+ *
+ * 주요 기능:
+ *
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Service
@RequiredArgsConstructor
-public class TaskExecutionService { // 📌 클래스 이름 변경
+public class TaskExecutionService {
+ /** 워크플로우 실행 이력 전용 로거 */
private static final Logger workflowLogger = LoggerFactory.getLogger("WORKFLOW_HISTORY");
+
private final Map주요 기능:
+ *
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Service
@RequiredArgsConstructor
public class WorkflowHistoryService implements PageableService주요 기능:
+ *
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Service
@RequiredArgsConstructor
public class WorkflowService implements PageableService사용 예제:
+ *
+ * {@code
+ * @Autowired
+ * private FastApiAdapter fastApiAdapter;
+ *
+ * String response = fastApiAdapter.call("/keywords/search", HttpMethod.POST, "{\"tag\":\"naver\"}");
+ * }
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Slf4j
@Component
@RequiredArgsConstructor
@@ -18,11 +37,30 @@ public class FastApiAdapter {
private final RestTemplate restTemplate;
private final FastApiProperties properties;
- // 📌 Task나 context에 대한 의존성이 완전히 사라짐
+ /**
+ * FastAPI 서버에 API 요청을 보내는 범용 메소드입니다.
+ *
+ * 주요 기능:
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Slf4j
@Component
@RequiredArgsConstructor
@@ -17,6 +33,15 @@ public class QuartzSchedulerInitializer implements CommandLineRunner {
private final ScheduleMapper scheduleMapper;
private final QuartzScheduleService quartzScheduleService;
+ /**
+ * Spring Boot 애플리케이션 시작 시 호출되는 메인 실행 메소드입니다.
+ *
+ * 주요 기능:
+ *
+ *
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Configuration
public class WebConfig {
+ /**
+ * 외부 API 통신을 위한 RestTemplate 빈을 생성하여 스프링 컨테이너에 등록합니다.
+ *
+ * MyBatis XML 매퍼에서의 사용 예제:
+ *
+ * {@code
+ *
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@MappedTypes(JsonNode.class)
public class JsonNodeTypeHandler extends BaseTypeHandler사용 예제:
+ *
+ * {@code
+ * @Component
+ * @RequiredArgsConstructor
+ * public class FastApiAdapter {
+ * private final FastApiProperties properties;
+ *
+ * public void someMethod() {
+ * String baseUrl = properties.getUrl(); // 설정된 URL 사용
+ * // ...
+ * }
+ * }
+ * }
+ *
+ * @author jihu0210@naver.com
+ * @since v0.1.0
+ */
@Getter
@Setter
@Component // Component로 등록하여 Spring이 Bean으로 관리하도록 함
@@ -16,10 +40,19 @@
@Validated // 아래의 유효성 검사 어노테이션을 활성화
public class FastApiProperties {
- /** FastAPI 서버의 기본 URL */
+ /**
+ * FastAPI 서버의 기본 URL 주소입니다.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *