Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
3d3151e
refactor: Id로 맴버 조회 dao 추가
mintcoke123 Dec 26, 2025
0c3e685
feat: POST/login 추가
mintcoke123 Dec 27, 2025
8b26e96
feat: 토큰 조회
mintcoke123 Dec 27, 2025
378e7c5
refactor: 키 제사용 가능하게 변경
mintcoke123 Dec 27, 2025
cfd15f6
test: 이단계 테스트 추가
mintcoke123 Dec 27, 2025
b456b89
로그인맴버 dto 추가
mintcoke123 Dec 27, 2025
97cf233
feat: resolver,config 추가
mintcoke123 Dec 27, 2025
451beff
refactor: reservationRequest에 생성자 생성
mintcoke123 Dec 27, 2025
f1e3288
refactor: createToken 메서드분리
mintcoke123 Dec 27, 2025
0d5c991
test: 테스트코드 리팩토링
mintcoke123 Dec 27, 2025
f6a7db0
refactor: 테스트코드대로 컨트롤러 수정
mintcoke123 Dec 27, 2025
20d7526
test: 삼단계 테스트코드 작성
mintcoke123 Dec 27, 2025
1357d07
feat: interceptor 구현
mintcoke123 Dec 27, 2025
12171bb
refactor: auth의 중복 제거
mintcoke123 Dec 31, 2025
1eb6513
refactor: 프로퍼티 주입으로 시크릿 키 관리
mintcoke123 Dec 31, 2025
a108bc6
refactor: application.properties 수정
mintcoke123 Dec 31, 2025
069e91f
test: 테스트코드 리팩토링
mintcoke123 Dec 31, 2025
f7a7485
refactor: 생성자 주입으로 테스트코드 변경
mintcoke123 Dec 31, 2025
b4869c1
test: truncate 데이터 구축
mintcoke123 Dec 31, 2025
97e0106
refactor: 검증을 requestDto단에서 적용하도록 변경
mintcoke123 Dec 31, 2025
4b90a36
refactor: 어드민 페이지 엔드포인트 수정
mintcoke123 Dec 31, 2025
9636a93
test: 미션 테스트코드 변경
mintcoke123 Jan 1, 2026
06b14d0
test: 미션 테스트코드 변경
mintcoke123 Jan 1, 2026
12f68bb
refactor: TimeDao에서 id로 time을 확인할 수 있게끔 변경
mintcoke123 Jan 1, 2026
ee61649
refactor: gradle 파일 수정
mintcoke123 Jan 1, 2026
589badc
test: 테스트코드 추가, dao를 jpa 방식으로 변경하며 삭제
mintcoke123 Jan 1, 2026
d70f3c3
refactor: dao를 repository 방식으로 변경경
mintcoke123 Jan 1, 2026
ee530e2
refactor: JdbcTemplate 기반 Dao를 jpa 레포지토리로 리팩토링
mintcoke123 Jan 1, 2026
7554215
test: 테스트코드를 바뀐 코드에 걸맞게 변경
mintcoke123 Jan 1, 2026
8d6b22b
refactor: 스키마, 시드 정리
mintcoke123 Jan 1, 2026
3a20e9f
refactor: 멤버 엔티티 매핑
mintcoke123 Jan 1, 2026
aacf60c
refactor: 내 예약 responseDto 구현
mintcoke123 Jan 1, 2026
b935aca
refactor: LoginMember 주입받아 서비스 호출출
mintcoke123 Jan 1, 2026
43de312
refactor: ReservationRepository 리팩토링
mintcoke123 Jan 1, 2026
744396a
test: 테스트코드 변경
mintcoke123 Jan 1, 2026
be881da
refactor: 스키마 테이블, 테스트코드 테이블블 변경
mintcoke123 Jan 1, 2026
ce83b57
feat: waiting 엔티티 추가
mintcoke123 Jan 1, 2026
c61db0a
feat: WaitingService 및 WaitingController추가가
mintcoke123 Jan 1, 2026
d7e38ec
refactor: step-6 구현
mintcoke123 Jan 1, 2026
1c30a05
refactor: waitingResponse의 setter 추가(테스트코드용)
mintcoke123 Jan 1, 2026
a44bd1f
refactor: myReservationResponse에 jsonIgnore 적용
mintcoke123 Jan 1, 2026
739e9db
refactor: 사용하지 않는 메서드 삭제
mintcoke123 Jan 7, 2026
c05f17e
refactor: @getter 어노테이션 적용
mintcoke123 Jan 7, 2026
5b99e63
refactor: 미사용중인 import 문 제거
mintcoke123 Jan 7, 2026
68ae9f5
refactor: 에러코드 수정
mintcoke123 Jan 7, 2026
c8ef4c0
refactor: dto로 네이밍 변경
mintcoke123 Jan 7, 2026
26a581a
refactor: record 사용으로 수정
mintcoke123 Jan 8, 2026
0149b40
refactor: service 의존성 필드에 final 사용
mintcoke123 Jan 8, 2026
94fd21f
refactor: @transactional 적용
mintcoke123 Jan 8, 2026
134fb43
refactor: ApiError 확장 후 적용
mintcoke123 Jan 8, 2026
43a92c7
refactor: Jwt 만료시간 관리 일관화
mintcoke123 Jan 8, 2026
6358623
refactor: N+1 방지, 상수에 enum도입
mintcoke123 Jan 8, 2026
02dc95d
refactor: 리뷰 반영
mintcoke123 Jan 16, 2026
b69d20f
refactor: 리뷰 반영(2차)
mintcoke123 Jan 18, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ out/

