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
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": "admin@email.com"\
}
### Response
HTTP/1.1 200\
Content-Type: application/json

{\
" token": ""\
}

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

{\
"email": "admin@email.com",
"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
48 changes: 48 additions & 0 deletions src/main/java/gift/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package gift.controller;


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(),
registeredMember.getRole());
return ResponseEntity.ok(Map.of("token", token));
}

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Member member) {
Member authenticatedMember = memberService.authenticate(member.getEmail(), member.getPassword());
if (authenticatedMember == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid email or password");
}
String token = jwtUtil.generateToken(authenticatedMember.getId(),
authenticatedMember.getName(), authenticatedMember.getRole());
return ResponseEntity.ok(Map.of("token", token));
}

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

import gift.exception.InvalidProductException;
import gift.exception.ProductNotFoundException;
import gift.model.Product;
import gift.service.ProductService;
import jakarta.validation.Valid;
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 String getProducts(Model model) {
model.addAttribute("products", productService.getAllProducts());
model.addAttribute("product", new Product());
return "product-list";
}

@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 {
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 {
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());
}
}
}
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);
}
}


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

public class Member {
private long id;
private String email;
private String password;
private String name;
private String role;

public Member() {
}

public Member(long id, String email, String password, String name, String role) {
this.id = id;
this.email = email;
this.password = password;
this.name = name;
this.role = role;
}


public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getRole() {
return role;
}

public void setRole(String role) {
this.role = role;
}
}
55 changes: 55 additions & 0 deletions src/main/java/gift/model/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package gift.model;

public class Product {
private Long id;
private String name;
private int price;
private String imageUrl;

// 기본 생성자
public Product() {
}

// 매개변수가 있는 생성자
public Product(Long id, String name, int price, String imageUrl) {
this.id = id;
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}

// Getter와 Setter 메서드
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}

public String getImageUrl() {
return imageUrl;
}

public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}


}
Loading