Skip to content
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
f1c90f6
docs: 요구사항 명세 작성
ljhee92 Jun 18, 2025
e8da869
docs: api 명세서 작성
ljhee92 Jun 18, 2025
c853262
chore: jpa, lombok 의존성 추가
ljhee92 Jun 18, 2025
d6d4c57
chore: h2 설정 추가
ljhee92 Jun 18, 2025
df6167d
chore: 미사용 테스트 삭제
ljhee92 Jun 18, 2025
5ef1950
feat: 사용자 도메인 생성
ljhee92 Jun 18, 2025
37973db
chore: validation 의존성 추가
ljhee92 Jun 18, 2025
d7b6735
feat: 비밀번호 일치 여부 확인 기능 구현
ljhee92 Jun 18, 2025
c198843
feat: 로그인 기능 구현
ljhee92 Jun 18, 2025
47829fe
chore: .gitignore 추가
ljhee92 Jun 18, 2025
66e91cb
feat: 로그인 인증 정보 확인 기능 구현
ljhee92 Jun 18, 2025
97ae716
feat: 네이버 API 활용 도서 검색 기능 구현
ljhee92 Jun 18, 2025
30aca00
feat: 예약 가능 도서 등록 기능 구현
ljhee92 Jun 18, 2025
0755f0f
feat: 예약 가능 도서 조회 기능 구현
ljhee92 Jun 18, 2025
0381165
refactor: 도메인 필드 접근제어자 수정
ljhee92 Jun 18, 2025
4454065
feat: 도서 예약 기능 구현
ljhee92 Jun 18, 2025
420030b
feat: 예약한 도서 리스트 조회 기능 구현
ljhee92 Jun 18, 2025
6ff5386
feat: 예약한 도서 상세 정보 조회 기능 구현
ljhee92 Jun 18, 2025
2bd79d9
feat: 예약한 도서 수정 기능 구현
ljhee92 Jun 18, 2025
55d2694
feat: 예약한 도서 삭제 기능 구현
ljhee92 Jun 18, 2025
2aad62a
docs: 만족시킨 필수 기능, 프로그래밍 요구사항 체크
ljhee92 Jun 18, 2025
9661559
test: 통합 테스트 추가
ljhee92 Jun 18, 2025
4f20b17
docs: 미션 시간 수정
ljhee92 Jun 18, 2025
b840f5a
refactor: 중복 response 수정
ljhee92 Jun 18, 2025
f6bf6ab
fix: 도서 예약 시 예약가능 수량 조절
ljhee92 Jun 18, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
secure.properties
.idea
*.iws
*.iml
Expand Down
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-gson:0.11.2'

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

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-validation'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
Expand Down
35 changes: 35 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 최종 미션

## 미션 목표
- AI 도구 미사용
- 배운 내용을 토대로 서비스 구현
- 미션 시간: 6/18 13:00 ~ 19:55(1차 6시간 55분), 21:10 ~ 21:50(2차 40분), 22:28 ~ 22:38(3차 10분) 총 7시간 45분

## 📚듀이의 도서 예약 서비스

### 필수 기능
- [x] 사용자는 특정 대상(회의실, 맛집, 클래스 등)을 예약할 수 있다.
- [x] 모든 사용자는 예약 현황을 확인할 수 있다.
- [x] 사용자 본인은 자신이 한 예약의 상세 정보까지 확인할 수 있다.
- [x] 사용자는 본인의 예약만 수정하고 삭제할 수 있다.

### 프로그래밍 요구사항
- [x] H2 활용
- [x] TDD
- [x] 외부 API 연동 - [네이버 도서 검색 API](https://developers.naver.com/docs/serviceapi/search/book/book.md)

### 기능 목록
1. 로그인
- [x] 관리자와 사용자는 email, password를 사용하여 로그인한다.

2. 도서 관리
- [x] 관리자는 네이버 도서 검색으로 도서를 검색할 수 있다.
- [x] 관리자는 예약 가능한 도서를 등록할 수 있다.

3. 예약
- [x] 사용자는 예약 가능한 도서를 확인할 수 있다.
- [x] 사용자는 도서를 예약할 수 있다.
- [x] 사용자는 자신의 예약 리스트를 확인할 수 있다.
- [x] 사용자는 자신의 예약 상세 정보를 확인할 수 있다.
- [x] 사용자는 본인의 예약을 수정할 수 있다.
- [x] 사용자는 본인의 예약을 취소할 수 있다.
286 changes: 286 additions & 0 deletions docs/apidocs.md

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions src/main/java/finalmission/application/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package finalmission.application;

import finalmission.domain.User;
import finalmission.dto.request.LoginRequest;
import finalmission.dto.request.LoginUser;
import finalmission.infrastructure.jwt.JwtTokenProvider;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

private final JwtTokenProvider jwtTokenProvider;
private final UserService userService;

public AuthService(
JwtTokenProvider jwtTokenProvider,
UserService userService
) {
this.jwtTokenProvider = jwtTokenProvider;
this.userService = userService;
}

public String login(LoginRequest request) {
User user = userService.findByEmail(request.email());
userService.checkPassword(user, request.password());
LoginUser loginUser = new LoginUser(user.getEmail(), user.getName(), user.getRole());
return jwtTokenProvider.createToken(loginUser);
}

public LoginUser findLoginUserByToken(String token) {
String emailFromToken = jwtTokenProvider.getEmailFromToken(token);
User user = userService.findByEmail(emailFromToken);
return new LoginUser(user.getEmail(), user.getName(), user.getRole());
}
}
68 changes: 68 additions & 0 deletions src/main/java/finalmission/application/BookService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package finalmission.application;

