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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,53 @@ ERD는 다음과 같이 짜봤습니다
2. 매점테이블의 존재

원래는 매점 테이블을 만들었었으나, 생각해보니 영화관에 매점은 무조건 하나씩이고, 메뉴까지 다 같아 굳이 만들 필요가 없다는 걸 깨달았습니다. 따라서 이후 테이블을 삭제하고 영화관과 상품테이블을 일대다로 매핑했습니다.

---

### ERD 최종본
<img width="498" height="695" alt="Image" src="https://github.com/user-attachments/assets/92ab58bb-65ec-483a-9c9f-4710ac86ae28" />


# 내용정리

### JWT 인증 방법
JWT란, JSON Web Token의 약자로, 인증에 필요한 정보들을 토큰에 담아 암호화하여 인증에 사용하는 인터넷 표준 인증 방식이다.
.을 기준으로 HEADER, SIGNATURE, PAYLOAD로 구분되며 관련 정보들은 각각에 담긴 채 암호화되어있다.

즉 이는 일종의 확인서로, 우리가 한 사이트에 로그인을 해서 인증이 이루어지면, 서버는 이에 대한 확인서(jwt)를 우리에게 제공한다.
이후 우리는 서버에 요청을 할 때마다 서버에게 jwt를 함께 보여주면서 권한을 확인받는다.

JWT를 그대로 사용할 경우, 토큰 탈취의 위험성이 있어 주로 Access Token, Refresh Token 으로 나누어서 인증을 하는 방식으로
주로 사용된다.

✅Access Token : 클라이언트가 갖고있는 실제로 유저의 정보가 담긴 토큰으로, 클라이언트에서 요청이 오면 서버에서 해당 토큰에 있는 정보를 활용하여 사용자 정보에 맞게 응답을 진행한다. 탈취 위험을 줄이기 위해, 짧은 수명을 유지한다.

✅Refresh Token : 새로운 Access Token을 발급해주기 위해 사용하는 토큰으로 짧은 수명을 가지는 Access Token에게 새로운 토큰을 발급해주기 위해 사용된다.

<img width="682" height="420" alt="Image" src="https://github.com/user-attachments/assets/bb556967-3179-41ec-97bb-121147a3eb68" />
JWT 인증방식은 사진과 같다.

### Cookie 인증 방식
- Key, Value 쌍으로 이루어진 문자열로, 클라이언트가 웹사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버를 통해
클라이언트의 브라우저에 설치되는 작은 데이터 조각이다.
- 다만 요청 시 쿠키의 값을 그대로 보내기때문에, 보안에 취약하다는 단점이 있다.

<img width="676" height="202" alt="Image" src="https://github.com/user-attachments/assets/d58e75d4-0eb2-4089-88f8-ee93a42f45df" />
쿠키 인증 방식이다.

### Session 인증 방식
- cookie의 보안적인 이슈 해결을 위한것으로, 세션은 민감한 인증 정보를 브라우저가 아닌, 서버측에서 저장하고 관리한다.
- 그러나 이 또한 세션ID를 탈취하여, 클라이언트로 위장할 수 있다는 단점이 존재한다.

<img width="694" height="203" alt="Image" src="https://github.com/user-attachments/assets/d97e0bff-5113-4860-b445-7e01080c6ef4" />
세션 인증 방식이다.

### Oauth 인증 방식
우선 Oauth란? 인터넷 사용자들이 다른 웹사이트 상의 자신의 정보에 대해 접근 권한을 부여할 수 있는 공통적인 수단으로서 사영되는
접근 위임을 위한 개방형 표준이다

-> 즉, 이를 이용한 인증 방식은, 한 어플리케이션을 이용할 때 사용자가 해당 어플리케이션이 아닌 외부 어플리케이션 (ex KAKAO, GOOGLE ...)의 Open API에서 로그인을 하여
해당 어플리케이션이 인증 과정을 처리해주는 인증 방식이다.

<img width="639" height="430" alt="Image" src="https://github.com/user-attachments/assets/ea5e67ab-743f-4562-b372-5017c330d634" />
Oauth 인증 방식이다.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ dependencies {

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

implementation 'org.springframework.boot:spring-boot-starter-security'
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.ceos22.cgv_clone.global.apiPayload;

import com.ceos22.cgv_clone.global.apiPayload.code.SuccessStatus;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess","code","message","result"})
public class ApiResponse<T> {


@JsonProperty("isSuccess")
private final Boolean isSuccess;
private String code;
private final String message;

private T result;

//성공한 경우 응답 생성
public static <T> ApiResponse<T> onSuccess(T result){
return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), result);
}

public static <T> ApiResponse<T> of(SuccessStatus status, T result){
return new ApiResponse<>(true, status.getReasonHttpStatus().getCode(), status.getReasonHttpStatus().getMessage(),result);
}

