2025/12/27~28 Admin-category

김기훈·2025년 12월 27일

TIL

목록 보기
96/191

Django Admin

Django Admin의 개념 및 목적

  • 개념

    • 신뢰할 수 있는 사용자가 사이트 콘텐츠를 편집할 수 있는 도구"
  • 목적

    • 데이터 CRUD 관리: 코딩 없이 GUI를 통해 데이터를 생성, 조회, 수정, 삭제 함
    • 모델 유효성 검증: 모델에 정의된 clean() 메서드나 필드 제약 조건을 자동으로 적용
    • 권한 제어: 어떤 사용자가 어떤 모델에 접근할 수 있는지 세밀하게 조정

Admin 등록 및 기본 구조

  • 기본 등록 방식

from django.contrib import admin
from .models import QuestionCategory

admin.site.register(QuestionCategory)
  • 클래스 데코레이터 방식

    • 설정을 더 세밀하게 제어하기 위해 ModelAdmin 클래스를 상속받아 등록하는 방식을 주로 사용
# admin.py 

@admin.register(QuestionCategory)
class QuestionCategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'type', 'parent', 'created_at') # 목록에 노출할 필드
    list_filter = ('type',)                                # 우측 필터 사이드바
    search_fields = ('name',)                             # 검색 바

핵심 속성

  • list_display

    • 모델 목록 페이지에서 보여줄 필드들을 튜플/리스트로 정의
  • list_filter

    • 특정 필드(주로 Choice나 ForeignKey)를 기준으로 데이터를 필터링하는 기능을 제공
  • search_fields

    • "검색 기능을 활성화하며, 어떤 필드에서 검색할지 정의
  • fields / fieldsets

    • 상세 수정 페이지에서 필드의 순서나 그룹화를 설정
  • readonly_fields

    • 수정이 불가능하고 읽기만 가능한 필드를 지정합니다. (예: 생
  • raw_id_fields

    • 관계형 필드에서 드롭다운 대신 ID를 직접 입력하거나 팝업으로 선택하게 합니다. (데이터가 많을용)
  • is_staff

    • Admin 페이지에 로그인할 수 있는 권한만 부여하며, 구체적인 모델 접근 권한은 별도로 할당해야 함

데이터 유효성 검사

  • 모델에 작성한 clean() 메서드는 Admin에서도 그대로 작동
      1. 사용자가 Admin 페이지에서 데이터를 입력하고 Save를 누름
      1. Django는 모델의 full_clean()을 호출, 이 과정에서 사용자가 정의한 clean()이 실행
      1. 조건에 맞지 않으면 Admin 화면 상단에 ValidationError 메시지가 출력되며 저장이 차단
    def clean(self):
        """계층 구조 유효성 검사"""
        if self.type == 'large' and self.parent is not None:
            raise ValidationError({'parent': '대분류는 부모 카테고리를 가질 수 없습니다.'})

        if self.type == 'medium':
            if self.parent is None or self.parent.type != 'large':
                raise ValidationError({'parent': '중분류의 부모는 [대분류]여야 합니다.'})

        if self.type == 'small':
            if self.parent is None or self.parent.type != 'medium':
                raise ValidationError({'parent': '소분류의 부모는 [중분류]여야 합니다.'})

카테고리 조건

  • 질의응답 카테고리 등록

    • 스태프 / 관리자 권한을 가진 유저는 어드민 페이지 내의 질의응답 관리 메뉴의
    • 카테고리 관리 메뉴에 접속하여 질의응답 카테고리를 등록 가능
      • 카테고리 분류

        • 대분류 카테고리 → 프론트엔드, 백엔드 등
        • 중분류 카테고리 → 프로그래밍 언어, 웹프레임워크, Web, OS, 라이브러리 등
        • 소분류 카테고리 → JavaScript, Python, Django, React Next.js, FastAPI 등
      • 카테고리 등록 시 입력 항목

        • 카테고리 종류 → 대분류, 중분류, 소분류
        • 카테고리 이름
        • 부모 카테고리 → 중, 소분류 카테고리의 경우
  • 카테고리 목록 조회

    • 스태프 / 관리자 권한을 가진 유저는 어드민 페이지 내의 질의응답 관리 메뉴의
    • 카테고리 관리 메뉴에 접속하여 시스템에 등록된 질의응답 카테고리들을 목록으로 조회 가능
    • 카테고리 목록 조회 시 검색필터 + 검색 기능을 활용하여 조회 가능
      • 카테고리 목록 조회에서 확인 가능한 항목

        • 카테고리 ID / 카테고리명 / 카테고리 분류 타입 ( 대, 중, 소 )
        • 자식 카테고리
          • 카테고리 분류가 대, 중일 경우 자식 카테고리의 명칭
          • 소분류일 경우 빈칸
        • 부모 카테고리
          • 카테고리 분류가 중, 소일 경우 부모 카테고리의 명칭
          • 대분류일 경우 빈칸
        • 등록일시
        • 수정일시
  • 카테고리 삭제

    • 스태프 / 관리자 권한을 가진 유저는 어드민 페이지 내의 질의응답 관리 메뉴의
    • 카테고리 관리 메뉴에 접속하여 시스템에 등록된 질의응답 카테고리를 삭제 가능
    • 조회된 카테고리 목록에서 삭제할 카테고리를 선택하고 삭제하기 버튼을 클릭하여 삭제
    • 카테고리 삭제 전, 대, 중, 소분류에 따라 경고 메시지 팝업이 나옴
      • 대분류의 경우

        • 해당 카테고리에 속한 중분류, 소분류 카테고리가 함께 삭제
        • 각 카테고리에 속한 질의응답은 일반질문 카테고리로 전환 / 삭제된 항목 복구 불가
      • 중분류의 경우

        • 해당 카테고리에 속한 소분류 카테고리가 함께 삭제
        • 각 카테고리에 속한 질의응답은 일반질문 카테고리로 전환 / 삭제된 항목 복구 불가
      • 소분류의 경우

        • 해당 카테고리에 속한 질의응답은 일반질문 카테고리로 전환 / 삭제된 항목 복구 불가

현재 카테고리 기능

  • 1. 데이터 모델 구조 (Model)

    • Question 모델에서 category 필드를 통해 QuestionCategory 모델을 참조
class Question(TimeStampedModel):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="questions")

    category = models.ForeignKey(QuestionCategory, on_delete=models.PROTECT, related_name="questions")

    title = models.CharField(max_length=50)
    content = models.TextField()

    view_count = models.BigIntegerField(default=0)
  • 2. 카테고리 등록 및 설정 (Create Process)

    • FE 요청: 프론트엔드는 카테고리의 '이름(예: Python)'이 아닌 'ID(예: 1)'를 전송
    {
      "title": "질문 있습니다",
      "content": "내용...",
      "category": 1  // Category ID
    }
    • Serializer 검증
      • QuestionCreateSerializer의 category 필드는 PrimaryKeyRelatedField로 선언
      • 이는 입력받은 ID(1)가 실제 DB에 존재하는 카테고리인지 자동으로 검증
    • Service 처리
      • 검증이 완료된 후 create_question 서비스 함수가 호출될 때,
      • category 인자는 단순 ID가 아니라 DB에서 조회된 QuestionCategory
        • 객체(Instance) 상태로 전달
    • 저장
      • Question.objects.create를 통해 질문이 생성될 때 카테고리 객체가 관계 필드에 저장

