Skip to content

Commit

Permalink
[1 - 2단계 방탈출 예약 대기] 에버(손채영) 미션 제출합니다. (#72)
Browse files Browse the repository at this point in the history
* migrate previous code

* refactor(all): JPA 전환

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* feat(client): 자신의 예약 조회 페이지

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* feat(PageController): 자신의 예약 조회 페이지 반환

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* feat(ReservationController): 자신의 예약 조회 API

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* refactor(Member): 불필요한 생성자 삭제

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* refactor(MemberRepository): 불필요한 메서드 삭제

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* refactor(Member): 리턴 타입 스트링으로 통일

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* refactor(Theme): 불필요한 생성자 삭제

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* test(ReservationRepository): 테스트 보완

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* test(ReservationRepository): 특정 멤버 예약 조회

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* refactor(ReservationTimeDto): 주생성자 활용

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* test(ReservationService): 테스트 보완

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* refactor(test): assertAll 활용

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* test(ReservationController): 자신의 예약 조회

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* docs(README): 기능 구현사항 반영

Co-authored-by: hyxrxn <gpfls981220@gmail.com>

* fix(model): 엔티티의 필드 검증

* chore(properties): SQL 쿼리 파라미터 활성화

* refactor(Reservation): 조인 시 지연 로딩

* refactor(Reservation): 필드에 null 들어가지 않도록

* refactor(member): Member 클래스 내부에서 사용되는 클래스 이름 변경

* refactor(model): PK를 통해 객체의 동등성 비교하도록

* refactor(repository): 불필요한 어노테이션 삭제

* test(ReservationService): 현재 시간 저장 로직 위치 이동

* style(all): 불필요한 import문 삭제

* chore(properties): transaction 로그 추가

* refactor(model): PK 타입 원시 타입에서 참조 타입으로 변경

* refactor(model): 불필요한 생성자 제거 및 형식 통일

* fix(ReservationServiceTest): 나노초로 인한 에러 해결

* refactor(Reservation): 불필요한 로직 제거

* style(ThemeServiceTest): 불필요한 import문 삭제

---------

Co-authored-by: hyxrxn <gpfls981220@gmail.com>
  • Loading branch information
helenason and hyxrxn authored May 21, 2024
1 parent fbd90e8 commit b63dc11
Show file tree
Hide file tree
Showing 107 changed files with 6,553 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.gitmessage.txt

HELP.md
.gradle
build/
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## 기능 요구사항

### 예외 처리
- [x] 시간 생성 시 시작 시간에 유효하지 않은 값이 입력되었을 때
- [x] null, "", HH:mm이 아닌 경우
- [x] 사용자의 예약 생성 시 날짜, 시간 id, 테마 id에 유효하지 않은 값이 입력 되었을 때
- [x] 날짜: null, "", yyyy-MM-dd이 아닌 경우
- [x] 시간 id: null, 1 이상이 아닌 경우
- [x] 테마 id: null, 1 이상이 아닌 경우
- [x] 관리자의 예약 생성 시 날짜, 시간 id, 테마 id, 예약자 id에 유효하지 않은 값이 입력 되었을 때
- [x] 날짜: null, "", yyyy-MM-dd이 아닌 경우
- [x] 시간 id: null, 1 이상이 아닌 경우
- [x] 테마 id: null, 1 이상이 아닌 경우
- [x] 예약자 id: null, 1 이상이 아닌 경우
- [x] 특정 시간에 대한 예약이 존재하는데, 그 시간을 삭제하려 할 때
- [x] 특정 테마에 대한 예약이 존재하는데, 그 테마를 삭제하려 할 때
- [x] 존재하지 않는 id를 가진 데이터에 접근하려 할 때
- [x] 지나간 날짜와 시간에 대한 예약을 생성하려 할 때
- [x] 중복 예약을 생성하려 할 때
- [x] 중복 예약 시간을 생성하려 할 때
- [x] 이름이 중복된 테마를 생성하려 할 때

### 예약 시간
- [x] 관리자 시간 관리 페이지 조회
- [x] 시간 추가
- [x] 시간 삭제
- [x] 시간 조회

### 테마
- [x] 관리자 테마 관리 페이지 조회
- [x] 테마 추가
- [x] 테마 삭제
- [x] 테마 조회

### 사용자
- [x] 로그인 페이지 조회
- [x] 로그인
- [x] 로그아웃
- [x] 사용자 정보 조회
- [x] 사용자 목록 조회
- [x] 접근 권한 제어
- [x] 관리자 페이지(/admin/**) 진입은 권한이 있는 사람만 할 수 있도록 제한

### 예약
- [x] 관리자의 예약 추가
- [x] 관리자 예약 페이지 조회
- [x] 사용자의 예약 추가
- [X] 사용자 예약 페이지 조회
- [x] 테마와 날짜 선택 시 예약 가능 시간 조회
- [x] 로그인 사용자 정보 활용
- [x] 예약 추가
- [x] 인기 테마 조회 기능
- [x] 인기 테마 페이지 조회
- [x] 최근 일주일 기준, 해당 기간 내 예약이 많은 테마 10개 확인
- [x] 예약 목록 검색
- [x] 관리자가 조건에 따라 예약 검색
- [x] 예약자별, 테마별, 날짜별 검색 조건
- [x] 사용자의 예약 목록 조회
- [x] 로그인 사용자 정보 활용
12 changes: 5 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ repositories {
}

dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
implementation 'org.springframework.boot:spring-boot-starter'
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 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-gson:0.11.2'

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'com.h2database:h2'

runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
}
Expand Down
1 change: 0 additions & 1 deletion src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ public class RoomescapeApplication {
public static void main(String[] args) {
SpringApplication.run(RoomescapeApplication.class, args);
}

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

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import roomescape.controller.request.AdminReservationRequest;
import roomescape.controller.response.ReservationResponse;
import roomescape.model.Reservation;
import roomescape.service.ReservationService;
import roomescape.service.dto.ReservationDto;

import java.net.URI;

@RestController
@RequestMapping("/admin")
public class AdminController {

private final ReservationService reservationService;

public AdminController(ReservationService reservationService) {
this.reservationService = reservationService;
}

@PostMapping("/reservations")
public ResponseEntity<ReservationResponse> addReservation(@Valid @RequestBody AdminReservationRequest request) {
ReservationDto reservationDto = ReservationDto.from(request);
Reservation reservation = reservationService.saveReservation(reservationDto);
ReservationResponse response = ReservationResponse.from(reservation);
return ResponseEntity
.created(URI.create("/admin/reservations/" + response.getId()))
.body(response);
}
}
30 changes: 30 additions & 0 deletions src/main/java/roomescape/controller/AdminPageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package roomescape.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/admin")
public class AdminPageController {

@GetMapping
public String getHome() {
return "admin/index";
}

@GetMapping("/reservation")
public String getReservation() {
return "admin/reservation-new";
}

@GetMapping("/time")
public String getReservationTime() {
return "admin/time";
}

@GetMapping("/theme")
public String getTheme() {
return "admin/theme";
}
}
74 changes: 74 additions & 0 deletions src/main/java/roomescape/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package roomescape.controller;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import roomescape.controller.request.LoginRequest;
import roomescape.controller.response.LoginResponse;
import roomescape.exception.AuthorizationException;
import roomescape.service.AuthService;
import roomescape.service.dto.AuthDto;
import roomescape.service.dto.MemberInfo;

import java.util.Arrays;
import java.util.Optional;

@Controller
public class AuthController {

private static final String AUTH_COOKIE_KEY = "token";

private final AuthService authService;

public AuthController(AuthService authService) {
this.authService = authService;
}

@PostMapping("/login")
public ResponseEntity<Void> login(@Valid @RequestBody LoginRequest loginRequest) {
AuthDto authDto = AuthDto.from(loginRequest);
String token = authService.createToken(authDto);
ResponseCookie cookie = createCookie(token, 3600);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.build();
}

@GetMapping("/login/check")
public ResponseEntity<LoginResponse> checkLogin(HttpServletRequest request) {
Cookie token = findCookieByKey(request.getCookies(), AUTH_COOKIE_KEY).orElseThrow(AuthorizationException::new);
MemberInfo loginMember = authService.checkToken(token.getValue());
LoginResponse response = LoginResponse.from(loginMember);
return ResponseEntity.ok(response);
}

@PostMapping("/logout")
public ResponseEntity<Void> logout() {
ResponseCookie cookie = createCookie(null, 0);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.build();
}

private ResponseCookie createCookie(String token, long maxAge) {
return ResponseCookie
.from(AUTH_COOKIE_KEY, token)
.maxAge(maxAge)
.httpOnly(true)
.path("/")
.build();
}

private Optional<Cookie> findCookieByKey(Cookie[] cookies, String key) {
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(key))
.findFirst();
}
}
31 changes: 31 additions & 0 deletions src/main/java/roomescape/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import roomescape.controller.response.MemberResponse;
import roomescape.model.member.Member;
import roomescape.service.MemberService;

import java.util.List;

@RestController
@RequestMapping("/members")
public class MemberController {

private final MemberService memberService;

public MemberController(MemberService memberService) {
this.memberService = memberService;
}

@GetMapping
public ResponseEntity<List<MemberResponse>> getMembers() {
List<Member> members = memberService.findAllMembers();
List<MemberResponse> response = members.stream()
.map(MemberResponse::from)
.toList();
return ResponseEntity.ok(response);
}
}
28 changes: 28 additions & 0 deletions src/main/java/roomescape/controller/PageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package roomescape.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {

@GetMapping
public String getHome() {
return "index";
}

@GetMapping("/reservation")
public String getReservation() {
return "reservation";
}

@GetMapping("/login")
public String getLoginPage() {
return "login";
}

@GetMapping("/reservation-mine")
public String getMyReservationPage() {
return "reservation-mine";
}
}
85 changes: 85 additions & 0 deletions src/main/java/roomescape/controller/ReservationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package roomescape.controller;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import roomescape.controller.request.ReservationRequest;
import roomescape.controller.response.MemberReservationResponse;
import roomescape.controller.response.ReservationResponse;
import roomescape.controller.response.ReservationTimeInfoResponse;
import roomescape.model.Reservation;
import roomescape.model.member.LoginMember;
import roomescape.service.ReservationService;
import roomescape.service.dto.ReservationDto;
import roomescape.service.dto.ReservationTimeInfoDto;

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

@RestController
@RequestMapping("/reservations")
public class ReservationController {

private final ReservationService reservationService;

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

@GetMapping
public ResponseEntity<List<ReservationResponse>> getReservations() {
List<Reservation> reservations = reservationService.findAllReservations();
List<ReservationResponse> response = reservations.stream()
.map(ReservationResponse::from)
.toList();
return ResponseEntity.ok(response);
}

@PostMapping
public ResponseEntity<ReservationResponse> addReservation(@Valid @RequestBody ReservationRequest request, LoginMember member) {
ReservationDto reservationDto = ReservationDto.of(request, member);
Reservation reservation = reservationService.saveReservation(reservationDto);
ReservationResponse response = ReservationResponse.from(reservation);
return ResponseEntity
.created(URI.create("/reservations/" + response.getId()))
.body(response);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteReservation(@NotNull @Min(1) @PathVariable("id") Long id) {
reservationService.deleteReservation(id);
return ResponseEntity.noContent().build();
}

@GetMapping(value = "/times", params = {"date", "themeId"})
public ResponseEntity<List<ReservationTimeInfoResponse>> showReservationTimesInformation(@NotNull LocalDate date,
@NotNull @Min(1) Long themeId) {
ReservationTimeInfoDto timesInfo = reservationService.findReservationTimesInformation(date, themeId);
List<ReservationTimeInfoResponse> response = ReservationTimeInfoResponse.from(timesInfo);
return ResponseEntity.ok(response);
}

@GetMapping(value = "/filter", params = {"memberId", "themeId", "from", "to"})
public ResponseEntity<List<ReservationResponse>> searchReservations(@NotNull @Min(1) Long memberId,
@NotNull @Min(1) Long themeId,
@NotNull LocalDate from,
@NotNull LocalDate to) {
List<Reservation> responses = reservationService.findReservationsByConditions(memberId, themeId, from, to);
List<ReservationResponse> response = responses.stream()
.map(ReservationResponse::from)
.toList();
return ResponseEntity.ok(response);
}

@GetMapping("/mine")
public ResponseEntity<List<MemberReservationResponse>> getReservationsOfMember(LoginMember member) {
List<Reservation> reservations = reservationService.findReservationsByMember(member);
List<MemberReservationResponse> response = reservations.stream()
.map((MemberReservationResponse::new))
.toList();
return ResponseEntity.ok(response);
}
}
Loading

0 comments on commit b63dc11

Please sign in to comment.