Skip to content

Commit ca4b819

Browse files
committed
fix
1 parent ba12700 commit ca4b819

4 files changed

Lines changed: 69 additions & 5 deletions

File tree

main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
from fastapi import FastAPI
22
from fastapi.middleware.cors import CORSMiddleware
33
from prometheus_fastapi_instrumentator import Instrumentator
4+
from sqlalchemy import text
45

56
from database import engine, Base
67
from routes import comments, likes, subscribe, reactions, views
78

89
# 테이블 자동 생성
910
Base.metadata.create_all(bind=engine)
1011

12+
# 기존 DB에 parent_id 컬럼 추가 (없는 경우에만)
13+
with engine.connect() as conn:
14+
try:
15+
conn.execute(text("ALTER TABLE comments ADD COLUMN parent_id INTEGER REFERENCES comments(id)"))
16+
conn.commit()
17+
except Exception:
18+
pass # 이미 존재하는 컬럼
19+
1120
app = FastAPI(title="코알라 오딧세이 Blog API", version="1.0.0")
1221

1322
Instrumentator().instrument(app).expose(app)

models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class Comment(Base):
1212
nickname: Mapped[str] = mapped_column(String(50), nullable=False)
1313
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
1414
content: Mapped[str] = mapped_column(Text, nullable=False)
15+
parent_id: Mapped[int | None] = mapped_column(
16+
Integer, nullable=True, index=True, default=None
17+
)
1518
created_at: Mapped[datetime] = mapped_column(
1619
DateTime(timezone=True),
1720
default=lambda: datetime.now(timezone.utc),

routes/comments.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,75 @@ def _verify_password(password: str, hashed: str) -> bool:
1818
return bcrypt.checkpw(password.encode(), hashed.encode())
1919

2020

21+
def _build_response(comment: Comment, replies: list[Comment]) -> CommentResponse:
22+
return CommentResponse(
23+
id=comment.id,
24+
post_slug=comment.post_slug,
25+
nickname=comment.nickname,
26+
content=comment.content,
27+
created_at=comment.created_at,
28+
parent_id=comment.parent_id,
29+
replies=[
30+
CommentResponse(
31+
id=r.id,
32+
post_slug=r.post_slug,
33+
nickname=r.nickname,
34+
content=r.content,
35+
created_at=r.created_at,
36+
parent_id=r.parent_id,
37+
replies=[],
38+
)
39+
for r in replies
40+
],
41+
)
42+
43+
2144
@router.get("/{slug:path}/comments", response_model=list[CommentResponse])
2245
def get_comments(slug: str, db: Session = Depends(get_db)):
23-
"""특정 포스트의 댓글 목록 조회"""
24-
return (
46+
"""특정 포스트의 댓글 목록 조회 (대댓글 포함 중첩 구조)"""
47+
all_comments = (
2548
db.query(Comment)
2649
.filter(Comment.post_slug == slug)
2750
.order_by(Comment.created_at.asc())
2851
.all()
2952
)
3053

54+
replies_map: dict[int, list[Comment]] = {}
55+
top_level: list[Comment] = []
56+
57+
for c in all_comments:
58+
if c.parent_id is None:
59+
top_level.append(c)
60+
replies_map[c.id] = []
61+
else:
62+
replies_map.setdefault(c.parent_id, []).append(c)
63+
64+
return [_build_response(c, replies_map.get(c.id, [])) for c in top_level]
65+
3166

3267
@router.post("/{slug:path}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
3368
def create_comment(slug: str, body: CommentCreate, db: Session = Depends(get_db)):
34-
"""댓글 작성"""
69+
"""댓글/대댓글 작성"""
70+
if body.parent_id is not None:
71+
parent = db.query(Comment).filter(
72+
Comment.id == body.parent_id,
73+
Comment.post_slug == slug,
74+
Comment.parent_id == None, # noqa: E711 대댓글의 대댓글 방지
75+
).first()
76+
if not parent:
77+
raise HTTPException(status_code=404, detail="원댓글을 찾을 수 없습니다.")
78+
3579
comment = Comment(
3680
post_slug=slug,
3781
nickname=body.nickname,
3882
password_hash=_hash_password(body.password),
3983
content=body.content,
84+
parent_id=body.parent_id,
4085
)
4186
db.add(comment)
4287
db.commit()
4388
db.refresh(comment)
44-
return comment
89+
return _build_response(comment, [])
4590

4691

4792
@router.delete("/{slug:path}/comments/{comment_id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -51,7 +96,7 @@ def delete_comment(
5196
body: CommentDelete,
5297
db: Session = Depends(get_db),
5398
):
54-
"""비밀번호 확인 후 댓글 삭제"""
99+
"""비밀번호 확인 후 댓글 삭제 (대댓글도 함께 삭제)"""
55100
comment = (
56101
db.query(Comment)
57102
.filter(Comment.id == comment_id, Comment.post_slug == slug)
@@ -63,5 +108,9 @@ def delete_comment(
63108
if not _verify_password(body.password, comment.password_hash):
64109
raise HTTPException(status_code=403, detail="비밀번호가 올바르지 않습니다.")
65110

111+
# 원댓글이면 대댓글도 함께 삭제
112+
if comment.parent_id is None:
113+
db.query(Comment).filter(Comment.parent_id == comment_id).delete()
114+
66115
db.delete(comment)
67116
db.commit()

schemas.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class CommentCreate(BaseModel):
88
nickname: str
99
password: str
1010
content: str
11+
parent_id: int | None = None
1112

1213
@field_validator("nickname")
1314
@classmethod
@@ -43,6 +44,8 @@ class CommentResponse(BaseModel):
4344
nickname: str
4445
content: str
4546
created_at: datetime
47+
parent_id: int | None = None
48+
replies: list["CommentResponse"] = []
4649

4750
model_config = {"from_attributes": True}
4851

0 commit comments

Comments
 (0)