카테고리 등록 구현 🔴

  • 질의응답 카테고리 등록

    • 스태프 / 관리자 권한을 가진 유저는 어드민 페이지 내의 질의응답 관리 메뉴의
    • 카테고리 관리 메뉴에 접속하여 질의응답 카테고리를 등록 가능
      • 카테고리 분류

        • 대분류 카테고리 → 프론트엔드, 백엔드 등
        • 중분류 카테고리 → 프로그래밍 언어, 웹프레임워크, Web, OS, 라이브러리 등
        • 소분류 카테고리 → JavaScript, Python, Django, React Next.js, FastAPI 등
      • 카테고리 등록 시 입력 항목

        • 카테고리 종류 → 대분류, 중분류, 소분류
        • 카테고리 이름
        • 부모 카테고리 → 중, 소분류 카테고리의 경우

모델 수정 🟢

from django.db import models
from django.core.exceptions import ValidationError
from apps.core.models import TimeStampedModel

class QuestionCategory(TimeStampedModel):
    CATEGORY_TYPES = (
        ('large', '대분류'),
        ('medium', '중분류'),
        ('small', '소분류'),
    )

    name = models.CharField(max_length=50, verbose_name="카테고리 이름")
    type = models.CharField(
        max_length=10, 
        choices=CATEGORY_TYPES, 
        default='large',
        verbose_name="카테고리 종류"
    )
    parent = models.ForeignKey(
        'self', 
        on_delete=models.CASCADE, 
        null=True, 
        blank=True, 
        related_name='children',
        verbose_name="부모 카테고리"
    )

    class Meta:
        db_table = "question_categories"
        verbose_name = "질의응답 카테고리"
        verbose_name_plural = "질의응답 카테고리 목록"

    def __str__(self) -> str:
        return f"[{self.get_type_display()}] {self.name}"

    def clean(self):
        """계층 구조 유효성 검사"""
        if self.type == 'large' and self.parent is not None:
            raise ValidationError({'parent': '대분류는 부모 카테고리를 가질 수 없습니다.'})
        
        if self.type == 'medium':
            if self.parent is None or self.parent.type != 'large':
                raise ValidationError({'parent': '중분류의 부모는 [대분류]여야 합니다.'})

        if self.type == 'small':
            if self.parent is None or self.parent.type != 'medium':
                raise ValidationError({'parent': '소분류의 부모는 [중분류]여야 합니다.'})

