diff --git a/README.md b/README.md index 3bc7e2563..c253ccc60 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,71 @@ # 2주차 위시 리스트 - 요청과 응답 심화 -### step 0 +# step 0 + 1주차 코드 가져오기 1주차 PR에 대해서 잘못된 저장소에서 fork하는 바람에 리뷰를 받지 못하였습니다. + +# step 1 + +목표 + +- 상품 추가, 수정 시 잘못된 값에 대한 처리 + 응답 설정 +- 상품 이름 제한 : 공백 포함 최대 15글자 +- 일부 특수 문자만 허용 +- "카카오" 포함 문구 입력 시 따로 confirm 이후 진행 가능하도록 + +## milestone + +-[X] 스프링 validation 의존성 추가 +-[X] feat : DTO valid 추가 +-[ ] refact : service - 상품 update 로직 변경 (하나로 통합) +-[X] feat : @ControllerAdvice 클래스 추가 +-[X] feat : "카카오" 검사를 위한 예외클래스 추가 + +## milestone 2 + +회원 가입, 로그인, 추후 회원별 기능 이용을 위해 + +회원은 이메일과 비밀번호를 입력하여 가입한다. +토큰을 받으려면 이메일과 비밀번호를 보내야 하며, 가입한 이메일과 비밀번호가 일치하면 토큰이 발급된다. +토큰은 JWT 를 사용하도록 한다. +관리자는 회원을 조회 추가 수정 삭제 할 수 있다. + +- [X] feat : 회원 모델 만들기 +- [X] feat : 회원 Repository 만들기 +- [X] feat : 회원 가입 서비스 만들기 +- [X] feat : 회원 가입 컨트롤러 만들기 +- [X] feat : 로그인 서비스 만들기 +- [X] feat : 인증 서비스 만들기 +- [X] feat : 인증 컨트롤러 만들기 + +### 회원 모델 만들기 + +Member.class : 회원모델 +MemberRole.class : 회원 등급 enum class + +### 응답 코드 + +헤더나 토큰이 유효하지 않은 경우 -> 401 +잘못된 로그인, 비밀번호 찾기, 비밀번호 변경 요청 -> 403 + +## milestone 3 + +위시 리스트 + +위시 리스트는 일종의 장바구니로 생각할 수 있다. +위시 리스트에 등록된 상품 목록을 조회할 수 있다. +위시 리스트에 상품을 추가할 수 있다. +위시 리스트에 담긴 상품을 삭제할 수 있다. +사용자 정보는 요청 헤더의 Authorization 필드를 사용한다. + +- [X] feat : 위시 리스트 모델 만들기 +- [X] feat : 위시리스트 Repository 만들기 +- [X] feat : 위시리스트 서비스 만들기 +- [ ] feat : 위시리스트 컨트롤러 만들기 +- [ ] feat : 인증을 위한 ArgumentResolver 만들기 +- [ ] test : 테스트 코드로 동작 확인 \ No newline at end of file diff --git a/build.gradle b/build.gradle index df7db9334..e32fc3a28 100644 --- a/build.gradle +++ b/build.gradle @@ -21,9 +21,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + testImplementation 'io.projectreactor:reactor-test' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + compileOnly 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } tasks.named('test') { diff --git a/history.md b/history.md new file mode 100644 index 000000000..2992dab22 --- /dev/null +++ b/history.md @@ -0,0 +1,103 @@ +# 작업 일지 + +스스로 기록용도로 작성 + +## 7월 4일 (목) + +### PR 코멘트 반영하기 + +1. ~~GetMapping 등에서 url 이 동일한 것에 대해서 통합표기 (@controller)~~ +2. rest 도메인과 관련해서 엔드포인트 네이밍의 해석의 모호함 +3. 문법 띄워쓰기 관련 google java style guide 참고하기 (내가 못하면 cmd + option + L) 을 매번 눌러주자 +4. ~~pm 변수명 삭제 → productService로 변경 (처음에는 productManager 정도로 생각하고 임시로 작성하였다가 바꾸지 않았던 것이 문제가 됨)~~ +5. 불필요한 주석 삭제 → 사용하지 않는 것들에 대해서 (가독성 떨어뜨림) + +에 대해서 진행 + +### valid 적용 테스트 코드 작성하기 + +이름 길이가 15글자를 넘는 경우와 0글자인 경우에 대해 로직과 테스트 추가 + +### TODO + +예외에 대한 메세지가 너무 흩어져있어서 관리가 안되고 있다고 생각되어짐. +한 군데로 모아서 예외에 대한 관리를 진행할 필요가 있다고 생각. +예외 테스트코드에서 문자열 비교를 통해서 assert 하는 것은 너무 안좋은 방법이라고 생각되어짐. + +## 7월 5일 (금) + +1. 회원가입 관련 구현 +2. 로그인 관련 구현 + +궁금증 : 모델에서 null 허용이 되는 wrapper class를 사용하는 것이 좋은 방법일까? 나쁜 방법일까? + +-[ ] Product 에 setter가 필요한가? + +readme 와 history와 notion 3개의 중복된 문서 작성 문제... 굳이 필요없는 내용을 많이 남긴다는 생각 자체가 잘못된 것 같기도? +주말에 특정한 규칙을 정하기 필요 + +-[ ] 문서정리규칙 정하기 + +### 수정해야할 것 + +1. 규격화된 테스트로 전체적인 동작에 문제가 없는지 확인하기 +2. 컨트롤러 - 서비스 - 모델 사이에서 명확하게 책임 정해서 작업하기 +3. JWT 공부해서 제대로 적용시키기 (비밀키 숨기기) +4. 인증토큰에 대해서 서버에서 어떻게 인증토큰을 통해서 권한을 설정하는지 제대로 알고 적용시키기 + +- [ ] 테스트코드를 위한 보일러플레이트 클래스를 만들어서 사용해도 되는가? - 좋은가? 나쁜가? + +- [ ] Repository는 정확히 어떤 책임을 지고 있는 것인지? 새로운 멤버를 등록하였다면 이걸 다시 Model로 반환해줘야하는가? + +- [ ] Repository는 무엇을 얼마만큼 제어해야할까? + 만약 Model의 내부 값이 변하면, 이것은 service에서 하나하나 분리해서 repository를 업데이트 해줘야하는가? + +## 로그인 인증 과정에서의 어려움 + +상품과 멤버에 대해 컨트롤러와 서비스에서 규칙 없이 작성하였고, 위시 리스트 구현에서 어디서 무엇을 가져와서 어썬 식으로 사용해야할 지 +생각하기에 어려움이 너무 커졌다. + +# 7월 7일 refact 계획 + +1. dto, 컨트롤, 서비스, repository에 대해 통일성 부여하기 + +dto -> 데이터 transfer를 위해 존재 +dto -> 모델로 변환하는 로직은 있으면 좋겠다는 생각 (model은 dto를 몰라도 되도록) +클라이언트 -> 컨트롤러 (dto사용) -> 서비스 (같은 dto 사용??) + +근데 서비스에서는 특정 필드의 값만 필요한 경우가 있다. +서비스 내부에서는 항상 model로 변환해서? 서비스는 여러 모델들의 협업? + +### repository 들 먼저 구조 변경해주기 + +실제 사용되는 String, int 등의 value를 직접 넘겨주도록 한다. +repository는 결국 DB와 매우 큰 밀착관계이고, Model을 몰라도 된다? + +반환은 Model을 해준다? + +- [X] Repository 통일성 부여 : 기본 검색은 id를 기반으로 한다. Model을 params로 받지 않는다. 반환은 Model로 해준다. + +# 7월 8일 (월) + +1. 피드백에 따라서 수정 진행 +2. wishlist 완벽하게 동작하도록 수정하기 + +## 피드백 반영 + +1. 공통적인 포멧이 있는 것은 좋지만, DTO는 용도에 맞게 사용하는 것이 좋다. 불필요한 데이터까지 내줄 필요 없음 + +2. 문서는 코드 자체로 잘 이해할 수 있도록 해두는 것이 좋다. -> 클린코드 + +3. 불필요한 축약어 사용 X, stream 적극적인 활용 -> ok + +4. 파일 전반적인 설명 주석은 상단에, 그리고 주석이 필요하다면 자세하게 -> ok + +5. rest 규격에 맞게 경로 설계하기 -> {id} 등으로 받기 -> ok + +6. 변수명 camelCase 로 -> ok + +7. 인증 토큰은 저장할 필요가 있는가? + +8. 중복된 이메일 -> 데이터베이스에서도 막을 수 있도록 유니크 설정시켜주기 -> ok + +9. addNew 와 같이 중복된 네이밍 X diff --git a/src/main/java/gift/ArgumentResolver/LoginMember.java b/src/main/java/gift/ArgumentResolver/LoginMember.java new file mode 100644 index 000000000..5b82d0dcd --- /dev/null +++ b/src/main/java/gift/ArgumentResolver/LoginMember.java @@ -0,0 +1,12 @@ +package gift.ArgumentResolver; + +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 { + +} diff --git a/src/main/java/gift/ArgumentResolver/LoginMemberArgumentResolver.java b/src/main/java/gift/ArgumentResolver/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..fffcf2aae --- /dev/null +++ b/src/main/java/gift/ArgumentResolver/LoginMemberArgumentResolver.java @@ -0,0 +1,34 @@ +package gift.ArgumentResolver; + +import gift.service.MemberService; +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 LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final MemberService memberService; + + public LoginMemberArgumentResolver(MemberService memberService) { + this.memberService = memberService; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + String token = request.getHeader("Authorization"); + if (token == null) { + return null; + } + return memberService.getLoginUser(token); + } +} diff --git a/src/main/java/gift/controller/AdminPageController.java b/src/main/java/gift/controller/AdminPageController.java index 7efe83bf7..2ff983800 100644 --- a/src/main/java/gift/controller/AdminPageController.java +++ b/src/main/java/gift/controller/AdminPageController.java @@ -2,6 +2,8 @@ import gift.dto.ProductDTO; import gift.service.ProductService; +import jakarta.validation.Valid; + import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.DeleteMapping; @@ -10,53 +12,56 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestMapping; +@RequestMapping("/admin/products") @Controller public class AdminPageController { - private final ProductService pm; + private final ProductService productService; - public AdminPageController(ProductService pm) { - this.pm = pm; + public AdminPageController(ProductService productService) { + this.productService = productService; } - @GetMapping("/admin") + @GetMapping public String adminPage(Model model) { - model.addAttribute("products",pm.readAll()); - model.addAttribute("productDTO",new ProductDTO()); - return "admin/index";//렌더링하는 html 이름 + model.addAttribute("products", productService.readAll()); + model.addAttribute("productDTO", new ProductDTO()); + return "admin/index"; } - @PostMapping("/admin") //admin으로 오는 post에 대해서 submit - public String adminPageSubmit(@ModelAttribute("productDTO") ProductDTO productDTO) { - pm.create(productDTO); //서비스에 접근해서 해당 부분을 추가해주도록 한다. - return "redirect:/admin"; + @PostMapping + public String adminPageSubmit(@ModelAttribute("productDTO") @Valid ProductDTO productDTO) { + productService.create(productDTO); + return "redirect:/admin/products"; } - @PutMapping("/admin/{id}") - public String adminPageUpdate(@PathVariable Long id,@ModelAttribute("productDTO") ProductDTO productDTO) { - changeCheckAndUpdate(id,productDTO); - return "redirect:/admin"; + @PutMapping("/{id}") + public String adminPageUpdate(@PathVariable Long id, + @ModelAttribute("productDTO") @Valid ProductDTO productDTO) { + changeCheckAndUpdate(id, productDTO); + return "redirect:/admin/products"; } - @DeleteMapping("/admin/{id}") + @DeleteMapping("/{id}") public String adminPageDelete(@PathVariable Long id) { - pm.delete(id); - return "redirect:/admin"; + productService.delete(id); + return "redirect:/admin/products"; + } private void changeCheckAndUpdate(Long id, ProductDTO dto) { - if (dto.getName().length()>0){ - pm.updateName(id, dto.getName()); + + if (dto.getName().length() > 0) { + productService.updateName(id, dto.getName()); } - if (dto.getPrice()!=null){ - pm.updatePrice(id, dto.getPrice()); + if (dto.getPrice() != null) { + productService.updatePrice(id, dto.getPrice()); } - if (dto.getImageUrl().length()>0){ - pm.updateImageUrl(id, dto.getImageUrl()); + if (dto.getImageUrl().length() > 0) { + productService.updateImageUrl(id, dto.getImageUrl()); } } } diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java new file mode 100644 index 000000000..451a6ab65 --- /dev/null +++ b/src/main/java/gift/controller/MemberController.java @@ -0,0 +1,39 @@ +package gift.controller; + +import gift.dto.LoginMemberToken; +import gift.dto.MemberDTO; +import gift.model.MemberRole; +import gift.service.MemberService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/api/member") +@RestController +public class MemberController { + + private final MemberService memberService; + + public MemberController(MemberService memberService) { + this.memberService = memberService; + } + + @PutMapping + public void register(@RequestBody @Valid MemberDTO memberDTO) { + if (memberDTO.getRole() == null) { + memberDTO.setRole(MemberRole.COMMON_MEMBER); + } + memberService.register(memberDTO); + } + + @GetMapping("/login") + public LoginMemberToken login(@RequestParam("email") @NotBlank String email, + @RequestParam("password") @NotBlank String password) { + return memberService.login(new MemberDTO(email, password, null)); + } +} diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java index 7b8a3de37..d2821825f 100644 --- a/src/main/java/gift/controller/ProductController.java +++ b/src/main/java/gift/controller/ProductController.java @@ -2,14 +2,15 @@ import gift.dto.ProductDTO; import gift.service.ProductService; +import jakarta.validation.Valid; import java.util.List; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** @@ -17,61 +18,67 @@ *

