기능 추가
포스트 공개 비공개 처리
모델 수정
class Post(TimeStampedModel):
class Visibility(models.TextChoices):
PUBLIC = "PUBLIC", "전체 공개"
PRIVATE = "PRIVATE", "비공개"
...
visibility = models.CharField(
max_length=10,
choices=Visibility.choices,
default=Visibility.PUBLIC,
help_text="게시글의 공개 범위를 설정합니다."
)
조회 서비스 조건 추가
def get_global_posts() -> QuerySet[Post]:
"""
모든 사용자의 공개된 포스트 목록을 가져옵니다. (전체 피드용)
"""
return (
Post.objects.filter(
is_temp=False,
visibility=Post.Visibility.PUBLIC,
deleted_at__isnull=True,
)
.select_related("user")
.order_by("-created_at")
)
시리얼라이저 컬럼 추가
class PostCreateSerializer(serializers.ModelSerializer):
...
class Meta:
model = Post
fields = [
"title",
"content",
"thumbnail",
"summary",
"is_temp",
"tags",
"visibility",
]
서비스 수정
def create_post(*, author: User, validated_data: dict[str, Any]):
"""
게시글을 생성하고 태그를 대량(Bulk)으로 처리하여 최적화한 로직입니다.
"""
tags_names = validated_data.pop("tags", [])
content = validated_data["content"]
summary = validated_data.get("summary") or content[:150]
post = Post.objects.create(
user=author,
title=validated_data["title"],
content=content,
summary=summary,
thumbnail=validated_data.get("thumbnail"),
is_temp=validated_data.get("is_temp", False),
visibility=validated_data.get("visibility", Post.Visibility.PUBLIC),
)
디자인 추가

코드 분석
포스트 작성 및 저장 플로우 (Create)
[백엔드: post/services/post_create_service.py]
- 전달받은
is_temp와 visibility 값을 그대로 데이터베이스에 저장
def create_post(*, author: User, validated_data: dict[str, Any]):
...
post = Post.objects.create(
user=author,
title=validated_data["title"],
content=content,
summary=summary,
is_temp=validated_data.get("is_temp", False),
visibility=validated_data.get("visibility", Post.Visibility.PUBLIC),
)
...
[프론트엔드: templates/post/write.html]
- 사용자가 클릭한 버튼에 따라 isTemp 변수 값이 true 또는 false로 API에 전달
// 발행하기 버튼 클릭 시 -> isTemp = false
document.getElementById('postForm').addEventListener('submit', function(e) {
e.preventDefault();
savePost(false);
});
// 임시저장 버튼 클릭 시 -> isTemp = true
document.getElementById('tempSaveBtn').addEventListener('click', function() {
savePost(true);
});
// fetch API 요청 본문
body: JSON.stringify({
title: title,
content: content,
tags: tags,
is_temp: isTemp, // 핵심 1: 임시저장 여부
visibility: visibility // 핵심 2: 공개 범위 (PUBLIC / PRIVATE)
})
목록 조회 플로우 (Read)
- 저장된 데이터는
is_temp와 visibility의 조합에 따라 3개의 각기 다른 페이지에 노출
전체 피드 (Global Feed)
- 조건: 정식 발행되었고(
is_temp=False), 전체 공개인 글(visibility=PUBLIC)
- 플로우:
global_list.html ➔ PostAPIView.get() ➔ get_global_posts()
def get_global_posts() -> QuerySet[Post]:
return Post.objects.filter(
is_temp=False,
visibility=Post.Visibility.PUBLIC,
deleted_at__isnull=True,
).select_related("user").order_by("-created_at")
내 블로그
- 조건: 내가 작성했고, 정식 발행된 글(
is_temp=False). (공개/비공개 무관)
- 플로우:
my_list.html ➔ MyPostAPIView.get() ➔ get_my_published_posts()
def get_my_published_posts(*, user: User) -> QuerySet[Post]:
return Post.objects.filter(
user=user,
is_temp=False,
deleted_at__isnull=True,
).select_related("user").order_by("-created_at")
임시 저장함 (Temp Posts)
- 조건: 내가 작성했고, 아직 작성 중인 글(is_temp=True)
- 플로우:
temp_list.html ➔ MyTempAPIView.get() ➔ get_my_temp_posts()
def get_my_temp_posts(*, user: User) -> QuerySet[Post]:
return Post.objects.filter(
user=user,
is_temp=True,
deleted_at__isnull=True,
).order_by("-created_at")
임시 저장글 관리 플로우 (Manage: Publish / Delete)
- 임시 저장함(
temp_list.html)에서는 작성 중인 글을 정식으로 발행하거나 삭제할 수 있습니다.
- 발행 (PATCH): 글의
is_temp 상태를 False로 바꿔줍니다.
- 삭제 (DELETE): 글을 실제로 지우지 않고
deleted_at 필드에 시간을 기록합니다(Soft Delete)
def restore_temp_post(post_id: int, user: User):
"""임시글을 공개글로 전환(발행)합니다."""
post = Post.objects.filter(id=post_id, user=user, is_temp=True).first()
post.is_temp = False
post.save(update_fields=["is_temp"])
def soft_delete_post(post_id: int, user: User):
"""게시글을 삭제(Soft Delete) 처리합니다."""
post = Post.objects.filter(id=post_id, user=user, deleted_at__isnull=True).first()
post.deleted_at = timezone.now()
post.save(update_fields=["deleted_at"])
Post UD
update
@transaction.atomic
def update_post(*, post_id: int, user: User, validated_data: dict):
post = Post.objects.filter(id=post_id, user=user, deleted_at__isnull=True).first()
if not post:
raise BaseCustomException(ErrorMessage.POST_NOT_FOUND)
tag_names = validated_data.pop("tags", None)
for attr, value in validated_data.items():
setattr(post, attr, value)
if not validated_data.get("summary"):
post.summary = post.content[:150]
post.save()
if tag_names is not None:
post.posttag_set.all().delete()
existing_tags = Tag.objects.filter(name__in=tag_names)
existing_names = {t.name for t in existing_tags}
new_names = set(tag_names) - existing_names
if new_names:
Tag.objects.bulk_create([Tag(name=name) for name in new_names])
all_tags = Tag.objects.filter(name__in=tag_names)
PostTag.objects.bulk_create([PostTag(post=post, tag=tag) for tag in all_tags])
return post
@extend_schema(tags=["포스트"], summary="게시글 수정", request=PostCreateSerializer)
def put(self, request: Request, post_id: int):
serializer = PostCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = cast(User, request.user)
updated_post = update_post(
post_id=post_id,
user=user,
validated_data=serializer.validated_data
)
return Response(PostCreateSerializer(updated_post).data, status=status.HTTP_200_OK)
delete
def delete(self, request: Request, post_id: int):
user = cast(User, request.user)
delete_post(post_id=post_id, user=user)
return Response(status=status.HTTP_204_NO_CONTENT)
def delete_post(post_id: int, user: User):
post = Post.objects.filter(id=post_id, user=user, deleted_at__isnull=True).first()
if not post:
raise BaseCustomException(ErrorMessage.POST_NOT_FOUND)
post.deleted_at = timezone.now()
post.save(update_fields=["deleted_at"])