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
42 changes: 41 additions & 1 deletion app/follow/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy.dialects.postgresql import insert

from app.database.deps import SessionDep
from app.users.models import user_follow_table
from app.users.models import User, user_follow_table
from app.utils.dependency import dependency


Expand Down Expand Up @@ -59,3 +59,43 @@ async def remove_all_for_user(self, *, user_id: int):
| (user_follow_table.c.following_id == user_id)
)
)

async def find_followers_by_user_id(
self, *, user_id: int, cursor: int | None = None, limit: int = 10
) -> list[User]:
stmt = (
select(User)
.join(user_follow_table, User.id == user_follow_table.c.follower_id)
.where(
user_follow_table.c.following_id == user_id,
User.deleted_at.is_(None),
)
.order_by(User.id.desc())
.limit(limit)
)

if cursor is not None:
stmt = stmt.where(User.id < cursor)

result = await self.session.scalars(stmt)
return list(result.all())

async def find_followings_by_user_id(
self, *, user_id: int, cursor: int | None = None, limit: int = 10
) -> list[User]:
stmt = (
select(User)
.join(user_follow_table, User.id == user_follow_table.c.following_id)
.where(
user_follow_table.c.follower_id == user_id,
User.deleted_at.is_(None),
)
.order_by(User.id.desc())
.limit(limit)
)

if cursor is not None:
stmt = stmt.where(User.id < cursor)

result = await self.session.scalars(stmt)
return list(result.all())
14 changes: 11 additions & 3 deletions app/like/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,24 @@ async def remove_all_by_user(self, *, user_id: int):
delete(post_like_table).where(post_like_table.c.user_id == user_id)
)

async def find_users_by_post_id(self, *, post_id: int) -> list[User]:
result = await self.session.scalars(
async def find_users_by_post_id(
self, *, post_id: int, cursor: int | None = None, limit: int = 10
) -> list[User]:
stmt = (
select(User)
.join(post_like_table, User.id == post_like_table.c.user_id)
.where(
post_like_table.c.post_id == post_id,
User.deleted_at.is_(None),
)
.order_by(User.id)
.order_by(User.id.desc())
.limit(limit)
)

if cursor is not None:
stmt = stmt.where(User.id < cursor)

result = await self.session.scalars(stmt)
return list(result.all())


Expand Down
12 changes: 9 additions & 3 deletions app/posts/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from app.core.router import create_router
from app.storage.schemas import ImageUrl
from app.storage.service import ImageMetadata, create_presigned_upload_url
from app.users.schemas import UserRead
from app.users.schemas import UserListCursor, UserRead

from .schemas import (
PostCompactRead,
Expand All @@ -25,6 +25,7 @@
router.include_router(comments_router, prefix="/{post_id}/comments")

POST_CURSOR_LIMIT = 10
USER_LIST_LIMIT = 20


@router.get("/home")
Expand Down Expand Up @@ -165,8 +166,13 @@ async def unlike_post(
async def read_post_liked_users(
post_service: PostService,
post_id: int,
pagination: PaginationDep,
current_user: CurrentUser,
) -> list[UserRead]:
) -> Page[UserRead]:
cursor = UserListCursor.decode(pagination.cursor)
return await post_service.get_post_liked_users(
post_id=post_id, current_user=current_user
post_id=post_id,
cursor=cursor,
limit=USER_LIST_LIMIT,
current_user=current_user,
)
27 changes: 22 additions & 5 deletions app/posts/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from app.storage.deps import S3ClientDep
from app.storage.service import get_image_metadata
from app.users.models import User
from app.users.schemas import UserRead
from app.users.schemas import UserListCursor, UserRead
from app.utils.dependency import dependency

from .models import Post, PostImage
Expand Down Expand Up @@ -283,13 +283,30 @@ async def unlike_post(self, *, post_id: int, current_user: User):
)

async def get_post_liked_users(
self, *, post_id: int, current_user: User
) -> list[UserRead]:
self,
*,
post_id: int,
cursor: UserListCursor | None,
limit: int,
current_user: User,
) -> Page[UserRead]:
post = await self.post_repository.find_by_id(post_id=post_id)
if not post:
raise HTTPException(status_code=404, detail="게시물을 찾을 수 없습니다.")

