Published on

새 모델 없이 Post + 카테고리로 FAQ 게시판 만들기

Authors
  • avatar
    Name
    Hyo814
    Twitter

새 모델 없이 Post + 카테고리로 FAQ 게시판 만들기

FAQ(자주 묻는 질문) 게시판을 추가했습니다. 처음엔 "FAQ 모델을 새로 만들까?" 였는데, 결국 기존 Post 모델에 FAQ 카테고리를 얹는 방향으로 갔어요. 게시판이 늘 때마다 모델을 늘리지 않는 게 이 프로젝트의 결이기도 했습니다. 그 결정과 구현을 정리합니다.

참고로 1:1 비공개 Q&A는 별도 글에서 다뤘는데, 그건 사용자별 비공개 문의였고 이번 FAQ는 공개·읽기 중심 이라 성격이 완전히 다릅니다.


1. 결정 — 새 모델 vs 기존 모델 + 카테고리

FAQ가 필요로 하는 건 결국 제목 + 본문 + 노출 상태 + 분류입니다. 그건 공지사항이 쓰는 Post와 똑같았어요. 새 모델을 파면 마이그레이션·관리자 화면·폼을 다 새로 만들어야 하는데, FAQ는 그만한 고유 필드가 없었습니다.

방식장점단점
Faq 모델도메인 명확마이그레이션·CRUD·폼 전부 신설, 공지와 중복
Post + FAQ 카테고리기존 CRUD·폼 재사용쿼리에 카테고리 조건 필수

후자로 갔어요. 대신 카테고리 조건을 빼먹으면 공지·FAQ가 섞이는 리스크가 있어서, 그걸 막는 게 구현의 핵심이 됐습니다.


2. 카테고리를 안전하게 얻기

FAQ 카테고리가 없는 환경(초기화 커맨드 미실행)에서도 깨지지 않도록, 없으면 만들어 반환 하는 헬퍼를 뒀어요.

FAQ_CATEGORY_NAME = "FAQ"

def _get_faq_category():
    """FAQ 분류를 반환한다. init_post_category 가 실행되지 않았어도 안전하게 생성한다."""
    category, _created = Category.objects.get_or_create(
        name=FAQ_CATEGORY_NAME,
        defaults={"description": "자주 묻는 질문 게시판"},
    )
    return category

"카테고리가 있다고 가정" 하는 대신 get_or_create로 두니, 새 환경에서도 첫 진입이 터지지 않았습니다.


3. 사용자 화면 — 공개·검색·큐레이션 순서

사용자용 목록 뷰는 카테고리 = FAQ 이면서 공개 상태 인 글만 보여줍니다. 검색은 제목/내용 부분일치로요.

class FaqListView(ListView):
    model = Post
    template_name = "common/faq/faq_list.html.j2"

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.search_keyword:
            queryset = queryset.filter(
                Q(category__name__exact="FAQ")
                & Q(state__exact=Post.State.PUBLIC)
                & (Q(subject__icontains=self.search_keyword)
                   | Q(content__icontains=self.search_keyword))
            ).distinct()
        else:
            queryset = queryset.filter(
                Q(category__name__exact="FAQ") & Q(state__exact=Post.State.PUBLIC)
            ).distinct()
        # 등록(큐레이션) 순서대로 노출 — faq.json 의 우선순위 순서를 그대로 따른다.
        return queryset.order_by("pk")

여기서 작은 결정이 정렬이었어요. FAQ는 최신순이 아니라 "중요한 질문이 위" 가 맞습니다. 그래서 최신순(-create_date) 대신 등록 순서(pk)로 노출했어요. 적재 데이터(faq.json)에 우선순위 순서대로 넣어두면, 그 순서가 곧 화면 순서가 됩니다. 큐레이션을 데이터 순서로 표현 한 셈이에요.

같은 김에 공지사항 목록 검색도 공지사항 카테고리로 한정 했습니다. 한 Post 모델을 공유하니, 각 화면이 자기 카테고리만 보도록 조건을 박아두는 게 중요했어요.


4. 관리자 CRUD와 읽기 전용 본문

관리자 쪽은 기존 PostCreateView/UpdateView/DeleteView를 그대로 재사용했습니다. FAQ 목록에 FAQ 추가 버튼을 얹고, 검토 탭에 FAQ를 추가한 정도예요. (아래 FaqListView는 사용자용과 이름은 같지만 web_admin/views/system/faq.py의 별도 클래스예요.)

class FaqListView(ListView):
    extra_context = _make_context({
        "extra_search_buttons": [{
            "title": _("FAQ 추가"), "class": "primary",
            "onclick": {"type": "url", "url": "web-admin:system:add_faq"},
        }],
        "detail_url": "web-admin:system:faq_detail",
        "tab_list": _get_tab_list(),
    })
    model = Post

상세 페이지에서는 CKEditor 본문을 읽기 전용 으로 처리했어요. FAQ·공지 상세는 보는 화면이지 편집 화면이 아니니, 에디터가 입력 가능한 상태로 뜨면 혼란스럽습니다.

데이터는 load_faq 커맨드로 faq.json을 적재하게 했습니다. 카테고리 초기화(init_post_category)와 함께 load.sh/load.bat에 묶어, 새 환경에서 한 번에 시드 되도록 했어요.


5. 교훈

  • 게시판이 늘 때마다 모델을 늘리지 않아도 됩니다. 고유 필드가 없다면 기존 모델 + 카테고리가 마이그레이션·CRUD·폼을 통째로 아껴줘요.
  • 모델을 공유하면 "카테고리 조건"이 안전장치입니다. 각 화면이 자기 카테고리만 보도록 쿼리에 못 박아야, 공지·FAQ가 섞이지 않아요. 공지 검색까지 카테고리로 한정한 게 그래서였습니다.
  • 정렬은 도메인 의미를 담습니다. FAQ는 최신순이 아니라 큐레이션 순서(중요도). 적재 순서를 곧 노출 순서로 쓰면 운영이 단순해져요.
  • 시드 커맨드를 같이 만들면 환경 재현이 쉬워집니다. 카테고리 생성을 get_or_create로 방어하고, 적재 커맨드를 load 스크립트에 묶어두면 새 환경에서 안 터집니다.

새 모델을 안 만들고도 FAQ가 깔끔하게 떨어졌어요. "이 데이터가 정말 새 모델을 필요로 하는가, 아니면 분류 하나면 되는가" 를 먼저 물은 게 이번 작업의 출발점이었습니다.