Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 BE_문성민 2주차 과제 (3단계) #402

Open
wants to merge 24 commits into
base: dalsungmin
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,56 @@
# spring-gift-wishlist
# spring-gift-wishlist
# Step 0
* 상품 관리 코드를 옮겨 온다. 코드를 옮기는 방법에는 디렉터리의 모든 파일을 직접 복사하여 붙여 넣는 것부터 필요한 일부 파일만 이동하는 것, Git을 사용하는 것까지 여러 가지 방법이 있다. 코드 이동 시 반드시 리소스 파일, 프로퍼티 파일, 테스트 코드 등을 함께 이동한다.
***
# Step 1유효성 검사 및 예외처리
- 상품을 추가하거나 수정하는 경우, 클라이언트로부터 잘못된 값이 전달될 수 있다.
- 잘못된 값이 전달되면 클라이언트가 어떤 부분이 왜 잘못되었는지 인지할 수 있도록 응답을 제공한다.
## 예외 조건
* 상품 이름은 공백을 포함하여 최대 15자까지 입력할 수 있다.
* 특수 문자
* 가능: ( ), [ ], +, -, &, /, _
* 그 외 특수 문자 사용 불가
* "카카오"가 포함된 문구는 담당 MD와 협의한 경우에만 사용할 수 있다.
***
# Step 2 인증
* 사용자가 회원 가입, 로그인, 추후 회원별 기능을 이용할 수 있도록 구현한다.
* 회원은 이메일과 비밀번호를 입력하여 가입한다.
* 토큰을 받으려면 이메일과 비밀번호를 보내야 하며, 가입한 이메일과 비밀번호가 일치하면 토큰이 발급된다.
* 토큰을 생성하는 방법에는 여러 가지가 있다. 방법 중 하나를 선택한다.
* (선택) 회원을 조회, 추가, 수정, 삭제할 수 있는 관리자 화면을 구현한다.
*
* 아래 예시와 같이 HTTP 메시지를 주고받도록 구현한다.
### Reaquest
POST /login/token HTTP/1.1 \
content-type: application/json\
host: localhost:8080

{\
"password": "password",\
"email": "[email protected]"\
}
### Response
HTTP/1.1 200\
Content-Type: application/json

{\
" token": ""\
}

### 로그인
POST /members/login HTTP/1.1
content-type: application/json
host: localhost:8080

{\
"email": "[email protected]",
"password": "password"\
}

### Response
HTTP/1.1 200
Content-Type: application/json

{\
"token": ""\
}
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
compileOnly 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('test') {
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/gift/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package gift.controller;


import gift.model.LoginResponse;
import gift.model.Member;
import gift.service.MemberService;
import gift.util.JwtUtil;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
private final JwtUtil jwtUtil;

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

@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody Member member) {
if (memberService.existsByEmail(member.getEmail())) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("Email already exists");
}
Member registeredMember = memberService.registerMember(member);
String token = jwtUtil.generateToken(registeredMember.getId(), registeredMember.getName(),

Choose a reason for hiding this comment

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

jwt의 원리와. 단점 파악하기.

  • jwt 는 대칭키 일까요 비대칭키 일까요?
  • jwt의 토큰값은 복호화가 가능할까요?
  • jwt의 단점은?
  • jwt의 단점을 위해서 보안해야할 방법은?
    위 처럼 jwt를 사용하는 정확한 이유과 장단점을 확인하면서 학습하기를 권장 드립니다 :)

registeredMember.getRole());
return ResponseEntity.ok(Map.of("token", token));
}

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Member loginRequest) {
Member member = memberService.authenticate(loginRequest.getEmail(),
loginRequest.getPassword());
if (member != null) {
String token = jwtUtil.generateToken(member.getId(), member.getName(),
member.getRole());
return ResponseEntity.ok(new LoginResponse(token));
} else {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid email or password");
}
}
}
90 changes: 90 additions & 0 deletions src/main/java/gift/controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package gift.controller;

import gift.exception.InvalidProductException;
import gift.exception.ProductNotFoundException;
import gift.model.Product;
import gift.service.ProductService;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;

public ProductController( ProductService productService) {
this.productService = productService;
}

@GetMapping
public ResponseEntity<List<Product>> getAllProducts(@RequestHeader("Authorization") String token) {
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}

@PostMapping
public String addProduct(@ModelAttribute @Valid Product product, RedirectAttributes redirectAttributes) {
try {
productService.addProduct(product);
} catch (InvalidProductException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid product: " + e.getMessage());
}

return "redirect:/products";
}

