From c9e1af317ca7e03b1a4f14df10901e706c5c786d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 15:57:41 +0900 Subject: [PATCH 01/49] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20url=20=ED=98=B8=EC=B6=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UsersController.java | 15 +++++++++++++ .../interfaces/api/UserApiE2ETest.java | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java new file mode 100644 index 00000000..ddae4f9f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -0,0 +1,15 @@ +package com.loopers.interfaces.api; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +public class UsersController { + + @PostMapping + public String signUp() { + return "ok"; + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java new file mode 100644 index 00000000..e6210ee7 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -0,0 +1,22 @@ +package com.loopers.interfaces.api; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class UserApiE2ETest { + + @Test + @DisplayName("회원가입 API 호출 테스트") + void userSignupApiTest() { + TestRestTemplate rest = new TestRestTemplate(); + + ResponseEntity res = + rest.postForEntity("http://localhost:8080/users",null, String.class); + + Assertions.assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + } +} From 40037d81a9940670d43e16e31110d5ad6291b358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 16:48:45 +0900 Subject: [PATCH 02/49] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20request=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserSignUpRequestDto.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java new file mode 100644 index 00000000..9299ac66 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java @@ -0,0 +1,16 @@ +package com.loopers.interfaces.api; + +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserSignUpRequestDto { + + private String loginId; + private String pwd; + private LocalDate birthDate; + private String name; + private String email; +} From 0768937f810e1194ad7d3555eb9f56790a91865c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 16:52:17 +0900 Subject: [PATCH 03/49] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20api=20=EC=9D=91=EB=8B=B5=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/interfaces/api/UsersController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index ddae4f9f..8c3aa95b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,6 +1,7 @@ package com.loopers.interfaces.api; 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; @@ -9,7 +10,7 @@ public class UsersController { @PostMapping - public String signUp() { - return "ok"; + public ApiResponse signUp(@RequestBody UserSignUpRequestDto requestDto) { + return ApiResponse.success("ok"); } } From fcfe9df6e3222db75ebd29170007789fe9a90966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 16:52:59 +0900 Subject: [PATCH 04/49] =?UTF-8?q?test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20mockMvc=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserApiE2ETest.java | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java index e6210ee7..29749fe8 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -1,22 +1,42 @@ package com.loopers.interfaces.api; -import org.assertj.core.api.Assertions; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(UsersController.class) +class UserApiE2ETest { + + @Autowired + private MockMvc mockMvc; -public class UserApiE2ETest { + @Autowired + private ObjectMapper objectMapper; @Test @DisplayName("회원가입 API 호출 테스트") - void userSignupApiTest() { - TestRestTemplate rest = new TestRestTemplate(); + void userSignupApiTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); - ResponseEntity res = - rest.postForEntity("http://localhost:8080/users",null, String.class); + String json = objectMapper.writeValueAsString(requestBody); - Assertions.assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()); } } From 7b5ee41f6b61f19ba62b21e354b24973d066f7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 21:42:48 +0900 Subject: [PATCH 05/49] =?UTF-8?q?feat=20:=20=EC=9A=94=EC=B2=AD=20dto?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/interfaces/api/UserSignUpRequestDto.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java index 9299ac66..99486306 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java @@ -1,5 +1,6 @@ package com.loopers.interfaces.api; +import jakarta.validation.constraints.Email; import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,5 +13,7 @@ public class UserSignUpRequestDto { private String pwd; private LocalDate birthDate; private String name; + + @Email(message = "올바른 이메일 형식이 아닙니다.") private String email; } From 01dfaf4500c8b760ab073d4604fc78dda29042c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 21:43:07 +0900 Subject: [PATCH 06/49] =?UTF-8?q?test=20:=20=EC=9A=94=EC=B2=AD=20dto?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5,=20=EC=8B=A4=ED=8C=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserDtoValidationTest.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java new file mode 100644 index 00000000..2f3d4c47 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java @@ -0,0 +1,57 @@ +package com.loopers.interfaces.api; + + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.time.LocalDate; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class UserDtoValidationTest { + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + @DisplayName("이메일 포맷이 맞으면 성공하는 테스트") + void emailFormatSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("이메일 포맷이 안맞으면 실패하는 테스트") + void emailFormatFailTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "ykadasdad" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).hasSize(1); + } +} From 1d431d45bac0e6e4faff370df7184e9eba00b520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 21:54:03 +0900 Subject: [PATCH 07/49] =?UTF-8?q?test=20:=20@NotBlank=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=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 --- .../interfaces/api/UserSignUpRequestDto.java | 2 + .../interfaces/api/UserDtoValidationTest.java | 72 ++++++++++++------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java index 99486306..e0224e8c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java @@ -1,6 +1,7 @@ package com.loopers.interfaces.api; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Getter; @@ -14,6 +15,7 @@ public class UserSignUpRequestDto { private LocalDate birthDate; private String name; + @NotBlank @Email(message = "올바른 이메일 형식이 아닙니다.") private String email; } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java index 2f3d4c47..5ebf0448 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java @@ -11,6 +11,7 @@ import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class UserDtoValidationTest { @@ -23,35 +24,56 @@ void setUp() { validator = factory.getValidator(); } - @Test - @DisplayName("이메일 포맷이 맞으면 성공하는 테스트") - void emailFormatSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권", - "yk@google.com" - ); + @DisplayName("이메일 검증") + @Nested + class EmailValidation { - Set> violations = validator.validate(dto); + @Test + @DisplayName("이메일 포맷이 맞으면 성공하는 테스트") + void emailFormatSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); - assertThat(violations).isEmpty(); - } + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("이메일 포맷이 안맞으면 실패하는 테스트") + void emailFormatFailTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "ykadasdad" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).hasSize(1); + } - @Test - @DisplayName("이메일 포맷이 안맞으면 실패하는 테스트") - void emailFormatFailTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권", - "ykadasdad" - ); + @Test + @DisplayName("이메일에 null이 들어오면 실패하는 테스트") + void emailFormatNullTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + null + ); - Set> violations = validator.validate(dto); + Set> violations = validator.validate(dto); - assertThat(violations).hasSize(1); + assertThat(violations).hasSize(1); + } } } From 4534d90de6e01f79df6ea4866fc53d2c51cc302a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 22:27:24 +0900 Subject: [PATCH 08/49] =?UTF-8?q?feat=20:=20=EC=83=9D=EB=85=84=EC=9B=94?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserSignUpRequestDto.java | 8 +++ .../interfaces/api/UserDtoValidationTest.java | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java index e0224e8c..4e4328ff 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java @@ -1,7 +1,10 @@ package com.loopers.interfaces.api; +import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,7 +15,12 @@ public class UserSignUpRequestDto { private String loginId; private String pwd; + + @NotNull(message = "생년월일은 필수입니다.") + @Past(message = "생년월일은 과거 날짜여야 합니다.") + @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate birthDate; + private String name; @NotBlank diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java index 5ebf0448..b909556a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java @@ -76,4 +76,57 @@ void emailFormatNullTest() { assertThat(violations).hasSize(1); } } + + @DisplayName("생년월일 검증") + @Nested + class BirthdayValidation { + + @Test + @DisplayName("포맷이 맞으면 성공하는 테스트") + void birthFormatSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("미래 날짜면 실패하는 테스트") + void birthFormatDateIsFutureTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.now().plusDays(1), // 내일 + "김용권", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + } + + @Test + @DisplayName("null이면 실패하는 테스트") + void birthFormatDateIsNullTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + null, + "김용권", + "yk@google.com" + ); + + Set> violations = validator.validate(dto);; + + assertThat(violations).isNotEmpty(); + } + } } From bb55305ec6d47beddf206ac1a1574ad931ceb36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 22:52:50 +0900 Subject: [PATCH 09/49] =?UTF-8?q?feat=20:=20=EC=9D=B4=EB=A6=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserSignUpRequestDto.java | 7 +- .../interfaces/api/UserDtoValidationTest.java | 238 +++++++++++++++++- 2 files changed, 243 insertions(+), 2 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java index 4e4328ff..a1290c68 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java @@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,9 +23,12 @@ public class UserSignUpRequestDto { @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate birthDate; + @NotBlank(message = "이름은 필수입니다.") + @Size(min = 2, max = 10, message = "이름은 2자 이상 30자 이하여야 합니다.") + @Pattern(regexp = "^[가-힣a-zA-Z\\s]+$", message = "이름은 한글, 영문, 공백만 입력 가능합니다.") private String name; - @NotBlank + @NotBlank(message = "이메일은 필수입니다.") @Email(message = "올바른 이메일 형식이 아닙니다.") private String email; } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java index b909556a..03925168 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java @@ -124,9 +124,245 @@ void birthFormatDateIsNullTest() { "yk@google.com" ); - Set> violations = validator.validate(dto);; + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + } + } + + @DisplayName("이름 검증") + @Nested + class NameValidation { + + @Test + @DisplayName("올바른 한글 이름이면 검증에 통과한다") + void validKoreanSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("올바른 영문 이름이면 검증에 통과한다") + void validEnglishSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "John", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("한글과 영문이 섞인 이름이면 검증에 통과한다") + void mixedKoreanAndEnglishSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김John", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("공백이 포함된 이름이면 검증에 통과한다") + void nameContainsSpaceSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "홍 길동", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("이름이 2자이면 검증에 통과한다") + void nameIsMinLengthSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("이름이 null이면 검증에 실패한다") + void nameIsNullSuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + null, + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 필수입니다."); + } + + @Test + @DisplayName("이름이 빈 문자열이면 검증에 실패한다") + void nameIsEmptySuccessTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + } + + @Test + @DisplayName("이름이 공백만 있으면 검증에 실패한다") + void nameFormatBlankTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + " ", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + } + + @Test + @DisplayName("이름이 1자이면 검증에 실패한다") + void nameFormatTooShortTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 2자 이상 30자 이하여야 합니다."); + } + + @Test + @DisplayName("이름이 11자 이상이면 검증에 실패한다") + void nameFormatTooLongTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "가나다라마바사아자차카", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 2자 이상 30자 이하여야 합니다."); + } + + @Test + @DisplayName("이름에 숫자가 포함되면 검증에 실패한다") + void nameFormatContainsNumberTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권1", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); + } + + @Test + @DisplayName("이름에 특수문자가 포함되면 검증에 실패한다") + void nameFormatContainsSpecialCharacterTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권!", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); + } + + @Test + @DisplayName("이름에 하이픈이 포함되면 검증에 실패한다") + void nameFormatContainsHyphenTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김-용권", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); + } + + @Test + @DisplayName("이름에 점이 포함되면 검증에 실패한다") + void nameFormatContainsDotTest() { + UserSignUpRequestDto dto = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김.용권", + "yk@google.com" + ); + + Set> violations = validator.validate(dto); assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); } } } From d5f56796ea80c81bdf6350324c35312c8bfd2b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 23:05:07 +0900 Subject: [PATCH 10/49] =?UTF-8?q?feat=20:=20@Valid=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=B4=20MethodArgumentNotVali?= =?UTF-8?q?dException=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/interfaces/api/ApiControllerAdvice.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java index 20b2809c..cf99b05d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java @@ -8,6 +8,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -46,6 +48,15 @@ public ResponseEntity> handleBadRequest(MissingServletRequestPara return failureResponse(ErrorType.BAD_REQUEST, message); } + @ExceptionHandler + public ResponseEntity> handleBadRequest(MethodArgumentNotValidException e) { + FieldError fieldError = e.getBindingResult().getFieldErrors().get(0); + String fieldName = fieldError.getField(); + String message = fieldError.getDefaultMessage(); + String errorMessage = String.format("필드 '%s': %s", fieldName, message); + return failureResponse(ErrorType.BAD_REQUEST, errorMessage); + } + @ExceptionHandler public ResponseEntity> handleBadRequest(HttpMessageNotReadableException e) { String errorMessage; From afce54c06c259d88f65e04f4ee29d9c3c183d309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Mon, 2 Feb 2026 23:10:14 +0900 Subject: [PATCH 11/49] =?UTF-8?q?test=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EC=8B=A4=ED=8C=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UsersController.java | 3 +- .../interfaces/api/UserApiE2ETest.java | 207 ++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 8c3aa95b..8aff9f42 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,5 +1,6 @@ package com.loopers.interfaces.api; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -10,7 +11,7 @@ public class UsersController { @PostMapping - public ApiResponse signUp(@RequestBody UserSignUpRequestDto requestDto) { + public ApiResponse signUp(@Valid @RequestBody UserSignUpRequestDto requestDto) { return ApiResponse.success("ok"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java index 29749fe8..d4e469f8 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -39,4 +40,210 @@ void userSignupApiTest() throws Exception { .content(json)) .andExpect(status().isOk()); } + + @DisplayName("회원가입 API 실패 테스트") + @Nested + class UserSignupFailureTest { + + @Test + @DisplayName("이메일 형식이 잘못되면 400 Bad Request를 반환한다") + void userSignupApiEmailInvalidTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + "invalid-email" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이메일이 null이면 400 Bad Request를 반환한다") + void userSignupApiEmailNullTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권", + null + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이름이 null이면 400 Bad Request를 반환한다") + void userSignupApiNameNullTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + null, + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이름이 빈 문자열이면 400 Bad Request를 반환한다") + void userSignupApiNameEmptyTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이름이 1자이면 400 Bad Request를 반환한다") + void userSignupApiNameTooShortTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이름이 11자 이상이면 400 Bad Request를 반환한다") + void userSignupApiNameTooLongTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "가나다라마바사아자차카", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이름에 숫자가 포함되면 400 Bad Request를 반환한다") + void userSignupApiNameContainsNumberTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권1", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이름에 특수문자가 포함되면 400 Bad Request를 반환한다") + void userSignupApiNameContainsSpecialCharacterTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.of(1991, 12, 3), + "김용권!", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("생년월일이 null이면 400 Bad Request를 반환한다") + void userSignupApiBirthDateNullTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + null, + "김용권", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("생년월일이 미래 날짜이면 400 Bad Request를 반환한다") + void userSignupApiBirthDateFutureTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "pw111", + LocalDate.now().plusDays(1), + "김용권", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("잘못된 JSON 형식이면 400 Bad Request를 반환한다") + void userSignupApiJsonInvalidTest() throws Exception { + String invalidJson = "{ invalid json }"; + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(invalidJson)) + .andExpect(status().isBadRequest()); + } + } } From ddb16e25b572b71973807fb71c2ded975367b55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 09:50:20 +0900 Subject: [PATCH 12/49] =?UTF-8?q?test=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=82=B4=EC=97=90=EC=84=9C=20dto=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserDtoValidationTest.java | 239 ++++++------------ 1 file changed, 78 insertions(+), 161 deletions(-) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java index 03925168..2e38bd44 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java @@ -16,6 +16,12 @@ public class UserDtoValidationTest { + private static final String DEFAULT_LOGIN_ID = "kim"; + private static final String DEFAULT_PWD = "Password1"; + private static final LocalDate DEFAULT_BIRTH_DATE = LocalDate.of(1991, 12, 3); + private static final String DEFAULT_NAME = "김용권"; + private static final String DEFAULT_EMAIL = "yk@google.com"; + private Validator validator; @BeforeEach @@ -24,6 +30,34 @@ void setUp() { validator = factory.getValidator(); } + private UserSignUpRequestDto defaultDto() { + return new UserSignUpRequestDto( + DEFAULT_LOGIN_ID, DEFAULT_PWD, DEFAULT_BIRTH_DATE, DEFAULT_NAME, DEFAULT_EMAIL + ); + } + + private UserSignUpRequestDto dtoWithEmail(String email) { + return new UserSignUpRequestDto( + DEFAULT_LOGIN_ID, DEFAULT_PWD, DEFAULT_BIRTH_DATE, DEFAULT_NAME, email + ); + } + + private UserSignUpRequestDto dtoWithBirthDate(LocalDate birthDate) { + return new UserSignUpRequestDto( + DEFAULT_LOGIN_ID, DEFAULT_PWD, birthDate, DEFAULT_NAME, DEFAULT_EMAIL + ); + } + + private UserSignUpRequestDto dtoWithName(String name) { + return new UserSignUpRequestDto( + DEFAULT_LOGIN_ID, DEFAULT_PWD, DEFAULT_BIRTH_DATE, name, DEFAULT_EMAIL + ); + } + + private Set> validate(UserSignUpRequestDto dto) { + return validator.validate(dto); + } + @DisplayName("이메일 검증") @Nested class EmailValidation { @@ -31,15 +65,9 @@ class EmailValidation { @Test @DisplayName("이메일 포맷이 맞으면 성공하는 테스트") void emailFormatSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권", - "yk@google.com" - ); + UserSignUpRequestDto dto = defaultDto(); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -47,15 +75,9 @@ void emailFormatSuccessTest() { @Test @DisplayName("이메일 포맷이 안맞으면 실패하는 테스트") void emailFormatFailTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권", - "ykadasdad" - ); + UserSignUpRequestDto dto = dtoWithEmail("ykadasdad"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).hasSize(1); } @@ -63,15 +85,9 @@ void emailFormatFailTest() { @Test @DisplayName("이메일에 null이 들어오면 실패하는 테스트") void emailFormatNullTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권", - null - ); + UserSignUpRequestDto dto = dtoWithEmail(null); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).hasSize(1); } @@ -84,15 +100,9 @@ class BirthdayValidation { @Test @DisplayName("포맷이 맞으면 성공하는 테스트") void birthFormatSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권", - "yk@google.com" - ); + UserSignUpRequestDto dto = defaultDto(); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -100,15 +110,9 @@ void birthFormatSuccessTest() { @Test @DisplayName("미래 날짜면 실패하는 테스트") void birthFormatDateIsFutureTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.now().plusDays(1), // 내일 - "김용권", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithBirthDate(LocalDate.now().plusDays(1)); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); } @@ -116,15 +120,9 @@ void birthFormatDateIsFutureTest() { @Test @DisplayName("null이면 실패하는 테스트") void birthFormatDateIsNullTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - null, - "김용권", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithBirthDate(null); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); } @@ -137,15 +135,9 @@ class NameValidation { @Test @DisplayName("올바른 한글 이름이면 검증에 통과한다") void validKoreanSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권", - "yk@google.com" - ); + UserSignUpRequestDto dto = defaultDto(); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -153,15 +145,9 @@ void validKoreanSuccessTest() { @Test @DisplayName("올바른 영문 이름이면 검증에 통과한다") void validEnglishSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "John", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("John"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -169,15 +155,9 @@ void validEnglishSuccessTest() { @Test @DisplayName("한글과 영문이 섞인 이름이면 검증에 통과한다") void mixedKoreanAndEnglishSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김John", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("김John"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -185,15 +165,9 @@ void mixedKoreanAndEnglishSuccessTest() { @Test @DisplayName("공백이 포함된 이름이면 검증에 통과한다") void nameContainsSpaceSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "홍 길동", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("홍 길동"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -201,15 +175,9 @@ void nameContainsSpaceSuccessTest() { @Test @DisplayName("이름이 2자이면 검증에 통과한다") void nameIsMinLengthSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("김용"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -217,15 +185,9 @@ void nameIsMinLengthSuccessTest() { @Test @DisplayName("이름이 null이면 검증에 실패한다") void nameIsNullSuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - null, - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName(null); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 필수입니다."); @@ -234,15 +196,9 @@ void nameIsNullSuccessTest() { @Test @DisplayName("이름이 빈 문자열이면 검증에 실패한다") void nameIsEmptySuccessTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName(""); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); } @@ -250,31 +206,22 @@ void nameIsEmptySuccessTest() { @Test @DisplayName("이름이 공백만 있으면 검증에 실패한다") void nameFormatBlankTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - " ", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName(" "); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); + // NotBlank 또는 Pattern 위반 가능 (구현/순서에 따라 메시지 상이) + assertThat(violations.iterator().next().getMessage()) + .isIn("이름은 필수입니다.", "이름은 한글, 영문, 공백만 입력 가능합니다."); } @Test @DisplayName("이름이 1자이면 검증에 실패한다") void nameFormatTooShortTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("김"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 2자 이상 30자 이하여야 합니다."); @@ -283,15 +230,9 @@ void nameFormatTooShortTest() { @Test @DisplayName("이름이 11자 이상이면 검증에 실패한다") void nameFormatTooLongTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "가나다라마바사아자차카", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("가나다라마바사아자차카"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 2자 이상 30자 이하여야 합니다."); @@ -300,15 +241,9 @@ void nameFormatTooLongTest() { @Test @DisplayName("이름에 숫자가 포함되면 검증에 실패한다") void nameFormatContainsNumberTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권1", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("김용권1"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); @@ -317,15 +252,9 @@ void nameFormatContainsNumberTest() { @Test @DisplayName("이름에 특수문자가 포함되면 검증에 실패한다") void nameFormatContainsSpecialCharacterTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김용권!", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("김용권!"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); @@ -334,15 +263,9 @@ void nameFormatContainsSpecialCharacterTest() { @Test @DisplayName("이름에 하이픈이 포함되면 검증에 실패한다") void nameFormatContainsHyphenTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김-용권", - "yk@google.com" - ); + UserSignUpRequestDto dto = dtoWithName("김-용권"); - Set> violations = validator.validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); @@ -351,15 +274,9 @@ void nameFormatContainsHyphenTest() { @Test @DisplayName("이름에 점이 포함되면 검증에 실패한다") void nameFormatContainsDotTest() { - UserSignUpRequestDto dto = new UserSignUpRequestDto( - "kim", - "pw111", - LocalDate.of(1991, 12, 3), - "김.용권", - "yk@google.com" - ); - - Set> violations = validator.validate(dto); + UserSignUpRequestDto dto = dtoWithName("김.용권"); + + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); From 28f3ab844802f1b58db1c28a8e7551f68d009aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 09:58:51 +0900 Subject: [PATCH 13/49] =?UTF-8?q?feat=20:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserSignUpRequestDto.java | 11 +- .../interfaces/api/UserApiE2ETest.java | 98 +++++++++++++-- .../interfaces/api/UserDtoValidationTest.java | 119 ++++++++++++++++-- 3 files changed, 205 insertions(+), 23 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java index a1290c68..cda625ab 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java @@ -16,6 +16,12 @@ public class UserSignUpRequestDto { private String loginId; + + @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") + @Pattern( + regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", + message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." + ) private String pwd; @NotNull(message = "생년월일은 필수입니다.") @@ -25,7 +31,10 @@ public class UserSignUpRequestDto { @NotBlank(message = "이름은 필수입니다.") @Size(min = 2, max = 10, message = "이름은 2자 이상 30자 이하여야 합니다.") - @Pattern(regexp = "^[가-힣a-zA-Z\\s]+$", message = "이름은 한글, 영문, 공백만 입력 가능합니다.") + @Pattern( + regexp = "^[가-힣a-zA-Z\\s]+$", + message = "이름은 한글, 영문, 공백만 입력 가능합니다." + ) private String name; @NotBlank(message = "이메일은 필수입니다.") diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java index d4e469f8..80238134 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -27,7 +27,7 @@ class UserApiE2ETest { void userSignupApiTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -50,7 +50,7 @@ class UserSignupFailureTest { void userSignupApiEmailInvalidTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "김용권", "invalid-email" @@ -69,7 +69,7 @@ void userSignupApiEmailInvalidTest() throws Exception { void userSignupApiEmailNullTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "김용권", null @@ -88,7 +88,7 @@ void userSignupApiEmailNullTest() throws Exception { void userSignupApiNameNullTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), null, "yk@google.com" @@ -107,7 +107,7 @@ void userSignupApiNameNullTest() throws Exception { void userSignupApiNameEmptyTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "", "yk@google.com" @@ -126,7 +126,7 @@ void userSignupApiNameEmptyTest() throws Exception { void userSignupApiNameTooShortTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "김", "yk@google.com" @@ -145,7 +145,7 @@ void userSignupApiNameTooShortTest() throws Exception { void userSignupApiNameTooLongTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "가나다라마바사아자차카", "yk@google.com" @@ -164,7 +164,7 @@ void userSignupApiNameTooLongTest() throws Exception { void userSignupApiNameContainsNumberTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "김용권1", "yk@google.com" @@ -183,7 +183,7 @@ void userSignupApiNameContainsNumberTest() throws Exception { void userSignupApiNameContainsSpecialCharacterTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.of(1991, 12, 3), "김용권!", "yk@google.com" @@ -202,7 +202,7 @@ void userSignupApiNameContainsSpecialCharacterTest() throws Exception { void userSignupApiBirthDateNullTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", null, "김용권", "yk@google.com" @@ -221,7 +221,7 @@ void userSignupApiBirthDateNullTest() throws Exception { void userSignupApiBirthDateFutureTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( "kim", - "pw111", + "Password1", LocalDate.now().plusDays(1), "김용권", "yk@google.com" @@ -235,6 +235,82 @@ void userSignupApiBirthDateFutureTest() throws Exception { .andExpect(status().isBadRequest()); } + @Test + @DisplayName("비밀번호가 7자 이하면 400 Bad Request를 반환한다") + void userSignupApiPwdTooShortTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "Abc12!", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("비밀번호가 17자 이상이면 400 Bad Request를 반환한다") + void userSignupApiPwdTooLongTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "Abcd123!@#efgh456", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("비밀번호에 한글이 포함되면 400 Bad Request를 반환한다") + void userSignupApiPwdContainsKoreanTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "Password1가", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("비밀번호에 공백이 포함되면 400 Bad Request를 반환한다") + void userSignupApiPwdContainsSpaceTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + "kim", + "Password 1!", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + @Test @DisplayName("잘못된 JSON 형식이면 400 Bad Request를 반환한다") void userSignupApiJsonInvalidTest() throws Exception { diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java index 2e38bd44..a6f105d8 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java @@ -54,6 +54,12 @@ private UserSignUpRequestDto dtoWithName(String name) { ); } + private UserSignUpRequestDto dtoWithPwd(String pwd) { + return new UserSignUpRequestDto( + DEFAULT_LOGIN_ID, pwd, DEFAULT_BIRTH_DATE, DEFAULT_NAME, DEFAULT_EMAIL + ); + } + private Set> validate(UserSignUpRequestDto dto) { return validator.validate(dto); } @@ -109,7 +115,7 @@ void birthFormatSuccessTest() { @Test @DisplayName("미래 날짜면 실패하는 테스트") - void birthFormatDateIsFutureTest() { + void birthFormatDateIsFutureFailTest() { UserSignUpRequestDto dto = dtoWithBirthDate(LocalDate.now().plusDays(1)); Set> violations = validate(dto); @@ -119,7 +125,7 @@ void birthFormatDateIsFutureTest() { @Test @DisplayName("null이면 실패하는 테스트") - void birthFormatDateIsNullTest() { + void birthFormatDateIsNullFailTest() { UserSignUpRequestDto dto = dtoWithBirthDate(null); Set> violations = validate(dto); @@ -128,6 +134,97 @@ void birthFormatDateIsNullTest() { } } + @DisplayName("비밀번호 검증") + @Nested + class PwdValidation { + + @Test + @DisplayName("8~16자 영문·숫자·특수문자 조합이면 검증에 통과한다") + void pwdFormatSuccessTest() { + UserSignUpRequestDto dto = defaultDto(); + + Set> violations = validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("비밀번호가 정확히 8자이면 검증에 통과한다") + void pwdMinLengthSuccessTest() { + UserSignUpRequestDto dto = dtoWithPwd("Abcd123!"); + + Set> violations = validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("비밀번호가 정확히 16자이면 검증에 통과한다") + void pwdMaxLengthSuccessTest() { + UserSignUpRequestDto dto = dtoWithPwd("Abcd123!@#efgh45"); + + Set> violations = validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("비밀번호가 영문·숫자·특수문자만 있으면 검증에 통과한다") + void pwdAlphanumericAndSpecialSuccessTest() { + UserSignUpRequestDto dto = dtoWithPwd("Pass@word1"); + + Set> violations = validate(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("비밀번호가 7자 이하면 검증에 실패한다") + void pwdTooShortFailTest() { + UserSignUpRequestDto dto = dtoWithPwd("Abc12!"); + + Set> violations = validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("8~16자로 입력해주세요."); + } + + @Test + @DisplayName("비밀번호가 17자 이상이면 검증에 실패한다") + void pwdTooLongFailTest() { + UserSignUpRequestDto dto = dtoWithPwd("Abcd123!@#efgh456"); + + Set> violations = validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("8~16자로 입력해주세요."); + } + + @Test + @DisplayName("비밀번호에 한글이 포함되면 검증에 실패한다") + void pwdContainsKoreanFailTest() { + UserSignUpRequestDto dto = dtoWithPwd("Password1가"); + + Set> violations = validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()) + .isEqualTo("영문 대소문자, 숫자, 특수문자만 사용 가능합니다."); + } + + @Test + @DisplayName("비밀번호에 공백이 포함되면 검증에 실패한다") + void pwdContainsSpaceFailTest() { + UserSignUpRequestDto dto = dtoWithPwd("Password 1!"); + + Set> violations = validate(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()) + .isEqualTo("영문 대소문자, 숫자, 특수문자만 사용 가능합니다."); + } + } + @DisplayName("이름 검증") @Nested class NameValidation { @@ -184,7 +281,7 @@ void nameIsMinLengthSuccessTest() { @Test @DisplayName("이름이 null이면 검증에 실패한다") - void nameIsNullSuccessTest() { + void nameIsNullFailTest() { UserSignUpRequestDto dto = dtoWithName(null); Set> violations = validate(dto); @@ -195,7 +292,7 @@ void nameIsNullSuccessTest() { @Test @DisplayName("이름이 빈 문자열이면 검증에 실패한다") - void nameIsEmptySuccessTest() { + void nameIsEmptyFailTest() { UserSignUpRequestDto dto = dtoWithName(""); Set> violations = validate(dto); @@ -205,7 +302,7 @@ void nameIsEmptySuccessTest() { @Test @DisplayName("이름이 공백만 있으면 검증에 실패한다") - void nameFormatBlankTest() { + void nameFormatBlankFailTest() { UserSignUpRequestDto dto = dtoWithName(" "); Set> violations = validate(dto); @@ -218,7 +315,7 @@ void nameFormatBlankTest() { @Test @DisplayName("이름이 1자이면 검증에 실패한다") - void nameFormatTooShortTest() { + void nameFormatTooShortFailTest() { UserSignUpRequestDto dto = dtoWithName("김"); Set> violations = validate(dto); @@ -229,7 +326,7 @@ void nameFormatTooShortTest() { @Test @DisplayName("이름이 11자 이상이면 검증에 실패한다") - void nameFormatTooLongTest() { + void nameFormatTooLongFailTest() { UserSignUpRequestDto dto = dtoWithName("가나다라마바사아자차카"); Set> violations = validate(dto); @@ -240,7 +337,7 @@ void nameFormatTooLongTest() { @Test @DisplayName("이름에 숫자가 포함되면 검증에 실패한다") - void nameFormatContainsNumberTest() { + void nameFormatContainsNumberFailTest() { UserSignUpRequestDto dto = dtoWithName("김용권1"); Set> violations = validate(dto); @@ -251,7 +348,7 @@ void nameFormatContainsNumberTest() { @Test @DisplayName("이름에 특수문자가 포함되면 검증에 실패한다") - void nameFormatContainsSpecialCharacterTest() { + void nameFormatContainsSpecialCharacterFailTest() { UserSignUpRequestDto dto = dtoWithName("김용권!"); Set> violations = validate(dto); @@ -262,7 +359,7 @@ void nameFormatContainsSpecialCharacterTest() { @Test @DisplayName("이름에 하이픈이 포함되면 검증에 실패한다") - void nameFormatContainsHyphenTest() { + void nameFormatContainsHyphenFailTest() { UserSignUpRequestDto dto = dtoWithName("김-용권"); Set> violations = validate(dto); @@ -273,7 +370,7 @@ void nameFormatContainsHyphenTest() { @Test @DisplayName("이름에 점이 포함되면 검증에 실패한다") - void nameFormatContainsDotTest() { + void nameFormatContainsDotFailTest() { UserSignUpRequestDto dto = dtoWithName("김.용권"); Set> violations = validate(dto); From 4bcad0d3754395ce23a269f19833ef4e13735f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 10:16:39 +0900 Subject: [PATCH 14/49] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20id=20+=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EB=A5=BC=20=ED=97=A4=EB=8D=94=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/LoopersHeaders.java | 17 +++ .../interfaces/api/UserSignUpRequestDto.java | 9 -- .../interfaces/api/UsersController.java | 23 +++- .../interfaces/api/UserApiE2ETest.java | 96 ++++++++++----- .../interfaces/api/UserDtoValidationTest.java | 115 +----------------- 5 files changed, 109 insertions(+), 151 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/LoopersHeaders.java diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/LoopersHeaders.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/LoopersHeaders.java new file mode 100644 index 00000000..e0ed063d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/LoopersHeaders.java @@ -0,0 +1,17 @@ +package com.loopers.interfaces.api; + +/** + * 유저 정보가 필요한 요청에서 사용하는 헤더 이름. + *
    + *
  • X-Loopers-LoginId : 로그인 ID
  • + *
  • X-Loopers-LoginPw : 비밀번호
  • + *
+ */ +public final class LoopersHeaders { + + public static final String X_LOOPERS_LOGIN_ID = "X-Loopers-LoginId"; + public static final String X_LOOPERS_LOGIN_PW = "X-Loopers-LoginPw"; + + private LoopersHeaders() { + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java index cda625ab..17e89c4a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java @@ -15,15 +15,6 @@ @AllArgsConstructor public class UserSignUpRequestDto { - private String loginId; - - @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") - @Pattern( - regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", - message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." - ) - private String pwd; - @NotNull(message = "생년월일은 필수입니다.") @Past(message = "생년월일은 과거 날짜여야 합니다.") @JsonFormat(pattern = "yyyy-MM-dd") diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 8aff9f42..621f0e1a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,17 +1,38 @@ package com.loopers.interfaces.api; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.validation.annotation.Validated; +@Validated @RestController @RequestMapping("/users") public class UsersController { @PostMapping - public ApiResponse signUp(@Valid @RequestBody UserSignUpRequestDto requestDto) { + public ApiResponse signUp( + @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID) + @NotBlank(message = "로그인 ID는 필수입니다.") + String loginId, + + @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW) + @NotBlank(message = "비밀번호는 필수입니다.") + @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") + @Pattern( + regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", + message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." + ) + String loginPw, + + @Valid @RequestBody UserSignUpRequestDto requestDto + ) { return ApiResponse.success("ok"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java index 80238134..1cb907fa 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -26,8 +26,6 @@ class UserApiE2ETest { @DisplayName("회원가입 API 호출 테스트") void userSignupApiTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -37,6 +35,8 @@ void userSignupApiTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isOk()); } @@ -49,8 +49,6 @@ class UserSignupFailureTest { @DisplayName("이메일 형식이 잘못되면 400 Bad Request를 반환한다") void userSignupApiEmailInvalidTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "김용권", "invalid-email" @@ -60,6 +58,8 @@ void userSignupApiEmailInvalidTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -68,8 +68,6 @@ void userSignupApiEmailInvalidTest() throws Exception { @DisplayName("이메일이 null이면 400 Bad Request를 반환한다") void userSignupApiEmailNullTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "김용권", null @@ -79,6 +77,8 @@ void userSignupApiEmailNullTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -87,8 +87,6 @@ void userSignupApiEmailNullTest() throws Exception { @DisplayName("이름이 null이면 400 Bad Request를 반환한다") void userSignupApiNameNullTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), null, "yk@google.com" @@ -98,6 +96,8 @@ void userSignupApiNameNullTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -106,8 +106,6 @@ void userSignupApiNameNullTest() throws Exception { @DisplayName("이름이 빈 문자열이면 400 Bad Request를 반환한다") void userSignupApiNameEmptyTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "", "yk@google.com" @@ -117,6 +115,8 @@ void userSignupApiNameEmptyTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -125,8 +125,6 @@ void userSignupApiNameEmptyTest() throws Exception { @DisplayName("이름이 1자이면 400 Bad Request를 반환한다") void userSignupApiNameTooShortTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "김", "yk@google.com" @@ -136,6 +134,8 @@ void userSignupApiNameTooShortTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -144,8 +144,6 @@ void userSignupApiNameTooShortTest() throws Exception { @DisplayName("이름이 11자 이상이면 400 Bad Request를 반환한다") void userSignupApiNameTooLongTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "가나다라마바사아자차카", "yk@google.com" @@ -155,6 +153,8 @@ void userSignupApiNameTooLongTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -163,8 +163,6 @@ void userSignupApiNameTooLongTest() throws Exception { @DisplayName("이름에 숫자가 포함되면 400 Bad Request를 반환한다") void userSignupApiNameContainsNumberTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "김용권1", "yk@google.com" @@ -174,6 +172,8 @@ void userSignupApiNameContainsNumberTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -182,8 +182,6 @@ void userSignupApiNameContainsNumberTest() throws Exception { @DisplayName("이름에 특수문자가 포함되면 400 Bad Request를 반환한다") void userSignupApiNameContainsSpecialCharacterTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.of(1991, 12, 3), "김용권!", "yk@google.com" @@ -193,6 +191,8 @@ void userSignupApiNameContainsSpecialCharacterTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -201,8 +201,6 @@ void userSignupApiNameContainsSpecialCharacterTest() throws Exception { @DisplayName("생년월일이 null이면 400 Bad Request를 반환한다") void userSignupApiBirthDateNullTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", null, "김용권", "yk@google.com" @@ -212,6 +210,8 @@ void userSignupApiBirthDateNullTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -220,8 +220,6 @@ void userSignupApiBirthDateNullTest() throws Exception { @DisplayName("생년월일이 미래 날짜이면 400 Bad Request를 반환한다") void userSignupApiBirthDateFutureTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1", LocalDate.now().plusDays(1), "김용권", "yk@google.com" @@ -231,6 +229,8 @@ void userSignupApiBirthDateFutureTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(json)) .andExpect(status().isBadRequest()); } @@ -239,8 +239,6 @@ void userSignupApiBirthDateFutureTest() throws Exception { @DisplayName("비밀번호가 7자 이하면 400 Bad Request를 반환한다") void userSignupApiPwdTooShortTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Abc12!", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -250,6 +248,8 @@ void userSignupApiPwdTooShortTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Abc12!") .content(json)) .andExpect(status().isBadRequest()); } @@ -258,8 +258,6 @@ void userSignupApiPwdTooShortTest() throws Exception { @DisplayName("비밀번호가 17자 이상이면 400 Bad Request를 반환한다") void userSignupApiPwdTooLongTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Abcd123!@#efgh456", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -269,6 +267,8 @@ void userSignupApiPwdTooLongTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Abcd123!@#efgh456") .content(json)) .andExpect(status().isBadRequest()); } @@ -277,8 +277,6 @@ void userSignupApiPwdTooLongTest() throws Exception { @DisplayName("비밀번호에 한글이 포함되면 400 Bad Request를 반환한다") void userSignupApiPwdContainsKoreanTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password1가", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -288,6 +286,8 @@ void userSignupApiPwdContainsKoreanTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1가") .content(json)) .andExpect(status().isBadRequest()); } @@ -296,8 +296,6 @@ void userSignupApiPwdContainsKoreanTest() throws Exception { @DisplayName("비밀번호에 공백이 포함되면 400 Bad Request를 반환한다") void userSignupApiPwdContainsSpaceTest() throws Exception { UserSignUpRequestDto requestBody = new UserSignUpRequestDto( - "kim", - "Password 1!", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -307,6 +305,42 @@ void userSignupApiPwdContainsSpaceTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password 1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("X-Loopers-LoginId 헤더가 없으면 400 Bad Request를 반환한다") + void userSignupApiMissingLoginIdHeaderTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("X-Loopers-LoginPw 헤더가 없으면 400 Bad Request를 반환한다") + void userSignupApiMissingLoginPwHeaderTest() throws Exception { + UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") .content(json)) .andExpect(status().isBadRequest()); } @@ -318,6 +352,8 @@ void userSignupApiJsonInvalidTest() throws Exception { mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") .content(invalidJson)) .andExpect(status().isBadRequest()); } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java index a6f105d8..318358ad 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java @@ -16,8 +16,6 @@ public class UserDtoValidationTest { - private static final String DEFAULT_LOGIN_ID = "kim"; - private static final String DEFAULT_PWD = "Password1"; private static final LocalDate DEFAULT_BIRTH_DATE = LocalDate.of(1991, 12, 3); private static final String DEFAULT_NAME = "김용권"; private static final String DEFAULT_EMAIL = "yk@google.com"; @@ -31,33 +29,19 @@ void setUp() { } private UserSignUpRequestDto defaultDto() { - return new UserSignUpRequestDto( - DEFAULT_LOGIN_ID, DEFAULT_PWD, DEFAULT_BIRTH_DATE, DEFAULT_NAME, DEFAULT_EMAIL - ); + return new UserSignUpRequestDto(DEFAULT_BIRTH_DATE, DEFAULT_NAME, DEFAULT_EMAIL); } private UserSignUpRequestDto dtoWithEmail(String email) { - return new UserSignUpRequestDto( - DEFAULT_LOGIN_ID, DEFAULT_PWD, DEFAULT_BIRTH_DATE, DEFAULT_NAME, email - ); + return new UserSignUpRequestDto(DEFAULT_BIRTH_DATE, DEFAULT_NAME, email); } private UserSignUpRequestDto dtoWithBirthDate(LocalDate birthDate) { - return new UserSignUpRequestDto( - DEFAULT_LOGIN_ID, DEFAULT_PWD, birthDate, DEFAULT_NAME, DEFAULT_EMAIL - ); + return new UserSignUpRequestDto(birthDate, DEFAULT_NAME, DEFAULT_EMAIL); } private UserSignUpRequestDto dtoWithName(String name) { - return new UserSignUpRequestDto( - DEFAULT_LOGIN_ID, DEFAULT_PWD, DEFAULT_BIRTH_DATE, name, DEFAULT_EMAIL - ); - } - - private UserSignUpRequestDto dtoWithPwd(String pwd) { - return new UserSignUpRequestDto( - DEFAULT_LOGIN_ID, pwd, DEFAULT_BIRTH_DATE, DEFAULT_NAME, DEFAULT_EMAIL - ); + return new UserSignUpRequestDto(DEFAULT_BIRTH_DATE, name, DEFAULT_EMAIL); } private Set> validate(UserSignUpRequestDto dto) { @@ -134,97 +118,6 @@ void birthFormatDateIsNullFailTest() { } } - @DisplayName("비밀번호 검증") - @Nested - class PwdValidation { - - @Test - @DisplayName("8~16자 영문·숫자·특수문자 조합이면 검증에 통과한다") - void pwdFormatSuccessTest() { - UserSignUpRequestDto dto = defaultDto(); - - Set> violations = validate(dto); - - assertThat(violations).isEmpty(); - } - - @Test - @DisplayName("비밀번호가 정확히 8자이면 검증에 통과한다") - void pwdMinLengthSuccessTest() { - UserSignUpRequestDto dto = dtoWithPwd("Abcd123!"); - - Set> violations = validate(dto); - - assertThat(violations).isEmpty(); - } - - @Test - @DisplayName("비밀번호가 정확히 16자이면 검증에 통과한다") - void pwdMaxLengthSuccessTest() { - UserSignUpRequestDto dto = dtoWithPwd("Abcd123!@#efgh45"); - - Set> violations = validate(dto); - - assertThat(violations).isEmpty(); - } - - @Test - @DisplayName("비밀번호가 영문·숫자·특수문자만 있으면 검증에 통과한다") - void pwdAlphanumericAndSpecialSuccessTest() { - UserSignUpRequestDto dto = dtoWithPwd("Pass@word1"); - - Set> violations = validate(dto); - - assertThat(violations).isEmpty(); - } - - @Test - @DisplayName("비밀번호가 7자 이하면 검증에 실패한다") - void pwdTooShortFailTest() { - UserSignUpRequestDto dto = dtoWithPwd("Abc12!"); - - Set> violations = validate(dto); - - assertThat(violations).isNotEmpty(); - assertThat(violations.iterator().next().getMessage()).isEqualTo("8~16자로 입력해주세요."); - } - - @Test - @DisplayName("비밀번호가 17자 이상이면 검증에 실패한다") - void pwdTooLongFailTest() { - UserSignUpRequestDto dto = dtoWithPwd("Abcd123!@#efgh456"); - - Set> violations = validate(dto); - - assertThat(violations).isNotEmpty(); - assertThat(violations.iterator().next().getMessage()).isEqualTo("8~16자로 입력해주세요."); - } - - @Test - @DisplayName("비밀번호에 한글이 포함되면 검증에 실패한다") - void pwdContainsKoreanFailTest() { - UserSignUpRequestDto dto = dtoWithPwd("Password1가"); - - Set> violations = validate(dto); - - assertThat(violations).isNotEmpty(); - assertThat(violations.iterator().next().getMessage()) - .isEqualTo("영문 대소문자, 숫자, 특수문자만 사용 가능합니다."); - } - - @Test - @DisplayName("비밀번호에 공백이 포함되면 검증에 실패한다") - void pwdContainsSpaceFailTest() { - UserSignUpRequestDto dto = dtoWithPwd("Password 1!"); - - Set> violations = validate(dto); - - assertThat(violations).isNotEmpty(); - assertThat(violations.iterator().next().getMessage()) - .isEqualTo("영문 대소문자, 숫자, 특수문자만 사용 가능합니다."); - } - } - @DisplayName("이름 검증") @Nested class NameValidation { From f33afac18b847e4912eb440291f2b5d8ba3b7c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 10:27:48 +0900 Subject: [PATCH 15/49] =?UTF-8?q?fix=20:=20exception=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=B6=94=EA=B0=80=ED=95=B4=EC=84=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B9=A8=EC=A7=80=EB=8A=94=20=ED=98=84?= =?UTF-8?q?=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/ApiControllerAdvice.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java index cf99b05d..2cee9a9b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java @@ -5,11 +5,14 @@ import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -57,6 +60,22 @@ public ResponseEntity> handleBadRequest(MethodArgumentNotValidExc return failureResponse(ErrorType.BAD_REQUEST, errorMessage); } + @ExceptionHandler + public ResponseEntity> handleBadRequest(MissingRequestHeaderException e) { + String headerName = e.getHeaderName(); + String message = String.format("필수 요청 헤더 '%s'가 누락되었습니다.", headerName); + return failureResponse(ErrorType.BAD_REQUEST, message); + } + + @ExceptionHandler + public ResponseEntity> handleBadRequest(ConstraintViolationException e) { + String message = e.getConstraintViolations().stream() + .findFirst() + .map(ConstraintViolation::getMessage) + .orElse("요청 값이 올바르지 않습니다."); + return failureResponse(ErrorType.BAD_REQUEST, message); + } + @ExceptionHandler public ResponseEntity> handleBadRequest(HttpMessageNotReadableException e) { String errorMessage; From 02792abe5b5ef1a7e9eda2161be1fddfc78d5991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 15:22:08 +0900 Subject: [PATCH 16/49] =?UTF-8?q?feat=20:=20=EC=8A=A4=ED=94=84=EB=A7=81=20?= =?UTF-8?q?=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20crypto=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EB=B9=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/commerce-api/build.gradle.kts | 1 + .../com/loopers/config/PasswordEncoderConfig.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/config/PasswordEncoderConfig.java diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts index 03ce68f0..cb54a44b 100644 --- a/apps/commerce-api/build.gradle.kts +++ b/apps/commerce-api/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { // web implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.security:spring-security-crypto") implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}") diff --git a/apps/commerce-api/src/main/java/com/loopers/config/PasswordEncoderConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/PasswordEncoderConfig.java new file mode 100644 index 00000000..d42feb17 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/config/PasswordEncoderConfig.java @@ -0,0 +1,15 @@ +package com.loopers.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} From 1380b79f43df0566c0201a463726ce44dbb6016e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 15:22:49 +0900 Subject: [PATCH 17/49] =?UTF-8?q?feat=20:=20UsersService=20=EA=B3=84?= =?UTF-8?q?=EC=B8=B5=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EC=BD=94=EB=93=9C=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 --- .../com/loopers/application/UserService.java | 35 +++++++++++++++++++ .../interfaces/api/UsersController.java | 18 +++++----- .../com/loopers/support/error/ErrorType.java | 5 ++- 3 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/UserService.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserService.java b/apps/commerce-api/src/main/java/com/loopers/application/UserService.java new file mode 100644 index 00000000..91d9b841 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserService.java @@ -0,0 +1,35 @@ +package com.loopers.application; + +import com.loopers.interfaces.api.UserSignUpRequestDto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class UserService { + + private final PasswordEncoder passwordEncoder; + + public void signup(String loginId, String loginPw, UserSignUpRequestDto requestDto) { + validatePasswordContent(loginPw, requestDto.getBirthDate()); + + String encodedPw = passwordEncoder.encode(loginPw); + + // TODO + // Users 엔티티 + } + + private void validatePasswordContent(String password, LocalDate birthDate) { + if (password == null || birthDate == null) return; + + String birthStr = birthDate.toString().replace("-", ""); + + if (password.contains(birthStr)) { + throw new CoreException(ErrorType.NOT_INCLUDE_BIRTH_IN_PASSWORD); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 621f0e1a..b19ed564 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,38 +1,38 @@ package com.loopers.interfaces.api; +import com.loopers.application.UserService; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.validation.annotation.Validated; @Validated @RestController @RequestMapping("/users") +@AllArgsConstructor public class UsersController { + private final UserService userService; + @PostMapping public ApiResponse signUp( - @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID) - @NotBlank(message = "로그인 ID는 필수입니다.") - String loginId, - - @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW) - @NotBlank(message = "비밀번호는 필수입니다.") - @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") + @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID) @NotBlank(message = "로그인 ID는 필수입니다.") String loginId, + @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW) @NotBlank(message = "비밀번호는 필수입니다.") @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") @Pattern( regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." ) String loginPw, - @Valid @RequestBody UserSignUpRequestDto requestDto ) { + userService.signup(loginId, loginPw, requestDto); return ApiResponse.success("ok"); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java index 5d142efb..dfae46a0 100644 --- a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java +++ b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java @@ -11,7 +11,10 @@ public enum ErrorType { INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."), BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."), NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."), - CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."); + CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."), + + NOT_INCLUDE_BIRTH_IN_PASSWORD(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "비밀번호에 생년월일을 포함할 수 없습니다.") + ; private final HttpStatus status; private final String code; From fe97a43aa8ccc6c37d07e369970ddd1fa8d60dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 15:24:47 +0900 Subject: [PATCH 18/49] =?UTF-8?q?test=20:=20UsersService=20slice=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- .../loopers/application/UserServiceTest.java | 58 +++++++++++++++++++ .../interfaces/api/UserApiE2ETest.java | 5 ++ 2 files changed, 63 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java new file mode 100644 index 00000000..aad83ef0 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java @@ -0,0 +1,58 @@ +package com.loopers.application; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.loopers.interfaces.api.UserSignUpRequestDto; +import com.loopers.support.error.CoreException; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @InjectMocks + private UserService userService; + + @Mock + private PasswordEncoder passwordEncoder; + + private UserSignUpRequestDto createDto(LocalDate birthDate) { + return new UserSignUpRequestDto(birthDate, "email@test.com", "kkk@gmail.com"); + } + + @Test + @DisplayName("회원가입 성공 테스트") + void success_signup() { + + String rawPw = "securePassword!@"; + String encodedPw = "encoded_hash"; + UserSignUpRequestDto dto = createDto(LocalDate.of(1995, 1, 1)); + + given(passwordEncoder.encode(rawPw)).willReturn(encodedPw); + + userService.signup("user123", rawPw, dto); + + verify(passwordEncoder, times(1)).encode(rawPw); + } + + @Test + @DisplayName("회원가입 실패 - 비밀번호에 생년월일 포함되어있으면 예외 발생") + void fail_with_birthDate() { + + String rawPw = "pw19911203!!"; // 생일 포함 + UserSignUpRequestDto dto = createDto(LocalDate.of(1991,12, 3)); + + assertThatThrownBy(() -> userService.signup("user123", rawPw, dto)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("생년월일을 포함할 수 없습니다"); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java index 1cb907fa..b923e85b 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -5,12 +5,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import com.loopers.application.UserService; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(UsersController.class) @@ -22,6 +24,9 @@ class UserApiE2ETest { @Autowired private ObjectMapper objectMapper; + @MockitoBean + private UserService userService; + @Test @DisplayName("회원가입 API 호출 테스트") void userSignupApiTest() throws Exception { From a94a2dd9d85246df06fb38e88e14a2692376e63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 15:32:47 +0900 Subject: [PATCH 19/49] =?UTF-8?q?refactor=20:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/application/UserService.java | 4 +- .../interfaces/api/UsersController.java | 2 +- ...estDto.java => UsersSignUpRequestDto.java} | 2 +- .../loopers/application/UserServiceTest.java | 10 +- .../interfaces/api/UserApiE2ETest.java | 68 ++++++------ .../{api => dto}/UserDtoValidationTest.java | 101 +++++++++--------- 6 files changed, 94 insertions(+), 93 deletions(-) rename apps/commerce-api/src/main/java/com/loopers/interfaces/api/{UserSignUpRequestDto.java => UsersSignUpRequestDto.java} (96%) rename apps/commerce-api/src/test/java/com/loopers/interfaces/{api => dto}/UserDtoValidationTest.java (62%) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserService.java b/apps/commerce-api/src/main/java/com/loopers/application/UserService.java index 91d9b841..cf41c9fe 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserService.java @@ -1,6 +1,6 @@ package com.loopers.application; -import com.loopers.interfaces.api.UserSignUpRequestDto; +import com.loopers.interfaces.api.UsersSignUpRequestDto; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import java.time.LocalDate; @@ -14,7 +14,7 @@ public class UserService { private final PasswordEncoder passwordEncoder; - public void signup(String loginId, String loginPw, UserSignUpRequestDto requestDto) { + public void signup(String loginId, String loginPw, UsersSignUpRequestDto requestDto) { validatePasswordContent(loginPw, requestDto.getBirthDate()); String encodedPw = passwordEncoder.encode(loginPw); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index b19ed564..4a323d2d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -30,7 +30,7 @@ public ApiResponse signUp( message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." ) String loginPw, - @Valid @RequestBody UserSignUpRequestDto requestDto + @Valid @RequestBody UsersSignUpRequestDto requestDto ) { userService.signup(loginId, loginPw, requestDto); return ApiResponse.success("ok"); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersSignUpRequestDto.java similarity index 96% rename from apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersSignUpRequestDto.java index 17e89c4a..d0186d74 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UserSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersSignUpRequestDto.java @@ -13,7 +13,7 @@ @Getter @AllArgsConstructor -public class UserSignUpRequestDto { +public class UsersSignUpRequestDto { @NotNull(message = "생년월일은 필수입니다.") @Past(message = "생년월일은 과거 날짜여야 합니다.") diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java index aad83ef0..31e9eb13 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java @@ -5,7 +5,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import com.loopers.interfaces.api.UserSignUpRequestDto; +import com.loopers.interfaces.api.UsersSignUpRequestDto; import com.loopers.support.error.CoreException; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; @@ -25,8 +25,8 @@ public class UserServiceTest { @Mock private PasswordEncoder passwordEncoder; - private UserSignUpRequestDto createDto(LocalDate birthDate) { - return new UserSignUpRequestDto(birthDate, "email@test.com", "kkk@gmail.com"); + private UsersSignUpRequestDto createDto(LocalDate birthDate) { + return new UsersSignUpRequestDto(birthDate, "email@test.com", "kkk@gmail.com"); } @Test @@ -35,7 +35,7 @@ void success_signup() { String rawPw = "securePassword!@"; String encodedPw = "encoded_hash"; - UserSignUpRequestDto dto = createDto(LocalDate.of(1995, 1, 1)); + UsersSignUpRequestDto dto = createDto(LocalDate.of(1995, 1, 1)); given(passwordEncoder.encode(rawPw)).willReturn(encodedPw); @@ -49,7 +49,7 @@ void success_signup() { void fail_with_birthDate() { String rawPw = "pw19911203!!"; // 생일 포함 - UserSignUpRequestDto dto = createDto(LocalDate.of(1991,12, 3)); + UsersSignUpRequestDto dto = createDto(LocalDate.of(1991,12, 3)); assertThatThrownBy(() -> userService.signup("user123", rawPw, dto)) .isInstanceOf(CoreException.class) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java index b923e85b..afb8eaf8 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -29,8 +29,8 @@ class UserApiE2ETest { @Test @DisplayName("회원가입 API 호출 테스트") - void userSignupApiTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void success_signup() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -52,8 +52,8 @@ class UserSignupFailureTest { @Test @DisplayName("이메일 형식이 잘못되면 400 Bad Request를 반환한다") - void userSignupApiEmailInvalidTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_email_format_invalid() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "invalid-email" @@ -71,8 +71,8 @@ void userSignupApiEmailInvalidTest() throws Exception { @Test @DisplayName("이메일이 null이면 400 Bad Request를 반환한다") - void userSignupApiEmailNullTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_email_null() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", null @@ -90,8 +90,8 @@ void userSignupApiEmailNullTest() throws Exception { @Test @DisplayName("이름이 null이면 400 Bad Request를 반환한다") - void userSignupApiNameNullTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_name_null() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), null, "yk@google.com" @@ -109,8 +109,8 @@ void userSignupApiNameNullTest() throws Exception { @Test @DisplayName("이름이 빈 문자열이면 400 Bad Request를 반환한다") - void userSignupApiNameEmptyTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_name_empty() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "", "yk@google.com" @@ -128,8 +128,8 @@ void userSignupApiNameEmptyTest() throws Exception { @Test @DisplayName("이름이 1자이면 400 Bad Request를 반환한다") - void userSignupApiNameTooShortTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_name_too_short() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김", "yk@google.com" @@ -147,8 +147,8 @@ void userSignupApiNameTooShortTest() throws Exception { @Test @DisplayName("이름이 11자 이상이면 400 Bad Request를 반환한다") - void userSignupApiNameTooLongTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_name_too_long() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "가나다라마바사아자차카", "yk@google.com" @@ -166,8 +166,8 @@ void userSignupApiNameTooLongTest() throws Exception { @Test @DisplayName("이름에 숫자가 포함되면 400 Bad Request를 반환한다") - void userSignupApiNameContainsNumberTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_name_contains_number() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권1", "yk@google.com" @@ -185,8 +185,8 @@ void userSignupApiNameContainsNumberTest() throws Exception { @Test @DisplayName("이름에 특수문자가 포함되면 400 Bad Request를 반환한다") - void userSignupApiNameContainsSpecialCharacterTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_name_contains_special_character() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권!", "yk@google.com" @@ -204,8 +204,8 @@ void userSignupApiNameContainsSpecialCharacterTest() throws Exception { @Test @DisplayName("생년월일이 null이면 400 Bad Request를 반환한다") - void userSignupApiBirthDateNullTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_birthDate_null() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( null, "김용권", "yk@google.com" @@ -223,8 +223,8 @@ void userSignupApiBirthDateNullTest() throws Exception { @Test @DisplayName("생년월일이 미래 날짜이면 400 Bad Request를 반환한다") - void userSignupApiBirthDateFutureTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_birthDate_future() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.now().plusDays(1), "김용권", "yk@google.com" @@ -242,8 +242,8 @@ void userSignupApiBirthDateFutureTest() throws Exception { @Test @DisplayName("비밀번호가 7자 이하면 400 Bad Request를 반환한다") - void userSignupApiPwdTooShortTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_password_too_short() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -261,8 +261,8 @@ void userSignupApiPwdTooShortTest() throws Exception { @Test @DisplayName("비밀번호가 17자 이상이면 400 Bad Request를 반환한다") - void userSignupApiPwdTooLongTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_password_too_long() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -280,8 +280,8 @@ void userSignupApiPwdTooLongTest() throws Exception { @Test @DisplayName("비밀번호에 한글이 포함되면 400 Bad Request를 반환한다") - void userSignupApiPwdContainsKoreanTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_password_contains_korean() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -299,8 +299,8 @@ void userSignupApiPwdContainsKoreanTest() throws Exception { @Test @DisplayName("비밀번호에 공백이 포함되면 400 Bad Request를 반환한다") - void userSignupApiPwdContainsSpaceTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_password_contains_space() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -318,8 +318,8 @@ void userSignupApiPwdContainsSpaceTest() throws Exception { @Test @DisplayName("X-Loopers-LoginId 헤더가 없으면 400 Bad Request를 반환한다") - void userSignupApiMissingLoginIdHeaderTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_loginId_header_missing() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" @@ -335,8 +335,8 @@ void userSignupApiMissingLoginIdHeaderTest() throws Exception { @Test @DisplayName("X-Loopers-LoginPw 헤더가 없으면 400 Bad Request를 반환한다") - void userSignupApiMissingLoginPwHeaderTest() throws Exception { - UserSignUpRequestDto requestBody = new UserSignUpRequestDto( + void fail_when_loginPw_header_missing() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( LocalDate.of(1991, 12, 3), "김용권", "yk@google.com" diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java similarity index 62% rename from apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java rename to apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java index 318358ad..d76f7494 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java @@ -1,8 +1,9 @@ -package com.loopers.interfaces.api; +package com.loopers.interfaces.dto; import static org.assertj.core.api.Assertions.assertThat; +import com.loopers.interfaces.api.UsersSignUpRequestDto; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; @@ -28,23 +29,23 @@ void setUp() { validator = factory.getValidator(); } - private UserSignUpRequestDto defaultDto() { - return new UserSignUpRequestDto(DEFAULT_BIRTH_DATE, DEFAULT_NAME, DEFAULT_EMAIL); + private UsersSignUpRequestDto defaultDto() { + return new UsersSignUpRequestDto(DEFAULT_BIRTH_DATE, DEFAULT_NAME, DEFAULT_EMAIL); } - private UserSignUpRequestDto dtoWithEmail(String email) { - return new UserSignUpRequestDto(DEFAULT_BIRTH_DATE, DEFAULT_NAME, email); + private UsersSignUpRequestDto dtoWithEmail(String email) { + return new UsersSignUpRequestDto(DEFAULT_BIRTH_DATE, DEFAULT_NAME, email); } - private UserSignUpRequestDto dtoWithBirthDate(LocalDate birthDate) { - return new UserSignUpRequestDto(birthDate, DEFAULT_NAME, DEFAULT_EMAIL); + private UsersSignUpRequestDto dtoWithBirthDate(LocalDate birthDate) { + return new UsersSignUpRequestDto(birthDate, DEFAULT_NAME, DEFAULT_EMAIL); } - private UserSignUpRequestDto dtoWithName(String name) { - return new UserSignUpRequestDto(DEFAULT_BIRTH_DATE, name, DEFAULT_EMAIL); + private UsersSignUpRequestDto dtoWithName(String name) { + return new UsersSignUpRequestDto(DEFAULT_BIRTH_DATE, name, DEFAULT_EMAIL); } - private Set> validate(UserSignUpRequestDto dto) { + private Set> validate(UsersSignUpRequestDto dto) { return validator.validate(dto); } @@ -55,9 +56,9 @@ class EmailValidation { @Test @DisplayName("이메일 포맷이 맞으면 성공하는 테스트") void emailFormatSuccessTest() { - UserSignUpRequestDto dto = defaultDto(); + UsersSignUpRequestDto dto = defaultDto(); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -65,9 +66,9 @@ void emailFormatSuccessTest() { @Test @DisplayName("이메일 포맷이 안맞으면 실패하는 테스트") void emailFormatFailTest() { - UserSignUpRequestDto dto = dtoWithEmail("ykadasdad"); + UsersSignUpRequestDto dto = dtoWithEmail("ykadasdad"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).hasSize(1); } @@ -75,9 +76,9 @@ void emailFormatFailTest() { @Test @DisplayName("이메일에 null이 들어오면 실패하는 테스트") void emailFormatNullTest() { - UserSignUpRequestDto dto = dtoWithEmail(null); + UsersSignUpRequestDto dto = dtoWithEmail(null); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).hasSize(1); } @@ -90,9 +91,9 @@ class BirthdayValidation { @Test @DisplayName("포맷이 맞으면 성공하는 테스트") void birthFormatSuccessTest() { - UserSignUpRequestDto dto = defaultDto(); + UsersSignUpRequestDto dto = defaultDto(); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -100,9 +101,9 @@ void birthFormatSuccessTest() { @Test @DisplayName("미래 날짜면 실패하는 테스트") void birthFormatDateIsFutureFailTest() { - UserSignUpRequestDto dto = dtoWithBirthDate(LocalDate.now().plusDays(1)); + UsersSignUpRequestDto dto = dtoWithBirthDate(LocalDate.now().plusDays(1)); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); } @@ -110,9 +111,9 @@ void birthFormatDateIsFutureFailTest() { @Test @DisplayName("null이면 실패하는 테스트") void birthFormatDateIsNullFailTest() { - UserSignUpRequestDto dto = dtoWithBirthDate(null); + UsersSignUpRequestDto dto = dtoWithBirthDate(null); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); } @@ -125,9 +126,9 @@ class NameValidation { @Test @DisplayName("올바른 한글 이름이면 검증에 통과한다") void validKoreanSuccessTest() { - UserSignUpRequestDto dto = defaultDto(); + UsersSignUpRequestDto dto = defaultDto(); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -135,9 +136,9 @@ void validKoreanSuccessTest() { @Test @DisplayName("올바른 영문 이름이면 검증에 통과한다") void validEnglishSuccessTest() { - UserSignUpRequestDto dto = dtoWithName("John"); + UsersSignUpRequestDto dto = dtoWithName("John"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -145,9 +146,9 @@ void validEnglishSuccessTest() { @Test @DisplayName("한글과 영문이 섞인 이름이면 검증에 통과한다") void mixedKoreanAndEnglishSuccessTest() { - UserSignUpRequestDto dto = dtoWithName("김John"); + UsersSignUpRequestDto dto = dtoWithName("김John"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -155,9 +156,9 @@ void mixedKoreanAndEnglishSuccessTest() { @Test @DisplayName("공백이 포함된 이름이면 검증에 통과한다") void nameContainsSpaceSuccessTest() { - UserSignUpRequestDto dto = dtoWithName("홍 길동"); + UsersSignUpRequestDto dto = dtoWithName("홍 길동"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -165,9 +166,9 @@ void nameContainsSpaceSuccessTest() { @Test @DisplayName("이름이 2자이면 검증에 통과한다") void nameIsMinLengthSuccessTest() { - UserSignUpRequestDto dto = dtoWithName("김용"); + UsersSignUpRequestDto dto = dtoWithName("김용"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isEmpty(); } @@ -175,9 +176,9 @@ void nameIsMinLengthSuccessTest() { @Test @DisplayName("이름이 null이면 검증에 실패한다") void nameIsNullFailTest() { - UserSignUpRequestDto dto = dtoWithName(null); + UsersSignUpRequestDto dto = dtoWithName(null); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 필수입니다."); @@ -186,9 +187,9 @@ void nameIsNullFailTest() { @Test @DisplayName("이름이 빈 문자열이면 검증에 실패한다") void nameIsEmptyFailTest() { - UserSignUpRequestDto dto = dtoWithName(""); + UsersSignUpRequestDto dto = dtoWithName(""); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); } @@ -196,9 +197,9 @@ void nameIsEmptyFailTest() { @Test @DisplayName("이름이 공백만 있으면 검증에 실패한다") void nameFormatBlankFailTest() { - UserSignUpRequestDto dto = dtoWithName(" "); + UsersSignUpRequestDto dto = dtoWithName(" "); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); // NotBlank 또는 Pattern 위반 가능 (구현/순서에 따라 메시지 상이) @@ -209,9 +210,9 @@ void nameFormatBlankFailTest() { @Test @DisplayName("이름이 1자이면 검증에 실패한다") void nameFormatTooShortFailTest() { - UserSignUpRequestDto dto = dtoWithName("김"); + UsersSignUpRequestDto dto = dtoWithName("김"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 2자 이상 30자 이하여야 합니다."); @@ -220,9 +221,9 @@ void nameFormatTooShortFailTest() { @Test @DisplayName("이름이 11자 이상이면 검증에 실패한다") void nameFormatTooLongFailTest() { - UserSignUpRequestDto dto = dtoWithName("가나다라마바사아자차카"); + UsersSignUpRequestDto dto = dtoWithName("가나다라마바사아자차카"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 2자 이상 30자 이하여야 합니다."); @@ -231,9 +232,9 @@ void nameFormatTooLongFailTest() { @Test @DisplayName("이름에 숫자가 포함되면 검증에 실패한다") void nameFormatContainsNumberFailTest() { - UserSignUpRequestDto dto = dtoWithName("김용권1"); + UsersSignUpRequestDto dto = dtoWithName("김용권1"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); @@ -242,9 +243,9 @@ void nameFormatContainsNumberFailTest() { @Test @DisplayName("이름에 특수문자가 포함되면 검증에 실패한다") void nameFormatContainsSpecialCharacterFailTest() { - UserSignUpRequestDto dto = dtoWithName("김용권!"); + UsersSignUpRequestDto dto = dtoWithName("김용권!"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); @@ -253,9 +254,9 @@ void nameFormatContainsSpecialCharacterFailTest() { @Test @DisplayName("이름에 하이픈이 포함되면 검증에 실패한다") void nameFormatContainsHyphenFailTest() { - UserSignUpRequestDto dto = dtoWithName("김-용권"); + UsersSignUpRequestDto dto = dtoWithName("김-용권"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); @@ -264,9 +265,9 @@ void nameFormatContainsHyphenFailTest() { @Test @DisplayName("이름에 점이 포함되면 검증에 실패한다") void nameFormatContainsDotFailTest() { - UserSignUpRequestDto dto = dtoWithName("김.용권"); + UsersSignUpRequestDto dto = dtoWithName("김.용권"); - Set> violations = validate(dto); + Set> violations = validate(dto); assertThat(violations).isNotEmpty(); assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); From 4a1e0350a99350be7485d85c7a905e1eb79f33c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 16:53:15 +0900 Subject: [PATCH 20/49] =?UTF-8?q?feat=20:=20User=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20&=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=20=EC=BD=94=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 --- .../java/com/loopers/domain/UserModel.java | 60 +++++++++++++++++++ .../com/loopers/domain/UserRepository.java | 9 +++ .../{application => domain}/UserService.java | 15 +++-- .../infrastructure/UserJpaRepository.java | 8 +++ .../infrastructure/UserJpaRepositoryImpl.java | 18 ++++++ .../interfaces/api/UsersController.java | 4 +- .../loopers/application/UserServiceTest.java | 5 +- .../interfaces/api/UserApiE2ETest.java | 2 +- 8 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java rename apps/commerce-api/src/main/java/com/loopers/{application => domain}/UserService.java (76%) create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepositoryImpl.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java new file mode 100644 index 00000000..0c2d4e69 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java @@ -0,0 +1,60 @@ +package com.loopers.domain; + +import com.loopers.interfaces.api.UsersSignUpRequestDto; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Entity +@Table(name = "users") +@NoArgsConstructor +public class UserModel extends BaseEntity { + + @Id @GeneratedValue + private Long id; + + @Comment("아이디") + @Column(name = "login_id", nullable = false) + private String loginId; + + @Comment("비밀번호") + @Column(name = "password", nullable = false) + private String password; + + @Comment("생년월일") + @Column(name = "birth", nullable = false) + private LocalDate birthDate; + + @Comment("이름") + @Column(name = "name", nullable = false) + private String name; + + @Comment("이메일") + @Column(name = "email", nullable = false) + private String email; + + @Builder + public UserModel(String loginId, String encodedPw, LocalDate birthDate, String name, String email) { + this.loginId = loginId; + this.password = encodedPw; + this.birthDate = birthDate; + this.name = name; + this.email = email; + } + + public static UserModel create(String loginId, String encodedPw, UsersSignUpRequestDto requestDto) { + return UserModel.builder() + .loginId(loginId) + .encodedPw(encodedPw) + .birthDate(requestDto.getBirthDate()) + .name(requestDto.getName()) + .email(requestDto.getEmail()) + .build(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java new file mode 100644 index 00000000..3e87373c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java @@ -0,0 +1,9 @@ +package com.loopers.domain; + +import java.util.Optional; + +public interface UserRepository { + + UserModel save(UserModel userModel); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java similarity index 76% rename from apps/commerce-api/src/main/java/com/loopers/application/UserService.java rename to apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index cf41c9fe..73fb9b5d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -1,4 +1,4 @@ -package com.loopers.application; +package com.loopers.domain; import com.loopers.interfaces.api.UsersSignUpRequestDto; import com.loopers.support.error.CoreException; @@ -14,17 +14,22 @@ public class UserService { private final PasswordEncoder passwordEncoder; - public void signup(String loginId, String loginPw, UsersSignUpRequestDto requestDto) { + public void signUp(String loginId, String loginPw, UsersSignUpRequestDto requestDto) { validatePasswordContent(loginPw, requestDto.getBirthDate()); String encodedPw = passwordEncoder.encode(loginPw); - // TODO - // Users 엔티티 + UserModel.create( + loginId, + encodedPw, + requestDto + ); } private void validatePasswordContent(String password, LocalDate birthDate) { - if (password == null || birthDate == null) return; + if (password == null || birthDate == null) { + return; + } String birthStr = birthDate.toString().replace("-", ""); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java new file mode 100644 index 00000000..b94483b5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java @@ -0,0 +1,8 @@ +package com.loopers.infrastructure; + +import com.loopers.domain.UserModel; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserJpaRepository extends JpaRepository { + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepositoryImpl.java new file mode 100644 index 00000000..1e0032e2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepositoryImpl.java @@ -0,0 +1,18 @@ +package com.loopers.infrastructure; + +import com.loopers.domain.UserModel; +import com.loopers.domain.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UserJpaRepositoryImpl implements UserRepository { + + private final UserJpaRepository userJpaRepository; + + @Override + public UserModel save(UserModel userModel) { + return userJpaRepository.save(userModel); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 4a323d2d..492b6a96 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api; -import com.loopers.application.UserService; +import com.loopers.domain.UserService; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; @@ -32,7 +32,7 @@ public ApiResponse signUp( String loginPw, @Valid @RequestBody UsersSignUpRequestDto requestDto ) { - userService.signup(loginId, loginPw, requestDto); + userService.signUp(loginId, loginPw, requestDto); return ApiResponse.success("ok"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java index 31e9eb13..48ce3a29 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import com.loopers.domain.UserService; import com.loopers.interfaces.api.UsersSignUpRequestDto; import com.loopers.support.error.CoreException; import java.time.LocalDate; @@ -39,7 +40,7 @@ void success_signup() { given(passwordEncoder.encode(rawPw)).willReturn(encodedPw); - userService.signup("user123", rawPw, dto); + userService.signUp("user123", rawPw, dto); verify(passwordEncoder, times(1)).encode(rawPw); } @@ -51,7 +52,7 @@ void fail_with_birthDate() { String rawPw = "pw19911203!!"; // 생일 포함 UsersSignUpRequestDto dto = createDto(LocalDate.of(1991,12, 3)); - assertThatThrownBy(() -> userService.signup("user123", rawPw, dto)) + assertThatThrownBy(() -> userService.signUp("user123", rawPw, dto)) .isInstanceOf(CoreException.class) .hasMessageContaining("생년월일을 포함할 수 없습니다"); } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java index afb8eaf8..5f4a92f8 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java @@ -5,7 +5,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import com.loopers.application.UserService; +import com.loopers.domain.UserService; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; From 00efb4696740a773f715eda10e54b9c4788fb292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Tue, 3 Feb 2026 22:36:47 +0900 Subject: [PATCH 21/49] =?UTF-8?q?test=20:=20slice=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/{UserApiE2ETest.java => UserControllerTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apps/commerce-api/src/test/java/com/loopers/interfaces/api/{UserApiE2ETest.java => UserControllerTest.java} (99%) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java similarity index 99% rename from apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java rename to apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 5f4a92f8..f7e97723 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -16,7 +16,7 @@ import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(UsersController.class) -class UserApiE2ETest { +class UserControllerTest { @Autowired private MockMvc mockMvc; From f31a7bb8d25036aebfc834a4548042e96942d6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Wed, 4 Feb 2026 23:23:24 +0900 Subject: [PATCH 22/49] =?UTF-8?q?fix=20:=20=EC=88=9C=ED=99=98=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{UserJpaRepositoryImpl.java => UserRepositoryImpl.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apps/commerce-api/src/main/java/com/loopers/infrastructure/{UserJpaRepositoryImpl.java => UserRepositoryImpl.java} (86%) diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java similarity index 86% rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepositoryImpl.java rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java index 1e0032e2..761fe288 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java @@ -7,7 +7,7 @@ @RequiredArgsConstructor @Component -public class UserJpaRepositoryImpl implements UserRepository { +public class UserRepositoryImpl implements UserRepository { private final UserJpaRepository userJpaRepository; From 402838e2c51dd12b0d1efc641bc3aebfa15bbecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Wed, 4 Feb 2026 23:26:29 +0900 Subject: [PATCH 23/49] =?UTF-8?q?refactor=20:=20SignCommand=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/SignUpCommand.java | 16 +++++++++++++ .../java/com/loopers/domain/UserModel.java | 12 +++++----- .../java/com/loopers/domain/UserService.java | 13 +++++----- .../interfaces/api/UsersController.java | 10 +++++++- .../loopers/application/UserServiceTest.java | 24 ++++++++++++------- 5 files changed, 53 insertions(+), 22 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/SignUpCommand.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/SignUpCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/SignUpCommand.java new file mode 100644 index 00000000..48a72547 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/SignUpCommand.java @@ -0,0 +1,16 @@ +package com.loopers.application; + +import java.time.LocalDate; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class SignUpCommand { + + private final String loginId; + private final String loginPw; + private final LocalDate birthDate; + private final String name; + private final String email; +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java index 0c2d4e69..8f5e752d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java @@ -1,6 +1,6 @@ package com.loopers.domain; -import com.loopers.interfaces.api.UsersSignUpRequestDto; +import com.loopers.application.SignUpCommand; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -48,13 +48,13 @@ public UserModel(String loginId, String encodedPw, LocalDate birthDate, String n this.email = email; } - public static UserModel create(String loginId, String encodedPw, UsersSignUpRequestDto requestDto) { + public static UserModel create(SignUpCommand command, String encodedPw) { return UserModel.builder() - .loginId(loginId) + .loginId(command.getLoginId()) .encodedPw(encodedPw) - .birthDate(requestDto.getBirthDate()) - .name(requestDto.getName()) - .email(requestDto.getEmail()) + .birthDate(command.getBirthDate()) + .name(command.getName()) + .email(command.getEmail()) .build(); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 73fb9b5d..5fb8f90d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -1,6 +1,6 @@ package com.loopers.domain; -import com.loopers.interfaces.api.UsersSignUpRequestDto; +import com.loopers.application.SignUpCommand; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import java.time.LocalDate; @@ -14,15 +14,14 @@ public class UserService { private final PasswordEncoder passwordEncoder; - public void signUp(String loginId, String loginPw, UsersSignUpRequestDto requestDto) { - validatePasswordContent(loginPw, requestDto.getBirthDate()); + public void signUp(SignUpCommand command) { + validatePasswordContent(command.getLoginPw(), command.getBirthDate()); - String encodedPw = passwordEncoder.encode(loginPw); + String encodedPw = passwordEncoder.encode(command.getLoginPw()); UserModel.create( - loginId, - encodedPw, - requestDto + command, + encodedPw ); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 492b6a96..60b439ba 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,5 +1,6 @@ package com.loopers.interfaces.api; +import com.loopers.application.SignUpCommand; import com.loopers.domain.UserService; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -32,7 +33,14 @@ public ApiResponse signUp( String loginPw, @Valid @RequestBody UsersSignUpRequestDto requestDto ) { - userService.signUp(loginId, loginPw, requestDto); + SignUpCommand command = new SignUpCommand( + loginId, + loginPw, + requestDto.getBirthDate(), + requestDto.getName(), + requestDto.getEmail() + ); + userService.signUp(command); return ApiResponse.success("ok"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java index 48ce3a29..3ff246e5 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java @@ -26,21 +26,23 @@ public class UserServiceTest { @Mock private PasswordEncoder passwordEncoder; - private UsersSignUpRequestDto createDto(LocalDate birthDate) { - return new UsersSignUpRequestDto(birthDate, "email@test.com", "kkk@gmail.com"); - } - @Test @DisplayName("회원가입 성공 테스트") void success_signup() { String rawPw = "securePassword!@"; String encodedPw = "encoded_hash"; - UsersSignUpRequestDto dto = createDto(LocalDate.of(1995, 1, 1)); + SignUpCommand signUpCommand = new SignUpCommand( + "user123", + rawPw, + LocalDate.of(1995, 1, 1), + "kim", + "yk@naver.com" + ); given(passwordEncoder.encode(rawPw)).willReturn(encodedPw); - userService.signUp("user123", rawPw, dto); + userService.signUp(signUpCommand); verify(passwordEncoder, times(1)).encode(rawPw); } @@ -50,9 +52,15 @@ void success_signup() { void fail_with_birthDate() { String rawPw = "pw19911203!!"; // 생일 포함 - UsersSignUpRequestDto dto = createDto(LocalDate.of(1991,12, 3)); + SignUpCommand signUpCommand = new SignUpCommand( + "user123", + rawPw, + LocalDate.of(1991, 12, 3), + "kim", + "yk@naver.com" + ); - assertThatThrownBy(() -> userService.signUp("user123", rawPw, dto)) + assertThatThrownBy(() -> userService.signUp(signUpCommand)) .isInstanceOf(CoreException.class) .hasMessageContaining("생년월일을 포함할 수 없습니다"); } From b8d02c6d4e9da8e78c456cb3df158ccf14cf9be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 00:07:44 +0900 Subject: [PATCH 24/49] =?UTF-8?q?test=20:=20user=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=BD=94=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 --- .../java/com/loopers/domain/UserModel.java | 46 ++++++++--- .../com/loopers/domain/UserModelTest.java | 79 +++++++++++++++++++ 2 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java index 8f5e752d..f1d4ad27 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java @@ -1,22 +1,26 @@ package com.loopers.domain; import com.loopers.application.SignUpCommand; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.time.LocalDate; -import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; @Entity @Table(name = "users") @NoArgsConstructor +@Getter public class UserModel extends BaseEntity { - @Id @GeneratedValue + @Id + @GeneratedValue private Long id; @Comment("아이디") @@ -39,22 +43,40 @@ public class UserModel extends BaseEntity { @Column(name = "email", nullable = false) private String email; - @Builder - public UserModel(String loginId, String encodedPw, LocalDate birthDate, String name, String email) { + private UserModel(String loginId, String password, LocalDate birthDate, String name, String email) { this.loginId = loginId; - this.password = encodedPw; + this.password = password; this.birthDate = birthDate; this.name = name; this.email = email; } + @Override + protected void guard() { + if (loginId == null || loginId.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "로그인 ID는 필수입니다."); + } + if (password == null || password.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 필수입니다."); + } + if (birthDate == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 필수입니다."); + } + if (name == null || name.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "이름은 필수입니다."); + } + if (email == null || email.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 필수입니다."); + } + } + public static UserModel create(SignUpCommand command, String encodedPw) { - return UserModel.builder() - .loginId(command.getLoginId()) - .encodedPw(encodedPw) - .birthDate(command.getBirthDate()) - .name(command.getName()) - .email(command.getEmail()) - .build(); + return new UserModel( + command.getLoginId(), + encodedPw, + command.getBirthDate(), + command.getName(), + command.getEmail() + ); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java new file mode 100644 index 00000000..fb786b10 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java @@ -0,0 +1,79 @@ +package com.loopers.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.loopers.application.SignUpCommand; +import com.loopers.support.error.CoreException; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class UserModelTest { + + @DisplayName("UserModel 생성 테스트") + @Test + void success_create_userModel() { + SignUpCommand command = new SignUpCommand( + "user123", + "rawPassword", + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + String encodedPw = "encoded_hash"; + + UserModel user = UserModel.create(command, encodedPw); + + assertThat(user.getLoginId()).isEqualTo(command.getLoginId()); + assertThat(user.getPassword()).isEqualTo(encodedPw); + assertThat(user.getBirthDate()).isEqualTo(command.getBirthDate()); + assertThat(user.getName()).isEqualTo(command.getName()); + assertThat(user.getEmail()).isEqualTo(command.getEmail()); + } + + @DisplayName("loginId가 null이면 guard에서 예외가 발생한다.") + @Test + void guard_fail_when_loginId_is_null() { + SignUpCommand command = new SignUpCommand(null, "rawPassword", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); + UserModel user = UserModel.create(command, "encoded_hash"); + + assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); + } + + @DisplayName("password가 null이면 guard에서 예외가 발생한다.") + @Test + void guard_fail_when_password_is_null() { + SignUpCommand command = new SignUpCommand("user123", "rawPassword", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); + UserModel user = UserModel.create(command, null); + + assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); + } + + @DisplayName("birthDate가 null이면 guard에서 예외가 발생한다.") + @Test + void guard_fail_when_birthDate_is_null() { + SignUpCommand command = new SignUpCommand("user123", "rawPassword", null, "김용권", "yk@google.com"); + UserModel user = UserModel.create(command, "encoded_hash"); + + assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); + } + + @DisplayName("name이 null이면 guard에서 예외가 발생한다.") + @Test + void guard_fail_when_name_is_null() { + SignUpCommand command = new SignUpCommand("user123", "rawPassword", LocalDate.of(1991, 12, 3), null, "yk@google.com"); + UserModel user = UserModel.create(command, "encoded_hash"); + + assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); + } + + @DisplayName("email이 null이면 guard에서 예외가 발생한다.") + @Test + void guard_fail_when_email_is_null() { + SignUpCommand command = new SignUpCommand("user123", "rawPassword", LocalDate.of(1991, 12, 3), "김용권", null); + UserModel user = UserModel.create(command, "encoded_hash"); + + assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); + } +} From 8609eaf6b00e1f6832c49ef023d9fb9caa2d30b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 00:27:42 +0900 Subject: [PATCH 25/49] =?UTF-8?q?test=20:=20users=20http=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 --- http/commerce-api/users.http | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 http/commerce-api/users.http diff --git a/http/commerce-api/users.http b/http/commerce-api/users.http new file mode 100644 index 00000000..aaba0b98 --- /dev/null +++ b/http/commerce-api/users.http @@ -0,0 +1,11 @@ +### 회원가입 +POST {{commerce-api}}/users +Content-Type: application/json +X-Loopers-LoginId: user123 +X-Loopers-LoginPw: Password1! + +{ + "birthDate": "1991-12-03", + "name": "김용권", + "email": "yk@google.com" +} From 4320bc175f0f593184eb9d316389601dde9100f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 00:31:24 +0900 Subject: [PATCH 26/49] =?UTF-8?q?feat=20:=20user=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=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 --- .../src/main/java/com/loopers/domain/UserService.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 5fb8f90d..60fc37a2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -3,6 +3,7 @@ import com.loopers.application.SignUpCommand; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import jakarta.transaction.Transactional; import java.time.LocalDate; import lombok.AllArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @@ -13,15 +14,19 @@ public class UserService { private final PasswordEncoder passwordEncoder; + private final UserRepository userRepository; + @Transactional public void signUp(SignUpCommand command) { validatePasswordContent(command.getLoginPw(), command.getBirthDate()); String encodedPw = passwordEncoder.encode(command.getLoginPw()); - UserModel.create( - command, - encodedPw + userRepository.save( + UserModel.create( + command, + encodedPw + ) ); } From 5b45d32b61681b58dd693bf220aa1fcc1c2d2ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 09:57:58 +0900 Subject: [PATCH 27/49] =?UTF-8?q?refactor=20:=20=EA=B8=B0=EC=A1=B4=20servi?= =?UTF-8?q?ce=20=EC=BD=94=EB=93=9C=EB=A5=BC=20facade=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/application/UserFacade.java | 44 +++++++++++++++++++ .../java/com/loopers/domain/UserService.java | 31 +------------ .../interfaces/api/UsersController.java | 6 +-- ...erServiceTest.java => UserFacadeTest.java} | 16 ++++--- .../interfaces/api/UserControllerTest.java | 4 +- 5 files changed, 62 insertions(+), 39 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java rename apps/commerce-api/src/test/java/com/loopers/application/{UserServiceTest.java => UserFacadeTest.java} (81%) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java new file mode 100644 index 00000000..8439faeb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java @@ -0,0 +1,44 @@ +package com.loopers.application; + +import com.loopers.domain.UserModel; +import com.loopers.domain.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public class UserFacade { + + private final PasswordEncoder passwordEncoder; + private final UserService userService; + + public void signUp(SignUpCommand command) { + // 만약 또다른 검증 조건이 생긴다면 + validatePasswordContent(command.getLoginPw(), command.getBirthDate()); + + String encodedPw = passwordEncoder.encode(command.getLoginPw()); + + userService.save( + UserModel.create( + command, + encodedPw + ) + ); + } + + private void validatePasswordContent(String password, LocalDate birthDate) { + if (password == null || birthDate == null) { + return; + } + + String birthStr = birthDate.toString().replace("-", ""); + + if (password.contains(birthStr)) { + throw new CoreException(ErrorType.NOT_INCLUDE_BIRTH_IN_PASSWORD); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 60fc37a2..3c1e96fc 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -1,44 +1,17 @@ package com.loopers.domain; -import com.loopers.application.SignUpCommand; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; import jakarta.transaction.Transactional; -import java.time.LocalDate; import lombok.AllArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service @AllArgsConstructor public class UserService { - private final PasswordEncoder passwordEncoder; private final UserRepository userRepository; @Transactional - public void signUp(SignUpCommand command) { - validatePasswordContent(command.getLoginPw(), command.getBirthDate()); - - String encodedPw = passwordEncoder.encode(command.getLoginPw()); - - userRepository.save( - UserModel.create( - command, - encodedPw - ) - ); - } - - private void validatePasswordContent(String password, LocalDate birthDate) { - if (password == null || birthDate == null) { - return; - } - - String birthStr = birthDate.toString().replace("-", ""); - - if (password.contains(birthStr)) { - throw new CoreException(ErrorType.NOT_INCLUDE_BIRTH_IN_PASSWORD); - } + public UserModel save(UserModel userModel) { + return userRepository.save(userModel); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 60b439ba..d613c600 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,7 +1,7 @@ package com.loopers.interfaces.api; import com.loopers.application.SignUpCommand; -import com.loopers.domain.UserService; +import com.loopers.application.UserFacade; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; @@ -20,7 +20,7 @@ @AllArgsConstructor public class UsersController { - private final UserService userService; + private final UserFacade userFacade; @PostMapping public ApiResponse signUp( @@ -40,7 +40,7 @@ public ApiResponse signUp( requestDto.getName(), requestDto.getEmail() ); - userService.signUp(command); + userFacade.signUp(command); return ApiResponse.success("ok"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java similarity index 81% rename from apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java rename to apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java index 3ff246e5..111279ad 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java @@ -1,12 +1,13 @@ package com.loopers.application; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import com.loopers.domain.UserModel; import com.loopers.domain.UserService; -import com.loopers.interfaces.api.UsersSignUpRequestDto; import com.loopers.support.error.CoreException; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; @@ -18,14 +19,17 @@ import org.springframework.security.crypto.password.PasswordEncoder; @ExtendWith(MockitoExtension.class) -public class UserServiceTest { +public class UserFacadeTest { @InjectMocks - private UserService userService; + private UserFacade userFacade; @Mock private PasswordEncoder passwordEncoder; + @Mock + private UserService userService; + @Test @DisplayName("회원가입 성공 테스트") void success_signup() { @@ -41,10 +45,12 @@ void success_signup() { ); given(passwordEncoder.encode(rawPw)).willReturn(encodedPw); + given(userService.save(any(UserModel.class))).willReturn(any(UserModel.class)); - userService.signUp(signUpCommand); + userFacade.signUp(signUpCommand); verify(passwordEncoder, times(1)).encode(rawPw); + verify(userService, times(1)).save(any(UserModel.class)); } @Test @@ -60,7 +66,7 @@ void fail_with_birthDate() { "yk@naver.com" ); - assertThatThrownBy(() -> userService.signUp(signUpCommand)) + assertThatThrownBy(() -> userFacade.signUp(signUpCommand)) .isInstanceOf(CoreException.class) .hasMessageContaining("생년월일을 포함할 수 없습니다"); } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index f7e97723..c0dbda83 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -5,7 +5,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import com.loopers.domain.UserService; +import com.loopers.application.UserFacade; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -25,7 +25,7 @@ class UserControllerTest { private ObjectMapper objectMapper; @MockitoBean - private UserService userService; + private UserFacade userFacade; @Test @DisplayName("회원가입 API 호출 테스트") From 1679ffeaa499c88706623742a990c64eca91c333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 09:59:24 +0900 Subject: [PATCH 28/49] =?UTF-8?q?docs=20:=20=EC=A3=BC=EC=84=9D=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 --- .../src/main/java/com/loopers/application/UserFacade.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java index 8439faeb..cd77a53b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java @@ -17,7 +17,7 @@ public class UserFacade { private final UserService userService; public void signUp(SignUpCommand command) { - // 만약 또다른 검증 조건이 생긴다면 + // 만약 또다른 검증 조건이 생긴다면, 클래스로 분리 validatePasswordContent(command.getLoginPw(), command.getBirthDate()); String encodedPw = passwordEncoder.encode(command.getLoginPw()); From 4b0b620de617c9ebf271c703b2f40c612470b4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 12:03:13 +0900 Subject: [PATCH 29/49] =?UTF-8?q?test=20:=20=EC=9D=B4=EB=AF=B8=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EB=90=98=EC=96=B4=EC=9E=88=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=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 가# 웬만하면 작성 --- .../com/loopers/application/UserFacade.java | 7 ++++++- .../java/com/loopers/domain/UserService.java | 4 ++++ .../loopers/application/UserFacadeTest.java | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java index cd77a53b..c7e68e5c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java @@ -17,7 +17,12 @@ public class UserFacade { private final UserService userService; public void signUp(SignUpCommand command) { - // 만약 또다른 검증 조건이 생긴다면, 클래스로 분리 + if (userService.existsByEmail(command.getEmail())) { + throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입되어 있는 아이디 입니다."); + } + + // TODO + // 만약 또다른 패스워드 검증 조건이 생기거나 다른 클래스에서도 같이 사용한다면 클래스로 분리해야함 validatePasswordContent(command.getLoginPw(), command.getBirthDate()); String encodedPw = passwordEncoder.encode(command.getLoginPw()); diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 3c1e96fc..74fef8e2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -14,4 +14,8 @@ public class UserService { public UserModel save(UserModel userModel) { return userRepository.save(userModel); } + + public Boolean existsByEmail(String email) { + return true; + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java index 111279ad..330ca9f4 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java @@ -70,4 +70,24 @@ void fail_with_birthDate() { .isInstanceOf(CoreException.class) .hasMessageContaining("생년월일을 포함할 수 없습니다"); } + + @Test + @DisplayName("회원가입 실패 - 이미 가입되어 있으면 예외 발생") + void fail_already_signUp() { + + String rawPw = "securePassword!@"; + SignUpCommand signUpCommand = new SignUpCommand( + "user123", + rawPw, + LocalDate.of(1995, 1, 1), + "kim", + "yk@naver.com" + ); + + given(userService.existsByEmail("yk@naver.com")).willReturn(true); + + assertThatThrownBy(() -> userFacade.signUp(signUpCommand)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("이미 가입되어 있는 아이디 입니다."); + } } From bc8954162d76b84642454ea828ebab8981d759b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 12:06:56 +0900 Subject: [PATCH 30/49] =?UTF-8?q?feat=20:=20=ED=99=98=EC=9E=90=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=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/com/loopers/domain/UserRepository.java | 1 + .../src/main/java/com/loopers/domain/UserService.java | 2 +- .../java/com/loopers/infrastructure/UserJpaRepository.java | 1 + .../java/com/loopers/infrastructure/UserRepositoryImpl.java | 5 +++++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java index 3e87373c..d21eb543 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java @@ -6,4 +6,5 @@ public interface UserRepository { UserModel save(UserModel userModel); + Boolean existsByEmail(String email); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 74fef8e2..47c42545 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -16,6 +16,6 @@ public UserModel save(UserModel userModel) { } public Boolean existsByEmail(String email) { - return true; + return userRepository.existsByEmail(email); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java index b94483b5..14c2cce1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java @@ -5,4 +5,5 @@ public interface UserJpaRepository extends JpaRepository { + Boolean existsByEmail(String email); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java index 761fe288..9abf6a69 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java @@ -15,4 +15,9 @@ public class UserRepositoryImpl implements UserRepository { public UserModel save(UserModel userModel) { return userJpaRepository.save(userModel); } + + @Override + public Boolean existsByEmail(String email) { + return userJpaRepository.existsByEmail(email); + } } From f6f3e3a19fd4692ce260b06a5cfac140c7bd5d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 14:31:04 +0900 Subject: [PATCH 31/49] =?UTF-8?q?fix=20:=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=EB=8A=94=20=EC=88=AB=EC=9E=90+=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EB=A7=8C=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD(=ED=8A=B9=EC=88=98=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=20x)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UsersController.java | 3 ++- .../interfaces/api/UserControllerTest.java | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index d613c600..18cbfa96 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -24,7 +24,8 @@ public class UsersController { @PostMapping public ApiResponse signUp( - @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID) @NotBlank(message = "로그인 ID는 필수입니다.") String loginId, + @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID) @NotBlank(message = "로그인 ID는 필수입니다.") + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "로그인 ID는 영문 대소문자, 숫자만 사용 가능합니다.") String loginId, @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW) @NotBlank(message = "비밀번호는 필수입니다.") @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") @Pattern( regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index c0dbda83..4655464b 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -333,6 +333,25 @@ void fail_when_loginId_header_missing() throws Exception { .andExpect(status().isBadRequest()); } + @Test + @DisplayName("로그인 ID에 특수문자가 포함되면 400 Bad Request를 반환한다") + void fail_when_loginId_contains_special_character() throws Exception { + UsersSignUpRequestDto requestBody = new UsersSignUpRequestDto( + LocalDate.of(1991, 12, 3), + "김용권", + "yk@google.com" + ); + + String json = objectMapper.writeValueAsString(requestBody); + + mockMvc.perform(post("/users") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim!") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + @Test @DisplayName("X-Loopers-LoginPw 헤더가 없으면 400 Bad Request를 반환한다") void fail_when_loginPw_header_missing() throws Exception { From 84b19be84d1f6afb3971d0200ac1d21f1d7fba48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 15:15:16 +0900 Subject: [PATCH 32/49] =?UTF-8?q?refactor=20:=20api=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20+=20application=20=EA=B3=84=EC=B8=B5=20dto?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/application/UserFacade.java | 6 ++++-- .../java/com/loopers/application/UserInfo.java | 14 ++++++++++++++ .../main/java/com/loopers/interfaces/UserDto.java | 14 ++++++++++++++ .../loopers/interfaces/api/UsersController.java | 10 +++++++--- .../com/loopers/application/UserFacadeTest.java | 3 ++- .../loopers/interfaces/api/UserControllerTest.java | 13 +++++++++++-- 6 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java index c7e68e5c..36f678b3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java @@ -16,7 +16,7 @@ public class UserFacade { private final PasswordEncoder passwordEncoder; private final UserService userService; - public void signUp(SignUpCommand command) { + public UserInfo signUp(SignUpCommand command) { if (userService.existsByEmail(command.getEmail())) { throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입되어 있는 아이디 입니다."); } @@ -27,12 +27,14 @@ public void signUp(SignUpCommand command) { String encodedPw = passwordEncoder.encode(command.getLoginPw()); - userService.save( + UserModel userModel = userService.save( UserModel.create( command, encodedPw ) ); + + return UserInfo.from(userModel); } private void validatePasswordContent(String password, LocalDate birthDate) { diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java new file mode 100644 index 00000000..edb72d3d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java @@ -0,0 +1,14 @@ +package com.loopers.application; + +import com.loopers.domain.UserModel; + +public record UserInfo(Long id, String name, String email) { + + public static UserInfo from(UserModel userModel) { + return new UserInfo( + userModel.getId(), + userModel.getName(), + userModel.getEmail() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java new file mode 100644 index 00000000..fe45466e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java @@ -0,0 +1,14 @@ +package com.loopers.interfaces; + +import com.loopers.application.UserInfo; + +public class UserDto { + + public record SignUpResponse(Long id) { + public static SignUpResponse from(UserInfo userInfo) { + return new SignUpResponse( + userInfo.id() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 18cbfa96..170d1c4c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -2,6 +2,8 @@ import com.loopers.application.SignUpCommand; import com.loopers.application.UserFacade; +import com.loopers.application.UserInfo; +import com.loopers.interfaces.UserDto; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; @@ -23,7 +25,7 @@ public class UsersController { private final UserFacade userFacade; @PostMapping - public ApiResponse signUp( + public ApiResponse signUp( @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID) @NotBlank(message = "로그인 ID는 필수입니다.") @Pattern(regexp = "^[A-Za-z0-9]+$", message = "로그인 ID는 영문 대소문자, 숫자만 사용 가능합니다.") String loginId, @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW) @NotBlank(message = "비밀번호는 필수입니다.") @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") @@ -41,7 +43,9 @@ public ApiResponse signUp( requestDto.getName(), requestDto.getEmail() ); - userFacade.signUp(command); - return ApiResponse.success("ok"); + + UserInfo userInfo = userFacade.signUp(command); + + return ApiResponse.success(UserDto.SignUpResponse.from(userInfo)); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java index 330ca9f4..8e812aaf 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java @@ -45,7 +45,8 @@ void success_signup() { ); given(passwordEncoder.encode(rawPw)).willReturn(encodedPw); - given(userService.save(any(UserModel.class))).willReturn(any(UserModel.class)); + UserModel savedUser = UserModel.create(signUpCommand, encodedPw); + given(userService.save(any(UserModel.class))).willReturn(savedUser); userFacade.signUp(signUpCommand); diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 4655464b..e904f9b9 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -1,11 +1,16 @@ package com.loopers.interfaces.api; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import com.loopers.application.SignUpCommand; import com.loopers.application.UserFacade; +import com.loopers.application.UserInfo; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -36,14 +41,18 @@ void success_signup() throws Exception { "yk@google.com" ); + UserInfo userInfo = new UserInfo(1L, "김용권", "yk@google.com"); + given(userFacade.signUp(any(SignUpCommand.class))).willReturn(userInfo); + String json = objectMapper.writeValueAsString(requestBody); mockMvc.perform(post("/users") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") - .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "Password1!") .content(json)) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").value(1)); } @DisplayName("회원가입 API 실패 테스트") From 3aa334910f0b0d4bbde9a9a243b8c6ba5ce8420b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 16:15:44 +0900 Subject: [PATCH 33/49] =?UTF-8?q?refactor=20:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20API=20=ED=97=A4=EB=8D=94=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(ArgumentRes?= =?UTF-8?q?olver=20=ED=99=9C=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/config/WebMvcConfig.java | 20 ++++++++ .../com/loopers/domain/UserRepository.java | 2 - .../infrastructure/UserJpaRepository.java | 3 ++ .../interfaces/api/CredentialsHeaders.java | 28 +++++++++++ .../CredentialsHeadersArgumentResolver.java | 49 +++++++++++++++++++ .../interfaces/api/UsersController.java | 19 ++----- .../interfaces/api/UserControllerTest.java | 8 +++ 7 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeaders.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeadersArgumentResolver.java diff --git a/apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java new file mode 100644 index 00000000..06989cfb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java @@ -0,0 +1,20 @@ +package com.loopers.config; + +import com.loopers.interfaces.api.CredentialsHeadersArgumentResolver; +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; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final CredentialsHeadersArgumentResolver credentialsHeadersArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(credentialsHeadersArgumentResolver); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java index d21eb543..9310a8da 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java @@ -1,7 +1,5 @@ package com.loopers.domain; -import java.util.Optional; - public interface UserRepository { UserModel save(UserModel userModel); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java index 14c2cce1..8cd5c03e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java @@ -1,9 +1,12 @@ package com.loopers.infrastructure; import com.loopers.domain.UserModel; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface UserJpaRepository extends JpaRepository { Boolean existsByEmail(String email); + + Optional findByLoginId(String loginId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeaders.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeaders.java new file mode 100644 index 00000000..e29333f8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeaders.java @@ -0,0 +1,28 @@ +package com.loopers.interfaces.api; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 로그인 ID/비밀번호를 담는 헤더 값 DTO. + * Argument Resolver를 통해 요청 헤더에서 바인딩·검증 후 주입된다. + */ +@Getter +@AllArgsConstructor +public class CredentialsHeaders { + + @NotBlank(message = "로그인 ID는 필수입니다.") + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "로그인 ID는 영문 대소문자, 숫자만 사용 가능합니다.") + private String loginId; + + @NotBlank(message = "비밀번호는 필수입니다.") + @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") + @Pattern( + regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", + message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." + ) + private String loginPw; +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeadersArgumentResolver.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeadersArgumentResolver.java new file mode 100644 index 00000000..d024ae74 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/CredentialsHeadersArgumentResolver.java @@ -0,0 +1,49 @@ +package com.loopers.interfaces.api; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +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; + +@Component +@RequiredArgsConstructor +public class CredentialsHeadersArgumentResolver implements HandlerMethodArgumentResolver { + + private final Validator validator; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType() == CredentialsHeaders.class; + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + if (request == null) { + throw new IllegalStateException("HttpServletRequest not available"); + } + + String loginId = request.getHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID); + String loginPw = request.getHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW); + + CredentialsHeaders headers = new CredentialsHeaders(loginId, loginPw); + Set> violations = validator.validate(headers); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + return headers; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 170d1c4c..f04f960e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -5,18 +5,12 @@ import com.loopers.application.UserInfo; import com.loopers.interfaces.UserDto; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Validated @RestController @RequestMapping("/users") @AllArgsConstructor @@ -26,19 +20,12 @@ public class UsersController { @PostMapping public ApiResponse signUp( - @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID) @NotBlank(message = "로그인 ID는 필수입니다.") - @Pattern(regexp = "^[A-Za-z0-9]+$", message = "로그인 ID는 영문 대소문자, 숫자만 사용 가능합니다.") String loginId, - @RequestHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW) @NotBlank(message = "비밀번호는 필수입니다.") @Size(min = 8, max = 16, message = "8~16자로 입력해주세요.") - @Pattern( - regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", - message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." - ) - String loginPw, + CredentialsHeaders credentialsHeaders, @Valid @RequestBody UsersSignUpRequestDto requestDto ) { SignUpCommand command = new SignUpCommand( - loginId, - loginPw, + credentialsHeaders.getLoginId(), + credentialsHeaders.getLoginPw(), requestDto.getBirthDate(), requestDto.getName(), requestDto.getEmail() diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index e904f9b9..7e4481fa 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -11,16 +11,21 @@ import com.loopers.application.SignUpCommand; import com.loopers.application.UserFacade; import com.loopers.application.UserInfo; +import com.loopers.domain.UserService; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import com.loopers.config.WebMvcConfig; + @WebMvcTest(UsersController.class) +@Import({WebMvcConfig.class, CredentialsHeadersArgumentResolver.class}) class UserControllerTest { @Autowired @@ -32,6 +37,9 @@ class UserControllerTest { @MockitoBean private UserFacade userFacade; + @MockitoBean + private UserService userService; + @Test @DisplayName("회원가입 API 호출 테스트") void success_signup() throws Exception { From c12224017cbb7c30c4b5f2a24c2764f6a73a8cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 16:51:25 +0900 Subject: [PATCH 34/49] =?UTF-8?q?feat=20:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20mock=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/interfaces/api/UsersController.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index f04f960e..b45e0d4c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -1,11 +1,13 @@ package com.loopers.interfaces.api; +import com.loopers.application.AuthUserPrincipal; import com.loopers.application.SignUpCommand; import com.loopers.application.UserFacade; import com.loopers.application.UserInfo; import com.loopers.interfaces.UserDto; import jakarta.validation.Valid; import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -35,4 +37,10 @@ public ApiResponse signUp( return ApiResponse.success(UserDto.SignUpResponse.from(userInfo)); } + + @GetMapping("/me") + public ApiResponse getMe() { + + return ApiResponse.success(UserDto.MyInfoResponse.mock()); + } } From cd56a8ad7a054f8c562445e43dbebb7db2f7bd16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 17:04:47 +0900 Subject: [PATCH 35/49] =?UTF-8?q?feat=20:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20ArgumentR?= =?UTF-8?q?esolver=EC=99=80=20service=20=EA=B3=84=EC=B8=B5=EB=82=B4=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94?= =?UTF-8?q?=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 --- .../application/AuthUserPrincipal.java | 23 +++++++++ .../java/com/loopers/config/WebMvcConfig.java | 3 ++ .../java/com/loopers/domain/UserService.java | 13 +++++ .../com/loopers/interfaces/api/AuthUser.java | 16 ++++++ .../api/AuthUserArgumentResolver.java | 51 +++++++++++++++++++ .../com/loopers/support/error/ErrorType.java | 3 +- 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/AuthUserPrincipal.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUser.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/AuthUserPrincipal.java b/apps/commerce-api/src/main/java/com/loopers/application/AuthUserPrincipal.java new file mode 100644 index 00000000..7c0086c2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/AuthUserPrincipal.java @@ -0,0 +1,23 @@ +package com.loopers.application; + +import com.loopers.domain.UserModel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 인증된 사용자의 식별 정보. 컨트롤러에서 "현재 사용자"를 식별하는 용도로 사용한다. + */ +@Getter +@AllArgsConstructor +public class AuthUserPrincipal { + + private final Long id; + private final String loginId; + + public static AuthUserPrincipal from(UserModel user) { + return new AuthUserPrincipal( + user.getId(), + user.getLoginId() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java index 06989cfb..c55b3d20 100644 --- a/apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java +++ b/apps/commerce-api/src/main/java/com/loopers/config/WebMvcConfig.java @@ -1,5 +1,6 @@ package com.loopers.config; +import com.loopers.interfaces.api.AuthUserArgumentResolver; import com.loopers.interfaces.api.CredentialsHeadersArgumentResolver; import java.util.List; import lombok.RequiredArgsConstructor; @@ -12,9 +13,11 @@ public class WebMvcConfig implements WebMvcConfigurer { private final CredentialsHeadersArgumentResolver credentialsHeadersArgumentResolver; + private final AuthUserArgumentResolver authUserArgumentResolver; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(credentialsHeadersArgumentResolver); + resolvers.add(authUserArgumentResolver); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 47c42545..977eb304 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -1,7 +1,10 @@ package com.loopers.domain; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service @@ -9,12 +12,22 @@ public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; @Transactional public UserModel save(UserModel userModel) { return userRepository.save(userModel); } + public UserModel authenticate(String loginId, String rawPassword) { + UserModel user = userRepository.findByLoginId(loginId) + .orElseThrow(() -> new CoreException(ErrorType.UNAUTHORIZED, "로그인 정보가 올바르지 않습니다.")); + if (!passwordEncoder.matches(rawPassword, user.getPassword())) { + throw new CoreException(ErrorType.UNAUTHORIZED, "로그인 정보가 올바르지 않습니다."); + } + return user; + } + public Boolean existsByEmail(String email) { return userRepository.existsByEmail(email); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUser.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUser.java new file mode 100644 index 00000000..e8eab205 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUser.java @@ -0,0 +1,16 @@ +package com.loopers.interfaces.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 인증된 사용자 인자를 주입받을 때 사용. + * {@link AuthUserArgumentResolver}가 헤더(X-Loopers-LoginId, X-Loopers-LoginPw)로 인증 후 + * {@link com.loopers.application.AuthUserPrincipal}을 주입한다. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthUser { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java new file mode 100644 index 00000000..b3ffb565 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java @@ -0,0 +1,51 @@ +package com.loopers.interfaces.api; + +import com.loopers.application.AuthUserPrincipal; +import com.loopers.domain.UserModel; +import com.loopers.domain.UserService; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +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; + +@Component +@RequiredArgsConstructor +public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { + + private final UserService userService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthUser.class) + && parameter.getParameterType().equals(AuthUserPrincipal.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + if (request == null) { + throw new IllegalStateException("HttpServletRequest not available"); + } + + String loginId = request.getHeader(LoopersHeaders.X_LOOPERS_LOGIN_ID); + String loginPw = request.getHeader(LoopersHeaders.X_LOOPERS_LOGIN_PW); + + if (loginId == null || loginId.isBlank() || loginPw == null || loginPw.isBlank()) { + throw new CoreException(ErrorType.UNAUTHORIZED, "인증 헤더가 누락되었습니다."); + } + + UserModel user = userService.authenticate(loginId, loginPw); + return AuthUserPrincipal.from(user); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java index dfae46a0..b8f29045 100644 --- a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java +++ b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java @@ -13,7 +13,8 @@ public enum ErrorType { NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."), CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."), - NOT_INCLUDE_BIRTH_IN_PASSWORD(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "비밀번호에 생년월일을 포함할 수 없습니다.") + NOT_INCLUDE_BIRTH_IN_PASSWORD(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "비밀번호에 생년월일을 포함할 수 없습니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.getReasonPhrase(), "인증이 필요합니다.") ; private final HttpStatus status; From bd6908bfc73d219399824845c156d9bae50bf13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 17:05:39 +0900 Subject: [PATCH 36/49] =?UTF-8?q?feat=20:=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EB=82=B4=EB=B6=80=20=EB=A1=9C=EC=A7=81=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 --- .../com/loopers/application/UserFacade.java | 20 +++++----- .../com/loopers/application/UserInfo.java | 7 +++- .../java/com/loopers/domain/UserModel.java | 11 +----- .../com/loopers/domain/UserRepository.java | 6 +++ .../java/com/loopers/domain/UserService.java | 12 ++++++ .../infrastructure/UserRepositoryImpl.java | 10 +++++ .../java/com/loopers/interfaces/UserDto.java | 13 +++++++ .../interfaces/api/UsersController.java | 6 +-- .../loopers/application/UserFacadeTest.java | 19 ++++------ .../com/loopers/domain/UserModelTest.java | 37 +++++++------------ .../interfaces/api/UserControllerTest.java | 4 +- 11 files changed, 85 insertions(+), 60 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java index 36f678b3..151164dc 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java @@ -6,14 +6,12 @@ import com.loopers.support.error.ErrorType; import java.time.LocalDate; import lombok.AllArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @Component @AllArgsConstructor public class UserFacade { - private final PasswordEncoder passwordEncoder; private final UserService userService; public UserInfo signUp(SignUpCommand command) { @@ -25,18 +23,22 @@ public UserInfo signUp(SignUpCommand command) { // 만약 또다른 패스워드 검증 조건이 생기거나 다른 클래스에서도 같이 사용한다면 클래스로 분리해야함 validatePasswordContent(command.getLoginPw(), command.getBirthDate()); - String encodedPw = passwordEncoder.encode(command.getLoginPw()); - - UserModel userModel = userService.save( - UserModel.create( - command, - encodedPw - ) + UserModel userModel = userService.createUser( + command.getLoginId(), + command.getLoginPw(), + command.getBirthDate(), + command.getName(), + command.getEmail() ); return UserInfo.from(userModel); } + public UserInfo getMyInfo(Long userId) { + UserModel user = userService.findById(userId); + return UserInfo.from(user); + } + private void validatePasswordContent(String password, LocalDate birthDate) { if (password == null || birthDate == null) { return; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java index edb72d3d..8ed8591e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java @@ -1,14 +1,17 @@ package com.loopers.application; import com.loopers.domain.UserModel; +import java.time.LocalDate; -public record UserInfo(Long id, String name, String email) { +public record UserInfo(Long id, String loginId, String name, String email, LocalDate birthDate) { public static UserInfo from(UserModel userModel) { return new UserInfo( userModel.getId(), + userModel.getLoginId(), userModel.getName(), - userModel.getEmail() + userModel.getEmail(), + userModel.getBirthDate() ); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java index f1d4ad27..f83190b7 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java @@ -1,6 +1,5 @@ package com.loopers.domain; -import com.loopers.application.SignUpCommand; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.Column; @@ -70,13 +69,7 @@ protected void guard() { } } - public static UserModel create(SignUpCommand command, String encodedPw) { - return new UserModel( - command.getLoginId(), - encodedPw, - command.getBirthDate(), - command.getName(), - command.getEmail() - ); + public static UserModel create(String loginId, String encodedPassword, LocalDate birthDate, String name, String email) { + return new UserModel(loginId, encodedPassword, birthDate, name, email); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java index 9310a8da..0a276c40 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java @@ -1,8 +1,14 @@ package com.loopers.domain; +import java.util.Optional; + public interface UserRepository { UserModel save(UserModel userModel); + Optional findById(Long id); + + Optional findByLoginId(String loginId); + Boolean existsByEmail(String email); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 977eb304..4ea81fe6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -19,6 +19,13 @@ public UserModel save(UserModel userModel) { return userRepository.save(userModel); } + @Transactional + public UserModel createUser(String loginId, String rawPassword, java.time.LocalDate birthDate, String name, String email) { + String encodedPassword = passwordEncoder.encode(rawPassword); + UserModel user = UserModel.create(loginId, encodedPassword, birthDate, name, email); + return userRepository.save(user); + } + public UserModel authenticate(String loginId, String rawPassword) { UserModel user = userRepository.findByLoginId(loginId) .orElseThrow(() -> new CoreException(ErrorType.UNAUTHORIZED, "로그인 정보가 올바르지 않습니다.")); @@ -31,4 +38,9 @@ public UserModel authenticate(String loginId, String rawPassword) { public Boolean existsByEmail(String email) { return userRepository.existsByEmail(email); } + + public UserModel findById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다.")); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java index 9abf6a69..6ec46b09 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java @@ -16,6 +16,16 @@ public UserModel save(UserModel userModel) { return userJpaRepository.save(userModel); } + @Override + public java.util.Optional findById(Long id) { + return userJpaRepository.findById(id); + } + + @Override + public java.util.Optional findByLoginId(String loginId) { + return userJpaRepository.findByLoginId(loginId); + } + @Override public Boolean existsByEmail(String email) { return userJpaRepository.existsByEmail(email); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java index fe45466e..69adcf0e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java @@ -1,6 +1,7 @@ package com.loopers.interfaces; import com.loopers.application.UserInfo; +import java.time.LocalDate; public class UserDto { @@ -11,4 +12,16 @@ public static SignUpResponse from(UserInfo userInfo) { ); } } + + public record MyInfoResponse(String loginId, String name, LocalDate birthDate, String email) { + + public static MyInfoResponse from(UserInfo userInfo) { + return new MyInfoResponse( + userInfo.loginId(), + userInfo.name(), + userInfo.birthDate(), + userInfo.email() + ); + } + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index b45e0d4c..74dc89ac 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -39,8 +39,8 @@ public ApiResponse signUp( } @GetMapping("/me") - public ApiResponse getMe() { - - return ApiResponse.success(UserDto.MyInfoResponse.mock()); + public ApiResponse getMe(@AuthUser AuthUserPrincipal authUser) { + UserInfo userInfo = userFacade.getMyInfo(authUser.getId()); + return ApiResponse.success(UserDto.MyInfoResponse.from(userInfo)); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java index 8e812aaf..d9d68678 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java @@ -1,7 +1,7 @@ package com.loopers.application; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -16,7 +16,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.crypto.password.PasswordEncoder; @ExtendWith(MockitoExtension.class) public class UserFacadeTest { @@ -24,9 +23,6 @@ public class UserFacadeTest { @InjectMocks private UserFacade userFacade; - @Mock - private PasswordEncoder passwordEncoder; - @Mock private UserService userService; @@ -35,23 +31,22 @@ public class UserFacadeTest { void success_signup() { String rawPw = "securePassword!@"; - String encodedPw = "encoded_hash"; + LocalDate birthDate = LocalDate.of(1995, 1, 1); SignUpCommand signUpCommand = new SignUpCommand( "user123", rawPw, - LocalDate.of(1995, 1, 1), + birthDate, "kim", "yk@naver.com" ); - given(passwordEncoder.encode(rawPw)).willReturn(encodedPw); - UserModel savedUser = UserModel.create(signUpCommand, encodedPw); - given(userService.save(any(UserModel.class))).willReturn(savedUser); + UserModel savedUser = UserModel.create("user123", "encoded_hash", birthDate, "kim", "yk@naver.com"); + given(userService.createUser(eq("user123"), eq(rawPw), eq(birthDate), eq("kim"), eq("yk@naver.com"))) + .willReturn(savedUser); userFacade.signUp(signUpCommand); - verify(passwordEncoder, times(1)).encode(rawPw); - verify(userService, times(1)).save(any(UserModel.class)); + verify(userService, times(1)).createUser(eq("user123"), eq(rawPw), eq(birthDate), eq("kim"), eq("yk@naver.com")); } @Test diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java index fb786b10..0499db88 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.loopers.application.SignUpCommand; import com.loopers.support.error.CoreException; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; @@ -14,29 +13,25 @@ class UserModelTest { @DisplayName("UserModel 생성 테스트") @Test void success_create_userModel() { - SignUpCommand command = new SignUpCommand( - "user123", - "rawPassword", - LocalDate.of(1991, 12, 3), - "김용권", - "yk@google.com" - ); + String loginId = "user123"; String encodedPw = "encoded_hash"; + LocalDate birthDate = LocalDate.of(1991, 12, 3); + String name = "김용권"; + String email = "yk@google.com"; - UserModel user = UserModel.create(command, encodedPw); + UserModel user = UserModel.create(loginId, encodedPw, birthDate, name, email); - assertThat(user.getLoginId()).isEqualTo(command.getLoginId()); + assertThat(user.getLoginId()).isEqualTo(loginId); assertThat(user.getPassword()).isEqualTo(encodedPw); - assertThat(user.getBirthDate()).isEqualTo(command.getBirthDate()); - assertThat(user.getName()).isEqualTo(command.getName()); - assertThat(user.getEmail()).isEqualTo(command.getEmail()); + assertThat(user.getBirthDate()).isEqualTo(birthDate); + assertThat(user.getName()).isEqualTo(name); + assertThat(user.getEmail()).isEqualTo(email); } @DisplayName("loginId가 null이면 guard에서 예외가 발생한다.") @Test void guard_fail_when_loginId_is_null() { - SignUpCommand command = new SignUpCommand(null, "rawPassword", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); - UserModel user = UserModel.create(command, "encoded_hash"); + UserModel user = UserModel.create(null, "encoded_hash", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); } @@ -44,8 +39,7 @@ void guard_fail_when_loginId_is_null() { @DisplayName("password가 null이면 guard에서 예외가 발생한다.") @Test void guard_fail_when_password_is_null() { - SignUpCommand command = new SignUpCommand("user123", "rawPassword", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); - UserModel user = UserModel.create(command, null); + UserModel user = UserModel.create("user123", null, LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); } @@ -53,8 +47,7 @@ void guard_fail_when_password_is_null() { @DisplayName("birthDate가 null이면 guard에서 예외가 발생한다.") @Test void guard_fail_when_birthDate_is_null() { - SignUpCommand command = new SignUpCommand("user123", "rawPassword", null, "김용권", "yk@google.com"); - UserModel user = UserModel.create(command, "encoded_hash"); + UserModel user = UserModel.create("user123", "encoded_hash", null, "김용권", "yk@google.com"); assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); } @@ -62,8 +55,7 @@ void guard_fail_when_birthDate_is_null() { @DisplayName("name이 null이면 guard에서 예외가 발생한다.") @Test void guard_fail_when_name_is_null() { - SignUpCommand command = new SignUpCommand("user123", "rawPassword", LocalDate.of(1991, 12, 3), null, "yk@google.com"); - UserModel user = UserModel.create(command, "encoded_hash"); + UserModel user = UserModel.create("user123", "encoded_hash", LocalDate.of(1991, 12, 3), null, "yk@google.com"); assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); } @@ -71,8 +63,7 @@ void guard_fail_when_name_is_null() { @DisplayName("email이 null이면 guard에서 예외가 발생한다.") @Test void guard_fail_when_email_is_null() { - SignUpCommand command = new SignUpCommand("user123", "rawPassword", LocalDate.of(1991, 12, 3), "김용권", null); - UserModel user = UserModel.create(command, "encoded_hash"); + UserModel user = UserModel.create("user123", "encoded_hash", LocalDate.of(1991, 12, 3), "김용권", null); assertThatThrownBy(user::guard).isInstanceOf(CoreException.class); } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 7e4481fa..5b3b2bc2 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -25,7 +25,7 @@ import com.loopers.config.WebMvcConfig; @WebMvcTest(UsersController.class) -@Import({WebMvcConfig.class, CredentialsHeadersArgumentResolver.class}) +@Import({WebMvcConfig.class, CredentialsHeadersArgumentResolver.class, AuthUserArgumentResolver.class}) class UserControllerTest { @Autowired @@ -49,7 +49,7 @@ void success_signup() throws Exception { "yk@google.com" ); - UserInfo userInfo = new UserInfo(1L, "김용권", "yk@google.com"); + UserInfo userInfo = new UserInfo(1L, "kim", "김용권", "yk@google.com", LocalDate.of(1991, 12, 3)); given(userFacade.signUp(any(SignUpCommand.class))).willReturn(userInfo); String json = objectMapper.writeValueAsString(requestBody); From 61c0372856b1fdd6cf090fc69453fcadd06d161e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 21:04:39 +0900 Subject: [PATCH 37/49] =?UTF-8?q?test=20:=20http=EC=97=90=20=EB=82=B4?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- http/commerce-api/users.http | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/http/commerce-api/users.http b/http/commerce-api/users.http index aaba0b98..5e758103 100644 --- a/http/commerce-api/users.http +++ b/http/commerce-api/users.http @@ -9,3 +9,9 @@ X-Loopers-LoginPw: Password1! "name": "김용권", "email": "yk@google.com" } + +### 내 정보 조회 +GET {{commerce-api}}/users/me +Content-Type: application/json +X-Loopers-LoginId: user123 +X-Loopers-LoginPw: Password1! From d9f704ca0f2ba06dfd027d6ff96d10e4a2b0dace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 21:05:03 +0900 Subject: [PATCH 38/49] =?UTF-8?q?feat=20:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EC=9D=B4=EB=A6=84=20=EB=A7=88?= =?UTF-8?q?=EC=A7=80=EB=A7=89=20=EB=B6=80=EB=B6=84=EC=9D=84=20=EB=A7=88?= =?UTF-8?q?=EC=8A=A4=ED=82=B9=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/interfaces/UserDto.java | 3 +- .../com/loopers/support/MaskingUtils.java | 13 ++++++ .../com/loopers/support/MaskingUtilsTest.java | 41 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/support/MaskingUtils.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/support/MaskingUtilsTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java index 69adcf0e..804a031a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java @@ -1,6 +1,7 @@ package com.loopers.interfaces; import com.loopers.application.UserInfo; +import com.loopers.support.MaskingUtils; import java.time.LocalDate; public class UserDto { @@ -18,7 +19,7 @@ public record MyInfoResponse(String loginId, String name, LocalDate birthDate, S public static MyInfoResponse from(UserInfo userInfo) { return new MyInfoResponse( userInfo.loginId(), - userInfo.name(), + MaskingUtils.maskLastCharacter(userInfo.name()), userInfo.birthDate(), userInfo.email() ); diff --git a/apps/commerce-api/src/main/java/com/loopers/support/MaskingUtils.java b/apps/commerce-api/src/main/java/com/loopers/support/MaskingUtils.java new file mode 100644 index 00000000..cd8b0fa5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/support/MaskingUtils.java @@ -0,0 +1,13 @@ +package com.loopers.support; + +public final class MaskingUtils { + + private MaskingUtils() {} + + public static String maskLastCharacter(String value) { + if (value == null || value.isEmpty()) { + return value; + } + return value.substring(0, value.length() - 1) + "*"; + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/support/MaskingUtilsTest.java b/apps/commerce-api/src/test/java/com/loopers/support/MaskingUtilsTest.java new file mode 100644 index 00000000..0d1bbb9d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/support/MaskingUtilsTest.java @@ -0,0 +1,41 @@ +package com.loopers.support; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MaskingUtilsTest { + + @DisplayName("마지막 글자를 *로 마스킹한다.") + @Test + void maskLastCharacter_success() { + String result = MaskingUtils.maskLastCharacter("김용권"); + + assertThat(result).isEqualTo("김용*"); + } + + @DisplayName("1글자인 경우 *만 반환한다.") + @Test + void maskLastCharacter_singleCharacter() { + String result = MaskingUtils.maskLastCharacter("김"); + + assertThat(result).isEqualTo("*"); + } + + @DisplayName("빈 문자열이면 빈 문자열을 반환한다.") + @Test + void maskLastCharacter_emptyString() { + String result = MaskingUtils.maskLastCharacter(""); + + assertThat(result).isEqualTo(""); + } + + @DisplayName("null이면 null을 반환한다.") + @Test + void maskLastCharacter_null() { + String result = MaskingUtils.maskLastCharacter(null); + + assertThat(result).isNull(); + } +} From 3048849280f07024d3a3a90537360f2b7721045d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 21:24:52 +0900 Subject: [PATCH 39/49] =?UTF-8?q?feat=20:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EC=9A=94=EC=B2=AD=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UsersController.java | 1 + .../request/ChangePasswordRequest.java | 19 +++ .../UsersSignUpRequestDto.java | 2 +- .../interfaces/dto/UserDtoValidationTest.java | 109 +++++++++++++++++- 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/request/ChangePasswordRequest.java rename apps/commerce-api/src/main/java/com/loopers/interfaces/{api => request}/UsersSignUpRequestDto.java (96%) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 74dc89ac..9eb97a25 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -5,6 +5,7 @@ import com.loopers.application.UserFacade; import com.loopers.application.UserInfo; import com.loopers.interfaces.UserDto; +import com.loopers.interfaces.request.UsersSignUpRequestDto; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/request/ChangePasswordRequest.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/request/ChangePasswordRequest.java new file mode 100644 index 00000000..1c29f41c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/request/ChangePasswordRequest.java @@ -0,0 +1,19 @@ +package com.loopers.interfaces.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record ChangePasswordRequest( + + @NotBlank(message = "기존 비밀번호는 필수입니다.") + String currentPassword, + + @NotBlank(message = "새 비밀번호는 필수입니다.") + @Size(min = 8, max = 16, message = "비밀번호는 8~16자로 입력해주세요.") + @Pattern( + regexp = "^[A-Za-z0-9\\p{P}\\p{S}]+$", + message = "영문 대소문자, 숫자, 특수문자만 사용 가능합니다." + ) + String newPassword +) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/request/UsersSignUpRequestDto.java similarity index 96% rename from apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersSignUpRequestDto.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/request/UsersSignUpRequestDto.java index d0186d74..79819014 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/request/UsersSignUpRequestDto.java @@ -1,4 +1,4 @@ -package com.loopers.interfaces.api; +package com.loopers.interfaces.request; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.Email; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java index d76f7494..75900e48 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java @@ -3,7 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.loopers.interfaces.api.UsersSignUpRequestDto; +import com.loopers.interfaces.request.ChangePasswordRequest; +import com.loopers.interfaces.request.UsersSignUpRequestDto; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; @@ -273,4 +274,110 @@ void nameFormatContainsDotFailTest() { assertThat(violations.iterator().next().getMessage()).isEqualTo("이름은 한글, 영문, 공백만 입력 가능합니다."); } } + + @DisplayName("비밀번호 변경 요청 검증") + @Nested + class ChangePasswordRequestValidation { + + private Set> validateChangePassword(ChangePasswordRequest dto) { + return validator.validate(dto); + } + + @Test + @DisplayName("올바른 요청이면 검증에 통과한다") + void validRequest() { + ChangePasswordRequest dto = new ChangePasswordRequest("OldPass1!", "NewPass1!"); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isEmpty(); + } + + @Test + @DisplayName("기존 비밀번호가 null이면 검증에 실패한다") + void fail_when_currentPassword_is_null() { + ChangePasswordRequest dto = new ChangePasswordRequest(null, "NewPass1!"); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("기존 비밀번호는 필수입니다."); + } + + @Test + @DisplayName("기존 비밀번호가 빈 문자열이면 검증에 실패한다") + void fail_when_currentPassword_is_blank() { + ChangePasswordRequest dto = new ChangePasswordRequest("", "NewPass1!"); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("기존 비밀번호는 필수입니다."); + } + + @Test + @DisplayName("새 비밀번호가 null이면 검증에 실패한다") + void fail_when_newPassword_is_null() { + ChangePasswordRequest dto = new ChangePasswordRequest("OldPass1!", null); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("새 비밀번호는 필수입니다."); + } + + @Test + @DisplayName("새 비밀번호가 빈 문자열이면 검증에 실패한다") + void fail_when_newPassword_is_blank() { + ChangePasswordRequest dto = new ChangePasswordRequest("OldPass1!", ""); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + } + + @Test + @DisplayName("새 비밀번호가 7자 이하면 검증에 실패한다") + void fail_when_newPassword_too_short() { + ChangePasswordRequest dto = new ChangePasswordRequest("OldPass1!", "Pass1!"); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("비밀번호는 8~16자로 입력해주세요."); + } + + @Test + @DisplayName("새 비밀번호가 17자 이상이면 검증에 실패한다") + void fail_when_newPassword_too_long() { + ChangePasswordRequest dto = new ChangePasswordRequest("OldPass1!", "Password123456789!"); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("비밀번호는 8~16자로 입력해주세요."); + } + + @Test + @DisplayName("새 비밀번호에 한글이 포함되면 검증에 실패한다") + void fail_when_newPassword_contains_korean() { + ChangePasswordRequest dto = new ChangePasswordRequest("OldPass1!", "Password1가"); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("영문 대소문자, 숫자, 특수문자만 사용 가능합니다."); + } + + @Test + @DisplayName("새 비밀번호에 공백이 포함되면 검증에 실패한다") + void fail_when_newPassword_contains_space() { + ChangePasswordRequest dto = new ChangePasswordRequest("OldPass1!", "Pass word1!"); + + Set> violations = validateChangePassword(dto); + + assertThat(violations).isNotEmpty(); + assertThat(violations.iterator().next().getMessage()).isEqualTo("영문 대소문자, 숫자, 특수문자만 사용 가능합니다."); + } + } } From 1c89ed247caf4e1a7f2bfe644d78be76ed674931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 21:52:22 +0900 Subject: [PATCH 40/49] =?UTF-8?q?feat=20:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EC=9A=94=EC=B2=AD=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/application/UserFacade.java | 5 + .../java/com/loopers/domain/UserModel.java | 4 + .../java/com/loopers/domain/UserService.java | 30 +++- .../interfaces/api/UsersController.java | 11 ++ .../com/loopers/domain/UserServiceTest.java | 130 ++++++++++++++++++ .../interfaces/api/UserControllerTest.java | 1 + 6 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/UserServiceTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java index 151164dc..099c180b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java @@ -2,6 +2,7 @@ import com.loopers.domain.UserModel; import com.loopers.domain.UserService; +import com.loopers.interfaces.request.ChangePasswordRequest; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import java.time.LocalDate; @@ -50,4 +51,8 @@ private void validatePasswordContent(String password, LocalDate birthDate) { throw new CoreException(ErrorType.NOT_INCLUDE_BIRTH_IN_PASSWORD); } } + + public void changePassword(Long userId, ChangePasswordRequest request) { + userService.changePassword(userId, request.currentPassword(), request.newPassword()); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java index f83190b7..4b91ae95 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java @@ -72,4 +72,8 @@ protected void guard() { public static UserModel create(String loginId, String encodedPassword, LocalDate birthDate, String name, String email) { return new UserModel(loginId, encodedPassword, birthDate, name, email); } + + public void changePassword(String newEncodedPassword) { + this.password = newEncodedPassword; + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java index 4ea81fe6..8f7aa655 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java @@ -14,11 +14,6 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - @Transactional - public UserModel save(UserModel userModel) { - return userRepository.save(userModel); - } - @Transactional public UserModel createUser(String loginId, String rawPassword, java.time.LocalDate birthDate, String name, String email) { String encodedPassword = passwordEncoder.encode(rawPassword); @@ -43,4 +38,29 @@ public UserModel findById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다.")); } + + @Transactional + public void changePassword(Long userId, String currentPassword, String newPassword) { + UserModel user = findById(userId); + + if (!passwordEncoder.matches(currentPassword, user.getPassword())) { + throw new CoreException(ErrorType.BAD_REQUEST, "기존 비밀번호가 일치하지 않습니다."); + } + + if (passwordEncoder.matches(newPassword, user.getPassword())) { + throw new CoreException(ErrorType.BAD_REQUEST, "새 비밀번호는 기존 비밀번호와 달라야 합니다."); + } + + validatePasswordNotContainsBirthDate(newPassword, user.getBirthDate()); + + String newEncodedPassword = passwordEncoder.encode(newPassword); + user.changePassword(newEncodedPassword); + } + + private void validatePasswordNotContainsBirthDate(String password, java.time.LocalDate birthDate) { + String birthStr = birthDate.toString().replace("-", ""); + if (password.contains(birthStr)) { + throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호에 생년월일을 포함할 수 없습니다."); + } + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java index 9eb97a25..08807af2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java @@ -5,10 +5,12 @@ import com.loopers.application.UserFacade; import com.loopers.application.UserInfo; import com.loopers.interfaces.UserDto; +import com.loopers.interfaces.request.ChangePasswordRequest; import com.loopers.interfaces.request.UsersSignUpRequestDto; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -44,4 +46,13 @@ public ApiResponse getMe(@AuthUser AuthUserPrincipal aut UserInfo userInfo = userFacade.getMyInfo(authUser.getId()); return ApiResponse.success(UserDto.MyInfoResponse.from(userInfo)); } + + @PatchMapping("/me/password") + public ApiResponse changePassword( + @AuthUser AuthUserPrincipal authUser, + @Valid @RequestBody ChangePasswordRequest request + ) { + userFacade.changePassword(authUser.getId(), request); + return ApiResponse.success(null); + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/UserServiceTest.java new file mode 100644 index 00000000..a7d43dbd --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/UserServiceTest.java @@ -0,0 +1,130 @@ +package com.loopers.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +import com.loopers.support.error.CoreException; +import java.time.LocalDate; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @InjectMocks + private UserService userService; + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @DisplayName("비밀번호 변경") + @Nested + class ChangePassword { + + @Test + @DisplayName("비밀번호 변경에 성공한다") + void success() { + Long userId = 1L; + String currentPassword = "OldPass1!"; + String newPassword = "NewPass1!"; + String encodedCurrentPassword = "encoded_old"; + String encodedNewPassword = "encoded_new"; + + UserModel user = UserModel.create("user123", encodedCurrentPassword, LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(currentPassword, encodedCurrentPassword)).willReturn(true); + given(passwordEncoder.matches(newPassword, encodedCurrentPassword)).willReturn(false); + given(passwordEncoder.encode(newPassword)).willReturn(encodedNewPassword); + + userService.changePassword(userId, currentPassword, newPassword); + + assertThat(user.getPassword()).isEqualTo(encodedNewPassword); + } + + @Test + @DisplayName("기존 비밀번호가 일치하지 않으면 예외가 발생한다") + void fail_when_currentPassword_not_match() { + Long userId = 1L; + String currentPassword = "WrongPass!"; + String newPassword = "NewPass1!"; + String encodedCurrentPassword = "encoded_old"; + + UserModel user = UserModel.create("user123", encodedCurrentPassword, LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(currentPassword, encodedCurrentPassword)).willReturn(false); + + assertThatThrownBy(() -> userService.changePassword(userId, currentPassword, newPassword)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("기존 비밀번호가 일치하지 않습니다"); + } + + @Test + @DisplayName("새 비밀번호가 기존 비밀번호와 같으면 예외가 발생한다") + void fail_when_newPassword_same_as_current() { + Long userId = 1L; + String currentPassword = "SamePass1!"; + String newPassword = "SamePass1!"; + String encodedCurrentPassword = "encoded_same"; + + UserModel user = UserModel.create("user123", encodedCurrentPassword, LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(currentPassword, encodedCurrentPassword)).willReturn(true); + given(passwordEncoder.matches(newPassword, encodedCurrentPassword)).willReturn(true); + + assertThatThrownBy(() -> userService.changePassword(userId, currentPassword, newPassword)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("새 비밀번호는 기존 비밀번호와 달라야 합니다"); + } + + @Test + @DisplayName("새 비밀번호에 생년월일이 포함되면 예외가 발생한다") + void fail_when_newPassword_contains_birthDate() { + // arrange + Long userId = 1L; + String currentPassword = "OldPass1!"; + String newPassword = "Pass19911203!"; + String encodedCurrentPassword = "encoded_old"; + + UserModel user = UserModel.create("user123", encodedCurrentPassword, LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(currentPassword, encodedCurrentPassword)).willReturn(true); + given(passwordEncoder.matches(newPassword, encodedCurrentPassword)).willReturn(false); + + // act & assert + assertThatThrownBy(() -> userService.changePassword(userId, currentPassword, newPassword)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("비밀번호에 생년월일을 포함할 수 없습니다"); + } + + @Test + @DisplayName("사용자를 찾을 수 없으면 예외가 발생한다") + void fail_when_user_not_found() { + // arrange + Long userId = 999L; + String currentPassword = "OldPass1!"; + String newPassword = "NewPass1!"; + + given(userRepository.findById(userId)).willReturn(Optional.empty()); + + // act & assert + assertThatThrownBy(() -> userService.changePassword(userId, currentPassword, newPassword)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("사용자를 찾을 수 없습니다"); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 5b3b2bc2..7383dfc4 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -12,6 +12,7 @@ import com.loopers.application.UserFacade; import com.loopers.application.UserInfo; import com.loopers.domain.UserService; +import com.loopers.interfaces.request.UsersSignUpRequestDto; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; From 3a52ab65864412cdff3835d3ae59c23f4f5b705e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 22:11:31 +0900 Subject: [PATCH 41/49] =?UTF-8?q?test=20:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20API=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20controller=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserControllerTest.java | 116 +++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 7383dfc4..2c89e835 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -1,8 +1,11 @@ package com.loopers.interfaces.api; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -11,7 +14,10 @@ import com.loopers.application.SignUpCommand; import com.loopers.application.UserFacade; import com.loopers.application.UserInfo; +import com.loopers.config.WebMvcConfig; +import com.loopers.domain.UserModel; import com.loopers.domain.UserService; +import com.loopers.interfaces.request.ChangePasswordRequest; import com.loopers.interfaces.request.UsersSignUpRequestDto; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; @@ -23,8 +29,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import com.loopers.config.WebMvcConfig; - @WebMvcTest(UsersController.class) @Import({WebMvcConfig.class, CredentialsHeadersArgumentResolver.class, AuthUserArgumentResolver.class}) class UserControllerTest { @@ -400,4 +404,112 @@ void userSignupApiJsonInvalidTest() throws Exception { .andExpect(status().isBadRequest()); } } + + @DisplayName("비밀번호 변경 API") + @Nested + class ChangePasswordTest { + + private UserModel mockUser() { + return UserModel.create("kim", "encodedOldPass", LocalDate.of(1991, 12, 3), "김용권", "yk@google.com"); + } + + @Test + @DisplayName("비밀번호 변경에 성공한다") + void success() throws Exception { + ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "NewPass1!"); + String json = objectMapper.writeValueAsString(request); + + mockMvc.perform(patch("/users/me/password") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "OldPass1!") + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.meta.result").value("SUCCESS")); + } + + @Test + @DisplayName("기존 비밀번호가 null이면 400 Bad Request를 반환한다") + void fail_when_currentPassword_is_null() throws Exception { + ChangePasswordRequest request = new ChangePasswordRequest(null, "NewPass1!"); + String json = objectMapper.writeValueAsString(request); + + mockMvc.perform(patch("/users/me/password") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "OldPass1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("새 비밀번호가 null이면 400 Bad Request를 반환한다") + void fail_when_newPassword_is_null() throws Exception { + ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", null); + String json = objectMapper.writeValueAsString(request); + + mockMvc.perform(patch("/users/me/password") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "OldPass1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("새 비밀번호가 7자 이하면 400 Bad Request를 반환한다") + void fail_when_newPassword_too_short() throws Exception { + ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "Pass1!"); + String json = objectMapper.writeValueAsString(request); + + mockMvc.perform(patch("/users/me/password") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "OldPass1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("새 비밀번호가 17자 이상이면 400 Bad Request를 반환한다") + void fail_when_newPassword_too_long() throws Exception { + ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "Password123456789!"); + String json = objectMapper.writeValueAsString(request); + + mockMvc.perform(patch("/users/me/password") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "OldPass1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("새 비밀번호에 한글이 포함되면 400 Bad Request를 반환한다") + void fail_when_newPassword_contains_korean() throws Exception { + ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "NewPass1가"); + String json = objectMapper.writeValueAsString(request); + + mockMvc.perform(patch("/users/me/password") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "OldPass1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("새 비밀번호에 공백이 포함되면 400 Bad Request를 반환한다") + void fail_when_newPassword_contains_space() throws Exception { + ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "New Pass1!"); + String json = objectMapper.writeValueAsString(request); + + mockMvc.perform(patch("/users/me/password") + .contentType(APPLICATION_JSON) + .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") + .header(LoopersHeaders.X_LOOPERS_LOGIN_PW, "OldPass1!") + .content(json)) + .andExpect(status().isBadRequest()); + } + } } From 39dc6b88a20732ee23ac4cb17edcd896bc2c9b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 22:14:23 +0900 Subject: [PATCH 42/49] =?UTF-8?q?fix=20:=20authenticate()=EC=9D=B4=20null?= =?UTF-8?q?=EC=9D=84=20=EC=9D=91=EB=8B=B5=ED=95=98=EB=8A=94=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/UserControllerTest.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 2c89e835..633e5252 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -1,7 +1,6 @@ package com.loopers.interfaces.api; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doNothing; import static org.springframework.http.MediaType.APPLICATION_JSON; @@ -419,6 +418,9 @@ void success() throws Exception { ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "NewPass1!"); String json = objectMapper.writeValueAsString(request); + given(userService.authenticate("kim", "OldPass1!")).willReturn(mockUser()); + doNothing().when(userFacade).changePassword(any(), any(ChangePasswordRequest.class)); + mockMvc.perform(patch("/users/me/password") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") @@ -434,6 +436,8 @@ void fail_when_currentPassword_is_null() throws Exception { ChangePasswordRequest request = new ChangePasswordRequest(null, "NewPass1!"); String json = objectMapper.writeValueAsString(request); + given(userService.authenticate("kim", "OldPass1!")).willReturn(mockUser()); + mockMvc.perform(patch("/users/me/password") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") @@ -448,6 +452,8 @@ void fail_when_newPassword_is_null() throws Exception { ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", null); String json = objectMapper.writeValueAsString(request); + given(userService.authenticate("kim", "OldPass1!")).willReturn(mockUser()); + mockMvc.perform(patch("/users/me/password") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") @@ -462,6 +468,8 @@ void fail_when_newPassword_too_short() throws Exception { ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "Pass1!"); String json = objectMapper.writeValueAsString(request); + given(userService.authenticate("kim", "OldPass1!")).willReturn(mockUser()); + mockMvc.perform(patch("/users/me/password") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") @@ -476,6 +484,8 @@ void fail_when_newPassword_too_long() throws Exception { ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "Password123456789!"); String json = objectMapper.writeValueAsString(request); + given(userService.authenticate("kim", "OldPass1!")).willReturn(mockUser()); + mockMvc.perform(patch("/users/me/password") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") @@ -490,6 +500,8 @@ void fail_when_newPassword_contains_korean() throws Exception { ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "NewPass1가"); String json = objectMapper.writeValueAsString(request); + given(userService.authenticate("kim", "OldPass1!")).willReturn(mockUser()); + mockMvc.perform(patch("/users/me/password") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") @@ -504,6 +516,8 @@ void fail_when_newPassword_contains_space() throws Exception { ChangePasswordRequest request = new ChangePasswordRequest("OldPass1!", "New Pass1!"); String json = objectMapper.writeValueAsString(request); + given(userService.authenticate("kim", "OldPass1!")).willReturn(mockUser()); + mockMvc.perform(patch("/users/me/password") .contentType(APPLICATION_JSON) .header(LoopersHeaders.X_LOOPERS_LOGIN_ID, "kim") From 0067927abbcceaecad62f12ae0eb93096823882b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 22:36:51 +0900 Subject: [PATCH 43/49] =?UTF-8?q?refactor=20:=20interfaces=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/application/UserFacade.java | 2 +- .../{request => user}/ChangePasswordRequest.java | 2 +- .../java/com/loopers/interfaces/{ => user}/UserDto.java | 2 +- .../loopers/interfaces/{api => user}/UsersController.java | 8 ++++---- .../{request => user}/UsersSignUpRequestDto.java | 2 +- .../com/loopers/interfaces/api/UserControllerTest.java | 5 +++-- .../com/loopers/interfaces/dto/UserDtoValidationTest.java | 4 ++-- 7 files changed, 13 insertions(+), 12 deletions(-) rename apps/commerce-api/src/main/java/com/loopers/interfaces/{request => user}/ChangePasswordRequest.java (93%) rename apps/commerce-api/src/main/java/com/loopers/interfaces/{ => user}/UserDto.java (95%) rename apps/commerce-api/src/main/java/com/loopers/interfaces/{api => user}/UsersController.java (90%) rename apps/commerce-api/src/main/java/com/loopers/interfaces/{request => user}/UsersSignUpRequestDto.java (96%) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java index 099c180b..7c946e1e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java @@ -2,7 +2,7 @@ import com.loopers.domain.UserModel; import com.loopers.domain.UserService; -import com.loopers.interfaces.request.ChangePasswordRequest; +import com.loopers.interfaces.user.ChangePasswordRequest; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import java.time.LocalDate; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/request/ChangePasswordRequest.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/ChangePasswordRequest.java similarity index 93% rename from apps/commerce-api/src/main/java/com/loopers/interfaces/request/ChangePasswordRequest.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/user/ChangePasswordRequest.java index 1c29f41c..4f145db3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/request/ChangePasswordRequest.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/ChangePasswordRequest.java @@ -1,4 +1,4 @@ -package com.loopers.interfaces.request; +package com.loopers.interfaces.user; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UserDto.java similarity index 95% rename from apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/user/UserDto.java index 804a031a..8e5b5542 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/UserDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UserDto.java @@ -1,4 +1,4 @@ -package com.loopers.interfaces; +package com.loopers.interfaces.user; import com.loopers.application.UserInfo; import com.loopers.support.MaskingUtils; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java similarity index 90% rename from apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java index 08807af2..29dad7dd 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java @@ -1,12 +1,12 @@ -package com.loopers.interfaces.api; +package com.loopers.interfaces.user; import com.loopers.application.AuthUserPrincipal; import com.loopers.application.SignUpCommand; import com.loopers.application.UserFacade; import com.loopers.application.UserInfo; -import com.loopers.interfaces.UserDto; -import com.loopers.interfaces.request.ChangePasswordRequest; -import com.loopers.interfaces.request.UsersSignUpRequestDto; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.AuthUser; +import com.loopers.interfaces.api.CredentialsHeaders; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/request/UsersSignUpRequestDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java similarity index 96% rename from apps/commerce-api/src/main/java/com/loopers/interfaces/request/UsersSignUpRequestDto.java rename to apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java index 79819014..742d560a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/request/UsersSignUpRequestDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersSignUpRequestDto.java @@ -1,4 +1,4 @@ -package com.loopers.interfaces.request; +package com.loopers.interfaces.user; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.Email; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 633e5252..028f4eab 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -16,8 +16,9 @@ import com.loopers.config.WebMvcConfig; import com.loopers.domain.UserModel; import com.loopers.domain.UserService; -import com.loopers.interfaces.request.ChangePasswordRequest; -import com.loopers.interfaces.request.UsersSignUpRequestDto; +import com.loopers.interfaces.user.ChangePasswordRequest; +import com.loopers.interfaces.user.UsersController; +import com.loopers.interfaces.user.UsersSignUpRequestDto; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java index 75900e48..084f7575 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/dto/UserDtoValidationTest.java @@ -3,8 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.loopers.interfaces.request.ChangePasswordRequest; -import com.loopers.interfaces.request.UsersSignUpRequestDto; +import com.loopers.interfaces.user.ChangePasswordRequest; +import com.loopers.interfaces.user.UsersSignUpRequestDto; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; From f84a49288ff77ec8034e8b8ab20c5b5222e201af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 22:46:26 +0900 Subject: [PATCH 44/49] =?UTF-8?q?refactor=20:=20application=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/{ => user}/AuthUserPrincipal.java | 2 +- .../com/loopers/application/{ => user}/SignUpCommand.java | 2 +- .../com/loopers/application/{ => user}/UserFacade.java | 2 +- .../java/com/loopers/application/{ => user}/UserInfo.java | 2 +- .../loopers/interfaces/api/AuthUserArgumentResolver.java | 2 +- .../main/java/com/loopers/interfaces/user/UserDto.java | 2 +- .../java/com/loopers/interfaces/user/UsersController.java | 8 ++++---- .../com/loopers/interfaces/api/UserControllerTest.java | 6 +++--- 8 files changed, 13 insertions(+), 13 deletions(-) rename apps/commerce-api/src/main/java/com/loopers/application/{ => user}/AuthUserPrincipal.java (93%) rename apps/commerce-api/src/main/java/com/loopers/application/{ => user}/SignUpCommand.java (89%) rename apps/commerce-api/src/main/java/com/loopers/application/{ => user}/UserFacade.java (98%) rename apps/commerce-api/src/main/java/com/loopers/application/{ => user}/UserInfo.java (92%) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/AuthUserPrincipal.java b/apps/commerce-api/src/main/java/com/loopers/application/user/AuthUserPrincipal.java similarity index 93% rename from apps/commerce-api/src/main/java/com/loopers/application/AuthUserPrincipal.java rename to apps/commerce-api/src/main/java/com/loopers/application/user/AuthUserPrincipal.java index 7c0086c2..df0ad932 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/AuthUserPrincipal.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/AuthUserPrincipal.java @@ -1,4 +1,4 @@ -package com.loopers.application; +package com.loopers.application.user; import com.loopers.domain.UserModel; import lombok.AllArgsConstructor; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/SignUpCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java similarity index 89% rename from apps/commerce-api/src/main/java/com/loopers/application/SignUpCommand.java rename to apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java index 48a72547..fb71d21e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/SignUpCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java @@ -1,4 +1,4 @@ -package com.loopers.application; +package com.loopers.application.user; import java.time.LocalDate; import lombok.Getter; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java similarity index 98% rename from apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java rename to apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index 7c946e1e..d44fa8e9 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -1,4 +1,4 @@ -package com.loopers.application; +package com.loopers.application.user; import com.loopers.domain.UserModel; import com.loopers.domain.UserService; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java similarity index 92% rename from apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java rename to apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java index 8ed8591e..0749f069 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/UserInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -1,4 +1,4 @@ -package com.loopers.application; +package com.loopers.application.user; import com.loopers.domain.UserModel; import java.time.LocalDate; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java index b3ffb565..97f68c2f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api; -import com.loopers.application.AuthUserPrincipal; +import com.loopers.application.user.AuthUserPrincipal; import com.loopers.domain.UserModel; import com.loopers.domain.UserService; import com.loopers.support.error.CoreException; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UserDto.java index 8e5b5542..28e7da86 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UserDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UserDto.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.user; -import com.loopers.application.UserInfo; +import com.loopers.application.user.UserInfo; import com.loopers.support.MaskingUtils; import java.time.LocalDate; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java index 29dad7dd..9125b817 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/user/UsersController.java @@ -1,9 +1,9 @@ package com.loopers.interfaces.user; -import com.loopers.application.AuthUserPrincipal; -import com.loopers.application.SignUpCommand; -import com.loopers.application.UserFacade; -import com.loopers.application.UserInfo; +import com.loopers.application.user.AuthUserPrincipal; +import com.loopers.application.user.SignUpCommand; +import com.loopers.application.user.UserFacade; +import com.loopers.application.user.UserInfo; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.AuthUser; import com.loopers.interfaces.api.CredentialsHeaders; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 028f4eab..52b05754 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -10,9 +10,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import com.loopers.application.SignUpCommand; -import com.loopers.application.UserFacade; -import com.loopers.application.UserInfo; +import com.loopers.application.user.SignUpCommand; +import com.loopers.application.user.UserFacade; +import com.loopers.application.user.UserInfo; import com.loopers.config.WebMvcConfig; import com.loopers.domain.UserModel; import com.loopers.domain.UserService; From 079a1497f5de4bf9619b6db2cd4e440dfad18bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Thu, 5 Feb 2026 22:53:31 +0900 Subject: [PATCH 45/49] =?UTF-8?q?refactor=20:=20domain=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/application/user/AuthUserPrincipal.java | 2 +- .../main/java/com/loopers/application/user/UserFacade.java | 4 ++-- .../main/java/com/loopers/application/user/UserInfo.java | 2 +- .../main/java/com/loopers/domain/{ => user}/UserModel.java | 3 ++- .../java/com/loopers/domain/{ => user}/UserRepository.java | 2 +- .../java/com/loopers/domain/{ => user}/UserService.java | 7 ++++--- .../java/com/loopers/infrastructure/UserJpaRepository.java | 2 +- .../com/loopers/infrastructure/UserRepositoryImpl.java | 4 ++-- .../loopers/interfaces/api/AuthUserArgumentResolver.java | 4 ++-- .../test/java/com/loopers/application/UserFacadeTest.java | 6 ++++-- .../java/com/loopers/domain/{ => user}/UserModelTest.java | 2 +- .../com/loopers/domain/{ => user}/UserServiceTest.java | 2 +- .../com/loopers/interfaces/api/UserControllerTest.java | 4 ++-- 13 files changed, 24 insertions(+), 20 deletions(-) rename apps/commerce-api/src/main/java/com/loopers/domain/{ => user}/UserModel.java (97%) rename apps/commerce-api/src/main/java/com/loopers/domain/{ => user}/UserRepository.java (88%) rename apps/commerce-api/src/main/java/com/loopers/domain/{ => user}/UserService.java (94%) rename apps/commerce-api/src/test/java/com/loopers/domain/{ => user}/UserModelTest.java (98%) rename apps/commerce-api/src/test/java/com/loopers/domain/{ => user}/UserServiceTest.java (99%) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/AuthUserPrincipal.java b/apps/commerce-api/src/main/java/com/loopers/application/user/AuthUserPrincipal.java index df0ad932..6400332b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/AuthUserPrincipal.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/AuthUserPrincipal.java @@ -1,6 +1,6 @@ package com.loopers.application.user; -import com.loopers.domain.UserModel; +import com.loopers.domain.user.UserModel; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index d44fa8e9..fdff883c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -1,7 +1,7 @@ package com.loopers.application.user; -import com.loopers.domain.UserModel; -import com.loopers.domain.UserService; +import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.UserService; import com.loopers.interfaces.user.ChangePasswordRequest; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java index 0749f069..09c90259 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -1,6 +1,6 @@ package com.loopers.application.user; -import com.loopers.domain.UserModel; +import com.loopers.domain.user.UserModel; import java.time.LocalDate; public record UserInfo(Long id, String loginId, String name, String email, LocalDate birthDate) { diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java similarity index 97% rename from apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java rename to apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java index 4b91ae95..b19ca04b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java @@ -1,5 +1,6 @@ -package com.loopers.domain; +package com.loopers.domain.user; +import com.loopers.domain.BaseEntity; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.Column; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java similarity index 88% rename from apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java rename to apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java index 0a276c40..9795de2d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -1,4 +1,4 @@ -package com.loopers.domain; +package com.loopers.domain.user; import java.util.Optional; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java similarity index 94% rename from apps/commerce-api/src/main/java/com/loopers/domain/UserService.java rename to apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 8f7aa655..5e0d2f8f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -1,8 +1,9 @@ -package com.loopers.domain; +package com.loopers.domain.user; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.transaction.Transactional; +import java.time.LocalDate; import lombok.AllArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -15,7 +16,7 @@ public class UserService { private final PasswordEncoder passwordEncoder; @Transactional - public UserModel createUser(String loginId, String rawPassword, java.time.LocalDate birthDate, String name, String email) { + public UserModel createUser(String loginId, String rawPassword, LocalDate birthDate, String name, String email) { String encodedPassword = passwordEncoder.encode(rawPassword); UserModel user = UserModel.create(loginId, encodedPassword, birthDate, name, email); return userRepository.save(user); @@ -57,7 +58,7 @@ public void changePassword(Long userId, String currentPassword, String newPasswo user.changePassword(newEncodedPassword); } - private void validatePasswordNotContainsBirthDate(String password, java.time.LocalDate birthDate) { + private void validatePasswordNotContainsBirthDate(String password, LocalDate birthDate) { String birthStr = birthDate.toString().replace("-", ""); if (password.contains(birthStr)) { throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호에 생년월일을 포함할 수 없습니다."); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java index 8cd5c03e..b38f54e4 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java @@ -1,6 +1,6 @@ package com.loopers.infrastructure; -import com.loopers.domain.UserModel; +import com.loopers.domain.user.UserModel; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java index 6ec46b09..568b9070 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java @@ -1,7 +1,7 @@ package com.loopers.infrastructure; -import com.loopers.domain.UserModel; -import com.loopers.domain.UserRepository; +import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java index 97f68c2f..61b17d31 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/AuthUserArgumentResolver.java @@ -1,8 +1,8 @@ package com.loopers.interfaces.api; import com.loopers.application.user.AuthUserPrincipal; -import com.loopers.domain.UserModel; -import com.loopers.domain.UserService; +import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.UserService; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.servlet.http.HttpServletRequest; diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java index d9d68678..469f1b22 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java @@ -6,8 +6,10 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import com.loopers.domain.UserModel; -import com.loopers.domain.UserService; +import com.loopers.application.user.SignUpCommand; +import com.loopers.application.user.UserFacade; +import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.UserService; import com.loopers.support.error.CoreException; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java similarity index 98% rename from apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java index 0499db88..66e70a92 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java @@ -1,4 +1,4 @@ -package com.loopers.domain; +package com.loopers.domain.user; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java similarity index 99% rename from apps/commerce-api/src/test/java/com/loopers/domain/UserServiceTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java index a7d43dbd..03ce2d2a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -1,4 +1,4 @@ -package com.loopers.domain; +package com.loopers.domain.user; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java index 52b05754..819ce7d6 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserControllerTest.java @@ -14,8 +14,8 @@ import com.loopers.application.user.UserFacade; import com.loopers.application.user.UserInfo; import com.loopers.config.WebMvcConfig; -import com.loopers.domain.UserModel; -import com.loopers.domain.UserService; +import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.UserService; import com.loopers.interfaces.user.ChangePasswordRequest; import com.loopers.interfaces.user.UsersController; import com.loopers.interfaces.user.UsersSignUpRequestDto; From 7e379b6d81f8998ab1961bb5c99b332826ac500b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Sat, 7 Feb 2026 10:01:48 +0900 Subject: [PATCH 46/49] =?UTF-8?q?refactor=20:=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20facade=20->=20service=20=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/user/UserFacade.java | 17 --------------- .../com/loopers/domain/user/UserService.java | 2 ++ .../loopers/application/UserFacadeTest.java | 18 ---------------- .../loopers/domain/user/UserServiceTest.java | 21 +++++++++++++++++++ 4 files changed, 23 insertions(+), 35 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index fdff883c..a08f94e5 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -5,7 +5,6 @@ import com.loopers.interfaces.user.ChangePasswordRequest; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; -import java.time.LocalDate; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; @@ -20,10 +19,6 @@ public UserInfo signUp(SignUpCommand command) { throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입되어 있는 아이디 입니다."); } - // TODO - // 만약 또다른 패스워드 검증 조건이 생기거나 다른 클래스에서도 같이 사용한다면 클래스로 분리해야함 - validatePasswordContent(command.getLoginPw(), command.getBirthDate()); - UserModel userModel = userService.createUser( command.getLoginId(), command.getLoginPw(), @@ -40,18 +35,6 @@ public UserInfo getMyInfo(Long userId) { return UserInfo.from(user); } - private void validatePasswordContent(String password, LocalDate birthDate) { - if (password == null || birthDate == null) { - return; - } - - String birthStr = birthDate.toString().replace("-", ""); - - if (password.contains(birthStr)) { - throw new CoreException(ErrorType.NOT_INCLUDE_BIRTH_IN_PASSWORD); - } - } - public void changePassword(Long userId, ChangePasswordRequest request) { userService.changePassword(userId, request.currentPassword(), request.newPassword()); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 5e0d2f8f..492c8fcc 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -17,6 +17,8 @@ public class UserService { @Transactional public UserModel createUser(String loginId, String rawPassword, LocalDate birthDate, String name, String email) { + validatePasswordNotContainsBirthDate(rawPassword, birthDate); + String encodedPassword = passwordEncoder.encode(rawPassword); UserModel user = UserModel.create(loginId, encodedPassword, birthDate, name, email); return userRepository.save(user); diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java index 469f1b22..82ec5245 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java @@ -51,24 +51,6 @@ void success_signup() { verify(userService, times(1)).createUser(eq("user123"), eq(rawPw), eq(birthDate), eq("kim"), eq("yk@naver.com")); } - @Test - @DisplayName("회원가입 실패 - 비밀번호에 생년월일 포함되어있으면 예외 발생") - void fail_with_birthDate() { - - String rawPw = "pw19911203!!"; // 생일 포함 - SignUpCommand signUpCommand = new SignUpCommand( - "user123", - rawPw, - LocalDate.of(1991, 12, 3), - "kim", - "yk@naver.com" - ); - - assertThatThrownBy(() -> userFacade.signUp(signUpCommand)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("생년월일을 포함할 수 없습니다"); - } - @Test @DisplayName("회원가입 실패 - 이미 가입되어 있으면 예외 발생") void fail_already_signUp() { diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java index 03ce2d2a..af6d35cb 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -28,6 +28,27 @@ class UserServiceTest { @Mock private PasswordEncoder passwordEncoder; + @DisplayName("회원 가입") + @Nested + class CreateUser { + + @Test + @DisplayName("비밀번호에 생년월일이 포함되면 예외가 발생한다") + void fail_when_password_contains_birthDate() { + // arrange + String loginId = "user123"; + String rawPassword = "Pass19911203!"; + LocalDate birthDate = LocalDate.of(1991, 12, 3); + String name = "김용권"; + String email = "yk@google.com"; + + // act & assert + assertThatThrownBy(() -> userService.createUser(loginId, rawPassword, birthDate, name, email)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("비밀번호에 생년월일을 포함할 수 없습니다"); + } + } + @DisplayName("비밀번호 변경") @Nested class ChangePassword { From 5b50c60fccc0eca3a689e912916f8780f8d86bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Sat, 7 Feb 2026 10:13:06 +0900 Subject: [PATCH 47/49] =?UTF-8?q?refactor=20:=20=EC=A4=91=EB=B3=B5=20email?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20&=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 --- .../loopers/application/user/UserFacade.java | 6 ---- .../loopers/domain/user/UserRepository.java | 2 ++ .../com/loopers/domain/user/UserService.java | 6 ++++ .../infrastructure/UserJpaRepository.java | 2 ++ .../infrastructure/UserRepositoryImpl.java | 5 +++ .../loopers/application/UserFacadeTest.java | 22 ------------- .../loopers/domain/user/UserServiceTest.java | 32 +++++++++++++++++++ 7 files changed, 47 insertions(+), 28 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index a08f94e5..e6b486f1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -3,8 +3,6 @@ import com.loopers.domain.user.UserModel; import com.loopers.domain.user.UserService; import com.loopers.interfaces.user.ChangePasswordRequest; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; @@ -15,10 +13,6 @@ public class UserFacade { private final UserService userService; public UserInfo signUp(SignUpCommand command) { - if (userService.existsByEmail(command.getEmail())) { - throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입되어 있는 아이디 입니다."); - } - UserModel userModel = userService.createUser( command.getLoginId(), command.getLoginPw(), diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java index 9795de2d..cbdcf135 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -11,4 +11,6 @@ public interface UserRepository { Optional findByLoginId(String loginId); Boolean existsByEmail(String email); + + Boolean existsByLoginId(String loginId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 492c8fcc..7f65e772 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -17,6 +17,12 @@ public class UserService { @Transactional public UserModel createUser(String loginId, String rawPassword, LocalDate birthDate, String name, String email) { + if (userRepository.existsByLoginId(loginId)) { + throw new CoreException(ErrorType.BAD_REQUEST, "이미 사용 중인 아이디입니다."); + } + if (userRepository.existsByEmail(email)) { + throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입된 이메일입니다."); + } validatePasswordNotContainsBirthDate(rawPassword, birthDate); String encodedPassword = passwordEncoder.encode(rawPassword); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java index b38f54e4..e1b9b594 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserJpaRepository.java @@ -8,5 +8,7 @@ public interface UserJpaRepository extends JpaRepository { Boolean existsByEmail(String email); + Boolean existsByLoginId(String loginId); + Optional findByLoginId(String loginId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java index 568b9070..19a304e6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/UserRepositoryImpl.java @@ -30,4 +30,9 @@ public java.util.Optional findByLoginId(String loginId) { public Boolean existsByEmail(String email) { return userJpaRepository.existsByEmail(email); } + + @Override + public Boolean existsByLoginId(String loginId) { + return userJpaRepository.existsByLoginId(loginId); + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java index 82ec5245..9a4a0400 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/UserFacadeTest.java @@ -1,6 +1,5 @@ package com.loopers.application; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.times; @@ -10,7 +9,6 @@ import com.loopers.application.user.UserFacade; import com.loopers.domain.user.UserModel; import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; import java.time.LocalDate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -50,24 +48,4 @@ void success_signup() { verify(userService, times(1)).createUser(eq("user123"), eq(rawPw), eq(birthDate), eq("kim"), eq("yk@naver.com")); } - - @Test - @DisplayName("회원가입 실패 - 이미 가입되어 있으면 예외 발생") - void fail_already_signUp() { - - String rawPw = "securePassword!@"; - SignUpCommand signUpCommand = new SignUpCommand( - "user123", - rawPw, - LocalDate.of(1995, 1, 1), - "kim", - "yk@naver.com" - ); - - given(userService.existsByEmail("yk@naver.com")).willReturn(true); - - assertThatThrownBy(() -> userFacade.signUp(signUpCommand)) - .isInstanceOf(CoreException.class) - .hasMessageContaining("이미 가입되어 있는 아이디 입니다."); - } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java index af6d35cb..2476123f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -47,6 +47,38 @@ void fail_when_password_contains_birthDate() { .isInstanceOf(CoreException.class) .hasMessageContaining("비밀번호에 생년월일을 포함할 수 없습니다"); } + + @Test + @DisplayName("이미 존재하는 이메일이면 예외가 발생한다") + void fail_when_email_already_exists() { + String loginId = "user123"; + String rawPassword = "Password1!"; + LocalDate birthDate = LocalDate.of(1991, 12, 3); + String name = "김용권"; + String email = "yk@google.com"; + + given(userRepository.existsByEmail(email)).willReturn(true); + + assertThatThrownBy(() -> userService.createUser(loginId, rawPassword, birthDate, name, email)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("이미 가입된 이메일입니다"); + } + + @Test + @DisplayName("이미 존재하는 로그인 아이디면 예외가 발생한다") + void fail_when_loginId_already_exists() { + String loginId = "user123"; + String rawPassword = "Password1!"; + LocalDate birthDate = LocalDate.of(1991, 12, 3); + String name = "김용권"; + String email = "yk@google.com"; + + given(userRepository.existsByLoginId(loginId)).willReturn(true); + + assertThatThrownBy(() -> userService.createUser(loginId, rawPassword, birthDate, name, email)) + .isInstanceOf(CoreException.class) + .hasMessageContaining("이미 사용 중인 아이디입니다"); + } } @DisplayName("비밀번호 변경") From 89930d501077f87a68892ead55526d5d21267d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Sat, 7 Feb 2026 17:50:26 +0900 Subject: [PATCH 48/49] =?UTF-8?q?test=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85,=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20E2E=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../interfaces/api/UsersApiE2ETest.java | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java new file mode 100644 index 00000000..1132357b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java @@ -0,0 +1,203 @@ +package com.loopers.interfaces.api; + +import com.loopers.domain.user.UserModel; +import com.loopers.infrastructure.UserJpaRepository; +import com.loopers.interfaces.user.ChangePasswordRequest; +import com.loopers.interfaces.user.UserDto; +import com.loopers.interfaces.user.UsersSignUpRequestDto; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UsersApiE2ETest { + + private static final String ENDPOINT_USERS = "/users"; + private static final String ENDPOINT_USERS_ME = "/users/me"; + + private final TestRestTemplate testRestTemplate; + private final UserJpaRepository userJpaRepository; + private final PasswordEncoder passwordEncoder; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public UsersApiE2ETest( + TestRestTemplate testRestTemplate, + UserJpaRepository userJpaRepository, + PasswordEncoder passwordEncoder, + DatabaseCleanUp databaseCleanUp + ) { + this.testRestTemplate = testRestTemplate; + this.userJpaRepository = userJpaRepository; + this.passwordEncoder = passwordEncoder; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + private HttpHeaders createHeaders(String loginId, String loginPw) { + HttpHeaders headers = new HttpHeaders(); + headers.set(LoopersHeaders.X_LOOPERS_LOGIN_ID, loginId); + headers.set(LoopersHeaders.X_LOOPERS_LOGIN_PW, loginPw); + headers.set("Content-Type", "application/json"); + return headers; + } + + private UserModel createUser(String loginId, String rawPassword, LocalDate birthDate, String name, String email) { + String encodedPassword = passwordEncoder.encode(rawPassword); + UserModel user = UserModel.create(loginId, encodedPassword, birthDate, name, email); + return userJpaRepository.save(user); + } + + @DisplayName("회원 가입 테스트 (POST /users)") + @Nested + class SignUp { + + @Test + @DisplayName("회원가입에 성공 테스트") + void success() { + // arrange + String loginId = "yktest"; + String loginPw = "Password1!"; + UsersSignUpRequestDto requestDto = new UsersSignUpRequestDto( + LocalDate.of(1991, 12, 3), + "김용권", + "test@google.com" + ); + + HttpHeaders headers = createHeaders(loginId, loginPw); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS, HttpMethod.POST, request, responseType); + + // assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(response.getBody().data().id()).isNotNull() + ); + } + + @Test + @DisplayName("이미 존재하는 로그인 아이디면 400 응답 받는 실패 테스트") + void fail_when_loginId_already_exists() { + // arrange + createUser("existinguser", "Password1!", LocalDate.of(1990, 1, 1), "기존유저", "existing@google.com"); + + UsersSignUpRequestDto requestDto = new UsersSignUpRequestDto( + LocalDate.of(1991, 12, 3), + "김용권", + "new@google.com" + ); + + HttpHeaders headers = createHeaders("existinguser", "Password1!"); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS, HttpMethod.POST, request, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + @DisplayName("이미 존재하는 이메일이면 400 응답을 받는다") + void fail_when_email_already_exists() { + // arrange + createUser("existinguser", "Password1!", LocalDate.of(1990, 1, 1), "기존유저", "existing@google.com"); + + UsersSignUpRequestDto requestDto = new UsersSignUpRequestDto( + LocalDate.of(1991, 12, 3), + "김용권", + "existing@google.com" // 같은 이메일 (중복!) + ); + + HttpHeaders headers = createHeaders("newuser", "Password1!"); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS, HttpMethod.POST, request, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + @DisplayName("비밀번호에 생년월일이 포함되면 400 응답을 받는다") + void fail_when_password_contains_birthDate() { + // arrange + UsersSignUpRequestDto requestDto = new UsersSignUpRequestDto( + LocalDate.of(1991, 12, 3), + "김용권", + "test@google.com" + ); + + HttpHeaders headers = createHeaders("testuser", "Pass19911203!"); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS, HttpMethod.POST, request, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + } + + @DisplayName("내 정보 조회 테스트 (GET /users/me)") + @Nested + class GetMe { + + @Test + @DisplayName("내 정보 조회 성공 테스트") + void success() { + // arrange + String loginId = "testuser"; + String rawPassword = "Password1!"; + createUser(loginId, rawPassword, LocalDate.of(1991, 12, 3), "김용권", "test@google.com"); + + HttpHeaders headers = createHeaders(loginId, rawPassword); + HttpEntity request = new HttpEntity<>(null, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() { + }; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS_ME, HttpMethod.GET, request, responseType); + + // assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(response.getBody().data().loginId()).isEqualTo(loginId), + () -> assertThat(response.getBody().data().email()).isEqualTo("test@google.com") + ); + } + } +} From fbb0b856aefd97888f4a29b99e82a9d98dd77d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EA=B6=8C?= Date: Sat, 7 Feb 2026 17:53:21 +0900 Subject: [PATCH 49/49] =?UTF-8?q?test=20:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20E2E=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=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 --- .../interfaces/api/UsersApiE2ETest.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java index 1132357b..1cfd7ef6 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UsersApiE2ETest.java @@ -31,6 +31,7 @@ class UsersApiE2ETest { private static final String ENDPOINT_USERS = "/users"; private static final String ENDPOINT_USERS_ME = "/users/me"; + private static final String ENDPOINT_USERS_ME_PASSWORD = "/users/me/password"; private final TestRestTemplate testRestTemplate; private final UserJpaRepository userJpaRepository; @@ -200,4 +201,94 @@ void success() { ); } } + + @DisplayName("비밀번호 변경 테스트 (PATCH /users/me/password)") + @Nested + class ChangePassword { + + @Test + @DisplayName("비밀번호 변경 성공 테스트") + void success() { + // arrange + String loginId = "testuser"; + String currentPassword = "Password1!"; + String newPassword = "NewPassword1!"; + createUser(loginId, currentPassword, LocalDate.of(1991, 12, 3), "김용권", "test@google.com"); + + ChangePasswordRequest requestDto = new ChangePasswordRequest(currentPassword, newPassword); + HttpHeaders headers = createHeaders(loginId, currentPassword); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS_ME_PASSWORD, HttpMethod.PATCH, request, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @DisplayName("현재 비밀번호가 틀리면 400 응답을 받는다") + void fail_when_current_password_not_match() { + // arrange + String loginId = "testuser"; + String currentPassword = "Password1!"; + createUser(loginId, currentPassword, LocalDate.of(1991, 12, 3), "김용권", "test@google.com"); + + ChangePasswordRequest requestDto = new ChangePasswordRequest("WrongPassword1!", "NewPassword1!"); + HttpHeaders headers = createHeaders(loginId, currentPassword); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS_ME_PASSWORD, HttpMethod.PATCH, request, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + @DisplayName("새 비밀번호가 기존 비밀번호와 같으면 400 응답을 받는다") + void fail_when_new_password_same_as_current() { + // arrange + String loginId = "testuser"; + String currentPassword = "Password1!"; + createUser(loginId, currentPassword, LocalDate.of(1991, 12, 3), "김용권", "test@google.com"); + + ChangePasswordRequest requestDto = new ChangePasswordRequest(currentPassword, currentPassword); + HttpHeaders headers = createHeaders(loginId, currentPassword); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS_ME_PASSWORD, HttpMethod.PATCH, request, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + @DisplayName("새 비밀번호에 생년월일이 포함되면 400 응답을 받는다") + void fail_when_new_password_contains_birthDate() { + // arrange + String loginId = "testuser"; + String currentPassword = "Password1!"; + createUser(loginId, currentPassword, LocalDate.of(1991, 12, 3), "김용권", "test@google.com"); + + ChangePasswordRequest requestDto = new ChangePasswordRequest(currentPassword, "Pass19911203!"); + HttpHeaders headers = createHeaders(loginId, currentPassword); + HttpEntity request = new HttpEntity<>(requestDto, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_USERS_ME_PASSWORD, HttpMethod.PATCH, request, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + } }