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개는 아래였어요.
- 강의 생성
- 강의 마감(OPEN → CLOSED)
- 신청자 목록 조회
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 |