diff --git a/app/api/chat_messages_api.py b/app/api/chat_messages_api.py index d63ca92..aa39d14 100644 --- a/app/api/chat_messages_api.py +++ b/app/api/chat_messages_api.py @@ -1,10 +1,10 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Path from app.core.enum.sender import SenderEnum from app.core.response import ResponseMessage from app.core.status import CommonCode from app.schemas.chat_message.request_model import ChatMessagesReqeust -from app.schemas.chat_message.response_model import ChatMessagesResponse +from app.schemas.chat_message.response_model import ALLChatMessagesResponseByTab, ChatMessagesResponse from app.services.chat_message_service import ChatMessageService, chat_message_service chat_message_service_dependency = Depends(lambda: chat_message_service) @@ -25,8 +25,6 @@ async def create_chat_message( """ new_messages = await service.create_chat_message(request) - print(ChatMessagesResponse.model_json_schema()) - response_data = ChatMessagesResponse( id=new_messages.id, chat_tab_id=new_messages.chat_tab_id, @@ -37,3 +35,20 @@ async def create_chat_message( ) return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_CREATE_CHAT_MESSAGES) + + +@router.get( + "/find/{tabId}", + response_model=ResponseMessage[ALLChatMessagesResponseByTab], + summary="특정 탭의 메시지 전체 조회", +) +def get_chat_messages_by_tabId( + tabId: str = Path(..., description="채팅 탭 고유 ID"), + service: ChatMessageService = chat_message_service_dependency, +) -> ResponseMessage[ALLChatMessagesResponseByTab]: + """tabId를 기준으로 해당 chat_tab의 전체 메시지를 가져옵니다.""" + + # 탭 정보와 메시지를 함께 조회 + response_data = service.get_chat_tab_and_messages_by_id(tabId) + + return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_GET_CHAT_MESSAGES) diff --git a/app/api/chat_tab_api.py b/app/api/chat_tab_api.py index de08f37..f578cde 100644 --- a/app/api/chat_tab_api.py +++ b/app/api/chat_tab_api.py @@ -3,7 +3,7 @@ from app.core.response import ResponseMessage from app.core.status import CommonCode from app.schemas.chat_tab.base_model import ChatTabBase -from app.schemas.chat_tab.response_model import ChatMessagesResponse, ChatTabResponse +from app.schemas.chat_tab.response_model import ChatTabResponse from app.schemas.chat_tab.update_model import ChatTabUpdate from app.services.chat_tab_service import ChatTabService, chat_tab_service @@ -61,30 +61,6 @@ def get_all_chat_tab( return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_GET_CHAT_TAB) -@router.get( - "/find/{tabId}", - response_model=ResponseMessage[ChatMessagesResponse], - summary="특정 탭의 메시지 전체 조회", -) -def get_chat_messages_by_tabId( - tabId: str = Path(..., description="채팅 탭 고유 ID"), service: ChatTabService = chat_tab_service_dependency -) -> ResponseMessage[list[ChatMessagesResponse]]: - """tabId를 기준으로 해당 chat_tab의 전체 메시지를 가져옵니다.""" - chat_tab = service.get_chat_tab_by_tabId(tabId) - - chat_messages = service.get_chat_messages_by_tabId(tabId) - - response_data = ChatMessagesResponse( - id=chat_tab.id, - name=chat_tab.name, - created_at=chat_tab.created_at, - updated_at=chat_tab.updated_at, - messages=chat_messages, - ) - - return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_GET_CHAT_MESSAGES) - - @router.put( "/modify/{tabId}", response_model=ResponseMessage[ChatTabResponse], diff --git a/app/repository/chat_message_repository.py b/app/repository/chat_message_repository.py index 4539b0c..b68f941 100644 --- a/app/repository/chat_message_repository.py +++ b/app/repository/chat_message_repository.py @@ -1,14 +1,16 @@ import sqlite3 +from app.core.enum.sender import SenderEnum +from app.core.exceptions import APIException +from app.core.status import CommonCode from app.core.utils import get_db_path from app.schemas.chat_message.db_model import ChatMessageInDB +from app.schemas.chat_message.response_model import ALLChatMessagesResponseByTab, ChatMessagesResponse class ChatMessageRepository: def create_chat_message(self, new_id: str, sender: str, chat_tab_id: str, message: str) -> ChatMessageInDB: - """ - 새로운 채팅을 데이터베이스에 저장하고, 저장된 객체를 반환합니다. - """ + """새로운 채팅을 데이터베이스에 저장하고, 저장된 객체를 반환합니다.""" db_path = get_db_path() conn = None @@ -43,7 +45,7 @@ def create_chat_message(self, new_id: str, sender: str, chat_tab_id: str, messag if conn: conn.close() - def get_chat_messages_by_tabId(self, id: str) -> list[ChatMessageInDB]: + def get_chat_tab_and_messages_by_id(self, tabId: str) -> ALLChatMessagesResponseByTab: """주어진 chat_tab_id에 해당하는 모든 메시지를 가져옵니다.""" db_path = get_db_path() conn = None @@ -52,17 +54,70 @@ def get_chat_messages_by_tabId(self, id: str) -> list[ChatMessageInDB]: conn.row_factory = sqlite3.Row cursor = conn.cursor() - # chat_message 테이블에서 chat_tab_id에 해당하는 모든 메시지를 조회합니다.' - # 메시지가 없을 경우, 빈 리스트를 반환합니다. + # 1. 채팅 탭 정보 조회 cursor.execute( - "SELECT * FROM chat_message WHERE chat_tab_id = ? ORDER BY created_at ASC", - (id,), + "SELECT id, name, created_at, updated_at FROM chat_tab WHERE id = ?", + (tabId,), ) - rows = cursor.fetchall() + tab_row = cursor.fetchone() - # 조회된 모든 행을 ChatMessageInDB 객체 리스트로 변환 - return [ChatMessageInDB.model_validate(dict(row)) for row in rows] + if not tab_row: + raise APIException(CommonCode.NO_CHAT_TAB_DATA) + # 2. 해당 탭의 메시지들 조회 + cursor.execute( + """ + SELECT id, chat_tab_id, sender, message, created_at, updated_at + FROM chat_message + WHERE chat_tab_id = ? + ORDER BY created_at ASC + """, + (tabId,), + ) + message_rows = cursor.fetchall() + + # 3. 메시지들을 ChatMessagesResponse로 변환 + messages = [ + ChatMessagesResponse( + id=row["id"], + chat_tab_id=row["chat_tab_id"], + sender=SenderEnum(row["sender"]), + message=row["message"], + created_at=row["created_at"], + updated_at=row["updated_at"], + ) + for row in message_rows + ] + + # 4. ALLChatMessagesResponseByTab 객체 생성 + return ALLChatMessagesResponseByTab( + id=tab_row["id"], + name=tab_row["name"], + created_at=tab_row["created_at"], + updated_at=tab_row["updated_at"], + messages=messages, + ) + except sqlite3.Error as e: + raise e + finally: + if conn: + conn.close() + + def get_chat_tab_by_id(self, tabId: str) -> None: + """데이터베이스에 저장된 특정 Chat Tab ID를 조회합니다.""" + db_path = get_db_path() + conn = None + try: + conn = sqlite3.connect(str(db_path), timeout=10) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute( + "SELECT id FROM chat_tab WHERE id = ?", + (tabId,), + ) + + return None finally: if conn: conn.close() diff --git a/app/repository/chat_tab_repository.py b/app/repository/chat_tab_repository.py index eb732d6..472056f 100644 --- a/app/repository/chat_tab_repository.py +++ b/app/repository/chat_tab_repository.py @@ -100,7 +100,7 @@ def delete_chat_tab(self, id: str) -> bool: conn.close() def get_all_chat_tab(self) -> list[ChatTabInDB]: - """데이터베이스에 저장된 모든 API Key를 조회합니다.""" + """데이터베이스에 저장된 모든 Chat Tab ID를 조회합니다.""" db_path = get_db_path() conn = None try: @@ -116,26 +116,5 @@ def get_all_chat_tab(self) -> list[ChatTabInDB]: if conn: conn.close() - def get_chat_tab_by_id(self, id: str | None) -> ChatTabInDB | None: - """ID에 해당하는 채팅 탭 정보를 가져옵니다.""" - db_path = get_db_path() - conn = None - try: - conn = sqlite3.connect(str(db_path), timeout=10) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - - cursor.execute("SELECT * FROM chat_tab WHERE id = ?", (id,)) - row = cursor.fetchone() - - if not row: - return None - - return ChatTabInDB.model_validate(dict(row)) - - finally: - if conn: - conn.close() - chat_tab_repository = ChatTabRepository() diff --git a/app/schemas/chat_message/base_model.py b/app/schemas/chat_message/base_model.py index 049770c..1e765d0 100644 --- a/app/schemas/chat_message/base_model.py +++ b/app/schemas/chat_message/base_model.py @@ -7,6 +7,13 @@ from app.core.status import CommonCode +def validate_chat_tab_id_format(tab_id: str) -> None: + """채팅 탭 ID 형식 유효성 검사""" + required_prefix = DBSaveIdEnum.chat_tab.value + "-" + if not tab_id.startswith(required_prefix): + raise APIException(CommonCode.INVALID_CHAT_TAB_ID_FORMAT) + + class ChatMessagesBase(BaseModel): id: str = Field(..., description="고유 ID") created_at: datetime = Field(..., description="생성 시각") @@ -16,9 +23,10 @@ class ChatMessagesBase(BaseModel): class RequestBase(BaseModel): """요청 스키마의 기본 모델""" - def validate_chat_tab_id(self) -> None: - """채팅 탭 ID에 대한 유효성 검증 로직을 수행합니다.""" + def validate_chat_tab_id(self, field_value: str) -> None: + validate_chat_tab_id_format(field_value) - required_prefix = DBSaveIdEnum.chat_tab.value + "-" - if not self.chat_tab_id.startswith(required_prefix): - raise APIException(CommonCode.INVALID_CHAT_TAB_ID_FORMAT) + def validate_message(self, field_value: str) -> None: + """메시지 유효성 검사""" + if not field_value or field_value.strip() == "": + raise APIException(CommonCode.INVALID_ANNOTATION_REQUEST) diff --git a/app/schemas/chat_message/request_model.py b/app/schemas/chat_message/request_model.py index fef913a..809bfb4 100644 --- a/app/schemas/chat_message/request_model.py +++ b/app/schemas/chat_message/request_model.py @@ -10,4 +10,5 @@ class ChatMessagesReqeust(RequestBase): message: str = Field(..., description="메시지 내용") def validate(self): - self.validate_chat_tab_id() + self.validate_chat_tab_id(self.chat_tab_id) + self.validate_message(self.message) diff --git a/app/schemas/chat_message/response_model.py b/app/schemas/chat_message/response_model.py index f1ce682..96d8bc1 100644 --- a/app/schemas/chat_message/response_model.py +++ b/app/schemas/chat_message/response_model.py @@ -1,4 +1,6 @@ -from pydantic import Field +from datetime import datetime + +from pydantic import BaseModel, Field from app.core.enum.sender import SenderEnum from app.schemas.chat_message.base_model import ChatMessagesBase @@ -11,3 +13,15 @@ class ChatMessagesResponse(ChatMessagesBase): class Config: use_enum_values = True + + +class ALLChatMessagesResponseByTab(BaseModel): + """채팅 탭의 메타데이터와 전체 메시지 목록을 담는 응답 스키마""" + + id: str = Field(..., description="채팅 탭의 고유 ID") + name: str = Field(..., description="채팅 탭의 이름") + created_at: datetime = Field(..., description="생성 시각") + updated_at: datetime = Field(..., description="마지막 수정 시각") + messages: list[ChatMessagesResponse] = Field( + default_factory=list, description="해당 채팅 탭에 속한 모든 메시지 목록" + ) diff --git a/app/schemas/chat_tab/base_model.py b/app/schemas/chat_tab/base_model.py index f642c19..9c4e9c8 100644 --- a/app/schemas/chat_tab/base_model.py +++ b/app/schemas/chat_tab/base_model.py @@ -2,7 +2,6 @@ from pydantic import BaseModel, Field -from app.core.enum.db_key_prefix_name import DBSaveIdEnum from app.core.exceptions import APIException from app.core.status import CommonCode @@ -36,11 +35,3 @@ def validate_chat_tab_name(self) -> None: # 특정 특수문자를 검사하는 예시 if re.search(r"[;\"'`<>]", self.name): raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_CONTENT) - - def validate_chat_tab_id(self) -> None: - """채팅 탭 ID에 대한 유효성 검증 로직을 수행합니다.""" - - # 1. 'CHAT-TAB-' 접두사 검증 - required_prefix = DBSaveIdEnum.chat_tab.value + "-" - if not self.id.startswith(required_prefix): - raise APIException(CommonCode.INVALID_CHAT_TAB_ID_FORMAT) diff --git a/app/schemas/chat_tab/db_model.py b/app/schemas/chat_tab/db_model.py index d548797..908de22 100644 --- a/app/schemas/chat_tab/db_model.py +++ b/app/schemas/chat_tab/db_model.py @@ -1,7 +1,5 @@ from datetime import datetime -from pydantic import Field - from app.schemas.chat_tab.base_model import ChatTabBase @@ -12,14 +10,3 @@ class ChatTabInDB(ChatTabBase): name: str created_at: datetime updated_at: datetime - - -class ChatMessageInDB(ChatTabBase): - """데이터베이스에 저장된 형태의 메시지 스키마 (내부용)""" - - id: str = Field(..., description="메시지의 고유 ID (서버에서 생성)") - chat_tab_id: str = Field(..., description="해당 메시지가 속한 채팅 탭의 ID") - sender: str = Field(..., description="메시지 발신자 ('AI' 또는 'User')") - message: str = Field(..., description="메시지 내용") - created_at: datetime - updated_at: datetime diff --git a/app/schemas/chat_tab/response_model.py b/app/schemas/chat_tab/response_model.py index 0f017d5..21fd932 100644 --- a/app/schemas/chat_tab/response_model.py +++ b/app/schemas/chat_tab/response_model.py @@ -3,7 +3,6 @@ from pydantic import Field from app.schemas.chat_tab.base_model import ChatTabBase -from app.schemas.chat_tab.db_model import ChatMessageInDB class ChatTabResponse(ChatTabBase): @@ -13,10 +12,3 @@ class ChatTabResponse(ChatTabBase): name: str = Field(..., description="채팅 탭의 이름") created_at: datetime updated_at: datetime - - -class ChatMessagesResponse(ChatTabResponse): - """AI 채팅 탭의 메타데이터와 전체 메시지 목록을 담는 API 응답 스키마""" - - # 해당 탭의 모든 메시지를 리스트로 담습니다. - messages: list[ChatMessageInDB] = Field(..., description="해당 채팅 탭에 속한 모든 메시지 목록") diff --git a/app/services/chat_message_service.py b/app/services/chat_message_service.py index 493554c..f7a23da 100644 --- a/app/services/chat_message_service.py +++ b/app/services/chat_message_service.py @@ -10,59 +10,74 @@ from app.core.status import CommonCode from app.core.utils import generate_prefixed_uuid from app.repository.chat_message_repository import ChatMessageRepository, chat_message_repository +from app.schemas.chat_message.base_model import validate_chat_tab_id_format from app.schemas.chat_message.db_model import ChatMessageInDB from app.schemas.chat_message.request_model import ChatMessagesReqeust -from app.schemas.chat_message.response_model import ChatMessagesResponse +from app.schemas.chat_message.response_model import ALLChatMessagesResponseByTab, ChatMessagesResponse chat_message_repository_dependency = Depends(lambda: chat_message_repository) AI_SERVER_URL = os.getenv("ENV_AI_SERVER_URL") +if not AI_SERVER_URL: + raise APIException(CommonCode.FAIL_AI_SERVER_CONNECTION) + +url: str = AI_SERVER_URL + class ChatMessageService: def __init__(self, repository: ChatMessageRepository = chat_message_repository): self.repository = repository - def get_chat_messages_by_tabId(self, tabId: str) -> ChatMessageInDB: + def get_chat_tab_and_messages_by_id(self, tab_id: str) -> ALLChatMessagesResponseByTab: """ - 채팅 탭 메타데이터와 메시지 목록을 모두 가져와서 조합합니다. + 채팅 탭 정보와 메시지들을 함께 조회 탭이 존재하지 않으면 예외를 발생시킵니다. """ - try: - return self.repository.get_chat_messages_by_tabId(tabId) + # chat_tab_id 형식 유효성 검사 + validate_chat_tab_id_format(tab_id) + # 채팅 탭 정보와 메시지 조회 + try: + return self.repository.get_chat_tab_and_messages_by_id(tab_id) except sqlite3.Error as e: raise APIException(CommonCode.FAIL) from e async def create_chat_message(self, request: ChatMessagesReqeust) -> ChatMessagesResponse: - # 1. tab_id 확인 - chat_tab_id = request.chat_tab_id + # 1. tab_id, message 유효성 검사 및 유무 확인 + request.validate() - # chat_tab_id 유효성 검사 - try: - request.validate() - except ValueError as e: - raise APIException(CommonCode.INVALID_CHAT_MESSAGE_REQUEST, detail=str(e)) from e - - try: - # 같은 서비스 메서드 호출 - self.get_chat_messages_by_tabId(chat_tab_id) - except sqlite3.Error as e: - raise APIException(CommonCode.FAIL) from e + self.repository.get_chat_tab_by_id(request.chat_tab_id) # 2. 사용자 질의 저장 try: - user_request = self._transform_user_request_to_db_models(request) + self._transform_user_request_to_db_models(request) except sqlite3.Error as e: raise APIException(CommonCode.FAIL) from e # 3. AI 서버에 요청 - ai_response = await self._request_chat_message_to_ai_server(user_request) + ai_response = await self._request_chat_message_to_ai_server(request) # 4. AI 서버 응답 저장 response = self._transform_ai_response_to_db_models(request, ai_response) - return response + # DB 모델을 API 응답 모델로 변환 + response_data = ChatMessagesResponse.model_validate(response) + + return response_data + + def get_chat_tab_by_id(self, request: ChatMessagesReqeust) -> ChatMessageInDB: + """특정 채팅 탭 조회""" + + # 채팅 탭 ID 조회 + try: + chat_tab = self.repository.get_chat_tab_by_id(request.chat_tab_id) + if not chat_tab: + raise APIException(CommonCode.NO_CHAT_TAB_DATA) + return chat_tab + + except sqlite3.Error as e: + raise APIException(CommonCode.FAIL) from e def _transform_user_request_to_db_models(self, request: ChatMessagesReqeust) -> ChatMessageInDB: """사용자 질의를 데이터베이스에 저장합니다.""" @@ -90,14 +105,15 @@ def _transform_user_request_to_db_models(self, request: ChatMessagesReqeust) -> raise APIException(CommonCode.DB_BUSY) from e raise APIException(CommonCode.FAIL) from e - async def _request_chat_message_to_ai_server(self, user_request: ChatMessagesReqeust) -> dict: + async def _request_chat_message_to_ai_server(self, request: ChatMessagesReqeust) -> dict: """AI 서버에 사용자 질의를 보내고 답변을 받아옵니다.""" # 1. DB에서 해당 탭의 모든 메시지 조회 - messages: list[ChatMessageInDB] = self.repository.get_chat_messages_by_tabId(user_request.chat_tab_id) + chat_tab_with_messages = self.repository.get_chat_tab_and_messages_by_id(request.chat_tab_id) + messages: list[ChatMessagesResponse] = chat_tab_with_messages.messages if not messages: history = [] - latest_message = user_request.message # DB에 없으면 요청 메시지 그대로 + latest_message = request.message # DB에 없으면 요청 메시지 그대로 else: history = [{"role": m.sender, "content": m.message} for m in messages[:-1]] latest_message = messages[-1].message @@ -108,7 +124,7 @@ async def _request_chat_message_to_ai_server(self, user_request: ChatMessagesReq # 4. AI 서버에 POST 요청 async with httpx.AsyncClient() as client: try: - response = await client.post(AI_SERVER_URL, json=request_body, timeout=60.0) + response = await client.post(url, json=request_body, timeout=60.0) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: @@ -116,7 +132,7 @@ async def _request_chat_message_to_ai_server(self, user_request: ChatMessagesReq except httpx.RequestError as e: raise APIException(CommonCode.FAIL_AI_SERVER_CONNECTION) from e - def _transform_ai_response_to_db_models(self, request: ChatMessagesReqeust, ai_response: str) -> ChatMessageInDB: + def _transform_ai_response_to_db_models(self, request: ChatMessagesReqeust, ai_response: dict) -> ChatMessageInDB: """AI 서버에서 받은 답변을 데이터베이스에 저장합니다.""" new_id = generate_prefixed_uuid(DBSaveIdEnum.chat_message.value) diff --git a/app/services/chat_tab_service.py b/app/services/chat_tab_service.py index f2e63ff..f417696 100644 --- a/app/services/chat_tab_service.py +++ b/app/services/chat_tab_service.py @@ -77,22 +77,5 @@ def get_all_chat_tab(self) -> ChatTabInDB: except sqlite3.Error as e: raise APIException(CommonCode.FAIL) from e - def get_chat_tab_by_tabId(self, tabId: str) -> ChatTabInDB: - """데이터베이스에 저장된 특정 Chat_tab을 조회합니다.""" - try: - tabId.validate(tabId) - except ValueError as e: - raise APIException(CommonCode.INVALID_ANNOTATION_REQUEST, detail=str(e)) from e - - try: - chat_tab = self.repository.get_chat_tab_by_id(tabId) - - if not chat_tab: - raise APIException(CommonCode.NO_CHAT_TAB_DATA) - return chat_tab - - except sqlite3.Error as e: - raise APIException(CommonCode.FAIL) from e - chat_tab_service = ChatTabService() diff --git a/poetry.lock b/poetry.lock index 1e97eca..3bd2a0b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "altgraph" @@ -1250,7 +1250,7 @@ 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"] +groups = ["main", "dev"] 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"}, @@ -1582,4 +1582,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "00f54478274f36f71dadcdcc177a0c7cd955da78a14647eb9f40c83195523f87" +content-hash = "2442053ca61e08e6f40c789af2794f17d75cbf96c2c8fca6853e23694a11552a" diff --git a/pyproject.toml b/pyproject.toml index e8351b6..ceb6149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ pytest-asyncio = "^1.1.0" # Ruff 설정 # ---------------------------- pytest-cov = "^6.2.1" +python-dotenv = "^1.1.1" [tool.ruff] line-length = 120 exclude = [