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 index e69de29b..2dd72b02 100644 --- a/apps/pre-processing-service/app/api/router.py +++ 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 + } \ No newline at end of file diff --git a/apps/pre-processing-service/app/core/config.py b/apps/pre-processing-service/app/core/config.py index e69de29b..134d0430 100644 --- a/apps/pre-processing-service/app/core/config.py +++ 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/core/decorators.py b/apps/pre-processing-service/app/core/decorators.py deleted file mode 100644 index 6ded6f9d..00000000 --- a/apps/pre-processing-service/app/core/decorators.py +++ /dev/null @@ -1,53 +0,0 @@ -from fastapi import FastAPI, Request -from loguru import logger -import functools -import time - - -# 2. 로그를 위한 커스텀 데코레이터 정의 -def log_api_call(func): - """ - FastAPI 라우트 함수에 대한 로깅을 수행하는 데코레이터입니다. - 함수 호출 시간, 인자, 반환값, 발생한 예외 등을 기록합니다. - """ - - @functools.wraps(func) - async def wrapper(*args, **kwargs): - # 데코레이터가 적용된 함수에 대한 정보를 가져옵니다. - # FastAPI 라우트 핸들러의 경우, 첫 번째 인자는 Request 객체입니다. - request: Request = kwargs.get('request', None) or args[0] - - # 함수 실행 전 로그 기록 - logger.info( - "API 호출 시작: URL='{}' 메서드='{}' 함수='{}'", - request.url, request.method, func.__name__ - ) - - start_time = time.time() - result = None - - try: - # 원래 함수 실행 - result = await func(*args, **kwargs) - return result - - except Exception as e: - # 예외 발생 시 로그 기록 - elapsed_time = time.time() - start_time - logger.error( - "API 호출 실패: URL='{}' 메서드='{}' 함수='{}' 예외='{}' ({:.4f}s)", - request.url, request.method, func.__name__, e, elapsed_time - ) - # 예외를 다시 발생시켜 FastAPI의 기본 예외 핸들러가 처리하도록 함 - raise - - finally: - # 함수 실행 완료(성공 또는 실패) 후 로그 기록 - if result is not None: - elapsed_time = time.time() - start_time - logger.success( - "API 호출 성공: URL='{}' 메서드='{}' 함수='{}' 반환값='{}' ({:.4f}s)", - request.url, request.method, func.__name__, result, elapsed_time - ) - - return wrapper \ No newline at end of file diff --git a/apps/pre-processing-service/app/db/db_connecter.py b/apps/pre-processing-service/app/db/db_connecter.py index e69de29b..0ed48b04 100644 --- a/apps/pre-processing-service/app/db/db_connecter.py +++ 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 index 6d7c6d96..2ca44875 100644 --- a/apps/pre-processing-service/app/main.py +++ b/apps/pre-processing-service/app/main.py @@ -1,13 +1,34 @@ +# main.py +import uvicorn from fastapi import FastAPI +from starlette.exceptions import HTTPException as StarletteHTTPException +from fastapi.exceptions import RequestValidationError -app = FastAPI() +# --- 애플리케이션 구성 요소 임포트 --- +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" +) -@app.get("/") -async def root(): - return {"message": "Hello World"} +# --- 예외 핸들러 등록 --- +# 등록 순서가 중요합니다: 구체적인 예외부터 등록하고 가장 일반적인 예외(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.get("/hello/{name}") -async def say_hello(name: str): - return {"message": f"Hello {name}"} +# --- 라우터 등록 --- +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/poetry.lock b/apps/pre-processing-service/poetry.lock index 7a1d9857..961f44e5 100644 --- a/apps/pre-processing-service/poetry.lock +++ b/apps/pre-processing-service/poetry.lock @@ -60,6 +60,20 @@ files = [ {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" @@ -109,6 +123,18 @@ files = [ [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" @@ -128,6 +154,34 @@ 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" @@ -262,6 +316,82 @@ files = [ [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" @@ -358,4 +488,4 @@ 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 = "f502f0b2a368d47796eadbf214561cf67b4bb1963815da5b09cb539ba2ff8371" +content-hash = "845e1778efdd87512efdd30eb0ba01aa1383061f662ccc3faa17ab1f8cebde5b" diff --git a/apps/pre-processing-service/pyproject.toml b/apps/pre-processing-service/pyproject.toml index b6afdde3..5a2017c3 100644 --- a/apps/pre-processing-service/pyproject.toml +++ b/apps/pre-processing-service/pyproject.toml @@ -10,7 +10,10 @@ 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)" + "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)" ] diff --git a/apps/user-service/Dockerfile b/apps/user-service/Dockerfile index e69de29b..3fb9d854 100644 --- a/apps/user-service/Dockerfile +++ b/apps/user-service/Dockerfile @@ -0,0 +1,42 @@ +# 1단계: 빌드 스테이지 +# Java 21 JDK가 포함된 경량 이미지를 사용합니다. +# 이 단계에서 애플리케이션을 빌드합니다. +FROM openjdk:21-jdk-slim AS builder + +# 컨테이너 내부에 작업 디렉토리를 생성하고 설정합니다. +WORKDIR /app + +# Gradle Wrapper, 설정 파일, 소스 코드를 복사합니다. +# Docker의 레이어 캐싱을 활용하여 빌드 속도를 높입니다. +COPY gradlew . +COPY gradle/ gradle/ +COPY build.gradle . +COPY settings.gradle . + +# 애플리케이션 소스 코드를 복사합니다. +COPY src src + +# 애플리케이션을 빌드하여 실행 가능한 JAR 파일을 만듭니다. +# `-x test`는 이미지 빌드 시 테스트를 건너뛰는 명령입니다. +RUN ./gradlew clean build -x test + +--- + +# 2단계: 실행 스테이지 +# 애플리케이션 실행에 필요한 Java 21 JRE만 포함된 경량 이미지를 사용합니다. +FROM openjdk:21-jre-slim + +# 컨테이너 내부의 작업 디렉토리를 설정합니다. +WORKDIR /app + +# 빌드 스테이지에서 생성된 JAR 파일을 복사합니다. +# `--from=builder` 옵션을 사용하여 첫 번째 단계에서 빌드된 JAR만 가져옵니다. +# 파일명은 `group`, `version`에 따라 `glt-korea-0.0.1-SNAPSHOT.jar`가 되므로, +# 이를 `app.jar`라는 간단한 이름으로 변경합니다. +COPY --from=builder /app/build/libs/glt-korea-0.0.1-SNAPSHOT.jar ./app.jar + +# 애플리케이션이 외부 요청을 받을 포트를 노출합니다. +EXPOSE 8080 + +# 컨테이너 시작 시 실행될 명령어를 정의합니다. +CMD ["java", "-jar", "app.jar"] \ No newline at end of file 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 b5fcbef4..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,9 +1,11 @@ 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) { 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 index fb8125bd..8cae624c 100644 --- a/apps/user-service/src/main/resources/application-develop.yml +++ b/apps/user-service/src/main/resources/application-develop.yml @@ -8,7 +8,7 @@ spring: datasource: url: jdbc:postgresql://localhost:5432/pre_process username: postgres - password: password123 + password: qwer1234 driver-class-name: org.postgresql.Driver hikari: @@ -19,21 +19,27 @@ spring: minimum-idle: 5 pool-name: HikariCP-MyBatis - # JPA/Hibernate 설정 - jpa: - hibernate: - ddl-auto: update # create, create-drop, update, validate, none - show-sql: true - format-sql: true - database: postgresql - database-platform: org.hibernate.dialect.PostgreSQLDialect - properties: - hibernate: - format_sql: true - use_sql_comments: true - jdbc: - lob: - non_contextual_creation: true +# # JPA/Hibernate 설정 +# jpa: +# hibernate: +# ddl-auto: update # create, create-drop, update, validate, none +# show-sql: true +# format-sql: true +# database: postgresql +# database-platform: org.hibernate.dialect.PostgreSQLDialect +# properties: +# hibernate: +# format_sql: true +# use_sql_comments: true +# jdbc: +# lob: +# non_contextual_creation: true + +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-test.yml b/apps/user-service/src/main/resources/application-test.yml index e69de29b..63217124 100644 --- a/apps/user-service/src/main/resources/application-test.yml +++ 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/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/resources/sql/create-schema.sql b/apps/user-service/src/test/resources/sql/create-schema.sql new file mode 100644 index 00000000..b9846fca --- /dev/null +++ b/apps/user-service/src/test/resources/sql/create-schema.sql @@ -0,0 +1,104 @@ +-- 테이블 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"; +DROP TABLE IF EXISTS "GROUP_INFO"; +DROP TABLE IF EXISTS "USER"; + + +-- 사용자 정보 +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_id" VARCHAR(36) NOT NULL, + "name" VARCHAR(255) NULL, + "description" TEXT NULL, + "status" VARCHAR(50) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("group_id") +); + +-- 사용자-그룹 관계 +CREATE TABLE "USER_GROUP" ( + "user_id" VARCHAR(36) NOT NULL, + "group_id" VARCHAR(36) NOT NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("user_id", "group_id"), + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("group_id") REFERENCES "GROUP_INFO" ("group_id") +); + +-- 역할 정보 +CREATE TABLE "ROLE" ( + "role_id" VARCHAR(36) NOT NULL, + "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, + PRIMARY KEY ("role_id") +); + +-- 권한 정보 +CREATE TABLE "PERMISSION" ( + "permission_id" VARCHAR(36) NOT NULL, + "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, + PRIMARY KEY ("permission_id") +); + +-- 사용자-역할 관계 +CREATE TABLE "USER_ROLE" ( + "user_id" VARCHAR(36) NOT NULL, + "role_id" VARCHAR(36) NOT NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("user_id", "role_id"), + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id") +); + +-- 역할-권한 관계 +CREATE TABLE "ROLE_PERMISSION" ( + "role_id" VARCHAR(36) NOT NULL, + "permission_id" VARCHAR(36) NOT NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("role_id", "permission_id"), + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), + FOREIGN KEY ("permission_id") REFERENCES "PERMISSION" ("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..9a190b4e --- /dev/null +++ b/apps/user-service/src/test/resources/sql/insert-user-data.sql @@ -0,0 +1,39 @@ +-- 데이터 삽입 +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" ("group_id", "name", "description", "status") +VALUES + ('0b5c1c4e-5e2a-438d-8c1d-1d2a3e3b4d5a', '개발팀', '애플리케이션 개발 그룹', 'ACTIVE'), + ('5c3f7b2c-8a1e-45a8-9d2a-7e7f6a8e9d2b', '기획팀', '프로젝트 기획 그룹', 'ACTIVE'); + +INSERT INTO "USER_GROUP" ("user_id", "group_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', '0b5c1c4e-5e2a-438d-8c1d-1d2a3e3b4d5a'), + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', '5c3f7b2c-8a1e-45a8-9d2a-7e7f6a8e9d2b'); + +INSERT INTO "ROLE" ("role_id", "name", "code", "description", "status") +VALUES + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', '관리자', 'ADMIN', '모든 권한을 가진 역할', 'ACTIVE'), + ('d1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b', '일반 사용자', 'USER', '기본 권한을 가진 역할', 'ACTIVE'); + +INSERT INTO "PERMISSION" ("permission_id", "name", "code", "resource", "action", "description") +VALUES + ('c3f5a2b8-7e1d-4c9a-8b1d-2e3f4a5b6c7d', '사용자 정보 읽기', 'USER_READ', 'USER', 'READ', '사용자 정보 조회 권한'), + ('b5c6a7d8-1e2f-3a4b-5c6d-7e8f9a0b1c2d', '사용자 정보 수정', 'USER_WRITE', 'USER', 'WRITE', '사용자 정보 수정 권한'), + ('a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', '로그인', 'AUTH_LOGIN', 'AUTH', 'LOGIN', '로그인 권한'); + +INSERT INTO "USER_ROLE" ("user_id", "role_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 'e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e'), + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 'd1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b'); + +INSERT INTO "ROLE_PERMISSION" ("role_id", "permission_id") +VALUES + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', 'c3f5a2b8-7e1d-4c9a-8b1d-2e3f4a5b6c7d'), + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', 'b5c6a7d8-1e2f-3a4b-5c6d-7e8f9a0b1c2d'), + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d'), + ('d1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b', 'c3f5a2b8-7e1d-4c9a-8b1d-2e3f4a5b6c7d'), + ('d1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b', 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d'); \ 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..b9846fca --- /dev/null +++ b/docker/local/init-scripts/create-schema.sql @@ -0,0 +1,104 @@ +-- 테이블 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"; +DROP TABLE IF EXISTS "GROUP_INFO"; +DROP TABLE IF EXISTS "USER"; + + +-- 사용자 정보 +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_id" VARCHAR(36) NOT NULL, + "name" VARCHAR(255) NULL, + "description" TEXT NULL, + "status" VARCHAR(50) NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("group_id") +); + +-- 사용자-그룹 관계 +CREATE TABLE "USER_GROUP" ( + "user_id" VARCHAR(36) NOT NULL, + "group_id" VARCHAR(36) NOT NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("user_id", "group_id"), + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("group_id") REFERENCES "GROUP_INFO" ("group_id") +); + +-- 역할 정보 +CREATE TABLE "ROLE" ( + "role_id" VARCHAR(36) NOT NULL, + "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, + PRIMARY KEY ("role_id") +); + +-- 권한 정보 +CREATE TABLE "PERMISSION" ( + "permission_id" VARCHAR(36) NOT NULL, + "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, + PRIMARY KEY ("permission_id") +); + +-- 사용자-역할 관계 +CREATE TABLE "USER_ROLE" ( + "user_id" VARCHAR(36) NOT NULL, + "role_id" VARCHAR(36) NOT NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("user_id", "role_id"), + FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id") +); + +-- 역할-권한 관계 +CREATE TABLE "ROLE_PERMISSION" ( + "role_id" VARCHAR(36) NOT NULL, + "permission_id" VARCHAR(36) NOT NULL, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("role_id", "permission_id"), + FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), + FOREIGN KEY ("permission_id") REFERENCES "PERMISSION" ("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..9a190b4e --- /dev/null +++ b/docker/local/init-scripts/insert-user-data.sql @@ -0,0 +1,39 @@ +-- 데이터 삽입 +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" ("group_id", "name", "description", "status") +VALUES + ('0b5c1c4e-5e2a-438d-8c1d-1d2a3e3b4d5a', '개발팀', '애플리케이션 개발 그룹', 'ACTIVE'), + ('5c3f7b2c-8a1e-45a8-9d2a-7e7f6a8e9d2b', '기획팀', '프로젝트 기획 그룹', 'ACTIVE'); + +INSERT INTO "USER_GROUP" ("user_id", "group_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', '0b5c1c4e-5e2a-438d-8c1d-1d2a3e3b4d5a'), + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', '5c3f7b2c-8a1e-45a8-9d2a-7e7f6a8e9d2b'); + +INSERT INTO "ROLE" ("role_id", "name", "code", "description", "status") +VALUES + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', '관리자', 'ADMIN', '모든 권한을 가진 역할', 'ACTIVE'), + ('d1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b', '일반 사용자', 'USER', '기본 권한을 가진 역할', 'ACTIVE'); + +INSERT INTO "PERMISSION" ("permission_id", "name", "code", "resource", "action", "description") +VALUES + ('c3f5a2b8-7e1d-4c9a-8b1d-2e3f4a5b6c7d', '사용자 정보 읽기', 'USER_READ', 'USER', 'READ', '사용자 정보 조회 권한'), + ('b5c6a7d8-1e2f-3a4b-5c6d-7e8f9a0b1c2d', '사용자 정보 수정', 'USER_WRITE', 'USER', 'WRITE', '사용자 정보 수정 권한'), + ('a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', '로그인', 'AUTH_LOGIN', 'AUTH', 'LOGIN', '로그인 권한'); + +INSERT INTO "USER_ROLE" ("user_id", "role_id") +VALUES + ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 'e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e'), + ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 'd1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b'); + +INSERT INTO "ROLE_PERMISSION" ("role_id", "permission_id") +VALUES + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', 'c3f5a2b8-7e1d-4c9a-8b1d-2e3f4a5b6c7d'), + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', 'b5c6a7d8-1e2f-3a4b-5c6d-7e8f9a0b1c2d'), + ('e2c3a5f9-8d1a-4b72-9c3f-4e3b2c1d8a1e', 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d'), + ('d1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b', 'c3f5a2b8-7e1d-4c9a-8b1d-2e3f4a5b6c7d'), + ('d1a2c3b4-5f6e-7d8c-9a0b-1c2d3e4f5a6b', 'a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d'); \ No newline at end of file