import finalmission.domain.Book;
import finalmission.domain.Keyword;
import finalmission.dto.request.BookCreateRequest;
import finalmission.dto.response.BookCreateResponse;
import finalmission.dto.response.BookResponse;
import finalmission.dto.response.NaverBookResponses;
import finalmission.infrastructure.thirdparty.ApiRestClient;
import finalmission.repository.BookRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.NoSuchElementException;

@Service
public class BookService {

private final ApiRestClient apiRestClient;
private final BookRepository bookRepository;

public BookService(
ApiRestClient apiRestClient,
BookRepository bookRepository
) {
this.apiRestClient = apiRestClient;
this.bookRepository = bookRepository;
}

public List<BookResponse> searchBooks(String keyword) {
NaverBookResponses naverBookResponses = apiRestClient.searchBooks(Keyword.from(keyword));
return naverBookResponses.items()
.stream()
.map(BookResponse::from)
.toList();
}

@Transactional
public BookCreateResponse registerBook(BookCreateRequest request) {
Book bookWithoutId = createBook(request);
Book bookWithId = bookRepository.save(bookWithoutId);
return BookCreateResponse.from(bookWithId);
}

private Book createBook(BookCreateRequest request) {
return Book.createBook(
request.title(),
request.author(),
request.image(),
request.publisher(),
request.pubdate(),
request.isbn(),
request.description(),
request.totalCount(),
request.regDate()
);
}

public List<Book> findAll() {
return bookRepository.findAll();
}

public Book findById(Long bookId) {
return bookRepository.findById(bookId)
.orElseThrow(() -> new NoSuchElementException("[ERROR] 존재하지 않는 도서입니다."));
}
}
98 changes: 98 additions & 0 deletions src/main/java/finalmission/application/ReservationService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package finalmission.application;

import finalmission.domain.Book;
import finalmission.domain.Reservation;
import finalmission.domain.User;
import finalmission.dto.request.ReservationCreateRequest;
import finalmission.dto.response.AvailableBookResponse;
import finalmission.dto.response.MyReservationDetailResponse;
import finalmission.dto.response.MyReservationResponse;
import finalmission.dto.response.ReservationCreateResponse;
import finalmission.repository.ReservationRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.NoSuchElementException;

@Service
public class ReservationService {

private final BookService bookService;
private final UserService userService;
private final ReservationRepository reservationRepository;

public ReservationService(
BookService bookService,
UserService userService,
ReservationRepository reservationRepository
) {
this.bookService = bookService;
this.userService = userService;
this.reservationRepository = reservationRepository;
}

public List<AvailableBookResponse> getAvailableBooks() {
List<Book> books = bookService.findAll();
return books.stream()
.map(AvailableBookResponse::from)
.toList();
}

@Transactional
public ReservationCreateResponse reserveBook(String email, ReservationCreateRequest request) {
User user = userService.findByEmail(email);
Book book = bookService.findById(request.bookId());
book.checkAvailableCount();

Reservation reservationWithoutId = Reservation.createReservation(
user, book, request.reserveDate(), request.reserveTime()
);
book.adjustAvailableCount(1);
Reservation reservationWithId = reservationRepository.save(reservationWithoutId);
return ReservationCreateResponse.from(reservationWithId);
}

public List<MyReservationResponse> getReservations(String email) {
User user = userService.findByEmail(email);
List<Reservation> reservations = reservationRepository.findByUser_Id(user.getId());
return reservations.stream()
.map(MyReservationResponse::from)
.toList();
}

public MyReservationDetailResponse getReservation(String email, Long reservationId) {
User user = userService.findByEmail(email);
Reservation reservation = findById(reservationId);
validateUserOfReservation(reservation, user);
return MyReservationDetailResponse.from(reservation);
}

public Reservation findById(Long id) {
return reservationRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("[ERROR] 예약 정보가 없습니다."));
}

private void validateUserOfReservation(Reservation reservation, User user) {
if (!reservation.isSameUser(user)) {
throw new NoSuchElementException("[ERROR] 예약 정보와 사용자 정보가 다릅니다.");
}
}

@Transactional
public MyReservationDetailResponse extendReservation(String email, Long reservationId) {
User user = userService.findByEmail(email);
Reservation reservation = findById(reservationId);
validateUserOfReservation(reservation, user);
reservation.extendReturnDate();
return MyReservationDetailResponse.from(reservation);
}

@Transactional
public void cancelReservation(String email, Long reservationId) {
User user = userService.findByEmail(email);
Reservation reservation = findById(reservationId);
validateUserOfReservation(reservation, user);
reservationRepository.delete(reservation);
}
}
32 changes: 32 additions & 0 deletions src/main/java/finalmission/application/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package finalmission.application;

import finalmission.domain.User;
import finalmission.domain.UserEmail;
import finalmission.domain.UserPassword;
import finalmission.repository.UserRepository;
import org.springframework.stereotype.Service;

import java.util.NoSuchElementException;

@Service
public class UserService {

private final UserRepository userRepository;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public User findByEmail(String email) {
UserEmail userEmail = UserEmail.from(email);
return userRepository.findByEmail(userEmail)
.orElseThrow(() -> new NoSuchElementException("[ERROR] 해당 이메일의 사용자가 존재하지 않습니다."));
}

public void checkPassword(User user, String password) {
UserPassword userPassword = UserPassword.from(password);
if (!user.isSamePassword(userPassword)) {
throw new IllegalArgumentException("[ERROR] 잘못된 비밀번호입니다.");
}
}
}
24 changes: 24 additions & 0 deletions src/main/java/finalmission/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package finalmission.config;

import finalmission.application.AuthService;
import finalmission.presentation.AuthenticationPrincipalResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Component
public class WebMvcConfig implements WebMvcConfigurer {

private final AuthService authService;

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

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticationPrincipalResolver(authService));
}
}
Loading