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
29a3411
feat : 일단계 테스트 통과
nonactress Dec 24, 2025
76e20ba
feat(dao) : 이메일로 멤버 찾기
nonactress Dec 24, 2025
6884888
feat(MemberController) : 회원 조회
nonactress Dec 26, 2025
b994a63
feat : 2단계 통과~!!!!
nonactress Dec 26, 2025
9234df4
feat : AdminInterceptor 생성
nonactress Dec 26, 2025
103e6bb
feat(webConfig) : admin인터셉터 추가
nonactress Dec 26, 2025
ecd0c7f
fix : 개행정리
nonactress Dec 26, 2025
334327e
fix : 개행정리2
nonactress Dec 26, 2025
eeb4f66
fix : 패키지 정리
nonactress Dec 26, 2025
a75257b
fix : 1단계 login 쿠키 설정 및 tokenResponse dto 제거
nonactress Dec 26, 2025
f95bdc5
feat : time 엔티티로 수정 및 repository 생성
nonactress Dec 31, 2025
c1610e8
fix(Time) : 서비스에서 timerepository 사용 리팩토링
nonactress Dec 31, 2025
aa8870e
fix(Theme) : 컨트롤러에서 DAO -> Repository 사용
nonactress Dec 31, 2025
9f54ac5
fix(All)
nonactress Jan 1, 2026
a674fa5
fix(All) : reservation.save 오버로딩 및 영속성 관리 하여 에러 수정
nonactress Jan 1, 2026
f9f63d1
refactor(Reservaiton) : 예약 조회 기능 생성
nonactress Jan 1, 2026
5ee3e74
feat : waiting 엔티티 생성
nonactress Jan 1, 2026
cc28fde
feat : WaitingRepository 생성
nonactress Jan 1, 2026
b5cca3b
fix : ReservationService 리팩토링
nonactress Jan 1, 2026
9deeb22
feat : waiting dto 생성
nonactress Jan 1, 2026
7742db7
feat : WaitingService,controller 생성
nonactress Jan 1, 2026
0812bd7
개행정리
nonactress Jan 1, 2026
8f084a7
feat : 예외 처리
nonactress Jan 1, 2026
4876179
fix : WaitingRepository 스프링 data jpa 사용
nonactress Jan 7, 2026
0f3aadf
fix : ReservationRepository 스프링 data jpa 사용
nonactress Jan 7, 2026
7a12ebf
fix : MemberRepository 스프링 data jpa 사용
nonactress Jan 7, 2026
b05b545
fix : ThemeRepository 스프링 data jpa 사용
nonactress Jan 8, 2026
e83a263
fix : TimeRepository 스프링 data jpa 사용
nonactress Jan 8, 2026
916af1e
rename : advice 패키지 -> exception 패키지
nonactress Jan 8, 2026
48a6984
fix : theme 생성자
nonactress Jan 8, 2026
659c17b
fix : 예외 처리
nonactress Jan 10, 2026
f4f58e9
fix : 예외 처리2
nonactress Jan 10, 2026
4c51bea
fix : authService 생성 및 상태코드 변경
nonactress Jan 10, 2026
db04811
refactor : 리포맷 적용 && 안쓰는 메소드 삭제
nonactress Jan 13, 2026
1ca9fa1
refactor : isDeleted->delete
nonactress Jan 13, 2026
494fffe
refactor : readOnly = true 적용
nonactress Jan 14, 2026
0f655d2
필드 값 final로 변경
nonactress Jan 14, 2026
5ce1144
refactor(themeService) : 클래스 단위 트랜젝션 적용
nonactress Jan 14, 2026
eecfe3a
refactor(themeService) : 중복된 삭제 로직 정리
nonactress Jan 14, 2026
bd4201c
refactor(themeService) : 중복된 삭제 로직 정리
nonactress Jan 14, 2026
d4fc462
refactor(all) : 안쓰는 import 과 메소드 정리
nonactress Jan 14, 2026
fd5d1a5
refactor(all) : 안쓰는 import 과 메소드 정리
nonactress Jan 14, 2026
7c953c7
refactor(all) : 안쓰는 import 과 메소드 정리2
nonactress Jan 14, 2026
ecf4071
feat : Auth 패키지 설정 및 jwtConfig 생성
nonactress Jan 14, 2026
c7303bc
feat : 7단계 테스트 통과
nonactress Jan 14, 2026
1bfd9e3
feat : TestDataLoader 생성
nonactress Jan 14, 2026
3e72013
feat : application-key.properties 생성 및 숨김 처리
nonactress Jan 14, 2026
62a0229
fix : ProductionDataLoader 와 TestDataLoader 주입 값 수정
nonactress Jan 14, 2026
6de128e
fix : 8단계 통과
nonactress Jan 14, 2026
d488aff
fix : 배포 파일 작성
nonactress Jan 14, 2026
8bd9234
fix : 소프트 딜리트 관련 삭제 방어코드
nonactress Jan 22, 2026
c5bdf60
fix : 토큰 만료 시간 로직 중복
nonactress Jan 22, 2026
216d01e
fix : 중복 로직 삭제
nonactress Jan 23, 2026
4c169c7
fix : 역할 enum 관리
nonactress Jan 23, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ out/

