From 3dc52b37bb74df45deede1c1fabe9f46683e4802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Tue, 16 Jul 2024 17:42:01 +0900 Subject: [PATCH 01/56] =?UTF-8?q?docs:=20=EC=87=BC=ED=95=91=EB=AA=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD,=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=EC=82=AC=EC=A0=84,=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EB=A7=81=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8142135..604871b 100644 --- a/README.md +++ b/README.md @@ -1 +1,106 @@ -# spring-shopping-product \ No newline at end of file +# spring-shopping-product + +## 기능요구사항 + +### 상품 + +- 상품을 추가, 조회, 수정, 삭제할 수 있다. +- 상품 이미지는 파일을 업로드하지 않고 URL로 저장한다. +- 상품 이름은 공백을 포함하여 최대 15자까지 입력할 수 있다. +- 특수문자는 `()`,`[]`,`+`,`-`,`&`,`/`,`_`만 가능하다. +- 상품 이름에는 비속어를 포함할 수 없다. + - [PurgoMalum](https://www.purgomalum.com/)에서 욕설이 포함되어 있는지 확인한다. + +### 회원 + +- 회원는 가입, 로그인을 할 수 있다. +- 회원는 이메일, 비밀번호, 이름을 입력하여 가입한다. +- 회원는 이메일과 비밀번호를 입력하여 로그인을 수행하고 토큰을 발급받는다. + - 토큰은 JWT와 SpringSecurity를 사용하여 구현한다. + +### 위시리스트 + +- 발급받은 토큰을 사용하여 위시리스트에 상품을 추가, 조회, 삭제할 수 있다. + +## 용어사전 + +### 공통(Common) + +| 한글명 | 영문명 | 설명 | +|-----|--------|----------------------------| +| 사장님 | Owner | 쇼핑몰에서 상품을 관리하는 회원 | +| 손님 | Client | 쇼핑몰에서 상품을 조회하고 구매를 희망하는 회원 | + +### 상품(Product) + +| 한글명 | 영문명 | 설명 | +|--------|---------------|------------------------------| +| 상품 | Product | 쇼핑몰에 등록된 상품 | +| 상품 아이디 | Product Id | 상품을 식별하기 위한 아이디. 중복될 수 없다. | +| 상품 이름 | Product Name | 상품을 명명하는 이름. 비속어를 사용할 수 없다. | +| 비속어 | Profanity | 불쾌감을 주는 단어로 상품 이름에 사용될 수 없다. | +| 상품 가격 | Product Price | 상품 1개의 가격. 가격은 0원이상이다. | +| 상품 이미지 | Product Image | 상품을 보여주는 이미지. | + +### 회원(Member) + +| 한글명 | 영문명 | 설명 | +|-------|-----------------|---------------------------------------------------| +| 회원 | Member | 쇼핑몰에 가입된 사람 | +| 이메일 | Email | 회원가입 및 로그인 시 사용되는 아이디. 중복될 수 없다. | +| 비밀번호 | Password | 회원가입 및 로그인 시 사용되는 비밀번호. 암호화되어 저장된다. | +| 회원 이름 | Member Name | 회원을 구분하기 위한 이름 | +| 회원 권한 | Member Priority | 회원별 행위를 제한하기 위한 권한. 사장님(Owner), 손님(Client)이 존재한다. | + +### 위시리스트(Wishlist) + +| 한글명 | 영문명 | 설명 | +|-------|----------|-----------------------------| +| 위시리스트 | Wishlist | 손님(Client)이 구매를 희망하는 상품 리스트 | + +## 모델링 + +### 상품 + +#### 프로퍼티 + +- `상품(Product)` + 은 `상품 아이디(Product Id)`, `상품 이름(Product Name)`, `상품 가격(Product Price)`, `상품 이미지(Product Image)`를 + 갖는다. +- `상품 이름(Product Name)`은 공백 포함 최대 15자까지 갖는다. +- `상품 이름(Product Name)`에는 `()`,`[]`,`+`,`-`,`&`,`/`,`_`을 제외한 특수문자를 사용할 수 없다. +- `상품 이름(Product Name)`에는 비속어를 사용할 수 없다. + +#### 행위 + +- `사장님(Owner)`은 `상품(Product)`을 추가할 수 있다. +- `사장님(Owner)`과 `손님(Client)`는 `상품(Product)`을 조회할 수 있다. +- `사장님(Owner)`은 `상품(Product)`을 수정할 수 있다. +- `사장님(Owner)`은 `상품(Product)`을 삭제할 수 있다. + +### 회원 + +#### 프로퍼티 + +- `회원(Member)`은 `이메일(Email)`, `비밀번호(Password)`, `회원 이름(Member Name)`, `회원 권한(Member Priority)`을 갖는다. +- `이메일(Email)`은 다른 회원과 중복될 수 없다. +- `비밀번호(Password)`는 암호화되어야한다. +- `회원 권한(Member Priority)`은 `사장님(Owner)`, `손님(Client)`이 있다. + +#### 행위 + +- `회원(Member)`은 `이메일(Email)`, `비밀번호(Password)`, `회원 이름(Member Name)`을 입력하여 가입한다. + - 기본적으로 갖는 권한은 `손님(Client)`이다. +- `회원(Member)`은 `이메일(Email)`과 `비밀번호(Password)`를 입력하여 로그인한다. + +### 위시리스트 + +#### 프로퍼티 + +- `위시리스트(Wishlist)`는 `상품 아이디(Product Id)`, `이메일(Email)`을 갖는다. + +#### 행위 + +- `손님(Client)`은 `위시리스트(Wishlist)`에 `상품(Product)`을 추가할 수 있다. +- `손님(Client)`은 `위시리스트(Wishlist)`에 `상품(Product)`을 조회할 수 있다. +- `손님(Client)`은 `위시리스트(Wishlist)`에 `상품(Product)`을 삭제할 수 있다. From d6efd0d504c473d86d16d368325fdda7598b3ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Tue, 16 Jul 2024 17:42:01 +0900 Subject: [PATCH 02/56] =?UTF-8?q?docs:=20=EC=87=BC=ED=95=91=EB=AA=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD,=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=EC=82=AC=EC=A0=84,=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EB=A7=81=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8142135..4adde9b 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# spring-shopping-product \ No newline at end of file +# spring-shopping-product + +## 기능요구사항 + +### 상품 + +- 상품을 추가, 조회, 수정, 삭제할 수 있다. +- 상품 이미지는 파일을 업로드하지 않고 URL로 저장한다. +- 상품 이름은 공백을 포함하여 최대 15자까지 입력할 수 있다. +- 특수문자는 `()`,`[]`,`+`,`-`,`&`,`/`,`_`만 가능하다. +- 상품 이름에는 비속어를 포함할 수 없다. + - [PurgoMalum](https://www.purgomalum.com/)에서 욕설이 포함되어 있는지 확인한다. +- 회원이 위시리스트에 상품을 담으면 상품 하트수가 올라간다. + +### 회원 + +- 회원는 가입, 로그인을 할 수 있다. +- 회원는 이메일, 비밀번호, 이름을 입력하여 가입한다. +- 회원는 이메일과 비밀번호를 입력하여 로그인을 수행하고 토큰을 발급받는다. + - 토큰은 JWT와 SpringSecurity를 사용하여 구현한다. + +### 위시리스트 + +- 발급받은 토큰을 사용하여 위시리스트에 상품을 추가, 조회, 삭제할 수 있다. + +## 용어사전 + +### 상품(Product) + +| 한글명 | 영문명 | 설명 | +|--------|---------------------|---------------------------------| +| 상품 | Product | 쇼핑몰에 등록된 상품. | +| 상품 이름 | Product Name | 상품을 구분하는 이름. 비속어를 사용할 수 없다. | +| 비속어 | Profanity | 불쾌감을 주는 단어로 상품 이름에 사용될 수 없다. | +| 상품 가격 | Product Price | 상품 1개의 가격. 가격은 0원이상이다. | +| 상품 이미지 | Product Image | 상품을 보여주는 이미지. | +| 상품 하트수 | Product Heart Count | 상품의 하트 수. 손님이 하트를 누르면 숫자가 올라간다. | + +### 회원(Member) + +| 한글명 | 영문명 | 설명 | +|-------|-----------------|------------------------------------------------| +| 회원 | Member | 쇼핑몰에 가입된 사람. | +| 이메일 | Email | 회원가입 및 로그인 시 사용되는 아이디. 중복될 수 없다. | +| 비밀번호 | Password | 회원가입 및 로그인 시 사용되는 비밀번호. 암호화되어 저장된다. | +| 회원 이름 | Member Name | 회원을 구분하기 위한 이름. | +| 회원 권한 | Member Priority | 회원별 행위를 제한하기 위한 권한. ex) 사장님(Owner), 손님(Client) | + +#### 사장님(Owner) + +| 한글명 | 영문명 | 설명 | +|-----|-------|--------------------| +| 사장님 | Owner | 쇼핑몰에서 상품을 관리하는 회원. | + +#### 손님(Client) + +| 한글명 | 영문명 | 설명 | +|--------|--------------|----------------------------------| +| 손님 | Client | 쇼핑몰에서 상품을 조회하고 구매를 희망하는 회원. | +| 위시 상품 | Wish Product | 손님이 구매를 희망하는 상품. | +| 하트 누르기 | Wish | 손님이 위시 상품 등록을 위해 상품에 하트를 누르는 행위. | + +## 모델링 + +### 상품 + +#### 프로퍼티 + +- `상품(Product)`은 `상품 이름(Product Name)`, `상품 가격(Product Price)`를 + 필수로 `상품 이미지(Product Image)`를 선택적으로 갖는다. +- `상품 이름(Product Name)`은 공백 포함 최대 15자까지 갖는다. +- `상품 이름(Product Name)`에는 `()`,`[]`,`+`,`-`,`&`,`/`,`_`을 제외한 특수문자를 사용할 수 없다. +- `상품 이름(Product Name)`에는 비속어를 사용할 수 없다. + +#### 행위 + +- `사장님(Owner)`은 `상품(Product)`을 추가할 수 있다. +- `사장님(Owner)`과 `손님(Client)`는 `상품(Product)`을 조회할 수 있다. +- `사장님(Owner)`은 `상품(Product)`을 수정할 수 있다. +- `사장님(Owner)`은 `상품(Product)`을 삭제할 수 있다. + +### 회원(Member) + +#### 프로퍼티 + +- `회원(Member)`은 `이메일(Email)`, `비밀번호(Password)`, `회원 이름(Member Name)`, `회원 권한(Member Priority)`을 필수로 + 갖는다. 그리고 `회원 권한(Member Priority)`에 따라 `위시 상품(Wish Product)`를 갖는다. +- `이메일(Email)`은 다른 회원과 중복될 수 없다. +- `비밀번호(Password)`는 암호화되어야한다. +- `회원 권한(Member Priority)`은 `사장님(Owner)`, `손님(Client)`이 있다. + +#### 행위 + +### 사장님(Owner) +- `사장님(Owner)`는 `이메일(Email)`과 `비밀번호(Password)`를 입력하여 로그인한다. + +### 손님(Client) +- `손님(Client)`는 `이메일(Email)`, `비밀번호(Password)`, `회원 이름(Member Name)`을 입력하여 가입한다. + - 기본적으로 갖는 권한은 `손님(Client)`이다. +- `손님(Client)`는 `이메일(Email)`과 `비밀번호(Password)`를 입력하여 로그인한다. +- `손님(Client)`는 `위시 상품(Wish Product)`등록을 위해 하트를 누를 수 있다. +- `손님(Client)`는 `위시 상품(Wish Product)`을 조회할 수 있다. +- `손님(Client)`는 `위시 상품(Wish Product)`을 삭제할 수 있다. From 678d643cc93ecd39239ff32b83cb86cdc2dcd264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Wed, 17 Jul 2024 17:10:58 +0900 Subject: [PATCH 03/56] =?UTF-8?q?feat(Member):=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추상클래스로 생성하여 상속받은 클래스만 도메인객체로 사용가능 --- .../shopping/member/common/domain/Member.java | 44 +++++++++++++++++++ .../member/common/domain/MemberPriority.java | 5 +++ .../member/common/domain/Password.java | 32 ++++++++++++++ .../member/common/domain/PasswordEncoder.java | 8 ++++ 4 files changed, 89 insertions(+) create mode 100644 src/main/java/shopping/member/common/domain/Member.java create mode 100644 src/main/java/shopping/member/common/domain/MemberPriority.java create mode 100644 src/main/java/shopping/member/common/domain/Password.java create mode 100644 src/main/java/shopping/member/common/domain/PasswordEncoder.java diff --git a/src/main/java/shopping/member/common/domain/Member.java b/src/main/java/shopping/member/common/domain/Member.java new file mode 100644 index 0000000..4e25947 --- /dev/null +++ b/src/main/java/shopping/member/common/domain/Member.java @@ -0,0 +1,44 @@ +package shopping.member.common.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; + +@Entity +@Table(name = "members") +@DiscriminatorColumn +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +public abstract class Member { + + @Id + @Column(name = "email", nullable = false) + private String email; + + @Embedded + private Password password; + + @Column(name = "member_name", nullable = false) + private String memberName; + + @Enumerated(EnumType.STRING) + @Column(name = "member_priority", nullable = false) + private MemberPriority memberPriority; + + protected Member() { + } + + protected Member(final String email, final Password password, final String memberName, + final MemberPriority memberPriority) { + this.email = email; + this.password = password; + this.memberName = memberName; + this.memberPriority = memberPriority; + } +} diff --git a/src/main/java/shopping/member/common/domain/MemberPriority.java b/src/main/java/shopping/member/common/domain/MemberPriority.java new file mode 100644 index 0000000..3e7ccc9 --- /dev/null +++ b/src/main/java/shopping/member/common/domain/MemberPriority.java @@ -0,0 +1,5 @@ +package shopping.member.common.domain; + +public enum MemberPriority { + CLIENT, OWNER +} diff --git a/src/main/java/shopping/member/common/domain/Password.java b/src/main/java/shopping/member/common/domain/Password.java new file mode 100644 index 0000000..65d0d27 --- /dev/null +++ b/src/main/java/shopping/member/common/domain/Password.java @@ -0,0 +1,32 @@ +package shopping.member.common.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public class Password { + + @Column(name = "password", nullable = false) + private String password; + + protected Password() { + } + + public Password(final String rawPassword, final PasswordEncoder encoder) { + this.password = encoder.encode(rawPassword); + } + + public boolean isMatch(final String rawPassword, final PasswordEncoder encoder) { + return encoder.matches(rawPassword, password); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } +} diff --git a/src/main/java/shopping/member/common/domain/PasswordEncoder.java b/src/main/java/shopping/member/common/domain/PasswordEncoder.java new file mode 100644 index 0000000..0bc43af --- /dev/null +++ b/src/main/java/shopping/member/common/domain/PasswordEncoder.java @@ -0,0 +1,8 @@ +package shopping.member.common.domain; + +public interface PasswordEncoder { + + String encode(final String rawPassword); + + boolean matches(final String rawPassword, final String password); +} From 8bd6257696ec0a1b3dfdf84e7b3cd715c312d988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Wed, 17 Jul 2024 17:16:26 +0900 Subject: [PATCH 04/56] =?UTF-8?q?feat(BaseEntity):=20=EC=83=9D=EC=84=B1,?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=8B=9C=EA=B0=84=20=EC=B6=94=EC=A0=81?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20BaseEntity=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/common/domain/BaseEntity.java | 21 +++++++++++++++++++ .../shopping/member/common/domain/Member.java | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/main/java/shopping/common/domain/BaseEntity.java diff --git a/src/main/java/shopping/common/domain/BaseEntity.java b/src/main/java/shopping/common/domain/BaseEntity.java new file mode 100644 index 0000000..873b192 --- /dev/null +++ b/src/main/java/shopping/common/domain/BaseEntity.java @@ -0,0 +1,21 @@ +package shopping.common.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/shopping/member/common/domain/Member.java b/src/main/java/shopping/member/common/domain/Member.java index 4e25947..3b0e86c 100644 --- a/src/main/java/shopping/member/common/domain/Member.java +++ b/src/main/java/shopping/member/common/domain/Member.java @@ -10,12 +10,13 @@ import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; import jakarta.persistence.Table; +import shopping.common.domain.BaseEntity; @Entity @Table(name = "members") @DiscriminatorColumn @Inheritance(strategy = InheritanceType.SINGLE_TABLE) -public abstract class Member { +public abstract class Member extends BaseEntity { @Id @Column(name = "email", nullable = false) From 6e7df10b38e1b78d31b12c0abb8cfdb9f5c6470e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Wed, 17 Jul 2024 17:38:51 +0900 Subject: [PATCH 05/56] =?UTF-8?q?feat(Client):=20=EC=86=90=EB=8B=98=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/member/client/domain/Client.java | 18 ++++++++++++++++++ .../member/client/domain/ClientRepository.java | 5 +++++ .../client/infra/JPAClientRepository.java | 9 +++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/main/java/shopping/member/client/domain/Client.java create mode 100644 src/main/java/shopping/member/client/domain/ClientRepository.java create mode 100644 src/main/java/shopping/member/client/infra/JPAClientRepository.java diff --git a/src/main/java/shopping/member/client/domain/Client.java b/src/main/java/shopping/member/client/domain/Client.java new file mode 100644 index 0000000..16de4ad --- /dev/null +++ b/src/main/java/shopping/member/client/domain/Client.java @@ -0,0 +1,18 @@ +package shopping.member.client.domain; + +import jakarta.persistence.Entity; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberPriority; +import shopping.member.common.domain.Password; +import shopping.member.common.domain.PasswordEncoder; + +@Entity +public class Client extends Member { + + protected Client() { + } + + public Client(String email, String rawPassword, PasswordEncoder encoder, String memberName){ + super(email, new Password(rawPassword, encoder), memberName, MemberPriority.CLIENT); + } +} diff --git a/src/main/java/shopping/member/client/domain/ClientRepository.java b/src/main/java/shopping/member/client/domain/ClientRepository.java new file mode 100644 index 0000000..ae3173e --- /dev/null +++ b/src/main/java/shopping/member/client/domain/ClientRepository.java @@ -0,0 +1,5 @@ +package shopping.member.client.domain; + +public interface ClientRepository { + +} diff --git a/src/main/java/shopping/member/client/infra/JPAClientRepository.java b/src/main/java/shopping/member/client/infra/JPAClientRepository.java new file mode 100644 index 0000000..d711980 --- /dev/null +++ b/src/main/java/shopping/member/client/infra/JPAClientRepository.java @@ -0,0 +1,9 @@ +package shopping.member.client.infra; + +import org.springframework.data.jpa.repository.JpaRepository; +import shopping.member.client.domain.Client; +import shopping.member.client.domain.ClientRepository; + +public interface JPAClientRepository extends ClientRepository, JpaRepository { + +} From 928534b2f72c261f6f9ee529f8ebc3d238dd45c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Wed, 17 Jul 2024 17:39:06 +0900 Subject: [PATCH 06/56] =?UTF-8?q?feat(Client):=20=EC=82=AC=EC=9E=A5?= =?UTF-8?q?=EB=8B=98=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/member/owner/domain/Owner.java | 18 ++++++++++++++++++ .../member/owner/domain/OwnerRepository.java | 5 +++++ .../member/owner/infra/JPAOwnerRepository.java | 9 +++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/main/java/shopping/member/owner/domain/Owner.java create mode 100644 src/main/java/shopping/member/owner/domain/OwnerRepository.java create mode 100644 src/main/java/shopping/member/owner/infra/JPAOwnerRepository.java diff --git a/src/main/java/shopping/member/owner/domain/Owner.java b/src/main/java/shopping/member/owner/domain/Owner.java new file mode 100644 index 0000000..f33533e --- /dev/null +++ b/src/main/java/shopping/member/owner/domain/Owner.java @@ -0,0 +1,18 @@ +package shopping.member.owner.domain; + +import jakarta.persistence.Entity; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberPriority; +import shopping.member.common.domain.Password; +import shopping.member.common.domain.PasswordEncoder; + +@Entity +public class Owner extends Member { + + protected Owner() { + } + + public Owner(String email, String rawPassword, PasswordEncoder encoder, String memberName){ + super(email, new Password(rawPassword, encoder), memberName, MemberPriority.OWNER); + } +} diff --git a/src/main/java/shopping/member/owner/domain/OwnerRepository.java b/src/main/java/shopping/member/owner/domain/OwnerRepository.java new file mode 100644 index 0000000..63c5a39 --- /dev/null +++ b/src/main/java/shopping/member/owner/domain/OwnerRepository.java @@ -0,0 +1,5 @@ +package shopping.member.owner.domain; + +public interface OwnerRepository { + +} diff --git a/src/main/java/shopping/member/owner/infra/JPAOwnerRepository.java b/src/main/java/shopping/member/owner/infra/JPAOwnerRepository.java new file mode 100644 index 0000000..8b6046d --- /dev/null +++ b/src/main/java/shopping/member/owner/infra/JPAOwnerRepository.java @@ -0,0 +1,9 @@ +package shopping.member.owner.infra; + +import org.springframework.data.jpa.repository.JpaRepository; +import shopping.member.owner.domain.Owner; +import shopping.member.owner.domain.OwnerRepository; + +public interface JPAOwnerRepository extends OwnerRepository, JpaRepository { + +} From d2e2c857d73b4556a6641dcbd82346d0d30a97b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Wed, 17 Jul 2024 17:40:05 +0900 Subject: [PATCH 07/56] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=ED=8C=8C=EC=9D=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - db, logging 설정 --- src/main/resources/application.properties | 1 - src/main/resources/application.yml | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index c33078c..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=spring-shopping diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..5d52f56 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + h2: + console: + enabled: true + path: /h2-console + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + database-platform: org.hibernate.dialect.H2Dialect + properties: + hibernate: + format_sql: true + show_sql: true +logging: + level: + org.hibernate.SQL: debug From 1835c1b77f563968545243b83046adf88ff5852b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Thu, 18 Jul 2024 13:25:23 +0900 Subject: [PATCH 08/56] =?UTF-8?q?feat(ProfanityChecker):=20=EB=B9=84?= =?UTF-8?q?=EC=86=8D=EC=96=B4=20=ED=8F=AC=ED=95=A8=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EC=B2=B4=EC=BB=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/shopping/product/domain/ProfanityChecker.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/shopping/product/domain/ProfanityChecker.java diff --git a/src/main/java/shopping/product/domain/ProfanityChecker.java b/src/main/java/shopping/product/domain/ProfanityChecker.java new file mode 100644 index 0000000..a7c7820 --- /dev/null +++ b/src/main/java/shopping/product/domain/ProfanityChecker.java @@ -0,0 +1,6 @@ +package shopping.product.domain; + +public interface ProfanityChecker { + + boolean isProfanity(String word); +} From bddba6eef6fcd3c1c63e9cb8f6ff84106c2e16fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Thu, 18 Jul 2024 13:26:22 +0900 Subject: [PATCH 09/56] =?UTF-8?q?feat(Product):=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/InvalidProductNameException.java | 8 ++++++ .../java/shopping/product/domain/Product.java | 26 +++++++++++++++++++ .../shopping/product/domain/ProductName.java | 22 ++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/main/java/shopping/product/domain/InvalidProductNameException.java create mode 100644 src/main/java/shopping/product/domain/Product.java create mode 100644 src/main/java/shopping/product/domain/ProductName.java diff --git a/src/main/java/shopping/product/domain/InvalidProductNameException.java b/src/main/java/shopping/product/domain/InvalidProductNameException.java new file mode 100644 index 0000000..f1d7c94 --- /dev/null +++ b/src/main/java/shopping/product/domain/InvalidProductNameException.java @@ -0,0 +1,8 @@ +package shopping.product.domain; + +public class InvalidProductNameException extends IllegalStateException { + + public InvalidProductNameException(String s) { + super(s); + } +} diff --git a/src/main/java/shopping/product/domain/Product.java b/src/main/java/shopping/product/domain/Product.java new file mode 100644 index 0000000..cf115d8 --- /dev/null +++ b/src/main/java/shopping/product/domain/Product.java @@ -0,0 +1,26 @@ +package shopping.product.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import java.math.BigDecimal; +import shopping.common.domain.BaseEntity; + +@Entity +public class Product extends BaseEntity { + + @Id + @GeneratedValue + private Long productId; + + @Column(name = "product_name", nullable = false) + private String name; + + @Column(name = "product_price", nullable = false) + private BigDecimal price; + + @Column(name = "product_image", nullable = false) + private String image; + +} diff --git a/src/main/java/shopping/product/domain/ProductName.java b/src/main/java/shopping/product/domain/ProductName.java new file mode 100644 index 0000000..b8730fb --- /dev/null +++ b/src/main/java/shopping/product/domain/ProductName.java @@ -0,0 +1,22 @@ +package shopping.product.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public class ProductName { + + @Column(name = "product_name", nullable = false) + private String name; + + protected ProductName(){ + } + + public ProductName(String name, ProfanityChecker profanityChecker) { + if(profanityChecker.isProfanity(name)){ + throw new InvalidProductNameException("상품 이름에 비속어를 사용할 수 없습니다. " + name); + } + + this.name = name; + } +} From a769498e1f1017448dc305a138ae391b2a176273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=88=EC=A0=95=ED=99=94?= Date: Thu, 18 Jul 2024 13:26:22 +0900 Subject: [PATCH 10/56] =?UTF-8?q?feat(Product):=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../domain/InvalidProductNameException.java | 8 +++ .../domain/InvalidProductPriceException.java | 8 +++ .../java/shopping/product/domain/Product.java | 37 ++++++++++++++ .../shopping/product/domain/ProductName.java | 51 +++++++++++++++++++ .../shopping/product/domain/ProductPrice.java | 23 +++++++++ 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/main/java/shopping/product/domain/InvalidProductNameException.java create mode 100644 src/main/java/shopping/product/domain/InvalidProductPriceException.java create mode 100644 src/main/java/shopping/product/domain/Product.java create mode 100644 src/main/java/shopping/product/domain/ProductName.java create mode 100644 src/main/java/shopping/product/domain/ProductPrice.java diff --git a/README.md b/README.md index 4adde9b..0b1b533 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ - `상품 이름(Product Name)`은 공백 포함 최대 15자까지 갖는다. - `상품 이름(Product Name)`에는 `()`,`[]`,`+`,`-`,`&`,`/`,`_`을 제외한 특수문자를 사용할 수 없다. - `상품 이름(Product Name)`에는 비속어를 사용할 수 없다. - +- `상품 가격(Product Price)`은 0원 이상이다. #### 행위 - `사장님(Owner)`은 `상품(Product)`을 추가할 수 있다. diff --git a/src/main/java/shopping/product/domain/InvalidProductNameException.java b/src/main/java/shopping/product/domain/InvalidProductNameException.java new file mode 100644 index 0000000..f1d7c94 --- /dev/null +++ b/src/main/java/shopping/product/domain/InvalidProductNameException.java @@ -0,0 +1,8 @@ +package shopping.product.domain; + +public class InvalidProductNameException extends IllegalStateException { + + public InvalidProductNameException(String s) { + super(s); + } +} diff --git a/src/main/java/shopping/product/domain/InvalidProductPriceException.java b/src/main/java/shopping/product/domain/InvalidProductPriceException.java new file mode 100644 index 0000000..0e795f8 --- /dev/null +++ b/src/main/java/shopping/product/domain/InvalidProductPriceException.java @@ -0,0 +1,8 @@ +package shopping.product.domain; + +public class InvalidProductPriceException extends IllegalArgumentException { + + public InvalidProductPriceException(final String s) { + super(s); + } +} diff --git a/src/main/java/shopping/product/domain/Product.java b/src/main/java/shopping/product/domain/Product.java new file mode 100644 index 0000000..6e50c79 --- /dev/null +++ b/src/main/java/shopping/product/domain/Product.java @@ -0,0 +1,37 @@ +package shopping.product.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import shopping.common.domain.AuditableBaseEntity; +import shopping.common.domain.BaseEntity; + +@Entity +public class Product extends AuditableBaseEntity { + + @Id + @GeneratedValue + @Column(name = "product_id", nullable = false) + private Long id; + + @Embedded + private ProductName name; + + @Embedded + private ProductPrice price; + + @Column(name = "product_image") + private String image; + + protected Product() { + } + + public Product(final String name, final ProfanityChecker profanityChecker, final Long price, + final String image) { + this.name = new ProductName(name, profanityChecker); + this.price = new ProductPrice(price); + this.image = image; + } +} diff --git a/src/main/java/shopping/product/domain/ProductName.java b/src/main/java/shopping/product/domain/ProductName.java new file mode 100644 index 0000000..ce9e695 --- /dev/null +++ b/src/main/java/shopping/product/domain/ProductName.java @@ -0,0 +1,51 @@ +package shopping.product.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Embeddable +public class ProductName { + + private static final Pattern PRODUCT_NAME_PATTERN = Pattern.compile( + "^[a-zA-Z0-9()\\[\\]+\\-&/_]*$\n"); + + @Column(name = "product_name", nullable = false) + private String name; + + protected ProductName() { + } + + public ProductName(final String name, final ProfanityChecker profanityChecker) { + validateName(name, profanityChecker); + this.name = name; + } + + private static void validateName(final String name, final ProfanityChecker profanityChecker) { + validateNameLength(name); + validateNamePattern(name); + validateProfanity(name, profanityChecker); + } + + private static void validateProfanity(final String name, + final ProfanityChecker profanityChecker) { + if (profanityChecker.isProfanity(name)) { + throw new InvalidProductNameException("상품 이름에 비속어를 사용할 수 없습니다. " + name); + } + } + + private static void validateNamePattern(final String name) { + final Matcher nameMatcher = PRODUCT_NAME_PATTERN.matcher(name); + if (!nameMatcher.matches()) { + throw new InvalidProductNameException( + "상품 이름에 (), [], +, -, &, /, _ 을 제외한 특수문자는 들어갈 수 없습니다. " + name); + } + } + + private static void validateNameLength(final String name) { + if (name.length() > 15) { + throw new InvalidProductNameException("상품 이름은 최대 15글자까지 입력가능합니다. " + name); + } + } +} diff --git a/src/main/java/shopping/product/domain/ProductPrice.java b/src/main/java/shopping/product/domain/ProductPrice.java new file mode 100644 index 0000000..8fcba0c --- /dev/null +++ b/src/main/java/shopping/product/domain/ProductPrice.java @@ -0,0 +1,23 @@ +package shopping.product.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.math.BigDecimal; + +@Embeddable +public class ProductPrice { + + @Column(name = "product_price", nullable = false) + private BigDecimal price; + + protected ProductPrice() { + } + + public ProductPrice(final Long price) { + if (price < 0) { + throw new InvalidProductPriceException("상품 금액은 0원 이어야 한다."); + } + + this.price = BigDecimal.valueOf(price); + } +} From b7f4d48a807d4b37d1f5a1b4e903e47fbd308640 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Thu, 18 Jul 2024 15:15:25 +0900 Subject: [PATCH 11/56] =?UTF-8?q?refactor(BaseEntity):=20BaseEntity,=20Aud?= =?UTF-8?q?itableBaseEntity=20=EB=B6=84=EB=A6=AC=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BaseEntity: 생성시간, 수정시간 - AuditableBaseEntity: 생성시간, 생성자, 수정시간, 수정자 --- .../common/domain/AuditableBaseEntity.java | 20 +++++++++++++++++++ .../shopping/common/domain/BaseEntity.java | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/main/java/shopping/common/domain/AuditableBaseEntity.java diff --git a/src/main/java/shopping/common/domain/AuditableBaseEntity.java b/src/main/java/shopping/common/domain/AuditableBaseEntity.java new file mode 100644 index 0000000..41cb4b7 --- /dev/null +++ b/src/main/java/shopping/common/domain/AuditableBaseEntity.java @@ -0,0 +1,20 @@ +package shopping.common.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class AuditableBaseEntity extends BaseEntity { + + @CreatedBy + @Column(updatable = false) + private String createdBy; + + @LastModifiedBy + private String updatedBy; +} diff --git a/src/main/java/shopping/common/domain/BaseEntity.java b/src/main/java/shopping/common/domain/BaseEntity.java index 873b192..02ba63e 100644 --- a/src/main/java/shopping/common/domain/BaseEntity.java +++ b/src/main/java/shopping/common/domain/BaseEntity.java @@ -4,12 +4,13 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; +import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @MappedSuperclass -@EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { @CreatedDate From 2c028978d182a1b9d7121452da651a873019d263 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Thu, 18 Jul 2024 15:16:01 +0900 Subject: [PATCH 12/56] =?UTF-8?q?docs:=20=ED=9A=8C=EC=9B=90=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=96=89=EC=9C=84=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0b1b533..6e32631 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,16 @@ - `이메일(Email)`은 다른 회원과 중복될 수 없다. - `비밀번호(Password)`는 암호화되어야한다. - `회원 권한(Member Priority)`은 `사장님(Owner)`, `손님(Client)`이 있다. +> `회원 권한(Member Priority)`에 따라 행위가 다르며 이는 아래에서 설명한다. #### 행위 -### 사장님(Owner) +#### 사장님(Owner) +- `사장님(Owner)`는 `이메일(Email)`, `비밀번호(Password)`, `회원 이름(Member Name)`을 입력하여 가입한다. + - 기본적으로 갖는 권한은 `사장님(Owner)`이다. - `사장님(Owner)`는 `이메일(Email)`과 `비밀번호(Password)`를 입력하여 로그인한다. -### 손님(Client) +#### 손님(Client) - `손님(Client)`는 `이메일(Email)`, `비밀번호(Password)`, `회원 이름(Member Name)`을 입력하여 가입한다. - 기본적으로 갖는 권한은 `손님(Client)`이다. - `손님(Client)`는 `이메일(Email)`과 `비밀번호(Password)`를 입력하여 로그인한다. From 023d495143fea2d72bd7aebbef1f445bfe5f0ab0 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Thu, 18 Jul 2024 15:43:28 +0900 Subject: [PATCH 13/56] =?UTF-8?q?refactor(Product):=20exception=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/shopping/product/domain/ProductName.java | 1 + src/main/java/shopping/product/domain/ProductPrice.java | 1 + .../{domain => exception}/InvalidProductNameException.java | 2 +- .../{domain => exception}/InvalidProductPriceException.java | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) rename src/main/java/shopping/product/{domain => exception}/InvalidProductNameException.java (80%) rename src/main/java/shopping/product/{domain => exception}/InvalidProductPriceException.java (81%) diff --git a/src/main/java/shopping/product/domain/ProductName.java b/src/main/java/shopping/product/domain/ProductName.java index ce9e695..2170e28 100644 --- a/src/main/java/shopping/product/domain/ProductName.java +++ b/src/main/java/shopping/product/domain/ProductName.java @@ -4,6 +4,7 @@ import jakarta.persistence.Embeddable; import java.util.regex.Matcher; import java.util.regex.Pattern; +import shopping.product.exception.InvalidProductNameException; @Embeddable public class ProductName { diff --git a/src/main/java/shopping/product/domain/ProductPrice.java b/src/main/java/shopping/product/domain/ProductPrice.java index 8fcba0c..a1c9218 100644 --- a/src/main/java/shopping/product/domain/ProductPrice.java +++ b/src/main/java/shopping/product/domain/ProductPrice.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.math.BigDecimal; +import shopping.product.exception.InvalidProductPriceException; @Embeddable public class ProductPrice { diff --git a/src/main/java/shopping/product/domain/InvalidProductNameException.java b/src/main/java/shopping/product/exception/InvalidProductNameException.java similarity index 80% rename from src/main/java/shopping/product/domain/InvalidProductNameException.java rename to src/main/java/shopping/product/exception/InvalidProductNameException.java index f1d7c94..74d8930 100644 --- a/src/main/java/shopping/product/domain/InvalidProductNameException.java +++ b/src/main/java/shopping/product/exception/InvalidProductNameException.java @@ -1,4 +1,4 @@ -package shopping.product.domain; +package shopping.product.exception; public class InvalidProductNameException extends IllegalStateException { diff --git a/src/main/java/shopping/product/domain/InvalidProductPriceException.java b/src/main/java/shopping/product/exception/InvalidProductPriceException.java similarity index 81% rename from src/main/java/shopping/product/domain/InvalidProductPriceException.java rename to src/main/java/shopping/product/exception/InvalidProductPriceException.java index 0e795f8..15ed714 100644 --- a/src/main/java/shopping/product/domain/InvalidProductPriceException.java +++ b/src/main/java/shopping/product/exception/InvalidProductPriceException.java @@ -1,4 +1,4 @@ -package shopping.product.domain; +package shopping.product.exception; public class InvalidProductPriceException extends IllegalArgumentException { From 9f0235fabddb2f1e40b1825f64d918b1741d4cdc Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Thu, 18 Jul 2024 15:51:49 +0900 Subject: [PATCH 14/56] =?UTF-8?q?refactor(WishProduct):=20=EC=9C=84?= =?UTF-8?q?=EC=8B=9C=20=EC=83=81=ED=92=88=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/client/domain/WishProduct.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/shopping/member/client/domain/WishProduct.java diff --git a/src/main/java/shopping/member/client/domain/WishProduct.java b/src/main/java/shopping/member/client/domain/WishProduct.java new file mode 100644 index 0000000..35cf322 --- /dev/null +++ b/src/main/java/shopping/member/client/domain/WishProduct.java @@ -0,0 +1,31 @@ +package shopping.member.client.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import shopping.common.domain.BaseEntity; + +@Entity +@Table(name = "wish_products") +public class WishProduct extends BaseEntity { + + @Id + @GeneratedValue + @Column(name = "wish_product_id", nullable = false) + private Long id; + + private Long productId; + + protected WishProduct() { + } + + public WishProduct(final Long productId) { + this.productId = productId; + } + + public boolean isSameProduct(WishProduct wishProduct){ + return this.productId.equals(wishProduct.productId); + } +} From 1bc4f89a67dc0fc7a2978358f33b44c37eab6105 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Thu, 18 Jul 2024 16:01:05 +0900 Subject: [PATCH 15/56] =?UTF-8?q?refactor(Client):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=8A=94=20=EC=9C=84=EC=8B=9C=20=EC=83=81=ED=92=88?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/member/client/domain/Client.java | 16 +++++++++-- .../member/client/domain/WishProducts.java | 27 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/main/java/shopping/member/client/domain/WishProducts.java diff --git a/src/main/java/shopping/member/client/domain/Client.java b/src/main/java/shopping/member/client/domain/Client.java index 16de4ad..74e3a7b 100644 --- a/src/main/java/shopping/member/client/domain/Client.java +++ b/src/main/java/shopping/member/client/domain/Client.java @@ -1,5 +1,6 @@ package shopping.member.client.domain; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import shopping.member.common.domain.Member; import shopping.member.common.domain.MemberPriority; @@ -8,11 +9,22 @@ @Entity public class Client extends Member { - + + @Embedded + private WishProducts wishProducts = new WishProducts(); + protected Client() { } - public Client(String email, String rawPassword, PasswordEncoder encoder, String memberName){ + public Client(String email, String rawPassword, PasswordEncoder encoder, String memberName) { super(email, new Password(rawPassword, encoder), memberName, MemberPriority.CLIENT); } + + public void wish(WishProduct wishProduct) { + wishProducts.wish(wishProduct); + } + + public void unWish(WishProduct wishProduct) { + wishProducts.unWish(wishProduct); + } } diff --git a/src/main/java/shopping/member/client/domain/WishProducts.java b/src/main/java/shopping/member/client/domain/WishProducts.java new file mode 100644 index 0000000..071c6da --- /dev/null +++ b/src/main/java/shopping/member/client/domain/WishProducts.java @@ -0,0 +1,27 @@ +package shopping.member.client.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Embeddable; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + +@Embeddable +public class WishProducts { + + @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinColumn(name = "email", nullable = false) + private List wishProducts = new ArrayList<>(); + + protected WishProducts(){ + } + + public void wish(WishProduct wishProduct) { + this.wishProducts.add(wishProduct); + } + + public void unWish(WishProduct wishProduct) { + this.wishProducts.removeIf(delWishProduct -> delWishProduct.isSameProduct(wishProduct)); + } +} From b81c74ddd7af5312e50b3c51aea3dc2c59ed2cd5 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 19 Jul 2024 15:36:44 +0900 Subject: [PATCH 16/56] =?UTF-8?q?test(ProfanityChecker):=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=ED=8E=98=EC=9D=B4=ED=81=AC=20Pro?= =?UTF-8?q?fanityChecker=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shopping/fake/FakeProfanityChecker.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/test/java/shopping/fake/FakeProfanityChecker.java diff --git a/src/test/java/shopping/fake/FakeProfanityChecker.java b/src/test/java/shopping/fake/FakeProfanityChecker.java new file mode 100644 index 0000000..7af3773 --- /dev/null +++ b/src/test/java/shopping/fake/FakeProfanityChecker.java @@ -0,0 +1,15 @@ +package shopping.fake; + +import java.util.List; +import shopping.product.domain.ProfanityChecker; + +public class FakeProfanityChecker implements ProfanityChecker { + + private static final List profanities = List.of("비속어", "욕", "메롱"); + + @Override + public boolean isProfanity(final String word) { + return profanities.stream() + .anyMatch(word::contains); + } +} From e2591dcd56e2d846717c3ad9ad8c34a1f7a089c8 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 19 Jul 2024 15:37:59 +0900 Subject: [PATCH 17/56] =?UTF-8?q?refactor(ProductName):=20ProductName=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95(?= =?UTF-8?q?=ED=95=9C=EA=B5=AD=EC=96=B4=20=EC=B6=94=EA=B0=80)=20=EB=B0=8F?= =?UTF-8?q?=20null=20=EC=B2=B4=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/product/domain/ProductName.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/shopping/product/domain/ProductName.java b/src/main/java/shopping/product/domain/ProductName.java index 2170e28..f9ff792 100644 --- a/src/main/java/shopping/product/domain/ProductName.java +++ b/src/main/java/shopping/product/domain/ProductName.java @@ -10,7 +10,8 @@ public class ProductName { private static final Pattern PRODUCT_NAME_PATTERN = Pattern.compile( - "^[a-zA-Z0-9()\\[\\]+\\-&/_]*$\n"); + "^[a-zA-Z0-9()\\[\\]+\\-&/_가-힣]*$"); + @Column(name = "product_name", nullable = false) private String name; @@ -19,6 +20,10 @@ protected ProductName() { } public ProductName(final String name, final ProfanityChecker profanityChecker) { + if (name == null || profanityChecker == null) { + throw new InvalidProductNameException("상품 이름은 필수값 입니다"); + } + validateName(name, profanityChecker); this.name = name; } @@ -29,10 +34,9 @@ private static void validateName(final String name, final ProfanityChecker profa validateProfanity(name, profanityChecker); } - private static void validateProfanity(final String name, - final ProfanityChecker profanityChecker) { - if (profanityChecker.isProfanity(name)) { - throw new InvalidProductNameException("상품 이름에 비속어를 사용할 수 없습니다. " + name); + private static void validateNameLength(final String name) { + if (name.length() > 15) { + throw new InvalidProductNameException("상품 이름은 최대 15글자까지 입력가능합니다. " + name); } } @@ -44,9 +48,10 @@ private static void validateNamePattern(final String name) { } } - private static void validateNameLength(final String name) { - if (name.length() > 15) { - throw new InvalidProductNameException("상품 이름은 최대 15글자까지 입력가능합니다. " + name); + private static void validateProfanity(final String name, + final ProfanityChecker profanityChecker) { + if (profanityChecker.isProfanity(name)) { + throw new InvalidProductNameException("상품 이름에 비속어를 사용할 수 없습니다. " + name); } } } From 38bf9cdd8c19e89794e5fe2b66e35e10282d3d52 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 19 Jul 2024 15:38:09 +0900 Subject: [PATCH 18/56] =?UTF-8?q?test(ProductName):=20ProductName=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/domain/ProductNameTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/test/java/shopping/product/domain/ProductNameTest.java diff --git a/src/test/java/shopping/product/domain/ProductNameTest.java b/src/test/java/shopping/product/domain/ProductNameTest.java new file mode 100644 index 0000000..1136af2 --- /dev/null +++ b/src/test/java/shopping/product/domain/ProductNameTest.java @@ -0,0 +1,66 @@ +package shopping.product.domain; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import shopping.fake.FakeProfanityChecker; +import shopping.product.exception.InvalidProductNameException; + +@DisplayName("ProductName") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ProductNameTest { + + private ProfanityChecker profanityChecker = new FakeProfanityChecker(); + + @Test + void 이름이_null인_ProductName을_생성하면_예외를_던진다() { + assertThatThrownBy( + () -> new ProductName(null, profanityChecker)) + .isInstanceOf(InvalidProductNameException.class); + } + + @Test + void 비속어체커가_null인_ProductName을_생성하면_예외를_던진다() { + assertThatThrownBy( + () -> new ProductName("맥북", null)) + .isInstanceOf(InvalidProductNameException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"가나다라마바사아자차카파타하하하", "1234567890123456", " "}) + void 이름의_길이가_15자를_넘어가는_ProductNmae을_생성하면_예외를_던진다(String name) { + assertThatThrownBy( + () -> new ProductName(name, profanityChecker)) + .isInstanceOf(InvalidProductNameException.class); + } + + @ParameterizedTest + @DisplayName("이름에 (), [], +, -, &, /, _ 을 제외한 특수문자가 들어가는 ProductName을 생성하면 예외를 던진다.") + @ValueSource(strings = {"*맥북*", "{맥북}", "^맥북^"}) + void validatePattern(String name) { + assertThatThrownBy( + () -> new ProductName(name, profanityChecker)) + .isInstanceOf(InvalidProductNameException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"비속어맥북", "욕욕욕", "메롱롱롱"}) + void 이름에_비속어가_들어가는_ProductName을_생성하면_예외를_던진다(String name) { + assertThatThrownBy( + () -> new ProductName(name, profanityChecker)) + .isInstanceOf(InvalidProductNameException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"맥북", "아이패드", "아이폰"}) + void ProductName을_생성할_수_있다(String name) { + assertThatNoException() + .isThrownBy(() -> new ProductName(name, profanityChecker)); + } +} \ No newline at end of file From 02f9f2d2e7262943ce3fa0a448057c850078a0aa Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 19 Jul 2024 15:42:47 +0900 Subject: [PATCH 19/56] =?UTF-8?q?refactor(ProductPrice):=20ProductPrice=20?= =?UTF-8?q?null=20=EC=B2=B4=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/shopping/product/domain/ProductPrice.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/shopping/product/domain/ProductPrice.java b/src/main/java/shopping/product/domain/ProductPrice.java index a1c9218..8b0dded 100644 --- a/src/main/java/shopping/product/domain/ProductPrice.java +++ b/src/main/java/shopping/product/domain/ProductPrice.java @@ -15,8 +15,12 @@ protected ProductPrice() { } public ProductPrice(final Long price) { + if (price == null) { + throw new InvalidProductPriceException("상품 금액은 필수값 입니다."); + } + if (price < 0) { - throw new InvalidProductPriceException("상품 금액은 0원 이어야 한다."); + throw new InvalidProductPriceException("상품 금액은 0원 이상이어야 합니다. " + price); } this.price = BigDecimal.valueOf(price); From e9fcbe7cb476ba7fc069700e82b472b2814bcc6a Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 19 Jul 2024 15:43:03 +0900 Subject: [PATCH 20/56] =?UTF-8?q?test(ProductPrice):=20ProductPrice=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/domain/ProductPriceTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/test/java/shopping/product/domain/ProductPriceTest.java diff --git a/src/test/java/shopping/product/domain/ProductPriceTest.java b/src/test/java/shopping/product/domain/ProductPriceTest.java new file mode 100644 index 0000000..cdd4a0f --- /dev/null +++ b/src/test/java/shopping/product/domain/ProductPriceTest.java @@ -0,0 +1,37 @@ +package shopping.product.domain; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import shopping.product.exception.InvalidProductPriceException; + +@DisplayName("ProductName") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ProductPriceTest { + + @Test + void 가격이_null인_ProductPrice를_생성하면_예외를_던진다() { + assertThatThrownBy(() -> new ProductPrice(null)) + .isInstanceOf(InvalidProductPriceException.class); + } + + @ParameterizedTest + @ValueSource(longs = {-1L, -100L, -100_000L}) + void 가격이_0미만인_ProductPrice를_생성하면_예외를_던진다(long price) { + assertThatThrownBy(() -> new ProductPrice(price)) + .isInstanceOf(InvalidProductPriceException.class); + } + + @ParameterizedTest + @ValueSource(longs = {1L, 100L, 100_000L}) + void ProductPrice를_생성할_수_있다(long price) { + assertThatNoException() + .isThrownBy(() -> new ProductPrice(price)); + } +} From 95744679a680b3170b81033b9dc586a836c9d16f Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 19 Jul 2024 15:49:58 +0900 Subject: [PATCH 21/56] =?UTF-8?q?test(PasswordEncoder):=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=ED=8E=98=EC=9D=B4=ED=81=AC=20Pas?= =?UTF-8?q?swordEncoder=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/fake/FakePasswordEncoder.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/test/java/shopping/fake/FakePasswordEncoder.java diff --git a/src/test/java/shopping/fake/FakePasswordEncoder.java b/src/test/java/shopping/fake/FakePasswordEncoder.java new file mode 100644 index 0000000..699d3eb --- /dev/null +++ b/src/test/java/shopping/fake/FakePasswordEncoder.java @@ -0,0 +1,21 @@ +package shopping.fake; + +import shopping.member.common.domain.PasswordEncoder; + +public class FakePasswordEncoder implements PasswordEncoder { + + @Override + public String encode(final String rawPassword) { + return reversePassword(rawPassword); + } + + @Override + public boolean matches(final String rawPassword, final String password) { + return reversePassword(rawPassword).equals(password); + } + + private String reversePassword(final String rawPassword) { + final StringBuilder stringBuilder = new StringBuilder(rawPassword); + return stringBuilder.reverse().toString(); + } +} From c4fb1600d5bfd061f8b594c88a132ac6685dbc30 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 20 Jul 2024 11:42:20 +0900 Subject: [PATCH 22/56] =?UTF-8?q?test(Password):=20Password=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/common/domain/PasswordTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/test/java/shopping/member/common/domain/PasswordTest.java diff --git a/src/test/java/shopping/member/common/domain/PasswordTest.java b/src/test/java/shopping/member/common/domain/PasswordTest.java new file mode 100644 index 0000000..84b902e --- /dev/null +++ b/src/test/java/shopping/member/common/domain/PasswordTest.java @@ -0,0 +1,33 @@ +package shopping.member.common.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import shopping.fake.FakePasswordEncoder; + +@DisplayName("Password") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PasswordTest { + + private PasswordEncoder passwordEncoder = new FakePasswordEncoder(); + + @Test + void Password를_생성할_수_있다() { + assertThatNoException() + .isThrownBy(() -> new Password("1234", passwordEncoder)); + } + + @ParameterizedTest + @ValueSource(strings = {"1234", "qwer", "password"}) + void Password가_맞는지_확인할_수_있다(String rawPassword) { + final Password password = new Password(rawPassword, passwordEncoder); + + assertThat(password.isMatch(rawPassword, passwordEncoder)).isTrue(); + } +} \ No newline at end of file From f4b44f820049aa0ee35f0e0370ef2aa1276ba9eb Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 20 Jul 2024 11:42:34 +0900 Subject: [PATCH 23/56] =?UTF-8?q?test(Product):=20Product=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/product/domain/ProductTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/test/java/shopping/product/domain/ProductTest.java diff --git a/src/test/java/shopping/product/domain/ProductTest.java b/src/test/java/shopping/product/domain/ProductTest.java new file mode 100644 index 0000000..7c80448 --- /dev/null +++ b/src/test/java/shopping/product/domain/ProductTest.java @@ -0,0 +1,22 @@ +package shopping.product.domain; + +import static org.assertj.core.api.Assertions.assertThatNoException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import shopping.fake.FakeProfanityChecker; + +@DisplayName("Product") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ProductTest { + + private ProfanityChecker profanityChecker = new FakeProfanityChecker(); + + @Test + void Product를_생성할_수_있다() { + assertThatNoException() + .isThrownBy(() -> new Product("맥북", profanityChecker, 1_000L, "image.jpg")); + } +} \ No newline at end of file From e926d3aa02ddea23ccde277ea3b73efbe592ddee Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 20 Jul 2024 14:31:50 +0900 Subject: [PATCH 24/56] =?UTF-8?q?refactor(WishProducts):=20=EC=9C=84?= =?UTF-8?q?=EC=8B=9C=EC=83=81=ED=92=88=EC=9D=80=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EC=B6=94=EA=B0=80=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=97=86=EC=9C=BC=EB=A9=B0,=20WishProducts=EC=97=90=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EC=83=81=ED=92=88=EC=9D=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=A0=20=EC=88=98=20=EC=97=86=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/DuplicateWishProductException.java | 8 ++++ .../member/client/domain/WishProduct.java | 4 ++ .../member/client/domain/WishProducts.java | 26 ++++++++--- .../NotFoundWishProductException.java | 8 ++++ .../client/domain/WishProductsTest.java | 45 +++++++++++++++++++ 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 src/main/java/shopping/member/client/domain/DuplicateWishProductException.java create mode 100644 src/main/java/shopping/member/client/exception/NotFoundWishProductException.java create mode 100644 src/test/java/shopping/member/client/domain/WishProductsTest.java diff --git a/src/main/java/shopping/member/client/domain/DuplicateWishProductException.java b/src/main/java/shopping/member/client/domain/DuplicateWishProductException.java new file mode 100644 index 0000000..a0c1eaf --- /dev/null +++ b/src/main/java/shopping/member/client/domain/DuplicateWishProductException.java @@ -0,0 +1,8 @@ +package shopping.member.client.domain; + +public class DuplicateWishProductException extends RuntimeException { + + public DuplicateWishProductException(final String message) { + super(message); + } +} diff --git a/src/main/java/shopping/member/client/domain/WishProduct.java b/src/main/java/shopping/member/client/domain/WishProduct.java index 35cf322..ba7b0d7 100644 --- a/src/main/java/shopping/member/client/domain/WishProduct.java +++ b/src/main/java/shopping/member/client/domain/WishProduct.java @@ -28,4 +28,8 @@ public WishProduct(final Long productId) { public boolean isSameProduct(WishProduct wishProduct){ return this.productId.equals(wishProduct.productId); } + + public Long getProductId() { + return productId; + } } diff --git a/src/main/java/shopping/member/client/domain/WishProducts.java b/src/main/java/shopping/member/client/domain/WishProducts.java index 071c6da..7def18a 100644 --- a/src/main/java/shopping/member/client/domain/WishProducts.java +++ b/src/main/java/shopping/member/client/domain/WishProducts.java @@ -6,6 +6,7 @@ import jakarta.persistence.OneToMany; import java.util.ArrayList; import java.util.List; +import shopping.member.client.exception.NotFoundWishProductException; @Embeddable public class WishProducts { @@ -14,14 +15,29 @@ public class WishProducts { @JoinColumn(name = "email", nullable = false) private List wishProducts = new ArrayList<>(); - protected WishProducts(){ + protected WishProducts() { } - public void wish(WishProduct wishProduct) { - this.wishProducts.add(wishProduct); + public void wish(WishProduct addWishProduct) { + validateAddWishItem(addWishProduct); + this.wishProducts.add(addWishProduct); } - public void unWish(WishProduct wishProduct) { - this.wishProducts.removeIf(delWishProduct -> delWishProduct.isSameProduct(wishProduct)); + private void validateAddWishItem(final WishProduct addWishProduct) { + final boolean match = this.wishProducts.stream() + .anyMatch(wishProduct -> wishProduct.isSameProduct(addWishProduct)); + if (match) { + throw new DuplicateWishProductException( + "중복되는 상품에 하트를 누를 수 없습니다. " + addWishProduct.getProductId()); + } + } + + public void unWish(WishProduct delWishProduct) { + final boolean removed = this.wishProducts.removeIf( + wishProduct -> wishProduct.isSameProduct(delWishProduct)); + if (!removed) { + throw new NotFoundWishProductException( + "해당 위시상품을 찾을 수 없습니다. " + delWishProduct.getProductId()); + } } } diff --git a/src/main/java/shopping/member/client/exception/NotFoundWishProductException.java b/src/main/java/shopping/member/client/exception/NotFoundWishProductException.java new file mode 100644 index 0000000..cccb755 --- /dev/null +++ b/src/main/java/shopping/member/client/exception/NotFoundWishProductException.java @@ -0,0 +1,8 @@ +package shopping.member.client.exception; + +public class NotFoundWishProductException extends RuntimeException{ + + public NotFoundWishProductException(final String message) { + super(message); + } +} diff --git a/src/test/java/shopping/member/client/domain/WishProductsTest.java b/src/test/java/shopping/member/client/domain/WishProductsTest.java new file mode 100644 index 0000000..9deba7f --- /dev/null +++ b/src/test/java/shopping/member/client/domain/WishProductsTest.java @@ -0,0 +1,45 @@ +package shopping.member.client.domain; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import shopping.member.client.exception.NotFoundWishProductException; + +@DisplayName("WishProducts") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class WishProductsTest { + + @Test + void WishProducts에_위시상품을_추가했다가_삭제할_수_있다() { + final WishProducts wishProducts = new WishProducts(); + final WishProduct wishProduct = new WishProduct(1L); + + assertThatNoException().isThrownBy(() -> { + wishProducts.wish(wishProduct); + wishProducts.unWish(wishProduct); + }); + } + + @Test + void WishProducts에_위시상품을_중복해서_추가할_수_없다() { + final WishProducts wishProducts = new WishProducts(); + final WishProduct wishProduct = new WishProduct(1L); + wishProducts.wish(wishProduct); + + assertThatThrownBy(() -> wishProducts.wish(wishProduct)) + .isInstanceOf(DuplicateWishProductException.class); + } + + @Test + void WishProducts에_없는_위시상품을_삭제할_수_없다() { + final WishProducts wishProducts = new WishProducts(); + final WishProduct wishProduct = new WishProduct(1L); + + assertThatThrownBy(() -> wishProducts.unWish(wishProduct)) + .isInstanceOf(NotFoundWishProductException.class); + } +} \ No newline at end of file From b68078f9b2ceb007ad69b90835d47e0c8ac0dca3 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 20 Jul 2024 14:32:11 +0900 Subject: [PATCH 25/56] =?UTF-8?q?test(Client):=20Client=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/client/domain/ClientTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/test/java/shopping/member/client/domain/ClientTest.java diff --git a/src/test/java/shopping/member/client/domain/ClientTest.java b/src/test/java/shopping/member/client/domain/ClientTest.java new file mode 100644 index 0000000..13a058a --- /dev/null +++ b/src/test/java/shopping/member/client/domain/ClientTest.java @@ -0,0 +1,27 @@ +package shopping.member.client.domain; + +import static org.assertj.core.api.Assertions.assertThatNoException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import shopping.fake.FakePasswordEncoder; +import shopping.member.common.domain.PasswordEncoder; + +@DisplayName("Client") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ClientTest { + + private PasswordEncoder passwordEncoder = new FakePasswordEncoder(); + + @Test + void Client를_생성할_수_있다() { + assertThatNoException() + .isThrownBy(() -> createClient()); + } + + private Client createClient() { + return new Client("test@test.com", "1234", passwordEncoder, "test"); + } +} \ No newline at end of file From 34dab1a71b879f10ed17358571e8e47c88463c8c Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 26 Jul 2024 15:11:52 +0900 Subject: [PATCH 26/56] =?UTF-8?q?refactor(Member):=20isValidPassword=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/member/common/domain/Member.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/shopping/member/common/domain/Member.java b/src/main/java/shopping/member/common/domain/Member.java index 3b0e86c..5244020 100644 --- a/src/main/java/shopping/member/common/domain/Member.java +++ b/src/main/java/shopping/member/common/domain/Member.java @@ -4,12 +4,11 @@ import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; import jakarta.persistence.Table; +import lombok.Getter; import shopping.common.domain.BaseEntity; @Entity @@ -20,6 +19,7 @@ public abstract class Member extends BaseEntity { @Id @Column(name = "email", nullable = false) + @Getter private String email; @Embedded @@ -28,18 +28,17 @@ public abstract class Member extends BaseEntity { @Column(name = "member_name", nullable = false) private String memberName; - @Enumerated(EnumType.STRING) - @Column(name = "member_priority", nullable = false) - private MemberPriority memberPriority; - protected Member() { } - protected Member(final String email, final Password password, final String memberName, - final MemberPriority memberPriority) { + protected Member(final String email, final Password password, final String memberName) { this.email = email; this.password = password; this.memberName = memberName; - this.memberPriority = memberPriority; + } + + public boolean isValidPassword(final String rawPassword, + final PasswordEncoder passwordEncoder) { + return password.isMatch(rawPassword, passwordEncoder); } } From b426646ddd80b4e049195afa47220fcf78585b8c Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 26 Jul 2024 15:13:37 +0900 Subject: [PATCH 27/56] =?UTF-8?q?feat(Member):=20MemberRepository=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/member/common/domain/MemberRepository.java | 8 ++++++++ .../member/common/infra/JPAMemberRepository.java | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/main/java/shopping/member/common/domain/MemberRepository.java create mode 100644 src/main/java/shopping/member/common/infra/JPAMemberRepository.java diff --git a/src/main/java/shopping/member/common/domain/MemberRepository.java b/src/main/java/shopping/member/common/domain/MemberRepository.java new file mode 100644 index 0000000..0c5636b --- /dev/null +++ b/src/main/java/shopping/member/common/domain/MemberRepository.java @@ -0,0 +1,8 @@ +package shopping.member.common.domain; + +import java.util.Optional; + +public interface MemberRepository { + + Optional findByEmail(String email); +} diff --git a/src/main/java/shopping/member/common/infra/JPAMemberRepository.java b/src/main/java/shopping/member/common/infra/JPAMemberRepository.java new file mode 100644 index 0000000..9332b3e --- /dev/null +++ b/src/main/java/shopping/member/common/infra/JPAMemberRepository.java @@ -0,0 +1,9 @@ +package shopping.member.common.infra; + +import org.springframework.data.jpa.repository.JpaRepository; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberRepository; + +public interface JPAMemberRepository extends MemberRepository, JpaRepository { + +} From b2ffb9f45d3145a5a6dad7aa5e02942b78b01fbd Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 26 Jul 2024 15:16:05 +0900 Subject: [PATCH 28/56] =?UTF-8?q?feat(AuthService):=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EC=B1=85=EC=9E=84=EC=9D=84=20=EA=B0=96=EB=8A=94=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이메일 검증, 비밀번호 인코딩, 로그인 --- .../common/application/AuthService.java | 45 +++++++++++ .../exception/InvalidEmailException.java | 8 ++ .../exception/InvalidPasswordException.java | 8 ++ .../exception/NotFoundMemberException.java | 8 ++ .../java/shopping/fake/InMemoryMembers.java | 20 +++++ .../common/application/AuthServiceTest.java | 79 +++++++++++++++++++ 6 files changed, 168 insertions(+) create mode 100644 src/main/java/shopping/member/common/application/AuthService.java create mode 100644 src/main/java/shopping/member/common/exception/InvalidEmailException.java create mode 100644 src/main/java/shopping/member/common/exception/InvalidPasswordException.java create mode 100644 src/main/java/shopping/member/common/exception/NotFoundMemberException.java create mode 100644 src/test/java/shopping/fake/InMemoryMembers.java create mode 100644 src/test/java/shopping/member/common/application/AuthServiceTest.java diff --git a/src/main/java/shopping/member/common/application/AuthService.java b/src/main/java/shopping/member/common/application/AuthService.java new file mode 100644 index 0000000..d77b7d1 --- /dev/null +++ b/src/main/java/shopping/member/common/application/AuthService.java @@ -0,0 +1,45 @@ +package shopping.member.common.application; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import shopping.member.common.exception.InvalidEmailException; +import shopping.member.common.domain.Password; +import shopping.member.common.domain.PasswordEncoder; +import shopping.member.common.exception.InvalidPasswordException; +import shopping.member.common.exception.NotFoundMemberException; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberRepository; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + public void validateEmail(final String email) { + final Optional existMember = memberRepository.findByEmail(email); + if (existMember.isPresent()) { + throw new InvalidEmailException("이미 존재하는 이메일 입니다. " + email); + } + } + + public Password encodePassword(final String rawPassword){ + return new Password(rawPassword, passwordEncoder); + } + + public String login(final String email, final String rawPassword){ + final Member member = findMember(email); + if(!member.isValidPassword(rawPassword, passwordEncoder)){ + throw new InvalidPasswordException("비밀번호가 틀렸습니다."); + } + + return ""; // 토큰넘겨주기 + } + + private Member findMember(String email){ + return memberRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundMemberException("찾을 수 없는 사용자입니다. " + email)); + } +} diff --git a/src/main/java/shopping/member/common/exception/InvalidEmailException.java b/src/main/java/shopping/member/common/exception/InvalidEmailException.java new file mode 100644 index 0000000..75ff5ad --- /dev/null +++ b/src/main/java/shopping/member/common/exception/InvalidEmailException.java @@ -0,0 +1,8 @@ +package shopping.member.common.exception; + +public class InvalidEmailException extends RuntimeException { + + public InvalidEmailException(final String message) { + super(message); + } +} diff --git a/src/main/java/shopping/member/common/exception/InvalidPasswordException.java b/src/main/java/shopping/member/common/exception/InvalidPasswordException.java new file mode 100644 index 0000000..13d47e7 --- /dev/null +++ b/src/main/java/shopping/member/common/exception/InvalidPasswordException.java @@ -0,0 +1,8 @@ +package shopping.member.common.exception; + +public class InvalidPasswordException extends RuntimeException { + + public InvalidPasswordException(final String message) { + super(message); + } +} diff --git a/src/main/java/shopping/member/common/exception/NotFoundMemberException.java b/src/main/java/shopping/member/common/exception/NotFoundMemberException.java new file mode 100644 index 0000000..a1586f0 --- /dev/null +++ b/src/main/java/shopping/member/common/exception/NotFoundMemberException.java @@ -0,0 +1,8 @@ +package shopping.member.common.exception; + +public class NotFoundMemberException extends RuntimeException { + + public NotFoundMemberException(final String message) { + super(message); + } +} diff --git a/src/test/java/shopping/fake/InMemoryMembers.java b/src/test/java/shopping/fake/InMemoryMembers.java new file mode 100644 index 0000000..4952586 --- /dev/null +++ b/src/test/java/shopping/fake/InMemoryMembers.java @@ -0,0 +1,20 @@ +package shopping.fake; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import shopping.member.common.domain.Member; + +public class InMemoryMembers { + + private final Map members = new HashMap<>(); + + public Member save(final Member member) { + members.put(member.getEmail(), member); + return member; + } + + public Optional findByEmail(final String email) { + return Optional.ofNullable(members.get(email)); + } +} diff --git a/src/test/java/shopping/member/common/application/AuthServiceTest.java b/src/test/java/shopping/member/common/application/AuthServiceTest.java new file mode 100644 index 0000000..fc28aeb --- /dev/null +++ b/src/test/java/shopping/member/common/application/AuthServiceTest.java @@ -0,0 +1,79 @@ +package shopping.member.common.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import shopping.fake.FakePasswordEncoder; +import shopping.fake.FakeMemberRepository; +import shopping.fake.InMemoryMembers; +import shopping.member.client.domain.Client; +import shopping.member.common.domain.MemberRepository; +import shopping.member.common.domain.Password; +import shopping.member.common.domain.PasswordEncoder; +import shopping.member.common.exception.InvalidEmailException; +import shopping.member.common.exception.InvalidPasswordException; +import shopping.member.common.exception.NotFoundMemberException; + +@DisplayName("AuthService") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class AuthServiceTest { + + private final InMemoryMembers inMemoryMembers = new InMemoryMembers(); + private final PasswordEncoder passwordEncoder = new FakePasswordEncoder(); + private AuthService authService; + + @BeforeEach + void setUp() { + final MemberRepository memberRepository = new FakeMemberRepository(inMemoryMembers); + authService = new AuthService(memberRepository, passwordEncoder); + } + + @Test + void 중복되는_이메일을_검증할_수_있다() { + saveMember(); + + assertThatThrownBy(() -> authService.validateEmail("test@test.com")) + .isInstanceOf(InvalidEmailException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"1234", "qwer", "password"}) + void 패스워드를_암호화한다(String rawPassword) { + final Password password = authService.encodePassword(rawPassword); + + assertThat(password.isMatch(rawPassword, passwordEncoder)).isTrue(); + } + + @Test + void 로그인을_수행한다() { + saveMember(); + + assertThat(authService.login("test@test.com", "1234")).isNotNull(); + } + + @Test + void 가입되지않은_멤버의_로그인을_수행하면_예외를_던진다() { + assertThatThrownBy(() -> authService.login("test@test.com", "1234")) + .isInstanceOf(NotFoundMemberException.class); + } + + @Test + void 로그인수행_시_비밀번호를_틀리면_예외를_던진다() { + saveMember(); + + assertThatThrownBy(() -> authService.login("test@test.com", "1111")) + .isInstanceOf(InvalidPasswordException.class); + } + + private void saveMember() { + final Password password = new Password("1234", passwordEncoder); + inMemoryMembers.save(new Client("test@test.com", password, "test")); + } +} \ No newline at end of file From ec28d39d2b3d957af16242c5b7ef1d9f051d4b87 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 26 Jul 2024 15:24:27 +0900 Subject: [PATCH 29/56] =?UTF-8?q?test(Client):=20Client=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shopping/member/client/domain/Client.java | 10 ++++------ .../java/shopping/member/client/domain/ClientTest.java | 6 ++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/shopping/member/client/domain/Client.java b/src/main/java/shopping/member/client/domain/Client.java index 74e3a7b..57211cf 100644 --- a/src/main/java/shopping/member/client/domain/Client.java +++ b/src/main/java/shopping/member/client/domain/Client.java @@ -3,9 +3,7 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import shopping.member.common.domain.Member; -import shopping.member.common.domain.MemberPriority; import shopping.member.common.domain.Password; -import shopping.member.common.domain.PasswordEncoder; @Entity public class Client extends Member { @@ -16,15 +14,15 @@ public class Client extends Member { protected Client() { } - public Client(String email, String rawPassword, PasswordEncoder encoder, String memberName) { - super(email, new Password(rawPassword, encoder), memberName, MemberPriority.CLIENT); + public Client(final String email, final Password password, final String memberName) { + super(email, password, memberName); } - public void wish(WishProduct wishProduct) { + public void wish(final WishProduct wishProduct) { wishProducts.wish(wishProduct); } - public void unWish(WishProduct wishProduct) { + public void unWish(final WishProduct wishProduct) { wishProducts.unWish(wishProduct); } } diff --git a/src/test/java/shopping/member/client/domain/ClientTest.java b/src/test/java/shopping/member/client/domain/ClientTest.java index 13a058a..027d3e1 100644 --- a/src/test/java/shopping/member/client/domain/ClientTest.java +++ b/src/test/java/shopping/member/client/domain/ClientTest.java @@ -7,13 +7,14 @@ import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import shopping.fake.FakePasswordEncoder; +import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; @DisplayName("Client") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class ClientTest { - private PasswordEncoder passwordEncoder = new FakePasswordEncoder(); + private final PasswordEncoder passwordEncoder = new FakePasswordEncoder(); @Test void Client를_생성할_수_있다() { @@ -22,6 +23,7 @@ class ClientTest { } private Client createClient() { - return new Client("test@test.com", "1234", passwordEncoder, "test"); + final Password password = new Password("1234", passwordEncoder); + return new Client("test@test.com", password, "test"); } } \ No newline at end of file From 9a370f37d103252c7949f491188dd7ab8b1c028f Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Fri, 26 Jul 2024 15:26:22 +0900 Subject: [PATCH 30/56] =?UTF-8?q?feat(OwnerLoginService):=20Owner=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=A0=84=EC=9A=A9=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../owner/application/OwnerLoginService.java | 34 +++++++++++ .../application/dto/OwnerCreateRequest.java | 14 +++++ .../application/dto/OwnerLoginRequest.java | 12 ++++ .../shopping/member/owner/domain/Owner.java | 6 +- .../member/owner/domain/OwnerRepository.java | 1 + .../shopping/fake/FakeOwnerRepository.java | 18 ++++++ .../application/OwnerLoginServiceTest.java | 58 +++++++++++++++++++ 7 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/main/java/shopping/member/owner/application/OwnerLoginService.java create mode 100644 src/main/java/shopping/member/owner/application/dto/OwnerCreateRequest.java create mode 100644 src/main/java/shopping/member/owner/application/dto/OwnerLoginRequest.java create mode 100644 src/test/java/shopping/fake/FakeOwnerRepository.java create mode 100644 src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java diff --git a/src/main/java/shopping/member/owner/application/OwnerLoginService.java b/src/main/java/shopping/member/owner/application/OwnerLoginService.java new file mode 100644 index 0000000..999c1a8 --- /dev/null +++ b/src/main/java/shopping/member/owner/application/OwnerLoginService.java @@ -0,0 +1,34 @@ +package shopping.member.owner.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import shopping.member.common.application.AuthService; +import shopping.member.common.domain.Password; +import shopping.member.owner.application.dto.OwnerCreateRequest; +import shopping.member.owner.application.dto.OwnerLoginRequest; +import shopping.member.owner.domain.Owner; +import shopping.member.owner.domain.OwnerRepository; + +@Service +@RequiredArgsConstructor +public class OwnerLoginService { + + private final AuthService authService; + private final OwnerRepository ownerRepository; + + public void signUp(final OwnerCreateRequest request) { + authService.validateEmail(request.email()); + + final Password password = authService.encodePassword(request.password()); + final Owner owner = new Owner( + request.email(), + password, + request.memberName() + ); + ownerRepository.save(owner); + } + + public String login(final OwnerLoginRequest request) { + return authService.login(request.email(), request.password()); + } +} diff --git a/src/main/java/shopping/member/owner/application/dto/OwnerCreateRequest.java b/src/main/java/shopping/member/owner/application/dto/OwnerCreateRequest.java new file mode 100644 index 0000000..bdd0dd3 --- /dev/null +++ b/src/main/java/shopping/member/owner/application/dto/OwnerCreateRequest.java @@ -0,0 +1,14 @@ +package shopping.member.owner.application.dto; + +import jakarta.validation.constraints.NotBlank; + +public record OwnerCreateRequest( + @NotBlank + String email, + @NotBlank + String password, + @NotBlank + String memberName +) { + +} diff --git a/src/main/java/shopping/member/owner/application/dto/OwnerLoginRequest.java b/src/main/java/shopping/member/owner/application/dto/OwnerLoginRequest.java new file mode 100644 index 0000000..7d2c73a --- /dev/null +++ b/src/main/java/shopping/member/owner/application/dto/OwnerLoginRequest.java @@ -0,0 +1,12 @@ +package shopping.member.owner.application.dto; + +import jakarta.validation.constraints.NotBlank; + +public record OwnerLoginRequest( + @NotBlank + String email, + @NotBlank + String password +) { + +} diff --git a/src/main/java/shopping/member/owner/domain/Owner.java b/src/main/java/shopping/member/owner/domain/Owner.java index f33533e..e1df0e0 100644 --- a/src/main/java/shopping/member/owner/domain/Owner.java +++ b/src/main/java/shopping/member/owner/domain/Owner.java @@ -2,9 +2,7 @@ import jakarta.persistence.Entity; import shopping.member.common.domain.Member; -import shopping.member.common.domain.MemberPriority; import shopping.member.common.domain.Password; -import shopping.member.common.domain.PasswordEncoder; @Entity public class Owner extends Member { @@ -12,7 +10,7 @@ public class Owner extends Member { protected Owner() { } - public Owner(String email, String rawPassword, PasswordEncoder encoder, String memberName){ - super(email, new Password(rawPassword, encoder), memberName, MemberPriority.OWNER); + public Owner(final String email, final Password password, final String memberName) { + super(email, password, memberName); } } diff --git a/src/main/java/shopping/member/owner/domain/OwnerRepository.java b/src/main/java/shopping/member/owner/domain/OwnerRepository.java index 63c5a39..b657ade 100644 --- a/src/main/java/shopping/member/owner/domain/OwnerRepository.java +++ b/src/main/java/shopping/member/owner/domain/OwnerRepository.java @@ -2,4 +2,5 @@ public interface OwnerRepository { + Owner save(Owner owner); } diff --git a/src/test/java/shopping/fake/FakeOwnerRepository.java b/src/test/java/shopping/fake/FakeOwnerRepository.java new file mode 100644 index 0000000..a70cff5 --- /dev/null +++ b/src/test/java/shopping/fake/FakeOwnerRepository.java @@ -0,0 +1,18 @@ +package shopping.fake; + +import shopping.member.owner.domain.Owner; +import shopping.member.owner.domain.OwnerRepository; + +public class FakeOwnerRepository implements OwnerRepository { + + private final InMemoryMembers inMemoryMembers; + + public FakeOwnerRepository(InMemoryMembers inMemoryMembers) { + this.inMemoryMembers = inMemoryMembers; + } + + @Override + public Owner save(final Owner owner) { + return (Owner) inMemoryMembers.save(owner); + } +} diff --git a/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java b/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java new file mode 100644 index 0000000..1a39c24 --- /dev/null +++ b/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java @@ -0,0 +1,58 @@ +package shopping.member.owner.application; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import shopping.fake.FakePasswordEncoder; +import shopping.fake.FakeMemberRepository; +import shopping.fake.InMemoryMembers; +import shopping.fake.FakeOwnerRepository; +import shopping.member.common.application.AuthService; +import shopping.member.common.domain.MemberRepository; +import shopping.member.common.exception.InvalidEmailException; +import shopping.member.owner.application.dto.OwnerCreateRequest; +import shopping.member.owner.domain.OwnerRepository; + +@DisplayName("OwnerLoginService") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class OwnerLoginServiceTest { + + private OwnerLoginService ownerService; + + @BeforeEach + void setUp() { + final InMemoryMembers inMemoryMembers = new InMemoryMembers(); + final MemberRepository memberRepository = new FakeMemberRepository(inMemoryMembers); + final AuthService authService = new AuthService(memberRepository, + new FakePasswordEncoder()); + final OwnerRepository ownerRepository = new FakeOwnerRepository(inMemoryMembers); + ownerService = new OwnerLoginService(authService, ownerRepository); + } + + @Test + void 회원가입을_진행할_수_있다() { + assertThatNoException().isThrownBy(() -> ownerService.signUp(createRequest())); + } + + @Test + void 같은_이메일로_중복해서_가입할_수_없다() { + final OwnerCreateRequest request = createRequest(); + ownerService.signUp(request); + + assertThatThrownBy(() -> ownerService.signUp(request)) + .isInstanceOf(InvalidEmailException.class); + } + + private OwnerCreateRequest createRequest() { + return new OwnerCreateRequest( + "admin@test.com", + "1234", + "admin" + ); + } +} \ No newline at end of file From 7f7c93cfc9c35b8ab1e8c4a151a393ecc4869d60 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 19:03:34 +0900 Subject: [PATCH 31/56] =?UTF-8?q?refactor(WishProduct):=20product=EC=9D=98?= =?UTF-8?q?=20id=EB=A7=8C=20=EC=9D=98=EC=A1=B4=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/client/domain/WishProduct.java | 10 +++----- .../member/client/domain/WishProducts.java | 25 ++++++++++++------- .../client/domain/WishProductsTest.java | 14 +++++------ 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/main/java/shopping/member/client/domain/WishProduct.java b/src/main/java/shopping/member/client/domain/WishProduct.java index ba7b0d7..a75bac6 100644 --- a/src/main/java/shopping/member/client/domain/WishProduct.java +++ b/src/main/java/shopping/member/client/domain/WishProduct.java @@ -5,6 +5,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.Getter; import shopping.common.domain.BaseEntity; @Entity @@ -16,6 +17,7 @@ public class WishProduct extends BaseEntity { @Column(name = "wish_product_id", nullable = false) private Long id; + @Getter private Long productId; protected WishProduct() { @@ -25,11 +27,7 @@ public WishProduct(final Long productId) { this.productId = productId; } - public boolean isSameProduct(WishProduct wishProduct){ - return this.productId.equals(wishProduct.productId); - } - - public Long getProductId() { - return productId; + public boolean isSameProduct(Long productId){ + return this.productId.equals(productId); } } diff --git a/src/main/java/shopping/member/client/domain/WishProducts.java b/src/main/java/shopping/member/client/domain/WishProducts.java index 7def18a..e10d640 100644 --- a/src/main/java/shopping/member/client/domain/WishProducts.java +++ b/src/main/java/shopping/member/client/domain/WishProducts.java @@ -6,6 +6,7 @@ import jakarta.persistence.OneToMany; import java.util.ArrayList; import java.util.List; +import shopping.member.client.exception.DuplicateWishProductException; import shopping.member.client.exception.NotFoundWishProductException; @Embeddable @@ -18,26 +19,32 @@ public class WishProducts { protected WishProducts() { } - public void wish(WishProduct addWishProduct) { - validateAddWishItem(addWishProduct); - this.wishProducts.add(addWishProduct); + public void wish(Long productId) { + validateAddWishItem(productId); + this.wishProducts.add(new WishProduct(productId)); } - private void validateAddWishItem(final WishProduct addWishProduct) { + private void validateAddWishItem(final Long productId) { final boolean match = this.wishProducts.stream() - .anyMatch(wishProduct -> wishProduct.isSameProduct(addWishProduct)); + .anyMatch(wishProduct -> wishProduct.isSameProduct(productId)); if (match) { throw new DuplicateWishProductException( - "중복되는 상품에 하트를 누를 수 없습니다. " + addWishProduct.getProductId()); + "중복되는 상품에 하트를 누를 수 없습니다. " + productId); } } - public void unWish(WishProduct delWishProduct) { + public void unWish(Long productId) { final boolean removed = this.wishProducts.removeIf( - wishProduct -> wishProduct.isSameProduct(delWishProduct)); + wishProduct -> wishProduct.isSameProduct(productId)); if (!removed) { throw new NotFoundWishProductException( - "해당 위시상품을 찾을 수 없습니다. " + delWishProduct.getProductId()); + "해당 위시상품을 찾을 수 없습니다. " + productId); } } + + public List productIds() { + return wishProducts.stream() + .map(WishProduct::getProductId) + .toList(); + } } diff --git a/src/test/java/shopping/member/client/domain/WishProductsTest.java b/src/test/java/shopping/member/client/domain/WishProductsTest.java index 9deba7f..c301b18 100644 --- a/src/test/java/shopping/member/client/domain/WishProductsTest.java +++ b/src/test/java/shopping/member/client/domain/WishProductsTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import shopping.member.client.exception.DuplicateWishProductException; import shopping.member.client.exception.NotFoundWishProductException; @DisplayName("WishProducts") @@ -16,30 +17,27 @@ class WishProductsTest { @Test void WishProducts에_위시상품을_추가했다가_삭제할_수_있다() { final WishProducts wishProducts = new WishProducts(); - final WishProduct wishProduct = new WishProduct(1L); assertThatNoException().isThrownBy(() -> { - wishProducts.wish(wishProduct); - wishProducts.unWish(wishProduct); + wishProducts.wish(1L); + wishProducts.unWish(1L); }); } @Test void WishProducts에_위시상품을_중복해서_추가할_수_없다() { final WishProducts wishProducts = new WishProducts(); - final WishProduct wishProduct = new WishProduct(1L); - wishProducts.wish(wishProduct); + wishProducts.wish(1L); - assertThatThrownBy(() -> wishProducts.wish(wishProduct)) + assertThatThrownBy(() -> wishProducts.wish(1L)) .isInstanceOf(DuplicateWishProductException.class); } @Test void WishProducts에_없는_위시상품을_삭제할_수_없다() { final WishProducts wishProducts = new WishProducts(); - final WishProduct wishProduct = new WishProduct(1L); - assertThatThrownBy(() -> wishProducts.unWish(wishProduct)) + assertThatThrownBy(() -> wishProducts.unWish(1L)) .isInstanceOf(NotFoundWishProductException.class); } } \ No newline at end of file From 57c267138ce4ecdf32397ee567812a5254ec5a4b Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 19:04:38 +0900 Subject: [PATCH 32/56] =?UTF-8?q?feat(WishProductMapper):=20Client?= =?UTF-8?q?=EB=B0=94=EC=9A=B4=EB=94=94=EB=93=9C=EC=BB=A8=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20product=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EA=B2=80=EC=A6=9D=EA=B3=BC=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=B1=85=EC=9E=84=EC=9D=84=20?= =?UTF-8?q?=EA=B0=96=EB=8A=94=20Mapper=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/applicaton/WishProductMapper.java | 30 +++++++++++++++++++ .../applicaton/dto/WishProductResponse.java | 12 ++++++++ .../product/application/ProductService.java | 23 ++++++++++++++ .../java/shopping/product/domain/Product.java | 30 +++++++++++++++++++ .../shopping/product/domain/ProductName.java | 2 ++ .../shopping/product/domain/ProductPrice.java | 2 ++ .../product/domain/ProductRepository.java | 13 ++++++++ .../java/shopping/fixture/ProductFixture.java | 12 ++++++++ .../product/domain/ProductPriceTest.java | 2 +- .../shopping/product/domain/ProductTest.java | 5 ++-- 10 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 src/main/java/shopping/member/client/applicaton/WishProductMapper.java create mode 100644 src/main/java/shopping/member/client/applicaton/dto/WishProductResponse.java create mode 100644 src/main/java/shopping/product/application/ProductService.java create mode 100644 src/main/java/shopping/product/domain/ProductRepository.java create mode 100644 src/test/java/shopping/fixture/ProductFixture.java diff --git a/src/main/java/shopping/member/client/applicaton/WishProductMapper.java b/src/main/java/shopping/member/client/applicaton/WishProductMapper.java new file mode 100644 index 0000000..4b79ba3 --- /dev/null +++ b/src/main/java/shopping/member/client/applicaton/WishProductMapper.java @@ -0,0 +1,30 @@ +package shopping.member.client.applicaton; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import shopping.member.client.applicaton.dto.WishProductResponse; +import shopping.product.application.ProductService; + +@Service +@RequiredArgsConstructor +public class WishProductMapper { + + private final ProductService productService; + + public void validateProduct(Long productId) { + productService.findProduct(productId); + } + + public List createResponse(List productIds) { + return productService.findProducts(productIds) + .stream() + .map(product -> new WishProductResponse( + product.getId(), + product.getName(), + product.getPrice(), + product.getImage()) + ) + .toList(); + } +} diff --git a/src/main/java/shopping/member/client/applicaton/dto/WishProductResponse.java b/src/main/java/shopping/member/client/applicaton/dto/WishProductResponse.java new file mode 100644 index 0000000..48660ca --- /dev/null +++ b/src/main/java/shopping/member/client/applicaton/dto/WishProductResponse.java @@ -0,0 +1,12 @@ +package shopping.member.client.applicaton.dto; + +import java.math.BigDecimal; + +public record WishProductResponse( + Long id, + String name, + BigDecimal bigDecimal, + String image +) { + +} diff --git a/src/main/java/shopping/product/application/ProductService.java b/src/main/java/shopping/product/application/ProductService.java new file mode 100644 index 0000000..917c914 --- /dev/null +++ b/src/main/java/shopping/product/application/ProductService.java @@ -0,0 +1,23 @@ +package shopping.product.application; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import shopping.product.domain.Product; +import shopping.product.domain.ProductRepository; + +@Service +@RequiredArgsConstructor +public class ProductService { + + private final ProductRepository productRepository; + + public Product findProduct(Long id) { + return productRepository.findById(id) + .orElseThrow(() -> new NotFoundProductException("해당 상품을 찾을 수 없습니다. " + id)); + } + + public List findProducts(List ids){ + return productRepository.findByIdIn(ids); + } +} diff --git a/src/main/java/shopping/product/domain/Product.java b/src/main/java/shopping/product/domain/Product.java index 95ce24f..2d46d45 100644 --- a/src/main/java/shopping/product/domain/Product.java +++ b/src/main/java/shopping/product/domain/Product.java @@ -5,6 +5,8 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import java.math.BigDecimal; +import lombok.Getter; import shopping.common.domain.AuditableBaseEntity; @Entity @@ -12,6 +14,7 @@ public class Product extends AuditableBaseEntity { @Id @GeneratedValue + @Getter @Column(name = "product_id", nullable = false) private Long id; @@ -22,15 +25,42 @@ public class Product extends AuditableBaseEntity { private ProductPrice price; @Column(name = "product_image") + @Getter private String image; + @Column(name = "product_heart_count") + private Long heartCount; + protected Product() { } public Product(final String name, final ProfanityChecker profanityChecker, final Long price, final String image) { + this(null, name, profanityChecker, price, image); + } + + public Product(final Long id, final String name, final ProfanityChecker profanityChecker, + final Long price, final String image) { + this.id = id; this.name = new ProductName(name, profanityChecker); this.price = new ProductPrice(price); this.image = image; + this.heartCount = 0L; + } + + public void wish() { + this.heartCount++; + } + + public void unWish() { + this.heartCount--; + } + + public String getName() { + return name.getName(); + } + + public BigDecimal getPrice() { + return price.getPrice(); } } diff --git a/src/main/java/shopping/product/domain/ProductName.java b/src/main/java/shopping/product/domain/ProductName.java index f9ff792..08fdab4 100644 --- a/src/main/java/shopping/product/domain/ProductName.java +++ b/src/main/java/shopping/product/domain/ProductName.java @@ -4,6 +4,7 @@ import jakarta.persistence.Embeddable; import java.util.regex.Matcher; import java.util.regex.Pattern; +import lombok.Getter; import shopping.product.exception.InvalidProductNameException; @Embeddable @@ -14,6 +15,7 @@ public class ProductName { @Column(name = "product_name", nullable = false) + @Getter private String name; protected ProductName() { diff --git a/src/main/java/shopping/product/domain/ProductPrice.java b/src/main/java/shopping/product/domain/ProductPrice.java index 8b0dded..291c18d 100644 --- a/src/main/java/shopping/product/domain/ProductPrice.java +++ b/src/main/java/shopping/product/domain/ProductPrice.java @@ -3,12 +3,14 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.math.BigDecimal; +import lombok.Getter; import shopping.product.exception.InvalidProductPriceException; @Embeddable public class ProductPrice { @Column(name = "product_price", nullable = false) + @Getter private BigDecimal price; protected ProductPrice() { diff --git a/src/main/java/shopping/product/domain/ProductRepository.java b/src/main/java/shopping/product/domain/ProductRepository.java new file mode 100644 index 0000000..7a851fe --- /dev/null +++ b/src/main/java/shopping/product/domain/ProductRepository.java @@ -0,0 +1,13 @@ +package shopping.product.domain; + +import java.util.List; +import java.util.Optional; + +public interface ProductRepository { + + Product save(Product product); + + Optional findById(Long id); + + List findByIdIn(List ids); +} diff --git a/src/test/java/shopping/fixture/ProductFixture.java b/src/test/java/shopping/fixture/ProductFixture.java new file mode 100644 index 0000000..9f2d71f --- /dev/null +++ b/src/test/java/shopping/fixture/ProductFixture.java @@ -0,0 +1,12 @@ +package shopping.fixture; + +import shopping.fake.FakeProfanityChecker; +import shopping.product.domain.Product; + +public class ProductFixture { + + public static Product createProduct(){ + final FakeProfanityChecker profanityChecker = new FakeProfanityChecker(); + return new Product(1L, "맥북", profanityChecker, 1_000L, "image.jpg"); + } +} diff --git a/src/test/java/shopping/product/domain/ProductPriceTest.java b/src/test/java/shopping/product/domain/ProductPriceTest.java index cdd4a0f..ae33a61 100644 --- a/src/test/java/shopping/product/domain/ProductPriceTest.java +++ b/src/test/java/shopping/product/domain/ProductPriceTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.params.provider.ValueSource; import shopping.product.exception.InvalidProductPriceException; -@DisplayName("ProductName") +@DisplayName("ProductPrice") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class ProductPriceTest { diff --git a/src/test/java/shopping/product/domain/ProductTest.java b/src/test/java/shopping/product/domain/ProductTest.java index 7c80448..39d948e 100644 --- a/src/test/java/shopping/product/domain/ProductTest.java +++ b/src/test/java/shopping/product/domain/ProductTest.java @@ -7,16 +7,15 @@ import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import shopping.fake.FakeProfanityChecker; +import shopping.fixture.ProductFixture; @DisplayName("Product") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class ProductTest { - private ProfanityChecker profanityChecker = new FakeProfanityChecker(); - @Test void Product를_생성할_수_있다() { assertThatNoException() - .isThrownBy(() -> new Product("맥북", profanityChecker, 1_000L, "image.jpg")); + .isThrownBy(ProductFixture::createProduct); } } \ No newline at end of file From fdb081e3f656bbe3d8d60005d09d4e832613229e Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 19:08:29 +0900 Subject: [PATCH 33/56] =?UTF-8?q?feat(ClientLoginService):=20Client?= =?UTF-8?q?=EC=9D=98=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=EA=B3=BC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=97=90=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=EC=9D=84=20=EA=B0=96=EB=8A=94=20=EC=96=B4?= =?UTF-8?q?=ED=94=8C=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/applicaton/ClientLoginService.java | 34 +++++++++++ .../applicaton/dto/ClientCreateRequest.java | 14 +++++ .../applicaton/dto/ClientLoginRequest.java | 12 ++++ .../shopping/member/client/domain/Client.java | 15 +++-- .../client/domain/ClientRepository.java | 1 + .../client/infra/JPAClientRepository.java | 1 + .../common/infra/DefaultPasswordEncoder.java | 23 ++++++++ .../shopping/fake/FakeClientRepository.java | 18 ++++++ .../shopping/fake/FakeMemberRepository.java | 19 ++++++ .../java/shopping/fixture/ClientFixture.java | 13 +++++ .../applicaton/ClientLoginServiceTest.java | 58 +++++++++++++++++++ .../member/client/domain/ClientTest.java | 13 +---- .../member/common/domain/PasswordTest.java | 2 +- 13 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 src/main/java/shopping/member/client/applicaton/ClientLoginService.java create mode 100644 src/main/java/shopping/member/client/applicaton/dto/ClientCreateRequest.java create mode 100644 src/main/java/shopping/member/client/applicaton/dto/ClientLoginRequest.java create mode 100644 src/main/java/shopping/member/common/infra/DefaultPasswordEncoder.java create mode 100644 src/test/java/shopping/fake/FakeClientRepository.java create mode 100644 src/test/java/shopping/fake/FakeMemberRepository.java create mode 100644 src/test/java/shopping/fixture/ClientFixture.java create mode 100644 src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java diff --git a/src/main/java/shopping/member/client/applicaton/ClientLoginService.java b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java new file mode 100644 index 0000000..faf230c --- /dev/null +++ b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java @@ -0,0 +1,34 @@ +package shopping.member.client.applicaton; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import shopping.member.client.applicaton.dto.ClientCreateRequest; +import shopping.member.client.applicaton.dto.ClientLoginRequest; +import shopping.member.client.domain.Client; +import shopping.member.client.domain.ClientRepository; +import shopping.member.common.application.AuthService; +import shopping.member.common.domain.Password; + +@Service +@RequiredArgsConstructor +public class ClientLoginService { + + private final AuthService authService; + private final ClientRepository clientRepository; + + public void signUp(final ClientCreateRequest request) { + authService.validateEmail(request.email()); + + final Password password = authService.encodePassword(request.password()); + final Client client = new Client( + request.email(), + password, + request.memberName() + ); + clientRepository.save(client); + } + + public String login(final ClientLoginRequest request) { + return authService.login(request.email(), request.password()); + } +} diff --git a/src/main/java/shopping/member/client/applicaton/dto/ClientCreateRequest.java b/src/main/java/shopping/member/client/applicaton/dto/ClientCreateRequest.java new file mode 100644 index 0000000..e87edcf --- /dev/null +++ b/src/main/java/shopping/member/client/applicaton/dto/ClientCreateRequest.java @@ -0,0 +1,14 @@ +package shopping.member.client.applicaton.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ClientCreateRequest( + @NotBlank + String email, + @NotBlank + String password, + @NotBlank + String memberName +) { + +} diff --git a/src/main/java/shopping/member/client/applicaton/dto/ClientLoginRequest.java b/src/main/java/shopping/member/client/applicaton/dto/ClientLoginRequest.java new file mode 100644 index 0000000..bdf7ed2 --- /dev/null +++ b/src/main/java/shopping/member/client/applicaton/dto/ClientLoginRequest.java @@ -0,0 +1,12 @@ +package shopping.member.client.applicaton.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ClientLoginRequest( + @NotBlank + String email, + @NotBlank + String password +) { + +} diff --git a/src/main/java/shopping/member/client/domain/Client.java b/src/main/java/shopping/member/client/domain/Client.java index 57211cf..513595d 100644 --- a/src/main/java/shopping/member/client/domain/Client.java +++ b/src/main/java/shopping/member/client/domain/Client.java @@ -2,6 +2,8 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import java.util.List; +import lombok.Getter; import shopping.member.common.domain.Member; import shopping.member.common.domain.Password; @@ -9,6 +11,7 @@ public class Client extends Member { @Embedded + @Getter private WishProducts wishProducts = new WishProducts(); protected Client() { @@ -18,11 +21,15 @@ public Client(final String email, final Password password, final String memberNa super(email, password, memberName); } - public void wish(final WishProduct wishProduct) { - wishProducts.wish(wishProduct); + public void wish(final Long productId) { + wishProducts.wish(productId); } - public void unWish(final WishProduct wishProduct) { - wishProducts.unWish(wishProduct); + public void unWish(final Long productId) { + wishProducts.unWish(productId); + } + + public List productIds() { + return wishProducts.productIds(); } } diff --git a/src/main/java/shopping/member/client/domain/ClientRepository.java b/src/main/java/shopping/member/client/domain/ClientRepository.java index ae3173e..0eb50f3 100644 --- a/src/main/java/shopping/member/client/domain/ClientRepository.java +++ b/src/main/java/shopping/member/client/domain/ClientRepository.java @@ -2,4 +2,5 @@ public interface ClientRepository { + Client save(Client client); } diff --git a/src/main/java/shopping/member/client/infra/JPAClientRepository.java b/src/main/java/shopping/member/client/infra/JPAClientRepository.java index d711980..4b1baf5 100644 --- a/src/main/java/shopping/member/client/infra/JPAClientRepository.java +++ b/src/main/java/shopping/member/client/infra/JPAClientRepository.java @@ -6,4 +6,5 @@ public interface JPAClientRepository extends ClientRepository, JpaRepository { + } diff --git a/src/main/java/shopping/member/common/infra/DefaultPasswordEncoder.java b/src/main/java/shopping/member/common/infra/DefaultPasswordEncoder.java new file mode 100644 index 0000000..716753b --- /dev/null +++ b/src/main/java/shopping/member/common/infra/DefaultPasswordEncoder.java @@ -0,0 +1,23 @@ +package shopping.member.common.infra; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; +import shopping.member.common.domain.PasswordEncoder; + +@Component +@RequiredArgsConstructor +public class DefaultPasswordEncoder implements PasswordEncoder { + + private final BCryptPasswordEncoder passwordEncoder; + + @Override + public String encode(final String rawPassword) { + return passwordEncoder.encode(rawPassword); + } + + @Override + public boolean matches(final String rawPassword, final String password) { + return passwordEncoder.matches(rawPassword, password); + } +} diff --git a/src/test/java/shopping/fake/FakeClientRepository.java b/src/test/java/shopping/fake/FakeClientRepository.java new file mode 100644 index 0000000..083b7c0 --- /dev/null +++ b/src/test/java/shopping/fake/FakeClientRepository.java @@ -0,0 +1,18 @@ +package shopping.fake; + +import shopping.member.client.domain.Client; +import shopping.member.client.domain.ClientRepository; + +public class FakeClientRepository implements ClientRepository { + + private final InMemoryMembers inMemoryMembers; + + public FakeClientRepository(InMemoryMembers inMemoryMembers) { + this.inMemoryMembers = inMemoryMembers; + } + + @Override + public Client save(final Client client) { + return (Client) inMemoryMembers.save(client); + } +} diff --git a/src/test/java/shopping/fake/FakeMemberRepository.java b/src/test/java/shopping/fake/FakeMemberRepository.java new file mode 100644 index 0000000..4773d8b --- /dev/null +++ b/src/test/java/shopping/fake/FakeMemberRepository.java @@ -0,0 +1,19 @@ +package shopping.fake; + +import java.util.Optional; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberRepository; + +public class FakeMemberRepository implements MemberRepository { + + private final InMemoryMembers inMemoryMembers; + + public FakeMemberRepository(InMemoryMembers inMemoryMembers) { + this.inMemoryMembers = inMemoryMembers; + } + + @Override + public Optional findByEmail(final String email) { + return inMemoryMembers.findByEmail(email); + } +} diff --git a/src/test/java/shopping/fixture/ClientFixture.java b/src/test/java/shopping/fixture/ClientFixture.java new file mode 100644 index 0000000..c92750a --- /dev/null +++ b/src/test/java/shopping/fixture/ClientFixture.java @@ -0,0 +1,13 @@ +package shopping.fixture; + +import shopping.fake.FakePasswordEncoder; +import shopping.member.client.domain.Client; +import shopping.member.common.domain.Password; + +public class ClientFixture { + + public static Client createClient(){ + final Password password = new Password("1234", new FakePasswordEncoder()); + return new Client("test@test.com", password, "test"); + } +} diff --git a/src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java b/src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java new file mode 100644 index 0000000..a533777 --- /dev/null +++ b/src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java @@ -0,0 +1,58 @@ +package shopping.member.client.applicaton; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import shopping.fake.FakePasswordEncoder; +import shopping.fake.FakeClientRepository; +import shopping.fake.FakeMemberRepository; +import shopping.fake.InMemoryMembers; +import shopping.member.client.applicaton.dto.ClientCreateRequest; +import shopping.member.client.domain.ClientRepository; +import shopping.member.common.application.AuthService; +import shopping.member.common.domain.MemberRepository; +import shopping.member.common.exception.InvalidEmailException; + +@DisplayName("ClientLoginService") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ClientLoginServiceTest { + + private ClientLoginService clientService; + + @BeforeEach + void setUp() { + final InMemoryMembers inMemoryMembers = new InMemoryMembers(); + final MemberRepository memberRepository = new FakeMemberRepository(inMemoryMembers); + final AuthService authService = new AuthService(memberRepository, + new FakePasswordEncoder()); + final ClientRepository clientRepository = new FakeClientRepository(inMemoryMembers); + clientService = new ClientLoginService(authService, clientRepository); + } + + @Test + void 회원가입을_진행할_수_있다() { + assertThatNoException().isThrownBy(() -> clientService.signUp(createRequest())); + } + + @Test + void 같은_이메일로_중복해서_가입할_수_없다() { + final ClientCreateRequest request = createRequest(); + clientService.signUp(request); + + assertThatThrownBy(() -> clientService.signUp(request)) + .isInstanceOf(InvalidEmailException.class); + } + + private ClientCreateRequest createRequest() { + return new ClientCreateRequest( + "test@test.com", + "1234", + "test" + ); + } +} \ No newline at end of file diff --git a/src/test/java/shopping/member/client/domain/ClientTest.java b/src/test/java/shopping/member/client/domain/ClientTest.java index 027d3e1..e0d6704 100644 --- a/src/test/java/shopping/member/client/domain/ClientTest.java +++ b/src/test/java/shopping/member/client/domain/ClientTest.java @@ -6,24 +6,15 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import shopping.fake.FakePasswordEncoder; -import shopping.member.common.domain.Password; -import shopping.member.common.domain.PasswordEncoder; +import shopping.fixture.ClientFixture; @DisplayName("Client") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class ClientTest { - private final PasswordEncoder passwordEncoder = new FakePasswordEncoder(); - @Test void Client를_생성할_수_있다() { assertThatNoException() - .isThrownBy(() -> createClient()); - } - - private Client createClient() { - final Password password = new Password("1234", passwordEncoder); - return new Client("test@test.com", password, "test"); + .isThrownBy(ClientFixture::createClient); } } \ No newline at end of file diff --git a/src/test/java/shopping/member/common/domain/PasswordTest.java b/src/test/java/shopping/member/common/domain/PasswordTest.java index 84b902e..f75269f 100644 --- a/src/test/java/shopping/member/common/domain/PasswordTest.java +++ b/src/test/java/shopping/member/common/domain/PasswordTest.java @@ -15,7 +15,7 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class PasswordTest { - private PasswordEncoder passwordEncoder = new FakePasswordEncoder(); + private final PasswordEncoder passwordEncoder = new FakePasswordEncoder(); @Test void Password를_생성할_수_있다() { From 7562872f054d4947e0c3246105b79a6281860207 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 19:09:22 +0900 Subject: [PATCH 34/56] =?UTF-8?q?feat(ClientWishService):=20Client?= =?UTF-8?q?=EC=9D=98=20=EC=9C=84=EC=8B=9C=EC=83=81=ED=92=88=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80/=EC=82=AD=EC=A0=9C/=EC=A1=B0=ED=9A=8C=EC=97=90?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EC=B1=85=EC=9E=84=EC=9D=84=20=EA=B0=96?= =?UTF-8?q?=EB=8A=94=20=EC=96=B4=ED=94=8C=EB=A6=AC=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/shopping/Application.java | 2 + .../client/applicaton/ClientWishService.java | 36 ++++++ .../DuplicateWishProductException.java | 2 +- .../application/NotFoundProductException.java | 8 ++ .../product/application/WishEventHandler.java | 33 ++++++ .../application/event/UnWishEvent.java | 7 ++ .../product/application/event/WishEvent.java | 7 ++ .../product/infra/JPAProductRepository.java | 9 ++ .../shopping/fake/FakeProductRepository.java | 33 ++++++ .../applicaton/ClientWishServiceTest.java | 112 ++++++++++++++++++ 10 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/main/java/shopping/member/client/applicaton/ClientWishService.java rename src/main/java/shopping/member/client/{domain => exception}/DuplicateWishProductException.java (80%) create mode 100644 src/main/java/shopping/product/application/NotFoundProductException.java create mode 100644 src/main/java/shopping/product/application/WishEventHandler.java create mode 100644 src/main/java/shopping/product/application/event/UnWishEvent.java create mode 100644 src/main/java/shopping/product/application/event/WishEvent.java create mode 100644 src/main/java/shopping/product/infra/JPAProductRepository.java create mode 100644 src/test/java/shopping/fake/FakeProductRepository.java create mode 100644 src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java diff --git a/src/main/java/shopping/Application.java b/src/main/java/shopping/Application.java index 9ab85bd..fa33ff7 100644 --- a/src/main/java/shopping/Application.java +++ b/src/main/java/shopping/Application.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +@EnableAsync @SpringBootApplication public class Application { public static void main(String[] args) { diff --git a/src/main/java/shopping/member/client/applicaton/ClientWishService.java b/src/main/java/shopping/member/client/applicaton/ClientWishService.java new file mode 100644 index 0000000..1acaa52 --- /dev/null +++ b/src/main/java/shopping/member/client/applicaton/ClientWishService.java @@ -0,0 +1,36 @@ +package shopping.member.client.applicaton; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shopping.member.client.applicaton.dto.WishProductResponse; +import shopping.member.client.domain.Client; +import shopping.product.application.event.UnWishEvent; +import shopping.product.application.event.WishEvent; + +@Service +@RequiredArgsConstructor +public class ClientWishService { + + private final WishProductMapper wishProductMapper; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public void wish(Long productId, Client client) { + wishProductMapper.validateProduct(productId); + client.wish(productId); + eventPublisher.publishEvent(new WishEvent(productId)); + } + + @Transactional + public void unWish(Long productId, Client client) { + client.unWish(productId); + eventPublisher.publishEvent(new UnWishEvent(productId)); + } + + public List findAll(Client client) { + return wishProductMapper.createResponse(client.productIds()); + } +} diff --git a/src/main/java/shopping/member/client/domain/DuplicateWishProductException.java b/src/main/java/shopping/member/client/exception/DuplicateWishProductException.java similarity index 80% rename from src/main/java/shopping/member/client/domain/DuplicateWishProductException.java rename to src/main/java/shopping/member/client/exception/DuplicateWishProductException.java index a0c1eaf..ce3607e 100644 --- a/src/main/java/shopping/member/client/domain/DuplicateWishProductException.java +++ b/src/main/java/shopping/member/client/exception/DuplicateWishProductException.java @@ -1,4 +1,4 @@ -package shopping.member.client.domain; +package shopping.member.client.exception; public class DuplicateWishProductException extends RuntimeException { diff --git a/src/main/java/shopping/product/application/NotFoundProductException.java b/src/main/java/shopping/product/application/NotFoundProductException.java new file mode 100644 index 0000000..5ebbe2f --- /dev/null +++ b/src/main/java/shopping/product/application/NotFoundProductException.java @@ -0,0 +1,8 @@ +package shopping.product.application; + +public class NotFoundProductException extends RuntimeException { + + public NotFoundProductException(final String message) { + super(message); + } +} diff --git a/src/main/java/shopping/product/application/WishEventHandler.java b/src/main/java/shopping/product/application/WishEventHandler.java new file mode 100644 index 0000000..1270d18 --- /dev/null +++ b/src/main/java/shopping/product/application/WishEventHandler.java @@ -0,0 +1,33 @@ +package shopping.product.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import shopping.product.application.event.UnWishEvent; +import shopping.product.application.event.WishEvent; +import shopping.product.domain.Product; + +@Component +@RequiredArgsConstructor +public class WishEventHandler { + + private final ProductService productService; + + @Async + @EventListener + @Transactional + public void handle(WishEvent wishEvent) { + final Product product = productService.findProduct(wishEvent.productId()); + product.wish(); + } + + @Async + @EventListener + @Transactional + public void handle(UnWishEvent wishEvent) { + final Product product = productService.findProduct(wishEvent.productId()); + product.unWish(); + } +} diff --git a/src/main/java/shopping/product/application/event/UnWishEvent.java b/src/main/java/shopping/product/application/event/UnWishEvent.java new file mode 100644 index 0000000..14b6988 --- /dev/null +++ b/src/main/java/shopping/product/application/event/UnWishEvent.java @@ -0,0 +1,7 @@ +package shopping.product.application.event; + +public record UnWishEvent( + Long productId +) { + +} diff --git a/src/main/java/shopping/product/application/event/WishEvent.java b/src/main/java/shopping/product/application/event/WishEvent.java new file mode 100644 index 0000000..1b6927a --- /dev/null +++ b/src/main/java/shopping/product/application/event/WishEvent.java @@ -0,0 +1,7 @@ +package shopping.product.application.event; + +public record WishEvent( + Long productId +) { + +} diff --git a/src/main/java/shopping/product/infra/JPAProductRepository.java b/src/main/java/shopping/product/infra/JPAProductRepository.java new file mode 100644 index 0000000..4c36364 --- /dev/null +++ b/src/main/java/shopping/product/infra/JPAProductRepository.java @@ -0,0 +1,9 @@ +package shopping.product.infra; + +import org.springframework.data.jpa.repository.JpaRepository; +import shopping.product.domain.Product; +import shopping.product.domain.ProductRepository; + +public interface JPAProductRepository extends ProductRepository, JpaRepository { + +} diff --git a/src/test/java/shopping/fake/FakeProductRepository.java b/src/test/java/shopping/fake/FakeProductRepository.java new file mode 100644 index 0000000..576d1b2 --- /dev/null +++ b/src/test/java/shopping/fake/FakeProductRepository.java @@ -0,0 +1,33 @@ +package shopping.fake; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import shopping.product.domain.Product; +import shopping.product.domain.ProductRepository; + +public class FakeProductRepository implements ProductRepository { + + private final Map products = new HashMap<>(); + + @Override + public Product save(final Product product) { + products.put(product.getId(), product); + return product; + } + + @Override + public Optional findById(final Long id) { + return Optional.ofNullable(products.get(id)); + } + + @Override + public List findByIdIn(final List ids) { + return ids.stream() + .map(products::get) + .filter(Objects::nonNull) + .toList(); + } +} diff --git a/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java b/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java new file mode 100644 index 0000000..31d3eaa --- /dev/null +++ b/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java @@ -0,0 +1,112 @@ +package shopping.member.client.applicaton; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import shopping.fake.FakeProductRepository; +import shopping.fixture.ClientFixture; +import shopping.fixture.ProductFixture; +import shopping.member.client.applicaton.dto.WishProductResponse; +import shopping.member.client.domain.Client; +import shopping.member.client.exception.DuplicateWishProductException; +import shopping.member.client.exception.NotFoundWishProductException; +import shopping.product.application.NotFoundProductException; +import shopping.product.application.ProductService; +import shopping.product.domain.Product; +import shopping.product.domain.ProductRepository; + +@DisplayName("ClientWishService") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ClientWishServiceTest { + + private final ProductRepository productRepository = new FakeProductRepository(); + private ClientWishService clientWishService; + + @BeforeEach + void setUp() { + final ProductService productService = new ProductService(productRepository); + final WishProductMapper wishProductMapper = new WishProductMapper(productService); + clientWishService = new ClientWishService(wishProductMapper, + (event) -> System.out.println("이벤트 발행")); + } + + @Test + void 존재하지_않는_Product의_하트를_누르면_예외를_던진다() { + assertThatThrownBy(() -> clientWishService.wish(1L, createClient())) + .isInstanceOf(NotFoundProductException.class); + } + + @Test + void Product의_하트를_누르면_Client의_위시상품리스트에_추가된다() { + final Client client = createClient(); + final Product product = createProduct(); + + clientWishService.wish(product.getId(), client); + + assertThat(client.productIds()).hasSize(1); + } + + @Test + void 중복되는_Product의_하트를_누르면_예외를_던진다() { + final Client client = createClient(); + final Product product = createProduct(); + clientWishService.wish(product.getId(), client); + + assertThatThrownBy(() -> clientWishService.wish(product.getId(), client)) + .isInstanceOf(DuplicateWishProductException.class); + } + + @Test + void Product의_하트를_취소하면_Client의_위시상품리스트에서_삭제된다() { + final Client client = createClient(); + final Product product = createProduct(); + clientWishService.wish(product.getId(), client); + + clientWishService.unWish(product.getId(), client); + + assertThat(client.productIds()).hasSize(0); + } + + @Test + void 위시상품에_없는_상품의_하트를_취소하면_예외를_던진다() { + final Client client = createClient(); + + assertThatThrownBy(() -> clientWishService.unWish(1L, client)) + .isInstanceOf(NotFoundWishProductException.class); + } + + @Test + void 위시상품리스트에_추가된_상품들을_조회할_수_있다() { + final Client client = createClient(); + final Product product = createProduct(); + clientWishService.wish(product.getId(), client); + + final List responses = clientWishService.findAll(client); + + assertThat(responses).hasSize(1); + } + + @Test + void 위시상품조회_시_추가된_상품이_없다면_빈리스트를_반환한다() { + final Client client = createClient(); + + final List responses = clientWishService.findAll(client); + + assertThat(responses).hasSize(0); + } + + private Client createClient() { + return ClientFixture.createClient(); + } + + private Product createProduct() { + final Product product = ProductFixture.createProduct(); + return productRepository.save(product); + } +} \ No newline at end of file From 5baba47601ad64dc923e8dab302175780f767bd2 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 20:21:29 +0900 Subject: [PATCH 35/56] =?UTF-8?q?test(RestClientTest):=20RestClient?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98=EC=97=AC=20purgomalum?= =?UTF-8?q?=20api=EB=A5=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shopping/study/RestClientTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/test/java/shopping/study/RestClientTest.java diff --git a/src/test/java/shopping/study/RestClientTest.java b/src/test/java/shopping/study/RestClientTest.java new file mode 100644 index 0000000..4ce9aed --- /dev/null +++ b/src/test/java/shopping/study/RestClientTest.java @@ -0,0 +1,36 @@ +package shopping.study; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.web.client.RestClient; + +@Disabled +public class RestClientTest { + + private static final String URL = "https://www.purgomalum.com/service/containsprofanity?text={text}"; + private static final RestClient restClient = RestClient.create(); + + @ParameterizedTest + @CsvSource(value = { + "fuck:true", + "apple:false", + "bitch:true", + "banana:false", + "fucking:true" + }, delimiter = ':') + void RestClient객체를_만들어_Purogomalum에_비속어체크를_요청한다(String text, boolean res) { + final boolean response = isProfanity(text); + assertThat(response).isEqualTo(res); + } + + private boolean isProfanity(final String text) { + final String response = restClient.get() + .uri(URL, text) + .retrieve() + .body(String.class); + return Boolean.parseBoolean(response); + } +} From 28c22b4e04064b21c950081da6e2bb3d8f137741 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 20:22:54 +0900 Subject: [PATCH 36/56] =?UTF-8?q?feat(ProfanityChecker):=20ProfanityChecke?= =?UTF-8?q?r=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RestClient를 사용하여 purgomalum api에 욕설확인 요청을 보낸다 --- .../common/config/RestClientConfig.java | 14 ++++++++++++ .../product/domain/ProfanityChecker.java | 2 +- .../infra/DefaultProfanityChecker.java | 22 +++++++++++++++++++ .../shopping/fake/FakeProfanityChecker.java | 4 ++-- 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/main/java/shopping/common/config/RestClientConfig.java create mode 100644 src/main/java/shopping/product/infra/DefaultProfanityChecker.java diff --git a/src/main/java/shopping/common/config/RestClientConfig.java b/src/main/java/shopping/common/config/RestClientConfig.java new file mode 100644 index 0000000..d93fef5 --- /dev/null +++ b/src/main/java/shopping/common/config/RestClientConfig.java @@ -0,0 +1,14 @@ +package shopping.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient restClient(){ + return RestClient.create(); + } +} diff --git a/src/main/java/shopping/product/domain/ProfanityChecker.java b/src/main/java/shopping/product/domain/ProfanityChecker.java index a7c7820..be0328b 100644 --- a/src/main/java/shopping/product/domain/ProfanityChecker.java +++ b/src/main/java/shopping/product/domain/ProfanityChecker.java @@ -2,5 +2,5 @@ public interface ProfanityChecker { - boolean isProfanity(String word); + boolean isProfanity(String text); } diff --git a/src/main/java/shopping/product/infra/DefaultProfanityChecker.java b/src/main/java/shopping/product/infra/DefaultProfanityChecker.java new file mode 100644 index 0000000..aee3455 --- /dev/null +++ b/src/main/java/shopping/product/infra/DefaultProfanityChecker.java @@ -0,0 +1,22 @@ +package shopping.product.infra; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import shopping.product.domain.ProfanityChecker; + +@Component +@RequiredArgsConstructor +public class DefaultProfanityChecker implements ProfanityChecker { + + private final RestClient restClient; + + @Override + public boolean isProfanity(final String text) { + final String response = restClient.get() + .uri("https://www.purgomalum.com/service/containsprofanity?text={text}", text) + .retrieve() + .body(String.class); + return Boolean.parseBoolean(response); + } +} diff --git a/src/test/java/shopping/fake/FakeProfanityChecker.java b/src/test/java/shopping/fake/FakeProfanityChecker.java index 7af3773..4e17efd 100644 --- a/src/test/java/shopping/fake/FakeProfanityChecker.java +++ b/src/test/java/shopping/fake/FakeProfanityChecker.java @@ -8,8 +8,8 @@ public class FakeProfanityChecker implements ProfanityChecker { private static final List profanities = List.of("비속어", "욕", "메롱"); @Override - public boolean isProfanity(final String word) { + public boolean isProfanity(final String text) { return profanities.stream() - .anyMatch(word::contains); + .anyMatch(text::contains); } } From 53798608ecce7788f7da940d22227dbc7325f2e5 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 20:25:43 +0900 Subject: [PATCH 37/56] =?UTF-8?q?refactor:=20exception=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/SpringShoppingException.java | 8 ++++++++ .../exception/DuplicateWishProductException.java | 4 +++- .../client/exception/NotFoundWishProductException.java | 4 +++- .../member/common/exception/InvalidEmailException.java | 4 +++- .../common/exception/InvalidPasswordException.java | 4 +++- .../common/exception/NotFoundMemberException.java | 4 +++- .../product/application/NotFoundProductException.java | 8 -------- .../shopping/product/application/ProductService.java | 1 + .../product/exception/InvalidProductNameException.java | 4 +++- .../exception/InvalidProductPriceException.java | 4 +++- .../product/exception/NotFoundProductException.java | 10 ++++++++++ .../client/applicaton/ClientWishServiceTest.java | 2 +- 12 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 src/main/java/shopping/common/exception/SpringShoppingException.java delete mode 100644 src/main/java/shopping/product/application/NotFoundProductException.java create mode 100644 src/main/java/shopping/product/exception/NotFoundProductException.java diff --git a/src/main/java/shopping/common/exception/SpringShoppingException.java b/src/main/java/shopping/common/exception/SpringShoppingException.java new file mode 100644 index 0000000..3da76c8 --- /dev/null +++ b/src/main/java/shopping/common/exception/SpringShoppingException.java @@ -0,0 +1,8 @@ +package shopping.common.exception; + +public class SpringShoppingException extends RuntimeException { + + public SpringShoppingException(final String message) { + super(message); + } +} diff --git a/src/main/java/shopping/member/client/exception/DuplicateWishProductException.java b/src/main/java/shopping/member/client/exception/DuplicateWishProductException.java index ce3607e..9f1de5e 100644 --- a/src/main/java/shopping/member/client/exception/DuplicateWishProductException.java +++ b/src/main/java/shopping/member/client/exception/DuplicateWishProductException.java @@ -1,6 +1,8 @@ package shopping.member.client.exception; -public class DuplicateWishProductException extends RuntimeException { +import shopping.common.exception.SpringShoppingException; + +public class DuplicateWishProductException extends SpringShoppingException { public DuplicateWishProductException(final String message) { super(message); diff --git a/src/main/java/shopping/member/client/exception/NotFoundWishProductException.java b/src/main/java/shopping/member/client/exception/NotFoundWishProductException.java index cccb755..aa39dfd 100644 --- a/src/main/java/shopping/member/client/exception/NotFoundWishProductException.java +++ b/src/main/java/shopping/member/client/exception/NotFoundWishProductException.java @@ -1,6 +1,8 @@ package shopping.member.client.exception; -public class NotFoundWishProductException extends RuntimeException{ +import shopping.common.exception.SpringShoppingException; + +public class NotFoundWishProductException extends SpringShoppingException { public NotFoundWishProductException(final String message) { super(message); diff --git a/src/main/java/shopping/member/common/exception/InvalidEmailException.java b/src/main/java/shopping/member/common/exception/InvalidEmailException.java index 75ff5ad..8bf4524 100644 --- a/src/main/java/shopping/member/common/exception/InvalidEmailException.java +++ b/src/main/java/shopping/member/common/exception/InvalidEmailException.java @@ -1,6 +1,8 @@ package shopping.member.common.exception; -public class InvalidEmailException extends RuntimeException { +import shopping.common.exception.SpringShoppingException; + +public class InvalidEmailException extends SpringShoppingException { public InvalidEmailException(final String message) { super(message); diff --git a/src/main/java/shopping/member/common/exception/InvalidPasswordException.java b/src/main/java/shopping/member/common/exception/InvalidPasswordException.java index 13d47e7..216235a 100644 --- a/src/main/java/shopping/member/common/exception/InvalidPasswordException.java +++ b/src/main/java/shopping/member/common/exception/InvalidPasswordException.java @@ -1,6 +1,8 @@ package shopping.member.common.exception; -public class InvalidPasswordException extends RuntimeException { +import shopping.common.exception.SpringShoppingException; + +public class InvalidPasswordException extends SpringShoppingException { public InvalidPasswordException(final String message) { super(message); diff --git a/src/main/java/shopping/member/common/exception/NotFoundMemberException.java b/src/main/java/shopping/member/common/exception/NotFoundMemberException.java index a1586f0..64ec7dd 100644 --- a/src/main/java/shopping/member/common/exception/NotFoundMemberException.java +++ b/src/main/java/shopping/member/common/exception/NotFoundMemberException.java @@ -1,6 +1,8 @@ package shopping.member.common.exception; -public class NotFoundMemberException extends RuntimeException { +import shopping.common.exception.SpringShoppingException; + +public class NotFoundMemberException extends SpringShoppingException { public NotFoundMemberException(final String message) { super(message); diff --git a/src/main/java/shopping/product/application/NotFoundProductException.java b/src/main/java/shopping/product/application/NotFoundProductException.java deleted file mode 100644 index 5ebbe2f..0000000 --- a/src/main/java/shopping/product/application/NotFoundProductException.java +++ /dev/null @@ -1,8 +0,0 @@ -package shopping.product.application; - -public class NotFoundProductException extends RuntimeException { - - public NotFoundProductException(final String message) { - super(message); - } -} diff --git a/src/main/java/shopping/product/application/ProductService.java b/src/main/java/shopping/product/application/ProductService.java index 917c914..809afe0 100644 --- a/src/main/java/shopping/product/application/ProductService.java +++ b/src/main/java/shopping/product/application/ProductService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import shopping.product.domain.Product; import shopping.product.domain.ProductRepository; +import shopping.product.exception.NotFoundProductException; @Service @RequiredArgsConstructor diff --git a/src/main/java/shopping/product/exception/InvalidProductNameException.java b/src/main/java/shopping/product/exception/InvalidProductNameException.java index 74d8930..bf3449f 100644 --- a/src/main/java/shopping/product/exception/InvalidProductNameException.java +++ b/src/main/java/shopping/product/exception/InvalidProductNameException.java @@ -1,6 +1,8 @@ package shopping.product.exception; -public class InvalidProductNameException extends IllegalStateException { +import shopping.common.exception.SpringShoppingException; + +public class InvalidProductNameException extends SpringShoppingException { public InvalidProductNameException(String s) { super(s); diff --git a/src/main/java/shopping/product/exception/InvalidProductPriceException.java b/src/main/java/shopping/product/exception/InvalidProductPriceException.java index 15ed714..e7d6a1a 100644 --- a/src/main/java/shopping/product/exception/InvalidProductPriceException.java +++ b/src/main/java/shopping/product/exception/InvalidProductPriceException.java @@ -1,6 +1,8 @@ package shopping.product.exception; -public class InvalidProductPriceException extends IllegalArgumentException { +import shopping.common.exception.SpringShoppingException; + +public class InvalidProductPriceException extends SpringShoppingException { public InvalidProductPriceException(final String s) { super(s); diff --git a/src/main/java/shopping/product/exception/NotFoundProductException.java b/src/main/java/shopping/product/exception/NotFoundProductException.java new file mode 100644 index 0000000..69071ed --- /dev/null +++ b/src/main/java/shopping/product/exception/NotFoundProductException.java @@ -0,0 +1,10 @@ +package shopping.product.exception; + +import shopping.common.exception.SpringShoppingException; + +public class NotFoundProductException extends SpringShoppingException { + + public NotFoundProductException(final String message) { + super(message); + } +} diff --git a/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java b/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java index 31d3eaa..7511bb6 100644 --- a/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java +++ b/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java @@ -16,7 +16,7 @@ import shopping.member.client.domain.Client; import shopping.member.client.exception.DuplicateWishProductException; import shopping.member.client.exception.NotFoundWishProductException; -import shopping.product.application.NotFoundProductException; +import shopping.product.exception.NotFoundProductException; import shopping.product.application.ProductService; import shopping.product.domain.Product; import shopping.product.domain.ProductRepository; From 77194087634fc099563789e235020928bbe913d3 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 28 Jul 2024 20:54:57 +0900 Subject: [PATCH 38/56] =?UTF-8?q?refactor(ProductService):=20Product=20CUD?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/application/ProductService.java | 29 ++++++++++++++++++- .../application/dto/ProductCreateRequest.java | 13 +++++++++ .../application/dto/ProductUpdateRequest.java | 16 ++++++++++ .../java/shopping/product/domain/Product.java | 7 +++++ .../product/domain/ProductRepository.java | 2 ++ .../shopping/fake/FakeProductRepository.java | 5 ++++ .../applicaton/ClientWishServiceTest.java | 7 +++-- 7 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/main/java/shopping/product/application/dto/ProductCreateRequest.java create mode 100644 src/main/java/shopping/product/application/dto/ProductUpdateRequest.java diff --git a/src/main/java/shopping/product/application/ProductService.java b/src/main/java/shopping/product/application/ProductService.java index 809afe0..561ce9d 100644 --- a/src/main/java/shopping/product/application/ProductService.java +++ b/src/main/java/shopping/product/application/ProductService.java @@ -3,22 +3,49 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import shopping.product.application.dto.ProductCreateRequest; +import shopping.product.application.dto.ProductUpdateRequest; import shopping.product.domain.Product; import shopping.product.domain.ProductRepository; +import shopping.product.domain.ProfanityChecker; import shopping.product.exception.NotFoundProductException; @Service @RequiredArgsConstructor public class ProductService { + private final ProfanityChecker profanityChecker; private final ProductRepository productRepository; + public Long create(ProductCreateRequest request) { + final Product product = new Product(request.name(), + profanityChecker, + request.price(), + request.image()); + final Product saved = productRepository.save(product); + return saved.getId(); + } + + public void update(ProductUpdateRequest request) { + final Product product = findProduct(request.id()); + product.update(request.name(), + profanityChecker, + request.price(), + request.image()); + productRepository.save(product); + } + + public void delete(Long id) { + final Product product = findProduct(id); + productRepository.delete(product); + } + public Product findProduct(Long id) { return productRepository.findById(id) .orElseThrow(() -> new NotFoundProductException("해당 상품을 찾을 수 없습니다. " + id)); } - public List findProducts(List ids){ + public List findProducts(List ids) { return productRepository.findByIdIn(ids); } } diff --git a/src/main/java/shopping/product/application/dto/ProductCreateRequest.java b/src/main/java/shopping/product/application/dto/ProductCreateRequest.java new file mode 100644 index 0000000..222ba78 --- /dev/null +++ b/src/main/java/shopping/product/application/dto/ProductCreateRequest.java @@ -0,0 +1,13 @@ +package shopping.product.application.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ProductCreateRequest( + @NotBlank + String name, + @NotBlank + Long price, + String image +) { + +} diff --git a/src/main/java/shopping/product/application/dto/ProductUpdateRequest.java b/src/main/java/shopping/product/application/dto/ProductUpdateRequest.java new file mode 100644 index 0000000..f242e5f --- /dev/null +++ b/src/main/java/shopping/product/application/dto/ProductUpdateRequest.java @@ -0,0 +1,16 @@ +package shopping.product.application.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ProductUpdateRequest( + @NotNull + Long id, + @NotBlank + String name, + @NotBlank + Long price, + String image +) { + +} diff --git a/src/main/java/shopping/product/domain/Product.java b/src/main/java/shopping/product/domain/Product.java index 2d46d45..6dc8554 100644 --- a/src/main/java/shopping/product/domain/Product.java +++ b/src/main/java/shopping/product/domain/Product.java @@ -48,6 +48,13 @@ public Product(final Long id, final String name, final ProfanityChecker profanit this.heartCount = 0L; } + public void update(final String name, final ProfanityChecker profanityChecker, final Long price, + final String image) { + this.name = new ProductName(name, profanityChecker); + this.price = new ProductPrice(price); + this.image = image; + } + public void wish() { this.heartCount++; } diff --git a/src/main/java/shopping/product/domain/ProductRepository.java b/src/main/java/shopping/product/domain/ProductRepository.java index 7a851fe..a47f56c 100644 --- a/src/main/java/shopping/product/domain/ProductRepository.java +++ b/src/main/java/shopping/product/domain/ProductRepository.java @@ -7,6 +7,8 @@ public interface ProductRepository { Product save(Product product); + void delete(Product product); + Optional findById(Long id); List findByIdIn(List ids); diff --git a/src/test/java/shopping/fake/FakeProductRepository.java b/src/test/java/shopping/fake/FakeProductRepository.java index 576d1b2..766ca15 100644 --- a/src/test/java/shopping/fake/FakeProductRepository.java +++ b/src/test/java/shopping/fake/FakeProductRepository.java @@ -18,6 +18,11 @@ public Product save(final Product product) { return product; } + @Override + public void delete(final Product product) { + products.remove(product.getId()); + } + @Override public Optional findById(final Long id) { return Optional.ofNullable(products.get(id)); diff --git a/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java b/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java index 7511bb6..25b668d 100644 --- a/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java +++ b/src/test/java/shopping/member/client/applicaton/ClientWishServiceTest.java @@ -10,16 +10,17 @@ import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import shopping.fake.FakeProductRepository; +import shopping.fake.FakeProfanityChecker; import shopping.fixture.ClientFixture; import shopping.fixture.ProductFixture; import shopping.member.client.applicaton.dto.WishProductResponse; import shopping.member.client.domain.Client; import shopping.member.client.exception.DuplicateWishProductException; import shopping.member.client.exception.NotFoundWishProductException; -import shopping.product.exception.NotFoundProductException; import shopping.product.application.ProductService; import shopping.product.domain.Product; import shopping.product.domain.ProductRepository; +import shopping.product.exception.NotFoundProductException; @DisplayName("ClientWishService") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @@ -30,7 +31,9 @@ class ClientWishServiceTest { @BeforeEach void setUp() { - final ProductService productService = new ProductService(productRepository); + final ProductService productService = new ProductService( + new FakeProfanityChecker(), + productRepository); final WishProductMapper wishProductMapper = new WishProductMapper(productService); clientWishService = new ClientWishService(wishProductMapper, (event) -> System.out.println("이벤트 발행")); From 9ddb97bb134637389d1f109bac6cac8251cd4666 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 16:12:32 +0900 Subject: [PATCH 39/56] =?UTF-8?q?refactor(Member):=20role=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=96=BB=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shopping/member/common/domain/Member.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/shopping/member/common/domain/Member.java b/src/main/java/shopping/member/common/domain/Member.java index 5244020..58679f2 100644 --- a/src/main/java/shopping/member/common/domain/Member.java +++ b/src/main/java/shopping/member/common/domain/Member.java @@ -10,6 +10,9 @@ import jakarta.persistence.Table; import lombok.Getter; import shopping.common.domain.BaseEntity; +import shopping.member.client.domain.Client; +import shopping.member.common.exception.InvalidMemberException; +import shopping.member.owner.domain.Owner; @Entity @Table(name = "members") @@ -41,4 +44,14 @@ public boolean isValidPassword(final String rawPassword, final PasswordEncoder passwordEncoder) { return password.isMatch(rawPassword, passwordEncoder); } + + public String getRole() { + if (this instanceof Client) { + return "Client"; + } + if (this instanceof Owner) { + return "Owner"; + } + throw new InvalidMemberException("유효하지 않은 회원입니다."); + } } From a52a72e7431599142c05f4c529c73fc121000c24 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 16:12:57 +0900 Subject: [PATCH 40/56] =?UTF-8?q?refactor(AuthService):=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=ED=86=A0=ED=81=B0=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=B4=EC=84=9C=20=EB=84=98=EA=B2=A8?= =?UTF-8?q?=EC=A4=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/auth/token/TokenGenerator.java | 7 +++++++ .../client/applicaton/ClientLoginService.java | 6 ++++-- .../applicaton/dto/ClientLoginResponse.java | 5 +++++ .../member/common/application/AuthService.java | 18 ++++++++++-------- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 src/main/java/shopping/common/auth/token/TokenGenerator.java create mode 100644 src/main/java/shopping/member/client/applicaton/dto/ClientLoginResponse.java diff --git a/src/main/java/shopping/common/auth/token/TokenGenerator.java b/src/main/java/shopping/common/auth/token/TokenGenerator.java new file mode 100644 index 0000000..287fa61 --- /dev/null +++ b/src/main/java/shopping/common/auth/token/TokenGenerator.java @@ -0,0 +1,7 @@ +package shopping.common.auth.token; + +@FunctionalInterface +public interface TokenGenerator { + + String generate(String email, String role); +} diff --git a/src/main/java/shopping/member/client/applicaton/ClientLoginService.java b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java index faf230c..09486a0 100644 --- a/src/main/java/shopping/member/client/applicaton/ClientLoginService.java +++ b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import shopping.member.client.applicaton.dto.ClientCreateRequest; import shopping.member.client.applicaton.dto.ClientLoginRequest; +import shopping.member.client.applicaton.dto.ClientLoginResponse; import shopping.member.client.domain.Client; import shopping.member.client.domain.ClientRepository; import shopping.member.common.application.AuthService; @@ -28,7 +29,8 @@ public void signUp(final ClientCreateRequest request) { clientRepository.save(client); } - public String login(final ClientLoginRequest request) { - return authService.login(request.email(), request.password()); + public ClientLoginResponse login(final ClientLoginRequest request) { + final String accessToken = authService.login(request.email(), request.password()); + return new ClientLoginResponse(accessToken); } } diff --git a/src/main/java/shopping/member/client/applicaton/dto/ClientLoginResponse.java b/src/main/java/shopping/member/client/applicaton/dto/ClientLoginResponse.java new file mode 100644 index 0000000..c26e9df --- /dev/null +++ b/src/main/java/shopping/member/client/applicaton/dto/ClientLoginResponse.java @@ -0,0 +1,5 @@ +package shopping.member.client.applicaton.dto; + +public record ClientLoginResponse(String accessToken) { + +} diff --git a/src/main/java/shopping/member/common/application/AuthService.java b/src/main/java/shopping/member/common/application/AuthService.java index d77b7d1..0c5a19c 100644 --- a/src/main/java/shopping/member/common/application/AuthService.java +++ b/src/main/java/shopping/member/common/application/AuthService.java @@ -3,13 +3,14 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import shopping.member.common.exception.InvalidEmailException; +import shopping.common.auth.token.TokenGenerator; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberRepository; import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; +import shopping.member.common.exception.InvalidEmailException; import shopping.member.common.exception.InvalidPasswordException; import shopping.member.common.exception.NotFoundMemberException; -import shopping.member.common.domain.Member; -import shopping.member.common.domain.MemberRepository; @Service @RequiredArgsConstructor @@ -17,6 +18,7 @@ public class AuthService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + private final TokenGenerator tokenGenerator; public void validateEmail(final String email) { final Optional existMember = memberRepository.findByEmail(email); @@ -25,20 +27,20 @@ public void validateEmail(final String email) { } } - public Password encodePassword(final String rawPassword){ + public Password encodePassword(final String rawPassword) { return new Password(rawPassword, passwordEncoder); } - public String login(final String email, final String rawPassword){ + public String login(final String email, final String rawPassword) { final Member member = findMember(email); - if(!member.isValidPassword(rawPassword, passwordEncoder)){ + if (!member.isValidPassword(rawPassword, passwordEncoder)) { throw new InvalidPasswordException("비밀번호가 틀렸습니다."); } - return ""; // 토큰넘겨주기 + return tokenGenerator.generate(member.getEmail(), member.getRole()); } - private Member findMember(String email){ + private Member findMember(String email) { return memberRepository.findByEmail(email) .orElseThrow(() -> new NotFoundMemberException("찾을 수 없는 사용자입니다. " + email)); } From 73d5c5f3ab1557ce2cd0182e45b87b97d19ce5f7 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 16:13:40 +0900 Subject: [PATCH 41/56] =?UTF-8?q?refactor(JwtGenerator):=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/auth/token/JwtGenerator.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/shopping/common/auth/token/JwtGenerator.java diff --git a/src/main/java/shopping/common/auth/token/JwtGenerator.java b/src/main/java/shopping/common/auth/token/JwtGenerator.java new file mode 100644 index 0000000..98ba319 --- /dev/null +++ b/src/main/java/shopping/common/auth/token/JwtGenerator.java @@ -0,0 +1,35 @@ +package shopping.common.auth.token; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.util.Date; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtGenerator implements TokenGenerator { + + private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60; + private final Key key; + + public JwtGenerator(@Value("${jwt.secret}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public String generate(String email, String role) { + + long now = (new Date()).getTime(); + + // Access Token 생성 + return Jwts.builder() + .setSubject(email) + .claim("auth", role) + .setExpiration(new Date(now + ACCESS_TOKEN_EXPIRE_TIME)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } +} From 432d67b5ed3d9cb0fba0b46ec20845db85ece631 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 16:14:32 +0900 Subject: [PATCH 42/56] =?UTF-8?q?feat(JwtProvider):=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=84=20=EA=B2=80=EC=A6=9D=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/auth/token/JwtProvider.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/main/java/shopping/common/auth/token/JwtProvider.java diff --git a/src/main/java/shopping/common/auth/token/JwtProvider.java b/src/main/java/shopping/common/auth/token/JwtProvider.java new file mode 100644 index 0000000..63ea4aa --- /dev/null +++ b/src/main/java/shopping/common/auth/token/JwtProvider.java @@ -0,0 +1,80 @@ +package shopping.common.auth.token; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class JwtProvider { + + private final Key key; + + public JwtProvider(@Value("${jwt.secret}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public Authentication getAuthentication(String accessToken) { + // 토큰 복호화 + Claims claims = parseClaims(accessToken); + + if (claims.get("auth") == null) { + throw new RuntimeException("권한 정보가 없는 토큰입니다."); + } + + // 클레임에서 권한 정보 가져오기 + Collection authorities = Arrays.stream( + claims.get("auth").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .toList(); + + // UserDetails 객체를 만들어서 Authentication 리턴 + UserDetails userDetails = new User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + } + return false; + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} From f33f0270f7d1bd0cdf2a9df342f9fb1c9702a20b Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 16:16:43 +0900 Subject: [PATCH 43/56] =?UTF-8?q?feat(JwtAuthenticationFilter):=20JWT=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=84=20=EC=B6=94=EC=B6=9C=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=9C=A0=ED=9A=A8=EC=84=B1=EC=9D=84=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=ED=95=98=EB=8A=94=20=ED=95=84=ED=84=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/filter/JwtAuthenticationFilter.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/main/java/shopping/common/auth/filter/JwtAuthenticationFilter.java diff --git a/src/main/java/shopping/common/auth/filter/JwtAuthenticationFilter.java b/src/main/java/shopping/common/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..d6340dc --- /dev/null +++ b/src/main/java/shopping/common/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,48 @@ +package shopping.common.auth.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import shopping.common.auth.token.JwtProvider; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String AUTHORIZATION_TYPE = "Bearer"; + + private final JwtProvider jwtTokenProvider; + + @Override + public void doFilterInternal(final HttpServletRequest request, + final HttpServletResponse response, + final FilterChain filterChain) throws IOException, ServletException { + // 1. Request Header에서 JWT 토큰 추출 + String token = resolveToken(request); + String requestURI = request.getRequestURI(); + + // 2. validateToken으로 토큰 유효성 검사 + if (token != null && jwtTokenProvider.validateToken(token)) { + // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장 + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + logger.debug("Security Context에 인증 정보를 저장했습니다. uri: " + requestURI); + } + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(AUTHORIZATION_TYPE)) { + return bearerToken.substring(7); + } + return null; + } +} From 7e57b7b8752c289a01bd9360104a1f8920013713 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 16:20:20 +0900 Subject: [PATCH 44/56] =?UTF-8?q?refactor(Member):=20role=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EC=B6=9C=20=EC=8B=9C=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9C=BC=EB=A9=B4=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EB=A5=BC=20=EB=8D=98=EC=A7=84=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/InvalidMemberException.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/shopping/member/common/exception/InvalidMemberException.java diff --git a/src/main/java/shopping/member/common/exception/InvalidMemberException.java b/src/main/java/shopping/member/common/exception/InvalidMemberException.java new file mode 100644 index 0000000..2eb7fec --- /dev/null +++ b/src/main/java/shopping/member/common/exception/InvalidMemberException.java @@ -0,0 +1,10 @@ +package shopping.member.common.exception; + +import shopping.common.exception.SpringShoppingException; + +public class InvalidMemberException extends SpringShoppingException { + + public InvalidMemberException(final String message) { + super(message); + } +} From 69e5ecfa76407426f7a15009fcfa7971dbb0dfeb Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 17:05:05 +0900 Subject: [PATCH 45/56] =?UTF-8?q?refactor(AuthService):=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/client/applicaton/ClientLoginService.java | 2 +- .../shopping/member/common/application/AuthService.java | 6 +++++- src/main/java/shopping/member/common/domain/Member.java | 6 +++++- .../member/owner/application/OwnerLoginService.java | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/shopping/member/client/applicaton/ClientLoginService.java b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java index 09486a0..e479cda 100644 --- a/src/main/java/shopping/member/client/applicaton/ClientLoginService.java +++ b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java @@ -30,7 +30,7 @@ public void signUp(final ClientCreateRequest request) { } public ClientLoginResponse login(final ClientLoginRequest request) { - final String accessToken = authService.login(request.email(), request.password()); + final String accessToken = authService.login(request.email(), request.password(), "Client"); return new ClientLoginResponse(accessToken); } } diff --git a/src/main/java/shopping/member/common/application/AuthService.java b/src/main/java/shopping/member/common/application/AuthService.java index 0c5a19c..6795484 100644 --- a/src/main/java/shopping/member/common/application/AuthService.java +++ b/src/main/java/shopping/member/common/application/AuthService.java @@ -9,6 +9,7 @@ import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; import shopping.member.common.exception.InvalidEmailException; +import shopping.member.common.exception.InvalidMemberException; import shopping.member.common.exception.InvalidPasswordException; import shopping.member.common.exception.NotFoundMemberException; @@ -31,11 +32,14 @@ public Password encodePassword(final String rawPassword) { return new Password(rawPassword, passwordEncoder); } - public String login(final String email, final String rawPassword) { + public String login(final String email, final String rawPassword, final String role) { final Member member = findMember(email); if (!member.isValidPassword(rawPassword, passwordEncoder)) { throw new InvalidPasswordException("비밀번호가 틀렸습니다."); } + if(!member.isValidRole(role)){ + throw new InvalidMemberException("권한이 없는 회원입니다."); + } return tokenGenerator.generate(member.getEmail(), member.getRole()); } diff --git a/src/main/java/shopping/member/common/domain/Member.java b/src/main/java/shopping/member/common/domain/Member.java index 58679f2..feb2b2e 100644 --- a/src/main/java/shopping/member/common/domain/Member.java +++ b/src/main/java/shopping/member/common/domain/Member.java @@ -52,6 +52,10 @@ public String getRole() { if (this instanceof Owner) { return "Owner"; } - throw new InvalidMemberException("유효하지 않은 회원입니다."); + return null; + } + + public boolean isValidRole(String role){ + return role.equals(getRole()); } } diff --git a/src/main/java/shopping/member/owner/application/OwnerLoginService.java b/src/main/java/shopping/member/owner/application/OwnerLoginService.java index 999c1a8..49bc2e5 100644 --- a/src/main/java/shopping/member/owner/application/OwnerLoginService.java +++ b/src/main/java/shopping/member/owner/application/OwnerLoginService.java @@ -29,6 +29,6 @@ public void signUp(final OwnerCreateRequest request) { } public String login(final OwnerLoginRequest request) { - return authService.login(request.email(), request.password()); + return authService.login(request.email(), request.password(), "Owner"); } } From bf4b266eaf79a649fc37c9c107f9e73f6f440618 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sat, 10 Aug 2024 17:34:31 +0900 Subject: [PATCH 46/56] =?UTF-8?q?test(AuthService):=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicaton/ClientLoginServiceTest.java | 8 ++-- .../common/application/AuthServiceTest.java | 23 +++++++--- .../member/common/domain/MemberTest.java | 43 +++++++++++++++++++ .../application/OwnerLoginServiceTest.java | 6 ++- 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 src/test/java/shopping/member/common/domain/MemberTest.java diff --git a/src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java b/src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java index a533777..bc4f5aa 100644 --- a/src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java +++ b/src/test/java/shopping/member/client/applicaton/ClientLoginServiceTest.java @@ -8,9 +8,9 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import shopping.fake.FakePasswordEncoder; import shopping.fake.FakeClientRepository; import shopping.fake.FakeMemberRepository; +import shopping.fake.FakePasswordEncoder; import shopping.fake.InMemoryMembers; import shopping.member.client.applicaton.dto.ClientCreateRequest; import shopping.member.client.domain.ClientRepository; @@ -28,8 +28,10 @@ class ClientLoginServiceTest { void setUp() { final InMemoryMembers inMemoryMembers = new InMemoryMembers(); final MemberRepository memberRepository = new FakeMemberRepository(inMemoryMembers); - final AuthService authService = new AuthService(memberRepository, - new FakePasswordEncoder()); + final AuthService authService = new AuthService( + memberRepository, + new FakePasswordEncoder(), + (email, token) -> email + " " + token); final ClientRepository clientRepository = new FakeClientRepository(inMemoryMembers); clientService = new ClientLoginService(authService, clientRepository); } diff --git a/src/test/java/shopping/member/common/application/AuthServiceTest.java b/src/test/java/shopping/member/common/application/AuthServiceTest.java index fc28aeb..36f229d 100644 --- a/src/test/java/shopping/member/common/application/AuthServiceTest.java +++ b/src/test/java/shopping/member/common/application/AuthServiceTest.java @@ -10,14 +10,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import shopping.fake.FakePasswordEncoder; import shopping.fake.FakeMemberRepository; +import shopping.fake.FakePasswordEncoder; import shopping.fake.InMemoryMembers; import shopping.member.client.domain.Client; import shopping.member.common.domain.MemberRepository; import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; import shopping.member.common.exception.InvalidEmailException; +import shopping.member.common.exception.InvalidMemberException; import shopping.member.common.exception.InvalidPasswordException; import shopping.member.common.exception.NotFoundMemberException; @@ -32,7 +33,11 @@ class AuthServiceTest { @BeforeEach void setUp() { final MemberRepository memberRepository = new FakeMemberRepository(inMemoryMembers); - authService = new AuthService(memberRepository, passwordEncoder); + authService = new AuthService( + memberRepository, + passwordEncoder, + (email, role) -> email + " " + email + ); } @Test @@ -55,12 +60,12 @@ void setUp() { void 로그인을_수행한다() { saveMember(); - assertThat(authService.login("test@test.com", "1234")).isNotNull(); + assertThat(authService.login("test@test.com", "1234", "Client")).isNotNull(); } @Test void 가입되지않은_멤버의_로그인을_수행하면_예외를_던진다() { - assertThatThrownBy(() -> authService.login("test@test.com", "1234")) + assertThatThrownBy(() -> authService.login("test@test.com", "1234", "Client")) .isInstanceOf(NotFoundMemberException.class); } @@ -68,10 +73,18 @@ void setUp() { void 로그인수행_시_비밀번호를_틀리면_예외를_던진다() { saveMember(); - assertThatThrownBy(() -> authService.login("test@test.com", "1111")) + assertThatThrownBy(() -> authService.login("test@test.com", "1111", "Client")) .isInstanceOf(InvalidPasswordException.class); } + @Test + void 로그인수행_시_권한이_없다면_예외를_던진다() { + saveMember(); + + assertThatThrownBy(() -> authService.login("test@test.com", "1234", "Owner")) + .isInstanceOf(InvalidMemberException.class); + } + private void saveMember() { final Password password = new Password("1234", passwordEncoder); inMemoryMembers.save(new Client("test@test.com", password, "test")); diff --git a/src/test/java/shopping/member/common/domain/MemberTest.java b/src/test/java/shopping/member/common/domain/MemberTest.java new file mode 100644 index 0000000..b23302e --- /dev/null +++ b/src/test/java/shopping/member/common/domain/MemberTest.java @@ -0,0 +1,43 @@ +package shopping.member.common.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import shopping.fake.FakePasswordEncoder; +import shopping.member.client.domain.Client; + +@DisplayName("Member") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class MemberTest { + + private final PasswordEncoder passwordEncoder = new FakePasswordEncoder(); + + @Test + void 올바른_Password인지_확인할_수_있다() { + final Member member = createMember(); + + assertThat(member.isValidPassword("1111", passwordEncoder)).isFalse(); + } + + @Test + void Member가_어떤_권한을_가졌는지_얻을_수_있다() { + final Member member = createMember(); + + assertThat(member.getRole()).isEqualTo("Client"); + } + + @Test + void Member가_유효한_권한을_가졌는지_확인할_수_있다() { + final Member member = createMember(); + + assertThat(member.isValidRole("Client")).isTrue(); + } + + private Member createMember() { + final Password password = new Password("1234", passwordEncoder); + return new Client("test@test.com", password, "test"); + } +} \ No newline at end of file diff --git a/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java b/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java index 1a39c24..0dc2427 100644 --- a/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java +++ b/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java @@ -28,8 +28,10 @@ class OwnerLoginServiceTest { void setUp() { final InMemoryMembers inMemoryMembers = new InMemoryMembers(); final MemberRepository memberRepository = new FakeMemberRepository(inMemoryMembers); - final AuthService authService = new AuthService(memberRepository, - new FakePasswordEncoder()); + final AuthService authService = new AuthService( + memberRepository, + new FakePasswordEncoder(), + (email, token) -> email+" "+token); final OwnerRepository ownerRepository = new FakeOwnerRepository(inMemoryMembers); ownerService = new OwnerLoginService(authService, ownerRepository); } From 9caf8e3d07ef299ca38082c497cf18f80e1d72c5 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:14:32 +0900 Subject: [PATCH 47/56] =?UTF-8?q?refactor(AuthService):=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20Enum=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/applicaton/ClientLoginService.java | 4 ++- .../shopping/member/client/domain/Client.java | 3 +- .../common/application/AuthService.java | 21 +++++++++----- .../shopping/member/common/domain/Member.java | 29 +++++++++---------- .../{MemberPriority.java => MemberRole.java} | 2 +- .../owner/application/OwnerLoginService.java | 8 +++-- .../application/dto/OwnerLoginResponse.java | 5 ++++ .../shopping/member/owner/domain/Owner.java | 3 +- src/main/resources/application.yml | 2 ++ .../common/application/AuthServiceTest.java | 9 +++--- .../member/common/domain/MemberTest.java | 4 +-- 11 files changed, 55 insertions(+), 35 deletions(-) rename src/main/java/shopping/member/common/domain/{MemberPriority.java => MemberRole.java} (67%) create mode 100644 src/main/java/shopping/member/owner/application/dto/OwnerLoginResponse.java diff --git a/src/main/java/shopping/member/client/applicaton/ClientLoginService.java b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java index e479cda..5aab2c5 100644 --- a/src/main/java/shopping/member/client/applicaton/ClientLoginService.java +++ b/src/main/java/shopping/member/client/applicaton/ClientLoginService.java @@ -8,6 +8,7 @@ import shopping.member.client.domain.Client; import shopping.member.client.domain.ClientRepository; import shopping.member.common.application.AuthService; +import shopping.member.common.domain.MemberRole; import shopping.member.common.domain.Password; @Service @@ -30,7 +31,8 @@ public void signUp(final ClientCreateRequest request) { } public ClientLoginResponse login(final ClientLoginRequest request) { - final String accessToken = authService.login(request.email(), request.password(), "Client"); + final String accessToken = authService.login(request.email(), request.password(), + MemberRole.CLIENT); return new ClientLoginResponse(accessToken); } } diff --git a/src/main/java/shopping/member/client/domain/Client.java b/src/main/java/shopping/member/client/domain/Client.java index 513595d..3301402 100644 --- a/src/main/java/shopping/member/client/domain/Client.java +++ b/src/main/java/shopping/member/client/domain/Client.java @@ -6,6 +6,7 @@ import lombok.Getter; import shopping.member.common.domain.Member; import shopping.member.common.domain.Password; +import shopping.member.common.domain.MemberRole; @Entity public class Client extends Member { @@ -18,7 +19,7 @@ protected Client() { } public Client(final String email, final Password password, final String memberName) { - super(email, password, memberName); + super(email, password, memberName, MemberRole.CLIENT); } public void wish(final Long productId) { diff --git a/src/main/java/shopping/member/common/application/AuthService.java b/src/main/java/shopping/member/common/application/AuthService.java index 6795484..3745e4b 100644 --- a/src/main/java/shopping/member/common/application/AuthService.java +++ b/src/main/java/shopping/member/common/application/AuthService.java @@ -8,6 +8,7 @@ import shopping.member.common.domain.MemberRepository; import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; +import shopping.member.common.domain.MemberRole; import shopping.member.common.exception.InvalidEmailException; import shopping.member.common.exception.InvalidMemberException; import shopping.member.common.exception.InvalidPasswordException; @@ -32,20 +33,24 @@ public Password encodePassword(final String rawPassword) { return new Password(rawPassword, passwordEncoder); } - public String login(final String email, final String rawPassword, final String role) { - final Member member = findMember(email); + public String login(final String email, final String rawPassword, final MemberRole role) { + final Member member = findMember(email, role); + if (!member.isValidPassword(rawPassword, passwordEncoder)) { throw new InvalidPasswordException("비밀번호가 틀렸습니다."); } - if(!member.isValidRole(role)){ - throw new InvalidMemberException("권한이 없는 회원입니다."); - } - return tokenGenerator.generate(member.getEmail(), member.getRole()); + return tokenGenerator.generate(member.getEmail(), member.getMemberRole()); } - private Member findMember(String email) { - return memberRepository.findByEmail(email) + public Member findMember(String email, MemberRole role) { + final Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new NotFoundMemberException("찾을 수 없는 사용자입니다. " + email)); + + if (!member.isValidRole(role)) { + throw new InvalidMemberException("권한이 없는 회원입니다."); + } + + return member; } } diff --git a/src/main/java/shopping/member/common/domain/Member.java b/src/main/java/shopping/member/common/domain/Member.java index feb2b2e..ac3dfc6 100644 --- a/src/main/java/shopping/member/common/domain/Member.java +++ b/src/main/java/shopping/member/common/domain/Member.java @@ -4,19 +4,18 @@ import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; import jakarta.persistence.Table; import lombok.Getter; import shopping.common.domain.BaseEntity; -import shopping.member.client.domain.Client; -import shopping.member.common.exception.InvalidMemberException; -import shopping.member.owner.domain.Owner; @Entity @Table(name = "members") -@DiscriminatorColumn +@DiscriminatorColumn(name = "member_role") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public abstract class Member extends BaseEntity { @@ -31,13 +30,19 @@ public abstract class Member extends BaseEntity { @Column(name = "member_name", nullable = false) private String memberName; + @Column(name = "member_role", insertable = false, updatable = false) + @Enumerated(EnumType.STRING) + private MemberRole memberRole; + protected Member() { } - protected Member(final String email, final Password password, final String memberName) { + protected Member(final String email, final Password password, final String memberName, + final MemberRole role) { this.email = email; this.password = password; this.memberName = memberName; + this.memberRole = role; } public boolean isValidPassword(final String rawPassword, @@ -45,17 +50,11 @@ public boolean isValidPassword(final String rawPassword, return password.isMatch(rawPassword, passwordEncoder); } - public String getRole() { - if (this instanceof Client) { - return "Client"; - } - if (this instanceof Owner) { - return "Owner"; - } - return null; + public String getMemberRole() { + return memberRole.name(); } - public boolean isValidRole(String role){ - return role.equals(getRole()); + public boolean isValidRole(MemberRole role) { + return this.memberRole.equals(role); } } diff --git a/src/main/java/shopping/member/common/domain/MemberPriority.java b/src/main/java/shopping/member/common/domain/MemberRole.java similarity index 67% rename from src/main/java/shopping/member/common/domain/MemberPriority.java rename to src/main/java/shopping/member/common/domain/MemberRole.java index 3e7ccc9..747ca5c 100644 --- a/src/main/java/shopping/member/common/domain/MemberPriority.java +++ b/src/main/java/shopping/member/common/domain/MemberRole.java @@ -1,5 +1,5 @@ package shopping.member.common.domain; -public enum MemberPriority { +public enum MemberRole { CLIENT, OWNER } diff --git a/src/main/java/shopping/member/owner/application/OwnerLoginService.java b/src/main/java/shopping/member/owner/application/OwnerLoginService.java index 49bc2e5..4b8154c 100644 --- a/src/main/java/shopping/member/owner/application/OwnerLoginService.java +++ b/src/main/java/shopping/member/owner/application/OwnerLoginService.java @@ -4,8 +4,10 @@ import org.springframework.stereotype.Service; import shopping.member.common.application.AuthService; import shopping.member.common.domain.Password; +import shopping.member.common.domain.MemberRole; import shopping.member.owner.application.dto.OwnerCreateRequest; import shopping.member.owner.application.dto.OwnerLoginRequest; +import shopping.member.owner.application.dto.OwnerLoginResponse; import shopping.member.owner.domain.Owner; import shopping.member.owner.domain.OwnerRepository; @@ -28,7 +30,9 @@ public void signUp(final OwnerCreateRequest request) { ownerRepository.save(owner); } - public String login(final OwnerLoginRequest request) { - return authService.login(request.email(), request.password(), "Owner"); + public OwnerLoginResponse login(final OwnerLoginRequest request) { + final String accessToken = authService.login(request.email(), request.password(), + MemberRole.OWNER); + return new OwnerLoginResponse(accessToken); } } diff --git a/src/main/java/shopping/member/owner/application/dto/OwnerLoginResponse.java b/src/main/java/shopping/member/owner/application/dto/OwnerLoginResponse.java new file mode 100644 index 0000000..1be0d3b --- /dev/null +++ b/src/main/java/shopping/member/owner/application/dto/OwnerLoginResponse.java @@ -0,0 +1,5 @@ +package shopping.member.owner.application.dto; + +public record OwnerLoginResponse(String accessToken) { + +} diff --git a/src/main/java/shopping/member/owner/domain/Owner.java b/src/main/java/shopping/member/owner/domain/Owner.java index e1df0e0..7fee6cc 100644 --- a/src/main/java/shopping/member/owner/domain/Owner.java +++ b/src/main/java/shopping/member/owner/domain/Owner.java @@ -3,6 +3,7 @@ import jakarta.persistence.Entity; import shopping.member.common.domain.Member; import shopping.member.common.domain.Password; +import shopping.member.common.domain.MemberRole; @Entity public class Owner extends Member { @@ -11,6 +12,6 @@ protected Owner() { } public Owner(final String email, final Password password, final String memberName) { - super(email, password, memberName); + super(email, password, memberName, MemberRole.OWNER); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5d52f56..c8b677d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,5 @@ +jwt: + secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa spring: h2: console: diff --git a/src/test/java/shopping/member/common/application/AuthServiceTest.java b/src/test/java/shopping/member/common/application/AuthServiceTest.java index 36f229d..00cbe80 100644 --- a/src/test/java/shopping/member/common/application/AuthServiceTest.java +++ b/src/test/java/shopping/member/common/application/AuthServiceTest.java @@ -17,6 +17,7 @@ import shopping.member.common.domain.MemberRepository; import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; +import shopping.member.common.domain.MemberRole; import shopping.member.common.exception.InvalidEmailException; import shopping.member.common.exception.InvalidMemberException; import shopping.member.common.exception.InvalidPasswordException; @@ -60,12 +61,12 @@ void setUp() { void 로그인을_수행한다() { saveMember(); - assertThat(authService.login("test@test.com", "1234", "Client")).isNotNull(); + assertThat(authService.login("test@test.com", "1234", MemberRole.CLIENT)).isNotNull(); } @Test void 가입되지않은_멤버의_로그인을_수행하면_예외를_던진다() { - assertThatThrownBy(() -> authService.login("test@test.com", "1234", "Client")) + assertThatThrownBy(() -> authService.login("test@test.com", "1234", MemberRole.CLIENT)) .isInstanceOf(NotFoundMemberException.class); } @@ -73,7 +74,7 @@ void setUp() { void 로그인수행_시_비밀번호를_틀리면_예외를_던진다() { saveMember(); - assertThatThrownBy(() -> authService.login("test@test.com", "1111", "Client")) + assertThatThrownBy(() -> authService.login("test@test.com", "1111", MemberRole.CLIENT)) .isInstanceOf(InvalidPasswordException.class); } @@ -81,7 +82,7 @@ void setUp() { void 로그인수행_시_권한이_없다면_예외를_던진다() { saveMember(); - assertThatThrownBy(() -> authService.login("test@test.com", "1234", "Owner")) + assertThatThrownBy(() -> authService.login("test@test.com", "1234", MemberRole.OWNER)) .isInstanceOf(InvalidMemberException.class); } diff --git a/src/test/java/shopping/member/common/domain/MemberTest.java b/src/test/java/shopping/member/common/domain/MemberTest.java index b23302e..ce63d79 100644 --- a/src/test/java/shopping/member/common/domain/MemberTest.java +++ b/src/test/java/shopping/member/common/domain/MemberTest.java @@ -26,14 +26,14 @@ class MemberTest { void Member가_어떤_권한을_가졌는지_얻을_수_있다() { final Member member = createMember(); - assertThat(member.getRole()).isEqualTo("Client"); + assertThat(member.getMemberRole()).isEqualTo("Client"); } @Test void Member가_유효한_권한을_가졌는지_확인할_수_있다() { final Member member = createMember(); - assertThat(member.isValidRole("Client")).isTrue(); + assertThat(member.isValidRole(MemberRole.CLIENT)).isTrue(); } private Member createMember() { From 7ae2ea440fa4b067b6a57c8c993535e7482a7d3f Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:15:21 +0900 Subject: [PATCH 48/56] =?UTF-8?q?feat(ArgumentResolver):=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=9D=84=20=EC=88=98=ED=96=89=ED=95=9C=20Mem?= =?UTF-8?q?ber=EB=A5=BC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=AC=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?Resolver=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/auth/resolver/LoginClient.java | 12 +++++ .../resolver/LoginClientArgumentResolver.java | 47 +++++++++++++++++++ .../common/auth/resolver/LoginOwner.java | 12 +++++ .../resolver/LoginOwnerArgumentResolver.java | 46 ++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 src/main/java/shopping/common/auth/resolver/LoginClient.java create mode 100644 src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java create mode 100644 src/main/java/shopping/common/auth/resolver/LoginOwner.java create mode 100644 src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java diff --git a/src/main/java/shopping/common/auth/resolver/LoginClient.java b/src/main/java/shopping/common/auth/resolver/LoginClient.java new file mode 100644 index 0000000..65893b5 --- /dev/null +++ b/src/main/java/shopping/common/auth/resolver/LoginClient.java @@ -0,0 +1,12 @@ +package shopping.common.auth.resolver; + +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 LoginClient { + +} diff --git a/src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java b/src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java new file mode 100644 index 0000000..ae05575 --- /dev/null +++ b/src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java @@ -0,0 +1,47 @@ +package shopping.common.auth.resolver; + +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +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; +import shopping.common.auth.exception.UnAuthenticatedException; +import shopping.member.client.domain.Client; +import shopping.member.common.application.AuthService; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberRole; + +@Component +@RequiredArgsConstructor +public class LoginClientArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthService authService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginClient.class) + && parameter.getParameterType().equals(Client.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + try { + return getClient(); + } catch (RuntimeException e) { + throw new UnAuthenticatedException("인증에 실패하였습니다."); + } + } + + private Member getClient() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return authService.findMember(userDetails.getUsername(), MemberRole.CLIENT); + } +} diff --git a/src/main/java/shopping/common/auth/resolver/LoginOwner.java b/src/main/java/shopping/common/auth/resolver/LoginOwner.java new file mode 100644 index 0000000..a7a2884 --- /dev/null +++ b/src/main/java/shopping/common/auth/resolver/LoginOwner.java @@ -0,0 +1,12 @@ +package shopping.common.auth.resolver; + +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 LoginOwner { + +} diff --git a/src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java b/src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java new file mode 100644 index 0000000..da54c8f --- /dev/null +++ b/src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java @@ -0,0 +1,46 @@ +package shopping.common.auth.resolver; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +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; +import shopping.common.auth.exception.UnAuthenticatedException; +import shopping.member.client.domain.Client; +import shopping.member.common.application.AuthService; +import shopping.member.common.domain.Member; +import shopping.member.common.domain.MemberRole; + +@Component +@RequiredArgsConstructor +public class LoginOwnerArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthService authService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginOwner.class) + && parameter.getParameterType().equals(Client.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + try { + return getOwner(); + } catch (RuntimeException e) { + throw new UnAuthenticatedException("인증에 실패하였습니다."); + } + } + + private Member getOwner() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return authService.findMember(userDetails.getUsername(), MemberRole.OWNER); + } +} From 787f1adf443c51355239bd06b0a6c45997134426 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:15:42 +0900 Subject: [PATCH 49/56] =?UTF-8?q?feat(Controller):=20Client,=20Owner=20?= =?UTF-8?q?=ED=91=9C=ED=98=84=EA=B3=84=EC=B8=B5=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/client/ui/ClientController.java | 53 +++++++++++++++++++ .../member/owner/ui/OwnerController.java | 34 ++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/main/java/shopping/member/client/ui/ClientController.java create mode 100644 src/main/java/shopping/member/owner/ui/OwnerController.java diff --git a/src/main/java/shopping/member/client/ui/ClientController.java b/src/main/java/shopping/member/client/ui/ClientController.java new file mode 100644 index 0000000..fae147a --- /dev/null +++ b/src/main/java/shopping/member/client/ui/ClientController.java @@ -0,0 +1,53 @@ +package shopping.member.client.ui; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shopping.common.auth.resolver.LoginClient; +import shopping.member.client.applicaton.ClientLoginService; +import shopping.member.client.applicaton.ClientWishService; +import shopping.member.client.applicaton.dto.ClientCreateRequest; +import shopping.member.client.applicaton.dto.ClientLoginRequest; +import shopping.member.client.applicaton.dto.ClientLoginResponse; +import shopping.member.client.domain.Client; + +@RequestMapping("/api/client") +@RestController +@RequiredArgsConstructor +public class ClientController { + + private final ClientLoginService clientLoginService; + private final ClientWishService clientWishService; + + @PostMapping("/signup") + public ResponseEntity signUp(@RequestBody @Valid final ClientCreateRequest request) { + clientLoginService.signUp(request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/login") + public ResponseEntity login( + @RequestBody @Valid final ClientLoginRequest request) { + final ClientLoginResponse response = clientLoginService.login(request); + return ResponseEntity.ok(response); + } + + + @PutMapping("/wish/{productId}") + public ResponseEntity wish(@PathVariable final Long productId, @LoginClient Client client) { + clientWishService.wish(productId, client); + return ResponseEntity.ok().build(); + } + + @PutMapping("/un-wish/{productId}") + public ResponseEntity unWish(@PathVariable final Long productId, @LoginClient Client client) { + clientWishService.unWish(productId, client); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/shopping/member/owner/ui/OwnerController.java b/src/main/java/shopping/member/owner/ui/OwnerController.java new file mode 100644 index 0000000..95c2307 --- /dev/null +++ b/src/main/java/shopping/member/owner/ui/OwnerController.java @@ -0,0 +1,34 @@ +package shopping.member.owner.ui; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shopping.member.owner.application.OwnerLoginService; +import shopping.member.owner.application.dto.OwnerCreateRequest; +import shopping.member.owner.application.dto.OwnerLoginRequest; +import shopping.member.owner.application.dto.OwnerLoginResponse; + +@RequestMapping("/api/owner") +@RestController +@RequiredArgsConstructor +public class OwnerController { + + private final OwnerLoginService ownerLoginService; + + @PostMapping("/signup") + public ResponseEntity signUp(@RequestBody @Valid final OwnerCreateRequest request) { + ownerLoginService.signUp(request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/login") + public ResponseEntity login( + @RequestBody @Valid final OwnerLoginRequest request) { + final OwnerLoginResponse response = ownerLoginService.login(request); + return ResponseEntity.ok(response); + } +} From 30832b20aea5507b62b6b291bb28d48dab91d5e9 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:17:34 +0900 Subject: [PATCH 50/56] =?UTF-8?q?feat(SecurityConfig):=20=EC=8A=A4?= =?UTF-8?q?=ED=94=84=EB=A7=81=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 11 +++ .../exception/UnAuthenticatedException.java | 10 +++ .../auth/handler/JwtAccessDeniedHandler.java | 20 ++++++ .../handler/JwtAuthenticationEntryPoint.java | 20 ++++++ .../common/config/SecurityConfig.java | 68 +++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 src/main/java/shopping/common/auth/exception/UnAuthenticatedException.java create mode 100644 src/main/java/shopping/common/auth/handler/JwtAccessDeniedHandler.java create mode 100644 src/main/java/shopping/common/auth/handler/JwtAuthenticationEntryPoint.java create mode 100644 src/main/java/shopping/common/config/SecurityConfig.java diff --git a/build.gradle.kts b/build.gradle.kts index 3f75395..bbf0d18 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,17 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + // security + implementation("org.springframework.boot:spring-boot-starter-security") + + // jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + implementation("io.jsonwebtoken:jjwt-impl:0.11.5") + implementation("io.jsonwebtoken:jjwt-jackson:0.11.5") + + // lombok + compileOnly("org.projectlombok:lombok") + annotationProcessor("org.projectlombok:lombok") } kotlin { diff --git a/src/main/java/shopping/common/auth/exception/UnAuthenticatedException.java b/src/main/java/shopping/common/auth/exception/UnAuthenticatedException.java new file mode 100644 index 0000000..59fa766 --- /dev/null +++ b/src/main/java/shopping/common/auth/exception/UnAuthenticatedException.java @@ -0,0 +1,10 @@ +package shopping.common.auth.exception; + +import shopping.common.exception.SpringShoppingException; + +public class UnAuthenticatedException extends SpringShoppingException { + + public UnAuthenticatedException(final String message) { + super(message); + } +} diff --git a/src/main/java/shopping/common/auth/handler/JwtAccessDeniedHandler.java b/src/main/java/shopping/common/auth/handler/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..97304f8 --- /dev/null +++ b/src/main/java/shopping/common/auth/handler/JwtAccessDeniedHandler.java @@ -0,0 +1,20 @@ +package shopping.common.auth.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + //필요한 권한이 없이 접근하려 할때 403 + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } +} \ No newline at end of file diff --git a/src/main/java/shopping/common/auth/handler/JwtAuthenticationEntryPoint.java b/src/main/java/shopping/common/auth/handler/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..7b3d4d6 --- /dev/null +++ b/src/main/java/shopping/common/auth/handler/JwtAuthenticationEntryPoint.java @@ -0,0 +1,20 @@ +package shopping.common.auth.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + // 유효한 자격증명을 제공하지 않고 접근하려 할때 401 + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } +} \ No newline at end of file diff --git a/src/main/java/shopping/common/config/SecurityConfig.java b/src/main/java/shopping/common/config/SecurityConfig.java new file mode 100644 index 0000000..31d1f30 --- /dev/null +++ b/src/main/java/shopping/common/config/SecurityConfig.java @@ -0,0 +1,68 @@ +package shopping.common.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsUtils; +import shopping.common.auth.filter.JwtAuthenticationFilter; +import shopping.common.auth.handler.JwtAccessDeniedHandler; +import shopping.common.auth.handler.JwtAuthenticationEntryPoint; +import shopping.common.auth.token.JwtProvider; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtProvider jwtTokenProvider; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .cors(Customizer.withDefaults()) + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .headers(headers -> headers.frameOptions( + FrameOptionsConfig::disable)) + .sessionManagement( + managementConfigurer -> managementConfigurer.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests((authorizeRequests) -> { + authorizeRequests.requestMatchers(CorsUtils::isPreFlightRequest).permitAll(); + authorizeRequests.requestMatchers( + "/h2-console/**" + ).permitAll(); + authorizeRequests.requestMatchers( + "/api/client/signup", + "/api/client/login", + "/api/owner/signup", + "/api/owner/login" + ).permitAll(); + authorizeRequests.anyRequest().authenticated(); + }) + .exceptionHandling(exceptionHandling -> + exceptionHandling + .accessDeniedHandler(jwtAccessDeniedHandler) + .authenticationEntryPoint(jwtAuthenticationEntryPoint)) + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file From 23be3db7066b6e41d5c763ad82f2bd2662f8bb6b Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:30:28 +0900 Subject: [PATCH 51/56] =?UTF-8?q?feat(Controller):=20Product=20=ED=91=9C?= =?UTF-8?q?=ED=98=84=EA=B3=84=EC=B8=B5=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/application/ProductService.java | 4 +- .../application/dto/ProductUpdateRequest.java | 3 -- .../product/ui/ProductController.java | 51 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 src/main/java/shopping/product/ui/ProductController.java diff --git a/src/main/java/shopping/product/application/ProductService.java b/src/main/java/shopping/product/application/ProductService.java index 561ce9d..42c5b26 100644 --- a/src/main/java/shopping/product/application/ProductService.java +++ b/src/main/java/shopping/product/application/ProductService.java @@ -26,8 +26,8 @@ public Long create(ProductCreateRequest request) { return saved.getId(); } - public void update(ProductUpdateRequest request) { - final Product product = findProduct(request.id()); + public void update(Long productId, ProductUpdateRequest request) { + final Product product = findProduct(productId); product.update(request.name(), profanityChecker, request.price(), diff --git a/src/main/java/shopping/product/application/dto/ProductUpdateRequest.java b/src/main/java/shopping/product/application/dto/ProductUpdateRequest.java index f242e5f..02253b2 100644 --- a/src/main/java/shopping/product/application/dto/ProductUpdateRequest.java +++ b/src/main/java/shopping/product/application/dto/ProductUpdateRequest.java @@ -1,11 +1,8 @@ package shopping.product.application.dto; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; public record ProductUpdateRequest( - @NotNull - Long id, @NotBlank String name, @NotBlank diff --git a/src/main/java/shopping/product/ui/ProductController.java b/src/main/java/shopping/product/ui/ProductController.java new file mode 100644 index 0000000..7aa4c19 --- /dev/null +++ b/src/main/java/shopping/product/ui/ProductController.java @@ -0,0 +1,51 @@ +package shopping.product.ui; + +import jakarta.validation.Valid; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shopping.product.application.ProductService; +import shopping.product.application.dto.ProductCreateRequest; +import shopping.product.application.dto.ProductUpdateRequest; +import shopping.product.domain.Product; + +@RequestMapping("/api/products") +@RestController +@RequiredArgsConstructor +public class ProductController { + + private final ProductService productService; + + @PostMapping() + public ResponseEntity create(@RequestBody @Valid final ProductCreateRequest request) { + final Long saved = productService.create(request); + return ResponseEntity.created(URI.create("/api/products/" + saved)) + .build(); + } + + @PutMapping("/{productId}") + public ResponseEntity update(@PathVariable Long productId, @RequestBody @Valid final ProductUpdateRequest request) { + productService.update(productId, request); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{productId}") + public ResponseEntity delete(@PathVariable Long productId) { + productService.delete(productId); + return ResponseEntity.ok().build(); + } + + @GetMapping("/{productId}") + public ResponseEntity find(@PathVariable Long productId) { + final Product product = productService.findProduct(productId); + return ResponseEntity.ok().body(product); + } +} From 53552fffb7d4c07def509aa5549c06782f375802 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:30:55 +0900 Subject: [PATCH 52/56] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/shopping/Application.java | 1 + .../common/auth/resolver/LoginClientArgumentResolver.java | 1 - src/main/java/shopping/common/config/RestClientConfig.java | 2 +- src/main/java/shopping/common/domain/BaseEntity.java | 4 ---- src/main/java/shopping/member/client/domain/Client.java | 2 +- .../java/shopping/member/client/domain/WishProduct.java | 2 +- .../java/shopping/member/client/ui/ClientController.java | 6 ++++-- .../shopping/member/common/application/AuthService.java | 2 +- .../member/owner/application/OwnerLoginService.java | 2 +- src/main/java/shopping/member/owner/domain/Owner.java | 2 +- src/main/java/shopping/product/ui/ProductController.java | 3 ++- src/test/java/shopping/fixture/ClientFixture.java | 2 +- src/test/java/shopping/fixture/ProductFixture.java | 2 +- .../shopping/member/common/application/AuthServiceTest.java | 2 +- .../member/owner/application/OwnerLoginServiceTest.java | 6 +++--- src/test/java/shopping/product/domain/ProductTest.java | 1 - 16 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/main/java/shopping/Application.java b/src/main/java/shopping/Application.java index fa33ff7..ab61b0f 100644 --- a/src/main/java/shopping/Application.java +++ b/src/main/java/shopping/Application.java @@ -7,6 +7,7 @@ @EnableAsync @SpringBootApplication public class Application { + public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java b/src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java index ae05575..53e2607 100644 --- a/src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java +++ b/src/main/java/shopping/common/auth/resolver/LoginClientArgumentResolver.java @@ -1,6 +1,5 @@ package shopping.common.auth.resolver; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.security.core.Authentication; diff --git a/src/main/java/shopping/common/config/RestClientConfig.java b/src/main/java/shopping/common/config/RestClientConfig.java index d93fef5..ccf485a 100644 --- a/src/main/java/shopping/common/config/RestClientConfig.java +++ b/src/main/java/shopping/common/config/RestClientConfig.java @@ -8,7 +8,7 @@ public class RestClientConfig { @Bean - public RestClient restClient(){ + public RestClient restClient() { return RestClient.create(); } } diff --git a/src/main/java/shopping/common/domain/BaseEntity.java b/src/main/java/shopping/common/domain/BaseEntity.java index 02ba63e..aae42bd 100644 --- a/src/main/java/shopping/common/domain/BaseEntity.java +++ b/src/main/java/shopping/common/domain/BaseEntity.java @@ -1,14 +1,10 @@ package shopping.common.domain; import jakarta.persistence.Column; -import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; -import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; @MappedSuperclass public abstract class BaseEntity { diff --git a/src/main/java/shopping/member/client/domain/Client.java b/src/main/java/shopping/member/client/domain/Client.java index 3301402..401428b 100644 --- a/src/main/java/shopping/member/client/domain/Client.java +++ b/src/main/java/shopping/member/client/domain/Client.java @@ -5,8 +5,8 @@ import java.util.List; import lombok.Getter; import shopping.member.common.domain.Member; -import shopping.member.common.domain.Password; import shopping.member.common.domain.MemberRole; +import shopping.member.common.domain.Password; @Entity public class Client extends Member { diff --git a/src/main/java/shopping/member/client/domain/WishProduct.java b/src/main/java/shopping/member/client/domain/WishProduct.java index a75bac6..6dc5917 100644 --- a/src/main/java/shopping/member/client/domain/WishProduct.java +++ b/src/main/java/shopping/member/client/domain/WishProduct.java @@ -27,7 +27,7 @@ public WishProduct(final Long productId) { this.productId = productId; } - public boolean isSameProduct(Long productId){ + public boolean isSameProduct(Long productId) { return this.productId.equals(productId); } } diff --git a/src/main/java/shopping/member/client/ui/ClientController.java b/src/main/java/shopping/member/client/ui/ClientController.java index fae147a..cd4ec26 100644 --- a/src/main/java/shopping/member/client/ui/ClientController.java +++ b/src/main/java/shopping/member/client/ui/ClientController.java @@ -40,13 +40,15 @@ public ResponseEntity login( @PutMapping("/wish/{productId}") - public ResponseEntity wish(@PathVariable final Long productId, @LoginClient Client client) { + public ResponseEntity wish(@PathVariable final Long productId, + @LoginClient Client client) { clientWishService.wish(productId, client); return ResponseEntity.ok().build(); } @PutMapping("/un-wish/{productId}") - public ResponseEntity unWish(@PathVariable final Long productId, @LoginClient Client client) { + public ResponseEntity unWish(@PathVariable final Long productId, + @LoginClient Client client) { clientWishService.unWish(productId, client); return ResponseEntity.ok().build(); } diff --git a/src/main/java/shopping/member/common/application/AuthService.java b/src/main/java/shopping/member/common/application/AuthService.java index 3745e4b..9aee42f 100644 --- a/src/main/java/shopping/member/common/application/AuthService.java +++ b/src/main/java/shopping/member/common/application/AuthService.java @@ -6,9 +6,9 @@ import shopping.common.auth.token.TokenGenerator; import shopping.member.common.domain.Member; import shopping.member.common.domain.MemberRepository; +import shopping.member.common.domain.MemberRole; import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; -import shopping.member.common.domain.MemberRole; import shopping.member.common.exception.InvalidEmailException; import shopping.member.common.exception.InvalidMemberException; import shopping.member.common.exception.InvalidPasswordException; diff --git a/src/main/java/shopping/member/owner/application/OwnerLoginService.java b/src/main/java/shopping/member/owner/application/OwnerLoginService.java index 4b8154c..2d9e7cd 100644 --- a/src/main/java/shopping/member/owner/application/OwnerLoginService.java +++ b/src/main/java/shopping/member/owner/application/OwnerLoginService.java @@ -3,8 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import shopping.member.common.application.AuthService; -import shopping.member.common.domain.Password; import shopping.member.common.domain.MemberRole; +import shopping.member.common.domain.Password; import shopping.member.owner.application.dto.OwnerCreateRequest; import shopping.member.owner.application.dto.OwnerLoginRequest; import shopping.member.owner.application.dto.OwnerLoginResponse; diff --git a/src/main/java/shopping/member/owner/domain/Owner.java b/src/main/java/shopping/member/owner/domain/Owner.java index 7fee6cc..6cb879e 100644 --- a/src/main/java/shopping/member/owner/domain/Owner.java +++ b/src/main/java/shopping/member/owner/domain/Owner.java @@ -2,8 +2,8 @@ import jakarta.persistence.Entity; import shopping.member.common.domain.Member; -import shopping.member.common.domain.Password; import shopping.member.common.domain.MemberRole; +import shopping.member.common.domain.Password; @Entity public class Owner extends Member { diff --git a/src/main/java/shopping/product/ui/ProductController.java b/src/main/java/shopping/product/ui/ProductController.java index 7aa4c19..3933975 100644 --- a/src/main/java/shopping/product/ui/ProductController.java +++ b/src/main/java/shopping/product/ui/ProductController.java @@ -32,7 +32,8 @@ public ResponseEntity create(@RequestBody @Valid final ProductCreateReques } @PutMapping("/{productId}") - public ResponseEntity update(@PathVariable Long productId, @RequestBody @Valid final ProductUpdateRequest request) { + public ResponseEntity update(@PathVariable Long productId, + @RequestBody @Valid final ProductUpdateRequest request) { productService.update(productId, request); return ResponseEntity.ok().build(); } diff --git a/src/test/java/shopping/fixture/ClientFixture.java b/src/test/java/shopping/fixture/ClientFixture.java index c92750a..7b2321f 100644 --- a/src/test/java/shopping/fixture/ClientFixture.java +++ b/src/test/java/shopping/fixture/ClientFixture.java @@ -6,7 +6,7 @@ public class ClientFixture { - public static Client createClient(){ + public static Client createClient() { final Password password = new Password("1234", new FakePasswordEncoder()); return new Client("test@test.com", password, "test"); } diff --git a/src/test/java/shopping/fixture/ProductFixture.java b/src/test/java/shopping/fixture/ProductFixture.java index 9f2d71f..7339a94 100644 --- a/src/test/java/shopping/fixture/ProductFixture.java +++ b/src/test/java/shopping/fixture/ProductFixture.java @@ -5,7 +5,7 @@ public class ProductFixture { - public static Product createProduct(){ + public static Product createProduct() { final FakeProfanityChecker profanityChecker = new FakeProfanityChecker(); return new Product(1L, "맥북", profanityChecker, 1_000L, "image.jpg"); } diff --git a/src/test/java/shopping/member/common/application/AuthServiceTest.java b/src/test/java/shopping/member/common/application/AuthServiceTest.java index 00cbe80..9e37ec6 100644 --- a/src/test/java/shopping/member/common/application/AuthServiceTest.java +++ b/src/test/java/shopping/member/common/application/AuthServiceTest.java @@ -15,9 +15,9 @@ import shopping.fake.InMemoryMembers; import shopping.member.client.domain.Client; import shopping.member.common.domain.MemberRepository; +import shopping.member.common.domain.MemberRole; import shopping.member.common.domain.Password; import shopping.member.common.domain.PasswordEncoder; -import shopping.member.common.domain.MemberRole; import shopping.member.common.exception.InvalidEmailException; import shopping.member.common.exception.InvalidMemberException; import shopping.member.common.exception.InvalidPasswordException; diff --git a/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java b/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java index 0dc2427..9f6ad3f 100644 --- a/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java +++ b/src/test/java/shopping/member/owner/application/OwnerLoginServiceTest.java @@ -8,10 +8,10 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import shopping.fake.FakePasswordEncoder; import shopping.fake.FakeMemberRepository; -import shopping.fake.InMemoryMembers; import shopping.fake.FakeOwnerRepository; +import shopping.fake.FakePasswordEncoder; +import shopping.fake.InMemoryMembers; import shopping.member.common.application.AuthService; import shopping.member.common.domain.MemberRepository; import shopping.member.common.exception.InvalidEmailException; @@ -31,7 +31,7 @@ void setUp() { final AuthService authService = new AuthService( memberRepository, new FakePasswordEncoder(), - (email, token) -> email+" "+token); + (email, token) -> email + " " + token); final OwnerRepository ownerRepository = new FakeOwnerRepository(inMemoryMembers); ownerService = new OwnerLoginService(authService, ownerRepository); } diff --git a/src/test/java/shopping/product/domain/ProductTest.java b/src/test/java/shopping/product/domain/ProductTest.java index 39d948e..8b4be99 100644 --- a/src/test/java/shopping/product/domain/ProductTest.java +++ b/src/test/java/shopping/product/domain/ProductTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import shopping.fake.FakeProfanityChecker; import shopping.fixture.ProductFixture; @DisplayName("Product") From 5e8767fb4faff3a78a3209bfa1c2330010e5c13c Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:36:14 +0900 Subject: [PATCH 53/56] =?UTF-8?q?refactor(ClientController):=20Client?= =?UTF-8?q?=EB=8A=94=20=ED=95=98=ED=8A=B8=EB=A5=BC=20=EB=88=84=EB=A5=B8?= =?UTF-8?q?=EC=83=81=ED=92=88=EB=93=A4=EC=9D=84=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shopping/member/client/ui/ClientController.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/shopping/member/client/ui/ClientController.java b/src/main/java/shopping/member/client/ui/ClientController.java index cd4ec26..fae3c0d 100644 --- a/src/main/java/shopping/member/client/ui/ClientController.java +++ b/src/main/java/shopping/member/client/ui/ClientController.java @@ -1,8 +1,10 @@ package shopping.member.client.ui; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +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; @@ -15,6 +17,7 @@ import shopping.member.client.applicaton.dto.ClientCreateRequest; import shopping.member.client.applicaton.dto.ClientLoginRequest; import shopping.member.client.applicaton.dto.ClientLoginResponse; +import shopping.member.client.applicaton.dto.WishProductResponse; import shopping.member.client.domain.Client; @RequestMapping("/api/client") @@ -52,4 +55,10 @@ public ResponseEntity unWish(@PathVariable final Long productId, clientWishService.unWish(productId, client); return ResponseEntity.ok().build(); } + + @GetMapping("/wish") + public ResponseEntity> findAll(@LoginClient Client client) { + final List responses = clientWishService.findAll(client); + return ResponseEntity.ok().body(responses); + } } From c8d8ee4e401a11a5ee67d975f678470d9aa600e5 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:36:38 +0900 Subject: [PATCH 54/56] =?UTF-8?q?refactor(ProductController):=20=EC=8B=9C?= =?UTF-8?q?=ED=81=90=EB=A6=AC=ED=8B=B0=20=EA=B6=8C=ED=95=9C=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/shopping/product/ui/ProductController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/shopping/product/ui/ProductController.java b/src/main/java/shopping/product/ui/ProductController.java index 3933975..5fab7db 100644 --- a/src/main/java/shopping/product/ui/ProductController.java +++ b/src/main/java/shopping/product/ui/ProductController.java @@ -4,6 +4,7 @@ import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -25,6 +26,7 @@ public class ProductController { private final ProductService productService; @PostMapping() + @PreAuthorize("hasRole('OWNER')") public ResponseEntity create(@RequestBody @Valid final ProductCreateRequest request) { final Long saved = productService.create(request); return ResponseEntity.created(URI.create("/api/products/" + saved)) @@ -32,6 +34,7 @@ public ResponseEntity create(@RequestBody @Valid final ProductCreateReques } @PutMapping("/{productId}") + @PreAuthorize("hasRole('OWNER')") public ResponseEntity update(@PathVariable Long productId, @RequestBody @Valid final ProductUpdateRequest request) { productService.update(productId, request); @@ -39,6 +42,7 @@ public ResponseEntity update(@PathVariable Long productId, } @DeleteMapping("/{productId}") + @PreAuthorize("hasRole('OWNER')") public ResponseEntity delete(@PathVariable Long productId) { productService.delete(productId); return ResponseEntity.ok().build(); From 070f6a1695475a258eb5d52e5ec1728845f6fb3d Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:38:41 +0900 Subject: [PATCH 55/56] =?UTF-8?q?refactor(LoginOwner):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20Resolver=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/auth/resolver/LoginOwner.java | 12 ----- .../resolver/LoginOwnerArgumentResolver.java | 46 ------------------- 2 files changed, 58 deletions(-) delete mode 100644 src/main/java/shopping/common/auth/resolver/LoginOwner.java delete mode 100644 src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java diff --git a/src/main/java/shopping/common/auth/resolver/LoginOwner.java b/src/main/java/shopping/common/auth/resolver/LoginOwner.java deleted file mode 100644 index a7a2884..0000000 --- a/src/main/java/shopping/common/auth/resolver/LoginOwner.java +++ /dev/null @@ -1,12 +0,0 @@ -package shopping.common.auth.resolver; - -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 LoginOwner { - -} diff --git a/src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java b/src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java deleted file mode 100644 index da54c8f..0000000 --- a/src/main/java/shopping/common/auth/resolver/LoginOwnerArgumentResolver.java +++ /dev/null @@ -1,46 +0,0 @@ -package shopping.common.auth.resolver; - -import lombok.RequiredArgsConstructor; -import org.springframework.core.MethodParameter; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; -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; -import shopping.common.auth.exception.UnAuthenticatedException; -import shopping.member.client.domain.Client; -import shopping.member.common.application.AuthService; -import shopping.member.common.domain.Member; -import shopping.member.common.domain.MemberRole; - -@Component -@RequiredArgsConstructor -public class LoginOwnerArgumentResolver implements HandlerMethodArgumentResolver { - - private final AuthService authService; - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(LoginOwner.class) - && parameter.getParameterType().equals(Client.class); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - try { - return getOwner(); - } catch (RuntimeException e) { - throw new UnAuthenticatedException("인증에 실패하였습니다."); - } - } - - private Member getOwner() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - return authService.findMember(userDetails.getUsername(), MemberRole.OWNER); - } -} From 5f40e92a0609f7f5771979063d57992a437fe092 Mon Sep 17 00:00:00 2001 From: hoa0217 Date: Sun, 11 Aug 2024 10:39:17 +0900 Subject: [PATCH 56/56] =?UTF-8?q?refactor(LoginClient):=20WebConfig?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shopping/common/config/WebConfig.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/shopping/common/config/WebConfig.java diff --git a/src/main/java/shopping/common/config/WebConfig.java b/src/main/java/shopping/common/config/WebConfig.java new file mode 100644 index 0000000..23910d8 --- /dev/null +++ b/src/main/java/shopping/common/config/WebConfig.java @@ -0,0 +1,20 @@ +package shopping.common.config; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import shopping.common.auth.resolver.LoginClientArgumentResolver; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final LoginClientArgumentResolver loginClientArgumentResolver; + + @Override + public void addArgumentResolvers(final List resolvers) { + resolvers.add(loginClientArgumentResolver); + } +}