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