users = await self.post_like_repository.find_users_by_post_id(post_id=post_id)
return [
cursor_id = cursor.user_id if cursor else None
users = await self.post_like_repository.find_users_by_post_id(
post_id=post_id, cursor=cursor_id, limit=limit
)

user_reads = [
UserRead.from_user(user, current_user_id=current_user.id) for user in users
]

next_cursor = (
UserListCursor(user_id=users[-1].id).encode()
if len(users) == limit
else None
)

return Page[UserRead](list=user_reads, next_cursor=next_cursor)
38 changes: 37 additions & 1 deletion app/users/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

from app.auth.deps import CurrentUser
from app.auth.schemas import AuthDeleteRequest
from app.common.deps import PaginationDep
from app.common.schemas import Page
from app.core.router import create_router
from app.storage.schemas import ImageUrl

from .schemas import UserProvider, UserReadProfile, UserUpdate
from .schemas import UserListCursor, UserProvider, UserRead, UserReadProfile, UserUpdate
from .service import UserImageMeta, UserService

USER_LIST_LIMIT = 20

router = create_router()


Expand Down Expand Up @@ -81,3 +85,35 @@ async def unfollow_user(
await user_service.unfollow(
to_user_handle=to_user_handle, current_user=current_user
)


@router.get("/{user_handle}/followers", status_code=status.HTTP_200_OK)
async def read_user_followers(
user_service: UserService,
user_handle: str,
pagination: PaginationDep,
current_user: CurrentUser,
) -> Page[UserRead]:
cursor = UserListCursor.decode(pagination.cursor)
return await user_service.get_user_followers(
user_handle=user_handle,
cursor=cursor,
limit=USER_LIST_LIMIT,
current_user=current_user,
)


@router.get("/{user_handle}/followings", status_code=status.HTTP_200_OK)
async def read_user_followings(
user_service: UserService,
user_handle: str,
pagination: PaginationDep,
current_user: CurrentUser,
) -> Page[UserRead]:
cursor = UserListCursor.decode(pagination.cursor)
return await user_service.get_user_followings(
user_handle=user_handle,
cursor=cursor,
limit=USER_LIST_LIMIT,
current_user=current_user,
)
16 changes: 16 additions & 0 deletions app/users/schemas.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
from datetime import date
from typing import Annotated

Expand Down Expand Up @@ -144,3 +145,18 @@ class UserProvider(APISchema):
email: bool = False
kakao: bool = False
apple: bool = False


class UserListCursor(APISchema):
user_id: int

def encode(self) -> str:
payload = self.model_dump_json().encode()
return base64.urlsafe_b64encode(payload).decode()

@classmethod
def decode(cls, token: str | None) -> "UserListCursor | None":
if not token:
return None
data = base64.urlsafe_b64decode(token.encode()).decode()
return cls.model_validate_json(data)
64 changes: 63 additions & 1 deletion app/users/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from app.auth.schemas import AuthDeleteRequest
from app.auth.service import AuthInfoService
from app.comments.repository import CommentRepository
from app.common.schemas import ValidationResult
from app.common.schemas import Page, ValidationResult
from app.follow.repository import FollowRepository
from app.like.repository import CommentLikeRepository, PostLikeRepository
from app.posts.repository import PostRepository
Expand All @@ -23,7 +23,9 @@
from .schemas import (
UserCreate,
UserImageMeta,
UserListCursor,
UserProvider,
UserRead,
UserReadProfile,
UserUpdate,
)
Expand Down Expand Up @@ -225,6 +227,66 @@ async def unfollow(self, *, to_user_handle: str, current_user: CurrentUser):
to_user_id=target_user_id,
)

async def get_user_followers(
self,
*,
user_handle: str,
cursor: UserListCursor | None,
limit: int,
current_user: CurrentUser,
) -> Page[UserRead]:
user_id = await self.user_repository.find_id_by_handle(user_handle=user_handle)
if not user_id:
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")

cursor_id = cursor.user_id if cursor else None
followers = await self.follow_repository.find_followers_by_user_id(
user_id=user_id, cursor=cursor_id, limit=limit
)

user_reads = [
UserRead.from_user(user=follower, current_user_id=current_user.id)
for follower in followers
]

next_cursor = (
UserListCursor(user_id=followers[-1].id).encode()
if len(followers) == limit
else None
)

return Page[UserRead](list=user_reads, next_cursor=next_cursor)

async def get_user_followings(
self,
*,
user_handle: str,
cursor: UserListCursor | None,
limit: int,
current_user: CurrentUser,
) -> Page[UserRead]:
user_id = await self.user_repository.find_id_by_handle(user_handle=user_handle)
if not user_id:
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")

cursor_id = cursor.user_id if cursor else None
followings = await self.follow_repository.find_followings_by_user_id(
user_id=user_id, cursor=cursor_id, limit=limit
)

user_reads = [
UserRead.from_user(user=following, current_user_id=current_user.id)
for following in followings
]

next_cursor = (
UserListCursor(user_id=followings[-1].id).encode()
if len(followings) == limit
else None
)

return Page[UserRead](list=user_reads, next_cursor=next_cursor)

async def _validate_target_user(
self, to_user_handle: str, current_user: CurrentUser
):
Expand Down
Loading