어드민 설정 🟢

  • RoleChoices를 사용하여 ST(Student)와 USER를 제외한 운영진에게만 등록 및 수정 권한을 부여
from django.contrib import admin
from apps.qna.models.question.question_category import QuestionCategory
from apps.user.models import RoleChoices

@admin.register(QuestionCategory)
class QuestionCategoryAdmin(admin.ModelAdmin):
    list_display = ('id', 'type', 'name', 'parent', 'created_at')
    list_filter = ('type',)
    search_fields = ('name',)

    fieldsets = (
        ('기본 정보', {'fields': ('type', 'name')}),
        ('계층 설정', {
            'fields': ('parent',),
            'description': '중분류는 대분류를, 소분류는 중분류를 부모로 선택해야 합니다.'
        }),
    )

    # --- 권한 설정 (Role 기반) ---

    def has_add_permission(self, request):
        """등록 권한: AD, OM, LC, TA만 허용"""
        excluded_roles = [RoleChoices.ST, RoleChoices.USER]
        return request.user.is_authenticated and \
               request.user.role not in excluded_roles and \
               request.user.is_staff

    def has_change_permission(self, request, obj=None):
        """수정 권한: AD, OM, LC, TA만 허용"""
        excluded_roles = [RoleChoices.ST, RoleChoices.USER]
        return request.user.is_authenticated and \
               request.user.role not in excluded_roles and \
               request.user.is_staff

    def has_view_permission(self, request, obj=None):
        """조회 권한: 모든 스태프 허용"""
        return request.user.is_authenticated and request.user.is_staff

    def has_delete_permission(self, request, obj=None):
        """삭제 권한: 현재 요구사항에 따라 AD(최고관리자)만 가능하게 설정 가능"""
        return request.user.is_authenticated and request.user.role == RoleChoices.AD

