diff --git a/README.md b/README.md index 8376bdfff..45b246003 100644 --- a/README.md +++ b/README.md @@ -1 +1,56 @@ -# spring-gift-wishlist \ No newline at end of file +# 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": ""\ +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index df7db9334..d2feced3a 100644 --- a/build.gradle +++ b/build.gradle @@ -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') { diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java new file mode 100644 index 000000000..1eabfbee6 --- /dev/null +++ b/src/main/java/gift/controller/MemberController.java @@ -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(), + 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"); + } + } +} diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java new file mode 100644 index 000000000..bdad66dfa --- /dev/null +++ b/src/main/java/gift/controller/ProductController.java @@ -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> getAllProducts(@RequestHeader("Authorization") String token) { + List 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 { + 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()); + } + } +} \ No newline at end of file diff --git a/src/main/java/gift/controller/WishListController.java b/src/main/java/gift/controller/WishListController.java new file mode 100644 index 000000000..41d94f2a0 --- /dev/null +++ b/src/main/java/gift/controller/WishListController.java @@ -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 = 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 removeProductFromWishList(@RequestHeader("Authorization") String token, @PathVariable Long id) { + wishListService.removeProductFromWishList(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/gift/exception/ForbiddenException.java b/src/main/java/gift/exception/ForbiddenException.java new file mode 100644 index 000000000..14800fc08 --- /dev/null +++ b/src/main/java/gift/exception/ForbiddenException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..a09749876 --- /dev/null +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -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"; + } +} \ No newline at end of file diff --git a/src/main/java/gift/exception/InvalidProductException.java b/src/main/java/gift/exception/InvalidProductException.java new file mode 100644 index 000000000..71d5e05d8 --- /dev/null +++ b/src/main/java/gift/exception/InvalidProductException.java @@ -0,0 +1,7 @@ +package gift.exception; + +public class InvalidProductException extends RuntimeException { + public InvalidProductException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/gift/exception/ProductNotFoundException.java b/src/main/java/gift/exception/ProductNotFoundException.java new file mode 100644 index 000000000..997966400 --- /dev/null +++ b/src/main/java/gift/exception/ProductNotFoundException.java @@ -0,0 +1,6 @@ +package gift.exception; +public class ProductNotFoundException extends RuntimeException { + public ProductNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/gift/exception/UnauthorizedException.java b/src/main/java/gift/exception/UnauthorizedException.java new file mode 100644 index 000000000..333618bfa --- /dev/null +++ b/src/main/java/gift/exception/UnauthorizedException.java @@ -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); + } +} + + diff --git a/src/main/java/gift/model/LoginResponse.java b/src/main/java/gift/model/LoginResponse.java new file mode 100644 index 000000000..0d9507d77 --- /dev/null +++ b/src/main/java/gift/model/LoginResponse.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/gift/model/Member.java b/src/main/java/gift/model/Member.java new file mode 100644 index 000000000..550c8d904 --- /dev/null +++ b/src/main/java/gift/model/Member.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/gift/model/Product.java b/src/main/java/gift/model/Product.java new file mode 100644 index 000000000..99a5ee07f --- /dev/null +++ b/src/main/java/gift/model/Product.java @@ -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; + } + + +} \ No newline at end of file diff --git a/src/main/java/gift/model/WishList.java b/src/main/java/gift/model/WishList.java new file mode 100644 index 000000000..3a1ac8425 --- /dev/null +++ b/src/main/java/gift/model/WishList.java @@ -0,0 +1,40 @@ +package gift.model; + +public class WishList { + + private Long id; + private Long memberId; + private Long productId; + + public WishList() {} + + public WishList(Long id, Long memberId, Long productId) { + this.id = id; + this.memberId = memberId; + this.productId = productId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public Long getProductId() { + return productId; + } + public void setProductId(Long productId) { + this.productId = productId; + } + +} diff --git a/src/main/java/gift/repository/MemberRepository.java b/src/main/java/gift/repository/MemberRepository.java new file mode 100644 index 000000000..9ae39b8e1 --- /dev/null +++ b/src/main/java/gift/repository/MemberRepository.java @@ -0,0 +1,85 @@ +package gift.repository; + + + +import gift.model.Member; +import java.util.HashMap; +import java.util.Map; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; + +@Repository +public class MemberRepository { + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + + public MemberRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + createMemberTable(); + this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("members") + .usingGeneratedKeyColumns("id"); + createMemberTable(); + } + + public void createMemberTable() { + String sql = "CREATE TABLE IF NOT EXISTS members (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY," + + "email VARCHAR(255) NOT NULL," + + "password VARCHAR(255) NOT NULL," + + "name VARCHAR(255)," + + "role VARCHAR(50)" + + ")"; + jdbcTemplate.execute(sql); + } + + public Member findByEmail(String email) { + String sql = "SELECT * FROM members WHERE email = ?"; + try { + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { + long id = rs.getLong("id"); + String password = rs.getString("password"); + String name = rs.getString("name"); + String role = rs.getString("role"); + return new Member(id, email, password, name, role); + }, email); + } catch (EmptyResultDataAccessException e) { + return null; + } + + } + + public Member findById(Long id) { + String sql = "SELECT * FROM members WHERE id = ?"; + try { + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { + String email = rs.getString("email"); + String password = rs.getString("password"); + String name = rs.getString("name"); + String role = rs.getString("role"); + return new Member(id, email, password, name, role); + }, id); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + public Member registerMember(Member member) { + Map parameters = new HashMap<>(); + parameters.put("email", member.getEmail()); + parameters.put("password", member.getPassword()); + parameters.put("name", member.getName()); + parameters.put("role", member.getRole()); + Number newId = simpleJdbcInsert.executeAndReturnKey(parameters); + member.setId(newId.longValue()); + return member; + } + public boolean existsByEmail(String email) { + String sql = "SELECT COUNT(*) FROM members WHERE email = ?"; + Integer count = jdbcTemplate.queryForObject(sql, Integer.class, email); + return count != null && count > 0; + } +} diff --git a/src/main/java/gift/repository/ProductRepository.java b/src/main/java/gift/repository/ProductRepository.java new file mode 100644 index 000000000..fd052dd42 --- /dev/null +++ b/src/main/java/gift/repository/ProductRepository.java @@ -0,0 +1,68 @@ +package gift.repository; + +import gift.model.Product; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +public class ProductRepository { + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + + public ProductRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("products") + .usingGeneratedKeyColumns("id"); + } + + public List findAll() { + return jdbcTemplate.query("SELECT * FROM products", (rs, rowNum) -> { + Product product = new Product(); + product.setId(rs.getLong("id")); + product.setName(rs.getString("name")); + product.setPrice(rs.getInt("price")); + product.setImageUrl(rs.getString("image_url")); + return product; + }); + } + + public Product findById(Long id) { + return jdbcTemplate.queryForObject("SELECT * FROM products WHERE id = ?", new Object[]{id}, (rs, rowNum) -> { + Product product = new Product(); + product.setId(rs.getLong("id")); + product.setName(rs.getString("name")); + product.setPrice(rs.getInt("price")); + product.setImageUrl(rs.getString("image_url")); + return product; + }); + } + + public Product save(Product product) { + Map parameters = new HashMap<>(); + parameters.put("name", product.getName()); + parameters.put("price", product.getPrice()); + parameters.put("image_url", product.getImageUrl()); + Number newId = simpleJdbcInsert.executeAndReturnKey(parameters); + product.setId(newId.longValue()); + return product; + } + + public void update(Product product) { + jdbcTemplate.update("UPDATE products SET name = ?, price = ?, image_url = ? WHERE id = ?", + product.getName(), product.getPrice(), product.getImageUrl(), product.getId()); + } + + public void deleteById(Long id) { + jdbcTemplate.update("DELETE FROM products WHERE id = ?", id); + } + + public boolean existsById(Long id) { + Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE id = ?", new Object[]{id}, Integer.class); + return count != null && count > 0; + } +} diff --git a/src/main/java/gift/repository/WishListRepository.java b/src/main/java/gift/repository/WishListRepository.java new file mode 100644 index 000000000..c81c46962 --- /dev/null +++ b/src/main/java/gift/repository/WishListRepository.java @@ -0,0 +1,59 @@ +package gift.repository; + +import gift.model.WishList; +import java.util.HashMap; +import java.util.List; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; + +@Repository +public class WishListRepository { + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + + public WishListRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("wishlist") + .usingGeneratedKeyColumns("id"); + createWishListTable(); + } + + private void createWishListTable() { + String sql = "CREATE TABLE IF NOT EXISTS wishlist (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY," + + "member_id BIGINT NOT NULL," + + "product_id BIGINT NOT NULL," + + "FOREIGN KEY (member_id) REFERENCES members(id)," + + "FOREIGN KEY (product_id) REFERENCES products(id)" + + ")"; + jdbcTemplate.execute(sql); + } + + + + public List findByMemberId(Long memberId) { + String sql = "SELECT * FROM wish_list_items WHERE member_id = ?"; + return jdbcTemplate.query(sql, (rs, rowNum) -> { + Long id = rs.getLong("id"); + Long productId = rs.getLong("product_id"); + return new WishList(id, memberId, productId); + }, memberId); + } + + public WishList addWishListItem(WishList wishList) { + var parameters = new HashMap(); + parameters.put("member_id", wishList.getMemberId()); + parameters.put("product_id", wishList.getProductId()); + Number newId = simpleJdbcInsert.executeAndReturnKey(parameters); + wishList.setId(newId.longValue()); + return wishList; + } + + public void deleteById(Long id) { + String sql = "DELETE FROM wishlist WHERE id = ?"; + jdbcTemplate.update(sql, id); + } +} \ No newline at end of file diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java new file mode 100644 index 000000000..be2cf35fb --- /dev/null +++ b/src/main/java/gift/service/MemberService.java @@ -0,0 +1,40 @@ +package gift.service; + + +import gift.model.Member; +import gift.repository.MemberRepository; +import gift.util.JwtUtil; +import gift.util.PasswordUtil; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestMapping; + +@Service +@RequestMapping +public class MemberService { + private final MemberRepository memberRepository; + private final JwtUtil jwtUtil; + + + public MemberService(MemberRepository memberRepository, JwtUtil jwtUtil) { + this.memberRepository = memberRepository; + this.jwtUtil = jwtUtil; + } + + public Member registerMember(Member member) { + member.setPassword(PasswordUtil.hashPassword(member.getPassword())); + return memberRepository.registerMember(member); + } + + public Member authenticate(String email, String password) { + Member member = memberRepository.findByEmail(email); + if (member != null && member.getPassword().equals(password)) { + return member; + } + return null; + } + + public boolean existsByEmail(String email) { + return memberRepository.existsByEmail(email); + } + +} diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java new file mode 100644 index 000000000..765a474ac --- /dev/null +++ b/src/main/java/gift/service/ProductService.java @@ -0,0 +1,70 @@ +package gift.service; +import gift.exception.InvalidProductException; +import gift.exception.ProductNotFoundException; +import gift.repository.ProductRepository; +import org.springframework.stereotype.Service; +import gift.model.Product; +import java.util.List; +import org.springframework.web.bind.annotation.RequestMapping; + + + +@Service +@RequestMapping +public class ProductService { + private final ProductRepository productRepository; + + public ProductService(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + + public List getAllProducts() { + return productRepository.findAll(); + } + + + public Product getProductById(Long id) { + try { + return productRepository.findById(id); + } catch (Exception e) { + throw new IllegalArgumentException("Product not found: " + e.getMessage()); + } + } + + + + public Product addProduct(Product product) { + validateProduct(product); + return productRepository.save(product); + } + + + public void updateProduct(Long id, Product product) { + if (!productRepository.existsById(id)) { + throw new ProductNotFoundException("Product not found"); + } + + product.setId(id); + validateProduct(product); + productRepository.update(product); + } + + + public void deleteProduct(Long id) { + productRepository.deleteById(id); + } + + public void validateProduct(Product product) { + if (product.getName().length() > 15 || product.getName().trim().isEmpty()) { + throw new InvalidProductException("상품 이름은 공백을 포함하여 최대 15자까지 입력할 수 있습니다."); + } + if(product.getName().contains("카카오")) { + throw new InvalidProductException("\"카카오\"가 포함된 문구는 담당 MD와 협의한 경우에만 사용할 수 있습니다."); + } + if (!product.getName().matches("^[\\w\\s\\(\\)\\[\\]\\+\\-\\&\\/\\_가-힣]+$")) { + throw new InvalidProductException("( ), [ ], +, -, &, /, _ 외 특수 문자는 사용이 불가합니다."); + } + + } +} diff --git a/src/main/java/gift/service/WishListService.java b/src/main/java/gift/service/WishListService.java new file mode 100644 index 000000000..86e395d28 --- /dev/null +++ b/src/main/java/gift/service/WishListService.java @@ -0,0 +1,27 @@ +package gift.service; + +import gift.model.WishList; +import gift.repository.WishListRepository; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class WishListService { + private final WishListRepository wishListRepository; + + public WishListService(WishListRepository wishListRepository) { + this.wishListRepository = wishListRepository; + } + + public List getWishListByMemberId(Long memberId) { + return wishListRepository.findByMemberId(memberId); + } + + public WishList addProductToWishList(Long memberId, Long productId) { + return wishListRepository.addWishListItem(new WishList(null, memberId, productId)); + } + + public void removeProductFromWishList(Long wishListId) { + wishListRepository.deleteById(wishListId); + } +} diff --git a/src/main/java/gift/util/JwtUtil.java b/src/main/java/gift/util/JwtUtil.java new file mode 100644 index 000000000..2ab70e0ff --- /dev/null +++ b/src/main/java/gift/util/JwtUtil.java @@ -0,0 +1,41 @@ +package gift.util; + +import gift.model.Member; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.util.Date; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + private final String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; + private final Key key = Keys.hmacShaKeyFor(secretKey.getBytes()); + + public String generateToken(Long memberId, String name, String role) { + return Jwts.builder() + .setSubject(memberId.toString()) + .claim("name", name) + .claim("role", role) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public Claims extractClaims(String token) { + return Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + + public Long extractMemberId(String token) { + return Long.parseLong(extractClaims(token).getSubject()); + } + +} diff --git a/src/main/java/gift/util/PasswordUtil.java b/src/main/java/gift/util/PasswordUtil.java new file mode 100644 index 000000000..67488f7b4 --- /dev/null +++ b/src/main/java/gift/util/PasswordUtil.java @@ -0,0 +1,25 @@ +package gift.util; + + +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordUtil { + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int SALT_LENGTH = 16; + + public static String hashPassword(String password) { + return Base64.getEncoder().encodeToString(password.getBytes()); + + } + + public static boolean checkPassword(String rawPassword, String hashedPassword) { + String encodedPassword = Base64.getEncoder().encodeToString(rawPassword.getBytes()); + return encodedPassword.equals(hashedPassword); + } + + public static String decodeCredentials(String base64Credentials) { + byte[] decodedBytes = Base64.getDecoder().decode(base64Credentials); + return new String(decodedBytes); + } +} diff --git a/src/main/login.http b/src/main/login.http new file mode 100644 index 000000000..e7e6c87f6 --- /dev/null +++ b/src/main/login.http @@ -0,0 +1,10 @@ +### GET request to example server +POST /members/login HTTP/1.1 +content-type: application/json +host: localhost:8080 + +{ + "email": "admin@email.com", + "password": "passwrd" +} +### \ No newline at end of file diff --git a/src/main/resources/WishList.sql b/src/main/resources/WishList.sql new file mode 100644 index 000000000..ed09b17cc --- /dev/null +++ b/src/main/resources/WishList.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS wish_list_items ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + product_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d16b65f4..86f144ceb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,12 @@ spring.application.name=spring-gift +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:WishList.sql +#spring.sql.init.data-locations=classpath:memberdata.sql +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +spring.datasource.initialize=true +spring.jpa.hibernate.ddl-auto=update \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..71f67572a --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1 @@ +INSERT INTO products (name, price, image_url) VALUES ('아이스 카페 아메리카노 T', 4500, 'https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg'); diff --git a/src/main/resources/memberdata.sql b/src/main/resources/memberdata.sql new file mode 100644 index 000000000..8d350f1ba --- /dev/null +++ b/src/main/resources/memberdata.sql @@ -0,0 +1 @@ +INSERT INTO members ( email, password, name, role) VALUES ('admin@email.com','password', 'admin', 'leader') diff --git a/src/main/resources/memberschema.sql b/src/main/resources/memberschema.sql new file mode 100644 index 000000000..e276434f4 --- /dev/null +++ b/src/main/resources/memberschema.sql @@ -0,0 +1,9 @@ +CREATE TABLE members ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + role VARCHAR(255) NOT NULL +); + + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..aec0f8e77 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE products ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + image_url VARCHAR(255) +); \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 000000000..973276672 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,85 @@ + + + + + + Login + + + +
+

Login

+
+ + +
+
+ + +
+
+ +
+
+
+ + + + diff --git a/src/main/resources/templates/product-detail.html b/src/main/resources/templates/product-detail.html new file mode 100644 index 000000000..f42ed2a60 --- /dev/null +++ b/src/main/resources/templates/product-detail.html @@ -0,0 +1,34 @@ + + + + Product Details + + + +
+

Product Details

+ + + +
+
+ Product ID: +
+
+
Name:
+

Price:

+ Product Image + +
+
+
+ + + + + + diff --git a/src/main/resources/templates/product-list.html b/src/main/resources/templates/product-list.html new file mode 100644 index 000000000..e6eb65a7a --- /dev/null +++ b/src/main/resources/templates/product-list.html @@ -0,0 +1,235 @@ + + + + Product Management + + + +
+

Manage Products

+ +
+ + + +
+ + + + + + + + + + + + + + + + +
IDNamePriceImageActions
+ + + +
+ + + + + + +