2026/03/12 Blog - 22

김기훈·2026년 3월 12일

TIL

목록 보기
162/194
post-thumbnail

예비군 이슈로 오늘 하루는 빈약하게...


코딩테스트(15829)


기능 구현


네비바 등급

  • 작성자 닉네임 옆에 작게 등급 이미지 들어가면 좋을듯

구현방식 고민

  • 모든 페이지에서 공통으로 보이는 네비게이션 바(base.html)에 등급 이미지를 띄우려면
    • 내 등급을 알아야 함
    • 하지만 모든 페이지를 이동할 때마다 내 글 목록 API(/api/v1/post/my/)를 호출해서
      • 개수를 세는 것은 서버에 엄청난 부담(트래픽)
  • 구현방식

    • 사용자가 처음 로그인하고 메인 홈페이지(home.html)에 접속했을 때
      • 이미 잔디밭을 그리기 위해 내 글 개수를 가져옴
    • 이때 계산된 내 등급 이미지 URL을 브라우저의 localStorage에 저장
    • 모든 페이지에 공통으로 적용된 base.html에서는 서버에 API 요청을 보내는 대신
      • 빠르게 localStorage에서 이미지만 꺼내서 닉네임 옆에 보여줌

등급 이미지 저장하기

// 계산된 현재 등급의 이미지 URL을 로컬 스토리지에 저장
            // 이렇게 하면 다른 페이지로 이동해도 서버 요청 없이 등급 이미지를 유지 가능
            localStorage.setItem('user_grade_img', currentGrade.imgUrl);

            // 내 정보가 업데이트 되었으므로 base.html의 UI 렌더링 함수를 다시 호출해 즉시 반영
            if (typeof checkLoginStatus === 'function') {
                checkLoginStatus();
            }

닉네임 옆에 이미지 보여주기

<li class="nav-item me-3 d-flex align-items-center">
    <img id="nav-grade-img" src="" alt="grade" class="d-none me-2" style="width: 24px; height: 24px; object-fit: contain;">
    
    <span class="text-white opacity-75 small">안녕하세요, <strong id="user-nickname" class="text-white">User</strong></span>
</li>
<li class="nav-item me-3">
                            <span class="text-white opacity-75 small">안녕하세요, <strong id="user-nickname"
                                                                                     class="text-white">User</strong></span>
                        </li>
——————————————————————————————————————[비교]—————————————————————————————————————————
<li class="nav-item me-3 d-flex align-items-center">
    <img id="nav-grade-img" src="" alt="grade" class="d-none me-2" style="width: 24px; height: 24px; object-fit: contain;">
    
    <span class="text-white opacity-75 small">안녕하세요, <strong id="user-nickname" class="text-white">User</strong></span>
</li>
function checkLoginStatus() {
    // 로컬 스토리지에서 사용자의 접근 토큰(JWT)을 가져옵니다.
    const token = localStorage.getItem('access_token');
    
    // 로컬 스토리지에서 사용자의 닉네임을 가져옵니다.
    const nickname = localStorage.getItem('nickname');
    
    // ✨ [추가된 부분] 로컬 스토리지에서 home.html에서 저장해둔 등급 이미지 주소를 가져옵니다.
    const gradeImgUrl = localStorage.getItem('user_grade_img');

    // 비로그인 상태일 때 보여줄 메뉴 요소를 찾습니다.
    const guestMenu = document.getElementById('guest-menu');
    
    // 로그인 상태일 때 보여줄 메뉴 요소를 찾습니다.
    const userMenu = document.getElementById('user-menu');
    
    // 닉네임이 들어갈 HTML 요소를 찾습니다.
    const nicknameElement = document.getElementById('user-nickname');
    
    // ✨ [추가된 부분] 등급 이미지가 들어갈 HTML 요소를 찾습니다.
    const navGradeImg = document.getElementById('nav-grade-img');

    if (token) {
        // 로그인 상태라면 손님 메뉴를 숨기고 유저 메뉴를 보여줍니다.
        guestMenu.style.setProperty('display', 'none', 'important');
        userMenu.style.setProperty('display', 'flex', 'important');

        // 화면에 닉네임을 표시합니다.
        if(nickname && nicknameElement) {
            nicknameElement.innerText = nickname;
        }
        
        // ✨ [추가된 부분] 로컬 스토리지에 등급 이미지가 존재한다면 실행합니다.
        if(gradeImgUrl && navGradeImg) {
            // img 태그의 주소(src)를 내 등급 이미지 주소로 설정합니다.
            navGradeImg.src = gradeImgUrl;
            // 이미지가 화면에 보이도록 숨김 클래스(d-none)를 제거합니다.
            navGradeImg.classList.remove('d-none');
        }
    } else {
        // 비로그인 상태라면 손님 메뉴를 보여주고 유저 메뉴를 숨깁니다.
        guestMenu.style.setProperty('display', 'flex', 'important');
        userMenu.style.setProperty('display', 'none', 'important');
    }
}

