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
59 changes: 43 additions & 16 deletions app/api/chat_tab_api.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,61 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Path

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
from app.schemas.chat_tab.create_model import ChatTabCreate
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

ai_chat_service_dependency = Depends(lambda: ai_chat_service)
chat_tab_service_dependency = Depends(lambda: chat_tab_service)

router = APIRouter()


@router.post(
"/actions",
response_model=ResponseMessage[AIChatResponse],
response_model=ResponseMessage[ChatTabResponse],
summary="Chat Tab 생성",
description="새로운 Chat Tab을 생성하여 로컬 데이터베이스에 저장합니다.",
)
def store_ai_chat(
chatName: AIChatCreate, service: AIChatService = ai_chat_service_dependency
) -> ResponseMessage[AIChatResponse]:
def store_chat_tab(
chatName: ChatTabCreate, service: ChatTabService = chat_tab_service_dependency
) -> ResponseMessage[ChatTabResponse]:
"""
- **name**: 새로운 Chat_tab 이름 (예: "채팅 타이틀")
"""
created_chat = service.store_ai_chat(chatName)
created_chat_tab = service.store_chat_tab(chatName)

response_data = AIChatResponse(
id=created_chat.id,
name=created_chat.name,
created_at=created_chat.created_at,
updated_at=created_chat.updated_at,
response_data = ChatTabResponse(
id=created_chat_tab.id,
name=created_chat_tab.name,
created_at=created_chat_tab.created_at,
updated_at=created_chat_tab.updated_at,
)
return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_AI_CHAT_CREATE)
return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_CHAT_TAB_CREATE)

@router.put(
"/modify/{tabId}",
response_model=ResponseMessage[ChatTabResponse],
summary="특정 Chat Tab Name 수정",
)
def updated_chat_tab(
chatName: ChatTabUpdate,
tabId: str = Path(..., description="수정할 채팅 탭의 고유 ID"),
service: ChatTabService = chat_tab_service_dependency
) -> ResponseMessage[ChatTabResponse]:
"""
채팅 탭 ID를 기준으로 채팅 탭의 이름을 새로운 값으로 수정합니다.
- **id**: 수정할 채팅 탭 ID
- **name**: 새로운 채팅 탭의 이름
"""
updated_chat_tab = service.updated_chat_tab(tabId, chatName)

response_data = ChatTabResponse(
id=updated_chat_tab.id,
name=updated_chat_tab.name,
created_at=updated_chat_tab.created_at,
updated_at=updated_chat_tab.updated_at,
)

return ResponseMessage.success(value=response_data, code=CommonCode.SUCCESS_CHAT_TAB_UPDATE)
8 changes: 5 additions & 3 deletions app/core/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class CommonCode(Enum):
SUCCESS_GET_API_KEY = (status.HTTP_200_OK, "2202", "API KEY 정보를 성공적으로 조회했습니다.")

""" AI CHAT, DB 성공 코드 - 23xx """
SUCCESS_AI_CHAT_CREATE = (status.HTTP_200_OK, "2300", "새로운 채팅 탭을 생성하였습니다.")
SUCCESS_CHAT_TAB_CREATE = (status.HTTP_200_OK, "2300", "새로운 채팅 탭을 성공적으로 생성되었습니다.")
SUCCESS_CHAT_TAB_UPDATE = (status.HTTP_200_OK, "2301", "채팅 탭 이름 수정이 성공적으로 처리되었습니다.")

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

Expand All @@ -62,8 +63,9 @@ class CommonCode(Enum):
"API 키가 선택한 서비스의 올바른 형식이 아닙니다. (예: OpenAI는 sk-로 시작)",
)

