177 lines
5.9 KiB
Python
177 lines
5.9 KiB
Python
"""
|
|
Comment API Router
|
|
|
|
영상 댓글 관련 엔드포인트를 제공합니다.
|
|
|
|
엔드포인트 목록:
|
|
- POST /comment/video/{video_id}: 댓글/대댓글 작성 (로그인 필수)
|
|
- GET /comment/video/{video_id}: 댓글 목록 조회 (비로그인 허용)
|
|
- DELETE /comment/{comment_id}: 본인 댓글 소프트 삭제 (로그인 필수)
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.comment.schemas.comment_schema import (
|
|
CommentCreateRequest,
|
|
CommentCreateResponse,
|
|
CommentItem,
|
|
DeleteCommentResponse,
|
|
)
|
|
from app.comment.services.comment import create_comment, delete_comment, list_comments
|
|
from app.database.session import get_session
|
|
from app.dependencies.pagination import PaginationParams, get_pagination_params
|
|
from app.user.dependencies.auth import get_current_user, get_current_user_optional
|
|
from app.user.models import User
|
|
from app.utils.logger import get_logger
|
|
from app.utils.pagination import PaginatedResponse
|
|
|
|
logger = get_logger("comment")
|
|
|
|
router = APIRouter(prefix="/comment", tags=["Comment"])
|
|
|
|
|
|
@router.post(
|
|
"/video/{video_id}",
|
|
summary="댓글/대댓글 작성",
|
|
description="""
|
|
## 개요
|
|
영상에 댓글 또는 대댓글을 작성합니다. 로그인 필수.
|
|
|
|
## 경로 파라미터
|
|
- **video_id**: 댓글을 달 영상의 ID
|
|
|
|
## 요청 본문
|
|
- **content**: 댓글 본문 (1~100자)
|
|
- **parent_id**: 대댓글일 때만 부모 댓글 id (생략 시 최상위 댓글)
|
|
|
|
## 참고
|
|
- 작성자 정보는 응답에 포함되지 않습니다 (익명 정책).
|
|
- 대댓글에 또 대댓글을 다는 것은 불가합니다 (최대 2-depth).
|
|
""",
|
|
response_model=CommentCreateResponse,
|
|
responses={
|
|
200: {"description": "댓글 작성 성공"},
|
|
400: {"description": "잘못된 parent_id (2-depth 초과, 다른 영상의 댓글 등)"},
|
|
401: {"description": "인증 실패"},
|
|
404: {"description": "영상을 찾을 수 없음"},
|
|
},
|
|
)
|
|
async def post_comment(
|
|
video_id: int,
|
|
body: CommentCreateRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> CommentCreateResponse:
|
|
logger.info(
|
|
f"[post_comment] START - video_id: {video_id}, user: {current_user.user_uuid}, "
|
|
f"parent_id: {body.parent_id}"
|
|
)
|
|
comment = await create_comment(
|
|
session=session,
|
|
video_id=video_id,
|
|
user_uuid=current_user.user_uuid,
|
|
nickname=body.nickname,
|
|
content=body.content,
|
|
parent_id=body.parent_id,
|
|
)
|
|
logger.info(f"[post_comment] SUCCESS - comment_id: {comment.id}")
|
|
return CommentCreateResponse(
|
|
id=comment.id,
|
|
nickname=comment.nickname or "익명",
|
|
parent_id=comment.parent_id,
|
|
content=comment.content,
|
|
created_at=comment.created_at,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/video/{video_id}",
|
|
summary="댓글 목록 조회",
|
|
description="""
|
|
## 개요
|
|
영상의 댓글 목록을 페이지네이션하여 반환합니다. 비로그인도 접근 가능.
|
|
|
|
## 경로 파라미터
|
|
- **video_id**: 댓글을 조회할 영상의 ID
|
|
|
|
## 쿼리 파라미터
|
|
- **page**: 페이지 번호 (기본값: 1)
|
|
- **page_size**: 페이지당 댓글 수 (기본값: 10, 최대: 100)
|
|
|
|
## 참고
|
|
- 최상위 댓글만 페이지네이션됩니다. 각 댓글의 대댓글은 전부 포함됩니다.
|
|
- 작성자 정보는 노출되지 않으며, is_mine으로 본인 댓글 여부만 확인 가능합니다.
|
|
- 삭제된 댓글은 content=null로 노출됩니다 (대댓글이 있는 경우).
|
|
""",
|
|
response_model=PaginatedResponse[CommentItem],
|
|
responses={
|
|
200: {"description": "댓글 목록 조회 성공"},
|
|
500: {"description": "조회 실패"},
|
|
},
|
|
)
|
|
async def get_comments(
|
|
video_id: int,
|
|
current_user: User | None = Depends(get_current_user_optional),
|
|
session: AsyncSession = Depends(get_session),
|
|
pagination: PaginationParams = Depends(get_pagination_params),
|
|
) -> PaginatedResponse[CommentItem]:
|
|
logger.info(
|
|
f"[get_comments] START - video_id: {video_id}, "
|
|
f"page: {pagination.page}, page_size: {pagination.page_size}"
|
|
)
|
|
current_user_uuid = current_user.user_uuid if current_user else None
|
|
result = await list_comments(
|
|
session=session,
|
|
video_id=video_id,
|
|
page=pagination.page,
|
|
page_size=pagination.page_size,
|
|
current_user_uuid=current_user_uuid,
|
|
)
|
|
logger.info(f"[get_comments] SUCCESS - total: {result.total}, items: {len(result.items)}")
|
|
return result
|
|
|
|
|
|
@router.delete(
|
|
"/{comment_id}",
|
|
summary="댓글 소프트 삭제",
|
|
description="""
|
|
## 개요
|
|
본인이 작성한 댓글을 소프트 삭제합니다. 로그인 필수.
|
|
|
|
## 경로 파라미터
|
|
- **comment_id**: 삭제할 댓글의 ID
|
|
|
|
## 참고
|
|
- 본인 댓글만 삭제 가능합니다.
|
|
- 소프트 삭제 방식으로 DB에 데이터는 유지됩니다.
|
|
- 부모 댓글 삭제 시 대댓글은 유지되며, 목록 조회 시 content=null로 표시됩니다.
|
|
""",
|
|
response_model=DeleteCommentResponse,
|
|
responses={
|
|
200: {"description": "삭제 성공"},
|
|
401: {"description": "인증 실패"},
|
|
403: {"description": "삭제 권한 없음"},
|
|
404: {"description": "댓글을 찾을 수 없음"},
|
|
},
|
|
)
|
|
async def remove_comment(
|
|
comment_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> DeleteCommentResponse:
|
|
logger.info(
|
|
f"[remove_comment] START - comment_id: {comment_id}, user: {current_user.user_uuid}"
|
|
)
|
|
await delete_comment(
|
|
session=session,
|
|
comment_id=comment_id,
|
|
current_user_uuid=current_user.user_uuid,
|
|
)
|
|
logger.info(f"[remove_comment] SUCCESS - comment_id: {comment_id}")
|
|
return DeleteCommentResponse(
|
|
success=True,
|
|
comment_id=comment_id,
|
|
message="댓글이 삭제되었습니다.",
|
|
)
|