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
17 changes: 17 additions & 0 deletions app/modules/favorite/dependencies.py
Original file line number Diff line number Diff line change
@@ -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)]
28 changes: 28 additions & 0 deletions app/modules/favorite/repository.py
Original file line number Diff line number Diff line change
@@ -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()
34 changes: 34 additions & 0 deletions app/modules/favorite/router.py
Original file line number Diff line number Diff line change
@@ -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,
)
)
17 changes: 17 additions & 0 deletions app/modules/favorite/schemas.py
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
68 changes: 68 additions & 0 deletions app/modules/favorite/service.py
Original file line number Diff line number Diff line change
@@ -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
Loading