Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 49 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'org.springframework.boot' version '3.4.4'
id 'io.spring.dependency-management' version '1.1.7'
id "org.asciidoctor.jvm.convert" version "3.3.2"
id 'java'
}

Expand All @@ -16,22 +17,67 @@ repositories {
mavenCentral()
}

configurations {
asciidoctorExt
}

dependencies {
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 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.apache.httpcomponents.client5:httpclient5:5.5'

implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-gson:0.11.2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

runtimeOnly 'com.h2database:h2'

asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
}

ext {
snippetsDir = file('build/generated-snippets')
}

test {
outputs.dir snippetsDir
}

asciidoctor {
dependsOn test
configurations 'asciidoctorExt'
baseDirFollowsSourceFile()
inputs.dir snippetsDir
}

asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}

tasks.register('copyDocument', Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}

bootJar {
dependsOn copyDocument
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}

test {
useJUnitPlatform()
}


26 changes: 26 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 술집 예약 서비스

## API 명세서
http://localhost:8080/docs/index.html

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

## 세부 기능
- [x] 회원 가입
- [x] 생년월일, 아이디, 비밀번호만 가능
- [x] 아이디는 중복 불가능
- [x] 성인만 회원 가입 가능
- [x] 닉네임은 외부 API를 통해서 랜덤으로 생성
- [x] 외부 API에 문제가 생길 경우 내부적으로 랜덤 값 부여
- [x] 로그인
- [x] JWT 토큰 사용
- [x] 아이디와 비밀번호 확인
- [x] 예약
- [x] 특정 시간에 대해서 가능
- [x] 만약 해당 시간에 이미 예약이 존재하는 경우 대기 상태
- [x] 예약 완료시 예약 성공 이메일 발송

11 changes: 11 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:doctype: book
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:

= 맛집 예약 API 명세서

include::member.adoc[]
include::login.adoc[]
include::reservation.adoc[]
4 changes: 4 additions & 0 deletions src/docs/asciidoc/login.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
== Login API

=== 로그인
operation::login-controller-test/login[snippets='curl-request,http-request,http-response']
4 changes: 4 additions & 0 deletions src/docs/asciidoc/member.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
== Member API

=== 회원가입
operation::member-controller-test/join[snippets='curl-request,http-request,http-response']
13 changes: 13 additions & 0 deletions src/docs/asciidoc/reservation.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
== Reservation API

=== 모든 예약 조회
operation::reservation-controller-test/get-all-reservations[snippets='curl-request,http-request,http-response']

=== 예약 생성
operation::reservation-controller-test/create-reservation[snippets='curl-request,http-request,http-response']

=== 내 예약 조회
operation::reservation-controller-test/get-my-reservation-info[snippets='curl-request,http-request,http-response']

=== 내 예약 삭제
operation::reservation-controller-test/delete-reservation[snippets='curl-request,http-request,http-response']
6 changes: 3 additions & 3 deletions src/main/java/finalmission/FinalMissionApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
@SpringBootApplication
public class FinalMissionApplication {

public static void main(String[] args) {
SpringApplication.run(FinalMissionApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(FinalMissionApplication.class, args);
}

}
32 changes: 32 additions & 0 deletions src/main/java/finalmission/common/config/RestClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package finalmission.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

@Bean
public RestClient restClient(RestClient.Builder builder) {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(2000);
factory.setReadTimeout(5000);

return builder.requestFactory(factory)
.build();
}

@Bean
public RestClient holidayRestClient(RestClient.Builder builder) {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(2000);
factory.setReadTimeout(5000);
builder.requestFactory(factory);

return builder.requestFactory(factory)
.baseUrl("http://apis.data.go.kr")
.build();
}
}
26 changes: 26 additions & 0 deletions src/main/java/finalmission/common/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package finalmission.common.config;

import finalmission.login.resolver.LoginArgumentResolver;
import finalmission.login.util.CookieManager;
import finalmission.login.util.JwtProvider;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final CookieManager cookieManager;
private final JwtProvider jwtProvider;

