⏰💡 오픈 시간(openAt) 전에는 신청 버튼 막기 + 카운트다운 표시

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

1️⃣ 왜 이 기능을 만들었을까?

선착순 강의 신청 시스템을 만들면서 가장 중요한 포인트는 하나였다.

👉 “정각에만 신청 가능해야 한다”

예를 들어:

  • 강의 오픈: 10:00
  • 그런데 9:59에 신청 가능?

👉 이건 선착순이 아니라 그냥 아무 때나 신청 가능한 시스템이다.

그래서 우리는 반드시:

  • ❌ 오픈 전 → 신청 불가
  • ✅ 오픈 후 → 신청 가능

이걸 정확하게 구현해야 했다.


2️⃣ 전체 구조 먼저 이해하기

내 프로젝트 흐름은 이렇게 되어 있다:

  1. lobby.html → 강의 목록
  2. detail.html → 신청 버튼 (대기열 등록)
  3. /api/lectures/{id}/queue → 대기열 등록 API
  4. apply.html → 입장권 받은 사람만 결제
  5. /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
'프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
  • ⏰💡 마이페이지 기능 구현
  • ⏰💡 관리자 기능 + UI (JWT Role기반)
  • ⏰🚨 “입장권이 있는데도 apply 페이지에서 입장권 없음 오류 발생”
  • ⏰🚨 선착순 강의 신청에서 “이미 신청했는데 다시 신청 가능?” 문제 해결
hak0622
hak0622
개발하면서 “이게 뭐지?”라는 순간마다 궁금한 점을 바탕으로 정리한 개발 블로그입니다.
  • hak0622
    궁금한 개발 이야기 Why?
    hak0622
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • 공부 (36)
        • 1. 자바 ORM 표준 JPA 프로그래밍 - 기본.. (35)
        • 시험 (1)
      • 프로젝트 (32)
        • 스프링 부트 3 백엔드 개발자 되기 Blog + .. (32)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hak0622
⏰💡 오픈 시간(openAt) 전에는 신청 버튼 막기 + 카운트다운 표시
상단으로

티스토리툴바