Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/api/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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"])
34 changes: 34 additions & 0 deletions app/api/chat_tab_api.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 14 additions & 1 deletion app/core/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CommonCode(Enum):
""" KEY 성공 코드 - 22xx """

""" AI CHAT, DB 성공 코드 - 23xx """
SUCCESS_AI_CHAT_CREATE = (status.HTTP_200_OK, "2300", "새로운 채팅 탭을 생성하였습니다.")

""" ANNOTATION 성공 코드 - 24xx """

Expand All @@ -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 """

Expand Down
2 changes: 1 addition & 1 deletion app/db/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
46 changes: 46 additions & 0 deletions app/repository/chat_tab_repository.py
Original file line number Diff line number Diff line change
@@ -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()
7 changes: 7 additions & 0 deletions app/schemas/chat_tab/base_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import BaseModel, Field


class AIChatBase(BaseModel):
"""모든 AI Chat Tab 스키마의 기본 모델"""

name: str = Field(..., description="새로운 채팅 탭 이름")
30 changes: 30 additions & 0 deletions app/schemas/chat_tab/create_model.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions app/schemas/chat_tab/db_model.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions app/schemas/chat_tab/response_model.py
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions app/services/chat_tab_service.py
Original file line number Diff line number Diff line change
@@ -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()