""" AI CHAT, DB 클라이언트 에러 코드 - 43xx """
INVALID_CHAT_TAB_NAME_FORMAT = (status.HTTP_400_BAD_REQUEST, "4300", "채팅 탭 이름의 형식이 올바르지 않습니다.")
""" 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",
Expand Down
45 changes: 38 additions & 7 deletions app/repository/chat_tab_repository.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import sqlite3

from app.core.utils import get_db_path
from app.schemas.chat_tab.db_model import AIChatInDB
from app.schemas.chat_tab.db_model import ChatTabInDB


class AIChatRepository:
class ChatTabRepository:

def create_ai_chat(self, new_id: str, name: str) -> AIChatInDB:
def create_chat_tab(self, new_id: str, name: str) -> ChatTabInDB:
"""
암호화된 API Key 정보를 받아 데이터베이스에 저장하고,
저장된 객체를 반환합니다.
새로운 채팅 탭 이름을 데이터베이스에 저장하고, 저장된 객체를 반환합니다.
"""
db_path = get_db_path()
conn = None
Expand All @@ -36,11 +35,43 @@ def create_ai_chat(self, new_id: str, name: str) -> AIChatInDB:
if not created_row:
raise None

return AIChatInDB.model_validate(dict(created_row))
return ChatTabInDB.model_validate(dict(created_row))

finally:
if conn:
conn.close()
def updated_chat_tab(self, id: str, new_name: str | None) -> ChatTabInDB | None:
"""채팅 탭ID에 해당하는 ChatName를 수정하고, 수정된 객체를 반환합니다."""
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 = ?", (id,))
if not cursor.fetchone():
return None

# 데이터 업데이트
cursor.execute(
"UPDATE chat_tab SET name = ?, updated_at = datetime('now', 'localtime') WHERE id = ?",
(new_name, id),
)
conn.commit()

# rowcount가 0이면 업데이트된 행이 없음 (정상적인 경우 발생하기 어려움)
if cursor.rowcount == 0:
return None

cursor.execute("SELECT * FROM chat_tab WHERE id = ?", (id,))
updated_row = cursor.fetchone()

return ChatTabInDB.model_validate(dict(updated_row))
finally:
if conn:
conn.close()


ai_chat_repository = AIChatRepository()
chat_tab_repository = ChatTabRepository()
2 changes: 1 addition & 1 deletion app/schemas/chat_tab/base_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pydantic import BaseModel, Field


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

name: str = Field(..., description="새로운 채팅 탭 이름")
28 changes: 4 additions & 24 deletions app/schemas/chat_tab/create_model.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
import re

from app.core.exceptions import APIException
from app.core.status import CommonCode
from app.schemas.chat_tab.base_model import AIChatBase
from app.schemas.chat_tab.base_model import ChatTabBase
from app.schemas.chat_tab.validation_utils import validate_chat_tab_name


class AIChatCreate(AIChatBase):
class ChatTabCreate(ChatTabBase):
"""새로운 Chat Tab 생성을 위한 스키마"""

def validate_with_name(self) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 메서드는 존재만 하고 실제 호출되지 않는 것 같습니다.

"""채팅 탭 이름에 대한 유효성 검증 로직을 수행합니다."""
# 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)
validate_chat_tab_name(self.name)
4 changes: 2 additions & 2 deletions app/schemas/chat_tab/db_model.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from datetime import datetime

from app.schemas.chat_tab.base_model import AIChatBase
from app.schemas.chat_tab.base_model import ChatTabBase


class AIChatInDB(AIChatBase):
class ChatTabInDB(ChatTabBase):
"""데이터베이스에 저장된 형태의 스키마 (내부용)"""

id: str
Expand Down
4 changes: 2 additions & 2 deletions app/schemas/chat_tab/response_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from pydantic import Field

from app.schemas.chat_tab.base_model import AIChatBase
from app.schemas.chat_tab.base_model import ChatTabBase


class AIChatResponse(AIChatBase):
class ChatTabResponse(ChatTabBase):
"""AI 채팅 탭 정보 API 응답용 스키마"""

id: str = Field(..., description="채팅 세션의 고유 ID (서버에서 생성)")
Expand Down
12 changes: 12 additions & 0 deletions app/schemas/chat_tab/update_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pydantic import Field

from app.schemas.chat_tab.base_model import ChatTabBase
from app.schemas.chat_tab.validation_utils import validate_chat_tab_name


class ChatTabUpdate(ChatTabBase):
"""채팅 탭 이름 수정을 위한 스키마"""
name: str | None = Field(None, description="수정할 채팅 탭 이름")

def validate_with_name(self) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 메서드도 존재만 하고 실제로 호출되지 않는 것 같습니다.

validate_chat_tab_name(self.name)
26 changes: 26 additions & 0 deletions app/schemas/chat_tab/validation_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import re

from app.core.exceptions import APIException
from app.core.status import CommonCode


def validate_chat_tab_name(name: str | None) -> None:
"""채팅 탭 이름에 대한 유효성 검증 로직을 수행합니다."""
# 1. 문자열이 None, 문자열 전체가 공백 문자인지 확인
if not name or name.isspace():
raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_FORMAT)

# 2. 길이 제한
if len(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 name.upper():
raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_CONTENT)

# 특정 특수문자를 검사하는 예시
if re.search(r"[;\"'`<>]", name):
raise APIException(CommonCode.INVALID_CHAT_TAB_NAME_CONTENT)
36 changes: 26 additions & 10 deletions app/services/chat_tab_service.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uuid 가져오는 부분은 이넘 생성했으니 추후 확인 후 수정해주시면 됩니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그럼 DB 쪽 머지 후 진행하는게 나을까요 ? 아니면 바로 추가할까요?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그건 상관없습니다!

Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@
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
from app.repository.chat_tab_repository import ChatTabRepository, chat_tab_repository
from app.schemas.chat_tab.create_model import ChatTabCreate
from app.schemas.chat_tab.db_model import ChatTabInDB
from app.schemas.chat_tab.update_model import ChatTabUpdate
from app.schemas.chat_tab.validation_utils import validate_chat_tab_name

ai_chat_repository_dependency = Depends(lambda: ai_chat_repository)
chat_tab_repository_dependency = Depends(lambda: chat_tab_repository)


class AIChatService:
def __init__(self, repository: AIChatRepository = ai_chat_repository):
class ChatTabService:
def __init__(self, repository: ChatTabRepository = chat_tab_repository):
self.repository = repository

def store_ai_chat(self, chatName: AIChatCreate) -> AIChatInDB:
def store_chat_tab(self, chatName: ChatTabCreate) -> ChatTabInDB:
"""새로운 AI 채팅을 데이터베이스에 저장합니다."""
chatName.validate_with_name()
validate_chat_tab_name(chatName.name)
Comment on lines -21 to +23

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검증 로직은 모델에 일임하는게 좋을 것 같습니다.
위에서 언급한대로 base_model로 검증 메서드를 옮긴 후 chatName의 메서드를 통해 직접 검증하도록 설정하는게 좋을 것 같습니다.
validation_utils.py로 따로 분리한 이유는 불필요한 코드 중복을 피하기 위함인 것 같지만 base_model의 메서드로 만들어서 이를 상속받는게 더 좋은 해결책일 것 같습니다.
기능상 문제는 없기에 나중에 리팩토링 해도 되고 편하신대로 해주세요.


new_id = generate_prefixed_uuid("CHAT_TAB")

try:
created_row = self.repository.create_ai_chat(
created_row = self.repository.create_chat_tab(
new_id=new_id,
name=chatName.name,
)
Expand All @@ -38,6 +40,20 @@ def store_ai_chat(self, chatName: AIChatCreate) -> AIChatInDB:
raise APIException(CommonCode.DB_BUSY) from e
# 기타 모든 sqlite3 오류
raise APIException(CommonCode.FAIL) from e
def updated_chat_tab(self, chatID: str, chatName: ChatTabUpdate) -> ChatTabInDB:
"""서비스 이름에 해당하는 API Key를 수정합니다."""
validate_chat_tab_name(chatName.name)
try:
updated_chat_tab = self.repository.updated_chat_tab(chatID, chatName.name)

if not updated_chat_tab:
raise APIException(CommonCode.NO_SEARCH_DATA)

return updated_chat_tab
except sqlite3.Error as e:
if "database is locked" in str(e):
raise APIException(CommonCode.DB_BUSY) from e
raise APIException(CommonCode.FAIL) from e


ai_chat_service = AIChatService()
chat_tab_service = ChatTabService()