public WebConfig(CookieManager cookieManager, JwtProvider jwtProvider) {
this.cookieManager = cookieManager;
this.jwtProvider = jwtProvider;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginArgumentResolver(cookieManager, jwtProvider));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package finalmission.common.exception;

import finalmission.common.exception.dto.ErrorResponse;
import finalmission.login.exception.LoginException;
import jakarta.servlet.http.HttpServletRequest;
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,
HttpServletRequest request) {
return ResponseEntity.badRequest().body(new ErrorResponse(request.getRequestURI(), e.getMessage()));
}

@ExceptionHandler(LoginException.class)
public ResponseEntity<ErrorResponse> handleLoginException(LoginException e, HttpServletRequest request) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse(request.getRequestURI(), e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package finalmission.common.exception.dto;

public record ErrorResponse(
String uri,
String errorMessage
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package finalmission.login.dto.request;

public record LoginRequest(String email, String password) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package finalmission.login.exception;

public class LoginException extends RuntimeException {
public LoginException(String message) {
super(message);
}
}
33 changes: 33 additions & 0 deletions src/main/java/finalmission/login/presentation/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package finalmission.login.presentation;

import finalmission.login.dto.request.LoginRequest;
import finalmission.login.service.LoginService;
import finalmission.login.util.CookieManager;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
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.RestController;

@Slf4j
@RestController
public class LoginController {

private final LoginService loginService;
private final CookieManager cookieManager;

public LoginController(LoginService loginService, CookieManager cookieManager) {
this.loginService = loginService;
this.cookieManager = cookieManager;
}

@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
log.info("로그인 시도 email = {}", loginRequest.email());
String accessToken = loginService.loginAndReturnAccessToken(loginRequest);
cookieManager.addAccessToken(accessToken, response);
log.info("로그인 성공 email = {}", loginRequest.email());
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package finalmission.login.resolver;

import finalmission.login.util.CookieManager;
import finalmission.login.util.JwtProvider;
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;

public class LoginArgumentResolver implements HandlerMethodArgumentResolver {

private final CookieManager cookieManager;
private final JwtProvider jwtProvider;

public LoginArgumentResolver(CookieManager cookieManager, JwtProvider jwtProvider) {
this.cookieManager = cookieManager;
this.jwtProvider = jwtProvider;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasAnnotation = parameter.hasParameterAnnotation(LoginMember.class);
boolean hasLongType = Long.class.isAssignableFrom(parameter.getParameterType());

return hasAnnotation && hasLongType;
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String accessToken = cookieManager.getAccessToken(request);
return jwtProvider.extractIdFromAccessToken(accessToken);
}
}
11 changes: 11 additions & 0 deletions src/main/java/finalmission/login/resolver/LoginMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package finalmission.login.resolver;

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 LoginMember {
}
30 changes: 30 additions & 0 deletions src/main/java/finalmission/login/service/LoginService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package finalmission.login.service;

import finalmission.login.dto.request.LoginRequest;
import finalmission.login.exception.LoginException;
import finalmission.login.util.JwtProvider;
import finalmission.member.domain.Email;
import finalmission.member.domain.Member;
import finalmission.member.domain.MemberRepository;
import org.springframework.stereotype.Service;

@Service
public class LoginService {

private final MemberRepository memberRepository;
private final JwtProvider jwtProvider;

public LoginService(MemberRepository memberRepository, JwtProvider jwtProvider) {
this.memberRepository = memberRepository;
this.jwtProvider = jwtProvider;
}

public String loginAndReturnAccessToken(LoginRequest loginRequest) {
Member member = memberRepository.findByEmail(new Email(loginRequest.email()))
.orElseThrow(() -> new LoginException("가입된 이메일이 아닙니다."));
if (member.isSamePassword(loginRequest.password())) {
return jwtProvider.createAccessToken(member);
}
throw new LoginException("비밀번호가 일치하지 않습니다.");
}
}
Loading