작동 흐름

  • 생성(Create) 기능이 동작하는 원리

    • admin.py에서 권한만 체크 / 실제 생성은 Django가 내부적으로 처리
    • 자동 폼(Form) 생성
      • Django Admin은 QuestionCategory 모델의 필드(name, type, parent)를 보고
        • 관리자 페이지에 입력 폼을 자동으로 그림
    • 권한 확인
      • has_add_permission이 True를 반환하면 Django는 화면에 "추가" 버튼을 노출
    • 데이터 검증 및 저장
        1. 관리자가 [저장]을 누르면 Django Admin은 모델의 clean() 메서드를 호출하여
        • 설정한 계층 구조 규칙을 검사
        1. 검증이 통과되면 Django 내부의 save() 메서드가 실행되어 DB의
        • question_categories 테이블에 데이터를 넣음
  • 삭제(Delete) 기능이 동작하는 원리

    • Django Admin의 표준 기능을 그대로 사용하면서, 모델에 설정한 '삭제 정책'에 따라 작동
    • 삭제 버튼 노출
      • has_delete_permission이 True인 관리자(AD)에게만 삭제 버튼이 노출
    • 연쇄 삭제(CASCADE)
      • 모델에서 parent = models.ForeignKey(..., on_delete=models.CASCADE)
      • 설정했기 때문에, Django는 "부모가 지워지면 자식도 지워라"라는 명령을 DB 수준에서 수행
    • 데이터 보호(SET_DEFAULT)
      • 질문 모델에서 category = models.ForeignKey(..., on_delete=models.SET_DEFAULT)
      • 카테고리가 삭제되는 순간 연결된 질문들의 카테고리 ID가 자동으로 '일반질문' ID로 업데이트
  • 간략한 코드가 가능한 이유

    • 모델(models.py)에 "데이터의 규칙과 삭제 시 행동"을 정의
    • 어드민(admin.py)에는 "누가 이 기능을 쓸 수 있는지"만 명시
      • Django Admin이라는 엔진이 이 두 정보를 읽어서
      • "아, 이 유저는 권한이 있으니 폼을 보여주고, 저장할 때는 이 규칙을 검사하고,
      • 삭제할 때는 저 정책을 따라야겠구나"라고 스스로 판단해서 로직을 실행

결과

  • 대분류 / 중분류 / 소분류 의 카테고리를 각자 추가 가능
  • 중분류 / 소분류는 각각 부모 카테고리 선택 가능
  • 대분류는 부모 카테고리 선택 불가

  • 상위 카테고리 삭제 시 하위 카테고리 전부 삭제(ex. 대분류 삭제시 중분류 / 소분류도 같이 삭제 )

2025.12.28

카테고리 목록 조회 / 카테고리 삭제 구현


카테고리 목록 조회 구현 🔴

  • 카테고리 목록 조회

    • 스태프 / 관리자 권한을 가진 유저는 어드민 페이지 내의 질의응답 관리 메뉴의
    • 카테고리 관리 메뉴에 접속하여 시스템에 등록된 질의응답 카테고리들을 목록으로 조회 가능
    • 카테고리 목록 조회 시 검색필터 + 검색 기능을 활용하여 조회 가능
      • 카테고리 목록 조회에서 확인 가능한 항목

        • 카테고리 ID / 카테고리명 / 카테고리 분류 타입 ( 대, 중, 소 )
        • 자식 카테고리
          • 카테고리 분류가 대, 중일 경우 자식 카테고리의 명칭
          • 소분류일 경우 빈칸
        • 부모 카테고리
          • 카테고리 분류가 중, 소일 경우 부모 카테고리의 명칭
          • 대분류일 경우 빈칸
        • 등록일시
        • 수정일시

조회 구현

from django.contrib import admin
from apps.qna.models.question.question_category import QuestionCategory
from apps.user.models.user import RoleChoices



