Skip to content

[Spring JPA] Piki 2주차 미션 제출합니다. #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: piki
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
356d87e
feat: 4단계 구현
seulnan Apr 25, 2025
55ec6e2
refactor: auth관련설정도 dao대신 jpa로 변경
seulnan Apr 25, 2025
e7865f8
refactor: 쿠키 기반 인증 흐름 개선 및 예외 처리 일관성 확보
seulnan Apr 25, 2025
e229644
feat: 5단계 구현
seulnan Apr 25, 2025
7178bdf
feat: 6단계 구현
seulnan Apr 25, 2025
e0d50f2
chore: 파일 끝에 개행추가
seulnan Apr 25, 2025
25984af
refactor: ReservationService에서 MemberService대신 repository주입받도록 설정
seulnan Apr 27, 2025
ba6fc0a
refactor: java컨벤션에 맞춰 카멜케이스로 네이밍 변경
seulnan Apr 27, 2025
f37a323
refactor: 엔티티 protected 기본 생성자를 @NoArgsConstructor로 대체
seulnan Apr 28, 2025
d75fcc8
fix: JWT 발급 시 유효기간 설정 오류 수정 및 properties 관리 개선
seulnan Apr 28, 2025
d850bb5
fix(waiting): 대기 등록 시 순번 반환 및 조회 성능 개선
seulnan Apr 28, 2025
b78ee27
fix(reservation): Reservation 엔티티 정규화 및 save 메서드 리팩터링
seulnan Apr 29, 2025
6b19d16
refactor: DTO 클래스들을 record로 변경하여 코드 간결화 및 불변성 강화
seulnan Apr 29, 2025
6892024
chore: 불필요한 필드 및 import 제거, @NotNull 추가로 null 체크 강화
seulnan May 1, 2025
0466798
refactor: record 미적용된 클래스 변경, @Transactional 누락된 서비스 메서드 보완
seulnan May 1, 2025
e28f6e7
fix: Reservation 및 Waiting 조회 시 N+1 문제 해결
seulnan May 1, 2025
5549795
feat: 예약 대기 취소 기능 구현
seulnan May 1, 2025
fcad67e
refactor: LoginMemberArgumentResolver DI방식으로 변경
seulnan May 2, 2025
ef45579
refactor(auth): 인증 흐름 내 중복 예외 제거 및 책임 분리
seulnan May 2, 2025
643dfb8
refactor: 전역 예외처리 리팩토링 및 ErrorCode enum 통합 적용
seulnan May 4, 2025
956e95f
fix: JWT 토큰에 member id 포함하여 충돌가능성 제거
seulnan May 4, 2025
453fd02
refactor: ThemeService로 책임 분리
seulnan May 4, 2025
8f18e74
refactor(time): setter 제거 및 markDeleted 도입으로 도메인 일관성 강화
seulnan May 4, 2025
578ec78
fix(reservation): 예약 중복 방지 및 예약자에 대한 예약대기 제한 로직 추가
seulnan May 4, 2025
e432e37
error: theme&time notFound에러 처리 개선
seulnan May 5, 2025
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.4.4'
implementation 'org.springframework.boot:spring-boot-starter-web:3.4.4'
implementation 'org.springframework.boot:spring-boot-starter-jdbc:3.4.4'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.4.4'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.5.1'
Expand Down
7 changes: 3 additions & 4 deletions src/main/java/com/yourssu/roomescape/auth/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public AuthController(AuthService authService) {
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest request, HttpServletResponse response) {
AuthResponse authResponse = authService.login(request);

Cookie cookie = new Cookie(CookieUtil.TOKEN, authResponse.getToken());
Cookie cookie = new Cookie(CookieUtil.TOKEN, authResponse.token());
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
Expand All @@ -32,8 +32,6 @@ public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest request, Http
@GetMapping("/login/check")
public ResponseEntity<LoginCheckResponse> checkLogin(HttpServletRequest request) {
String token = CookieUtil.extractTokenFromCookies(request.getCookies());
if (token == null || token.isEmpty()) return ResponseEntity.status(401).build();

String name = authService.getNameFromToken(token);
return ResponseEntity.ok(new LoginCheckResponse(name));
}
Expand All @@ -47,4 +45,5 @@ public ResponseEntity<Void> logout(HttpServletResponse response) {
response.addCookie(cookie);
return ResponseEntity.ok().build();
}
}

}
13 changes: 2 additions & 11 deletions src/main/java/com/yourssu/roomescape/auth/AuthRequest.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
package com.yourssu.roomescape.auth;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
public record AuthRequest(String email, String password) {
}