### VS Code ###
.vscode/

**/application-key.properties
10 changes: 7 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@ repositories {
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 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-gson:0.11.2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testImplementation 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'




runtimeOnly 'com.h2database:h2'
}
Expand Down
42 changes: 42 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

현진님이 작성해주신 배포스크립트를 활용하면 어떻게 배포할 수 있나요?

즉, 이 배포 스크립트 실행 전에 해야할 사전 작업이 있나요?

Copy link
Copy Markdown
Author

@nonactress nonactress Jan 23, 2026

Choose a reason for hiding this comment

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

일단 EC2 기준으로 기본 사전 작업에 대해 생각해보면

  1. 서버에 jdk 설치 ( 버전이 맞도록 설치)
    2.git pull origin main 을 사용하므로 git 설치도 해야합니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

그렇군요! 👍👍👍

몇 가지 더 사전작업이 필요할 것 같아요.

  • Git 저장소 존재 여부 확인 및 클론
  • PID 파일 경로 설정 확인

등이 더 필요할 것 같아보여요.

배포스크립트 목적에 따라 사전 작업 부분들까지 스크립트에 추가해도 좋을 것 같네요!

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.

아아 넵 위 두가지 사전작업도 기억해놓겠습니다!!

echo "=========================================="
echo "배포 시작"
echo "=========================================="

echo ">>> Git Pull"
git pull origin main

if [ $? -ne 0 ]; then
echo "Git Pull 실패"
exit 1
fi

echo ">>> 프로젝트 빌드 시작"
./gradlew clean build

if [ $? -ne 0 ]; then
echo "빌드 실패"
exit 1
fi

echo ">>> 실행 중인 애플리케이션 확인"
CURRENT_PID=$(pgrep -f roomescape)

if [ -z "$CURRENT_PID" ]; then
echo ">>> 실행 중인 애플리케이션이 없습니다."
else
echo ">>> 애플리케이션 종료 (PID: $CURRENT_PID)"
kill -15 $CURRENT_PID
sleep 5
fi

echo ">>> 새 애플리케이션 실행"
nohup java -jar build/libs/roomescape-0.0.1-SNAPSHOT.jar > application.log 2>&1 &

sleep 3

NEW_PID=$(pgrep -f roomescape)
echo ">>> 배포 완료 (PID: $NEW_PID)"

echo "=========================================="
50 changes: 50 additions & 0 deletions src/main/java/auth/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;


