diff --git a/app/modules/favorite/dependencies.py b/app/modules/favorite/dependencies.py new file mode 100644 index 0000000..8467b98 --- /dev/null +++ b/app/modules/favorite/dependencies.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import Depends + +from app.core.database import DbSessionDep +from app.modules.favorite.repository import FavoriteRepository +from app.modules.favorite.service import FavoriteService +from app.modules.project.dependencies import get_project_service + + +def get_favorite_service(session: DbSessionDep) -> FavoriteService: + repository = FavoriteRepository(session) + project_service = get_project_service(session) + return FavoriteService(session, repository, project_service) + + +FavoriteServiceDep = Annotated[FavoriteService, Depends(get_favorite_service)] \ No newline at end of file diff --git a/app/modules/favorite/repository.py b/app/modules/favorite/repository.py new file mode 100644 index 0000000..79663fa --- /dev/null +++ b/app/modules/favorite/repository.py @@ -0,0 +1,28 @@ +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlmodel import select + +from app.modules.favorite.models import Favorite + + +class FavoriteRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def get_by_project_member(self, project_id: UUID, member_id: UUID) -> Favorite | None: + stmt = select(Favorite).where( + Favorite.project_id == project_id, + Favorite.member_id == member_id, + ) + result = await self.session.execute(stmt) + return result.scalar() + + async def create_favorite(self, favorite: Favorite) -> Favorite: + self.session.add(favorite) + await self.session.flush() + return favorite + + async def delete_favorite(self, favorite: Favorite) -> None: + await self.session.delete(favorite) + await self.session.flush() \ No newline at end of file diff --git a/app/modules/favorite/router.py b/app/modules/favorite/router.py new file mode 100644 index 0000000..2afc4b8 --- /dev/null +++ b/app/modules/favorite/router.py @@ -0,0 +1,34 @@ +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Path + +from app.modules.favorite.dependencies import FavoriteServiceDep +from app.modules.favorite.schemas import FavoriteOut +from app.modules.member.dependencies import CurrentMemberDep +from app.shared.schemas import ApiResponse + +router = APIRouter(prefix="/favorite", tags=["Favorite"]) + + +@router.post( + path="/{project_id}", + response_model=ApiResponse[FavoriteOut], + summary="프로젝트 즐겨찾기", + description="특정 프로젝트를 즐겨찾기에 추가하거나 이미 존재하면 삭제합니다.", +) +async def favorite_project( + service: FavoriteServiceDep, + current_member: CurrentMemberDep, + project_id: Annotated[UUID, Path(..., description="프로젝트 ID")], +): + is_favorite = await service.favorite(project_id, current_member.id) + + return ApiResponse.success( + code="FAVORITE_UPDATED", + message="즐겨찾기 상태 변경 성공", + data=FavoriteOut( + project_id=project_id, + is_favorite=is_favorite, + ) + ) \ No newline at end of file diff --git a/app/modules/favorite/schemas.py b/app/modules/favorite/schemas.py new file mode 100644 index 0000000..3ebe8e7 --- /dev/null +++ b/app/modules/favorite/schemas.py @@ -0,0 +1,17 @@ +from uuid import UUID + +from sqlmodel import SQLModel, Field + + +class FavoriteOut(SQLModel): + project_id: UUID = Field(description="프로젝트 ID") + is_favorite: bool = Field(description="즐겨찾기 여부") + + model_config = { + "json_schema_extra": { + "example": { + "project_id": "3e1672cf-8d99-4b1c-9b5e-9c3ece11b089", + "is_favorite": True, + } + } + } \ No newline at end of file diff --git a/app/modules/favorite/service.py b/app/modules/favorite/service.py new file mode 100644 index 0000000..fbeec4f --- /dev/null +++ b/app/modules/favorite/service.py @@ -0,0 +1,68 @@ +from uuid import UUID + +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.exceptions import AppError +from app.modules.favorite.models import Favorite +from app.modules.favorite.repository import FavoriteRepository +from app.modules.project.service import ProjectService + + +class FavoriteService: + def __init__( + self, + session: AsyncSession, + repository: FavoriteRepository, + project_service: ProjectService, + ): + self.session = session + self.repository = repository + self.project_service = project_service + + async def create(self, project_id: UUID, member_id: UUID) -> Favorite: + project = await self.project_service.get(project_id, include_deleted=False) + if not project: + raise AppError.not_found(f"Project[{project_id}]") + + exists = await self.repository.get_by_project_member(project_id, member_id) + if exists: + raise AppError.bad_request("이미 즐겨찾기한 프로젝트입니다.") + + favorite = Favorite( + project_id=project_id, + member_id=member_id, + ) + + try: + created = await self.repository.create_favorite(favorite) + await self.session.commit() + await self.session.refresh(created) + return created + + except IntegrityError: + await self.session.rollback() + raise AppError.bad_request("즐겨찾기 추가 중 무결성 오류가 발생했습니다.") + + async def delete(self, project_id: UUID, member_id: UUID) -> None: + favorite = await self.repository.get_by_project_member(project_id, member_id) + if not favorite: + raise AppError.not_found("즐겨찾기하지 않은 프로젝트입니다.") + + try: + await self.repository.delete_favorite(favorite) + await self.session.commit() + + except Exception: + await self.session.rollback() + raise + + async def favorite(self, project_id: UUID, member_id: UUID) -> bool: + favorite = await self.repository.get_by_project_member(project_id, member_id) + + if favorite: + await self.delete(project_id, member_id) + return False + + await self.create(project_id, member_id) + return True \ No newline at end of file