1️⃣ 왜 이 기능이 필요한가?
우리가 만든 서비스는 강의를 신청하는 시스템이다.
그렇다면 사용자는 자연스럽게 이런 생각을 한다:
- “내가 어떤 강의를 신청했지?”
- “신청 상태는 뭐지?”
- “다시 확인하고 싶은데 어디서 보지?”
👉 그래서 필요한 기능이 바로
“내 신청 내역 (마이페이지)” 이다.
2️⃣ 전체 흐름 먼저 이해하기 (중요🔥)
이 기능은 단순히 DB 조회가 아니라 전체 흐름을 이해하는 게 핵심이다.
[사용자]
↓ 클릭
"내 강의장" 버튼
↓
/me 페이지 이동
↓
JS가 API 호출
(GET /api/me/enrollments)
↓
Controller
↓
Service
↓
Repository (DB 조회)
↓
DTO 변환
↓
JSON 응답
↓
화면에 카드로 렌더링
👉 초보자 기준 핵심 포인트:
- 화면(HTML) + API + DB가 연결된 기능이다
3️⃣ DB 구조 이해하기
우리는 이미 이런 구조를 가지고 있다.
📌 Enrollment (신청 테이블)
- userId (누가)
- lecture (어떤 강의)
- createdAt (언제 신청)
👉 즉
👉 “userId 기준으로 조회하면 = 내 신청 목록”
이걸 이용하면 된다.
4️⃣ Repository에서 조회 기능 만들기
📌 위치: EnrollmentRepository
@Query("select e from Enrollment e join fetch e.lecture where e.userId = :userId order by e.createdAt desc")
List<Enrollment> findByUserIdWithLecture(@Param("userId") Long userId);
🔥 왜 join fetch를 쓰는가?
초보자들이 가장 많이 놓치는 부분이다.
❌ 그냥 조회하면
- Enrollment 가져옴
- Lecture는 나중에 또 쿼리 발생 (N+1 문제)
✅ join fetch 사용하면
- 한 번에 다 가져옴 (성능 GOOD)
5️⃣ DTO 만들기
왜 DTO를 만들까?
👉 Entity를 그대로 보내면 위험하고 불필요한 데이터도 포함됨
그래서 필요한 데이터만 담는다.
@Getter
@Builder
@AllArgsConstructor
public class MyEnrollmentResponse {
private Long lectureId;
private String lectureTitle;
private int capacity;
private int enrolledCount;
private String status;
public static MyEnrollmentResponse from(Enrollment e) {
Lecture l = e.getLecture();
return MyEnrollmentResponse.builder()
.lectureId(l.getId())
.lectureTitle(l.getTitle())
.capacity(l.getCapacity())
.enrolledCount(l.getEnrolledCount())
.status(l.getStatus().name())
.build();
}
}
💡 핵심 포인트
👉 DTO는 “프론트에 보여줄 데이터만” 담는다
6️⃣ Service 구현
📌 위치: MyPageService
@Service
@RequiredArgsConstructor
public class MyPageService {
private final EnrollmentRepository enrollmentRepository;
@Transactional(readOnly = true)
public List<MyEnrollmentResponse> getMyEnrollments(Long userId) {
return enrollmentRepository.findByUserIdWithLecture(userId)
.stream()
.map(MyEnrollmentResponse::from)
.toList();
}
}
💡 초보자 포인트
- readOnly = true → 조회 성능 최적화
- stream().map() → DTO 변환
7️⃣ Controller 구현
📌 API: GET /api/me/enrollments
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/me")
public class MyPageApiController {
private final MyPageService myPageService;
private Long currentUserId() {
CustomPrincipal principal = (CustomPrincipal) SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
return principal.getUserId();
}
@GetMapping("/enrollments")
public ResponseEntity<List<MyEnrollmentResponse>> myEnrollments() {
Long userId = currentUserId();
return ResponseEntity.ok(myPageService.getMyEnrollments(userId));
}
}
🔥 핵심 포인트
👉 SecurityContextHolder에서 userId 꺼내는 구조
이게 JWT 기반 인증의 핵심이다.
8️⃣ View (페이지) 연결 및 lobby.html 수정
📌 /me 페이지를 만들어야 한다
@GetMapping("/me")
public String me() {
return "lecture/me";
}
<a sec:authorize="isAuthenticated()"
href="/me"
class="bg-slate-900 text-white px-5 py-2.5 rounded-xl">
내 강의장
</a>
💡 초보자 포인트
- sec:authorize="isAuthenticated()"
- 👉 로그인한 사람만 보이게 하는 기능
9️⃣ 프론트에서 API 호출
const res = await fetch("/api/me/enrollments", {
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token")
}
});
💡 핵심
👉 JWT는 반드시 헤더에 넣어야 한다
🔟 결과 화면
- 강의 제목
- 정원
- 상태
- 신청일
👉 사용자 입장에서 “내 강의 목록” 완성
'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
| ⏰💡 관리자 기능 + UI (JWT Role기반) (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 |