public class JwtTokenProvider {
private final String secretKey;
private final long validityInMilliseconds;

public JwtTokenProvider(String secretKey, long validityInMilliseconds) {
this.secretKey = secretKey;
this.validityInMilliseconds = validityInMilliseconds;
}

public String createToken(String payload) {
Claims claims = Jwts.claims().setSubject(payload);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

public String getPayload(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}

public boolean validateToken(String token) {
try {
Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
return true;

} catch (JwtException | IllegalArgumentException e) {

return false;
}
}
}

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

This file was deleted.

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

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import roomescape.member.Member;
import roomescape.member.MemberRepository;
import roomescape.member.Role;
import roomescape.reservation.ReservationRepository;
import roomescape.theme.Theme;
import roomescape.theme.ThemeRepository;
import roomescape.time.Time;
import roomescape.time.TimeRepository;

@Component
public class ProductionDataLoader implements CommandLineRunner {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

.sql 파일을 활용하는 대신 CommandLineRunner를 활용했을 때 어떤 차이가 있었나요?

장단점은 있을까요?

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.

sql


장점 - 퀴리 문으로 작동해서 매우 빠르다
단점 - 퀴리 문법을 알아야 한다....

CommandLineRunner


장점 - 자바 로직으로 데이터를 만들 수 있다, 복잡한 엔티티 연관관계 보다 쉽게 만들 수 있다!
단점 - 속도가 느리다....

차이는 속도적인 차이가 존재하고 sql 엔티티 연관관계 구조를 매번 적용 시켜줘야 한다는 단점이 있는 거 같습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

오호 그렇군요~ 차이를 공유해주셔서 감사합니다.

제가 경험했을 때는 sql문을 사용하다보면 테스트가 변경될 때마다 매번 복잡한 쿼리문을 작성하느라 까다로웠던 것 같아요.
서비스 크기가 크지 않다면 문제 없지만 점점 커질 수록 다루기 번거롭더라구요. 다만 데이터 대량 적재를 해야했던 상황에서 활용하기 편했던 적도 있었던 것 같아요.

어떤 상황에 테스트 더미 데이터를 적재하냐에 따라서 방법이 달라지는 것 같네요 👍


private final MemberRepository memberRepository;
private final ThemeRepository themeRepository;
private final TimeRepository timeRepository;
private final ReservationRepository reservationRepository;

public ProductionDataLoader(
MemberRepository memberRepository,
ThemeRepository themeRepository,
TimeRepository timeRepository,
ReservationRepository reservationRepository
) {
this.memberRepository = memberRepository;
this.themeRepository = themeRepository;
this.timeRepository = timeRepository;
this.reservationRepository = reservationRepository;
}

@Override
@Transactional
public void run(String... args) throws Exception {
// 1. 관리자 계정 생성
if (memberRepository.findByEmail("admin").isEmpty()) {
Member admin = new Member("admin", "admin", "admin", Role.ADMIN);
memberRepository.save(admin);
System.out.println("관리자 계정이 생성되었습니다.");
}

// 2. 테마 데이터 생성
if (themeRepository.count() == 0) {
Theme theme1 = new Theme("테마1", "테마1입니다.");
Theme theme2 = new Theme("테마2", "테마2입니다.");
Theme theme3 = new Theme("테마3", "테마3입니다.");

themeRepository.save(theme1);
themeRepository.save(theme2);
themeRepository.save(theme3);
System.out.println("테마 데이터가 생성되었습니다.");
}

// 3. 시간 데이터 생성
if (timeRepository.count() == 0) {
Time time1 = new Time("10:00");
Time time2 = new Time("12:00");
Time time3 = new Time("14:00");
Time time4 = new Time("16:00");
Time time5 = new Time("18:00");
Time time6 = new Time("20:00");

timeRepository.save(time1);
timeRepository.save(time2);
timeRepository.save(time3);
timeRepository.save(time4);
timeRepository.save(time5);
timeRepository.save(time6);
System.out.println("시간 데이터가 생성되었습니다.");
}
}
}
108 changes: 108 additions & 0 deletions src/main/java/roomescape/Loader/TestDataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package roomescape.Loader;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import roomescape.member.Member;
import roomescape.member.MemberRepository;
import roomescape.member.Role;
import roomescape.reservation.Reservation;
import roomescape.reservation.ReservationRepository;
import roomescape.theme.Theme;
import roomescape.theme.ThemeRepository;
import roomescape.time.Time;
import roomescape.time.TimeRepository;

@Profile("test")
@Component
public class TestDataLoader implements CommandLineRunner {

private final MemberRepository memberRepository;
private final ThemeRepository themeRepository;
private final TimeRepository timeRepository;
private final ReservationRepository reservationRepository;

public TestDataLoader(
MemberRepository memberRepository,
ThemeRepository themeRepository,
TimeRepository timeRepository,
ReservationRepository reservationRepository
) {
this.memberRepository = memberRepository;
this.themeRepository = themeRepository;
this.timeRepository = timeRepository;
this.reservationRepository = reservationRepository;
}

@Override
@Transactional
public void run(String... args) throws Exception {
// 1. 관리자 계정 생성
if (memberRepository.findByEmail("admin").isEmpty()) {
Member admin = new Member("admin", "admin", "admin", Role.ADMIN);
memberRepository.save(admin);
System.out.println("관리자 계정이 생성되었습니다.");
}

// 2. 테마 데이터 생성
if (themeRepository.count() == 0) {
Theme theme1 = new Theme("테마1", "테마1입니다.");
Theme theme2 = new Theme("테마2", "테마2입니다.");
Theme theme3 = new Theme("테마3", "테마3입니다.");

themeRepository.save(theme1);
themeRepository.save(theme2);
themeRepository.save(theme3);
System.out.println("테마 데이터가 생성되었습니다.");
}

// 3. 시간 데이터 생성
if (timeRepository.count() == 0) {
Time time1 = new Time("10:00");
Time time2 = new Time("12:00");
Time time3 = new Time("14:00");
Time time4 = new Time("16:00");
Time time5 = new Time("18:00");
Time time6 = new Time("20:00");

timeRepository.save(time1);
timeRepository.save(time2);
timeRepository.save(time3);
timeRepository.save(time4);
timeRepository.save(time5);
timeRepository.save(time6);
System.out.println("시간 데이터가 생성되었습니다.");
}

if (reservationRepository.count() == 0) {
Member admin = memberRepository.findByEmail("admin")
.orElseThrow(() -> new RuntimeException("Admin not found"));

Time time1 = timeRepository.findById(1L).orElseThrow();
Time time2 = timeRepository.findById(2L).orElseThrow();
Time time3 = timeRepository.findById(3L).orElseThrow();

Theme theme1 = themeRepository.findById(1L).orElseThrow();
Theme theme2 = themeRepository.findById(2L).orElseThrow();
Theme theme3 = themeRepository.findById(3L).orElseThrow();

Reservation reservation1 = new Reservation("", "2024-03-01", time1, theme1, admin);
Reservation reservation2 = new Reservation("", "2024-03-01", time2, theme2, admin);
Reservation reservation3 = new Reservation("", "2024-03-01", time3, theme3, admin);

reservationRepository.save(reservation1);
reservationRepository.save(reservation2);
reservationRepository.save(reservation3);

Reservation reservation4 = new Reservation("브라운", "2024-03-01", time1, theme2);

reservationRepository.save(reservation4);
System.out.println("예약 데이터가 생성되었습니다.");
}

System.out.println("초기 데이터 로딩이 완료되었습니다.");
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package roomescape.exception;

public class AuthenticationException extends RuntimeException {

public AuthenticationException(String message) {
super(message);
}
}
4 changes: 4 additions & 0 deletions src/main/java/roomescape/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package roomescape.exception;

public record ErrorResponse(String message) {
}
36 changes: 36 additions & 0 deletions src/main/java/roomescape/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package roomescape.exception;

import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(e.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
return ResponseEntity.internalServerError()
.body(new ErrorResponse("오류가 발생했습니다."));
}

@ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity<ErrorResponse> handleExpiredJwtException(ExpiredJwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("토큰 만료 시간 초과"));
}
}

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

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Retention 어노테이션은 무엇인가요?

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.

위 어노테이션은 언제까지 살아남아 있을지를 결정하는 설정을 도와주는 어노테이션 입니다!

타입

SOURCE: 소스 코드(.java)에만 존재하며, 컴파일 시점에 제거됩니다. (예: @OverRide)

CLASS: 컴파일된 클래스 파일(.class)까지 유지되지만, 런타임에는 읽을 수 없습니다. (기본값)

RUNTIME: 실행 중에도 유지되어 리플렉션을 통해 정보를 읽을 수 있습니다.

주로 RUNTIME을 사용하는 이유

스프링이 실행 시점에 클래스를 스캔하여 빈(Bean)을 자동으로 등록하거나 의존성을 주입하려면, 런타임까지 어노테이션 정보가 살아있어야 하기 때문입니다.

public @interface AuthMember {
}
Loading