Skip to content
Open
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
db19123
chore: 초기 세팅 설정
htdufhc-bit Jun 29, 2026
b6ba2cb
refactor: 공통 예외 처리 구조 도입
htdufhc-bit Jun 29, 2026
5e7b687
feat: 로그인 및 인증 정보 조회 (1 & 2단계)
htdufhc-bit Jun 29, 2026
ec6ee6d
feat: 인증 정보 기반 예약 생성 (1 & 2단계)
htdufhc-bit Jun 29, 2026
b17debb
test: 1, 2단계 테스트 추가
htdufhc-bit Jun 29, 2026
6e1c475
chore: 미사용 import 제거 및 코드 포맷 정리
htdufhc-bit Jun 29, 2026
ba98c27
refactor: 로그아웃 기능을 auth 도메인으로 이동
htdufhc-bit Jun 29, 2026
2b95ab2
feat: 회원 예외 처리를 위한 MemberErrorCode 추가 및 적용
htdufhc-bit Jun 29, 2026
1c8cfde
fix: SQL 문법 오류 해결
htdufhc-bit Jun 29, 2026
ab7f70c
docs: README.md 추가
htdufhc-bit Jun 29, 2026
eae5e9a
refactor: 예외 로깅 방식을 printStackTrace에서 Logger로 변경
htdufhc-bit Jul 2, 2026
bf6ac7b
refactor: MalformedJwtException UnsupportedJwtException 예외 처리 추가
htdufhc-bit Jul 2, 2026
fd847c1
feat: 인증 필요를 가리키는 커스텀 어노테이션 추가
htdufhc-bit Jul 2, 2026
8acaa21
refactor: bean validation을 통해 검증하도록 수정
htdufhc-bit Jul 2, 2026
787f1a6
refactor: CookieUtil의 책임 분리
htdufhc-bit Jul 2, 2026
6d4cc45
fix: 파일 끝에 개행 문자 추가
htdufhc-bit Jul 2, 2026
219413c
refactor: 사용하지 않는 import문 제거
htdufhc-bit Jul 2, 2026
5d94f7e
refactor: 예약자 name 검증을 DTO에서 service로 이동
htdufhc-bit Jul 2, 2026
7daa481
refactor: 도메인별 패키지를 계층 구조로 재구성
htdufhc-bit Jul 3, 2026
1e3fdb3
refactor: 도메인별 패키지를 계층 구조로 추가 재구성
htdufhc-bit Jul 6, 2026
aac8aa3
refactor: Member 권한(Role)을 String에서 enum으로 변경
htdufhc-bit Jul 6, 2026
4093c0b
feat: 권한 검증을 위한 인가 Interceptor 추가
htdufhc-bit Jul 6, 2026
557971d
test: 3단계 테스트 추가
htdufhc-bit Jul 6, 2026
1d58693
test: import문 재정렬
htdufhc-bit Jul 6, 2026
7c289c3
Merge remote-tracking branch 'upstream/htdufhc-bit' into roomescape-M…
htdufhc-bit Jul 6, 2026
a5fa426
style: 불필요한 개행 문자 제거
htdufhc-bit Jul 6, 2026
50a30f4
refactor: JwtTokenProvider에 name/role 추출 기능 추가 및 파싱 로직 통합
htdufhc-bit Jul 6, 2026
31bb114
refactor: LoginMember에서 email 필드 제거, JWT 기반으로 인증 정보 생성
htdufhc-bit Jul 6, 2026
4975da3
docs: README에 패키지 구조 개편과 관리자 권한 기능 반영
htdufhc-bit Jul 7, 2026
5bc54ca
fix: 회원 저장 시 Role이 ADMIN으로 고정되는 문제 수정
htdufhc-bit Jul 8, 2026
6eb6095
refactor: 로그인 실패 시 EmptyResultDataAccessException 대신 401 응답 반환
htdufhc-bit Jul 8, 2026
b400cf2
refactor: 토큰 추출 공통 로직을 TokenExtractor로 분리
htdufhc-bit Jul 8, 2026
753f82a
feat: refresh token 발급·저장 환경 추가
htdufhc-bit Jul 9, 2026
dded032
refactor: CookieUtil을 access/refresh 쿠키 지원 구조로 개선
htdufhc-bit Jul 9, 2026
5e0c8a7
feat: refresh token 기반 재발급 구현
htdufhc-bit Jul 9, 2026
ccb028c
test: createToken 메서드가 access token을 추출하도록 수정
htdufhc-bit Jul 9, 2026
e1b65c3
build: Spring Data JPA 도입
htdufhc-bit Jul 12, 2026
ce5ad9a
refactor: DAO를 JPA Repository로 전환하고 엔티티/DTO 재구성
htdufhc-bit Jul 12, 2026
0a3086e
fix: 쿠키 만료 시 path 불일치 문제 해결
htdufhc-bit Jul 12, 2026
0a7c180
refactor: refresh token을 EntityManager 기반 JPA로 전환
htdufhc-bit Jul 12, 2026
da760e9
refactor: 서비스 메서드에 Transactional 어노테이션 적용
htdufhc-bit Jul 12, 2026
b325fc5
refactor: 토큰 쿠키 설정 로직을 setTokenCookies 메서드로 추출
htdufhc-bit Jul 12, 2026
3c1f8f2
fix: 쓰기 메서드에 Transactional 어노테이션 추가
htdufhc-bit Jul 12, 2026
4108114
refactor: 엔티티의 사용되지 않는 생성자 삭제
htdufhc-bit Jul 12, 2026
f7b7531
refactor: RefreshTokenEntity.java를 RefreshToken.java로 수정
htdufhc-bit Jul 12, 2026
f16fde7
docs: README.md 업데이트
htdufhc-bit Jul 13, 2026
084d3eb
fix: 존재하지 않는 id 삭제 시 예외가 발생하는 문제 해결
htdufhc-bit Jul 13, 2026
4b53082
fix: 오타 수정 (예약` -> 예약)
htdufhc-bit Jul 13, 2026
1902301
Merge branch 'htdufhc-bit' into roomescape-JPA
htdufhc-bit Jul 14, 2026
6813feb
fix: spring.jpa.ddl-auto 오타 수정
htdufhc-bit Jul 15, 2026
fb8ea71
fix: ThemeService.create()에 @Transactional 추가
htdufhc-bit Jul 15, 2026
72a68ca
fix: refresh token 재발급 시 delete-save 쿼리 순서 문제 수정
htdufhc-bit Jul 15, 2026
0a710ce
fix: findAllByMemberId 메서드의 파라미터 오타 수정
htdufhc-bit Jul 15, 2026
5a080cf
chore: schema.sql 파일 끝 개행 추가
htdufhc-bit Jul 15, 2026
85ad22d
chore: JdbcTemplate 시절 잔여 파일 재삭제
htdufhc-bit Jul 15, 2026
363a773
fix: N+1 문제 해결 - Reservation 관련하여 LAZY 로딩 명시 및 fetch join 적용
htdufhc-bit Jul 15, 2026
436ecde
fix: RefreshToken 관련하여 LAZY 로딩 명시
htdufhc-bit Jul 15, 2026
8d117f1
refactor: EntityManager 기반 Repository를 JpaRepository로 전환
htdufhc-bit Jul 18, 2026
dc5b5e7
feat: 예약 대기열 구현
htdufhc-bit Jul 18, 2026
621f6a1
feat: Time과 Theme에 soft delete 적용
htdufhc-bit Jul 20, 2026
f81cc37
refactor: 불필요한 member의 JOIN FETCH 제거
htdufhc-bit Jul 20, 2026
322d8c1
refactor: 사용하지 않는 필드 제거
htdufhc-bit Jul 20, 2026
fa7cd36
refactor: 필드에 final 붙이도록 수정
htdufhc-bit Jul 20, 2026
787091a
test: 변경된 요구사항에 따라 기존 테스트 수정
htdufhc-bit Jul 20, 2026
2343bc4
test: 변경된 요구사항에 따른 test 추가
htdufhc-bit Jul 20, 2026
e1fac12
test: 인증과 예약 api에 대한 test 추가
htdufhc-bit Jul 20, 2026
c3bda17
Merge branch 'htdufhc-bit' into roomescape-JPA
htdufhc-bit Jul 20, 2026
6402408
refactor: 사용하지 않는 repository 제거
htdufhc-bit Jul 20, 2026
eaec41b
refactor: soft delete로 제거된 경우를 제외하고 findAll하도록 수정
htdufhc-bit Jul 20, 2026
63bc184
refactor: repository 필드를 final로 설정
htdufhc-bit Jul 20, 2026
6eaf8f3
refactor: resolveMember 메서드명을 getTargetMember로 변경하여 역할 명확화
htdufhc-bit Jul 21, 2026
ca1f694
refactor: 회원 삭제 시 연관 예약 자동 삭제를 위한 `@OnDelete` 추가
htdufhc-bit Jul 21, 2026
6e99544
refactor: config, exception, util을 global 패키지로 이동
htdufhc-bit Jul 21, 2026
f2fd69f
refactor: BaseSoftDeleteEntity를 도입해 Theme/Time의 소프트 삭제 공통화
htdufhc-bit Jul 21, 2026
4ff2550
refactor: 불필요한 import문 제거
htdufhc-bit Jul 21, 2026
795d0ad
refactor: 모든 dto를 record로 변경
htdufhc-bit Jul 21, 2026
d1da150
refactor: 객체 생성 및 DTO 변환 방식을 정적 팩토리 메서드와 record 접근자로 통일
htdufhc-bit Jul 21, 2026
1e36599
test: API 테스트를 api 패키지로 이동
htdufhc-bit Jul 22, 2026
69db99b
test: 데이터베이스 초기화를 통해 테스트 컨텍스트 재사용
htdufhc-bit Jul 22, 2026
5b1a969
test: 요청별 쿼리 카운터와 N+1 경고 기능 추가
htdufhc-bit Jul 22, 2026
72349b2
test: 리포지토리 테스트와 테스트 픽스처 추가
htdufhc-bit Jul 22, 2026
773b636
style: 대기 도메인 코드 포맷 정리
htdufhc-bit Jul 22, 2026
e385e04
test: 사용하지 않는 필드 제거
htdufhc-bit Jul 22, 2026
dfba04f
feat: DatabaseCleaner를 사용하는 테스트에 BeforeEachCallback 적용
htdufhc-bit Jul 25, 2026
bada4a5
test: Reservation과 Waiting api 테스트 추가
htdufhc-bit Jul 25, 2026
14acc26
refactor: 엔티티 생성자를 비공개로 전환하고 정적 팩토리가 요청 DTO 대신 값을 받도록 변경
htdufhc-bit Jul 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package roomescape.auth.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import roomescape.auth.entity.RefreshToken;

import java.util.Optional;

public interface RefreshTokenRepository {

void save(RefreshToken refreshToken);
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시간이 좀 있으면 JpaRepository 내부 구현체가 대략적으로 어떤 클래스로 나뉘고 각각 어떤 역할을 하는지 찾아보시면 좋아보여요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 한 번 따로 공부해보겠습니다!!


Optional<RefreshToken> findByToken(String token);

Optional<RefreshToken> findByMemberId(Long memberId);

void deleteByMemberId(Long memberId);

void flush();
}

This file was deleted.

10 changes: 4 additions & 6 deletions src/main/java/roomescape/member/repository/MemberRepository.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package roomescape.member.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import roomescape.member.entity.Member;

import java.util.Optional;

public interface MemberRepository {

Member save(Member member);

Optional<Member> findById(Long memberId);

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmailAndPassword(String email, String password);

Optional<Member> findByName(String name);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/roomescape/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@Service
@Transactional(readOnly = true)
public class MemberService {
private MemberRepository memberRepository;
private final MemberRepository memberRepository;

public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@
import roomescape.reservation.dto.ReservationRequest;
import roomescape.reservation.dto.ReservationResponse;
import roomescape.reservation.service.ReservationService;
import roomescape.waiting.service.WaitingService;

import java.net.URI;
import java.util.List;
import java.util.stream.Stream;

@RestController
public class ReservationController {

private final ReservationService reservationService;
private final WaitingService waitingService;

public ReservationController(ReservationService reservationService) {
public ReservationController(ReservationService reservationService, WaitingService waitingService) {
this.reservationService = reservationService;
this.waitingService = waitingService;
}

@GetMapping("/reservations")
Expand All @@ -34,9 +38,13 @@ public List<ReservationResponse> list() {

@GetMapping("/reservations-mine")
public ResponseEntity<List<MyReservationResponse>> getUserReservations(@Login LoginMember loginMember) {
List<MyReservationResponse> responseList = reservationService.findReservationsByMember(loginMember);

return ResponseEntity.ok(responseList);
List<MyReservationResponse> responses = Stream.concat(
reservationService.findReservationsByMember(loginMember)
.stream(),
waitingService.findWaitingsByMember(loginMember)
.stream()
).toList();
return ResponseEntity.ok(responses);
}

@PostMapping("/reservations")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package roomescape.reservation.exception;

import org.springframework.http.HttpStatus;
import roomescape.exception.ErrorCode;

public enum ReservationErrorCode implements ErrorCode {

DUPLICATE_RESERVATION(HttpStatus.CONFLICT, "해당 시간에 이미 예약이 존재합니다.");

private HttpStatus httpStatus;
private String message;

ReservationErrorCode(HttpStatus httpStatus, String message) {
this.httpStatus = httpStatus;
this.message = message;
}

@Override
public HttpStatus getHttpStatus() {
return httpStatus;
}

@Override
public String getMessage() {
return message;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
package roomescape.reservation.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import roomescape.reservation.entity.Reservation;
import roomescape.theme.entity.Theme;
import roomescape.time.entity.Time;

import java.time.LocalDate;
import java.util.List;

public interface ReservationRepository {
@Repository
public interface ReservationRepository extends JpaRepository<Reservation, Long> {

@Override
@Query("SELECT r FROM reservation r " +
"JOIN FETCH r.member " +
"JOIN FETCH r.time " +
"JOIN FETCH r.theme")
List<Reservation> findAll();

List<Reservation> findAllByMemberId(Long memberId);
@Query("SELECT r FROM reservation r " +
"JOIN FETCH r.time " +
"JOIN FETCH r.theme " +
"WHERE r.member.id = :memberId")
List<Reservation> findAllByMemberId(@Param("memberId") Long memberId);

List<Reservation> findByDateAndThemeId(LocalDate date, Long themeId);

Reservation save(Reservation reservation);

void deleteById(Long id);
boolean existsByDateAndTimeAndTheme(
@Param("date") LocalDate date,
@Param("time") Time time,
@Param("theme") Theme theme
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import roomescape.reservation.dto.ReservationRequest;
import roomescape.reservation.dto.ReservationResponse;
import roomescape.reservation.entity.Reservation;
import roomescape.reservation.exception.ReservationErrorCode;
import roomescape.reservation.repository.ReservationRepository;
import roomescape.theme.entity.Theme;
import roomescape.theme.repository.ThemeRepository;
Expand All @@ -23,6 +24,7 @@
@Service
@Transactional(readOnly = true)
public class ReservationService {

private final ReservationRepository reservationRepository;
private final MemberRepository memberRepository;
private final TimeRepository timeRepository;
Expand All @@ -37,12 +39,14 @@ public ReservationService(ReservationRepository reservationRepository, MemberRep

@Transactional
public ReservationResponse create(ReservationRequest request, LoginMember loginMember) {
Member member = resolveMember(request, loginMember);
LocalDate date = LocalDate.parse(request.getDate());
Time time = timeRepository.findById(request.getTime())
.orElseThrow();
Theme theme = themeRepository.findById(request.getTheme())
.orElseThrow();
validateDuplicateReservation(date, time, theme);

Member member = resolveMember(request, loginMember);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveName 대신 더 나은 네이밍으로 지을 수 있어보여요~

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드의 역할이 원하는 멤버의 이름을 받아오는 것이기 때문에 getTargetMember()로 수정해보겠습니다!

[반영 커밋] - 6eaf8f3


Reservation reservation = new Reservation(member, date, time, theme);
Reservation savedReservation = reservationRepository.save(reservation);
Expand Down Expand Up @@ -95,4 +99,10 @@ private Member resolveMember(ReservationRequest reservationRequest, LoginMember
return memberRepository.findById(loginMember.id())
.orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND));
}

private void validateDuplicateReservation(LocalDate date, Time time, Theme theme) {
if (reservationRepository.existsByDateAndTimeAndTheme(date, time, theme)) {
throw new ApplicationException(ReservationErrorCode.DUPLICATE_RESERVATION);
}
}
}
4 changes: 4 additions & 0 deletions src/main/java/roomescape/theme/entity/Theme.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public Theme(String name, String description) {
this.description = description;
}

public void markDeleted() {
this.deleted = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 여기는 softDelete를 사용하게된 이유가 있을까요?
  2. 참고로 softDelete를 사용하게 된다면 boolean보다 LocalDateTime 같은걸 사용하는게 언제 삭제됐는지 기록이 되서 더 좋더라구요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Theme 및 Time 삭제 기능에 대한 명확한 요구사항이 없어, 도메인 영향도를 고려해 다음과 같이 가정하고 설계했습니다.

    • Hard Delete 방식을 사용할 경우, Theme나 Time 삭제 시 기존 예약 데이터까지 함께 삭제되거나, 외래 키(FK) 제약조건으로 인해 삭제 자체가 불가능해집니다. 관리자 입장에서 "이후 추가 예약만 막고, 기존 예약 내역은 유지"하고 싶을 때 Hard Delete는 적합하지 않다고 판단했습니다.
    • 따라서 기존 예약을 안전하게 보존하면서 특정 시간과 테마의 신규 예약만 제한할 수 있도록 Soft Delete 방식을 적용했습니다.
    • 회원 탈퇴의 경우, Soft Delete를 적용하면 탈퇴한 회원의 예약 정보만 남아 "방문할 사용자는 없는데 예약만 존재하는" 상태가 발생하는 문제가 있습니다. 따라서 서비스 데이터의 정합성을 위해 탈퇴 시 관련 데이터를 완벽히 제거하는 Hard Delete 방식으로 구현했습니다. (Hard Delete를 위해 Cascade로 삭제되도록 수정했습니다!!)
  2. 남겨주신 리뷰대로 LocalDateTime을 활용하고, null이냐 아니냐로 삭제 여부를 판단하게 되면 soft delete도 구현하고 언제 삭제되었는지에 대한 기록도 남아 더 좋을 것 같습니다. 수정하겠습니다!!

@htdufhc-bit htdufhc-bit Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time, Theme처럼 Soft Delete가 필요한 엔티티마다 deletedAt 관련 필드와 로직이 중복되고 있었습니다. 이를 공통 상위 클래스로 추출해, Soft Delete가 필요한 엔티티는 상속만 받으면 되도록 했습니다.

[반영 커밋] - f2fd69f

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theme 및 Time 삭제 기능에 대한 명확한 요구사항이 없어, 도메인 영향도를 고려해 다음과 같이 가정하고 설계했습니다.
Hard Delete 방식을 사용할 경우, Theme나 Time 삭제 시 기존 예약 데이터까지 함께 삭제되거나, 외래 키(FK) 제약조건으로 인해 삭제 자체가 불가능해집니다. 관리자 입장에서 "이후 추가 예약만 막고, 기존 예약 내역은 유지"하고 싶을 때 Hard Delete는 적합하지 않다고 판단했습니다.
따라서 기존 예약을 안전하게 보존하면서 특정 시간과 테마의 신규 예약만 제한할 수 있도록 Soft Delete 방식을 적용했습니다.
회원 탈퇴의 경우, Soft Delete를 적용하면 탈퇴한 회원의 예약 정보만 남아 "방문할 사용자는 없는데 예약만 존재하는" 상태가 발생하는 문제가 있습니다. 따라서 서비스 데이터의 정합성을 위해 탈퇴 시 관련 데이터를 완벽히 제거하는 Hard Delete 방식으로 구현했습니다. (Hard Delete를 위해 Cascade로 삭제되도록 수정했습니다!!)

Hard Delete, Soft Delete 사용한 근거까지 좋네요 👍 이런 내용들이 코딩하기 전에 설계할 때 중요하더라구요

남겨주신 리뷰대로 LocalDateTime을 활용하고, null이냐 아니냐로 삭제 여부를 판단하게 되면 soft delete도 구현하고 언제 삭제되었는지에 대한 기록도 남아 더 좋을 것 같습니다. 수정하겠습니다!!

이거는 본인이 과거에 데이터 삭제한걸 깜빡하고 문의주시는 사용자가 생각보다 있어서 유용했어요 ㅋㅋ..

public Long getId() {
return id;
}
Expand Down
Loading