* $/api/products */ + +@RequestMapping("/api") @RestController public class ProductController { - private final ProductService pm; + private final ProductService productService; - public ProductController(ProductService pm) { - this.pm = pm; + public ProductController(ProductService productService) { + this.productService = productService; } /** * 상품 전체 목록 반환 + * * @return 상품 DTO */ - @GetMapping("/api/products") - public List getList(){ - List dto = pm.readAll(); + @GetMapping("/products") + public List getList() { + List dto = productService.readAll(); return dto; } /** * 새로운 상품 생성 + + * * @param dto id가 존재하는 상태로 입력되더라도 무시됨. */ - @PostMapping("/api/products") - public void add(ProductDTO dto){ - pm.create(dto); + @PostMapping("/products") + public void add(@RequestBody @Valid ProductDTO dto) { + productService.create(dto); } /** * 기존 상품 수정 - * @param id 수정하고자 하는 상품의 id + * + * @param id 수정하고자 하는 상품의 id * @param dto 수정하고자 하는 값 이외 null로 지정 */ - @PutMapping("/api/products") - public void update(@RequestParam("id") Long id, @RequestBody ProductDTO dto){ - if(id==null){ + @PutMapping("/products/{id}") + public void update(@PathVariable Long id, @RequestBody @Valid ProductDTO dto) { + if (id == null) { throw new IllegalArgumentException("id를 입력해주세요"); } changeCheckAndUpdate(id, dto); } - @DeleteMapping("/api/products") - public void delete(@RequestParam("id") Long id){ - pm.delete(id); + @DeleteMapping("/products/{id}") + public void delete(@PathVariable Long id) { + productService.delete(id); } private void changeCheckAndUpdate(Long id, ProductDTO dto) { - if (dto.getName()!=null){ - pm.updateName(id, dto.getName()); + if (dto.getName() != null) { + productService.updateName(id, dto.getName()); } - if (dto.getPrice()!=null){ - pm.updatePrice(id, dto.getPrice()); + if (dto.getPrice() != null) { + productService.updatePrice(id, dto.getPrice()); } - if (dto.getImageUrl()!=null){ - pm.updateImageUrl(id, dto.getImageUrl()); + if (dto.getImageUrl() != null) { + productService.updateImageUrl(id, dto.getImageUrl()); } } diff --git a/src/main/java/gift/controller/WishListController.java b/src/main/java/gift/controller/WishListController.java new file mode 100644 index 000000000..a9626fdff --- /dev/null +++ b/src/main/java/gift/controller/WishListController.java @@ -0,0 +1,53 @@ +package gift.controller; + +import gift.ArgumentResolver.LoginMember; +import gift.dto.MemberDTO; +import gift.dto.WishListDTO; +import gift.service.WishListService; +import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/api/wishlist") +@RestController +public class WishListController { + + private final WishListService wishListService; + + public WishListController(WishListService wishListService) { + this.wishListService = wishListService; + } + + @GetMapping + public List getWishList(@LoginMember MemberDTO memberDTO) { + return wishListService.getWishList(memberDTO.getId()); + } + + //상품 추가 + @PostMapping + public void addWishList(@LoginMember MemberDTO memberDTO, + @RequestBody WishListDTO wishListDTO) { + //wishListDTO.setMemberId(memberDTO.getId()); + wishListService.addProduct(wishListDTO.getMemberId(), wishListDTO.getProductId()); + } + + //상품 삭제 + @DeleteMapping + public void deleteWishList(@RequestBody WishListDTO wishListDTO) { + wishListService.deleteProduct(wishListDTO.getMemberId(), wishListDTO.getProductId()); + } + + //상품 수정 + @PutMapping + public void updateWishList(@RequestBody WishListDTO wishListDTO) { + wishListService.updateProduct(wishListDTO.getMemberId(), wishListDTO.getProductId(), + wishListDTO.getProductValue()); + } + + +} diff --git a/src/main/java/gift/database/JdbcMemeberRepository.java b/src/main/java/gift/database/JdbcMemeberRepository.java new file mode 100644 index 000000000..6563b79f4 --- /dev/null +++ b/src/main/java/gift/database/JdbcMemeberRepository.java @@ -0,0 +1,75 @@ +package gift.database; + +import gift.model.Member; +import gift.model.MemberRole; +import java.sql.PreparedStatement; +import javax.sql.DataSource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +@Repository +public class JdbcMemeberRepository { + + private final JdbcTemplate template; + + public JdbcMemeberRepository(DataSource dataSource) { + this.template = new JdbcTemplate(dataSource); + createTable(); + } + + private void createTable() { + template.update( + "create table if not exists member(" + + "id long primary key auto_increment, " + + "email varchar(255) unique not null, " + + "password varchar(255) not null, " + + "role varchar(255) not null)"); + } + + + public void create(String email, String password, String role) { + String sql = "insert into member (email, password,role) values (?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + template.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, email); + ps.setString(2, password); + ps.setString(3, role); + return ps; + }, keyHolder); + } + + public void update(long id, Member member) { + String sql = "update member set email=?, password=?, role=? where id=?"; + template.update(sql, member.getEmail(), member.getPassword(), member.getRole().toString(), + id); + } + + public void delete(long id) { + String sql = "delete from member where id = ?"; + template.update(sql, id); + } + + public Member findById(long id) { + String sql = "select * from member where id = ?"; + return template.queryForObject(sql, memberRowMapper(), id); + } + + public Member findByEmail(String email) { + String sql = "select * from member where email = ?"; + return template.queryForObject(sql, memberRowMapper(), email); + } + + private RowMapper memberRowMapper() { + return (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("email"), + rs.getString("password"), + MemberRole.valueOf(rs.getString("role"))); + } + + +} diff --git a/src/main/java/gift/database/JdbcProductRepository.java b/src/main/java/gift/database/JdbcProductRepository.java index 10db1ac7b..9ed6db39a 100644 --- a/src/main/java/gift/database/JdbcProductRepository.java +++ b/src/main/java/gift/database/JdbcProductRepository.java @@ -28,46 +28,46 @@ private void createTable() { + "imageUrl varchar(255))"); } - /** - * 새로운 상품 추가 - * @param product id 값은 무시됨. - * @return DB에 저장된 id값이 포함된 객체 반환 - */ - public Product insertProduct(Product product) { + + public Product create(String name, int price, String imageUrl) { String sql = "insert into product (name, price, imageUrl) values (?,?,?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); template.update(connection -> { PreparedStatement ps = connection.prepareStatement(sql,new String[]{"id"}); - ps.setString(1, product.getName()); - ps.setInt(2, product.getPrice()); - ps.setString(3, product.getImageUrl()); + + ps.setString(1, name); + ps.setInt(2, price); + ps.setString(3, imageUrl); return ps; },keyHolder); long key = keyHolder.getKey().longValue(); - product.setId(key); - return product; + + return new Product(key, name, price, imageUrl); } //기존 상품 수정 - public void updateProduct(Long id,Product product) { + public void update(long id, Product product) { String sql = "update product set name = ?, price = ?, imageUrl = ? where id = ?"; template.update(sql,product.getName(),product.getPrice(),product.getImageUrl(),id); } //상품 단일 조회 - public Product getProduct(Long id) { + + public Product findById(long id) { String sql = "select * from product where id = ?"; return template.queryForObject(sql,productRowMapper(),id); } //상품 전체 조회 - public List findAllProducts() { + + public List findAll() { String sql = "select * from product"; return template.query(sql,productRowMapper()); } //상품 삭제 - public void deleteProduct(Long id) { + + public void delete(long id) { String sql = "delete from product where id = ?"; template.update(sql,id); } diff --git a/src/main/java/gift/database/JdbcWishListRepository.java b/src/main/java/gift/database/JdbcWishListRepository.java new file mode 100644 index 000000000..eff0d0530 --- /dev/null +++ b/src/main/java/gift/database/JdbcWishListRepository.java @@ -0,0 +1,86 @@ +package gift.database; + +import gift.model.WishList; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import javax.sql.DataSource; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +@Repository +public class JdbcWishListRepository { + + private final JdbcTemplate template; + + public JdbcWishListRepository(DataSource dataSource) { + this.template = new JdbcTemplate(dataSource); + createTable(); + } + + private void createTable() { + template.update("create table if not exists wishlist(" + + "id long primary key auto_increment, " + + "member_id long not null," + + "product_id long not null," + + "product_value int not null)"); + } + + public WishList findByMemeberId(Long memberId) { + String sql = "select * from wishlist where member_id = ?"; + return template.queryForObject(sql, wishListRowMapper()); + } + + public void insertWishList(WishList wishList) { + String sql = "insert into wishlist (member_id, product_id, product_value) values (?,?,?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + List productIds = wishList.getWishList().keySet().stream().toList(); + + template.batchUpdate(sql, new BatchPreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + + ps.setLong(1, wishList.getMemberId()); + ps.setLong(2, productIds.get(i)); + ps.setInt(3, wishList.getWishList().get(productIds.get(i))); + } + + @Override + public int getBatchSize() { + return 1; + } + }); + } + + public void updateWishList(WishList wishList) { + String sql = "update wishlist set member_id = ?, product_id = ?," + + " product_value = ? where member_id =?,product_id=?"; + for (long productId : wishList.getWishList().keySet()) { + template.update(sql, wishList.getMemberId(), productId, + wishList.getWishList().get(productId)); + } + } + + public void deleteWishList(long memberId, long productId) { + String sql = "delete from wishlist where member_id = ? and product_id = ?"; + template.update(sql, memberId, productId); + } + + private RowMapper wishListRowMapper() { + return (rs, rowNum) -> { + WishList wishList = new WishList( + rs.getLong("id"), + rs.getLong("member_id"), + new HashMap<>() + ); + wishList.updateProduct(rs.getLong("product_id"), rs.getInt("product_value")); + return wishList; + }; + } +} diff --git a/src/main/java/gift/dto/ExceptionResponse.java b/src/main/java/gift/dto/ExceptionResponse.java new file mode 100644 index 000000000..1a9021905 --- /dev/null +++ b/src/main/java/gift/dto/ExceptionResponse.java @@ -0,0 +1,12 @@ +package gift.dto; + +public class ExceptionResponse { + private String message; + + public ExceptionResponse(String message) { + this.message = message; + } + public String getMessage() { + return message; + } +} diff --git a/src/main/java/gift/dto/LoginMemberToken.java b/src/main/java/gift/dto/LoginMemberToken.java new file mode 100644 index 000000000..63e4a7069 --- /dev/null +++ b/src/main/java/gift/dto/LoginMemberToken.java @@ -0,0 +1,21 @@ +package gift.dto; + +public class LoginMemberToken { + + private String token; + + public LoginMemberToken() { + } + + public LoginMemberToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/java/gift/dto/MemberDTO.java b/src/main/java/gift/dto/MemberDTO.java new file mode 100644 index 000000000..fea9789e5 --- /dev/null +++ b/src/main/java/gift/dto/MemberDTO.java @@ -0,0 +1,65 @@ +package gift.dto; + +import gift.model.MemberRole; +import jakarta.validation.constraints.NotBlank; + +public class MemberDTO { + + private Long id; + @NotBlank(message = "이메일을 입력해주세요") + private String email; + @NotBlank(message = "비밀번호를 입력해주세요") + private String password; + private MemberRole role; + + public MemberDTO() { + } + + public MemberDTO(String email, String password, MemberRole role) { + this.email = email; + this.password = password; + this.role = role; + if (this.role == null) { + this.role = MemberRole.COMMON_MEMBER; + } + } + + public MemberDTO(Long id, String email, String password, MemberRole role) { + this.id = id; + this.email = email; + this.password = password; + 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 MemberRole getRole() { + return role; + } + + public void setRole(MemberRole role) { + this.role = role; + } +} diff --git a/src/main/java/gift/dto/ProductDTO.java b/src/main/java/gift/dto/ProductDTO.java index 12b4a8dbc..5478b916f 100644 --- a/src/main/java/gift/dto/ProductDTO.java +++ b/src/main/java/gift/dto/ProductDTO.java @@ -2,15 +2,27 @@ import gift.model.Product; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + + public class ProductDTO { - private Long id; - private String name; - private Integer price; - private String imageUrl; + private Long id; + @Pattern(regexp = "^[ㄱ-ㅎ가-힣a-zA-Z0-9\\(\\)\\[\\]\\+\\-&/_]+$", message = "사용할 수 없는 특수문자입니다.") + @Size(min = 1, max = 15, message = "상품 이름은 1~15글자로 제한됩니다.") + //이 정규표현식을 만족해야지만 ok + private String name; + @NotNull(message = "가격을 입력해주세요") + private Integer price; + @NotBlank(message = "이미지 주소를 입력해주세요") + private String imageUrl; //타임리프 사용을 위한 기본 생성자 - public ProductDTO() {} + public ProductDTO() { + } public ProductDTO(Long id, String name, Integer price, String imageUrl) { this.id = id; @@ -30,12 +42,9 @@ public ProductDTO(Product product) { //디버깅 필요 시 체크용 toString @Override public String toString() { - return "ProductDTO{" + - "id=" + id + - ", name='" + name + '\'' + - ", price=" + price + - ", imageUrl='" + imageUrl + '\'' + - '}'; + + return "ProductDTO{" + "id=" + id + ", name='" + name + '\'' + ", price=" + price + + ", imageUrl='" + imageUrl + '\'' + '}'; } public Long getId() { diff --git a/src/main/java/gift/dto/WishListDTO.java b/src/main/java/gift/dto/WishListDTO.java new file mode 100644 index 000000000..64bb8ae60 --- /dev/null +++ b/src/main/java/gift/dto/WishListDTO.java @@ -0,0 +1,41 @@ +package gift.dto; + +public class WishListDTO { + + private Long memberId; + private Long productId; + private Integer productValue; + + public WishListDTO() { + } + + public WishListDTO(Long memberId, Long productId, Integer productValue) { + this.memberId = memberId; + this.productId = productId; + this.productValue = productValue; + } + + 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; + } + + public Integer getProductValue() { + return productValue; + } + + public void setProductValue(Integer productValue) { + this.productValue = productValue; + } +} diff --git a/src/main/java/gift/exceptionAdvisor/ExceptionAdvisor.java b/src/main/java/gift/exceptionAdvisor/ExceptionAdvisor.java new file mode 100644 index 000000000..fc7674a96 --- /dev/null +++ b/src/main/java/gift/exceptionAdvisor/ExceptionAdvisor.java @@ -0,0 +1,47 @@ +package gift.exceptionAdvisor; + + +import gift.dto.ExceptionResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; + +@ControllerAdvice +@RestController +public class ExceptionAdvisor { + + /* + ProductController 유효성 검사 실패 핸들러 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity productValidationException( + MethodArgumentNotValidException exception) { + return new ResponseEntity<>(new ExceptionResponse( + exception.getBindingResult().getFieldError().getDefaultMessage()), + exception.getStatusCode()); + } + + /* + ProductService 예외 핸들러 + 금지된 문구 사용 etc + */ + @ExceptionHandler(ProductServiceException.class) + public ResponseEntity productServiceException( + ProductServiceException exception) { + return new ResponseEntity<>(new ExceptionResponse(exception.getMessage()), + exception.getStatusCode()); + } + + /* + MemberService 예외 핸들러 + 이메일 중복 등 + */ + @ExceptionHandler(MemberServiceException.class) + public ResponseEntity memberServiceException( + MemberServiceException exception) { + return new ResponseEntity<>(new ExceptionResponse(exception.getMessage()), + exception.getStatusCode()); + } +} diff --git a/src/main/java/gift/exceptionAdvisor/MemberServiceException.java b/src/main/java/gift/exceptionAdvisor/MemberServiceException.java new file mode 100644 index 000000000..660ff4314 --- /dev/null +++ b/src/main/java/gift/exceptionAdvisor/MemberServiceException.java @@ -0,0 +1,21 @@ +package gift.exceptionAdvisor; + +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; + +public class MemberServiceException extends RuntimeException { + + private HttpStatus responseStatus; + + public MemberServiceException() { + } + + public MemberServiceException(String message, HttpStatus responseStatus) { + super(message); + this.responseStatus = responseStatus; + } + + public HttpStatusCode getStatusCode() { + return responseStatus; + } +} diff --git a/src/main/java/gift/exceptionAdvisor/ProductServiceException.java b/src/main/java/gift/exceptionAdvisor/ProductServiceException.java new file mode 100644 index 000000000..46723054b --- /dev/null +++ b/src/main/java/gift/exceptionAdvisor/ProductServiceException.java @@ -0,0 +1,20 @@ +package gift.exceptionAdvisor; + +import org.springframework.http.HttpStatus; + +public class ProductServiceException extends RuntimeException { + + private HttpStatus responseStatus; + + public ProductServiceException() { + } + + public ProductServiceException(String message, HttpStatus responseStatus) { + super(message); + this.responseStatus = responseStatus; + } + + public HttpStatus getStatusCode() { + return responseStatus; + } +} diff --git a/src/main/java/gift/model/Member.java b/src/main/java/gift/model/Member.java new file mode 100644 index 000000000..d19785e8d --- /dev/null +++ b/src/main/java/gift/model/Member.java @@ -0,0 +1,57 @@ +package gift.model; + +public class Member { + + private Long id; + private String email; + private String password; + private MemberRole role; + private String token; + + public Member(Long id, String email, String password, MemberRole role) { + this.id = id; + this.email = email; + this.password = password; + 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 MemberRole getRole() { + return role; + } + + public void setRole(MemberRole role) { + this.role = role; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/java/gift/model/MemberRole.java b/src/main/java/gift/model/MemberRole.java new file mode 100644 index 000000000..2a555e5bc --- /dev/null +++ b/src/main/java/gift/model/MemberRole.java @@ -0,0 +1,5 @@ +package gift.model; + +public enum MemberRole { + GUEST, COMMON_MEMBER, ADMIN_MEMBER +} diff --git a/src/main/java/gift/model/WishList.java b/src/main/java/gift/model/WishList.java new file mode 100644 index 000000000..b2e3c88a8 --- /dev/null +++ b/src/main/java/gift/model/WishList.java @@ -0,0 +1,36 @@ +package gift.model; + +import java.util.Map; + +public class WishList { + + private Long id; + private Long memberId; + private Map wishList; + + public WishList(Long id, Long memberId, Map wishList) { + this.id = id; + this.memberId = memberId; + this.wishList = wishList; + } + + public Long getId() { + return id; + } + + public Long getMemberId() { + return memberId; + } + + public Map getWishList() { + return wishList; + } + + public void updateProduct(Long productId, Integer count) { + wishList.put(productId, count); + } + + public void deleteProduct(Long productId) { + wishList.remove(productId); + } +} diff --git a/src/main/java/gift/service/AuthenticationTool.java b/src/main/java/gift/service/AuthenticationTool.java new file mode 100644 index 000000000..52c2fa014 --- /dev/null +++ b/src/main/java/gift/service/AuthenticationTool.java @@ -0,0 +1,37 @@ +package gift.service; + +import gift.exceptionAdvisor.MemberServiceException; +import gift.model.Member; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.Jwts.SIG; +import javax.crypto.SecretKey; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +@Service +public class AuthenticationTool { + + private final SecretKey key = SIG.HS256.key().build(); + + public AuthenticationTool() { + } + + public String makeToken(Member member) { + return Jwts.builder().claim("id", member.getId()) + .signWith(key).compact(); + } + + public long parseToken(String token) { + try { + var claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(token) + .getPayload();//TODO : 수정필요 + return Long.parseLong(claims.get("id").toString()); + } catch (JwtException e) { + throw new MemberServiceException("JWT 인증 실패", HttpStatus.FORBIDDEN); + } + + } + + +} diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java new file mode 100644 index 000000000..21d8dee99 --- /dev/null +++ b/src/main/java/gift/service/MemberService.java @@ -0,0 +1,15 @@ +package gift.service; + +import gift.dto.LoginMemberToken; +import gift.dto.MemberDTO; + +public interface MemberService { + + void register(MemberDTO memberDTO); + + LoginMemberToken login(MemberDTO memberDTO); + + boolean checkRole(MemberDTO memberDTO); + + MemberDTO getLoginUser(String token); +} diff --git a/src/main/java/gift/service/MemberServiceImpl.java b/src/main/java/gift/service/MemberServiceImpl.java new file mode 100644 index 000000000..1e554a518 --- /dev/null +++ b/src/main/java/gift/service/MemberServiceImpl.java @@ -0,0 +1,77 @@ +package gift.service; + +import gift.database.JdbcMemeberRepository; +import gift.dto.LoginMemberToken; +import gift.dto.MemberDTO; +import gift.exceptionAdvisor.MemberServiceException; +import gift.model.Member; +import gift.model.MemberRole; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +@Service +public class MemberServiceImpl implements MemberService { + + private JdbcMemeberRepository jdbcMemeberRepository; + + private AuthenticationTool authenticationTool; + + public MemberServiceImpl(JdbcMemeberRepository jdbcMemeberRepository, + AuthenticationTool authenticationTool) { + this.jdbcMemeberRepository = jdbcMemeberRepository; + this.authenticationTool = authenticationTool; + } + + @Override + public void register(MemberDTO memberDTO) { + if (checkEmailDuplication(memberDTO.getEmail())) { + throw new MemberServiceException("이메일이 중복됩니다", HttpStatus.FORBIDDEN); + } + + jdbcMemeberRepository.create(memberDTO.getEmail(), memberDTO.getPassword(), + MemberRole.COMMON_MEMBER.toString()); + } + + @Override + public LoginMemberToken login(MemberDTO memberDTO) { + Member member = findByEmail(memberDTO.getEmail()); + + if (memberDTO.getPassword().equals(member.getPassword())) { + String token = authenticationTool.makeToken(member); + return new LoginMemberToken(token); + } + + throw new MemberServiceException("잘못된 로그인 시도입니다.", HttpStatus.FORBIDDEN); + } + + @Override + public boolean checkRole(MemberDTO memberDTO) { + return false; + } + + @Override + public MemberDTO getLoginUser(String token) { + long id = authenticationTool.parseToken(token); + Member member = jdbcMemeberRepository.findById(id); + return new MemberDTO(member.getEmail(), member.getPassword(), member.getRole()); + } + + + private boolean checkEmailDuplication(String email) { + try { + jdbcMemeberRepository.findByEmail(email); + return true; + } catch (EmptyResultDataAccessException e) { + return false; + } + } + + private Member findByEmail(String email) { + try { + return jdbcMemeberRepository.findByEmail(email); + } catch (EmptyResultDataAccessException e) { + throw new MemberServiceException("잘못된 로그인 시도입니다.", HttpStatus.FORBIDDEN); + } + } +} diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index 68c5f3456..a316ccce1 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -5,22 +5,17 @@ public interface ProductService { - /* - 상품 수정 시 만약 문제가 생기면 어떤 값에 의해서 생기는 지 파악의 어려움을 막기 위해 - update를 각 항목마다 분리 - */ - //상품 리스트 전체 조회 List readAll(); - //새 상품 생성 + void create(ProductDTO prod); - //상품 이름 변경 - void updateName(long id,String name); - //상품 가격 변경 - void updatePrice(long id,int price); - //상품 이미지 변경 - void updateImageUrl(long id,String url); - //상품 삭제 + + void updateName(long id, String name); + + void updatePrice(long id, int price); + + void updateImageUrl(long id, String url); + void delete(long id); } diff --git a/src/main/java/gift/service/ProductServiceImpl.java b/src/main/java/gift/service/ProductServiceImpl.java index 393bb4d14..4f35e9397 100644 --- a/src/main/java/gift/service/ProductServiceImpl.java +++ b/src/main/java/gift/service/ProductServiceImpl.java @@ -2,13 +2,14 @@ import gift.database.JdbcProductRepository; import gift.dto.ProductDTO; -import gift.model.Product; -import java.util.ArrayList; +import gift.exceptionAdvisor.ProductServiceException; import java.util.List; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @Service -public class ProductServiceImpl implements ProductService{ +public class ProductServiceImpl implements ProductService { + private final JdbcProductRepository jdbcProductRepository; @@ -18,47 +19,60 @@ public ProductServiceImpl(JdbcProductRepository jdbcProductRepository) { @Override public List readAll() { - var products = jdbcProductRepository.findAllProducts(); - List productDTOList = new ArrayList<>(); - for(var product : products) { //DTO로 전환 - productDTOList.add(new ProductDTO(product)); - } + var products = jdbcProductRepository.findAll(); - return productDTOList; + return products.stream().map(product -> new ProductDTO(product.getId(), product.getName(), + product.getPrice(), product.getImageUrl())).toList(); } //새로운 상품 추가 @Override - public void create(ProductDTO prod) { - jdbcProductRepository.insertProduct(new Product(null,prod.getName(),prod.getPrice(),prod.getImageUrl())); + + public void create(ProductDTO dto) { + checkKakao(dto.getName()); + jdbcProductRepository.create(dto.getName(), dto.getPrice(), dto.getImageUrl()); + } @Override public void updateName(long id, String name) { - var prod = jdbcProductRepository.getProduct(id); + var prod = jdbcProductRepository.findById(id); + checkKakao(prod.getName()); prod.setName(name); - jdbcProductRepository.updateProduct(id,prod); + jdbcProductRepository.update(id, prod); + } @Override public void updatePrice(long id, int price) { - var prod = jdbcProductRepository.getProduct(id); + var prod = jdbcProductRepository.findById(id); prod.setPrice(price); - jdbcProductRepository.updateProduct(id,prod); + jdbcProductRepository.update(id, prod); + } @Override public void updateImageUrl(long id, String url) { - var prod = jdbcProductRepository.getProduct(id); + var prod = jdbcProductRepository.findById(id); prod.setImageUrl(url); - jdbcProductRepository.updateProduct(id,prod); + jdbcProductRepository.update(id, prod); + } @Override public void delete(long id) { - jdbcProductRepository.deleteProduct(id); + + jdbcProductRepository.delete(id); + } + + private void checkKakao(String productName) { + if (productName.contains("카카오")) { + throw new ProductServiceException("카카오 문구는 md협의 이후 사용할 수 있습니다.", + HttpStatus.BAD_REQUEST); + } + } } diff --git a/src/main/java/gift/service/WishListService.java b/src/main/java/gift/service/WishListService.java new file mode 100644 index 000000000..0269423af --- /dev/null +++ b/src/main/java/gift/service/WishListService.java @@ -0,0 +1,16 @@ +package gift.service; + +import gift.dto.WishListDTO; +import java.util.List; + +public interface WishListService { + + void addProduct(long memberId, long productId); + + void deleteProduct(long memberId, long productId); + + void updateProduct(long memberId, long productId, int productValue); + + List getWishList(long memberId); + +} diff --git a/src/main/java/gift/service/WishListServiceImpl.java b/src/main/java/gift/service/WishListServiceImpl.java new file mode 100644 index 000000000..a9208c8c2 --- /dev/null +++ b/src/main/java/gift/service/WishListServiceImpl.java @@ -0,0 +1,46 @@ +package gift.service; + +import gift.database.JdbcWishListRepository; +import gift.dto.WishListDTO; +import gift.model.WishList; +import java.util.HashMap; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class WishListServiceImpl implements WishListService { + + private JdbcWishListRepository jdbcWishListRepository; + + public WishListServiceImpl(JdbcWishListRepository jdbcWishListRepository) { + this.jdbcWishListRepository = jdbcWishListRepository; + } + + @Override + public void addProduct(long memberId, long productId) { + WishList wishList = new WishList(null, memberId, new HashMap<>()); + wishList.updateProduct(productId, 1); + jdbcWishListRepository.insertWishList(wishList); + } + + @Override + public void deleteProduct(long memberId, long productId) { + jdbcWishListRepository.deleteWishList(memberId, productId); + } + + @Override + public void updateProduct(long memberId, long productId, int productValue) { + WishList wishList = jdbcWishListRepository.findByMemeberId(memberId); + wishList.updateProduct(productId, productValue); + jdbcWishListRepository.updateWishList(wishList); + } + + @Override + public List getWishList(long memberId) { + WishList wishList = jdbcWishListRepository.findByMemeberId(memberId); + var products = wishList.getWishList(); + return products.keySet().stream() + .map(key -> new WishListDTO(wishList.getMemberId(), key, products.get(key))).toList(); + + } +} diff --git a/src/main/resources/templates/admin/index.html b/src/main/resources/templates/admin/index.html new file mode 100644 index 000000000..47c7a64a3 --- /dev/null +++ b/src/main/resources/templates/admin/index.html @@ -0,0 +1,66 @@ + + + + + 상품 관리자 페이지 + + +

gift 상품 주문 관리자 페이지

+ + + + + + + + + + + + + +
아이디 상품명 가격 이미지 +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+
+ + + +
+ +
+ + +
+
+ + +
+
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/src/test/java/gift/controller/MemberControllerTest.java b/src/test/java/gift/controller/MemberControllerTest.java new file mode 100644 index 000000000..1ec605bd6 --- /dev/null +++ b/src/test/java/gift/controller/MemberControllerTest.java @@ -0,0 +1,156 @@ +package gift.controller; + +import gift.dto.LoginMemberToken; +import gift.dto.MemberDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import org.springframework.web.reactive.function.BodyInserters; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class MemberControllerTest { + + @LocalServerPort + private int port; + private String baseUrl; + private WebTestClient webClient; + + @BeforeEach + void setUp() { + baseUrl = "http://localhost:" + port; + webClient = WebTestClient.bindToServer().baseUrl(baseUrl).build(); + } + + @Test + @DisplayName("회원가입 성공") + void registerMember() { + //given + String email = "asdef@gmail.com"; + String password = "abcd"; + MemberDTO dto = new MemberDTO(email, password, null); + + //when + ResponseSpec responseSpec = registerMemberPutRequest(dto); + + //then + responseSpec.expectStatus().isOk(); + } + + @Test + @DisplayName("중복된 이메일로 회원가입 시도") + void registerDuplicateEmail() { + //given + String email = "abcd@gmail.com"; + + MemberDTO dto = new MemberDTO(email, "1234", null); + MemberDTO dto2 = new MemberDTO(email, "4567", null); + + //when + registerMemberPutRequest(dto); + ResponseSpec responseSpec2 = registerMemberPutRequest(dto2); + + responseSpec2.expectStatus().isForbidden(); + } + + @Test + @DisplayName("이메일을 입력하지 않은 경우") + void nullEmail() { + //given + MemberDTO dto = new MemberDTO(null, "1234", null); + + //when + ResponseSpec responseSpec = registerMemberPutRequest(dto); + + //then + responseSpec.expectStatus().isBadRequest(); + } + + @Test + @DisplayName("패스워드를 입력하지 않은 경우") + void nullPassword() { + //given + MemberDTO dto = new MemberDTO("abcd@abcd", null, null); + + //when + ResponseSpec responseSpec = registerMemberPutRequest(dto); + + //then + responseSpec.expectStatus().isBadRequest(); + } + + + @Test + @DisplayName("로그인 성공") + void login() { + //given + String email = "abcd@gmail.com"; + String password = "abcd"; + MemberDTO dto = new MemberDTO(email, password, null); + registerMemberPutRequest(dto); + + //when + ResponseSpec responseSpec = webClient.get().uri(uriBuilder -> uriBuilder + .path("/api/member/login") + .queryParam("email", dto.getEmail()) + .queryParam("password", dto.getPassword()) + .build()).accept(MediaType.APPLICATION_JSON).exchange(); + + //then + responseSpec.expectStatus().isOk(); + responseSpec.expectBody(LoginMemberToken.class); + } + + @Test + @DisplayName("패스워드가 불일치한 경우") + void wrongPassword() { + //given + String email = "abcd@gmail.com"; + String password = "abcd"; + String wrongPassword = "wrong"; + MemberDTO dto = new MemberDTO(email, password, null); + registerMemberPutRequest(dto); + + //when + ResponseSpec responseSpec = webClient.get().uri(uriBuilder -> uriBuilder + .path("/api/member/login") + .queryParam("email", dto.getEmail()) + .queryParam("password", wrongPassword) + .build()).accept(MediaType.APPLICATION_JSON).exchange(); + + //then + responseSpec.expectStatus().isForbidden(); + + } + + @Test + @DisplayName("회원가입이 되지않은 이메일로 로그인을 시도하는 경우") + void noRegister() { + //given + String email = "imno@gmail.com"; + String password = "abcd"; + MemberDTO dto = new MemberDTO(email, password, null); + + //when + ResponseSpec responseSpec = webClient.get().uri(uriBuilder -> uriBuilder + .path("/api/member/login") + .queryParam("email", dto.getEmail()) + .queryParam("password", dto.getPassword()) + .build()).accept(MediaType.APPLICATION_JSON).exchange(); + + //then + responseSpec.expectStatus().isForbidden(); + } + + + private ResponseSpec registerMemberPutRequest(MemberDTO dto) { + ResponseSpec responseSpec = webClient.put().uri("/api/member") + .accept(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(dto)).exchange(); + return responseSpec; + } + +} \ No newline at end of file diff --git a/src/test/java/gift/controller/ProductControllerTest.java b/src/test/java/gift/controller/ProductControllerTest.java new file mode 100644 index 000000000..0daf32819 --- /dev/null +++ b/src/test/java/gift/controller/ProductControllerTest.java @@ -0,0 +1,128 @@ +package gift.controller; + +import gift.dto.ProductDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import org.springframework.web.reactive.function.BodyInserters; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ProductControllerTest { + + @LocalServerPort + private int port; + private String baseUrl; + @Autowired + private WebTestClient webClient; + + @BeforeEach + void setUp() { + baseUrl = "http://localhost:" + port; + webClient = WebTestClient.bindToServer().baseUrl(baseUrl).build(); + } + + //사실상 get test는 필요없다고 생각되어짐. + //post나 put 등의 작업에서 잘못된 데이터에 대해서 해당 응답을 돌려주는 지를 검증해야함. + @Test + @DisplayName("정상 get 응답 확인") + void getProduct() { + + webClient.get().uri("/api/products").accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isOk(); + + } + + + private static ProductDTO getProductDTO(String name, Integer price, String imageUrl) { + return new ProductDTO(null, name, price, imageUrl); + } + + @Test + @DisplayName("name이 15글자가 넘는 경우 실패") + void addMore15word() { + //given + String name = "123123123123123123123"; + int price = 123; + String imageUrl = "abcd"; + ProductDTO dto = getProductDTO(name, price, imageUrl); + + //when then + createPostAndCheckBadRequest(dto, "상품 이름은 1~15글자로 제한됩니다."); + + } + + @Test + @DisplayName("name이 0글자인 경우 실패") + void addZero() { + //given + ProductDTO dto = getProductDTO("", 123, "test"); + + //when + createPostAndCheckBadRequest(dto, "상품 이름은 1~15글자로 제한됩니다."); + + } + + @Test + @DisplayName("price가 0인 경우 실패") + void priceZero() { + + ProductDTO dto = getProductDTO("asd", null, "test"); + + createPostAndCheckBadRequest(dto, "가격을 입력해주세요"); + + } + + @Test + @DisplayName("imgUrl 이 없는 경우 실패") + void imgUrlNotInput() { + ProductDTO dto = getProductDTO("asd", 123, null); + + createPostAndCheckBadRequest(dto, "이미지 주소를 입력해주세요"); + } + + @Test + @DisplayName("사용할 수 없는 특수문자") + void nameCantUseWords() { + ProductDTO dto = getProductDTO("asd!", 123, "test"); + ProductDTO dto2 = getProductDTO("asd?", 123, "test2"); + ProductDTO dto3 = getProductDTO("&/_+-[]()", 123, "test3"); + ProductDTO dto4 = getProductDTO("asdd", 123, "ttt"); + + createPostAndCheckBadRequest(dto, "사용할 수 없는 특수문자입니다."); + createPostAndCheckBadRequest(dto2, "사용할 수 없는 특수문자입니다."); + + createPostReqeust(dto3).expectStatus().isOk(); + createPostReqeust(dto4).expectStatus().isOk(); + } + + @Test + @DisplayName("카카오 사용하기") + void useKakao() { + ProductDTO dto = getProductDTO("나카카오콩따러간다", 123, "test"); + + createPostAndCheckBadRequest(dto, "카카오 문구는 md협의 이후 사용할 수 있습니다."); + } + + //private function// + + + private ResponseSpec createPostReqeust(ProductDTO dto) { + return webClient.post().uri("/api/products").accept(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(dto)).exchange(); + + //request body 에는 BodyInserters.formValue로 객체 -> body 데이터로 변환 + } + + private void createPostAndCheckBadRequest(ProductDTO dto, String compareMsg) { + createPostReqeust(dto).expectStatus().isBadRequest().expectBody().jsonPath("$.message") + .isEqualTo(compareMsg); + } + +} \ No newline at end of file diff --git a/src/test/java/gift/controller/WebTestClientHelper.java b/src/test/java/gift/controller/WebTestClientHelper.java new file mode 100644 index 000000000..57bf87679 --- /dev/null +++ b/src/test/java/gift/controller/WebTestClientHelper.java @@ -0,0 +1,92 @@ +package gift.controller; + +import jakarta.validation.constraints.NotNull; +import java.util.Map; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * webflux 를 이용한 webTestClient를 편하게 사용할 수 있습니다.
build.gradle 추가-> implementation + * 'org.springframework.boot:spring-boot-starter-webflux'
(@SpringBootTest) 에서 Rest api test에 + * 사용할 수 있습니다.
url에 매개변수를 삽입 : uriMakeUseParameters로 url 생성
+ *

+ * 사용예시
+ *

{@code
+ *     @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+ *     class ControllerTest {
+ *
+ *     @LocalServerPort
+ *     private int port;
+ *     WebTestClientHelper webClient;
+ *
+ *     @BeforeEach
+ *     void setUp() {
+ *         webClient = new WebTestClientHelper(port);
+ *     }
+ *
+ *     @Test
+ *     @DisplayName("WebTestClientHelper 정상 작동 확인")
+ *     void isOk() {
+ *         //given
+ *         String url = "/admin/products";
+ *
+ *         //when
+ *         var response = webClient.get(url);
+ *
+ *         //then
+ *         response.expectStatus().isOk();
+ *         response.expectHeader().contentType("application/json");
+ *         response.expectBody().json(...)
+ *
+ *     }
+ *  }
+ */ +public class WebTestClientHelper { + + private final WebTestClient webTestClient; + + public WebTestClientHelper(int port) { + this.webTestClient = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .build(); + } + + public ResponseSpec get(String url) { + return webTestClient.get().uri(url).accept(MediaType.APPLICATION_JSON).exchange(); + } + + public ResponseSpec post(String url, Object body) { + return webTestClient.post().uri(url).accept(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(body)).exchange(); + } + + public ResponseSpec put(String url, Object body) { + return webTestClient.put().uri(url).accept(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(body)).exchange(); + } + + public ResponseSpec delete(String url) { + return webTestClient.delete().uri(url).accept(MediaType.APPLICATION_JSON).exchange(); + } + + public WebTestClient moreAction() { + return webTestClient; + } + + /** + * url을 만들어 준다. + * + * @param path 기본경로 ex)/api/products + * @param parameters url 매개변수 ex) [["email" = "happy"],["password" = "1234"]] + * @return String type 생성된 uri + */ + public String uriMakeUseParameters(@NotNull String path, Map parameters) { + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + builder.path(path); + parameters.forEach(builder::queryParam); + return builder.build().toString(); + + } +} diff --git a/src/test/java/gift/controller/WishListControllerTest.java b/src/test/java/gift/controller/WishListControllerTest.java new file mode 100644 index 000000000..95ce02c28 --- /dev/null +++ b/src/test/java/gift/controller/WishListControllerTest.java @@ -0,0 +1,77 @@ +package gift.controller; + +import gift.dto.LoginMemberToken; +import gift.dto.MemberDTO; +import gift.dto.WishListDTO; +import java.util.HashMap; +import java.util.Objects; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class WishListControllerTest { + + @LocalServerPort + private int port; + WebTestClientHelper webClient; + + @BeforeEach + void setUp() { + webClient = new WebTestClientHelper(port); + } + + @Test + @DisplayName("위시 리스트 아이템 추가") + void addWishList() { + //given + String email = "abec"; + String password = "abecdddd"; + LoginMemberToken loginMemberToken = registerAndLogin(email, password); + webClient.moreAction().post().uri("api/wishlist") + .header("Authorization", loginMemberToken.getToken()) + .accept(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(new WishListDTO(0L, 123L, 1))) + .exchange().expectStatus().isOk(); + } + + + @Test + @DisplayName("위시 리스트 조회") + void getWishList() { + + } + + @Test + @DisplayName("위시 리스트 아이템 수량 변경") + void updateWishList() { + + } + + @Test + @DisplayName("위시 리스트 아이템 삭제") + void deleteWishList() { + + } + + private LoginMemberToken registerAndLogin(String email, String password) { + //register + MemberDTO memberDTO = new MemberDTO(email, password, null); + webClient.put("/api/member", memberDTO); + + //login + HashMap userInfo = new HashMap<>(); + userInfo.put("email", memberDTO.getEmail()); + userInfo.put("password", memberDTO.getPassword()); + String uri = webClient.uriMakeUseParameters("/api/member/login", userInfo); + String token = Objects.requireNonNull( + webClient.moreAction().get().uri(uri).accept(MediaType.APPLICATION_JSON).exchange() + .expectBody(LoginMemberToken.class).returnResult().getResponseBody()).getToken(); + return new LoginMemberToken(token); + } + +} \ No newline at end of file diff --git a/src/test/java/gift/service/MemberServiceTest.java b/src/test/java/gift/service/MemberServiceTest.java new file mode 100644 index 000000000..0de6d9460 --- /dev/null +++ b/src/test/java/gift/service/MemberServiceTest.java @@ -0,0 +1,39 @@ +package gift.service; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import gift.database.JdbcMemeberRepository; +import gift.dto.MemberDTO; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MemberServiceTest { + + @Autowired + private MemberService memberService; + + @Autowired + private JdbcMemeberRepository jdbcMemeberRepository; + + @Test + @DisplayName("기본 로그인 테스트") + void register() { + + //given + //email과 password를 입력하면, 계정을 생성해준다. + //user role이 null 이면 common으로 설정한다. + String email = "testmail"; + MemberDTO memberDTO = new MemberDTO(email, "abcd", null); + + //when + memberService.register(memberDTO); + + //then + assertNotNull(jdbcMemeberRepository.findByEmail(email)); + + } + +} \ No newline at end of file