diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..d7b5b424 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,25 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: 'Feature' + labels: + - 'enhancement' + - title: 'Bug Fixes' + labels: + - 'bug' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + $CHANGES \ No newline at end of file diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml new file mode 100644 index 00000000..42ac78b0 --- /dev/null +++ b/.github/workflows/ci-java.yml @@ -0,0 +1,130 @@ +name: CI (Java) + +on: + push: + branches: + - main + paths: + - "apps/user-service/**" + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - main + - develop + - release/** + paths: + - "apps/user-service/**" + +permissions: + contents: read + packages: write + security-events: write + checks: write + pull-requests: write + +jobs: + spotless-check: + name: Lint Check + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'gradle' + + - name: Grant execute permission for Gradle wrapper + run: chmod +x ./gradlew + working-directory: apps/user-service + + - name: Run Spotless Check + run: ./gradlew spotlessCheck + working-directory: apps/user-service + + build: + name: Build + runs-on: ubuntu-latest + needs: spotless-check + strategy: + matrix: + java-version: [ "21" ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + java-version: '${{ matrix.java-version }}' + distribution: 'temurin' + cache: 'gradle' + + - name: Grant execute permission for Gradle wrapper + run: chmod +x ./gradlew + working-directory: apps/user-service + + - name: Run Gradle Build + run: ./gradlew build -x test + working-directory: apps/user-service + +# - name: Run Tests +# run: | +# if [ "${{ github.base_ref }}" == "main" ]; then +# ./gradlew test +# else +# ./gradlew prTest +# fi +# working-directory: apps/user-service + - name: Upload build artifacts + if: matrix.java-version == '21' && github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: apps/user-service/build/libs/ + + docker: + name: Build Spring Boot Docker Image and push to registry + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + needs: + - build + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download build artifacts (JAR) + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: apps/user-service/build/libs/ + + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set repo lowercase + run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./apps/user-service + push: true + tags: | + ghcr.io/${{ env.REPO_LC }}/user-service:latest + ghcr.io/${{ env.REPO_LC }}/user-service:${{ github.sha }} + + - name: Analyze image layers + run: | + echo "=== Image Layer Analysis ===" + docker history ghcr.io/${{ env.REPO_LC }}/user-service:latest --human --no-trunc \ No newline at end of file diff --git a/.github/workflows/deploy-java.yml b/.github/workflows/deploy-java.yml new file mode 100644 index 00000000..9d1e2e03 --- /dev/null +++ b/.github/workflows/deploy-java.yml @@ -0,0 +1,74 @@ +name: Deploy + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +jobs: + deploy: + name: Deploy to AWS EC2 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set repo lowercase + run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + + - name: Copy docker compose files to EC2 + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + source: "docker/production/docker-compose.yml" + target: "~/app" + + - name: Deploy on EC2 + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_HOST }} +# username: ${{ secrets.SERVER_USER }} + username: ubuntu + key: ${{ secrets.SERVER_SSH_KEY }} + script: | + cd ~/app/docker/production + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + docker pull ghcr.io/${{ env.REPO_LC }}/user-service:latest + + docker compose down + docker compose up -d + + sleep 10 + docker compose ps + + docker image prune -f + +# - name: Send Discord notification - Success +# if: success() +# uses: Ilshidur/action-discord@master +# env: +# DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} +# with: +# args: | +# **배포 성공** +# **Repository:** ${{ env.REPO_LC }} +# **Tag:** ${{ github.ref_name }} +# **Server:** ${{ secrets.SERVER_HOST }} +# **Status:** Success! +# +# - name: Send Discord notification - Failure +# if: failure() +# uses: Ilshidur/action-discord@master +# env: +# DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} +# with: +# args: | +# **배포 실패** +# **Repository:** ${{ env.REPO_LC }} +# **Tag:** ${{ github.ref_name }} +# **Error:** 배포 중 오류가 발생했습니다. +# **Check:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..5adb153f --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,34 @@ +name: Release Drafter + +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize] +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "main" + - uses: release-drafter/release-drafter@v5 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 723ef36f..30de240b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.idea \ No newline at end of file +.idea +logs +*.log \ No newline at end of file diff --git a/apps/pre-processing-service/app/__init__.py b/apps/pre-processing-service/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/api/__init__.py b/apps/pre-processing-service/app/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/api/endpoints/__init__.py b/apps/pre-processing-service/app/api/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/api/endpoints/embedding.py b/apps/pre-processing-service/app/api/endpoints/embedding.py new file mode 100644 index 00000000..8a8d1d6f --- /dev/null +++ b/apps/pre-processing-service/app/api/endpoints/embedding.py @@ -0,0 +1,12 @@ +# app/api/endpoints/embedding.py +from fastapi import APIRouter +from app.decorators.logging import log_api_call +from ...errors.CustomException import * +from fastapi import APIRouter + +# 이 파일만의 독립적인 라우터를 생성합니다. +router = APIRouter() + +@router.get("/") +async def root(): + return {"message": "Items API"} \ No newline at end of file diff --git a/apps/pre-processing-service/app/api/endpoints/processing.py b/apps/pre-processing-service/app/api/endpoints/processing.py new file mode 100644 index 00000000..51c8ff27 --- /dev/null +++ b/apps/pre-processing-service/app/api/endpoints/processing.py @@ -0,0 +1,12 @@ +# app/api/endpoints/embedding.py +from fastapi import APIRouter +from app.decorators.logging import log_api_call +from ...errors.CustomException import * +from fastapi import APIRouter + +# 이 파일만의 독립적인 라우터를 생성합니다. +router = APIRouter() + +@router.get("/") +async def root(): + return {"message": "사용자API"} \ No newline at end of file diff --git a/apps/pre-processing-service/app/api/endpoints/test.py b/apps/pre-processing-service/app/api/endpoints/test.py new file mode 100644 index 00000000..2a33591e --- /dev/null +++ b/apps/pre-processing-service/app/api/endpoints/test.py @@ -0,0 +1,35 @@ +# app/api/endpoints/embedding.py +from fastapi import APIRouter +from app.decorators.logging import log_api_call +from ...errors.CustomException import * +from fastapi import APIRouter + +# 이 파일만의 독립적인 라우터를 생성합니다. +router = APIRouter() + +@router.get("/") +async def root(): + return {"message": "테스트 API"} + + +@router.get("/hello/{name}" , tags=["hello"]) +# @log_api_call +async def say_hello(name: str): + return {"message": f"Hello {name}"} + + +# 특정 경로에서 의도적으로 에러 발생 +#커스텀에러 테스터 url +@router.get("/error/{item_id}") +async def trigger_error(item_id: int): + if item_id == 0: + raise InvalidItemDataException() + + if item_id == 404: + raise ItemNotFoundException(item_id=item_id) + + if item_id == 500: + raise ValueError("이것은 테스트용 값 오류입니다.") + + + return {"result": item_id} \ No newline at end of file diff --git a/apps/pre-processing-service/app/api/router.py b/apps/pre-processing-service/app/api/router.py new file mode 100644 index 00000000..a1c064cc --- /dev/null +++ b/apps/pre-processing-service/app/api/router.py @@ -0,0 +1,29 @@ +# app/api/router.py +from fastapi import APIRouter +from .endpoints import embedding, processing,test +from ..core.config import settings + +api_router = APIRouter() + +# embedding API URL +api_router.include_router(embedding.router, prefix="/emb", tags=["Embedding"]) + +# processing API URL +api_router.include_router(processing.router, prefix="/prc", tags=["Processing"]) + +#모듈 테스터를 위한 endpoint +api_router.include_router(test.router, prefix="/test", tags=["Test"]) + +@api_router.get("/") +async def root(): + return {"message": "서버 실행중입니다."} + +@api_router.get("/db") +def get_settings(): + """ + 환경 변수가 올바르게 로드되었는지 확인하는 엔드포인트 + """ + return { + "환경": settings.env_name, + "데이터베이스 URL": settings.db_url + } diff --git a/apps/pre-processing-service/app/core/__init__.py b/apps/pre-processing-service/app/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/core/config.py b/apps/pre-processing-service/app/core/config.py new file mode 100644 index 00000000..134d0430 --- /dev/null +++ b/apps/pre-processing-service/app/core/config.py @@ -0,0 +1,48 @@ +from pydantic_settings import BaseSettings +import os +from typing import Optional + + +# 공통 설정을 위한 BaseSettings +class BaseSettingsConfig(BaseSettings): + + # db_url 대신 개별 필드를 정의합니다. + db_host: str + db_port: str + db_user: str + db_pass: str + db_name: str + env_name: str = "dev" + + @property + def db_url(self) -> str: + """개별 필드를 사용하여 DB URL을 동적으로 생성""" + return f"postgresql://{self.db_user}:{self.db_pass}@{self.db_host}:{self.db_port}/{self.db_name}" + + class Config: + env_file = ['.env'] + + +# 환경별 설정 클래스 +class DevSettings(BaseSettingsConfig): + class Config: + env_file = ['.env', 'dev.env'] + + +class PrdSettings(BaseSettingsConfig): + class Config: + env_file = ['.env', 'prd.env'] + + +def get_settings() -> BaseSettingsConfig: + """환경 변수에 따라 적절한 설정 객체를 반환하는 함수""" + mode = os.getenv("MODE", "dev") + if mode == "dev": + return DevSettings() + elif mode == "prd": + return PrdSettings() + else: + raise ValueError(f"Invalid MODE environment variable: {mode}") + + +settings = get_settings() \ No newline at end of file diff --git a/apps/pre-processing-service/app/db/__init__.py b/apps/pre-processing-service/app/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/db/db_connecter.py b/apps/pre-processing-service/app/db/db_connecter.py new file mode 100644 index 00000000..0ed48b04 --- /dev/null +++ b/apps/pre-processing-service/app/db/db_connecter.py @@ -0,0 +1 @@ +from ..core.config import settings \ No newline at end of file diff --git a/apps/pre-processing-service/app/decorators/__init__.py b/apps/pre-processing-service/app/decorators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/decorators/logging.py b/apps/pre-processing-service/app/decorators/logging.py new file mode 100644 index 00000000..145cb0a0 --- /dev/null +++ b/apps/pre-processing-service/app/decorators/logging.py @@ -0,0 +1,88 @@ +# app/decorators/logging.py + +from fastapi import Request +from loguru import logger +import functools +import time + + +def log_api_call(func): + """ + FastAPI API 호출에 대한 상세 정보를 로깅하는 데코레이터입니다. + IP 주소, User-Agent, URL, 메서드, 실행 시간 등을 기록합니다. + """ + + @functools.wraps(func) + async def wrapper(*args, **kwargs): + # 1. request 객체를 안전하게 가져옵니다. + # kwargs에서 'request'를 찾고, 없으면 args가 비어있지 않은 경우에만 args[0]을 시도합니다. + request: Request | None = kwargs.get('request') + if request is None and args and isinstance(args[0], Request): + request = args[0] + + # 2. 로깅에 사용할 추가 정보를 추출합니다. + client_ip: str | None = None + user_agent: str | None = None + if request: + client_ip = request.client.host + user_agent = request.headers.get("user-agent", "N/A") + + # 3. 요청 정보를 로그로 기록합니다. + log_context = { + "func": func.__name__, + "ip": client_ip, + "user_agent": user_agent + } + if request: + log_context.update({ + "url": str(request.url), + "method": request.method, + }) + logger.info( + "API 호출 시작: URL='{url}' 메서드='{method}' 함수='{func}' IP='{ip}' User-Agent='{user_agent}'", + **log_context + ) + else: + logger.info("API 호출 시작: 함수='{func}'", **log_context) + + start_time = time.time() + result = None + + try: + # 4. 원본 함수를 실행합니다. + result = await func(*args, **kwargs) + return result + except Exception as e: + # 5. 예외 발생 시 에러 로그를 기록합니다. + elapsed_time = time.time() - start_time + log_context["exception"] = e + log_context["elapsed"] = f"{elapsed_time:.4f}s" + + if request: + logger.error( + "API 호출 실패: URL='{url}' 메서드='{method}' IP='{ip}' 예외='{exception}' ({elapsed})", + **log_context + ) + else: + logger.error( + "API 호출 실패: 함수='{func}' 예외='{exception}' ({elapsed})", + **log_context + ) + raise # 예외를 다시 발생시켜 FastAPI가 처리하도록 합니다. + finally: + # 6. 성공적으로 완료되면 성공 로그를 기록합니다. + if result is not None: + elapsed_time = time.time() - start_time + log_context["elapsed"] = f"{elapsed_time:.4f}s" + if request: + logger.success( + "API 호출 성공: URL='{url}' 메서드='{method}' IP='{ip}' ({elapsed})", + **log_context + ) + else: + logger.success( + "API 호출 성공: 함수='{func}' ({elapsed})", + **log_context + ) + + return wrapper \ No newline at end of file diff --git a/apps/pre-processing-service/app/errors/CustomException.py b/apps/pre-processing-service/app/errors/CustomException.py new file mode 100644 index 00000000..c228748e --- /dev/null +++ b/apps/pre-processing-service/app/errors/CustomException.py @@ -0,0 +1,26 @@ +# app/errors/CustomException.py +class CustomException(Exception): + """ + 개발자가 비지니스 로직에 맞게 의도적으로 에러를 정의 + """ + def __init__(self, status_code: int, detail: str, code: str): + self.status_code = status_code + self.detail = detail + self.code = code + +# 구체적인 커스텀 예외 정의 +class ItemNotFoundException(CustomException): + def __init__(self, item_id: int): + super().__init__( + status_code=404, + detail=f"{item_id}를 찾을수 없습니다.", + code="ITEM_NOT_FOUND" + ) + +class InvalidItemDataException(CustomException): + def __init__(self): + super().__init__( + status_code=422, + detail="데이터가 유효하지않습니다..", + code="INVALID_ITEM_DATA" + ) \ No newline at end of file diff --git a/apps/pre-processing-service/app/errors/__init__.py b/apps/pre-processing-service/app/errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/errors/handlers.py b/apps/pre-processing-service/app/errors/handlers.py new file mode 100644 index 00000000..db05e176 --- /dev/null +++ b/apps/pre-processing-service/app/errors/handlers.py @@ -0,0 +1,65 @@ +# app/errors/handlers.py +from fastapi import Request, status +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException as StarletteHTTPException +from fastapi.exceptions import RequestValidationError +from .messages import ERROR_MESSAGES, get_error_message +from ..errors.CustomException import CustomException + +# CustomException 핸들러 +async def custom_exception_handler(request: Request, exc: CustomException): + """ + CustomException을 상속받는 모든 예외를 처리합니다. + """ + return JSONResponse( + status_code=exc.status_code, + content={ + "error_code": exc.code, + "message": exc.detail, + }, + ) + +# FastAPI의 HTTPException 핸들러 (예: 404 Not Found) +async def http_exception_handler(request: Request, exc: StarletteHTTPException): + """ + FastAPI에서 기본적으로 발생하는 HTTP 관련 예외를 처리합니다. + """ + if exc.status_code == status.HTTP_404_NOT_FOUND: + # 404 에러의 경우, FastAPI의 기본 "Not Found" 메시지 대신 우리가 정의한 메시지를 사용합니다. + message = ERROR_MESSAGES.get(exc.status_code, "요청하신 리소스를 찾을 수 없습니다.") + else: + # 다른 HTTP 예외들은 FastAPI가 제공하는 detail 메시지를 우선적으로 사용합니다. + message = get_error_message(exc.status_code, exc.detail) + + return JSONResponse( + status_code=exc.status_code, + content={ + "error_code": f"HTTP_{exc.status_code}", + "message": message + }, + ) + +# Pydantic Validation Error 핸들러 (422) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """ + Pydantic 모델 유효성 검사 실패 시 발생하는 예외를 처리합니다. + """ + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "error_code": "VALIDATION_ERROR", + "message": ERROR_MESSAGES[status.HTTP_422_UNPROCESSABLE_ENTITY], + "details": exc.errors(), + }, + ) + +# 처리되지 않은 모든 예외 핸들러 (500) +async def unhandled_exception_handler(request: Request, exc: Exception): + # ... + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error_code": "INTERNAL_SERVER_ERROR", + "message": ERROR_MESSAGES[status.HTTP_500_INTERNAL_SERVER_ERROR], + }, + ) diff --git a/apps/pre-processing-service/app/errors/messages.py b/apps/pre-processing-service/app/errors/messages.py new file mode 100644 index 00000000..80139492 --- /dev/null +++ b/apps/pre-processing-service/app/errors/messages.py @@ -0,0 +1,16 @@ +# app/errors/messages.py +from fastapi import status + +ERROR_MESSAGES = { + status.HTTP_400_BAD_REQUEST: "잘못된 요청입니다.", + status.HTTP_401_UNAUTHORIZED: "인증이 필요합니다.", + status.HTTP_403_FORBIDDEN: "접근 권한이 없습니다.", + status.HTTP_404_NOT_FOUND: "요청하신 리소스를 찾을 수 없습니다.", + status.HTTP_422_UNPROCESSABLE_ENTITY: "입력 데이터가 유효하지 않습니다.", + status.HTTP_500_INTERNAL_SERVER_ERROR: "서버 내부 오류가 발생했습니다.", +} + + +def get_error_message(status_code: int, detail: str | None = None) -> str: + """상태 코드에 맞는 기본 메시지를 가져오되, detail이 있으면 우선""" + return detail or ERROR_MESSAGES.get(status_code, "알 수 없는 오류가 발생했습니다.") diff --git a/apps/pre-processing-service/app/main.py b/apps/pre-processing-service/app/main.py new file mode 100644 index 00000000..2ca44875 --- /dev/null +++ b/apps/pre-processing-service/app/main.py @@ -0,0 +1,34 @@ +# main.py +import uvicorn +from fastapi import FastAPI +from starlette.exceptions import HTTPException as StarletteHTTPException +from fastapi.exceptions import RequestValidationError + +# --- 애플리케이션 구성 요소 임포트 --- +from app.api.router import api_router +from app.middleware.logging import LoggingMiddleware +from app.errors.CustomException import * +from app.errors.handlers import * + +# --- FastAPI 애플리케이션 인스턴스 생성 --- +app = FastAPI( + title="pre-processing-service", + description="", + version="1.0.0" +) + +# --- 예외 핸들러 등록 --- +# 등록 순서가 중요합니다: 구체적인 예외부터 등록하고 가장 일반적인 예외(Exception)를 마지막에 등록합니다. +app.add_exception_handler(CustomException, custom_exception_handler) +app.add_exception_handler(StarletteHTTPException, http_exception_handler) +app.add_exception_handler(RequestValidationError, validation_exception_handler) +app.add_exception_handler(Exception, unhandled_exception_handler) + +# --- 미들웨어 등록 --- +app.add_middleware(LoggingMiddleware) + +# --- 라우터 등록 --- +app.include_router(api_router, prefix="", tags=["api"]) + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/apps/pre-processing-service/app/middleware/__init__.py b/apps/pre-processing-service/app/middleware/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/middleware/logging.py b/apps/pre-processing-service/app/middleware/logging.py new file mode 100644 index 00000000..29cbe738 --- /dev/null +++ b/apps/pre-processing-service/app/middleware/logging.py @@ -0,0 +1,38 @@ + +import time +from fastapi import Request +from loguru import logger +from starlette.middleware.base import BaseHTTPMiddleware + + +class LoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + start_time = time.time() + + # 1. 요청 시작 로그 + logger.info( + "요청 시작: IP='{}' 메서드='{}' URL='{}'", + request.client.host, request.method, request.url.path + ) + + try: + # 2. 다음 미들웨어 또는 최종 엔드포인트 함수 실행 + response = await call_next(request) + + # 3. 요청 성공 시 로그 + process_time = time.time() - start_time + logger.info( + "요청 성공: 메서드='{}' URL='{}' 상태코드='{}' (처리 시간: {:.4f}s)", + request.method, request.url.path, response.status_code, process_time + ) + return response + + except Exception as e: + # 4. 예외 발생 시 로그 + process_time = time.time() - start_time + logger.error( + "요청 실패: IP='{}' 메서드='{}' URL='{}' 예외='{}' (처리 시간: {:.4f}s)", + request.client.host, request.method, request.url.path, e, process_time + ) + # 예외를 다시 발생시켜 FastAPI의 기본 핸들러가 처리하도록 함 + raise \ No newline at end of file diff --git a/apps/pre-processing-service/app/model/__init__.py b/apps/pre-processing-service/app/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/model/schemas.py b/apps/pre-processing-service/app/model/schemas.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/services/__init__.py b/apps/pre-processing-service/app/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/app/services/preprocessing_service.py b/apps/pre-processing-service/app/services/preprocessing_service.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/pre-processing-service/poetry.lock b/apps/pre-processing-service/poetry.lock new file mode 100644 index 00000000..961f44e5 --- /dev/null +++ b/apps/pre-processing-service/poetry.lock @@ -0,0 +1,491 @@ +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +description = "Deprecated package" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"}, +] + +[package.dependencies] +python-dotenv = "*" + +[[package]] +name = "fastapi" +version = "0.116.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"}, + {file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.48.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +groups = ["main"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.11.7" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, + {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.47.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b"}, + {file = "starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "uvicorn" +version = "0.35.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, + {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.11,<4.0" +content-hash = "845e1778efdd87512efdd30eb0ba01aa1383061f662ccc3faa17ab1f8cebde5b" diff --git a/apps/pre-processing-service/pyproject.toml b/apps/pre-processing-service/pyproject.toml new file mode 100644 index 00000000..5a2017c3 --- /dev/null +++ b/apps/pre-processing-service/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "pre-processing-service" +version = "0.1.0" +description = "" +authors = [ + {name = "skip"} +] +readme = "README.md" +requires-python = ">=3.11,<4.0" +dependencies = [ + "fastapi (>=0.116.1,<0.117.0)", + "uvicorn (>=0.35.0,<0.36.0)", + "loguru (>=0.7.3,<0.8.0)", + "pytest (>=8.4.1,<9.0.0)", + "dotenv (>=0.9.9,<0.10.0)", + "pydantic-settings (>=2.10.1,<3.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/apps/pre-processing-service/test_main.http b/apps/pre-processing-service/test_main.http new file mode 100644 index 00000000..a2d81a92 --- /dev/null +++ b/apps/pre-processing-service/test_main.http @@ -0,0 +1,11 @@ +# Test your FastAPI endpoints + +GET http://127.0.0.1:8000/ +Accept: application/json + +### + +GET http://127.0.0.1:8000/hello/User +Accept: application/json + +### diff --git a/apps/user-service/Dockerfile b/apps/user-service/Dockerfile new file mode 100644 index 00000000..e3fb24a4 --- /dev/null +++ b/apps/user-service/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:21-jre + +WORKDIR /app + +COPY build/libs/*.jar app.jar + +EXPOSE 8080 + +CMD ["java", "-jar", "app.jar"] diff --git a/apps/user-service/build.gradle b/apps/user-service/build.gradle index f373d429..4da4d368 100644 --- a/apps/user-service/build.gradle +++ b/apps/user-service/build.gradle @@ -2,10 +2,11 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.4' id 'io.spring.dependency-management' version '1.1.7' + id 'com.diffplug.spotless' version '7.2.1' } group = 'com.gltkorea' -version = '0.0.1-SNAPSHOT' +version = '0.0.1-alpha-SNAPSHOT' description = 'GLT korea - fast campus team4 ice bang' java { @@ -18,6 +19,10 @@ configurations { compileOnly { extendsFrom annotationProcessor } + // Spring Boot의 기본 로깅(Logback) 제외 + all { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + } } repositories { @@ -25,13 +30,35 @@ repositories { } dependencies { + // Spring Boot Starters implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-aop' + + // MyBatis implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.5' - compileOnly 'org.projectlombok:lombok' + + // Log4j2 - 모든 모듈을 2.22.1로 통일 + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation 'org.apache.logging.log4j:log4j-core:2.22.1' + implementation 'org.apache.logging.log4j:log4j-api:2.22.1' + implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.22.1' + implementation 'org.apache.logging.log4j:log4j-jul:2.22.1' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' + + // 비동기 로깅 + implementation 'com.lmax:disruptor:3.4.4' + + // Lombok + compileOnly 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' + + // Database runtimeOnly 'com.h2database:h2' - annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'org.postgresql:postgresql' + + // Test Dependencies testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.5' @@ -43,3 +70,21 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +spotless { + java { + googleJavaFormat('1.17.0') + importOrder('java', 'javax', 'org', 'com', '', 'com.movement') + endWithNewline() + removeUnusedImports() + encoding('UTF-8') + + targetExclude("**/generated/**", "**/Q*.java") + } + format 'misc', { + target '**/*.gradle', '**/*.md', '**/.gitignore' + trimTrailingWhitespace() + indentWithTabs() + endWithNewline() + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/UserServiceApplication.java b/apps/user-service/src/main/java/com/gltkorea/icebang/UserServiceApplication.java index 110b7d92..c69c1773 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/UserServiceApplication.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/UserServiceApplication.java @@ -1,13 +1,14 @@ package com.gltkorea.icebang; +import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication +@MapperScan("com.gltkorea.icebang.mapper") public class UserServiceApplication { - public static void main(String[] args) { - SpringApplication.run(UserServiceApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(UserServiceApplication.class, args); + } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/aop/logging/LoggingAspect.java b/apps/user-service/src/main/java/com/gltkorea/icebang/aop/logging/LoggingAspect.java new file mode 100644 index 00000000..0441820d --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/aop/logging/LoggingAspect.java @@ -0,0 +1,54 @@ +package com.gltkorea.icebang.aop.logging; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Aspect +@Component +public class LoggingAspect { + + @Pointcut("execution(public * com.gltkorea.icebang..controller..*(..))") + public void controllerMethods() {} + + @Pointcut("execution(public * com.gltkorea.icebang..service..*(..))") + public void serviceMethods() {} + + @Pointcut("execution(public * com.gltkorea.icebang..service..repository..*(..))") + public void repositoryMethods() {} + + @Around("controllerMethods()") + public Object logController(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + log.info("[CONTROLLER] Start: {} args={}", joinPoint.getSignature(), joinPoint.getArgs()); + Object result = joinPoint.proceed(); + long duration = System.currentTimeMillis() - start; + log.info("[CONTROLLER] End: {} ({}ms)", joinPoint.getSignature(), duration); + return result; + } + + @Around("serviceMethods()") + public Object logService(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + log.info("[SERVICE] Start: {} args={}", joinPoint.getSignature(), joinPoint.getArgs()); + Object result = joinPoint.proceed(); + long duration = System.currentTimeMillis() - start; + log.info("[SERVICE] End: {} ({}ms)", joinPoint.getSignature(), duration); + return result; + } + + @Around("repositoryMethods()") + public Object logRepository(ProceedingJoinPoint joinPoint) throws Throwable { + long start = System.currentTimeMillis(); + log.debug("[REPOSITORY] Start: {} args={}", joinPoint.getSignature(), joinPoint.getArgs()); + Object result = joinPoint.proceed(); + long duration = System.currentTimeMillis() - start; + log.debug("[REPOSITORY] End: {} ({}ms)", joinPoint.getSignature(), duration); + return result; + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/controller/AuthController.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/controller/AuthController.java new file mode 100644 index 00000000..1e2f9dae --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/controller/AuthController.java @@ -0,0 +1,50 @@ +package com.gltkorea.icebang.auth.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.gltkorea.icebang.auth.dto.DefaultRequestWrapper; +import com.gltkorea.icebang.auth.dto.LoginDto; +import com.gltkorea.icebang.auth.dto.SignUpDto; +import com.gltkorea.icebang.auth.provider.AuthProvider; +import com.gltkorea.icebang.auth.provider.AuthProviderFactory; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v0/auth") +public class AuthController { + private final AuthProviderFactory authProviderFactory; + + @PostMapping("/signup") + public ResponseEntity signUp(@RequestBody SignUpDto signUpDto) { + // 1. Wrapper DTO 생성 + DefaultRequestWrapper wrapper = DefaultRequestWrapper.builder().signUpDto(signUpDto).build(); + + // 2. Factory에서 Provider 선택 + @SuppressWarnings("unchecked") + AuthProvider provider = + (AuthProvider) authProviderFactory.getProvider("default"); + + // 3. Provider에 인증 위임 (Provider 내부에서 signUp + login 처리) + Authentication auth = provider.authenticate(wrapper); + + // 4. 결과 반환 + return ResponseEntity.status(201).body(auth); + } + + @PostMapping("/signin") + public ResponseEntity signIn(@RequestBody LoginDto loginDto) { + DefaultRequestWrapper wrapper = DefaultRequestWrapper.builder().loginDto(loginDto).build(); + @SuppressWarnings("unchecked") + AuthProvider provider = + (AuthProvider) authProviderFactory.getProvider("default"); + Authentication auth = provider.authenticate(wrapper); + return ResponseEntity.ok(auth); + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/controller/Oauth2CallbackController.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/controller/Oauth2CallbackController.java new file mode 100644 index 00000000..f21d206a --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/controller/Oauth2CallbackController.java @@ -0,0 +1,20 @@ +package com.gltkorea.icebang.auth.controller; + +import org.springframework.web.bind.annotation.*; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/v0/oauth2/callback") +@RequiredArgsConstructor +public class Oauth2CallbackController { + + @GetMapping("/kakao") + public void handleKakaoCallback(@RequestParam String code) { + // OAuth2RequestWrapper wrapper = new OAuth2RequestWrapper(new + // Oauth2CallbackContent("kakao", code)); + // OAuth2AuthProvider provider = (OAuth2AuthProvider) + // authProviderFactory.getProvider(providerKey); + // Authentication auth = provider.authenticate(wrapper); + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/AuthRequestWrapper.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/AuthRequestWrapper.java new file mode 100644 index 00000000..3aac5ee4 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/AuthRequestWrapper.java @@ -0,0 +1,3 @@ +package com.gltkorea.icebang.auth.dto; + +public interface AuthRequestWrapper {} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/DefaultRequestWrapper.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/DefaultRequestWrapper.java new file mode 100644 index 00000000..4c2ebc93 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/DefaultRequestWrapper.java @@ -0,0 +1,13 @@ +package com.gltkorea.icebang.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@AllArgsConstructor +@Getter +public class DefaultRequestWrapper implements AuthRequestWrapper { + private final LoginDto loginDto; + private final SignUpDto signUpDto; +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/LoginDto.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/LoginDto.java new file mode 100644 index 00000000..73fda179 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/LoginDto.java @@ -0,0 +1,3 @@ +package com.gltkorea.icebang.auth.dto; + +public class LoginDto {} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/OAuth2RequestWrapper.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/OAuth2RequestWrapper.java new file mode 100644 index 00000000..86e9b5d7 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/OAuth2RequestWrapper.java @@ -0,0 +1,12 @@ +package com.gltkorea.icebang.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class OAuth2RequestWrapper implements AuthRequestWrapper { + private Oauth2CallbackContent callbackContent; +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/Oauth2CallbackContent.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/Oauth2CallbackContent.java new file mode 100644 index 00000000..d496b534 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/Oauth2CallbackContent.java @@ -0,0 +1,7 @@ +package com.gltkorea.icebang.auth.dto; + +public class Oauth2CallbackContent { + private String code; + private String state; + private String provider; +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/SignUpDto.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/SignUpDto.java new file mode 100644 index 00000000..5dc869fa --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/dto/SignUpDto.java @@ -0,0 +1,3 @@ +package com.gltkorea.icebang.auth.dto; + +public class SignUpDto {} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/AuthProvider.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/AuthProvider.java new file mode 100644 index 00000000..8380e96e --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/AuthProvider.java @@ -0,0 +1,11 @@ +package com.gltkorea.icebang.auth.provider; + +import org.springframework.security.core.Authentication; + +import com.gltkorea.icebang.auth.dto.AuthRequestWrapper; + +public interface AuthProvider { + boolean supports(T request); + + Authentication authenticate(T request); +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/AuthProviderFactory.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/AuthProviderFactory.java new file mode 100644 index 00000000..4f41363e --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/AuthProviderFactory.java @@ -0,0 +1,44 @@ +package com.gltkorea.icebang.auth.provider; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import com.gltkorea.icebang.auth.dto.AuthRequestWrapper; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AuthProviderFactory { + + private final Map> providers; + + /** + * providerKey에 해당하는 AuthProvider 반환 + * + * @param providerKey "google", "naver", "default" 등, enum으로 refactoring 필요 + * @return AuthProvider + */ + public AuthProvider getProvider(String providerKey) { + AuthProvider provider = providers.get(providerKey.toLowerCase()); + if (provider == null) { + throw new IllegalArgumentException("Unknown auth provider: " + providerKey); + } + return provider; + } + + /** + * OAuth2 전용 Provider 반환 + * + * @param providerKey OAuth2 provider key + * @return OAuth2AuthProvider + */ + public OAuth2AuthProvider getOAuth2Provider(String providerKey) { + AuthProvider provider = getProvider(providerKey); + if (!(provider instanceof OAuth2AuthProvider oauthProvider)) { + throw new IllegalArgumentException(providerKey + " is not an OAuth2 provider"); + } + return oauthProvider; + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/DefaultProvider.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/DefaultProvider.java new file mode 100644 index 00000000..40a44c7c --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/DefaultProvider.java @@ -0,0 +1,19 @@ +package com.gltkorea.icebang.auth.provider; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import com.gltkorea.icebang.auth.dto.DefaultRequestWrapper; + +@Component("default") +public class DefaultProvider implements AuthProvider { + @Override + public boolean supports(DefaultRequestWrapper request) { + return false; + } + + @Override + public Authentication authenticate(DefaultRequestWrapper request) { + return null; + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/OAuth2AuthProvider.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/OAuth2AuthProvider.java new file mode 100644 index 00000000..30be51f4 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/provider/OAuth2AuthProvider.java @@ -0,0 +1,9 @@ +package com.gltkorea.icebang.auth.provider; + +import org.springframework.security.core.Authentication; + +import com.gltkorea.icebang.auth.dto.OAuth2RequestWrapper; + +public interface OAuth2AuthProvider extends AuthProvider { + Authentication authenticateWithCode(OAuth2RequestWrapper oauthContent); +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/AuthService.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/AuthService.java new file mode 100644 index 00000000..30a2a131 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/AuthService.java @@ -0,0 +1,13 @@ +package com.gltkorea.icebang.auth.service; + +import com.gltkorea.icebang.auth.dto.LoginDto; +import com.gltkorea.icebang.auth.dto.SignUpDto; +import com.gltkorea.icebang.domain.user.model.Users; + +public interface AuthService { + Users signUp(SignUpDto signUpDto); + + Users login(LoginDto loginDto); + + Users loadUser(String identifier); +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/AuthServiceImpl.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/AuthServiceImpl.java new file mode 100644 index 00000000..aa118d2d --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/AuthServiceImpl.java @@ -0,0 +1,25 @@ +package com.gltkorea.icebang.auth.service; + +import org.springframework.stereotype.Service; + +import com.gltkorea.icebang.auth.dto.LoginDto; +import com.gltkorea.icebang.auth.dto.SignUpDto; +import com.gltkorea.icebang.domain.user.model.Users; + +@Service +public class AuthServiceImpl implements AuthService { + @Override + public Users signUp(SignUpDto signUpDto) { + return null; + } + + @Override + public Users login(LoginDto loginDto) { + return null; + } + + @Override + public Users loadUser(String identifier) { + return null; + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/AuthStateService.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/AuthStateService.java new file mode 100644 index 00000000..c802b309 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/AuthStateService.java @@ -0,0 +1,9 @@ +package com.gltkorea.icebang.auth.service.state; + +import org.springframework.security.core.userdetails.UserDetails; + +public sealed interface AuthStateService permits SessionStateService, JwtTokenStateService { + String create(UserDetails userDetails); + + UserDetails validate(String identifier); +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/JwtTokenStateService.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/JwtTokenStateService.java new file mode 100644 index 00000000..9065820e --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/JwtTokenStateService.java @@ -0,0 +1,15 @@ +package com.gltkorea.icebang.auth.service.state; + +import org.springframework.security.core.userdetails.UserDetails; + +public final class JwtTokenStateService implements AuthStateService { + @Override + public String create(UserDetails userDetails) { + return ""; + } + + @Override + public UserDetails validate(String identifier) { + return null; + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/SessionStateService.java b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/SessionStateService.java new file mode 100644 index 00000000..9bed4d1a --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/auth/service/state/SessionStateService.java @@ -0,0 +1,15 @@ +package com.gltkorea.icebang.auth.service.state; + +import org.springframework.security.core.userdetails.UserDetails; + +public final class SessionStateService implements AuthStateService { + @Override + public String create(UserDetails userDetails) { + return ""; + } + + @Override + public UserDetails validate(String identifier) { + return null; + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java new file mode 100644 index 00000000..8a81b429 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.gltkorea.icebang.config.security; + +import java.security.SecureRandom; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +import com.gltkorea.icebang.config.security.endpoints.SecurityEndpoints; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + private final Environment environment; + + @Bean + public SecureRandom secureRandom() { + return new SecureRandom(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests( + auth -> + auth.requestMatchers(SecurityEndpoints.PUBLIC.getMatchers()) + .permitAll() + .requestMatchers(SecurityEndpoints.ADMIN.getMatchers()) + .hasRole("ADMIN") + .requestMatchers(SecurityEndpoints.USER.getMatchers()) + .hasRole("USER") + .anyRequest() + .authenticated()) + .formLogin(form -> form.loginPage("/login").defaultSuccessUrl("/").permitAll()) + .logout(logout -> logout.logoutSuccessUrl("/login").permitAll()) + .build(); + } + + @Bean + public PasswordEncoder bCryptPasswordEncoder() { + String[] activeProfiles = environment.getActiveProfiles(); + + for (String profile : activeProfiles) { + if ("dev".equals(profile) || "test".equals(profile)) { + return NoOpPasswordEncoder.getInstance(); + } + } + return new BCryptPasswordEncoder(); + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/endpoints/SecurityEndpoints.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/endpoints/SecurityEndpoints.java new file mode 100644 index 00000000..0a24605f --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/endpoints/SecurityEndpoints.java @@ -0,0 +1,20 @@ +package com.gltkorea.icebang.config.security.endpoints; + +public enum SecurityEndpoints { + PUBLIC( + "/", "/login", "/register", "/api/public/**", "/health", "/css/**", "/js/**", "/images/**"), + + ADMIN("/admin/**", "/api/admin/**", "/management/**", "/actuator/**"), + + USER("/user/**", "/api/user/**", "/profile/**", "/dashboard"); + + private final String[] patterns; + + SecurityEndpoints(String... patterns) { + this.patterns = patterns.clone(); + } + + public String[] getMatchers() { + return patterns.clone(); + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/UserStatus.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/UserStatus.java new file mode 100644 index 00000000..b23e47e8 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/UserStatus.java @@ -0,0 +1,8 @@ +package com.gltkorea.icebang.domain.user; + +public enum UserStatus { + ONBOARDING, // email, password만 된 경우 + ACTIVE, // 완전히 활성화됨 + SUSPENDED, // 일시 정지 + DELETED // 삭제됨 +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/model/UserAccountPrincipal.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/model/UserAccountPrincipal.java new file mode 100644 index 00000000..abca6682 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/model/UserAccountPrincipal.java @@ -0,0 +1,27 @@ +package com.gltkorea.icebang.domain.user.model; + +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import lombok.Builder; + +@Builder +public class UserAccountPrincipal implements UserDetails { + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public String getPassword() { + return ""; + } + + @Override + public String getUsername() { + return ""; + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/model/Users.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/model/Users.java new file mode 100644 index 00000000..ab4d28d1 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/model/Users.java @@ -0,0 +1,7 @@ +package com.gltkorea.icebang.domain.user.model; + +import com.gltkorea.icebang.domain.user.UserStatus; + +public class Users { + private UserStatus status; +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/service/SecurityAuthenticateAdapter.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/service/SecurityAuthenticateAdapter.java new file mode 100644 index 00000000..f33f546f --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/service/SecurityAuthenticateAdapter.java @@ -0,0 +1,18 @@ +package com.gltkorea.icebang.domain.user.service; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import com.gltkorea.icebang.auth.service.AuthService; +import com.gltkorea.icebang.domain.user.model.UserAccountPrincipal; +import com.gltkorea.icebang.domain.user.model.Users; + +public class SecurityAuthenticateAdapter implements UserAuthService { + private AuthService authService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Users user = authService.loadUser(username); + return UserAccountPrincipal.builder().build(); // @TODO users -> userdetail로 + } +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/service/UserAuthService.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/service/UserAuthService.java new file mode 100644 index 00000000..dc8396ee --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/user/service/UserAuthService.java @@ -0,0 +1,5 @@ +package com.gltkorea.icebang.domain.user.service; + +import org.springframework.security.core.userdetails.UserDetailsService; + +public interface UserAuthService extends UserDetailsService {} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/dto/UserDto.java b/apps/user-service/src/main/java/com/gltkorea/icebang/dto/UserDto.java new file mode 100644 index 00000000..6763bac9 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/dto/UserDto.java @@ -0,0 +1,13 @@ +package com.gltkorea.icebang.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserDto { + private String userId; + private String name; + private String email; + // ... 필요한 다른 필드들 +} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java b/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java new file mode 100644 index 00000000..f09a152a --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java @@ -0,0 +1,13 @@ +package com.gltkorea.icebang.mapper; + +import java.util.Optional; + +import org.apache.ibatis.annotations.Mapper; + +import com.gltkorea.icebang.dto.UserDto; + +@Mapper // Spring이 MyBatis Mapper로 인식하도록 설정 +public interface UserMapper { + // XML 파일의 id와 메서드 이름을 일치시켜야 합니다. + Optional findByEmail(String email); +} diff --git a/apps/user-service/src/main/resources/application-develop.yml b/apps/user-service/src/main/resources/application-develop.yml new file mode 100644 index 00000000..d640ff77 --- /dev/null +++ b/apps/user-service/src/main/resources/application-develop.yml @@ -0,0 +1,29 @@ +# application-develop.yml +spring: + config: + activate: + on-profile: develop + + # PostgreSQL 데이터베이스 연결 설정 + datasource: + url: jdbc:postgresql://localhost:5432/pre_process + username: postgres + password: qwer1234 + driver-class-name: org.postgresql.Driver + + hikari: + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + maximum-pool-size: 10 + minimum-idle: 5 + pool-name: HikariCP-MyBatis + +mybatis: + mapper-locations: classpath:mybatis/mapper/**/*.xml + type-aliases-package: com.gltkorea.icebang.dto + configuration: + map-underscore-to-camel-case: true + +logging: + config: classpath:log4j2-develop.yml \ No newline at end of file diff --git a/apps/user-service/src/main/resources/application-production.yml b/apps/user-service/src/main/resources/application-production.yml new file mode 100644 index 00000000..e69de29b diff --git a/apps/user-service/src/main/resources/application-test.yml b/apps/user-service/src/main/resources/application-test.yml new file mode 100644 index 00000000..63217124 --- /dev/null +++ b/apps/user-service/src/main/resources/application-test.yml @@ -0,0 +1,34 @@ +# src/test/resources/application-test.yml +spring: + config: + activate: + on-profile: test + + # PostgreSQL 데이터베이스 연결 설정 + datasource: + url: jdbc:postgresql://localhost:5432/pre_process + username: postgres + password: qwer1234 + driver-class-name: org.postgresql.Driver + + hikari: + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + maximum-pool-size: 10 + minimum-idle: 5 + pool-name: HikariCP-MyBatis + + # SQL 스크립트 초기화 설정 추가 + sql: + init: + mode: always # 내장 DB가 아니더라도 항상 스크립트를 실행하도록 설정 + +mybatis: + mapper-locations: classpath:mybatis/mapper/**/*.xml + type-aliases-package: com.gltkorea.icebang.dto + configuration: + map-underscore-to-camel-case: true + +logging: + config: classpath:log4j2-test.yml \ No newline at end of file diff --git a/apps/user-service/src/main/resources/application.properties b/apps/user-service/src/main/resources/application.properties deleted file mode 100644 index 79b5bda2..00000000 --- a/apps/user-service/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=user-service diff --git a/apps/user-service/src/main/resources/application.yml b/apps/user-service/src/main/resources/application.yml new file mode 100644 index 00000000..278dfb11 --- /dev/null +++ b/apps/user-service/src/main/resources/application.yml @@ -0,0 +1,8 @@ +spring: + application: + name: mvp + profiles: + active: develop +mybatis: + # Mapper XML 파일 위치 + mapper-locations: classpath:mapper/**/*.xml \ No newline at end of file diff --git a/apps/user-service/src/main/resources/log4j2-develop.yml b/apps/user-service/src/main/resources/log4j2-develop.yml new file mode 100644 index 00000000..4734a1ce --- /dev/null +++ b/apps/user-service/src/main/resources/log4j2-develop.yml @@ -0,0 +1,126 @@ +Configuration: + name: develop + + properties: + property: + - name: "log-path" + value: "./logs" + - name: "charset-UTF-8" + value: "UTF-8" + # 통일된 콘솔 패턴 - 모든 로그에 RequestId 포함 + - name: "console-layout-pattern" + value: "%highlight{[%-5level]} [%X{id}] %d{MM-dd HH:mm:ss} [%t] %n %msg%n%n" + # 파일용 상세 패턴 - RequestId 포함 + - name: "file-layout-pattern" + value: "[%X{id}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" + # 로그 파일 경로들 + - name: "info-log" + value: ${log-path}/user-service/info.log + - name: "error-log" + value: ${log-path}/user-service/error.log + - name: "auth-log" + value: ${log-path}/user-service/auth.log + - name: "json-log" + value: ${log-path}/user-service/json-info.log + + # [Appenders] 로그 기록방식 정의 + Appenders: + # 통일된 콘솔 출력 + Console: + name: console-appender + target: SYSTEM_OUT + PatternLayout: + pattern: ${console-layout-pattern} + + # 롤링 파일 로그 + RollingFile: + name: rolling-file-appender + fileName: ${log-path}/rolling-file.log + filePattern: "logs/archive/rolling-file.log.%d{yyyy-MM-dd-hh-mm}_%i.gz" + PatternLayout: + charset: ${charset-UTF-8} + pattern: ${file-layout-pattern} + Policies: + SizeBasedTriggeringPolicy: + size: "200KB" + TimeBasedTriggeringPolicy: + interval: "1" + DefaultRollOverStrategy: + max: "30" + fileIndex: "max" + + # 파일 로그들 + File: + - name: file-info-appender + fileName: ${info-log} + PatternLayout: + pattern: ${file-layout-pattern} + - name: file-error-appender + fileName: ${error-log} + PatternLayout: + pattern: ${file-layout-pattern} + - name: file-auth-appender + fileName: ${auth-log} + PatternLayout: + pattern: ${file-layout-pattern} + - name: file-json-info-appender + fileName: ${json-log} + PatternLayout: + pattern: ${file-layout-pattern} + + # [Loggers] 로그 출력 범위를 정의 + Loggers: + # [Loggers - Root] 모든 로그를 기록하는 최상위 로그를 정의 + Root: + level: OFF + AppenderRef: + - ref: console-appender + - ref: rolling-file-appender + + # [Loggers - Loggers] 특정 패키지나 클래스에 대한 로그를 정의 + Logger: + # 1. Spring Framework 로그 + - name: org.springframework + additivity: "false" + level: DEBUG + AppenderRef: + - ref: console-appender + - ref: file-info-appender + - ref: file-error-appender + + # 2. 애플리케이션 로그 + - name: com.movement.mvp + additivity: "false" + level: TRACE + AppenderRef: + - ref: console-appender + - ref: file-info-appender + - ref: file-error-appender + + # 3. HikariCP 로그 비활성화 + - name: com.zaxxer.hikari + level: OFF + + # 4. Spring Security 로그 - 인증/인가 추적에 중요 + - name: org.springframework.security + level: DEBUG + additivity: "false" + AppenderRef: + - ref: console-appender + - ref: file-auth-appender + + # 5. 웹 요청 로그 - 요청 처리 과정 추적 + - name: org.springframework.web + level: DEBUG + additivity: "false" + AppenderRef: + - ref: console-appender + - ref: file-info-appender + + # 6. 트랜잭션 로그 - DB 작업 추적 + - name: org.springframework.transaction + level: DEBUG + additivity: "false" + AppenderRef: + - ref: console-appender + - ref: file-info-appender \ No newline at end of file diff --git a/apps/user-service/src/main/resources/log4j2-test.yml b/apps/user-service/src/main/resources/log4j2-test.yml new file mode 100644 index 00000000..05333338 --- /dev/null +++ b/apps/user-service/src/main/resources/log4j2-test.yml @@ -0,0 +1,70 @@ +Configuration: + name: test + + properties: + property: + - name: "log-path" + value: "./logs" + - name: "charset-UTF-8" + value: "UTF-8" + # 통일된 콘솔 패턴 - 모든 로그에 RequestId 포함 + - name: "console-layout-pattern" + value: "%highlight{[%-5level]} [%X{id}] %d{MM-dd HH:mm:ss} [%t] %n %msg%n%n" + + # [Appenders] 로그 기록방식 정의 + Appenders: + # 통일된 콘솔 출력 + Console: + name: console-appender + target: SYSTEM_OUT + PatternLayout: + pattern: ${console-layout-pattern} + + # [Loggers] 로그 출력 범위를 정의 + Loggers: + # [Loggers - Root] 모든 로그를 기록하는 최상위 로그를 정의 + Root: + level: OFF + AppenderRef: + - ref: console-appender + + # [Loggers - Loggers] 특정 패키지나 클래스에 대한 로그를 정의 + Logger: + # 1. Spring Framework 로그 + - name: org.springframework + additivity: "false" + level: INFO + AppenderRef: + - ref: console-appender + + # 2. 애플리케이션 로그 + - name: com.movement + additivity: "false" + level: INFO + AppenderRef: + - ref: console-appender + + # 3. HikariCP 로그 비활성화 + - name: com.zaxxer.hikari + level: OFF + + # 4. Spring Security 로그 - 인증/인가 추적에 중요 + - name: org.springframework.security + level: INFO + additivity: "false" + AppenderRef: + - ref: console-appender + + # 5. 웹 요청 로그 - 요청 처리 과정 추적 + - name: org.springframework.web + level: INFO + additivity: "false" + AppenderRef: + - ref: console-appender + + # 6. 트랜잭션 로그 - DB 작업 추적 + - name: org.springframework.transaction + level: INFO + additivity: "false" + AppenderRef: + - ref: console-appender \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml new file mode 100644 index 00000000..68be89f9 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java b/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java new file mode 100644 index 00000000..a3dd2e77 --- /dev/null +++ b/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java @@ -0,0 +1,81 @@ +package com.gltkorea.icebang; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Optional; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +import com.gltkorea.icebang.dto.UserDto; +import com.gltkorea.icebang.mapper.UserMapper; + +@SpringBootTest +@Import(TestcontainersConfiguration.class) +@AutoConfigureTestDatabase(replace = Replace.NONE) +@ActiveProfiles("test") // application-test.yml 설정을 활성화 +@Transactional // 테스트 후 데이터 롤백 +@Sql( + scripts = {"classpath:sql/create-schema.sql", "classpath:sql/insert-user-data.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +class DatabaseConnectionTest { + + @Autowired private DataSource dataSource; + + @Autowired private UserMapper userMapper; // JPA Repository 대신 MyBatis Mapper를 주입 + + @Test + @DisplayName("DataSource를 통해 DB 커넥션을 성공적으로 얻을 수 있다.") + void canGetDatabaseConnection() { + try (Connection connection = dataSource.getConnection()) { + assertThat(connection).isNotNull(); + assertThat(connection.isValid(1)).isTrue(); + System.out.println("DB Connection successful: " + connection.getMetaData().getURL()); + } catch (SQLException e) { + org.junit.jupiter.api.Assertions.fail("Failed to get database connection", e); + } + } + + @Test + @DisplayName("MyBatis Mapper를 통해 '홍길동' 사용자를 이메일로 조회") + void findUserByEmailWithMyBatis() { + // given + String testEmail = "hong.gildong@example.com"; + + // when + Optional foundUser = userMapper.findByEmail(testEmail); + + // then + // 사용자가 존재하고, 이름이 '홍길동'인지 확인 + assertThat(foundUser).isPresent(); + assertThat(foundUser.get().getName()).isEqualTo("홍길동"); + System.out.println("Successfully found user with MyBatis: " + foundUser.get().getName()); + } + + @Test + @DisplayName("샘플 데이터가 올바르게 삽입되었는지 확인") + void verifyAllSampleDataInserted() { + // 사용자 데이터 확인 + Optional hong = userMapper.findByEmail("hong.gildong@example.com"); + assertThat(hong).isPresent(); + assertThat(hong.get().getName()).isEqualTo("홍길동"); + + Optional kim = userMapper.findByEmail("kim.chulsu@example.com"); + assertThat(kim).isPresent(); + assertThat(kim.get().getName()).isEqualTo("김철수"); + + System.out.println("샘플 데이터 삽입 성공 - 홍길동, 김철수 확인"); + } +} diff --git a/apps/user-service/src/test/java/com/gltkorea/icebang/TestUserServiceApplication.java b/apps/user-service/src/test/java/com/gltkorea/icebang/TestUserServiceApplication.java index 624e8df7..f53fa0a9 100644 --- a/apps/user-service/src/test/java/com/gltkorea/icebang/TestUserServiceApplication.java +++ b/apps/user-service/src/test/java/com/gltkorea/icebang/TestUserServiceApplication.java @@ -4,8 +4,9 @@ public class TestUserServiceApplication { - public static void main(String[] args) { - SpringApplication.from(UserServiceApplication::main).with(TestcontainersConfiguration.class).run(args); - } - + public static void main(String[] args) { + SpringApplication.from(UserServiceApplication::main) + .with(TestcontainersConfiguration.class) + .run(args); + } } diff --git a/apps/user-service/src/test/java/com/gltkorea/icebang/TestcontainersConfiguration.java b/apps/user-service/src/test/java/com/gltkorea/icebang/TestcontainersConfiguration.java index a302d914..bbe8ed02 100644 --- a/apps/user-service/src/test/java/com/gltkorea/icebang/TestcontainersConfiguration.java +++ b/apps/user-service/src/test/java/com/gltkorea/icebang/TestcontainersConfiguration.java @@ -3,6 +3,4 @@ import org.springframework.boot.test.context.TestConfiguration; @TestConfiguration(proxyBeanMethods = false) -class TestcontainersConfiguration { - -} +class TestcontainersConfiguration {} diff --git a/apps/user-service/src/test/java/com/gltkorea/icebang/UserServiceApplicationTests.java b/apps/user-service/src/test/java/com/gltkorea/icebang/UserServiceApplicationTests.java index c83e584d..26cfc86b 100644 --- a/apps/user-service/src/test/java/com/gltkorea/icebang/UserServiceApplicationTests.java +++ b/apps/user-service/src/test/java/com/gltkorea/icebang/UserServiceApplicationTests.java @@ -8,8 +8,6 @@ @SpringBootTest class UserServiceApplicationTests { - @Test - void contextLoads() { - } - + @Test + void contextLoads() {} } diff --git a/apps/user-service/src/test/resources/sql/create-schema.sql b/apps/user-service/src/test/resources/sql/create-schema.sql new file mode 100644 index 00000000..5980c3ab --- /dev/null +++ b/apps/user-service/src/test/resources/sql/create-schema.sql @@ -0,0 +1,98 @@ +-- 테이블 DROP (재생성을 위해 기존 테이블을 삭제) +DROP TABLE IF EXISTS "ROLE_PERMISSION"; +DROP TABLE IF EXISTS "USER_ROLE"; +DROP TABLE IF EXISTS "PERMISSION"; +DROP TABLE IF EXISTS "ROLE"; +DROP TABLE IF EXISTS "USER_GROUP_INFO"; +DROP TABLE IF EXISTS "GROUP_INFO"; +DROP TABLE IF EXISTS "USER"; + + +-- 사용자 정보 (외부 노출 가능성 높음 -> UUID) +CREATE TABLE "USER" ( + "user_id" VARCHAR(36) NOT NULL, + "name" VARCHAR(100) NULL, + "email" VARCHAR(255) NULL UNIQUE, + "password" VARCHAR(255) NULL, + "phone_number" VARCHAR(50) NULL, + "fax_number" VARCHAR(50) NULL, + "zip_code" VARCHAR(20) NULL, + "main_address" VARCHAR(255) NULL, + "detail_address" VARCHAR(255) NULL, + "recommender_id" VARCHAR(36) NULL, + "resident_number" VARCHAR(100) NULL, + "corporate_number" VARCHAR(100) NULL, + "business_number" VARCHAR(100) NULL, + "type" VARCHAR(50) NULL, + "department" VARCHAR(100) NULL, + "job_title" VARCHAR(50) NULL, + "grade" VARCHAR(50) NULL, + "status" VARCHAR(50) NULL, + "joined_at" TIMESTAMP NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("user_id") +); + +CREATE TABLE "GROUP_INFO" ( + "group_info_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "name" VARCHAR(255) NULL, + "description" TEXT NULL, + "status" VARCHAR(50) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "ROLE" ( + "role_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "name" VARCHAR(50) NULL, + "code" VARCHAR(50) NULL UNIQUE, + "description" VARCHAR(255) NULL, + "status" VARCHAR(50) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "PERMISSION" ( + "permission_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "name" VARCHAR(50) NULL, + "code" VARCHAR(50) NULL UNIQUE, + "resource" VARCHAR(50) NULL, + "action" VARCHAR(50) NULL, + "description" VARCHAR(255) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "USER_GROUP_INFO" ( + "user_group_info_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "user_id" VARCHAR(36) NOT NULL, -- USER 테이블 참조 + "group_info_id" BIGINT NOT NULL, -- GROUP_INFO 테이블 참조 + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("group_info_id") REFERENCES "GROUP_INFO" ("group_info_id"), + UNIQUE ("user_id", "group_info_id") +); + +CREATE TABLE "USER_ROLE" ( + "user_role_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "user_id" VARCHAR(36) NOT NULL, -- USER 테이블 참조 + "role_id" BIGINT NOT NULL, -- ROLE 테이블 참조 + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), + UNIQUE ("user_id", "role_id") +); + +CREATE TABLE "ROLE_PERMISSION" ( + "role_permission_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "role_id" BIGINT NOT NULL, -- ROLE 테이블 참조 + "permission_id" BIGINT NOT NULL, -- PERMISSION 테이블 참조 + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), + FOREIGN KEY ("permission_id") REFERENCES "PERMISSION" ("permission_id"), + UNIQUE ("role_id", "permission_id") +); \ No newline at end of file diff --git a/apps/user-service/src/test/resources/sql/insert-user-data.sql b/apps/user-service/src/test/resources/sql/insert-user-data.sql new file mode 100644 index 00000000..95a24551 --- /dev/null +++ b/apps/user-service/src/test/resources/sql/insert-user-data.sql @@ -0,0 +1,38 @@ +INSERT INTO "USER" ("user_id", "name", "email", "password", "phone_number", "type", "status", "joined_at") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', '홍길동', 'hong.gildong@example.com', 'hashed_password_1', '010-1234-5678', 'INDIVIDUAL', 'ACTIVE', NOW()), + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', '김철수', 'kim.chulsu@example.com', 'hashed_1b590e829a28', '010-9876-5432', 'INDIVIDUAL', 'ACTIVE', NOW()); + +INSERT INTO "GROUP_INFO" ("name", "description", "status") +VALUES + ('개발팀', '애플리케이션 개발 그룹', 'ACTIVE'), -- ID 1로 생성됨 + ('기획팀', '프로젝트 기획 그룹', 'ACTIVE'); -- ID 2로 생성됨 + +INSERT INTO "USER_GROUP_INFO" ("user_id", "group_info_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 1), -- 홍길동 -> 개발팀 + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 2); -- 김철수 -> 기획팀 + +INSERT INTO "ROLE" ("name", "code", "description", "status") +VALUES + ('관리자', 'ADMIN', '모든 권한을 가진 역할', 'ACTIVE'), -- ID 1로 생성됨 + ('일반 사용자', 'USER', '기본 권한을 가진 역할', 'ACTIVE'); -- ID 2로 생성됨 + +INSERT INTO "PERMISSION" ("name", "code", "resource", "action", "description") +VALUES + ('사용자 정보 읽기', 'USER_READ', 'USER', 'READ', '사용자 정보 조회 권한'), -- ID 1로 생성됨 + ('사용자 정보 수정', 'USER_WRITE', 'USER', 'WRITE', '사용자 정보 수정 권한'), -- ID 2로 생성됨 + ('로그인', 'AUTH_LOGIN', 'AUTH', 'LOGIN', '로그인 권한'); -- ID 3으로 생성됨 + +INSERT INTO "USER_ROLE" ("user_id", "role_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 1), -- 홍길동 -> 관리자 + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 2); -- 김철수 -> 일반 사용자 + +INSERT INTO "ROLE_PERMISSION" ("role_id", "permission_id") +VALUES + (1, 1), -- 관리자 -> 사용자 정보 읽기 + (1, 2), -- 관리자 -> 사용자 정보 수정 + (1, 3), -- 관리자 -> 로그인 + (2, 1), -- 일반 사용자 -> 사용자 정보 읽기 + (2, 3); -- 일반 사용자 -> 로그인 \ No newline at end of file diff --git a/docker/local/docker-compose.yml b/docker/local/docker-compose.yml new file mode 100644 index 00000000..62dabdd0 --- /dev/null +++ b/docker/local/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + postgres: + image: postgres:15 + container_name: postgres_db + restart: unless-stopped + environment: + POSTGRES_DB: pre_process + POSTGRES_USER: postgres + POSTGRES_PASSWORD: qwer1234 + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-scripts:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + pgadmin: + image: dpage/pgadmin4:latest + container_name: pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: qwer1234 + ports: + - "8888:80" + volumes: + - pgadmin_data:/var/lib/pgadmin + depends_on: + - postgres + +volumes: + postgres_data: + pgadmin_data: \ No newline at end of file diff --git a/docker/local/init-scripts/create-schema.sql b/docker/local/init-scripts/create-schema.sql new file mode 100644 index 00000000..5980c3ab --- /dev/null +++ b/docker/local/init-scripts/create-schema.sql @@ -0,0 +1,98 @@ +-- 테이블 DROP (재생성을 위해 기존 테이블을 삭제) +DROP TABLE IF EXISTS "ROLE_PERMISSION"; +DROP TABLE IF EXISTS "USER_ROLE"; +DROP TABLE IF EXISTS "PERMISSION"; +DROP TABLE IF EXISTS "ROLE"; +DROP TABLE IF EXISTS "USER_GROUP_INFO"; +DROP TABLE IF EXISTS "GROUP_INFO"; +DROP TABLE IF EXISTS "USER"; + + +-- 사용자 정보 (외부 노출 가능성 높음 -> UUID) +CREATE TABLE "USER" ( + "user_id" VARCHAR(36) NOT NULL, + "name" VARCHAR(100) NULL, + "email" VARCHAR(255) NULL UNIQUE, + "password" VARCHAR(255) NULL, + "phone_number" VARCHAR(50) NULL, + "fax_number" VARCHAR(50) NULL, + "zip_code" VARCHAR(20) NULL, + "main_address" VARCHAR(255) NULL, + "detail_address" VARCHAR(255) NULL, + "recommender_id" VARCHAR(36) NULL, + "resident_number" VARCHAR(100) NULL, + "corporate_number" VARCHAR(100) NULL, + "business_number" VARCHAR(100) NULL, + "type" VARCHAR(50) NULL, + "department" VARCHAR(100) NULL, + "job_title" VARCHAR(50) NULL, + "grade" VARCHAR(50) NULL, + "status" VARCHAR(50) NULL, + "joined_at" TIMESTAMP NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("user_id") +); + +CREATE TABLE "GROUP_INFO" ( + "group_info_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "name" VARCHAR(255) NULL, + "description" TEXT NULL, + "status" VARCHAR(50) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "ROLE" ( + "role_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "name" VARCHAR(50) NULL, + "code" VARCHAR(50) NULL UNIQUE, + "description" VARCHAR(255) NULL, + "status" VARCHAR(50) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "PERMISSION" ( + "permission_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "name" VARCHAR(50) NULL, + "code" VARCHAR(50) NULL UNIQUE, + "resource" VARCHAR(50) NULL, + "action" VARCHAR(50) NULL, + "description" VARCHAR(255) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "USER_GROUP_INFO" ( + "user_group_info_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "user_id" VARCHAR(36) NOT NULL, -- USER 테이블 참조 + "group_info_id" BIGINT NOT NULL, -- GROUP_INFO 테이블 참조 + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("group_info_id") REFERENCES "GROUP_INFO" ("group_info_id"), + UNIQUE ("user_id", "group_info_id") +); + +CREATE TABLE "USER_ROLE" ( + "user_role_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "user_id" VARCHAR(36) NOT NULL, -- USER 테이블 참조 + "role_id" BIGINT NOT NULL, -- ROLE 테이블 참조 + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), + UNIQUE ("user_id", "role_id") +); + +CREATE TABLE "ROLE_PERMISSION" ( + "role_permission_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "role_id" BIGINT NOT NULL, -- ROLE 테이블 참조 + "permission_id" BIGINT NOT NULL, -- PERMISSION 테이블 참조 + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), + FOREIGN KEY ("permission_id") REFERENCES "PERMISSION" ("permission_id"), + UNIQUE ("role_id", "permission_id") +); \ No newline at end of file diff --git a/docker/local/init-scripts/insert-user-data.sql b/docker/local/init-scripts/insert-user-data.sql new file mode 100644 index 00000000..95a24551 --- /dev/null +++ b/docker/local/init-scripts/insert-user-data.sql @@ -0,0 +1,38 @@ +INSERT INTO "USER" ("user_id", "name", "email", "password", "phone_number", "type", "status", "joined_at") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', '홍길동', 'hong.gildong@example.com', 'hashed_password_1', '010-1234-5678', 'INDIVIDUAL', 'ACTIVE', NOW()), + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', '김철수', 'kim.chulsu@example.com', 'hashed_1b590e829a28', '010-9876-5432', 'INDIVIDUAL', 'ACTIVE', NOW()); + +INSERT INTO "GROUP_INFO" ("name", "description", "status") +VALUES + ('개발팀', '애플리케이션 개발 그룹', 'ACTIVE'), -- ID 1로 생성됨 + ('기획팀', '프로젝트 기획 그룹', 'ACTIVE'); -- ID 2로 생성됨 + +INSERT INTO "USER_GROUP_INFO" ("user_id", "group_info_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 1), -- 홍길동 -> 개발팀 + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 2); -- 김철수 -> 기획팀 + +INSERT INTO "ROLE" ("name", "code", "description", "status") +VALUES + ('관리자', 'ADMIN', '모든 권한을 가진 역할', 'ACTIVE'), -- ID 1로 생성됨 + ('일반 사용자', 'USER', '기본 권한을 가진 역할', 'ACTIVE'); -- ID 2로 생성됨 + +INSERT INTO "PERMISSION" ("name", "code", "resource", "action", "description") +VALUES + ('사용자 정보 읽기', 'USER_READ', 'USER', 'READ', '사용자 정보 조회 권한'), -- ID 1로 생성됨 + ('사용자 정보 수정', 'USER_WRITE', 'USER', 'WRITE', '사용자 정보 수정 권한'), -- ID 2로 생성됨 + ('로그인', 'AUTH_LOGIN', 'AUTH', 'LOGIN', '로그인 권한'); -- ID 3으로 생성됨 + +INSERT INTO "USER_ROLE" ("user_id", "role_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 1), -- 홍길동 -> 관리자 + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 2); -- 김철수 -> 일반 사용자 + +INSERT INTO "ROLE_PERMISSION" ("role_id", "permission_id") +VALUES + (1, 1), -- 관리자 -> 사용자 정보 읽기 + (1, 2), -- 관리자 -> 사용자 정보 수정 + (1, 3), -- 관리자 -> 로그인 + (2, 1), -- 일반 사용자 -> 사용자 정보 읽기 + (2, 3); -- 일반 사용자 -> 로그인 \ No newline at end of file diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml new file mode 100644 index 00000000..3d417fe8 --- /dev/null +++ b/docker/production/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.9" + +services: + user-service: + image: ghcr.io/kernel180-be12/final-4team-icebang/user-service:latest + container_name: user-service + restart: always + ports: + - "80:8080" + env_file: + - .env + networks: + - app-network + +networks: + app-network: + driver: bridge