@PostMapping("/{id}")
public String updateProduct(@Valid @PathVariable Long id, @ModelAttribute Product product, RedirectAttributes redirectAttributes) {
try {

Choose a reason for hiding this comment

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

여기서 try catch로 작업을 다 해주셨는데 이렇게되면
@ControllerAdvice - GlobalExceptionHandler 클래스를 생성한 이유가 없어요. ^^

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({Exception.class, RuntimeException.class})
    public String handleException(Exception e, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("errorMessage", "Error: " + e.getMessage());
        return "redirect:/products";
    }

    @ExceptionHandler(ProductNotFoundException.class)
    public String handleProductNotFoundException(ProductNotFoundException e, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
        return "redirect:/products";
    }

    @ExceptionHandler(InvalidProductException.class)
    public String handleInvalidProductException(InvalidProductException e, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("errorMessage", "Invalid product: " + e.getMessage());
        return "redirect:/products";
    }
}

이렇게 구성하고 컨트롤러 단에서 try catch 걷어 냅시다!

 @PostMapping("/{id}")
    public String updateProduct(@PathVariable Long id, @ModelAttribute @Valid Product product, RedirectAttributes redirectAttributes) {
        productService.updateProduct(id, product);
        return "redirect:/products";
    }

모든지 새로운 기술을 습득하거나 적용할때 왜 왜 왜 ! 항상 머리속에 왜라는 생각을 꼭 하셔서 @ControllerAdvice 를 왜 사용할까?
사용하면 좋은 이유는 무엇인가? 동작 원리는 무엇인가? 항상 자기 자신에게 역질문하면서 개발해보셔요 !!

productService.updateProduct(id, product);
} catch (InvalidProductException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Invalid product: " + e.getMessage());
} catch (ProductNotFoundException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error updating product: " + e.getMessage());
}
return "redirect:/products";
}

@PostMapping("/delete/{id}")
public String deleteProduct(@PathVariable Long id, RedirectAttributes redirectAttributes) {
try {

Choose a reason for hiding this comment

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

위와 같이 수정해주세요.

productService.deleteProduct(id);
} catch (ProductNotFoundException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Error deleting product: " + e.getMessage());
}
return "redirect:/products";
}

@GetMapping("/view/{id}")
public String getProductDetails(@PathVariable("id") Long id, Model model, RedirectAttributes redirectAttributes) {
try {
Product product = productService.getProductById(id);
model.addAttribute("product", product);
} catch (ProductNotFoundException e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
} catch (Exception e) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found" );
return "redirect:/products";
}
return "product-detail";
}

@GetMapping("/{id}")
@ResponseBody
public Product getProductById(@PathVariable("id") Long id) {
try {
return productService.getProductById(id);
} catch (Exception e) {
throw new IllegalArgumentException("Product not found: " + e.getMessage());
}
}
}
42 changes: 42 additions & 0 deletions src/main/java/gift/controller/WishListController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package gift.controller;

import gift.model.WishList;
import gift.service.WishListService;
import gift.util.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/wishlist")
public class WishListController {
private final WishListService wishListService;
private final JwtUtil jwtUtil;

public WishListController(WishListService wishListService, JwtUtil jwtUtil) {
this.wishListService = wishListService;
this.jwtUtil = jwtUtil;
}

@GetMapping
public ResponseEntity<?> getWishList(@RequestHeader("Authorization") String token) {
Long memberId = jwtUtil.extractMemberId(token);
List<WishList> wishList = wishListService.getWishListByMemberId(memberId);
return ResponseEntity.ok(wishList);
}

@PostMapping
public ResponseEntity<?> addProductToWishList(@RequestHeader("Authorization") String token, @RequestParam Long productId) {
Long memberId = jwtUtil.extractMemberId(token);
WishList wishList = wishListService.addProductToWishList(memberId, productId);
return ResponseEntity.ok(wishList);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> removeProductFromWishList(@RequestHeader("Authorization") String token, @PathVariable Long id) {
wishListService.removeProductFromWishList(id);
return ResponseEntity.noContent().build();
}
}
11 changes: 11 additions & 0 deletions src/main/java/gift/exception/ForbiddenException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gift.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.FORBIDDEN)
public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message);
}
}
20 changes: 20 additions & 0 deletions src/main/java/gift/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package gift.exception;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class)
public String handleException(Exception e, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("errorMessage", "Error: " + e.getMessage());
return "redirect:/products";
}

@ExceptionHandler(ProductNotFoundException.class)
public String handleProductNotFoundException(ProductNotFoundException e, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("errorMessage", "Product not found: " + e.getMessage());
return "redirect:/products";
}
}
7 changes: 7 additions & 0 deletions src/main/java/gift/exception/InvalidProductException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gift.exception;

public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message);
}
}
6 changes: 6 additions & 0 deletions src/main/java/gift/exception/ProductNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gift.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}
13 changes: 13 additions & 0 deletions src/main/java/gift/exception/UnauthorizedException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gift.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
}


17 changes: 17 additions & 0 deletions src/main/java/gift/model/LoginResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package gift.model;

public class LoginResponse {
private String token;

public LoginResponse(String token) {
this.token = token;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}
}
Loading