Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 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
6d27e4c
refactor : syso -> log.info 로 수정
nonactress Jan 9, 2026
3a50097
refactor : 메소드 delete로 수정
nonactress Jan 9, 2026
4178346
fix : readOnly로 수정
nonactress Jan 9, 2026
dcdd017
fix : 정적 펙토리 메소드를 사용
nonactress Jan 9, 2026
c7a6a64
DEL : 주석 정리
nonactress Jan 9, 2026
4252f96
DEL : import문 제거
nonactress Jan 9, 2026
e948375
fix : 닷 기준 메소드 이쁘게 배치
nonactress Jan 13, 2026
bb539bd
fix : service에서 return dto -> return reservaitonId로 변경
nonactress Jan 13, 2026
c9f6c0d
Update src/main/java/roomescape/member/MemberController.java
nonactress Jan 13, 2026
841a720
fix : 체인닝 메소드 정리
nonactress Jan 13, 2026
47e66f3
fix : dao 파일 정리 및 NPE 방어 코드 작성
nonactress Jan 13, 2026
941299b
fix : 비로그인 login/check 상태코드 401로 수정
nonactress Jan 13, 2026
42fe9b0
refactor : setter 삭제 및 안쓰는 메소드 삭제
nonactress Jan 13, 2026
658a29b
refactor : NotFoundException 적용
nonactress Jan 13, 2026
d10537a
Merge branch 'hyeonjin2' of https://github.com/nonactress/spring-basi…
nonactress Jan 13, 2026
e13ba3b
refactor : 줄바꿈 정리
nonactress Jan 13, 2026
34debcb
refactor : 리포맷 적용
nonactress Jan 13, 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
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
4 changes: 4 additions & 0 deletions src/main/java/roomescape/advice/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package roomescape.advice;

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

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(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(RuntimeException e) {
return ResponseEntity.status(404)
.body(new ErrorResponse(e.getMessage()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
return ResponseEntity.internalServerError()
.body(new ErrorResponse("오류가 발생했습니다."));
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/advice/NotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.advice;

public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
11 changes: 11 additions & 0 deletions src/main/java/roomescape/auth/AuthMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package roomescape.auth;

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)
public @interface AuthMember {
}
106 changes: 106 additions & 0 deletions src/main/java/roomescape/infrastructure/DataInitializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package roomescape.infrastructure;

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.reservation.Reservation;
import roomescape.reservation.ReservationRepository;
import roomescape.theme.Theme;
import roomescape.theme.ThemeRepository;
import roomescape.time.Time;
import roomescape.time.TimeRepository;

@Component
public class DataInitializer implements CommandLineRunner {

private final MemberRepository memberRepository;
private final ThemeRepository themeRepository;
private final TimeRepository timeRepository;
private final ReservationRepository reservationRepository;
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DataInitializer.class);

public DataInitializer(
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", "ADMIN");
Member user = new Member("user", "user", "user", "USER");
memberRepository.save(admin);
memberRepository.save(user);
log.info("관리자 계정이 생성되었습니다.");
}

// 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);
log.info("테마 데이터가 생성되었습니다.");
}

// 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);
log.info("시간 데이터가 생성되었습니다.");
}

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);
log.info("예약 데이터가 생성되었습니다.");
}

log.info("초기 데이터 로딩이 완료되었습니다.");
}
}
52 changes: 52 additions & 0 deletions src/main/java/roomescape/infrastructure/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.infrastructure;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {
@Value("${security.jwt.token.secret-key:this-is-a-sample-secret-key-at-least-32-bytes-long}")
private String secretKey;
@Value("${security.jwt.token.expire-length:3600000}")
private long 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 {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);

return !claims.getBody()
.getExpiration()
.before(new Date());

} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}

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

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

@Column(nullable = false, unique = true)
private String email;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String role;

@Column(name = "deleted")
Copy link

Choose a reason for hiding this comment

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

여기만 이름을 명시해둔 이유가 있나요?!

Copy link
Author

Choose a reason for hiding this comment

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

따로 이유는 없고 다른 필드들 nullable처리를 해서 이름도 이렇게 설정하는 구나 해서 넣어봤습니다!

private boolean deleted = false;
Copy link

Choose a reason for hiding this comment

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

delete 종류도 soft delete, hard delete가 있는데, 현진님이 soft delete를 선택한 이유는 무엇인가요?

Copy link
Author

Choose a reason for hiding this comment

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

만약 member가 삭제되더라도 reservation 값들을 가지고 있어야 하는데 만약 hard delete를 이용하면 member가 없어짐에 따라reservation에 있는 member_fk가 가르키는 값이 없어지게 될 수 있으므로 soft delete를 사용했습니다!

Copy link

@70825 70825 Jan 9, 2026

Choose a reason for hiding this comment

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

그러면 reservation 데이터도 같이 없어지면 되는거 아닌가라는 생각이 드네요
그런 의미로 cascade 옵션도 찾아보시면 좋아보입니다 ㅋㅋㅋ (반영은 선택적으로..)

제가 같이 없어지면 되는게 아닌가라고 생각하는 이유
= 사용자가 예약 했다가, 탈퇴하면 예약 정보도 당연히 사라지는게 아닐까라는 생각


protected Member() {
}

public Member(Long id, String name, String email, String role) {
this.id = id;
this.name = name;
Expand Down Expand Up @@ -40,4 +65,8 @@ public String getPassword() {
public String getRole() {
return role;
}

public void delete() {
this.deleted = true;
}
}
36 changes: 35 additions & 1 deletion src/main/java/roomescape/member/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package roomescape.member;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.auth.AuthMember;

import java.net.URI;

Expand All @@ -19,6 +20,39 @@ public MemberController(MemberService memberService) {
this.memberService = memberService;
}

@PostMapping("/login")
public ResponseEntity<Void> login(
@RequestBody MemberRequest memberRequest,
HttpServletResponse response
) {

String tokenValue = memberService.login(memberRequest);

Cookie cookie = new Cookie("token", tokenValue);
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(3600);
response.addCookie(cookie);

return ResponseEntity.ok()
.header("Keep-Alive", "timeout=60")
.build();
}

@GetMapping("/login/check")
public ResponseEntity<MemberResponse> check(
@AuthMember Member member
) {
if (member == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

return ResponseEntity.ok()
.header("Connection", "keep-alive")
.header("Keep-Alive", "timeout=60")
Comment on lines +51 to +52
Copy link

Choose a reason for hiding this comment

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

요기 궁금한 부분이 있는데요
keep-alive 헤더 설정한 이유가 있나요?

Copy link
Author

@nonactress nonactress Jan 13, 2026

Choose a reason for hiding this comment

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

/login/login/check 이후에는 추가적으로 통신하는 작업이 거의 필수적이라고 생각해서 connection을 유지 할 수 있도록 해보았습니다!

.body(new MemberResponse(null, member.getName(), null));
}

@PostMapping("/members")
public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) {
MemberResponse member = memberService.createMember(memberRequest);
Expand Down
55 changes: 0 additions & 55 deletions src/main/java/roomescape/member/MemberDao.java

This file was deleted.

Loading