@admin.register(QuestionCategory)
class QuestionCategoryAdmin(admin.ModelAdmin):
    # 1. [목록 조회 시 보여줄 항목]
    list_display = (
        'id',
        'name',
        'get_type_display_custom',
        'get_children_names',
        'get_parent_name',
        'created_at',
        'updated_at'
    )

    # 2. [검색 및 필터 설정]
    list_filter = ('type', 'created_at')
    search_fields = ('name', 'parent__name')
    ordering = ('type', 'parent', 'id')

    # 3. [성능 최적화]
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        return queryset.select_related('parent').prefetch_related('children')

    # --- [커스텀 컬럼 메서드] ---
    @admin.display(description='분류 타입')
    def get_type_display_custom(self, obj):
        return obj.get_type_display()

    @admin.display(description='자식 카테고리')
    def get_children_names(self, obj):
        if obj.type == 'small':
            return "-"

        children = obj.children.all()
        if children:
            return ", ".join([child.name for child in children])
        return "-"

    @admin.display(description='부모 카테고리')
    def get_parent_name(self, obj):
        if obj.type == 'large':
            return "-"

        if obj.parent:
            return f"{obj.parent.name} ({obj.parent.get_type_display()})"
        return "-"

    # --- [권한 설정] ---
    def _has_common_permission(self, request):
        """
        [공통 권한 체크]
        - ST(학생), USER(일반) 제외
        - 어드민 접속 권한(is_staff)이 있는 운영진(AD, OM, LC, TA)만 허용
        """
        if not request.user.is_authenticated:
            return False

        excluded_roles = [RoleChoices.ST, RoleChoices.USER]
        return request.user.role not in excluded_roles and request.user.is_staff

    # 통합된 권한 함수 적용
    def has_add_permission(self, request):
        return self._has_common_permission(request)

    def has_change_permission(self, request, obj=None):
        return self._has_common_permission(request)

    def has_delete_permission(self, request, obj=None):
        return self._has_common_permission(request)

    def has_view_permission(self, request, obj=None):
        return self._has_common_permission(request)

결과

  • 모델에 없는 컬럼은 @admin.display를 이용하여 표현
    • 대분류의 경우 부모 카테고리를 - / 소분류의 경우 자식 카테고리를 -

  • 검색 기능 + 카테고리 필터까지 작동 확인 완료

카테고리 삭제

  • 카테고리 삭제

    • 스태프 / 관리자 권한을 가진 유저는 어드민 페이지 내의 질의응답 관리 메뉴의
    • 카테고리 관리 메뉴에 접속하여 시스템에 등록된 질의응답 카테고리를 삭제 가능
    • 조회된 카테고리 목록에서 삭제할 카테고리를 선택하고 삭제하기 버튼을 클릭하여 삭제
    • 카테고리 삭제 전, 대, 중, 소분류에 따라 경고 메시지 팝업이 나옴
      • 대분류의 경우

        • 해당 카테고리에 속한 중분류, 소분류 카테고리가 함께 삭제
        • 각 카테고리에 속한 질의응답은 일반질문 카테고리로 전환 / 삭제된 항목 복구 불가
      • 중분류의 경우

        • 해당 카테고리에 속한 소분류 카테고리가 함께 삭제
        • 각 카테고리에 속한 질의응답은 일반질문 카테고리로 전환 / 삭제된 항목 복구 불가
      • 소분류의 경우

        • 해당 카테고리에 속한 질의응답은 일반질문 카테고리로 전환 / 삭제된 항목 복구 불가

삭제 구현

from django.contrib import admin
from django.utils.html import format_html
from apps.qna.models.question.question_category import QuestionCategory
from apps.user.models.user import RoleChoices


