1️⃣ 왜 이 기능을 만들었을까?
선착순 강의 신청 시스템을 만들면서 가장 중요한 포인트는 하나였다.
👉 “정각에만 신청 가능해야 한다”
예를 들어:
- 강의 오픈: 10:00
- 그런데 9:59에 신청 가능?
👉 이건 선착순이 아니라 그냥 아무 때나 신청 가능한 시스템이다.
그래서 우리는 반드시:
- ❌ 오픈 전 → 신청 불가
- ✅ 오픈 후 → 신청 가능
이걸 정확하게 구현해야 했다.
2️⃣ 전체 구조 먼저 이해하기
내 프로젝트 흐름은 이렇게 되어 있다:
- lobby.html → 강의 목록
- detail.html → 신청 버튼 (대기열 등록)
- /api/lectures/{id}/queue → 대기열 등록 API
- apply.html → 입장권 받은 사람만 결제
- /api/lectures/{id}/enroll → 최종 신청
👉 여기서 핵심은:
“신청 버튼” = 대기열 등록(queue)
즉, 큐 등록 API를 막는 게 핵심이다.
3️⃣ 백엔드 먼저 막기 (중요)
프론트만 막으면 의미 없다.
개발자 도구로 버튼을 다시 활성화할 수 있기 때문이다.
그래서 서버에서 먼저 막았다.
3-1. Queue API에서 openAt 검사
// LectureQueueApiController 또는 QueueService 내부
Lecture lecture = lectureRepository.findById(lectureId)
.orElseThrow(() -> new IllegalArgumentException("Lecture not found"));
if (lecture.getOpenAt().isAfter(LocalDateTime.now())) {
throw new IllegalArgumentException("아직 오픈 전입니다.");
}
✔ 설명
- lecture.getOpenAt() → 강의 오픈 시간
- now()보다 미래면 → 아직 오픈 안됨
- 이 경우 API 자체를 막는다
3-2. 결과
이제:
- 개발자 도구로 버튼 활성화해도 ❌
- 직접 API 호출해도 ❌
👉 완전히 차단됨
4️⃣ 프론트(detail.html)에서 UX 개선
백엔드만 막으면 사용자 입장에서는 이런 느낌이다:
“왜 안돼?? 오류났나??”
그래서 UX를 개선했다.
4-1. 목표
- 버튼 비활성화
- “오픈까지 남은 시간” 표시
- 시간이 되면 자동 활성화
4-2. openAt을 JS로 넘기기
<body th:attr="data-open-at=${#temporals.format(lecture.openAt, 'yyyy-MM-dd''T''HH:mm:ss')}">
👉 JS에서 쉽게 쓰려고 ISO 형태로 넘김
4-3. 카운트다운 UI 추가
<p id="countdownText" class="text-xs text-gray-500 mt-2"></p>
4-4. 핵심 JS 코드
<script>
const openAtIso = document.body.dataset.openAt;
const applyBtn = document.getElementById("applyBtn");
const countdownText = document.getElementById("countdownText");
let timer = null;
function tick() {
const openAtMs = new Date(openAtIso).getTime();
const diff = openAtMs - Date.now();
// ⛔ 오픈 전
if (diff > 0) {
applyBtn.disabled = true;
applyBtn.textContent = "오픈 예정";
const s = Math.floor(diff / 1000);
const hh = String(Math.floor(s / 3600)).padStart(2, "0");
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
countdownText.textContent = `오픈까지 ${hh}:${mm}:${ss}`;
return;
}
// ✅ 오픈됨
clearInterval(timer);
applyBtn.disabled = false;
applyBtn.textContent = "신청하기";
countdownText.textContent = "신청 가능합니다!";
}
window.addEventListener("load", () => {
tick();
timer = setInterval(tick, 1000);
});
</script>
5️⃣ 왜 여러 파일을 수정해야 했을까?
이 기능 하나 때문에 내가 수정한 파일은 생각보다 많았다.
📌 수정된 주요 파일
1) Lecture (엔티티)
- openAt 필드 사용
- 시간 기준 로직의 기준 데이터
2) QueueService / Queue API
- openAt 검사 추가
- 오픈 전 요청 차단
3) detail.html
- 버튼 disabled 처리
- 카운트다운 UI
- openAt을 JS로 전달
4) GlobalExceptionHandler
- "아직 오픈 전입니다." 메시지 처리
👉 즉, 이 기능은 단순 UI가 아니라:
도메인 + API + UI가 모두 연결된 기능
6️⃣ 최종 결과 (UX 변화)
❌ 구현 전
- 아무 때나 신청 가능
- 선착순 의미 없음
✅ 구현 후
- “오픈까지 00:03:12” 표시
- 버튼 비활성화
- 정각에 자동 활성화
- 서버에서도 완벽 차단
👉 진짜 서비스 같은 느낌 완성
7️⃣ 한 줄 정리
👉 선착순 시스템은 “시간 통제”가 핵심이다. 프론트는 UX, 백엔드는 보안을 담당한다. 둘 다 반드시 구현해야 한다.
'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
| ⏰💡 마이페이지 기능 구현 (0) | 2026.03.22 |
|---|---|
| ⏰💡 관리자 기능 + UI (JWT Role기반) (0) | 2026.03.22 |
| ⏰🚨 “입장권이 있는데도 apply 페이지에서 입장권 없음 오류 발생” (0) | 2026.03.06 |
| ⏰🚨 선착순 강의 신청에서 “이미 신청했는데 다시 신청 가능?” 문제 해결 (0) | 2026.03.06 |
| ⏰💡 선착순 강의 Redis 대기열(ZSET) + 입장권 원자화 + 소비(consume) 적용하기 (Lua 1개로 끝) (0) | 2026.02.26 |