### VS Code ###
.vscode/


application-local.properties
10 changes: 8 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ plugins {

group = 'nextstep'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

오잉 요게 삭제된 이유가 있나요??

gradle.properties, /node_modules/yarn-integrity는 추가됐는데 요거는 왜 추가된걸까요?

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.

제 컴퓨터에서 자바 저번이 맞지 않아 빌드가 안되는 상황이 발생해 이것저것 시도해보다가 생긴 결과물입니다ㅠㅠ
sourceCompatibility 부분과 gradle.properties부분에 충돌이 일어나는 상황이라고 판단해 지웠었습니다만, 결론적으로는 로컬 경로 문제였기 때문에 삭제할 이유는 없었습니다.

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.

프로젝트의 자바 문법 고정을 위해 sourceCompatibility를 다시 추가하겠습니다!


repositories {
mavenCentral()
}

dependencies {


implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'

compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'

Expand Down
4 changes: 4 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-17.0.17.10-hotspot
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이거는 동현님만 사용 가능한 경로이기 때문에 요 파일 삭제해도 제대로 구동 가능하게 해주세요~




10 changes: 10 additions & 0 deletions node_modules/.yarn-integrity

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions src/main/java/auth/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;

import java.util.Date;

public final class JwtUtils {
public static final int DEFAULT_MAX_AGE_SECONDS = 10 * 60; // 10 minutes

private final String secretKey;

public JwtUtils(String secretKey) {
this.secretKey = secretKey;
}

public String extractTokenFromCookies(Cookie[] cookies) {
if (cookies == null || cookies.length == 0) {
return "";
}
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
return cookie.getValue();
}
}
return "";
}

public Claims parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}

public String createToken(String subject, String name, String role) {
Date now = new Date();
Date expiresAt = new Date(now.getTime() + (long) DEFAULT_MAX_AGE_SECONDS * 1000);
return Jwts.builder()
.setSubject(subject)
.setExpiration(expiresAt)
.claim("name", name)
.claim("role", role)
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.compact();
}
}


14 changes: 0 additions & 14 deletions src/main/java/roomescape/ExceptionController.java

This file was deleted.

33 changes: 33 additions & 0 deletions src/main/java/roomescape/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package roomescape;

import auth.JwtUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import roomescape.auth.AdminAuthInterceptor;
import roomescape.auth.LoginMemberArgumentResolver;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {
private final JwtUtils jwtUtils;

public WebConfig(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver(jwtUtils));
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AdminAuthInterceptor(jwtUtils))
.addPathPatterns("/admin", "/admin/**");
}
}


57 changes: 57 additions & 0 deletions src/main/java/roomescape/auth/AdminAuthInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package roomescape.auth;

import auth.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.common.ApiError;

import java.io.IOException;

public class AdminAuthInterceptor implements HandlerInterceptor {

private final JwtUtils jwtUtils;

public AdminAuthInterceptor(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = jwtUtils.extractTokenFromCookies(request.getCookies());
if (token.isEmpty()) {
writeError(response, ApiError.UNAUTHORIZED_MISSING_TOKEN);
return false;
}

try {
Claims claims = jwtUtils.parseClaims(token);
String role = claims.get("role", String.class);
if (!"ADMIN".equals(role)) {
writeError(response, ApiError.FORBIDDEN_ADMIN_ONLY);
return false;
}
return true;
} catch (ExpiredJwtException e) {
writeError(response, ApiError.UNAUTHORIZED_EXPIRED_TOKEN);
return false;
} catch (Exception e) {
writeError(response, ApiError.UNAUTHORIZED_INVALID_TOKEN);
return false;
}
}

private void writeError(HttpServletResponse response, ApiError apiError) {
response.setStatus(apiError.getHttpStatus().value());
response.setContentType("application/json;charset=UTF-8");
try {
String payload = "{\"code\":" + apiError.getCode() + ",\"message\":\"" + apiError.getMessage() + "\"}";
response.getWriter().write(payload);
} catch (IOException ignored) {
}
}
}
Comment on lines +46 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