@admin.register(QuestionCategory)
class QuestionCategoryAdmin(admin.ModelAdmin):
    # 1. [목록 조회 시 보여줄 항목]
    list_display = (
        'id',
        'name',
        'get_type_display_custom',
        'get_children_names',
        'get_parent_name',
        'created_at',
        'updated_at'
    )

    # 2. [검색 및 필터 설정]
    list_filter = ('type', 'created_at')
    search_fields = ('name', 'parent__name')
    ordering = ('type', 'parent', 'id')

    # 3. [성능 최적화]
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        return queryset.select_related('parent').prefetch_related('children')

    # --- [권한 설정] ---

    def _has_common_permission(self, request):
        """
        [공통 권한 체크]
        - ST(학생), USER(일반) 제외
        - 어드민 접속 권한(is_staff)이 있는 운영진(AD, OM, LC, TA)만 허용
        """
        if not request.user.is_authenticated:
            return False

        excluded_roles = [RoleChoices.ST, RoleChoices.USER]
        return request.user.role not in excluded_roles and request.user.is_staff

    # 통합된 권한 함수 적용
    def has_add_permission(self, request):
        return self._has_common_permission(request)

    def has_change_permission(self, request, obj=None):
        return self._has_common_permission(request)

    def has_delete_permission(self, request, obj=None):
        return self._has_common_permission(request)

    def has_view_permission(self, request, obj=None):
        return self._has_common_permission(request)

    # --- [삭제 시 경고 메시지 출력] ---
    def delete_view(self, request, object_id, extra_context=None):
        obj = self.get_object(request, object_id)
        extra_context = extra_context or {}

        if obj:
            warning_msg = ""
            base_msg = " 해당 카테고리의 질의응답은 '일반질문'으로 자동 전환되며, 삭제된 카테고리는 복구할 수 없습니다."

            if obj.type == 'large':
                warning_msg = f"⚠️ [대분류 삭제 경고] 하위 '중분류' 및 '소분류'가 모두 함께 삭제됩니다!{base_msg}"
            elif obj.type == 'medium':
                warning_msg = f"⚠️ [중분류 삭제 경고] 하위 '소분류'가 모두 함께 삭제됩니다!{base_msg}"
            else:  # small
                warning_msg = f"⚠️ [소분류 삭제 경고]{base_msg}"

            extra_context['title'] = warning_msg

        return super().delete_view(request, object_id, extra_context=extra_context)

    # --- [커스텀 컬럼 메서드: 색상 적용 ] ---
    @admin.display(description='분류 타입')
    def get_type_display_custom(self, obj):
        # 1. 타입별 색상 매핑 (Bootstrap 색상 참고)
        color_map = {
            'large': '28a745',  # Green (대분류)
            'medium': '17a2b8',  # Cyan (중분류)
            'small': '6c757d',  # Grey (소분류)
        }
        color = color_map.get(obj.type, '333')  # 기본값: 검정

        # 2. format_html로 안전하게 뱃지 생성
        return format_html(
            '<span style="color: white; background-color: #{}; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{}</span>',
            color,
            obj.get_type_display()
        )

    @admin.display(description='자식 카테고리')
    def get_children_names(self, obj):
        if obj.type == 'small': return "-"
        children = obj.children.all()
        return ", ".join([child.name for child in children]) if children else "-"

    @admin.display(description='부모 카테고리')
    def get_parent_name(self, obj):
        if obj.type == 'large': return "-"
        return f"{obj.parent.name} ({obj.parent.get_type_display()})" if obj.parent else "-"

format_html

  • Django에서 HTML 코드를 Python 문자열로 만들 때, 보안 사고(XSS 공격)를 막기 위해 사용하는 안전한 함수
  • 사용 이유

    • 일반적으로 Python에서 문자열을 합칠 때 f-string을 많이 사용하는데
      • 웹 개발에서 HTML 태그를 직접 만들 때 f-string을 쓰면 위험할 수 있다.
# --- [위험 예시(f-string)] ---
user_input = "<script>해킹스크립트()</script>"
html = f"<b>{user_input}</b>" 
# 결과: <b><script>해킹스크립트()</script></b> 
# -> 브라우저가 이 스크립트를 실제로 실행 (XSS 공격)

# --- [안전 예시(format_html)] ---
from django.utils.html import format_html

user_input = "<script>해킹스크립트()</script>"
html = format_html("<b>{}</b>", user_input)
# 결과: <b>&lt;script&gt;해킹스크립트()&lt;/script&gt;</b>
# -> <b> 태그는 살아있지만, 입력값의 < > 특수문자는 문자로 변환(Escape)되어 안전하게 출력
  • 사용처

    • Django Admin 페이지에서 많이 사용
      • 단순히 글로 표현하지 않고, 색깔이 들어간 뱃지, 이미지 썸네일, 링크 버튼 등을 코드로 그려줌
from django.utils.html import format_html