@Getter
@AllArgsConstructor
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
public class AuthRequest {
private final String email;
private final String password;
}
10 changes: 2 additions & 8 deletions src/main/java/com/yourssu/roomescape/auth/AuthResponse.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
package com.yourssu.roomescape.auth;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class AuthResponse {
private final String token;
}
public record AuthResponse(String token) {
}
20 changes: 11 additions & 9 deletions src/main/java/com/yourssu/roomescape/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
package com.yourssu.roomescape.auth;

import com.yourssu.roomescape.member.Member;
import com.yourssu.roomescape.member.MemberDao;
import com.yourssu.roomescape.exception.MemberNotFoundException;
import com.yourssu.roomescape.exception.ErrorCode;
import com.yourssu.roomescape.member.MemberRepository;
import com.yourssu.roomescape.util.JwtTokenProvider;
import org.springframework.stereotype.Service;


@Service
public class AuthService {

private final MemberDao memberDao;
private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;

public AuthService(MemberDao memberDao, JwtTokenProvider jwtTokenProvider) {
this.memberDao = memberDao;
public AuthService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) {
this.memberRepository = memberRepository;
this.jwtTokenProvider = jwtTokenProvider;
}

public AuthResponse login(AuthRequest request) {
Member member = memberDao.findByEmailAndPassword(request.getEmail(), request.getPassword())
.orElseThrow(() -> new MemberNotFoundException("이메일 또는 비밀번호가 틀렸습니다."));
Member member = memberRepository.findByEmailAndPassword(request.email(), request.password())
.orElseThrow(() -> new MemberNotFoundException(ErrorCode.INVALID_LOGIN, "이메일: " + request.email()));
String token = jwtTokenProvider.createToken(member);
return new AuthResponse(token);
}

public String getNameFromToken(String token) {
String email = jwtTokenProvider.getEmail(token);
Member member = memberDao.findByEmail(email)
.orElseThrow(() -> new MemberNotFoundException("토큰에 해당하는 사용자를 찾을 수 없습니다."));
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND, "이메일: " + email));
return member.getName();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
package com.yourssu.roomescape.auth;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class LoginCheckResponse {
private final String name;
}
public record LoginCheckResponse(String name) {
}
13 changes: 2 additions & 11 deletions src/main/java/com/yourssu/roomescape/auth/LoginMember.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
package com.yourssu.roomescape.auth;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class LoginMember {
private final Long id;
private final String name;
private final String email;
private final String role;
}
public record LoginMember(Long id, String name, String email, String role) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.yourssu.roomescape.auth;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginMemberAnnotation {
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
package com.yourssu.roomescape.auth;

import com.yourssu.roomescape.exception.ErrorCode;
import com.yourssu.roomescape.exception.MemberNotFoundException;
import com.yourssu.roomescape.member.Member;
import com.yourssu.roomescape.member.MemberDao;
import com.yourssu.roomescape.member.MemberRepository;
import com.yourssu.roomescape.util.CookieUtil;
import com.yourssu.roomescape.util.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Configuration
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final MemberDao memberDao;
private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;

public LoginMemberArgumentResolver(MemberDao memberDao, JwtTokenProvider jwtTokenProvider) {
this.memberDao = memberDao;
public LoginMemberArgumentResolver(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) {
this.memberRepository = memberRepository;
this.jwtTokenProvider = jwtTokenProvider;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(LoginMember.class);
return parameter.hasParameterAnnotation(LoginMemberAnnotation.class) &&
parameter.getParameterType().equals(com.yourssu.roomescape.auth.LoginMember.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

String token = CookieUtil.extractTokenFromCookies(request.getCookies());
if (token == null || token.isBlank()) return null;

String email = jwtTokenProvider.getEmail(token);
Member member = memberDao.findByEmail(email)
.orElseThrow(() -> new RuntimeException("사용자 정보를 찾을 수 없습니다."));
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND, "이메일: " + email));

return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.yourssu.roomescape.util.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

Expand All @@ -17,7 +18,7 @@ public AdminInterceptor(JwtTokenProvider jwtTokenProvider) {
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
String token = CookieUtil.extractTokenFromCookies(request.getCookies());

if (token == null || token.isBlank()) {
Expand Down
13 changes: 5 additions & 8 deletions src/main/java/com/yourssu/roomescape/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.yourssu.roomescape.config;

import com.yourssu.roomescape.auth.LoginMemberArgumentResolver;
import com.yourssu.roomescape.member.MemberDao;
import com.yourssu.roomescape.util.JwtTokenProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.*;
Expand All @@ -12,19 +10,17 @@
@Configuration
public class WebConfig implements WebMvcConfigurer {

private final MemberDao memberDao;
private final JwtTokenProvider jwtTokenProvider;
private final AdminInterceptor adminInterceptor;
private final LoginMemberArgumentResolver loginMemberArgumentResolver;

public WebConfig(MemberDao memberDao, JwtTokenProvider jwtTokenProvider, AdminInterceptor adminInterceptor) {
this.memberDao = memberDao;
this.jwtTokenProvider = jwtTokenProvider;
public WebConfig(AdminInterceptor adminInterceptor, LoginMemberArgumentResolver loginMemberArgumentResolver) {
this.adminInterceptor = adminInterceptor;
this.loginMemberArgumentResolver = loginMemberArgumentResolver;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver(memberDao, jwtTokenProvider));
resolvers.add(loginMemberArgumentResolver);
}

@Override
Expand All @@ -33,3 +29,4 @@ public void addInterceptors(InterceptorRegistry registry) {
.addPathPatterns("/admin");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.yourssu.roomescape.exception;

import lombok.Getter;

@Getter
public class DuplicateException extends RuntimeException {
private final ErrorCode errorCode;

public DuplicateException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/yourssu/roomescape/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.yourssu.roomescape.exception;

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum ErrorCode {

INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 생겼습니다."),

// Auth
COOKIE_NOT_FOUND(HttpStatus.UNAUTHORIZED, "쿠키가 존재하지 않습니다."),
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "쿠키에 토큰이 존재하지 않습니다."),
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."),
UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "지원하지 않는 토큰 형식입니다."),
MALFORMED_TOKEN(HttpStatus.UNAUTHORIZED, "손상된 JWT 토큰입니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT 토큰입니다."),

// Member
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 정보를 찾을 수 없습니다."),
INVALID_LOGIN(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 틀렸습니다."),
NOT_ADMIN(HttpStatus.FORBIDDEN, "관리자 권한이 필요합니다."),

// Reservation
RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "예약 정보를 찾을 수 없습니다."),
NO_PERMISSION_FOR_RESERVATION(HttpStatus.FORBIDDEN, "해당 예약에 대한 권한이 없습니다."),
RESERVATION_ALREADY_EXISTS(HttpStatus.CONFLICT, "예약 정보가 이미 존재합니다."),
WAITING_ALREADY_EXISTS(HttpStatus.CONFLICT, "예약 대기 정보가 이미 존재합니다."),

// Time
TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "시간 정보를 찾을 수 없습니다."),

// Theme
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "테마 정보를 찾을 수 없습니다.");

private final HttpStatus status;
private final String message;

ErrorCode(HttpStatus status, String message) {
this.status = status;
this.message = message;
}

}
15 changes: 4 additions & 11 deletions src/main/java/com/yourssu/roomescape/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package com.yourssu.roomescape.exception;

import lombok.Getter;

@Getter
public class ErrorResponse {

private final String message;

public ErrorResponse(String message) {
this.message = message;
public record ErrorResponse(int status, String message) {
public ErrorResponse(ErrorCode errorCode) {
this(errorCode.getStatus().value(), errorCode.getMessage());
}

}
}
Loading