//실패한 경우 응답 생성
public static <T> ApiResponse<T> onFailure(String code, String message,T data){
return new ApiResponse<>(false, code, message, data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ceos22.cgv_clone.global.apiPayload.code;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@Builder
public class ErrorReasonDto {
private HttpStatus httpStatus;

private final boolean isSuccess;
private final String code;
private final String message;

public boolean getIsSuceess() {return isSuccess;}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.ceos22.cgv_clone.global.apiPayload.code;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ErrorStatus{

//일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),

//Not Found
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER404", "해당 유저를 찾을 수 없습니다."),
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "SCHEDULE404", "해당 상영 스케줄이 존재하지 않습니다."),
SEAT_NOT_FOUND(HttpStatus.NOT_FOUND, "SEAT404", "존재하지 않는 좌석이 포함되어 있습니다."),
RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "RESERVATION404", "해당 예약 정보를 찾을 수 없습니다."),
MOVIE_NOT_FOUND(HttpStatus.NOT_FOUND,"MOVIE404","해당 영화를 찾을 수 없습니다."),
CINEMA_NOT_FOUND(HttpStatus.NOT_FOUND,"CINEMA404", "해당 영화관을 찾을 수 없습니다."),
PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND,"PRODUCT404","해당 상품을 찾을 수 없습니다."),


//User관련 응답
ALREADY_EXISTS_EMAIL(HttpStatus.BAD_REQUEST,"USER4001","이미 존재하는 이메일 입니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST,"USER4002","잘못된 비밀번호 입니다."),
PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST,"USER4003","비밀번호와 확인 비밀번호가 일치하지 않습니다."),
EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST,"USER4004","이메일이 존재하지 않습니다."),

//예약관련 응답
SCHEDULE_INACTIVE(HttpStatus.BAD_REQUEST, "RESERVATION4001", "상영 스케줄이 활성 상태가 아닙니다."),
SEAT_ALREADY_RESERVED(HttpStatus.BAD_REQUEST, "RESERVATION4002", "이미 예약된 좌석이 포함되어 있습니다."),

//영화 관련 응답
ALREADY_PREFERED_MOVIE(HttpStatus.BAD_REQUEST, "MOVIE4001", "이미 찜한 영화입니다."),
ALREADY_EXISTS_MOVIE(HttpStatus.BAD_REQUEST,"MOVIE4002","이미 등록된 영화입니다."),

//영화관 관련 응답
ALREADY_PREFERED_CINEMA(HttpStatus.BAD_REQUEST, "CINEMA4001", "이미 찜한 영화관입니다."),

//구매 관련 응답
INVALID_QUANTITY(HttpStatus.BAD_REQUEST,"PURCHASE4001","상품 구매 수량은 최소 1개 이상이어야 합니다."),
;


private final HttpStatus httpStatus;
private final String code;
private final String message;


public ErrorReasonDto getReason() {
return ErrorReasonDto.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}


public ErrorReasonDto getReasonHttpStatus() {
return ErrorReasonDto.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ceos22.cgv_clone.global.apiPayload.code;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@Builder
public class ReasonDto {
Copy link

Choose a reason for hiding this comment

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

ReasonDto를 별도로 두시게 된 배경이 궁금합니다!


private HttpStatus httpStatus;

private final boolean isSuccess;
private final String code;
private final String message;

public boolean getIsSuccess() {return isSuccess;}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.ceos22.cgv_clone.global.apiPayload.code;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum SuccessStatus {

//일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;


public ReasonDto getReason(){
return ReasonDto.builder()
.message(message)
.code(code)
.isSuccess(true)
.build();
}


public ReasonDto getReasonHttpStatus() {
return ReasonDto.builder()
.message(message)
.code(code)
.isSuccess(true)
.httpStatus(httpStatus)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ceos22.cgv_clone.global.apiPayload.exception;

import com.ceos22.cgv_clone.global.apiPayload.code.ErrorReasonDto;
import com.ceos22.cgv_clone.global.apiPayload.code.ErrorStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {

private ErrorStatus errorStatus;

public GeneralException() {
super();
}

public ErrorReasonDto getErrorReason(){
return this.errorStatus.getReason();
}

public ErrorReasonDto getErrorReasonHttpStatus(){
return this.errorStatus.getReasonHttpStatus();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.ceos22.cgv_clone.global.apiPayload.exception;

import com.ceos22.cgv_clone.global.apiPayload.ApiResponse;
import com.ceos22.cgv_clone.global.apiPayload.code.ErrorReasonDto;
import com.ceos22.cgv_clone.global.apiPayload.code.ErrorStatus;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));

return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request);
}

@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {

Map<String, String> errors = new LinkedHashMap<>();

e.getBindingResult().getFieldErrors().stream()
.forEach(fieldError -> {
String fieldName = fieldError.getField();
String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
});

return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors);
}

@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
e.printStackTrace();

return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage());
}

@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDto errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}

private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDto reason,
HttpHeaders headers, HttpServletRequest request) {

ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
// e.printStackTrace();

WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}

private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint);
return super.handleExceptionInternal(
e,
body,
headers,
status,
request
);
}

private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus,
WebRequest request, Map<String, String> errorArgs) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}

private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, WebRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
}
Loading