diff --git a/app/grad_2025/schemas.py b/app/grad_2025/schemas.py index 4fb5433..c4c4d40 100644 --- a/app/grad_2025/schemas.py +++ b/app/grad_2025/schemas.py @@ -20,6 +20,7 @@ def from_grad_2025(cls, grad_2025: Grad2025): class Grad2025Cursor(APISchema): id: int + seed: str | None = None def encode(self) -> str: payload = self.model_dump_json().encode() diff --git a/app/grad_2025/service.py b/app/grad_2025/service.py index 2f8e122..1196b28 100644 --- a/app/grad_2025/service.py +++ b/app/grad_2025/service.py @@ -1,3 +1,5 @@ +import uuid + from app.category.enums import Category from app.common.schemas import Page from app.posts.repository import PostRepository @@ -28,9 +30,11 @@ async def search_posts( categories: list[Category] | None = None, univ_majors: list[str] | None = None, ) -> Page[PostCompactRead]: - cursor_id = cursor.id if cursor else None + seed = cursor.seed if cursor and cursor.seed else str(uuid.uuid4()) + posts = await self.post_repository.find_grad_2025_posts( - cursor=cursor_id, + seed=seed, + cursor=cursor, limit=limit + 1, categories=categories, univ_majors=univ_majors, @@ -38,7 +42,7 @@ async def search_posts( has_next = len(posts) > limit if has_next: posts = posts[:-1] - next_cursor = Grad2025Cursor(id=posts[-1].id).encode() + next_cursor = Grad2025Cursor(id=posts[-1].id, seed=seed).encode() else: next_cursor = None compact_posts = [PostCompactRead.from_post(post) for post in posts] diff --git a/app/posts/repository.py b/app/posts/repository.py index e813967..9c550da 100644 --- a/app/posts/repository.py +++ b/app/posts/repository.py @@ -8,6 +8,7 @@ from app.common.schemas import FeedCursor from app.database.deps import SessionDep from app.grad_2025.models import Grad2025 +from app.grad_2025.schemas import Grad2025Cursor from app.users.models import User from app.utils.dependency import dependency @@ -296,12 +297,17 @@ async def find_univ_major_by_post_id(self, *, post_id: int) -> Grad2025 | None: async def find_grad_2025_posts( self, *, - cursor: int | None, + seed: str, + cursor: Grad2025Cursor | None, limit: int, categories: list[Category] | None = None, univ_majors: list[str] | None = None, ) -> list[Post]: - """post_grad_2025_table에 존재하는 Post만 조회합니다.""" + """post_grad_2025_table에 존재하는 Post만 조회합니다. 시드 기반 랜덤 정렬.""" + random_order = func.md5(func.concat(seed, Post.id.cast(String))).label( + "random_order" + ) + stmt = ( select(Post) .options( @@ -310,19 +316,16 @@ async def find_grad_2025_posts( ) .join(post_grad_2025_table, Post.id == post_grad_2025_table.c.post_id) .where(Post.deleted_at.is_(None)) - .order_by(Post.created_at.desc(), Post.id.desc()) + .order_by(random_order, Post.id.desc()) .limit(limit) ) if cursor is not None: - # 커서 id의 created_at을 가져와서 그 이전 것들만 조회 - cursor_created_at = ( - select(Post.created_at).where(Post.id == cursor).scalar_subquery() - ) + current_random = func.md5(func.concat(seed, str(cursor.id))) stmt = stmt.where( or_( - Post.created_at < cursor_created_at, - and_(Post.created_at == cursor_created_at, Post.id < cursor), + random_order > current_random, + and_(random_order == current_random, Post.id < cursor.id), ) )