diff --git a/app/api/api_router.py b/app/api/api_router.py index b1c2d39..346914a 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -2,7 +2,7 @@ from fastapi import APIRouter -from app.api import api_key_api, driver_api, test_api, user_db_api +from app.api import api_key_api, chat_tab_api, driver_api, test_api, user_db_api api_router = APIRouter() @@ -13,3 +13,4 @@ api_router.include_router(driver_api.router, prefix="/driver", tags=["Driver"]) api_router.include_router(user_db_api.router, prefix="/user/db", tags=["UserDb"]) api_router.include_router(api_key_api.router, prefix="/keys", tags=["API Key"]) +api_router.include_router(chat_tab_api.router, prefix="/chats", tags=["AI Chat"]) diff --git a/app/api/chat_tab_api.py b/app/api/chat_tab_api.py new file mode 100644 index 0000000..7aa4bcb --- /dev/null +++ b/app/api/chat_tab_api.py @@ -0,0 +1,34 @@ +from fastapi import APIRouter, Depends + +from app.core.response import ResponseMessage +from app.core.status import CommonCode +from app.schemas.chat_tab.create_model import AIChatCreate +from app.schemas.chat_tab.response_model import AIChatResponse +from app.services.chat_tab_service import AIChatService, ai_chat_service + +ai_chat_service_dependency = Depends(lambda: ai_chat_service) + +router = APIRouter() + + +@router.post( + "/actions", + response_model=ResponseMessage[AIChatResponse], + summary="Chat Tab 생성", + description="새로운 Chat Tab을 생성하여 로컬 데이터베이스에 저장합니다.", +) +def store_ai_chat( + chatName: AIChatCreate, service: AIChatService = ai_chat_service_dependency +) -> ResponseMessage[AIChatResponse]: + """ + - **name**: 새로운 Chat_tab 이름 (예: "채팅 타이틀") + """ + created_chat = service.store_ai_chat(chatName) + + response_data = AIChatResponse( + id=created_chat.id, + name=created_chat.name, + created_at=created_chat.created_at, + updated_at=created_chat.updated_at, + ) + return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_AI_CHAT_CREATE) diff --git a/app/core/status.py b/app/core/status.py index 6d5e538..d20e742 100644 --- a/app/core/status.py +++ b/app/core/status.py @@ -24,6 +24,7 @@ class CommonCode(Enum): """ KEY 성공 코드 - 22xx """ """ AI CHAT, DB 성공 코드 - 23xx """ + SUCCESS_AI_CHAT_CREATE = (status.HTTP_200_OK, "2300", "새로운 채팅 탭을 생성하였습니다.") """ ANNOTATION 성공 코드 - 24xx """ @@ -50,7 +51,19 @@ class CommonCode(Enum): "API 키가 선택한 서비스의 올바른 형식이 아닙니다. (예: OpenAI는 sk-로 시작)", ) - """ AI CHAT, DB 클라이언트 오류 코드 - 43xx """ + """ AI CHAT TAB 클라이언트 오류 코드 - 43xx """ + INVALID_CHAT_TAB_NAME_FORMAT = (status.HTTP_400_BAD_REQUEST, "4300", "채팅 탭 이름의 형식이 올바르지 않습니다.") + INVALID_CHAT_TAB_NAME_LENGTH = ( + status.HTTP_400_BAD_REQUEST, + "4301", + "채팅 탭 이름의 길이는 128자를 초과할 수 없습니다.", + ) + INVALID_CHAT_TAB_NAME_CONTENT = ( + status.HTTP_400_BAD_REQUEST, + "4302", + "채팅 탭 이름에 SQL 예약어나 허용되지 않는 특수문자가 포함되어 있습니다. " + "허용되지 않는 특수 문자: 큰따옴표(\"), 작은따옴표('), 세미콜론(;), 꺾쇠괄호(<, >)", + ) """ ANNOTATION 클라이언트 오류 코드 - 44xx """ diff --git a/app/db/init_db.py b/app/db/init_db.py index 79fa774..34a8c95 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -72,7 +72,7 @@ def initialize_database(): """ CREATE TABLE IF NOT EXISTS chat_tab ( id VARCHAR(64) PRIMARY KEY NOT NULL, - name VARCHAR(255), + name VARCHAR(128), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/app/repository/chat_tab_repository.py b/app/repository/chat_tab_repository.py new file mode 100644 index 0000000..bf96fe8 --- /dev/null +++ b/app/repository/chat_tab_repository.py @@ -0,0 +1,46 @@ +import sqlite3 + +from app.core.utils import get_db_path +from app.schemas.chat_tab.db_model import AIChatInDB + + +class AIChatRepository: + + def create_ai_chat(self, new_id: str, name: str) -> AIChatInDB: + """ + 암호화된 API Key 정보를 받아 데이터베이스에 저장하고, + 저장된 객체를 반환합니다. + """ + 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( + """ + INSERT INTO chat_tab (id, name) + VALUES (?, ?) + """, + ( + new_id, + name, + ), + ) + conn.commit() + + cursor.execute("SELECT * FROM chat_tab WHERE id = ?", (new_id,)) + created_row = cursor.fetchone() + + if not created_row: + raise None + + return AIChatInDB.model_validate(dict(created_row)) + + finally: + if conn: + conn.close() + + +ai_chat_repository = AIChatRepository() diff --git a/app/schemas/chat_tab/base_model.py b/app/schemas/chat_tab/base_model.py new file mode 100644 index 0000000..d59c248 --- /dev/null +++ b/app/schemas/chat_tab/base_model.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field + + +class AIChatBase(BaseModel): + """모든 AI Chat Tab 스키마의 기본 모델""" + + name: str = Field(..., description="새로운 채팅 탭 이름") diff --git a/app/schemas/chat_tab/create_model.py b/app/schemas/chat_tab/create_model.py new file mode 100644 index 0000000..f811adc --- /dev/null +++ b/app/schemas/chat_tab/create_model.py @@ -0,0 +1,30 @@ +import re + +from app.core.exceptions import APIException +from app.core.status import CommonCode +from app.schemas.chat_tab.base_model import AIChatBase + + +class AIChatCreate(AIChatBase): + """새로운 Chat Tab 생성을 위한 스키마""" + + def validate_with_name(self) -> None: + """채팅 탭 이름에 대한 유효성 검증 로직을 수행합니다.""" + # 1. 문자열 전체가 공백 문자인지 확인 + if not self.name or self.name.isspace(): + raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_FORMAT) + + # 2. 길이 제한 + if len(self.name) > 128: + raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_LENGTH) + + # 3. 특수문자 및 SQL 예약어 확인 + # SQL 예약어와 위험한 특수문자를 검사합니다. + sql_keywords = ["SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "OR", "AND"] + for keyword in sql_keywords: + if keyword in self.name.upper(): + raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_CONTENT) + + # 특정 특수문자를 검사하는 예시 + if re.search(r"[;\"'`<>]", self.name): + raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_CONTENT) diff --git a/app/schemas/chat_tab/db_model.py b/app/schemas/chat_tab/db_model.py new file mode 100644 index 0000000..833f95a --- /dev/null +++ b/app/schemas/chat_tab/db_model.py @@ -0,0 +1,15 @@ +from datetime import datetime + +from app.schemas.chat_tab.base_model import AIChatBase + + +class AIChatInDB(AIChatBase): + """데이터베이스에 저장된 형태의 스키마 (내부용)""" + + id: str + name: str + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/app/schemas/chat_tab/response_model.py b/app/schemas/chat_tab/response_model.py new file mode 100644 index 0000000..5a1c46f --- /dev/null +++ b/app/schemas/chat_tab/response_model.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from pydantic import Field + +from app.schemas.chat_tab.base_model import AIChatBase + + +class AIChatResponse(AIChatBase): + """AI 채팅 탭 정보 API 응답용 스키마""" + + id: str = Field(..., description="채팅 세션의 고유 ID (서버에서 생성)") + name: str = Field(..., description="채팅 세션의 이름") + created_at: datetime + updated_at: datetime diff --git a/app/services/chat_tab_service.py b/app/services/chat_tab_service.py new file mode 100644 index 0000000..27ac918 --- /dev/null +++ b/app/services/chat_tab_service.py @@ -0,0 +1,43 @@ +import sqlite3 + +from fastapi import Depends + +from app.core.exceptions import APIException +from app.core.status import CommonCode +from app.core.utils import generate_prefixed_uuid +from app.repository.chat_tab_repository import AIChatRepository, ai_chat_repository +from app.schemas.chat_tab.create_model import AIChatCreate +from app.schemas.chat_tab.db_model import AIChatInDB + +ai_chat_repository_dependency = Depends(lambda: ai_chat_repository) + + +class AIChatService: + def __init__(self, repository: AIChatRepository = ai_chat_repository): + self.repository = repository + + def store_ai_chat(self, chatName: AIChatCreate) -> AIChatInDB: + """새로운 AI 채팅을 데이터베이스에 저장합니다.""" + chatName.validate_with_name() + + new_id = generate_prefixed_uuid("CHAT_TAB") + + try: + created_row = self.repository.create_ai_chat( + new_id=new_id, + name=chatName.name, + ) + if not created_row: + raise APIException(CommonCode.FAIL_TO_VERIFY_CREATION) + + return created_row + + except sqlite3.Error as e: + # "database is locked" 오류를 명시적으로 처리 + if "database is locked" in str(e): + raise APIException(CommonCode.DB_BUSY) from e + # 기타 모든 sqlite3 오류 + raise APIException(CommonCode.FAIL) from e + + +ai_chat_service = AIChatService()