From dcaab49c6ae23e2ce690d50d6bf8a4dc09e5ed00 Mon Sep 17 00:00:00 2001 From: kakusiA Date: Fri, 22 Aug 2025 14:18:28 +0900 Subject: [PATCH 01/12] =?UTF-8?q?chore:=20FastApi=20logging=EC=A0=84?= =?UTF-8?q?=EC=97=AD=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pre-processing-service/app/api/router.py | 15 ++++++ .../app/core/decorators.py | 53 ------------------ .../app/decorators/__init__.py | 0 .../app/decorators/logging.py | 54 +++++++++++++++++++ apps/pre-processing-service/app/main.py | 24 +++++---- .../app/middleware/__init__.py | 0 6 files changed, 83 insertions(+), 63 deletions(-) delete mode 100644 apps/pre-processing-service/app/core/decorators.py create mode 100644 apps/pre-processing-service/app/decorators/__init__.py create mode 100644 apps/pre-processing-service/app/decorators/logging.py create mode 100644 apps/pre-processing-service/app/middleware/__init__.py diff --git a/apps/pre-processing-service/app/api/router.py b/apps/pre-processing-service/app/api/router.py index e69de29b..9f2572e1 100644 --- a/apps/pre-processing-service/app/api/router.py +++ b/apps/pre-processing-service/app/api/router.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter +from app.decorators.logging import log_api_call + +router = APIRouter() + +@router.get("/") +async def root(): + return {"message": "Hello World"} + + +@router.get("/hello/{name}") +# @log_api_call +async def say_hello(name: str): + return {"message": f"Hello {name}"} + 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/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..29eb2204 --- /dev/null +++ b/apps/pre-processing-service/app/decorators/logging.py @@ -0,0 +1,54 @@ +# app/decorators/logging.py + +from fastapi import Request +from loguru import logger +import functools +import time + + +def log_api_call(func): + @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] + + # 요청 정보를 로그로 기록 (request 객체가 있는 경우에만) + if request: + logger.info( + "API 호출 시작: URL='{}' 메서드='{}' 함수='{}'", + request.url, request.method, func.__name__ + ) + else: + logger.info("API 호출 시작: 함수='{}'", 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 + if request: + logger.error( + "API 호출 실패: URL='{}' 메서드='{}' 예외='{}' ({:.4f}s)", + request.url, request.method, e, elapsed_time + ) + else: + logger.error("API 호출 실패: 함수='{}' 예외='{}' ({:.4f}s)", func.__name__, e, elapsed_time) + raise + finally: + if result is not None: + elapsed_time = time.time() - start_time + if request: + logger.success( + "API 호출 성공: URL='{}' 메서드='{}' ({:.4f}s)", + request.url, request.method, elapsed_time + ) + else: + logger.success("API 호출 성공: 함수='{}' ({:.4f}s)", func.__name__, elapsed_time) + + return wrapper \ No newline at end of file diff --git a/apps/pre-processing-service/app/main.py b/apps/pre-processing-service/app/main.py index 6d7c6d96..85d86e27 100644 --- a/apps/pre-processing-service/app/main.py +++ b/apps/pre-processing-service/app/main.py @@ -1,13 +1,17 @@ -from fastapi import FastAPI - -app = FastAPI() +# main.py +from fastapi import FastAPI -@app.get("/") -async def root(): - return {"message": "Hello World"} - +# app/api/v1/items.py에서 라우터를 임포트합니다. +from app.api.router import router as api_router +from app.middleware.logging import LoggingMiddleware -@app.get("/hello/{name}") -async def say_hello(name: str): - return {"message": f"Hello {name}"} +# FastAPI 애플리케이션 인스턴스를 생성합니다. +app = FastAPI( + title="My FastAPI Project", + description="A demonstration of a well-structured FastAPI project.", + version="1.0.0" +) +app.add_middleware(LoggingMiddleware) +# APIRouter를 메인 앱에 포함시킵니다. +app.include_router(api_router, prefix="", tags=["api"]) \ No newline at end of file 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 From ec102d563c6f5396ad5806a441a2fd97d941961d Mon Sep 17 00:00:00 2001 From: kakusiA Date: Fri, 22 Aug 2025 14:19:46 +0900 Subject: [PATCH 02/12] =?UTF-8?q?chore:=20FastApi=20logging=20middleware?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/middleware/logging.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 apps/pre-processing-service/app/middleware/logging.py 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 From 931058659d329b465e3c04b4c2827c4997092cdd Mon Sep 17 00:00:00 2001 From: kakusiA Date: Sun, 24 Aug 2025 16:02:51 +0900 Subject: [PATCH 03/12] chore: FastApi global exception add --- .../app/api/__init__.py | 4 + apps/pre-processing-service/app/api/router.py | 12 +- .../app/decorators/logging.py | 56 +++++++-- .../app/errors/__init__.py | 0 .../app/errors/handlers.py | 34 ++++++ .../app/errors/messages.py | 17 +++ apps/pre-processing-service/app/main.py | 30 +++-- apps/pre-processing-service/poetry.lock | 108 +++++++++++++++++- apps/pre-processing-service/pyproject.toml | 4 +- 9 files changed, 243 insertions(+), 22 deletions(-) create mode 100644 apps/pre-processing-service/app/errors/__init__.py create mode 100644 apps/pre-processing-service/app/errors/handlers.py create mode 100644 apps/pre-processing-service/app/errors/messages.py diff --git a/apps/pre-processing-service/app/api/__init__.py b/apps/pre-processing-service/app/api/__init__.py index e69de29b..92573e7d 100644 --- a/apps/pre-processing-service/app/api/__init__.py +++ b/apps/pre-processing-service/app/api/__init__.py @@ -0,0 +1,4 @@ +# from fastapi import APIRouter +# from .v1.routes import router as v1_router +# router = APIRouter() +# router.include_router(v1_router, prefix="/v1") \ 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 9f2572e1..fb43ab0b 100644 --- a/apps/pre-processing-service/app/api/router.py +++ b/apps/pre-processing-service/app/api/router.py @@ -8,8 +8,18 @@ async def root(): return {"message": "Hello World"} -@router.get("/hello/{name}") +@router.get("/hello/{name}" , tags=["hello"]) # @log_api_call async def say_hello(name: str): return {"message": f"Hello {name}"} +# 이 엔드포인트는 테스트를 위해 예외를 발생시킵니다. +@router.get("/test-error") +def test_error(): + raise ValueError("이것은 테스트용 값 오류입니다.") + +# 특정 경로에서 의도적으로 에러 발생 +@router.get("/error") +async def trigger_error(): + result = 1 / 0 # ZeroDivisionError 발생 + return {"result": result} \ No newline at end of file diff --git a/apps/pre-processing-service/app/decorators/logging.py b/apps/pre-processing-service/app/decorators/logging.py index 29eb2204..145cb0a0 100644 --- a/apps/pre-processing-service/app/decorators/logging.py +++ b/apps/pre-processing-service/app/decorators/logging.py @@ -7,6 +7,11 @@ def log_api_call(func): + """ + FastAPI API 호출에 대한 상세 정보를 로깅하는 데코레이터입니다. + IP 주소, User-Agent, URL, 메서드, 실행 시간 등을 기록합니다. + """ + @functools.wraps(func) async def wrapper(*args, **kwargs): # 1. request 객체를 안전하게 가져옵니다. @@ -15,40 +20,69 @@ async def wrapper(*args, **kwargs): if request is None and args and isinstance(args[0], Request): request = args[0] - # 요청 정보를 로그로 기록 (request 객체가 있는 경우에만) + # 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='{}' 메서드='{}' 함수='{}'", - request.url, request.method, func.__name__ + "API 호출 시작: URL='{url}' 메서드='{method}' 함수='{func}' IP='{ip}' User-Agent='{user_agent}'", + **log_context ) else: - logger.info("API 호출 시작: 함수='{}'", func.__name__) + 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='{}' 메서드='{}' 예외='{}' ({:.4f}s)", - request.url, request.method, e, elapsed_time + "API 호출 실패: URL='{url}' 메서드='{method}' IP='{ip}' 예외='{exception}' ({elapsed})", + **log_context ) else: - logger.error("API 호출 실패: 함수='{}' 예외='{}' ({:.4f}s)", func.__name__, e, elapsed_time) - raise + 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='{}' 메서드='{}' ({:.4f}s)", - request.url, request.method, elapsed_time + "API 호출 성공: URL='{url}' 메서드='{method}' IP='{ip}' ({elapsed})", + **log_context ) else: - logger.success("API 호출 성공: 함수='{}' ({:.4f}s)", func.__name__, elapsed_time) + logger.success( + "API 호출 성공: 함수='{func}' ({elapsed})", + **log_context + ) return wrapper \ 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..3de738ac --- /dev/null +++ b/apps/pre-processing-service/app/errors/handlers.py @@ -0,0 +1,34 @@ +# 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 + +# HTTPException 핸들러 (예: 404, 403 등) +async def http_exception_handler(request: Request, exc: StarletteHTTPException): + return JSONResponse( + status_code=exc.status_code, + content={ + "error": get_error_message(exc.status_code, str(exc.detail)) + }, + ) + +# ValidationError 핸들러 (422) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "error": ERROR_MESSAGES[status.HTTP_422_UNPROCESSABLE_ENTITY], + "errors": exc.errors(), + }, + ) + +# 기타 모든 예외 +async def unhandled_exception_handler(request: Request, exc: Exception): + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": 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..96e0f39e --- /dev/null +++ b/apps/pre-processing-service/app/errors/messages.py @@ -0,0 +1,17 @@ +# 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이 있으면 우선""" + from app.errors.messages import ERROR_MESSAGES + 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 85d86e27..6a4cc676 100644 --- a/apps/pre-processing-service/app/main.py +++ b/apps/pre-processing-service/app/main.py @@ -1,17 +1,31 @@ # main.py +from fastapi import FastAPI, Request +from loguru import logger +import uvicorn +from starlette import status +from starlette.responses import JSONResponse -from fastapi import FastAPI - -# app/api/v1/items.py에서 라우터를 임포트합니다. from app.api.router import router as api_router from app.middleware.logging import LoggingMiddleware - # FastAPI 애플리케이션 인스턴스를 생성합니다. app = FastAPI( - title="My FastAPI Project", - description="A demonstration of a well-structured FastAPI project.", + title="pre-processing-service", + description="", version="1.0.0" ) +#미들 웨어 등록 app.add_middleware(LoggingMiddleware) -# APIRouter를 메인 앱에 포함시킵니다. -app.include_router(api_router, prefix="", tags=["api"]) \ No newline at end of file +#라우터 등록 +app.include_router(api_router, prefix="", tags=["api"]) + +# 모든 Exception을 처리하는 글로벌 핸들러 +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + logger.error(f"에러발생{exc}") + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"message": f"서버 내부 오류가 발생했습니312312321다: {exc}"}, + ) + +# if __name__ == "__main__": + # uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/apps/pre-processing-service/poetry.lock b/apps/pre-processing-service/poetry.lock index 7a1d9857..2d2b59ab 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,58 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[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 +464,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 = "20e9b26b37de9cfbf87c16440dddc9f46e21f890bc12280b5d6b96e86f38da69" diff --git a/apps/pre-processing-service/pyproject.toml b/apps/pre-processing-service/pyproject.toml index b6afdde3..1c919a62 100644 --- a/apps/pre-processing-service/pyproject.toml +++ b/apps/pre-processing-service/pyproject.toml @@ -10,7 +10,9 @@ 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)" ] From adc848df98b2e2b237b14c8de604c9f89210aa41 Mon Sep 17 00:00:00 2001 From: kakusia Date: Sun, 24 Aug 2025 18:08:22 +0900 Subject: [PATCH 04/12] =?UTF-8?q?chroe:=20FASTAPI=20=ED=86=B5=ED=95=A9=20e?= =?UTF-8?q?rror=20handler=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/__init__.py | 4 -- .../app/api/endpoints/__init__.py | 0 .../app/api/endpoints/embedding.py | 12 +++++ .../app/api/endpoints/processing.py | 12 +++++ .../app/api/endpoints/test.py | 35 +++++++++++++++ apps/pre-processing-service/app/api/router.py | 31 +++++-------- .../app/errors/CustomException.py | 26 +++++++++++ .../app/errors/handlers.py | 45 ++++++++++++++++--- .../app/errors/messages.py | 1 - apps/pre-processing-service/app/main.py | 39 ++++++++-------- 10 files changed, 156 insertions(+), 49 deletions(-) create mode 100644 apps/pre-processing-service/app/api/endpoints/__init__.py create mode 100644 apps/pre-processing-service/app/api/endpoints/embedding.py create mode 100644 apps/pre-processing-service/app/api/endpoints/processing.py create mode 100644 apps/pre-processing-service/app/api/endpoints/test.py create mode 100644 apps/pre-processing-service/app/errors/CustomException.py diff --git a/apps/pre-processing-service/app/api/__init__.py b/apps/pre-processing-service/app/api/__init__.py index 92573e7d..e69de29b 100644 --- a/apps/pre-processing-service/app/api/__init__.py +++ b/apps/pre-processing-service/app/api/__init__.py @@ -1,4 +0,0 @@ -# from fastapi import APIRouter -# from .v1.routes import router as v1_router -# router = APIRouter() -# router.include_router(v1_router, prefix="/v1") \ No newline at end of file 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 fb43ab0b..eefc00c9 100644 --- a/apps/pre-processing-service/app/api/router.py +++ b/apps/pre-processing-service/app/api/router.py @@ -1,25 +1,18 @@ +# app/api/router.py from fastapi import APIRouter -from app.decorators.logging import log_api_call +from .endpoints import embedding, processing,test -router = APIRouter() - -@router.get("/") -async def root(): - return {"message": "Hello World"} +api_router = APIRouter() +# embedding API URL +api_router.include_router(embedding.router, prefix="/emb", tags=["Embedding"]) -@router.get("/hello/{name}" , tags=["hello"]) -# @log_api_call -async def say_hello(name: str): - return {"message": f"Hello {name}"} +# processing API URL +api_router.include_router(processing.router, prefix="/prc", tags=["Processing"]) -# 이 엔드포인트는 테스트를 위해 예외를 발생시킵니다. -@router.get("/test-error") -def test_error(): - raise ValueError("이것은 테스트용 값 오류입니다.") +#모듈 테스터를 위한 endpoint +api_router.include_router(test.router, prefix="/test", tags=["Test"]) -# 특정 경로에서 의도적으로 에러 발생 -@router.get("/error") -async def trigger_error(): - result = 1 / 0 # ZeroDivisionError 발생 - return {"result": result} \ No newline at end of file +@api_router.get("/") +async def root(): + return {"message": "서버 실행중입니다."} \ 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/handlers.py b/apps/pre-processing-service/app/errors/handlers.py index 3de738ac..db05e176 100644 --- a/apps/pre-processing-service/app/errors/handlers.py +++ b/apps/pre-processing-service/app/errors/handlers.py @@ -4,31 +4,62 @@ 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 -# HTTPException 핸들러 (예: 404, 403 등) +# 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": get_error_message(exc.status_code, str(exc.detail)) + "error_code": f"HTTP_{exc.status_code}", + "message": message }, ) -# ValidationError 핸들러 (422) +# 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": ERROR_MESSAGES[status.HTTP_422_UNPROCESSABLE_ENTITY], - "errors": exc.errors(), + "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": ERROR_MESSAGES[status.HTTP_500_INTERNAL_SERVER_ERROR], + "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 index 96e0f39e..80139492 100644 --- a/apps/pre-processing-service/app/errors/messages.py +++ b/apps/pre-processing-service/app/errors/messages.py @@ -13,5 +13,4 @@ def get_error_message(status_code: int, detail: str | None = None) -> str: """상태 코드에 맞는 기본 메시지를 가져오되, detail이 있으면 우선""" - from app.errors.messages import ERROR_MESSAGES 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 6a4cc676..4a13e940 100644 --- a/apps/pre-processing-service/app/main.py +++ b/apps/pre-processing-service/app/main.py @@ -1,31 +1,34 @@ # main.py -from fastapi import FastAPI, Request -from loguru import logger -import uvicorn -from starlette import status -from starlette.responses import JSONResponse -from app.api.router import router as api_router +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 -# FastAPI 애플리케이션 인스턴스를 생성합니다. +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"]) -# 모든 Exception을 처리하는 글로벌 핸들러 -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception): - logger.error(f"에러발생{exc}") - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={"message": f"서버 내부 오류가 발생했습니312312321다: {exc}"}, - ) +# --- 라우터 등록 --- +app.include_router(api_router, prefix="", tags=["api"]) # if __name__ == "__main__": # uvicorn.run(app, host="0.0.0.0", port=8000) From f57d550c2e2fcd2293829bf481fded3d94360e12 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 25 Aug 2025 17:58:53 +0900 Subject: [PATCH 05/12] =?UTF-8?q?fix:=20postgre=20password=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.yml에 설정되어있는 password로 변경 - docker-compose.yml에 설정되어있는 password와 application-develop.yml에 설정되어있는 password 통일 시킴 --- .../main/resources/application-develop.yml | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) 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 From 75a82637a5e4b451c390959a0aeaa9b303b0c771 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 25 Aug 2025 17:59:54 +0900 Subject: [PATCH 06/12] =?UTF-8?q?chore:=20application-test.yml=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test를 위한 application-test.yml 작성 --- .../src/main/resources/application-test.yml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 From 02a301c5440f194e857de577c097c26c0e7fb669 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 25 Aug 2025 18:02:14 +0900 Subject: [PATCH 07/12] test: DBConnectionTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DBConnectionTest를 위한 create-schema.sql, insert-user-data.sql 작성 - DBConnectionTest 코드 작성 --- .../icebang/DatabaseConnectionTest.java | 79 +++++++++++++ .../src/test/resources/sql/create-schema.sql | 104 ++++++++++++++++++ .../test/resources/sql/insert-user-data.sql | 39 +++++++ 3 files changed, 222 insertions(+) create mode 100644 apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java create mode 100644 apps/user-service/src/test/resources/sql/create-schema.sql create mode 100644 apps/user-service/src/test/resources/sql/insert-user-data.sql 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..acc2ee3a --- /dev/null +++ b/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java @@ -0,0 +1,79 @@ +package com.gltkorea.icebang; + +import com.gltkorea.icebang.dto.UserDto; +import com.gltkorea.icebang.mapper.UserMapper; +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 javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@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("샘플 데이터 삽입 성공 - 홍길동, 김철수 확인"); + } +} \ No newline at end of file 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 From 3d9f686fce5d67f554afd75265213dc92634a4d5 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 25 Aug 2025 18:04:30 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=EB=8B=A4=EC=A4=91=20=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=20=EB=B9=8C=EB=93=9C=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20Dockerfile=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 빌드 스테이지와 실행 스테이지를 분리하여 최종 이미지의 크기를 줄였습니다. - `openjdk:21-jdk-slim` 이미지를 빌드에 사용하고, `openjdk:21-jre-slim` 이미지를 실행에 사용하여 불필요한 JDK 종속성을 제거했습니다. - Docker 레이어 캐싱을 활용하도록 파일 복사 순서를 조정하여 빌드 속도를 개선했습니다. - 빌드 시 테스트를 건너뛰는 옵션(`-x test`)을 추가했습니다. - 최종 JAR 파일명을 `app.jar`로 간소화했습니다. --- apps/user-service/Dockerfile | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) 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 From 7403b1c0e17a76ff8aa79d5254894397de845a4e Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 25 Aug 2025 18:06:15 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20Mybatis=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B8=B0=EB=B3=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 정보 관리를 위한 UserDto 클래스 생성 - Mybatis Mapper 인터페이스(UserMapper) 및 XML 파일 추가 - Spring 컨텍스트에 MapperScanner 설정 - 이를 통해 사용자 데이터를 데이터베이스에서 가져올 수 있는 기반 마련 --- .../icebang/UserServiceApplication.java | 2 ++ .../java/com/gltkorea/icebang/dto/UserDto.java | 13 +++++++++++++ .../com/gltkorea/icebang/mapper/UserMapper.java | 11 +++++++++++ .../resources/mybatis/mapper/UserMapper.xml | 17 +++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 apps/user-service/src/main/java/com/gltkorea/icebang/dto/UserDto.java create mode 100644 apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml 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..355192ed --- /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; + // ... 필요한 다른 필드들 +} \ No newline at end of file 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..f220ae54 --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java @@ -0,0 +1,11 @@ +package com.gltkorea.icebang.mapper; + +import com.gltkorea.icebang.dto.UserDto; +import org.apache.ibatis.annotations.Mapper; +import java.util.Optional; + +@Mapper // Spring이 MyBatis Mapper로 인식하도록 설정 +public interface UserMapper { + // XML 파일의 id와 메서드 이름을 일치시켜야 합니다. + Optional findByEmail(String email); +} \ 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 From 36bbf3d105f2b84269813de191ca479a0663b635 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Mon, 25 Aug 2025 18:08:03 +0900 Subject: [PATCH 10/12] =?UTF-8?q?chore:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=8A=A4=ED=82=A4=EB=A7=88=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B4=88=EA=B8=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create-schema.sql: 애플리케이션에 필요한 데이터베이스 테이블 스키마를 정의 - insert-user-data.sql: 테스트용 사용자 데이터를 초기화 시 삽입 이제 Docker Compose를 사용하여 컨테이너를 실행할 때 데이터베이스가 자동으로 초기화됩니다. --- docker/local/init-scripts/create-schema.sql | 104 ++++++++++++++++++ .../local/init-scripts/insert-user-data.sql | 39 +++++++ 2 files changed, 143 insertions(+) create mode 100644 docker/local/init-scripts/create-schema.sql create mode 100644 docker/local/init-scripts/insert-user-data.sql 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 From cce2ed6c40f82454fc88f9b76225dbc29e796d60 Mon Sep 17 00:00:00 2001 From: kakusiA Date: Tue, 26 Aug 2025 12:26:47 +0900 Subject: [PATCH 11/12] chore:config setting add --- apps/pre-processing-service/app/api/router.py | 13 ++++- .../pre-processing-service/app/core/config.py | 48 +++++++++++++++++++ .../app/db/db_connecter.py | 1 + apps/pre-processing-service/app/main.py | 6 +-- .../app/model/__init__.py | 0 .../app/model/schemas.py | 0 apps/pre-processing-service/poetry.lock | 26 +++++++++- apps/pre-processing-service/pyproject.toml | 3 +- 8 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 apps/pre-processing-service/app/model/__init__.py create mode 100644 apps/pre-processing-service/app/model/schemas.py diff --git a/apps/pre-processing-service/app/api/router.py b/apps/pre-processing-service/app/api/router.py index eefc00c9..2dd72b02 100644 --- a/apps/pre-processing-service/app/api/router.py +++ b/apps/pre-processing-service/app/api/router.py @@ -1,6 +1,7 @@ # app/api/router.py from fastapi import APIRouter from .endpoints import embedding, processing,test +from ..core.config import settings api_router = APIRouter() @@ -15,4 +16,14 @@ @api_router.get("/") async def root(): - return {"message": "서버 실행중입니다."} \ No newline at end of file + 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/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/main.py b/apps/pre-processing-service/app/main.py index 4a13e940..2ca44875 100644 --- a/apps/pre-processing-service/app/main.py +++ b/apps/pre-processing-service/app/main.py @@ -1,5 +1,5 @@ # main.py - +import uvicorn from fastapi import FastAPI from starlette.exceptions import HTTPException as StarletteHTTPException from fastapi.exceptions import RequestValidationError @@ -30,5 +30,5 @@ # --- 라우터 등록 --- app.include_router(api_router, prefix="", tags=["api"]) -# if __name__ == "__main__": - # uvicorn.run(app, host="0.0.0.0", port=8000) +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) 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 2d2b59ab..961f44e5 100644 --- a/apps/pre-processing-service/poetry.lock +++ b/apps/pre-processing-service/poetry.lock @@ -316,6 +316,30 @@ 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" @@ -464,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 = "20e9b26b37de9cfbf87c16440dddc9f46e21f890bc12280b5d6b96e86f38da69" +content-hash = "845e1778efdd87512efdd30eb0ba01aa1383061f662ccc3faa17ab1f8cebde5b" diff --git a/apps/pre-processing-service/pyproject.toml b/apps/pre-processing-service/pyproject.toml index 1c919a62..5a2017c3 100644 --- a/apps/pre-processing-service/pyproject.toml +++ b/apps/pre-processing-service/pyproject.toml @@ -12,7 +12,8 @@ dependencies = [ "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)" + "dotenv (>=0.9.9,<0.10.0)", + "pydantic-settings (>=2.10.1,<3.0.0)" ] From 30362879af4bf723dcfe4f187613f829214eb7ef Mon Sep 17 00:00:00 2001 From: kakusiA Date: Tue, 26 Aug 2025 17:44:32 +0900 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EB=A9=A7=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gltkorea/icebang/dto/UserDto.java | 10 +- .../gltkorea/icebang/mapper/UserMapper.java | 12 ++- .../icebang/DatabaseConnectionTest.java | 100 +++++++++--------- 3 files changed, 63 insertions(+), 59 deletions(-) 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 index 355192ed..6763bac9 100644 --- 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 @@ -6,8 +6,8 @@ @Getter @Setter public class UserDto { - private String userId; - private String name; - private String email; - // ... 필요한 다른 필드들 -} \ No newline at end of file + 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 index f220ae54..f09a152a 100644 --- 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 @@ -1,11 +1,13 @@ package com.gltkorea.icebang.mapper; -import com.gltkorea.icebang.dto.UserDto; -import org.apache.ibatis.annotations.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); -} \ No newline at end of file + // XML 파일의 id와 메서드 이름을 일치시켜야 합니다. + Optional findByEmail(String email); +} 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 index acc2ee3a..a3dd2e77 100644 --- a/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java +++ b/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java @@ -1,7 +1,13 @@ package com.gltkorea.icebang; -import com.gltkorea.icebang.dto.UserDto; -import com.gltkorea.icebang.mapper.UserMapper; +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; @@ -13,67 +19,63 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; +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) +@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 DataSource dataSource; - @Autowired - private UserMapper userMapper; // JPA Repository 대신 MyBatis Mapper를 주입 + @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("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"; + @Test + @DisplayName("MyBatis Mapper를 통해 '홍길동' 사용자를 이메일로 조회") + void findUserByEmailWithMyBatis() { + // given + String testEmail = "hong.gildong@example.com"; - // when - Optional foundUser = userMapper.findByEmail(testEmail); + // 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()); - } + // 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("홍길동"); + @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("김철수"); + Optional kim = userMapper.findByEmail("kim.chulsu@example.com"); + assertThat(kim).isPresent(); + assertThat(kim.get().getName()).isEqualTo("김철수"); - System.out.println("샘플 데이터 삽입 성공 - 홍길동, 김철수 확인"); - } -} \ No newline at end of file + System.out.println("샘플 데이터 삽입 성공 - 홍길동, 김철수 확인"); + } +}