def type_badge(self, obj):
    # '대분류' 글자에 초록색 배경을 입히는 HTML을 안전하게 생성
    return format_html(
        '<span style="background-color: green; color: white;">{}</span>',
        obj.get_type_display()
    )
  • 적용 예시

# 기존 코드 수정
@admin.display(description='분류 타입')
def get_type_display_custom(self, obj):
    # 1. 타입별 색상 지정
    color_map = {
        'large': '28a745',  # 초록색 (대분류)
        'medium': '17a2b8', # 파란색 (중분류)
        'small': '6c757d',  # 회색 (소분류)
    }
    color = color_map.get(obj.type, '333') # 기본값 검정
    
    # 2. format_html을 사용하여 안전하게 HTML 생성
    return format_html(
        '<span style="color: white; background-color: #{}; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{}</span>',
        color,
        obj.get_type_display()
    )
  • 대분류
    • <span style="color: green; font-weight:bold">[대분류]</span> 뱃지가 초록색으로 표시됨
  • 중분류
    • <span style="color: blue; font-weight:bold">[중분류]</span> 뱃지가 파란색으로 표시됨
  • 소분류
    • <span style="color: gray; font-weight:bold">[소분류]</span> 뱃지가 회색으로 표시됨

새롭게 알게된 내용 ✅

is_staff=True

  • Django Admin에 접속하려면 is_staff=True가 필수

어드민 권한

  • 일반적으로 Django Admin의 권한 제어는 admin.py에서 처리하는 것이 기본이자 가장 흔한 방식
  • admin.py에서 전부 처리하는 이유

    • Django Admin은 그 자체로 하나의 독립된 "관리 도구"
    • 따라서 해당 모델을 관리 화면에서 어떻게 보여주고,
      • 누가 제어할지를 한곳(admin.py)에 모아두는 것이 응집도(Cohesion) 측면에서 유리하기 때문
  • 분리하는 경우

    • 권한 로직이 매우 복잡해지거나,
      • Admin뿐만 아니라 다른 Service 레이어에서도 동일한 권한 체크 로직을 공유해야 한다면 별도로 분리
    • apps/qna/permissions/admin_permissions.py 같은 파일을 만들어 로직을 짠 뒤
      • admin.py에서 불러와 호출만 하는 방식

카테고리DB 저장 됬는지 확인 법

  • python manage.py shell

from apps.qna.models import QuestionCategory
print(QuestionCategory.objects.all())  # 저장된 모든 카테고리 출력

연쇄 삭제(Cascade)

  • parent 필드에 models.CASCADE를 설정하면
    • 대분류 삭제 시 하위 카테고리들도 함께 삭제되는 로직을 자동으로 수행

on_delete=models.PROTECT

  • 현재 질문 모델의 카테고리 컬럼은 이렇게 세팅되어 있다
    • 하지만 이렇게 진행하면
      • PROTECT는 하위 데이터(질문)가 존재할 경우 상위 데이터(카테고리)의 삭제를 아예 차단 해버림
      • 이 카테고리에 질문이 단 하나라도 연결되어 있다면, 관리자 페이지에서 해당 카테고리를 삭제하려고
        • 할 때 ProtectedError가 발생하며 삭제가 거부됨

on_delete=SET_DEFAULT

  • 하지만 지금 원하는 조건은
    • 카테고리가 삭제되더라도 질문(Question) 데이터는 삭제되지 않고 '일반질문' 카테고리로 이동해야 함
  • 카테고리를 삭제하면 연결된 질문들의 카테고리 칸이 자동으로 '일반질문'으로 변경
    • "질의응답은 일반질문 카테고리로 전환"되어야 한다는 요구사항을 코드 수준에서 자동으로 처리

함수 내부 임포트

  • 순환 참조 방지

    • get_default_category 함수 내부에서 QuestionCategory를 임포트할 때,
    • 파일 상단이 아닌 함수 내부에서 임포트(Local Import)하여 모델 간의 순환 참조 에러를 방지해야 함

오늘 발생한 문제(발생 했다면) ✅

profile
안녕하세요.

0개의 댓글