function handleLogout() {
    // 로그아웃 시 확인 알림창을 띄웁니다.
    if(confirm("정말 로그아웃 하시겠습니까?")) {
        // 로그아웃 하므로 모든 인증 관련 데이터와 닉네임을 삭제합니다.
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        localStorage.removeItem('nickname'); 
        
        // ✨ [추가된 부분] 로그아웃 시 다른 계정으로 들어왔을 때 섞이지 않도록 등급 이미지 정보도 함께 삭제합니다.
        localStorage.removeItem('user_grade_img');

        // 삭제가 완료되면 메인 페이지로 이동합니다.
        alert("로그아웃 되었습니다.");
        window.location.href = "/";
    }
}

1차 완료


포스트 닉네임 등급

프론트

<div class="text-muted small">
                            by <span id="post-author" class="text-success fw-bold"></span> |
                            <span id="post-date"></span>
                            <span id="post-visibility-badge" class="ms-2"></span>
                        </div>
——————————————————————————————————————[비교]—————————————————————————————————————————
<div class="text-muted small d-flex align-items-center"> by 
    <img id="post-author-grade" src="" alt="grade" class="d-none ms-1 me-1" style="width: 20px; height: 20px; object-fit: contain;">
    <span id="post-author" class="text-success fw-bold"></span> |
    <span id="post-date" class="ms-1"></span>
    <span id="post-visibility-badge" class="ms-2"></span>
</div>

백엔드

from rest_framework import serializers
from apps.post.models import Post

GRADE_SETTINGS = [
    		...
]


class PostDetailSerializer(serializers.ModelSerializer):
	...
    author_grade_image = serializers.SerializerMethodField()
	...

    class Meta:
        model = Post
        fields = [
			...
            "author_grade_image",
			...
        ]

    # ✨ author_grade_image 필드의 값을 실제로 계산하는 메서드 (메서드명 규칙: get_ + 필드명)
    def get_author_grade_image(self, obj):
        # 1. 작성자(obj.user)가 지금까지 작성한 글 중에서 삭제되지 않은(deleted_at__isnull=True) 전체 글의 개수를 셉니다.
        total_count = Post.objects.filter(user=obj.user, deleted_at__isnull=True).count()

        # 2. GRADE_SETTINGS를 위에서부터 순회하며 개수(min) 조건을 충족하는 등급 이미지를 찾습니다.
        for grade in GRADE_SETTINGS:
            if total_count >= grade["min"]:
                return grade["imgUrl"]  # 조건을 만족하면 바로 해당 이미지 URL을 반환하고 종료합니다.

        # 3. 만약 매칭되는게 없다면 (혹시 모를 에러 방지용) 제일 기본 씨앗 이미지를 반환합니다.
        return GRADE_SETTINGS[-1]["imgUrl"]

    def get_is_liked(self, obj) -> bool:
        # 1. 뷰(View)에서 넘겨준 context 안에서 현재 요청(request) 객체를 가져옵니다.
        request = self.context.get("request")

        # 2. 요청 객체가 존재하고, 로그인된 사용자(is_authenticated)일 경우에만 검사합니다.
        if request and request.user.is_authenticated:
            # 3. 현재 게시글(obj)의 좋아요(likes) 목록 중에 현재 로그인한 유저가 있는지(exists) 확인하여 True/False를 반환합니다.
            return obj.likes.filter(user=request.user).exists()

        # 4. 로그인하지 않은 사용자라면 무조건 False(좋아요 안 누름)를 반환합니다.
        return False

  • 작성글 조회에서 작성자의 닉네임 옆에 등급이 나오도록 추가


profile
안녕하세요.

0개의 댓글