⏰💡 관리자 기능 + UI (JWT Role기반)

2026. 3. 22. 16:31·프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트

1️⃣ 내가 만들고 싶은 최종 결과

사용자는 보통 /lectures(로비)에서 시작해요.

  • 누구나 /lectures에서 강의 목록을 구경할 수 있음
  • 로그인하면:
    • 일반 USER는 “내 강의장” 버튼이 보임(나중에 마이페이지용)
    • ADMIN은 “내 강의장” 대신 “관리자 페이지” 버튼이 보임 → /admin/lectures로 이동
  • /admin/lectures에서는 운영자가
    • 강의 생성
    • 강의 목록 확인
    • 강의 마감 처리(CLOSED)
    • 신청자 목록 조회
    • 를 할 수 있음

그리고 가장 중요한 것!

✅ 버튼을 숨긴다고 보안이 되는 건 아니고, 서버에서 /api/admin/**를 ADMIN만 통과시키는 것이 진짜 보안이에요.


2️⃣ 현재 프로젝트 구조 (핵심만)

  • config/
    • WebOAuthSecurityConfig (보안 설정)
    • TokenProvider, TokenAuthenticationFilter (JWT 인증)
  • controller/
    • 기존 lecture 관련 API 컨트롤러들
    • (추가) Admin용 ViewController, Admin API Controller
  • domain/
    • User, Lecture, Enrollment, LectureStatus
  • dto/
    • LectureResponse
    • (추가) Admin용 요청/응답 DTO
  • repository/
    • LectureRepository, EnrollmentRepository 등
  • templates/
    • lecture/lobby.html (로비)
    • (추가) admin/lectures.html (관리자 화면)

3️⃣ Role(권한) 설계: USER vs ADMIN

나는 소셜 로그인(OAuth2)로 로그인하고, 서버는 로그인 성공 시 access_token(JWT)을 발급해요.

JWT payload에 아래처럼 role이 들어가도록 구성했어요.

{
  "id": 2,
  "sub": "admin@gmail.com",
  "role": "ADMIN"
}

DB에는 user가 이렇게 저장되어 있어요.

1 ... USER
2 ... ADMIN

즉, 누가 ADMIN인지의 기준은 DB의 User.role 이고,

JWT를 만들 때 DB role 값을 claim으로 넣어서 프론트/서버에서 활용하는 구조예요.


4️⃣ 서버 보안: /api/admin/** 는 ADMIN만 접근 가능해야 함

여기서 핵심은 “관리자 기능은 결국 API로 동작한다”는 점이에요.

  • 관리자가 화면에서 “강의 생성” 버튼을 눌러도
  • 내부적으로는 /api/admin/lectures 같은 API 호출이 일어남

그래서 보안은 무조건 서버에서 걸어야 합니다.

4-1) TokenProvider에서 role을 Authentication 권한으로 넣기

이미 너는 JWT에 role claim을 넣었고, 인증 필터가 TokenProvider.getAuthentication()을 사용하고 있어요.

여기서 중요한 것:

  • Spring Security 권한은 보통 "ROLE_ADMIN", "ROLE_USER" 이런 형태로 처리해요.
  • hasRole("ADMIN") 은 내부적으로 "ROLE_ADMIN"을 찾습니다.

그래서 JWT에서 role을 읽어서 authorities에 ROLE_ADMIN or ROLE_USER 로 넣어줘야 합니다.


5️⃣ 관리자 API MVP 3개 만들기

“운영 가능한 서비스” 느낌이 나는 최소 기능 3개는 아래였어요.

  1. 강의 생성
  2. 강의 마감(OPEN → CLOSED)
  3. 신청자 목록 조회

5-1) 강의 생성 API

  • POST /api/admin/lectures
  • Body(JSON)
{
  "title":"관리자 테스트 강의",
  "openAt":"2026-02-28T10:00:00",
  "capacity":30,
  "status":"OPEN"
}

✅ 여기서 자주 나오는 에러: 400 Bad Request

대부분 날짜(openAt) 형식 문제예요.

  • LocalDateTime이면 "2026-02-28T10:00:00" 형태가 안전
  • "2026.02.28 10:00" 처럼 보내면 JSON 파싱이 실패할 수 있어요.

6️⃣ 관리자 화면(/admin/lectures)을 Thymeleaf로 만든 이유

“관리자 화면도 API로 만들면 되지 않나요?”

가능은 해요.

그런데 포트폴리오 관점에서 “서비스 운영까지 고려했다”는 느낌을 주려면

  • 관리자 화면(Thymeleaf or React 등)
  • 운영 버튼/폼
  • 이 있으면 훨씬 임팩트가 커요.

또한 우리 프로젝트는 이미 Thymeleaf 기반이라서 템플릿 하나 추가하는 게 가장 빠름.


7️⃣ /admin/lectures 화면 구성 (최소 버전)

화면에는 크게 2개 섹션만 있어도 충분해요.

7-1) 강의 생성 폼

  • 제목(title)
  • 오픈시간(openAt)
  • 정원(capacity)
  • 상태(status)

입력 후 “생성하기” 버튼을 누르면 POST /api/admin/lectures 호출

7-2) 강의 목록 테이블

각 row에 버튼 2개:

  • “마감” 버튼 → PATCH /api/admin/lectures/{id}/close
  • “신청자 보기” → GET /api/admin/lectures/{id}/enrollments

7-3) 신청자 목록은 모달로 보여주면 더 서비스 같음

신청자 보기 클릭하면 모달 오픈 → API 호출 → 리스트 렌더링


8️⃣ 로비(/lectures)에서 ADMIN이면 “관리자 페이지” 버튼을 보여주기

문제: /lectures 로비는 사람들이 처음 접속하는 곳인데

관리자는 여기서 바로 운영 화면으로 들어가고 싶어요.

우리는 다음 UX로 만들었어요.

  • 로그인하면 기본은 “내 강의장”
  • ADMIN 토큰이면 “내 강의장” 버튼을 “관리자 페이지” 버튼으로 교체

8-1) lobby.html에서 버튼 2개 준비하기

둘 다 로그인한 경우에만 보일 수 있게 하고,

admin 버튼은 기본 hidden.

<a id="myLectureBtn"
   sec:authorize="isAuthenticated()"
   href="#"
   class="bg-slate-900 text-white px-5 py-2.5 rounded-xl">
  내 강의장
</a>

<a id="adminBtn"
   sec:authorize="isAuthenticated()"
   href="/admin/lectures"
   class="hidden bg-blue-600 text-white px-5 py-2.5 rounded-xl">
  관리자 페이지
</a>

8-2) token.js로 JWT payload에서 role 읽어서 교체

token.js로 access_token을 가져온 뒤 payload를 디코딩해요.

(function swapMyLectureToAdminIfNeeded() {
  const myBtn = document.getElementById('myLectureBtn');
  const adminBtn = document.getElementById('adminBtn');

  if (!myBtn || !adminBtn) return;
  if (!window.Token || !Token.getToken()) return;

  try {
    const token = Token.getToken();
    const payload = token.split('.')[1];

    // base64url 디코딩
    const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
    const json = JSON.parse(decodeURIComponent(escape(atob(base64))));

    if (json.role === 'ADMIN') {
      myBtn.classList.add('hidden');
      adminBtn.classList.remove('hidden');
    } else {
      adminBtn.classList.add('hidden');
      myBtn.classList.remove('hidden');
    }
  } catch (e) {
    // 디코딩 실패하면 기본(내 강의장) 유지
  }
})();

✅ 이렇게 하면 같은 로비 화면이라도

ADMIN은 “관리자 페이지” 버튼이 뜨고,

USER는 “내 강의장” 버튼이 유지됩니다.


9️⃣ “버튼 숨기기”는 UX일 뿐, 진짜 보안은 서버에서

중요한 포인트를 꼭 짚고 갈게요.

  • 로비에서 버튼을 숨겨도
  • USER가 직접 /admin/lectures를 주소창에 치면 들어올 수 있어요.

그래서 진짜 보안은:

✅ /api/admin/**에 hasRole("ADMIN") 적용

→ USER는 API 호출 자체가 403으로 막히기 때문에 데이터/기능이 실행되지 않음

즉, 화면은 열려도 핵심 데이터 API가 막혀있으면 운영 기능은 안전합니다.

(원하면 /admin/** 페이지 자체도 role로 막는 개선도 가능)


🔟 테스트 방법

10-1) USER 계정으로 로그인

  • /lectures에서 “내 강의장”만 보임
  • /admin/lectures 들어가도 목록이 안 뜨거나, 기능 호출 시 403

10-2) ADMIN 계정으로 로그인

  • /lectures에서 “관리자 페이지” 버튼이 보임
  • /admin/lectures에서
    • 생성 성공
    • 마감 성공
    • 신청자 보기 성공

'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글

⏰💡 마이페이지 기능 구현  (0) 2026.03.22
⏰💡 오픈 시간(openAt) 전에는 신청 버튼 막기 + 카운트다운 표시  (0) 2026.03.07
⏰🚨 “입장권이 있는데도 apply 페이지에서 입장권 없음 오류 발생”  (0) 2026.03.06
⏰🚨 선착순 강의 신청에서 “이미 신청했는데 다시 신청 가능?” 문제 해결  (0) 2026.03.06
⏰💡 선착순 강의 Redis 대기열(ZSET) + 입장권 원자화 + 소비(consume) 적용하기 (Lua 1개로 끝)  (0) 2026.02.26
'프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
  • ⏰💡 마이페이지 기능 구현
  • ⏰💡 오픈 시간(openAt) 전에는 신청 버튼 막기 + 카운트다운 표시
  • ⏰🚨 “입장권이 있는데도 apply 페이지에서 입장권 없음 오류 발생”
  • ⏰🚨 선착순 강의 신청에서 “이미 신청했는데 다시 신청 가능?” 문제 해결
hak0622
hak0622
개발하면서 “이게 뭐지?”라는 순간마다 궁금한 점을 바탕으로 정리한 개발 블로그입니다.
  • hak0622
    궁금한 개발 이야기 Why?
    hak0622
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • 공부 (36)
        • 1. 자바 ORM 표준 JPA 프로그래밍 - 기본.. (35)
        • 시험 (1)
      • 프로젝트 (32)
        • 스프링 부트 3 백엔드 개발자 되기 Blog + .. (32)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hak0622
⏰💡 관리자 기능 + UI (JWT Role기반)
상단으로

티스토리툴바