나중에 이런 곳에 에러 로그 (ex. slf4j)를 추가하는 것도 좋아보이네요~

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.

넵! 이후 코드를 짤때는 logger를 적용해보겠습니다!



55 changes: 55 additions & 0 deletions src/main/java/roomescape/auth/LoginMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package roomescape.auth;

import auth.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.HttpServletRequest;
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;
import roomescape.member.LoginMemberDto;

public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final JwtUtils jwtUtils;

public LoginMemberArgumentResolver(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(LoginMemberDto.class);
}

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String token = jwtUtils.extractTokenFromCookies(request.getCookies());

if (token.isEmpty()) {
return null;
}

try {
Claims claims = jwtUtils.parseClaims(token);

Long id = Long.valueOf(claims.getSubject());
String name = claims.get("name", String.class);
String role = claims.get("role", String.class);

return new LoginMemberDto(id, name, null, role);
} catch (ExpiredJwtException e) {
return null;
} catch (Exception e) {
return null;
}
}
}
26 changes: 26 additions & 0 deletions src/main/java/roomescape/common/ApiError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package roomescape.common;

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

@Getter
public enum ApiError {
BAD_REQUEST_INVALID_INPUT(HttpStatus.BAD_REQUEST, 40001, "잘못된 요청입니다."),
BAD_REQUEST_ILLEGAL_STATE(HttpStatus.BAD_REQUEST, 40002, "요청을 처리할 수 없습니다."),
NOT_FOUND_RESOURCE(HttpStatus.NOT_FOUND, 40401, "리소스를 찾을 수 없습니다."),
UNAUTHORIZED_MISSING_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "토큰이 없습니다."),
UNAUTHORIZED_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 40102, "토큰이 유효하지 않습니다."),
UNAUTHORIZED_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, 40103, "토큰이 만료되었습니다."),
FORBIDDEN_ADMIN_ONLY(HttpStatus.FORBIDDEN, 40301, "관리자 권한이 필요합니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50000, "서버 오류가 발생했습니다.");

private final HttpStatus httpStatus;
private final int code;
private final String message;

ApiError(HttpStatus httpStatus, int code, String message) {
this.httpStatus = httpStatus;
this.code = code;
this.message = message;
}
}
50 changes: 50 additions & 0 deletions src/main/java/roomescape/common/ExceptionController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package roomescape.common;

import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

@RestControllerAdvice
public class ExceptionController {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException e) {
return build(ApiError.BAD_REQUEST_INVALID_INPUT);
}

@ExceptionHandler({IllegalStateException.class})
public ResponseEntity<Map<String, Object>> handleIllegalState(IllegalStateException e) {
return build(ApiError.BAD_REQUEST_ILLEGAL_STATE);
}

@ExceptionHandler({NoSuchElementException.class, EntityNotFoundException.class})
public ResponseEntity<Map<String, Object>> handleNotFound(RuntimeException e) {
return build(ApiError.NOT_FOUND_RESOURCE);
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleNotReadable(HttpMessageNotReadableException e) {
return build(ApiError.BAD_REQUEST_INVALID_INPUT);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleUnknown(Exception e) {
e.printStackTrace();
return build(ApiError.INTERNAL_SERVER_ERROR);
}

private ResponseEntity<Map<String, Object>> build(ApiError apiError) {
Map<String, Object> body = new HashMap<>();
body.put("code", apiError.getCode());
body.put("message", apiError.getMessage());
return ResponseEntity.status(apiError.getHttpStatus()).body(body);
}
}


17 changes: 17 additions & 0 deletions src/main/java/roomescape/config/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package roomescape.config;

import auth.JwtUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AuthConfig {

@Bean
public JwtUtils jwtUtils(@Value("${roomescape.auth.jwt.secret}") String secretKey) {
return new JwtUtils(secretKey);
}
}


7 changes: 7 additions & 0 deletions src/main/java/roomescape/member/LoginMemberDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.member;

public record LoginMemberDto(Long id, String name, String email, String role) {
}



Loading