From c0dcfd8aa69e2c7e316fd19b2c35527541e59906 Mon Sep 17 00:00:00 2001 From: "hanyoung.park" Date: Mon, 2 Feb 2026 01:26:59 +0900 Subject: [PATCH 01/40] remove: deprecated codeguide --- .codeguide/loopers-1-week.md | 45 ------------------------------------ 1 file changed, 45 deletions(-) delete mode 100644 .codeguide/loopers-1-week.md diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md deleted file mode 100644 index a8ace53e..00000000 --- a/.codeguide/loopers-1-week.md +++ /dev/null @@ -1,45 +0,0 @@ -## ๐Ÿงช Implementation Quest - -> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. - -### ํšŒ์› ๊ฐ€์ž… - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [ ] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) -- [ ] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ๋‚ด ์ •๋ณด ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. From 83c0fa6c3eb5be2bd70463376ee86124f6786f47 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Mon, 2 Feb 2026 16:42:54 +0900 Subject: [PATCH 02/40] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=95=94=ED=98=B8=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .../loopers/domain/user/PasswordEncoder.java | 6 ++++ .../java/com/loopers/domain/user/User.java | 14 ++++++++ .../loopers/domain/user/UserRepository.java | 6 ++++ .../com/loopers/domain/user/UserService.java | 19 ++++++++++ .../user/UserRepositoryImpl.java | 4 +++ .../api/user/dto/CreateUserRequestV1.java | 16 +++++++++ .../domain/user/InMemoryUserRepository.java | 18 ++++++++++ .../loopers/domain/user/UserServiceTest.java | 35 +++++++++++++++++++ 8 files changed, 118 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/User.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java new file mode 100644 index 00000000..7e353b8a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java @@ -0,0 +1,6 @@ +package com.loopers.domain.user; + +@FunctionalInterface +public interface PasswordEncoder { + String encode(String rawPassword); +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java new file mode 100644 index 00000000..7b1be8dd --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -0,0 +1,14 @@ +package com.loopers.domain.user; + +import lombok.Getter; + +@Getter +public class User { + private String email; + private String password; + + public User(String email, String password) { + this.email = email; + this.password = password; + } +} 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 new file mode 100644 index 00000000..62de3f59 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -0,0 +1,6 @@ +package com.loopers.domain.user; + +public interface UserRepository { + void save(User user); + User findByEmail(String email); +} 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 new file mode 100644 index 00000000..e897ade7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -0,0 +1,19 @@ +package com.loopers.domain.user; + +import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; + +public class UserService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + public void signUp(CreateUserRequestV1 request) { + String encodedPassword = passwordEncoder.encode(request.getPassword()); + User user = new User(request.getEmail(), encodedPassword); + userRepository.save(user); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java new file mode 100644 index 00000000..40da862e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -0,0 +1,4 @@ +package com.loopers.infrastructure.user; + +public class UserRepositoryImpl { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java new file mode 100644 index 00000000..545c10a5 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java @@ -0,0 +1,16 @@ +package com.loopers.interfaces.api.user.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; + +@Getter +@Builder +public class CreateUserRequestV1 { + private String loginId; + private String password; + private String name; + private LocalDate birthDate; + private String email; +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java new file mode 100644 index 00000000..f826c54d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java @@ -0,0 +1,18 @@ +package com.loopers.domain.user; + +import java.util.HashMap; +import java.util.Map; + +public class InMemoryUserRepository implements UserRepository { + private final Map store = new HashMap<>(); + + @Override + public void save(User user) { + store.put(user.getEmail(), user); + } + + @Override + public User findByEmail(String email) { + return store.get(email); + } +} 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 new file mode 100644 index 00000000..5cb01e86 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -0,0 +1,35 @@ +package com.loopers.domain.user; + +import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.time.LocalDate; + +public class UserServiceTest { + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•œ๋‹ค.") + void signUp_encryptsPassword() { + // given + UserRepository userRepository = new InMemoryUserRepository(); + PasswordEncoder passwordEncoder = raw -> "test_encrypt(" + raw + ")"; + UserService userService = new UserService(userRepository, passwordEncoder); + + String rawPassword = "passwordRaw123"; + CreateUserRequestV1 userJoinRequest = CreateUserRequestV1.builder() + .loginId("test_user") + .password(rawPassword) + .name("ํ…Œ์Šคํ„ฐ") + .birthDate(LocalDate.of(2000, 1, 15)) + .email("test@example.com") + .build(); + + // when + userService.signUp(userJoinRequest); + + // then + User saved = userRepository.findByEmail(userJoinRequest.getEmail()); + Assertions.assertThat(saved.getPassword()).isNotEqualTo(userJoinRequest.getPassword()); + } +} From 62db8d0cd58afc1ab51fd6d4fe8d92a53312e096 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Mon, 2 Feb 2026 22:47:07 +0900 Subject: [PATCH 03/40] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20ID=20=EC=98=81?= =?UTF-8?q?=EB=AC=B8/=EC=88=AB=EC=9E=90=20=EA=B2=80=EC=A6=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 --- .../java/com/loopers/domain/user/User.java | 27 ++++++++-- .../com/loopers/domain/user/UserFixture.java | 48 ++++++++++++++++++ .../com/loopers/domain/user/UserTest.java | 50 +++++++++++++++++++ 3 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 7b1be8dd..88d731c0 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,14 +1,33 @@ 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.Entity; +import jakarta.persistence.Table; import lombok.Getter; @Getter -public class User { - private String email; +@Entity +@Table(name = "user") +public class User extends BaseEntity { + private Long id; + private String loginId; private String password; + private String name; + private String birthDate; + private String email; - public User(String email, String password) { - this.email = email; + protected User() {} + + public User(String loginId, String password, String name, String birthDate, String email) { + if (loginId == null || !loginId.matches("^[a-zA-Z0-9]+$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋กœ๊ทธ์ธ ID๋Š” ์˜๋ฌธ/์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); + } + this.loginId = loginId; this.password = password; + this.name = name; + this.birthDate = birthDate; + this.email = email; } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java new file mode 100644 index 00000000..63eec1e0 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java @@ -0,0 +1,48 @@ +package com.loopers.domain.user; + +public class UserFixture { + public static final String VALID_LOGIN_ID = "testUser123"; + public static final String VALID_PASSWORD = "ValidPass1!"; + public static final String VALID_NAME = "ํ™๊ธธ๋™"; + public static final String VALID_BIRTH_DATE = "19900115"; + public static final String VALID_EMAIL = "test@example.com"; + + private String loginId = VALID_LOGIN_ID; + private String password = VALID_PASSWORD; + private String name = VALID_NAME; + private String birthDate = VALID_BIRTH_DATE; + private String email = VALID_EMAIL; + + public static UserFixture builder() { + return new UserFixture(); + } + + public UserFixture loginId(String loginId) { + this.loginId = loginId; + return this; + } + + public UserFixture password(String password) { + this.password = password; + return this; + } + + public UserFixture name(String name) { + this.name = name; + return this; + } + + public UserFixture birthDate(String birthDate) { + this.birthDate = birthDate; + return this; + } + + public UserFixture email(String email) { + this.email = email; + return this; + } + + public User build() { + return new User(loginId, password, name, birthDate, email); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java new file mode 100644 index 00000000..5f9a3fdb --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -0,0 +1,50 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class UserTest { + @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ, ") + @Nested + class LoginIdCreate { + @DisplayName("loginId๊ฐ€ ์˜๋ฌธ/์ˆซ์ž๋งŒ ํฌํ•จํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createsUser_whenLoginIdIsAlphanumeric() { + // arrange + String loginId = "uniqueTester123"; + + // act + User user = UserFixture.builder() + .loginId(loginId) + .build(); + // assert + assertAll( + () -> assertThat(user.getLoginId()).isNotNull() + ); + } + + @DisplayName("loginId์— ์˜๋ฌธ/์ˆซ์ž ์™ธ์˜ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenLoginIdIsNotAlphanumeric() { + // arrange + String loginId = "invalidId#?*"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .loginId(loginId) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + } +} From c45bbb5d67b70ce7870128359102e1e13811c8d4 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Mon, 2 Feb 2026 22:53:44 +0900 Subject: [PATCH 04/40] =?UTF-8?q?refactor:=20UserServiceTest=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/domain/user/UserService.java | 2 +- .../user/UserServiceIntegrationTest.java | 16 +++++++++ .../loopers/domain/user/UserServiceTest.java | 35 ------------------- 3 files changed, 17 insertions(+), 36 deletions(-) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java 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 e897ade7..aa847cac 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 @@ -13,7 +13,7 @@ public UserService(UserRepository userRepository, PasswordEncoder passwordEncode public void signUp(CreateUserRequestV1 request) { String encodedPassword = passwordEncoder.encode(request.getPassword()); - User user = new User(request.getEmail(), encodedPassword); + User user = new User(); // TODO ํ†ตํ•ฉํ…Œ์ŠคํŠธ์‹œ ๊ตฌํ˜„ userRepository.save(user); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java new file mode 100644 index 00000000..2e02d161 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -0,0 +1,16 @@ +package com.loopers.domain.user; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +public class UserServiceIntegrationTest { + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•œ๋‹ค.") + void signUp_encryptsPassword() { + // given + + // when + + // then + } +} 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 deleted file mode 100644 index 5cb01e86..00000000 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.loopers.domain.user; - -import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - -import java.time.LocalDate; - -public class UserServiceTest { - @Test - @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•œ๋‹ค.") - void signUp_encryptsPassword() { - // given - UserRepository userRepository = new InMemoryUserRepository(); - PasswordEncoder passwordEncoder = raw -> "test_encrypt(" + raw + ")"; - UserService userService = new UserService(userRepository, passwordEncoder); - - String rawPassword = "passwordRaw123"; - CreateUserRequestV1 userJoinRequest = CreateUserRequestV1.builder() - .loginId("test_user") - .password(rawPassword) - .name("ํ…Œ์Šคํ„ฐ") - .birthDate(LocalDate.of(2000, 1, 15)) - .email("test@example.com") - .build(); - - // when - userService.signUp(userJoinRequest); - - // then - User saved = userRepository.findByEmail(userJoinRequest.getEmail()); - Assertions.assertThat(saved.getPassword()).isNotEqualTo(userJoinRequest.getPassword()); - } -} From 79683a6ff53d894b2b749549bf67996971db0e77 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 3 Feb 2026 19:19:54 +0900 Subject: [PATCH 05/40] =?UTF-8?q?feat:=20User=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - name: ๋นˆ ๊ฐ’/๊ณต๋ฐฑ ํฌํ•จ ๋ถˆ๊ฐ€ - email: @์™€ . ํฌํ•จ ํ•„์ˆ˜ - birthDate: YYYYMMDD ํ˜•์‹ (8์ž๋ฆฌ ์ˆซ์ž) - password: 8~16์ž ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž, ์ƒ๋…„์›”์ผ ํฌํ•จ ๋ถˆ๊ฐ€ Co-Authored-By: Claude Opus 4.5 --- .../java/com/loopers/domain/user/User.java | 21 ++ .../com/loopers/domain/user/UserTest.java | 251 +++++++++++++++++- 2 files changed, 271 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 88d731c0..268e4c56 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -24,6 +24,27 @@ public User(String loginId, String password, String name, String birthDate, Stri if (loginId == null || !loginId.matches("^[a-zA-Z0-9]+$")) { throw new CoreException(ErrorType.BAD_REQUEST, "๋กœ๊ทธ์ธ ID๋Š” ์˜๋ฌธ/์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); } + + if (name == null || !name.matches("^\\S+$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ๋นˆ ๊ฐ’์ด๊ฑฐ๋‚˜ ๊ณต๋ฐฑ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + if (email == null || !email.contains("@") || !email.contains(".")) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + if (birthDate == null || !birthDate.matches("^\\d{8}$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์€ YYYYMMDD ํ˜•์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + + if (password == null || !password.matches("^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); + } + + if (password.contains(birthDate)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + this.loginId = loginId; this.password = password; this.name = name; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index 5f9a3fdb..53954f92 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -13,7 +13,7 @@ public class UserTest { @DisplayName("ํšŒ์› ๊ฐ€์ž…์‹œ, ") @Nested - class LoginIdCreate { + class Create { @DisplayName("loginId๊ฐ€ ์˜๋ฌธ/์ˆซ์ž๋งŒ ํฌํ•จํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @Test void createsUser_whenLoginIdIsAlphanumeric() { @@ -46,5 +46,254 @@ void throwsBadRequestException_whenLoginIdIsNotAlphanumeric() { // assert assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); } + + @DisplayName("์ด๋ฆ„์ด ๊ณต๋ฐฑ ์—†๋Š” ๋ฌธ์ž์—ด์ด๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createsUser_whenNameIsValid() { + // arrange + String name = "๋ฐ•์ž๋ฐ”"; + + // act + User user = UserFixture.builder() + .name(name) + .build(); + + // assert + assertThat(user.getName()).isEqualTo(name); + } + + @DisplayName("name์ด null์ด๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenNameIsNull() { + // arrange + String name = null; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .name(name) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("name์ด ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenNameIsEmpty() { + // arrange + String name = ""; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .name(name) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("name์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenNameContainsWhitespace() { + // arrange + String name = "๋ฐ• ์ž๋ฐ”"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .name(name) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("email์ด @์™€ .์„ ํฌํ•จํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createsUser_whenEmailIsValid() { + // arrange + String email = "test@example.com"; + + // act + User user = UserFixture.builder() + .email(email) + .build(); + + // assert + assertThat(user.getEmail()).isEqualTo(email); + } + + @DisplayName("email์— @๊ฐ€ ์—†์œผ๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenEmailHasNoAtSign() { + // arrange + String email = "testexample.com"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .email(email) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("email์— .์ด ์—†์œผ๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenEmailHasNoDot() { + // arrange + String email = "test@examplecom"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .email(email) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("birthDate๊ฐ€ YYYYMMDD ํ˜•์‹์ด๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createsUser_whenBirthDateIsValid() { + // arrange + String birthDate = "19900115"; + + // act + User user = UserFixture.builder() + .birthDate(birthDate) + .build(); + + // assert + assertThat(user.getBirthDate()).isEqualTo(birthDate); + } + + @DisplayName("birthDate๊ฐ€ 8์ž๋ฆฌ๊ฐ€ ์•„๋‹ˆ๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenBirthDateIsNot8Digits() { + // arrange + String birthDate = "1990011"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .birthDate(birthDate) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("birthDate์— ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenBirthDateContainsNonDigit() { + // arrange + String birthDate = "1990-01-15"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .birthDate(birthDate) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("password๊ฐ€ 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @Test + void createsUser_whenPasswordIsValid() { + // arrange + String password = "Valid1Pass!"; + + // act + User user = UserFixture.builder() + .password(password) + .build(); + + // assert + assertThat(user.getPassword()).isEqualTo(password); + } + + @DisplayName("password๊ฐ€ 8์ž ๋ฏธ๋งŒ์ด๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenPasswordIsTooShort() { + // arrange + String password = "Short1!"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .password(password) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("password๊ฐ€ 16์ž ์ดˆ๊ณผ๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenPasswordIsTooLong() { + // arrange + String password = "VeryLongPass123!!"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .password(password) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("password์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenPasswordContainsWhitespace() { + // arrange + String password = "Pass 1234!"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .password(password) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @DisplayName("password์— ์ƒ๋…„์›”์ผ์ด ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsBadRequestException_whenPasswordContainsBirthDate() { + // arrange + String birthDate = "19900115"; + String password = "Pass19900115!"; + + // act + CoreException result = assertThrows(CoreException.class, () -> { + UserFixture.builder() + .birthDate(birthDate) + .password(password) + .build(); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } } } From a5392aa74f119b15ad8d236f9498efcc29f70d5a Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 3 Feb 2026 19:37:06 +0900 Subject: [PATCH 06/40] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserService์—์„œ ์›๋ณธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (8~16์ž, ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž, ์ƒ๋…„์›”์ผ ํฌํ•จ ๋ถˆ๊ฐ€) - loginId ์ค‘๋ณต ์ฒดํฌ ๋กœ์ง ์ถ”๊ฐ€ (CONFLICT ์—๋Ÿฌ ์‚ฌ์šฉ) - UserRepository์— findByLoginId ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ - ํ†ตํ•ฉํ…Œ์ŠคํŠธ 6๊ฐœ ์ž‘์„ฑ (์•”ํ˜ธํ™” ์ €์žฅ, ์ค‘๋ณต ์ฒดํฌ, ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ) - User ์—”ํ‹ฐํ‹ฐ์—์„œ password ๊ฒ€์ฆ ์ œ๊ฑฐ (Service ๋ ˆ์ด์–ด๋กœ ์ด๋™) Co-Authored-By: Claude Opus 4.5 --- .../java/com/loopers/domain/user/User.java | 8 - .../loopers/domain/user/UserRepository.java | 3 + .../com/loopers/domain/user/UserService.java | 33 +++- .../domain/user/InMemoryUserRepository.java | 14 +- .../user/UserServiceIntegrationTest.java | 151 +++++++++++++++++- .../com/loopers/domain/user/UserTest.java | 85 ---------- 6 files changed, 194 insertions(+), 100 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 268e4c56..5fae3d2c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -37,14 +37,6 @@ public User(String loginId, String password, String name, String birthDate, Stri throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์€ YYYYMMDD ํ˜•์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); } - if (password == null || !password.matches("^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$")) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); - } - - if (password.contains(birthDate)) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } - this.loginId = loginId; this.password = password; this.name = name; 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 62de3f59..10493d0a 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 @@ -1,6 +1,9 @@ package com.loopers.domain.user; +import java.util.Optional; + public interface UserRepository { void save(User user); User findByEmail(String email); + Optional findByLoginId(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 aa847cac..9c2506a7 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 @@ -1,19 +1,50 @@ package com.loopers.domain.user; import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.time.format.DateTimeFormatter; public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private static final DateTimeFormatter BIRTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } + private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; + public void signUp(CreateUserRequestV1 request) { + if (userRepository.findByLoginId(request.getLoginId()).isPresent()) { + throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋กœ๊ทธ์ธ ID์ž…๋‹ˆ๋‹ค."); + } + + String birthDateString = request.getBirthDate().format(BIRTH_DATE_FORMATTER); + validatePassword(request.getPassword(), birthDateString); + String encodedPassword = passwordEncoder.encode(request.getPassword()); - User user = new User(); // TODO ํ†ตํ•ฉํ…Œ์ŠคํŠธ์‹œ ๊ตฌํ˜„ + + User user = new User( + request.getLoginId(), + encodedPassword, + request.getName(), + birthDateString, + request.getEmail() + ); userRepository.save(user); } + + private void validatePassword(String password, String birthDate) { + if (password == null || !password.matches(PASSWORD_PATTERN)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); + } + if (password.contains(birthDate)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java index f826c54d..c88ea864 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java @@ -2,17 +2,25 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; public class InMemoryUserRepository implements UserRepository { - private final Map store = new HashMap<>(); + private final Map storeByEmail = new HashMap<>(); + private final Map storeByLoginId = new HashMap<>(); @Override public void save(User user) { - store.put(user.getEmail(), user); + storeByEmail.put(user.getEmail(), user); + storeByLoginId.put(user.getLoginId(), user); } @Override public User findByEmail(String email) { - return store.get(email); + return storeByEmail.get(email); + } + + @Override + public Optional findByLoginId(String loginId) { + return Optional.ofNullable(storeByLoginId.get(loginId)); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index 2e02d161..85552dfd 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -1,16 +1,161 @@ package com.loopers.domain.user; +import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class UserServiceIntegrationTest { + private InMemoryUserRepository userRepository; + private PasswordEncoder passwordEncoder; + private UserService userService; + + @BeforeEach + void setUp() { + userRepository = new InMemoryUserRepository(); + passwordEncoder = rawPassword -> "encoded_" + rawPassword; + userService = new UserService(userRepository, passwordEncoder); + } + @Test @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•œ๋‹ค.") void signUp_encryptsPassword() { - // given + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .loginId("testUser123") + .password("ValidPass1!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); + + // act + userService.signUp(request); + + // assert + User savedUser = userRepository.findByLoginId("testUser123").orElse(null); + assertThat(savedUser).isNotNull(); + assertThat(savedUser.getPassword()).isNotEqualTo("ValidPass1!"); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ loginId๊ฐ€ ์ค‘๋ณต๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void signUp_throwsException_whenLoginIdIsDuplicated() { + // arrange + CreateUserRequestV1 firstRequest = CreateUserRequestV1.builder() + .loginId("duplicateId") + .password("ValidPass1!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("first@example.com") + .build(); + userService.signUp(firstRequest); + + CreateUserRequestV1 secondRequest = CreateUserRequestV1.builder() + .loginId("duplicateId") + .password("ValidPass2!") + .name("๊น€์ž๋ฐ”") + .birthDate(LocalDate.of(1995, 5, 20)) + .email("second@example.com") + .build(); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + userService.signUp(secondRequest); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.CONFLICT); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 8์ž ๋ฏธ๋งŒ์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void signUp_throwsException_whenPasswordIsTooShort() { + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .loginId("testUser") + .password("Short1!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + userService.signUp(request); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 16์ž ์ดˆ๊ณผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void signUp_throwsException_whenPasswordIsTooLong() { + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .loginId("testUser") + .password("VeryLongPass123!!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + userService.signUp(request); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void signUp_throwsException_whenPasswordContainsWhitespace() { + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .loginId("testUser") + .password("Pass 1234!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + userService.signUp(request); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ์ƒ๋…„์›”์ผ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void signUp_throwsException_whenPasswordContainsBirthDate() { + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .loginId("testUser") + .password("Pass19900115!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); - // when + // act + CoreException result = assertThrows(CoreException.class, () -> { + userService.signUp(request); + }); - // then + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index 53954f92..543af781 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -210,90 +210,5 @@ void throwsBadRequestException_whenBirthDateContainsNonDigit() { // assert assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); } - - @DisplayName("password๊ฐ€ 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") - @Test - void createsUser_whenPasswordIsValid() { - // arrange - String password = "Valid1Pass!"; - - // act - User user = UserFixture.builder() - .password(password) - .build(); - - // assert - assertThat(user.getPassword()).isEqualTo(password); - } - - @DisplayName("password๊ฐ€ 8์ž ๋ฏธ๋งŒ์ด๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenPasswordIsTooShort() { - // arrange - String password = "Short1!"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .password(password) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("password๊ฐ€ 16์ž ์ดˆ๊ณผ๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenPasswordIsTooLong() { - // arrange - String password = "VeryLongPass123!!"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .password(password) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("password์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenPasswordContainsWhitespace() { - // arrange - String password = "Pass 1234!"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .password(password) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("password์— ์ƒ๋…„์›”์ผ์ด ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenPasswordContainsBirthDate() { - // arrange - String birthDate = "19900115"; - String password = "Pass19900115!"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .birthDate(birthDate) - .password(password) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } } } From f1897e896017d747e956713aaa0fece40241764d Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 3 Feb 2026 20:06:09 +0900 Subject: [PATCH 07/40] =?UTF-8?q?test:=20UserService=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=84=B1=EA=B2=A9=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{UserServiceIntegrationTest.java => UserServiceTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apps/commerce-api/src/test/java/com/loopers/domain/user/{UserServiceIntegrationTest.java => UserServiceTest.java} (99%) diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.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/user/UserServiceIntegrationTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java index 85552dfd..e0e3bd6f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -public class UserServiceIntegrationTest { +public class UserServiceTest { private InMemoryUserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; From f030d42e19c0437adffcee5b61541b03a31b5890 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 3 Feb 2026 21:46:44 +0900 Subject: [PATCH 08/40] =?UTF-8?q?fix:=20TestContainers=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=97=90=20@Profile("test")=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - local ํ”„๋กœํ•„์—์„œ TestContainers ๋กœ๋“œ ๋ฐฉ์ง€ - ๋กœ์ปฌ ์ธํ”„๋ผ๋กœ ํ†ตํ•ฉํ…Œ์ŠคํŠธ ์‹คํ–‰ ๊ฐ€๋Šฅ --- .../com/loopers/testcontainers/MySqlTestContainersConfig.java | 2 ++ .../com/loopers/testcontainers/RedisTestContainersConfig.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java index 9c41edac..a4af2b43 100644 --- a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java +++ b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java @@ -1,10 +1,12 @@ package com.loopers.testcontainers; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.utility.DockerImageName; @Configuration +@Profile("test") public class MySqlTestContainersConfig { private static final MySQLContainer mySqlContainer; diff --git a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java index 35bf94f0..bbd7ab03 100644 --- a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java +++ b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java @@ -2,9 +2,11 @@ import com.redis.testcontainers.RedisContainer; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.testcontainers.utility.DockerImageName; @Configuration +@Profile("test") public class RedisTestContainersConfig { private static final RedisContainer redisContainer = new RedisContainer(DockerImageName.parse("redis:latest")); From adad0bda37e65a37275d8112570e9bcfa6895dff Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 3 Feb 2026 21:47:45 +0900 Subject: [PATCH 09/40] =?UTF-8?q?feat:=20BCrypt=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=95=94=ED=98=B8=ED=99=94=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=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 - spring-security-crypto ์˜์กด์„ฑ ์ถ”๊ฐ€ - BcryptPasswordEncoder ๊ตฌํ˜„ --- apps/commerce-api/build.gradle.kts | 1 + .../user/BcryptPasswordEncoder.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.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/infrastructure/user/BcryptPasswordEncoder.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.java new file mode 100644 index 00000000..b066b416 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.java @@ -0,0 +1,15 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.PasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class BcryptPasswordEncoder implements PasswordEncoder { + private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + @Override + public String encode(String rawPassword) { + return encoder.encode(rawPassword); + } +} From f4dbb647904e4757048f76bea35b26afe1e3cdc6 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 3 Feb 2026 21:48:58 +0900 Subject: [PATCH 10/40] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20DB=20=EC=A0=80=EC=9E=A5=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/domain/user/UserRepository.java | 1 - .../com/loopers/domain/user/UserService.java | 9 ++- .../user/UserJpaRepository.java | 10 ++++ .../user/UserRepositoryImpl.java | 23 +++++++- .../domain/user/InMemoryUserRepository.java | 5 -- .../user/UserServiceIntegrationTest.java | 56 +++++++++++++++++++ 6 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java 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 10493d0a..d33a021a 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 @@ -4,6 +4,5 @@ public interface UserRepository { void save(User user); - User findByEmail(String email); Optional findByLoginId(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 9c2506a7..405be18b 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 @@ -3,20 +3,19 @@ import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; import java.time.format.DateTimeFormatter; +@Service +@RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private static final DateTimeFormatter BIRTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); - public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - } - private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; public void signUp(CreateUserRequestV1 request) { diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java new file mode 100644 index 00000000..d89ee854 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -0,0 +1,10 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserJpaRepository extends JpaRepository { + Optional findByLoginId(String loginId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java index 40da862e..b0f42f5d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -1,4 +1,25 @@ package com.loopers.infrastructure.user; -public class UserRepositoryImpl { +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@RequiredArgsConstructor +@Repository +public class UserRepositoryImpl implements UserRepository { + private final UserJpaRepository userJpaRepository; + + + @Override + public void save(User user) { + userJpaRepository.save(user); + } + + @Override + public Optional findByLoginId(String loginId) { + return userJpaRepository.findByLoginId(loginId); + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java index c88ea864..9c1535db 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/InMemoryUserRepository.java @@ -14,11 +14,6 @@ public void save(User user) { storeByLoginId.put(user.getLoginId(), user); } - @Override - public User findByEmail(String email) { - return storeByEmail.get(email); - } - @Override public Optional findByLoginId(String loginId) { return Optional.ofNullable(storeByLoginId.get(loginId)); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java new file mode 100644 index 00000000..2d9c9842 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -0,0 +1,56 @@ +package com.loopers.domain.user; + +import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import com.loopers.infrastructure.user.UserJpaRepository; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("local") +public class UserServiceIntegrationTest { + @Autowired + private UserService userService; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…ํ•˜๋ฉด DB์— ์ €์žฅ๋œ๋‹ค.") + void signUp_savesUser() { + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .loginId("testUser123") + .password("ValidPass1!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); + + // act + userService.signUp(request); + + // assert + User savedUser = userJpaRepository.findByLoginId("testUser123") + .orElseThrow(() -> new AssertionError("User not saved")); + + assertThat(savedUser.getLoginId()).isEqualTo("testUser123"); + assertThat(savedUser.getId()).isGreaterThan(0L); + } +} From 081a4e40f6b3d2c6f92ecffb3917c93cd99ed39c Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 09:51:26 +0900 Subject: [PATCH 11/40] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=96=B4=20DTO=EB=A5=BC=20RequestV1?= =?UTF-8?q?=EC=97=90=EC=84=9C=20Command=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 --- .../com/loopers/domain/user/UserService.java | 18 +-- .../user/UserServiceIntegrationTest.java | 18 +-- .../loopers/domain/user/UserServiceTest.java | 116 +++++++++--------- 3 files changed, 76 insertions(+), 76 deletions(-) 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 405be18b..282785c5 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 @@ -1,6 +1,6 @@ package com.loopers.domain.user; -import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.application.user.SignUpCommand; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -18,22 +18,22 @@ public class UserService { private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; - public void signUp(CreateUserRequestV1 request) { - if (userRepository.findByLoginId(request.getLoginId()).isPresent()) { + public void signUp(SignUpCommand command) { + if (userRepository.findByLoginId(command.loginId()).isPresent()) { throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋กœ๊ทธ์ธ ID์ž…๋‹ˆ๋‹ค."); } - String birthDateString = request.getBirthDate().format(BIRTH_DATE_FORMATTER); - validatePassword(request.getPassword(), birthDateString); + String birthDateString = command.birthDate().format(BIRTH_DATE_FORMATTER); + validatePassword(command.password(), birthDateString); - String encodedPassword = passwordEncoder.encode(request.getPassword()); + String encodedPassword = passwordEncoder.encode(command.password()); User user = new User( - request.getLoginId(), + command.loginId(), encodedPassword, - request.getName(), + command.name(), birthDateString, - request.getEmail() + command.email() ); userRepository.save(user); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index 2d9c9842..74fbfc26 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -1,6 +1,6 @@ package com.loopers.domain.user; -import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.application.user.SignUpCommand; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -35,16 +35,16 @@ void tearDown() { @DisplayName("ํšŒ์›๊ฐ€์ž…ํ•˜๋ฉด DB์— ์ €์žฅ๋œ๋‹ค.") void signUp_savesUser() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .loginId("testUser123") - .password("ValidPass1!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + SignUpCommand command = new SignUpCommand( + "testUser123", + "ValidPass1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act - userService.signUp(request); + userService.signUp(command); // assert User savedUser = userJpaRepository.findByLoginId("testUser123") 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 e0e3bd6f..44213a40 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 @@ -1,6 +1,6 @@ package com.loopers.domain.user; -import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.application.user.SignUpCommand; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import org.junit.jupiter.api.BeforeEach; @@ -28,16 +28,16 @@ void setUp() { @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•œ๋‹ค.") void signUp_encryptsPassword() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .loginId("testUser123") - .password("ValidPass1!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + SignUpCommand command = new SignUpCommand( + "testUser123", + "ValidPass1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act - userService.signUp(request); + userService.signUp(command); // assert User savedUser = userRepository.findByLoginId("testUser123").orElse(null); @@ -49,26 +49,26 @@ void signUp_encryptsPassword() { @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ loginId๊ฐ€ ์ค‘๋ณต๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") void signUp_throwsException_whenLoginIdIsDuplicated() { // arrange - CreateUserRequestV1 firstRequest = CreateUserRequestV1.builder() - .loginId("duplicateId") - .password("ValidPass1!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("first@example.com") - .build(); - userService.signUp(firstRequest); - - CreateUserRequestV1 secondRequest = CreateUserRequestV1.builder() - .loginId("duplicateId") - .password("ValidPass2!") - .name("๊น€์ž๋ฐ”") - .birthDate(LocalDate.of(1995, 5, 20)) - .email("second@example.com") - .build(); + SignUpCommand firstCommand = new SignUpCommand( + "duplicateId", + "ValidPass1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "first@example.com" + ); + userService.signUp(firstCommand); + + SignUpCommand secondCommand = new SignUpCommand( + "duplicateId", + "ValidPass2!", + "๊น€์ž๋ฐ”", + LocalDate.of(1995, 5, 20), + "second@example.com" + ); // act CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(secondRequest); + userService.signUp(secondCommand); }); // assert @@ -79,17 +79,17 @@ void signUp_throwsException_whenLoginIdIsDuplicated() { @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 8์ž ๋ฏธ๋งŒ์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") void signUp_throwsException_whenPasswordIsTooShort() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .loginId("testUser") - .password("Short1!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + SignUpCommand command = new SignUpCommand( + "testUser", + "Short1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(request); + userService.signUp(command); }); // assert @@ -100,17 +100,17 @@ void signUp_throwsException_whenPasswordIsTooShort() { @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 16์ž ์ดˆ๊ณผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") void signUp_throwsException_whenPasswordIsTooLong() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .loginId("testUser") - .password("VeryLongPass123!!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + SignUpCommand command = new SignUpCommand( + "testUser", + "VeryLongPass123!!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(request); + userService.signUp(command); }); // assert @@ -121,17 +121,17 @@ void signUp_throwsException_whenPasswordIsTooLong() { @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") void signUp_throwsException_whenPasswordContainsWhitespace() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .loginId("testUser") - .password("Pass 1234!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + SignUpCommand command = new SignUpCommand( + "testUser", + "Pass 1234!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(request); + userService.signUp(command); }); // assert @@ -142,17 +142,17 @@ void signUp_throwsException_whenPasswordContainsWhitespace() { @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ์ƒ๋…„์›”์ผ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") void signUp_throwsException_whenPasswordContainsBirthDate() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .loginId("testUser") - .password("Pass19900115!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + SignUpCommand command = new SignUpCommand( + "testUser", + "Pass19900115!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(request); + userService.signUp(command); }); // assert From a3e884af4e0be1cb257d734b6abc8cf943b2c3e9 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 09:56:04 +0900 Subject: [PATCH 12/40] =?UTF-8?q?chore:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=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/user/SignUpCommand.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java new file mode 100644 index 00000000..db26ad0f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java @@ -0,0 +1,11 @@ +package com.loopers.application.user; + +import java.time.LocalDate; + +public record SignUpCommand( + String loginId, + String password, + String name, + LocalDate birthDate, + String email +) {} \ No newline at end of file From cf65c849150872224f179a24e7c11d5c8554ba2a Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 11:02:40 +0900 Subject: [PATCH 13/40] =?UTF-8?q?refactor:=20birthDate=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20LocalDate=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 --- .../java/com/loopers/domain/user/User.java | 18 +++--- .../com/loopers/domain/user/UserService.java | 13 +++-- .../com/loopers/domain/user/UserFixture.java | 8 ++- .../com/loopers/domain/user/UserTest.java | 57 ++----------------- 4 files changed, 24 insertions(+), 72 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 5fae3d2c..bca4175c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -7,6 +7,8 @@ import jakarta.persistence.Table; import lombok.Getter; +import java.time.LocalDate; + @Getter @Entity @Table(name = "user") @@ -15,28 +17,24 @@ public class User extends BaseEntity { private String loginId; private String password; private String name; - private String birthDate; + private LocalDate birthDate; private String email; protected User() {} - public User(String loginId, String password, String name, String birthDate, String email) { - if (loginId == null || !loginId.matches("^[a-zA-Z0-9]+$")) { + public User(String loginId, String password, String name, LocalDate birthDate, String email) { + if (!loginId.matches("^[a-zA-Z0-9]+$")) { throw new CoreException(ErrorType.BAD_REQUEST, "๋กœ๊ทธ์ธ ID๋Š” ์˜๋ฌธ/์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); } - - if (name == null || !name.matches("^\\S+$")) { + + if (!name.matches("^\\S+$")) { throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ๋นˆ ๊ฐ’์ด๊ฑฐ๋‚˜ ๊ณต๋ฐฑ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } - if (email == null || !email.contains("@") || !email.contains(".")) { + if (!email.contains("@") || !email.contains(".")) { throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } - if (birthDate == null || !birthDate.matches("^\\d{8}$")) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์€ YYYYMMDD ํ˜•์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); - } - this.loginId = loginId; this.password = password; this.name = name; 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 282785c5..ac89da3c 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 @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.time.format.DateTimeFormatter; @Service @@ -14,7 +15,7 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private static final DateTimeFormatter BIRTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; @@ -23,8 +24,7 @@ public void signUp(SignUpCommand command) { throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋กœ๊ทธ์ธ ID์ž…๋‹ˆ๋‹ค."); } - String birthDateString = command.birthDate().format(BIRTH_DATE_FORMATTER); - validatePassword(command.password(), birthDateString); + validatePassword(command.password(), command.birthDate()); String encodedPassword = passwordEncoder.encode(command.password()); @@ -32,17 +32,18 @@ public void signUp(SignUpCommand command) { command.loginId(), encodedPassword, command.name(), - birthDateString, + command.birthDate(), command.email() ); userRepository.save(user); } - private void validatePassword(String password, String birthDate) { + private void validatePassword(String password, LocalDate birthDate) { if (password == null || !password.matches(PASSWORD_PATTERN)) { throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); } - if (password.contains(birthDate)) { + String birthDateString = birthDate.format(BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK); + if (password.contains(birthDateString)) { throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java index 63eec1e0..5a479d0b 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java @@ -1,16 +1,18 @@ package com.loopers.domain.user; +import java.time.LocalDate; + public class UserFixture { public static final String VALID_LOGIN_ID = "testUser123"; public static final String VALID_PASSWORD = "ValidPass1!"; public static final String VALID_NAME = "ํ™๊ธธ๋™"; - public static final String VALID_BIRTH_DATE = "19900115"; + public static final LocalDate VALID_BIRTH_DATE = LocalDate.of(1990, 1, 15); public static final String VALID_EMAIL = "test@example.com"; private String loginId = VALID_LOGIN_ID; private String password = VALID_PASSWORD; private String name = VALID_NAME; - private String birthDate = VALID_BIRTH_DATE; + private LocalDate birthDate = VALID_BIRTH_DATE; private String email = VALID_EMAIL; public static UserFixture builder() { @@ -32,7 +34,7 @@ public UserFixture name(String name) { return this; } - public UserFixture birthDate(String birthDate) { + public UserFixture birthDate(LocalDate birthDate) { this.birthDate = birthDate; return this; } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index 543af781..990127fa 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.time.LocalDate; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -62,23 +64,6 @@ void createsUser_whenNameIsValid() { assertThat(user.getName()).isEqualTo(name); } - @DisplayName("name์ด null์ด๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenNameIsNull() { - // arrange - String name = null; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .name(name) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - @DisplayName("name์ด ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") @Test void throwsBadRequestException_whenNameIsEmpty() { @@ -162,11 +147,11 @@ void throwsBadRequestException_whenEmailHasNoDot() { assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); } - @DisplayName("birthDate๊ฐ€ YYYYMMDD ํ˜•์‹์ด๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") + @DisplayName("birthDate๊ฐ€ ์œ ํšจํ•œ LocalDate์ด๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @Test void createsUser_whenBirthDateIsValid() { // arrange - String birthDate = "19900115"; + LocalDate birthDate = LocalDate.of(1990, 1, 15); // act User user = UserFixture.builder() @@ -176,39 +161,5 @@ void createsUser_whenBirthDateIsValid() { // assert assertThat(user.getBirthDate()).isEqualTo(birthDate); } - - @DisplayName("birthDate๊ฐ€ 8์ž๋ฆฌ๊ฐ€ ์•„๋‹ˆ๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenBirthDateIsNot8Digits() { - // arrange - String birthDate = "1990011"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .birthDate(birthDate) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @DisplayName("birthDate์— ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenBirthDateContainsNonDigit() { - // arrange - String birthDate = "1990-01-15"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .birthDate(birthDate) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } } } From 3c36e5ed8647d7565fe3ab17be8d48bcba71e1e2 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 11:02:52 +0900 Subject: [PATCH 14/40] =?UTF-8?q?feat:=20Bean=20Validation=20=EB=B0=8F=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=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/interfaces/api/ApiControllerAdvice.java | 9 +++++++++ .../interfaces/api/user/dto/CreateUserRequestV1.java | 12 ++++++++++++ 2 files changed, 21 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..8a438fb5 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,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +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 +47,14 @@ public ResponseEntity> handleBadRequest(MissingServletRequestPara return failureResponse(ErrorType.BAD_REQUEST, message); } + @ExceptionHandler + public ResponseEntity> handleValidationException(MethodArgumentNotValidException e) { + String errorMessage = e.getBindingResult().getFieldErrors().stream() + .map(error -> error.getDefaultMessage()) + .collect(Collectors.joining(", ")); + return failureResponse(ErrorType.BAD_REQUEST, errorMessage); + } + @ExceptionHandler public ResponseEntity> handleBadRequest(HttpMessageNotReadableException e) { String errorMessage; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java index 545c10a5..07e71aeb 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java @@ -1,5 +1,7 @@ package com.loopers.interfaces.api.user.dto; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; @@ -8,9 +10,19 @@ @Getter @Builder public class CreateUserRequestV1 { + @NotNull(message = "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String loginId; + + @NotNull(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String password; + + @NotNull(message = "์ด๋ฆ„์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String name; + + @NotNull(message = "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate birthDate; + + @NotNull(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String email; } From 5eec4e97e39d7f793cdf3987bcdb051d044f03d1 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 11:15:29 +0900 Subject: [PATCH 15/40] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=20dto=20?= =?UTF-8?q?=EB=B9=88=EA=B0=92=EC=B2=B4=ED=81=AC=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EB=B3=80=EA=B2=BD=20@NotNull=20-?= =?UTF-8?q?>=20@NotBlank?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/user/dto/CreateUserRequestV1.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java index 07e71aeb..581d22a9 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java @@ -1,6 +1,7 @@ package com.loopers.interfaces.api.user.dto; import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; @@ -10,19 +11,19 @@ @Getter @Builder public class CreateUserRequestV1 { - @NotNull(message = "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @NotBlank(message = "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String loginId; - @NotNull(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @NotBlank(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String password; - @NotNull(message = "์ด๋ฆ„์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @NotBlank(message = "์ด๋ฆ„์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String name; - @NotNull(message = "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @NotBlank(message = "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate birthDate; - @NotNull(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @NotBlank(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String email; } From ad2ee7f7cc55f9fe7e9ec9e8a7b1e9b38057d1f3 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 11:49:17 +0900 Subject: [PATCH 16/40] =?UTF-8?q?refactor:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20SignUpValidator=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=ED=95=98=EA=B3=A0=20User=20=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/domain/user/SignUpValidator.java | 45 +++++++++++++ .../java/com/loopers/domain/user/User.java | 63 +++++++++++++++---- .../com/loopers/domain/user/UserService.java | 32 ++-------- .../com/loopers/domain/user/UserFixture.java | 2 +- .../loopers/domain/user/UserServiceTest.java | 4 +- 5 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java new file mode 100644 index 00000000..7d98b133 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java @@ -0,0 +1,45 @@ +package com.loopers.domain.user; + +import com.loopers.application.user.SignUpCommand; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +@Component +@RequiredArgsConstructor +public class SignUpValidator { + private final UserRepository userRepository; + + private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = + DateTimeFormatter.ofPattern("yyyyMMdd"); + + private static final String PASSWORD_PATTERN = + "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; + + public void validate(SignUpCommand command) { + if (userRepository.findByLoginId(command.loginId()).isPresent()) { + throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋กœ๊ทธ์ธ ID์ž…๋‹ˆ๋‹ค."); + } + + if (command.birthDate().isAfter(LocalDate.now())) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์€ ๋ฏธ๋ž˜์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + validatePassword(command.password(), command.birthDate()); + } + + private void validatePassword(String password, LocalDate birthDate) { + if (!password.matches(PASSWORD_PATTERN)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); + } + + String birthDateString = birthDate.format(BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK); + if (password.contains(birthDateString)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index bca4175c..f0c48955 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -13,32 +13,73 @@ @Entity @Table(name = "user") public class User extends BaseEntity { + private Long id; private String loginId; - private String password; + private String password; // encoded private String name; private LocalDate birthDate; private String email; protected User() {} - public User(String loginId, String password, String name, LocalDate birthDate, String email) { + private User(String loginId, String encodedPassword, String name, LocalDate birthDate, String email) { + validateLoginId(loginId); + validateEncodedPassword(encodedPassword); + validateName(name); + validateBirthDate(birthDate); + validateEmail(email); + + this.loginId = loginId; + this.password = encodedPassword; + this.name = name; + this.birthDate = birthDate; + this.email = email; + } + + public static User create(String loginId, String encodedPassword, String name, LocalDate birthDate, String email) { + return new User(loginId, encodedPassword, name, birthDate, email); + } + + private void validateLoginId(String loginId) { + if (loginId == null || loginId.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค."); + } + if (!loginId.matches("^[a-zA-Z0-9]+$")) { throw new CoreException(ErrorType.BAD_REQUEST, "๋กœ๊ทธ์ธ ID๋Š” ์˜๋ฌธ/์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); } + } - if (!name.matches("^\\S+$")) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ๋นˆ ๊ฐ’์ด๊ฑฐ๋‚˜ ๊ณต๋ฐฑ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + private void validateEncodedPassword(String encodedPassword) { + if (encodedPassword == null || encodedPassword.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค."); } + } - if (!email.contains("@") || !email.contains(".")) { - throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค."); } - this.loginId = loginId; - this.password = password; - this.name = name; - this.birthDate = birthDate; - this.email = email; + if (name.length() < 2) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฆ„์€ ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + } + + private void validateBirthDate(LocalDate birthDate) { + if (birthDate == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค."); + } + } + + private void validateEmail(String email) { + if (email == null || email.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค."); + } + + if (!email.contains("@") || !email.matches("^[\\w\\.]+@[\\w\\.]+\\.[a-zA-Z]{2,}$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } } } 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 ac89da3c..1bf11409 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 @@ -1,50 +1,28 @@ package com.loopers.domain.user; import com.loopers.application.user.SignUpCommand; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; - @Service @RequiredArgsConstructor public class UserService { - private final UserRepository userRepository; + private final SignUpValidator signUpValidator; private final PasswordEncoder passwordEncoder; - - private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = DateTimeFormatter.ofPattern("yyyyMMdd"); - - private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; + private final UserRepository userRepository; public void signUp(SignUpCommand command) { - if (userRepository.findByLoginId(command.loginId()).isPresent()) { - throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋กœ๊ทธ์ธ ID์ž…๋‹ˆ๋‹ค."); - } - - validatePassword(command.password(), command.birthDate()); + signUpValidator.validate(command); String encodedPassword = passwordEncoder.encode(command.password()); - - User user = new User( + User user = User.create( command.loginId(), encodedPassword, command.name(), command.birthDate(), command.email() ); - userRepository.save(user); - } - private void validatePassword(String password, LocalDate birthDate) { - if (password == null || !password.matches(PASSWORD_PATTERN)) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); - } - String birthDateString = birthDate.format(BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK); - if (password.contains(birthDateString)) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } + userRepository.save(user); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java index 5a479d0b..89045d0e 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java @@ -45,6 +45,6 @@ public UserFixture email(String email) { } public User build() { - return new User(loginId, password, name, birthDate, email); + return User.create(loginId, password, name, birthDate, email); } } 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 44213a40..d5cee817 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 @@ -15,13 +15,15 @@ public class UserServiceTest { private InMemoryUserRepository userRepository; private PasswordEncoder passwordEncoder; + private SignUpValidator signUpValidator; private UserService userService; @BeforeEach void setUp() { userRepository = new InMemoryUserRepository(); passwordEncoder = rawPassword -> "encoded_" + rawPassword; - userService = new UserService(userRepository, passwordEncoder); + signUpValidator = new SignUpValidator(userRepository); + userService = new UserService(signUpValidator, passwordEncoder, userRepository); } @Test From dc7df5c7113f3f9fb04451a95e165c0b22c114db Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 14:43:31 +0900 Subject: [PATCH 17/40] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20UserServ?= =?UTF-8?q?iceTest=EC=9D=98=20=EC=9D=BC=EB=B6=80=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?SignUpValidatorTest=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/SignUpValidatorTest.java | 142 ++++++++++++++++++ .../loopers/domain/user/UserServiceTest.java | 117 --------------- 2 files changed, 142 insertions(+), 117 deletions(-) create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java new file mode 100644 index 00000000..6690f066 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java @@ -0,0 +1,142 @@ +package com.loopers.domain.user; + +import com.loopers.application.user.SignUpCommand; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SignUpValidatorTest { + private InMemoryUserRepository userRepository; + private PasswordEncoder passwordEncoder; + private SignUpValidator signUpValidator; + private UserService userService; + + @BeforeEach + void setUp() { + userRepository = new InMemoryUserRepository(); + passwordEncoder = rawPassword -> "encoded_" + rawPassword; + signUpValidator = new SignUpValidator(userRepository); + userService = new UserService(signUpValidator, passwordEncoder, userRepository); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ loginId๊ฐ€ ์ค‘๋ณต๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void validate_throwsException_whenLoginIdIsDuplicated() { + // arrange + SignUpCommand firstCommand = new SignUpCommand( + "duplicateId", + "ValidPass1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "first@example.com" + ); + userService.signUp(firstCommand); + + SignUpCommand secondCommand = new SignUpCommand( + "duplicateId", + "ValidPass2!", + "๊น€์ž๋ฐ”", + LocalDate.of(1995, 5, 20), + "second@example.com" + ); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + signUpValidator.validate(secondCommand); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.CONFLICT); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 8์ž ๋ฏธ๋งŒ์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void validate_throwsException_whenPasswordIsTooShort() { + // arrange + SignUpCommand command = new SignUpCommand( + "testUser", + "Short1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + signUpValidator.validate(command); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 16์ž ์ดˆ๊ณผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void validate_throwsException_whenPasswordIsTooLong() { + // arrange + SignUpCommand command = new SignUpCommand( + "testUser", + "VeryLongPass123!!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + signUpValidator.validate(command); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void validate_throwsException_whenPasswordContainsWhitespace() { + // arrange + SignUpCommand command = new SignUpCommand( + "testUser", + "Pass 1234!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + signUpValidator.validate(command); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ์ƒ๋…„์›”์ผ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + void validate_throwsException_whenPasswordContainsBirthDate() { + // arrange + SignUpCommand command = new SignUpCommand( + "testUser", + "Pass19900115!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); + + // act + CoreException result = assertThrows(CoreException.class, () -> { + signUpValidator.validate(command); + }); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } +} 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 d5cee817..7b0fcdd2 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 @@ -1,8 +1,6 @@ package com.loopers.domain.user; import com.loopers.application.user.SignUpCommand; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; @@ -10,7 +8,6 @@ import java.time.LocalDate; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; public class UserServiceTest { private InMemoryUserRepository userRepository; @@ -46,118 +43,4 @@ void signUp_encryptsPassword() { assertThat(savedUser).isNotNull(); assertThat(savedUser.getPassword()).isNotEqualTo("ValidPass1!"); } - - @Test - @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ loginId๊ฐ€ ์ค‘๋ณต๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - void signUp_throwsException_whenLoginIdIsDuplicated() { - // arrange - SignUpCommand firstCommand = new SignUpCommand( - "duplicateId", - "ValidPass1!", - "๋ฐ•์ž๋ฐ”", - LocalDate.of(1990, 1, 15), - "first@example.com" - ); - userService.signUp(firstCommand); - - SignUpCommand secondCommand = new SignUpCommand( - "duplicateId", - "ValidPass2!", - "๊น€์ž๋ฐ”", - LocalDate.of(1995, 5, 20), - "second@example.com" - ); - - // act - CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(secondCommand); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.CONFLICT); - } - - @Test - @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 8์ž ๋ฏธ๋งŒ์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - void signUp_throwsException_whenPasswordIsTooShort() { - // arrange - SignUpCommand command = new SignUpCommand( - "testUser", - "Short1!", - "๋ฐ•์ž๋ฐ”", - LocalDate.of(1990, 1, 15), - "test@example.com" - ); - - // act - CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(command); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @Test - @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password๊ฐ€ 16์ž ์ดˆ๊ณผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - void signUp_throwsException_whenPasswordIsTooLong() { - // arrange - SignUpCommand command = new SignUpCommand( - "testUser", - "VeryLongPass123!!", - "๋ฐ•์ž๋ฐ”", - LocalDate.of(1990, 1, 15), - "test@example.com" - ); - - // act - CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(command); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @Test - @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - void signUp_throwsException_whenPasswordContainsWhitespace() { - // arrange - SignUpCommand command = new SignUpCommand( - "testUser", - "Pass 1234!", - "๋ฐ•์ž๋ฐ”", - LocalDate.of(1990, 1, 15), - "test@example.com" - ); - - // act - CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(command); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - - @Test - @DisplayName("ํšŒ์›๊ฐ€์ž…์‹œ password์— ์ƒ๋…„์›”์ผ์ด ํฌํ•จ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - void signUp_throwsException_whenPasswordContainsBirthDate() { - // arrange - SignUpCommand command = new SignUpCommand( - "testUser", - "Pass19900115!", - "๋ฐ•์ž๋ฐ”", - LocalDate.of(1990, 1, 15), - "test@example.com" - ); - - // act - CoreException result = assertThrows(CoreException.class, () -> { - userService.signUp(command); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } } From b63e5469a807bf7d2661527ddd9fab515c9a0d5d Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 15:03:08 +0900 Subject: [PATCH 18/40] =?UTF-8?q?refactor:=20SignUpCommand=EC=97=90=20from?= =?UTF-8?q?=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=84=9C=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 --- .../application/user/SignUpCommand.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java index db26ad0f..eb001bff 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java @@ -1,11 +1,23 @@ package com.loopers.application.user; +import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; + import java.time.LocalDate; public record SignUpCommand( - String loginId, - String password, - String name, - LocalDate birthDate, - String email -) {} \ No newline at end of file + String loginId, + String password, + String name, + LocalDate birthDate, + String email +) { + public static SignUpCommand from(CreateUserRequestV1 request) { + return new SignUpCommand( + request.getLoginId(), + request.getPassword(), + request.getName(), + request.getBirthDate(), + request.getEmail() + ); + } +} From 74dd37bc690ebc76aa4baf7295e9186f0f52db19 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 15:07:07 +0900 Subject: [PATCH 19/40] =?UTF-8?q?refactor:=20birthDate=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EC=9D=84=20NotBlank=EC=97=90=EC=84=9C=20NotNull?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/user/dto/CreateUserRequestV1.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java index 581d22a9..eed614de 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java @@ -3,13 +3,17 @@ import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDate; @Getter @Builder +@NoArgsConstructor +@AllArgsConstructor public class CreateUserRequestV1 { @NotBlank(message = "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String loginId; @@ -20,7 +24,7 @@ public class CreateUserRequestV1 { @NotBlank(message = "์ด๋ฆ„์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") private String name; - @NotBlank(message = "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @NotNull(message = "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate birthDate; From 0f7fa42d237b35496286eb87b06d58e66c8fdb5c Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 15:11:48 +0900 Subject: [PATCH 20/40] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B0=8F=20?= =?UTF-8?q?E2E=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/user/SignUpV1Controller.java | 31 +++++ .../interfaces/api/SignUpV1ApiE2ETest.java | 128 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java new file mode 100644 index 00000000..fa95e461 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java @@ -0,0 +1,31 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.SignUpCommand; +import com.loopers.domain.user.UserService; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class SignUpV1Controller { + + private final UserService userService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse signUp(@Valid @RequestBody CreateUserRequestV1 request) { + SignUpCommand command = SignUpCommand.from(request); + userService.signUp(command); + + return ApiResponse.success(null); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java new file mode 100644 index 00000000..8b4a5f7b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java @@ -0,0 +1,128 @@ +package com.loopers.interfaces.api; + +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +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.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +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 SignUpV1ApiE2ETest { + + private static final String ENDPOINT_SIGN_UP = "/api/v1/users"; + + private final TestRestTemplate testRestTemplate; + private final UserJpaRepository userJpaRepository; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public SignUpV1ApiE2ETest( + TestRestTemplate testRestTemplate, + UserJpaRepository userJpaRepository, + DatabaseCleanUp databaseCleanUp + ) { + this.testRestTemplate = testRestTemplate; + this.userJpaRepository = userJpaRepository; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("POST /api/v1/users (ํšŒ์›๊ฐ€์ž…)") + @Nested + class SignUp { + @DisplayName("์œ ํšจํ•œ ํšŒ์›๊ฐ€์ž… ์š”์ฒญ์ด๋ฉด, 201 Created ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void returnsCreated_whenValidRequest() { + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .loginId("testUser123") + .password("ValidPass1!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_SIGN_UP, HttpMethod.POST, new HttpEntity<>(request), responseType); + + // assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED), + () -> assertThat(userJpaRepository.findByLoginId("testUser123")).isPresent() + ); + } + + @DisplayName("loginId๊ฐ€ ๋ˆ„๋ฝ๋˜๋ฉด, 400 Bad Request ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void returnsBadRequest_whenLoginIdIsMissing() { + // arrange + CreateUserRequestV1 request = CreateUserRequestV1.builder() + .password("ValidPass1!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("test@example.com") + .build(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_SIGN_UP, HttpMethod.POST, new HttpEntity<>(request), responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @DisplayName("์ด๋ฏธ ์กด์žฌํ•˜๋Š” loginId๋กœ ์š”์ฒญํ•˜๋ฉด, 409 Conflict ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @Test + void returnsConflict_whenLoginIdAlreadyExists() { + // arrange + CreateUserRequestV1 firstRequest = CreateUserRequestV1.builder() + .loginId("duplicateId") + .password("ValidPass1!") + .name("๋ฐ•์ž๋ฐ”") + .birthDate(LocalDate.of(1990, 1, 15)) + .email("first@example.com") + .build(); + + testRestTemplate.exchange(ENDPOINT_SIGN_UP, HttpMethod.POST, new HttpEntity<>(firstRequest), + new ParameterizedTypeReference>() {}); + + CreateUserRequestV1 secondRequest = CreateUserRequestV1.builder() + .loginId("duplicateId") + .password("ValidPass2!") + .name("๊น€์ž๋ฐ”") + .birthDate(LocalDate.of(1995, 5, 20)) + .email("second@example.com") + .build(); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_SIGN_UP, HttpMethod.POST, new HttpEntity<>(secondRequest), responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + } +} From 6834d10aac0b4bd348879e63256fc859533ae462 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 16:31:08 +0900 Subject: [PATCH 21/40] =?UTF-8?q?feat:=20command=EC=99=80=20=EC=95=94?= =?UTF-8?q?=ED=98=B8=ED=99=94=EB=90=9C=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EB=A5=BC=20=EA=B0=80=EC=A7=80=EA=B3=A0=20User?= =?UTF-8?q?=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=84=9C=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 --- .../src/main/java/com/loopers/domain/user/User.java | 6 ++++++ .../main/java/com/loopers/domain/user/UserService.java | 9 +-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index f0c48955..96c73e25 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,5 +1,6 @@ package com.loopers.domain.user; +import com.loopers.application.user.SignUpCommand; import com.loopers.domain.BaseEntity; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; @@ -41,6 +42,11 @@ public static User create(String loginId, String encodedPassword, String name, L return new User(loginId, encodedPassword, name, birthDate, email); } + public static User create(SignUpCommand command, String encodedPassword) { + return new User(command.loginId(), encodedPassword, command.name(), command.birthDate(), command.email()); + + } + private void validateLoginId(String loginId) { if (loginId == null || loginId.isBlank()) { throw new CoreException(ErrorType.BAD_REQUEST, "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค."); 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 1bf11409..b396ae21 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 @@ -13,15 +13,8 @@ public class UserService { public void signUp(SignUpCommand command) { signUpValidator.validate(command); - String encodedPassword = passwordEncoder.encode(command.password()); - User user = User.create( - command.loginId(), - encodedPassword, - command.name(), - command.birthDate(), - command.email() - ); + User user = User.create(command, encodedPassword); userRepository.save(user); } From 91c99665e48e94ee1948744707c203e11d8aa0b0 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 16:36:05 +0900 Subject: [PATCH 22/40] =?UTF-8?q?refactor:=20UserService=20->=20SignUpServ?= =?UTF-8?q?ice=EB=A1=9C=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 --- .../domain/user/{UserService.java => SignUpService.java} | 2 +- .../loopers/interfaces/api/user/SignUpV1Controller.java | 6 +++--- ...grationTest.java => SignUpServiceIntegrationTest.java} | 6 +++--- .../user/{UserServiceTest.java => SignUpServiceTest.java} | 8 ++++---- .../java/com/loopers/domain/user/SignUpValidatorTest.java | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) rename apps/commerce-api/src/main/java/com/loopers/domain/user/{UserService.java => SignUpService.java} (95%) rename apps/commerce-api/src/test/java/com/loopers/domain/user/{UserServiceIntegrationTest.java => SignUpServiceIntegrationTest.java} (92%) rename apps/commerce-api/src/test/java/com/loopers/domain/user/{UserServiceTest.java => SignUpServiceTest.java} (86%) 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/SignUpService.java similarity index 95% rename from apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java rename to apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpService.java index b396ae21..e625e0c5 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/SignUpService.java @@ -6,7 +6,7 @@ @Service @RequiredArgsConstructor -public class UserService { +public class SignUpService { private final SignUpValidator signUpValidator; private final PasswordEncoder passwordEncoder; private final UserRepository userRepository; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java index fa95e461..f8a1f79a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java @@ -1,7 +1,7 @@ package com.loopers.interfaces.api.user; import com.loopers.application.user.SignUpCommand; -import com.loopers.domain.user.UserService; +import com.loopers.domain.user.SignUpService; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; import jakarta.validation.Valid; @@ -18,13 +18,13 @@ @RequestMapping("/api/v1/users") public class SignUpV1Controller { - private final UserService userService; + private final SignUpService signUpService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public ApiResponse signUp(@Valid @RequestBody CreateUserRequestV1 request) { SignUpCommand command = SignUpCommand.from(request); - userService.signUp(command); + signUpService.signUp(command); return ApiResponse.success(null); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java similarity index 92% rename from apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java index 74fbfc26..2f1c8bce 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java @@ -16,9 +16,9 @@ @SpringBootTest @ActiveProfiles("local") -public class UserServiceIntegrationTest { +public class SignUpServiceIntegrationTest { @Autowired - private UserService userService; + private SignUpService signUpService; @Autowired private UserJpaRepository userJpaRepository; @@ -44,7 +44,7 @@ void signUp_savesUser() { ); // act - userService.signUp(command); + signUpService.signUp(command); // assert User savedUser = userJpaRepository.findByLoginId("testUser123") 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/SignUpServiceTest.java similarity index 86% rename from apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceTest.java index 7b0fcdd2..0bbdd4a9 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/SignUpServiceTest.java @@ -9,18 +9,18 @@ import static org.assertj.core.api.Assertions.assertThat; -public class UserServiceTest { +public class SignUpServiceTest { private InMemoryUserRepository userRepository; private PasswordEncoder passwordEncoder; private SignUpValidator signUpValidator; - private UserService userService; + private SignUpService signUpService; @BeforeEach void setUp() { userRepository = new InMemoryUserRepository(); passwordEncoder = rawPassword -> "encoded_" + rawPassword; signUpValidator = new SignUpValidator(userRepository); - userService = new UserService(signUpValidator, passwordEncoder, userRepository); + signUpService = new SignUpService(signUpValidator, passwordEncoder, userRepository); } @Test @@ -36,7 +36,7 @@ void signUp_encryptsPassword() { ); // act - userService.signUp(command); + signUpService.signUp(command); // assert User savedUser = userRepository.findByLoginId("testUser123").orElse(null); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java index 6690f066..0133aafa 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java @@ -16,14 +16,14 @@ public class SignUpValidatorTest { private InMemoryUserRepository userRepository; private PasswordEncoder passwordEncoder; private SignUpValidator signUpValidator; - private UserService userService; + private SignUpService signUpService; @BeforeEach void setUp() { userRepository = new InMemoryUserRepository(); passwordEncoder = rawPassword -> "encoded_" + rawPassword; signUpValidator = new SignUpValidator(userRepository); - userService = new UserService(signUpValidator, passwordEncoder, userRepository); + signUpService = new SignUpService(signUpValidator, passwordEncoder, userRepository); } @Test @@ -37,7 +37,7 @@ void validate_throwsException_whenLoginIdIsDuplicated() { LocalDate.of(1990, 1, 15), "first@example.com" ); - userService.signUp(firstCommand); + signUpService.signUp(firstCommand); SignUpCommand secondCommand = new SignUpCommand( "duplicateId", From 60aca5fcbcb04e970ce3ccfd82dec30292158b6b Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 17:19:37 +0900 Subject: [PATCH 23/40] =?UTF-8?q?refactor:=20UserFixture=20=EB=82=B4?= =?UTF-8?q?=EC=97=90=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=EB=8A=94=20?= =?UTF-8?q?=EC=95=94=ED=98=B8=ED=99=94=EB=90=9C=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=EC=97=AC=EC=95=BC=20=ED=95=98=EB=AF=80?= =?UTF-8?q?=EB=A1=9C=20=EC=83=81=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/java/com/loopers/domain/user/UserFixture.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java index 89045d0e..b02fa210 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserFixture.java @@ -4,13 +4,13 @@ public class UserFixture { public static final String VALID_LOGIN_ID = "testUser123"; - public static final String VALID_PASSWORD = "ValidPass1!"; - public static final String VALID_NAME = "ํ™๊ธธ๋™"; + public static final String VALID_ENCODED_PASSWORD = "ValidEncodedPass1!"; + public static final String VALID_NAME = "๋ฐ•์ฝ”ํ‹€๋ฆฐ"; public static final LocalDate VALID_BIRTH_DATE = LocalDate.of(1990, 1, 15); public static final String VALID_EMAIL = "test@example.com"; private String loginId = VALID_LOGIN_ID; - private String password = VALID_PASSWORD; + private String password = VALID_ENCODED_PASSWORD; private String name = VALID_NAME; private LocalDate birthDate = VALID_BIRTH_DATE; private String email = VALID_EMAIL; From b4031b7a488a1ed8598f22e5dea9845f101a349d Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 17:20:00 +0900 Subject: [PATCH 24/40] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/user/UserInfo.java | 22 ++++++++++ .../com/loopers/domain/user/UserService.java | 22 ++++++++++ .../interfaces/api/user/dto/UserV1Dto.java | 18 +++++++++ .../loopers/domain/user/UserServiceTest.java | 40 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java 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 new file mode 100644 index 00000000..7e1d4828 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -0,0 +1,22 @@ +package com.loopers.application.user; + +import com.loopers.domain.user.User; + +import java.time.LocalDate; + +public record UserInfo(String loginId, String name, LocalDate birthDate, String email) { + public static UserInfo from(User user) { + return new UserInfo( + user.getLoginId(), + maskLastChar(user.getName()), + user.getBirthDate(), + user.getEmail() + ); + } + private static String maskLastChar(String name) { + if (name == null || name.isBlank()) return name; + if (name.length() == 1) return "*"; + + return name.substring(0, name.length() - 1) + "*"; + } +} 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 new file mode 100644 index 00000000..0d671d32 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -0,0 +1,22 @@ +package com.loopers.domain.user; + +import com.loopers.application.user.UserInfo; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class UserService { + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public UserInfo getMyInfo(String loginId) { + User user = userRepository.findByLoginId(loginId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + return UserInfo.from(user); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java new file mode 100644 index 00000000..02a0ce75 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java @@ -0,0 +1,18 @@ +package com.loopers.interfaces.api.user.dto; + +import com.loopers.application.user.UserInfo; + +import java.time.LocalDate; + +public class UserV1Dto { + public record UserResponse(String loginId, String name, LocalDate birthDate, String email) { + public static UserResponse from(UserInfo info) { + return new UserResponse( + info.loginId(), + info.name(), + info.birthDate(), + info.email() + ); + } + } +} 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 new file mode 100644 index 00000000..9947d73a --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -0,0 +1,40 @@ +package com.loopers.domain.user; + +import com.loopers.application.user.UserInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UserServiceTest { + private InMemoryUserRepository userRepository; + private UserService userService; + + @BeforeEach + void setUp() { + userRepository = new InMemoryUserRepository(); + userService = new UserService(userRepository); + } + + @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ ์‹œ ์ด๋ฆ„์˜ ๋งˆ์ง€๋ง‰ ๊ธ€์ž๋Š” ๋งˆ์Šคํ‚น(*)๋˜์–ด ๋ฐ˜ํ™˜๋œ๋‹ค") + @Test + void getMyInfo_masks_last_character_of_name() { + // given + User user = UserFixture.builder() + .name("ํ…Œ์Šคํ„ฐ") + .build(); + + userRepository.save(user); + + // when + UserInfo myInfo = userService.getMyInfo(user.getLoginId()); + + // then + assertThat(myInfo.name()).isEqualTo("ํ…Œ์Šค*"); + assertThat(myInfo.loginId()).isEqualTo(user.getLoginId()); + assertThat(myInfo.email()).isEqualTo(user.getEmail()); + assertThat(myInfo.birthDate()).isEqualTo(user.getBirthDate()); + } + +} From 7e7e913b6d3fab7c257a97509421a9edb8deee61 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Wed, 4 Feb 2026 17:20:59 +0900 Subject: [PATCH 25/40] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=A0=88=EB=B2=A8=EC=97=90=20@Transactional=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/user/SignUpService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpService.java index e625e0c5..a24218e1 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpService.java @@ -1,10 +1,12 @@ package com.loopers.domain.user; import com.loopers.application.user.SignUpCommand; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@Transactional @RequiredArgsConstructor public class SignUpService { private final SignUpValidator signUpValidator; From 503a581814bbd97106985d479b285c2cdb0adb01 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Thu, 5 Feb 2026 15:51:28 +0900 Subject: [PATCH 26/40] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=82=B4=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20DisplayName=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/domain/user/SignUpServiceIntegrationTest.java | 1 - .../java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java index 2f1c8bce..6766179a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceIntegrationTest.java @@ -15,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest -@ActiveProfiles("local") public class SignUpServiceIntegrationTest { @Autowired private SignUpService signUpService; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java index 8b4a5f7b..14e5e364 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java @@ -49,7 +49,7 @@ void tearDown() { @DisplayName("POST /api/v1/users (ํšŒ์›๊ฐ€์ž…)") @Nested class SignUp { - @DisplayName("์œ ํšจํ•œ ํšŒ์›๊ฐ€์ž… ์š”์ฒญ์ด๋ฉด, 201 Created ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @DisplayName("์œ ํšจํ•œ ํšŒ์›๊ฐ€์ž… ์š”์ฒญ์ด๋ฉด, 201 Created ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnsCreated_whenValidRequest() { // arrange @@ -73,7 +73,7 @@ void returnsCreated_whenValidRequest() { ); } - @DisplayName("loginId๊ฐ€ ๋ˆ„๋ฝ๋˜๋ฉด, 400 Bad Request ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @DisplayName("loginId๊ฐ€ ๋ˆ„๋ฝ๋˜๋ฉด, 400 Bad Request ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnsBadRequest_whenLoginIdIsMissing() { // arrange @@ -93,7 +93,7 @@ void returnsBadRequest_whenLoginIdIsMissing() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } - @DisplayName("์ด๋ฏธ ์กด์žฌํ•˜๋Š” loginId๋กœ ์š”์ฒญํ•˜๋ฉด, 409 Conflict ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.") + @DisplayName("์ด๋ฏธ ์กด์žฌํ•˜๋Š” loginId๋กœ ์š”์ฒญํ•˜๋ฉด, 409 Conflict ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnsConflict_whenLoginIdAlreadyExists() { // arrange From cf55e494e3e8ad894048ad0b9ec3ff99b54230d2 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Thu, 5 Feb 2026 17:13:58 +0900 Subject: [PATCH 27/40] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20PasswordPolicyValidator=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/domain/user/PasswordEncoder.java | 2 +- .../domain/user/PasswordPolicyValidator.java | 28 ++++++++++++++++++ .../loopers/domain/user/SignUpValidator.java | 20 +------------ .../domain/user/UpdatePasswordCommand.java | 6 ++++ .../java/com/loopers/domain/user/User.java | 4 +++ .../com/loopers/domain/user/UserService.java | 16 ++++++++++ .../user/BcryptPasswordEncoder.java | 5 ++++ .../domain/user/SignUpServiceTest.java | 3 +- .../domain/user/SignUpValidatorTest.java | 3 +- .../loopers/domain/user/UserServiceTest.java | 29 ++++++++++++++++++- 10 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java index 7e353b8a..30ea71ff 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java @@ -1,6 +1,6 @@ package com.loopers.domain.user; -@FunctionalInterface public interface PasswordEncoder { String encode(String rawPassword); + boolean matches(String rawPassword, String encodedPassword); } \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java new file mode 100644 index 00000000..ef970bf0 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java @@ -0,0 +1,28 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class PasswordPolicyValidator { + + private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = + DateTimeFormatter.ofPattern("yyyyMMdd"); + + private static final String PASSWORD_PATTERN = + "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; + + private PasswordPolicyValidator() {} + + public static void validate(String password, LocalDate birthDate) { + if (!password.matches(PASSWORD_PATTERN)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); + } + + String birthDateString = birthDate.format(BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK); + if (password.contains(birthDateString)) { + throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } +} \ No newline at end of file diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java index 7d98b133..38c4d2a3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/SignUpValidator.java @@ -7,19 +7,12 @@ import org.springframework.stereotype.Component; import java.time.LocalDate; -import java.time.format.DateTimeFormatter; @Component @RequiredArgsConstructor public class SignUpValidator { private final UserRepository userRepository; - private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = - DateTimeFormatter.ofPattern("yyyyMMdd"); - - private static final String PASSWORD_PATTERN = - "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; - public void validate(SignUpCommand command) { if (userRepository.findByLoginId(command.loginId()).isPresent()) { throw new CoreException(ErrorType.CONFLICT, "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋กœ๊ทธ์ธ ID์ž…๋‹ˆ๋‹ค."); @@ -29,17 +22,6 @@ public void validate(SignUpCommand command) { throw new CoreException(ErrorType.BAD_REQUEST, "์ƒ๋…„์›”์ผ์€ ๋ฏธ๋ž˜์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } - validatePassword(command.password(), command.birthDate()); - } - - private void validatePassword(String password, LocalDate birthDate) { - if (!password.matches(PASSWORD_PATTERN)) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8~16์ž์˜ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."); - } - - String birthDateString = birthDate.format(BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK); - if (password.contains(birthDateString)) { - throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); - } + PasswordPolicyValidator.validate(command.password(), command.birthDate()); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java new file mode 100644 index 00000000..f53d0c1d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java @@ -0,0 +1,6 @@ +package com.loopers.domain.user; + +public record UpdatePasswordCommand( + String loginId, + String newPassword +) {} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 96c73e25..f9a27180 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -88,4 +88,8 @@ private void validateEmail(String email) { throw new CoreException(ErrorType.BAD_REQUEST, "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); } } + + public void updatePassword(String newEncodedPassword) { + this.password = newEncodedPassword; + } } 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 0d671d32..58bf795b 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 @@ -11,6 +11,7 @@ @Service public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; @Transactional(readOnly = true) public UserInfo getMyInfo(String loginId) { @@ -19,4 +20,19 @@ public UserInfo getMyInfo(String loginId) { return UserInfo.from(user); } + + @Transactional + public void updatePassword(UpdatePasswordCommand command) { + String loginId = command.loginId(); + User user = userRepository.findByLoginId(loginId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + + PasswordPolicyValidator.validate(command.newPassword(), user.getBirthDate()); + + if (passwordEncoder.matches(command.newPassword(), user.getPassword())) { + throw new CoreException(ErrorType.BAD_REQUEST, "ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + user.updatePassword(passwordEncoder.encode(command.newPassword())); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.java index b066b416..dc75c1de 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.java @@ -12,4 +12,9 @@ public class BcryptPasswordEncoder implements PasswordEncoder { public String encode(String rawPassword) { return encoder.encode(rawPassword); } + + @Override + public boolean matches(String rawPassword, String encodedPassword) { + return encoder.matches(rawPassword, encodedPassword); + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceTest.java index 0bbdd4a9..b1b09d19 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpServiceTest.java @@ -1,6 +1,7 @@ package com.loopers.domain.user; import com.loopers.application.user.SignUpCommand; +import com.loopers.infrastructure.user.BcryptPasswordEncoder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; @@ -18,7 +19,7 @@ public class SignUpServiceTest { @BeforeEach void setUp() { userRepository = new InMemoryUserRepository(); - passwordEncoder = rawPassword -> "encoded_" + rawPassword; + passwordEncoder = new BcryptPasswordEncoder(); signUpValidator = new SignUpValidator(userRepository); signUpService = new SignUpService(signUpValidator, passwordEncoder, userRepository); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java index 0133aafa..4ac27b40 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/SignUpValidatorTest.java @@ -1,6 +1,7 @@ package com.loopers.domain.user; import com.loopers.application.user.SignUpCommand; +import com.loopers.infrastructure.user.BcryptPasswordEncoder; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import org.junit.jupiter.api.BeforeEach; @@ -21,7 +22,7 @@ public class SignUpValidatorTest { @BeforeEach void setUp() { userRepository = new InMemoryUserRepository(); - passwordEncoder = rawPassword -> "encoded_" + rawPassword; + passwordEncoder = new BcryptPasswordEncoder(); signUpValidator = new SignUpValidator(userRepository); signUpService = new SignUpService(signUpValidator, passwordEncoder, userRepository); } 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 9947d73a..e0cec566 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 @@ -1,20 +1,26 @@ package com.loopers.domain.user; import com.loopers.application.user.UserInfo; +import com.loopers.infrastructure.user.BcryptPasswordEncoder; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; public class UserServiceTest { private InMemoryUserRepository userRepository; + private PasswordEncoder passwordEncoder; private UserService userService; @BeforeEach void setUp() { userRepository = new InMemoryUserRepository(); - userService = new UserService(userRepository); + passwordEncoder = new BcryptPasswordEncoder(); + userService = new UserService(userRepository, passwordEncoder); } @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ ์‹œ ์ด๋ฆ„์˜ ๋งˆ์ง€๋ง‰ ๊ธ€์ž๋Š” ๋งˆ์Šคํ‚น(*)๋˜์–ด ๋ฐ˜ํ™˜๋œ๋‹ค") @@ -37,4 +43,25 @@ void getMyInfo_masks_last_character_of_name() { assertThat(myInfo.birthDate()).isEqualTo(user.getBirthDate()); } + @DisplayName("์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๊ฐ™์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsException_whenNewPasswordSameAsCurrent() { + // given + String rawPassword = "ValidPass1!"; + String encodedPassword = passwordEncoder.encode(rawPassword); + User user = UserFixture.builder() + .password(encodedPassword) + .build(); + userRepository.save(user); + + UpdatePasswordCommand command = new UpdatePasswordCommand(user.getLoginId(), rawPassword); + + // when + CoreException result = assertThrows(CoreException.class, () -> { + userService.updatePassword(command); + }); + + // then + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } } From 7fe825e42889d7a2b442a03f05cfa1607052cf30 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Thu, 5 Feb 2026 17:18:21 +0900 Subject: [PATCH 28/40] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/domain/user/PasswordPolicyValidator.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java index ef970bf0..b1fa4455 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordPolicyValidator.java @@ -7,11 +7,8 @@ public class PasswordPolicyValidator { - private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = - DateTimeFormatter.ofPattern("yyyyMMdd"); - - private static final String PASSWORD_PATTERN = - "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; + private static final DateTimeFormatter BIRTH_DATE_FORMAT_FOR_PASSWORD_CHECK = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?~`]{8,16}$"; private PasswordPolicyValidator() {} @@ -25,4 +22,4 @@ public static void validate(String password, LocalDate birthDate) { throw new CoreException(ErrorType.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } } -} \ No newline at end of file +} From 8182abb5184fc6de620303d5587c40183da24df3 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 11:46:43 +0900 Subject: [PATCH 29/40] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20controller=20=EA=B5=AC=ED=98=84=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=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/user/UserV1Controller.java | 25 +++++ .../user/UserServiceIntegrationTest.java | 52 ++++++++++ .../interfaces/api/UserV1ApiE2ETest.java | 99 +++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java new file mode 100644 index 00000000..1552ff07 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -0,0 +1,25 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.application.user.UserInfo; +import com.loopers.domain.user.UserService; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.user.dto.UserV1Dto; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class UserV1Controller { + + private final UserService userService; + + @GetMapping("/me") + public ApiResponse getMyInfo(@RequestHeader("X-Loopers-LoginId") String loginId) { + UserInfo userInfo = userService.getMyInfo(loginId); + return ApiResponse.success(UserV1Dto.UserResponse.from(userInfo)); + } +} \ No newline at end of file diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java new file mode 100644 index 00000000..77626e49 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -0,0 +1,52 @@ +package com.loopers.domain.user; + +import com.loopers.application.user.UserInfo; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SpringBootTest +class UserServiceIntegrationTest { + + @Autowired + private UserService userService; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @Test + @DisplayName("๋‚ด ์ •๋ณด ์กฐํšŒ ์‹œ ์ด๋ฆ„ ๋งˆ์ง€๋ง‰ ๊ธ€์ž๊ฐ€ ๋งˆ์Šคํ‚น๋œ๋‹ค.") + void getMyInfo_returnsUserInfoWithMaskedName() { + // arrange + String loginId = "testUser123"; + User user = UserFixture.builder() + .loginId(loginId) + .name("๋ฐ•์ž๋ฐ”") + .build(); + userJpaRepository.save(user); + + // act + UserInfo result = userService.getMyInfo(loginId); + + // assert + assertAll( + () -> assertThat(result.loginId()).isEqualTo(loginId), + () -> assertThat(result.name()).isEqualTo("๋ฐ•์ž*") + ); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java new file mode 100644 index 00000000..a1cc2a59 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -0,0 +1,99 @@ +package com.loopers.interfaces.api; + +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserFixture; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.interfaces.api.user.dto.UserV1Dto; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UserV1ApiE2ETest { + + private static final String ENDPOINT_GET_MY_INFO = "/api/v1/users/me"; + + private final TestRestTemplate testRestTemplate; + private final UserJpaRepository userJpaRepository; + private final DatabaseCleanUp databaseCleanUp; + + @Autowired + public UserV1ApiE2ETest( + TestRestTemplate testRestTemplate, + UserJpaRepository userJpaRepository, + DatabaseCleanUp databaseCleanUp + ) { + this.testRestTemplate = testRestTemplate; + this.userJpaRepository = userJpaRepository; + this.databaseCleanUp = databaseCleanUp; + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("GET /api/v1/users/me (๋‚ด ์ •๋ณด ์กฐํšŒ)") + @Nested + class GetMyInfo { + + @DisplayName("์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์กฐํšŒํ•˜๋ฉด, 200 OK์™€ ๋งˆ์Šคํ‚น๋œ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsOk_whenUserExists() { + // arrange + User savedUser = UserFixture.builder() + .loginId("testUser123") + .name("๋ฐ•์ž๋ฐ”") + .build(); + userJpaRepository.save(savedUser); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", savedUser.getLoginId()); + HttpEntity requestEntity = new HttpEntity<>(headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_GET_MY_INFO, HttpMethod.GET, requestEntity, responseType); + + // assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data().loginId()).isEqualTo("testUser123"), + () -> assertThat(response.getBody().data().name()).isEqualTo("๋ฐ•์ž*") + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž๋ฅผ ์กฐํšŒํ•˜๋ฉด, 404 Not Found๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsNotFound_whenUserNotExists() { + // arrange + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", "nonExistingId"); + HttpEntity requestEntity = new HttpEntity<>(headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_GET_MY_INFO, HttpMethod.GET, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + } +} From f3b968c31feb443db665f36236364b37a3750efa Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 11:57:10 +0900 Subject: [PATCH 30/40] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20controller=20=EA=B5=AC=ED=98=84=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=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/user/SignUpCommand.java | 14 ++--- .../api/user/SignUpV1Controller.java | 4 +- .../api/user/dto/CreateUserRequestV1.java | 33 ----------- .../interfaces/api/user/dto/UserV1Dto.java | 23 ++++++++ .../interfaces/api/SignUpV1ApiE2ETest.java | 59 ++++++++++--------- 5 files changed, 62 insertions(+), 71 deletions(-) delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java index eb001bff..12eef0a4 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/SignUpCommand.java @@ -1,6 +1,6 @@ package com.loopers.application.user; -import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.interfaces.api.user.dto.UserV1Dto; import java.time.LocalDate; @@ -11,13 +11,13 @@ public record SignUpCommand( LocalDate birthDate, String email ) { - public static SignUpCommand from(CreateUserRequestV1 request) { + public static SignUpCommand from(UserV1Dto.CreateRequest request) { return new SignUpCommand( - request.getLoginId(), - request.getPassword(), - request.getName(), - request.getBirthDate(), - request.getEmail() + request.loginId(), + request.password(), + request.name(), + request.birthDate(), + request.email() ); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java index f8a1f79a..90ae6e0e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java @@ -3,7 +3,7 @@ import com.loopers.application.user.SignUpCommand; import com.loopers.domain.user.SignUpService; import com.loopers.interfaces.api.ApiResponse; -import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.interfaces.api.user.dto.UserV1Dto; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -22,7 +22,7 @@ public class SignUpV1Controller { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public ApiResponse signUp(@Valid @RequestBody CreateUserRequestV1 request) { + public ApiResponse signUp(@Valid @RequestBody UserV1Dto.CreateRequest request) { SignUpCommand command = SignUpCommand.from(request); signUpService.signUp(command); diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java deleted file mode 100644 index eed614de..00000000 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/CreateUserRequestV1.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.loopers.interfaces.api.user.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDate; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class CreateUserRequestV1 { - @NotBlank(message = "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") - private String loginId; - - @NotBlank(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") - private String password; - - @NotBlank(message = "์ด๋ฆ„์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") - private String name; - - @NotNull(message = "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") - @JsonFormat(pattern = "yyyy-MM-dd") - private LocalDate birthDate; - - @NotBlank(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") - private String email; -} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java index 02a0ce75..6fa7ccb3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/dto/UserV1Dto.java @@ -1,10 +1,33 @@ package com.loopers.interfaces.api.user.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import com.loopers.application.user.UserInfo; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; public class UserV1Dto { + + public record CreateRequest( + @NotBlank(message = "๋กœ๊ทธ์ธ ID๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + String loginId, + @NotBlank(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + String password, + @NotBlank(message = "์ด๋ฆ„์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + String name, + @NotNull(message = "์ƒ๋…„์›”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate birthDate, + @NotBlank(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + String email + ) {} + + public record UpdatePasswordRequest( + @NotBlank(message = "์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") + String newPassword + ) {} + public record UserResponse(String loginId, String name, LocalDate birthDate, String email) { public static UserResponse from(UserInfo info) { return new UserResponse( diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java index 14e5e364..87a8508c 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/SignUpV1ApiE2ETest.java @@ -1,7 +1,7 @@ package com.loopers.interfaces.api; import com.loopers.infrastructure.user.UserJpaRepository; -import com.loopers.interfaces.api.user.dto.CreateUserRequestV1; +import com.loopers.interfaces.api.user.dto.UserV1Dto; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -53,13 +53,13 @@ class SignUp { @Test void returnsCreated_whenValidRequest() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .loginId("testUser123") - .password("ValidPass1!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + UserV1Dto.CreateRequest request = new UserV1Dto.CreateRequest( + "testUser123", + "ValidPass1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; @@ -77,12 +77,13 @@ void returnsCreated_whenValidRequest() { @Test void returnsBadRequest_whenLoginIdIsMissing() { // arrange - CreateUserRequestV1 request = CreateUserRequestV1.builder() - .password("ValidPass1!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("test@example.com") - .build(); + UserV1Dto.CreateRequest request = new UserV1Dto.CreateRequest( + null, + "ValidPass1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "test@example.com" + ); // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; @@ -97,24 +98,24 @@ void returnsBadRequest_whenLoginIdIsMissing() { @Test void returnsConflict_whenLoginIdAlreadyExists() { // arrange - CreateUserRequestV1 firstRequest = CreateUserRequestV1.builder() - .loginId("duplicateId") - .password("ValidPass1!") - .name("๋ฐ•์ž๋ฐ”") - .birthDate(LocalDate.of(1990, 1, 15)) - .email("first@example.com") - .build(); + UserV1Dto.CreateRequest firstRequest = new UserV1Dto.CreateRequest( + "duplicateId", + "ValidPass1!", + "๋ฐ•์ž๋ฐ”", + LocalDate.of(1990, 1, 15), + "first@example.com" + ); testRestTemplate.exchange(ENDPOINT_SIGN_UP, HttpMethod.POST, new HttpEntity<>(firstRequest), new ParameterizedTypeReference>() {}); - CreateUserRequestV1 secondRequest = CreateUserRequestV1.builder() - .loginId("duplicateId") - .password("ValidPass2!") - .name("๊น€์ž๋ฐ”") - .birthDate(LocalDate.of(1995, 5, 20)) - .email("second@example.com") - .build(); + UserV1Dto.CreateRequest secondRequest = new UserV1Dto.CreateRequest( + "duplicateId", + "ValidPass2!", + "๊น€์ž๋ฐ”", + LocalDate.of(1995, 5, 20), + "second@example.com" + ); // act ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; @@ -125,4 +126,4 @@ void returnsConflict_whenLoginIdAlreadyExists() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } } -} +} \ No newline at end of file From 56c0a6b1b5198e4ed85a0bb5571bb3293a748186 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 12:15:57 +0900 Subject: [PATCH 31/40] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=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 --- .../domain/user/UpdatePasswordCommand.java | 8 +- .../interfaces/api/user/UserV1Controller.java | 13 +++ .../user/UserServiceIntegrationTest.java | 26 ++++++ .../com/loopers/domain/user/UserTest.java | 17 ---- .../interfaces/api/UserV1ApiE2ETest.java | 92 +++++++++++++++++++ 5 files changed, 138 insertions(+), 18 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java index f53d0c1d..b2ec13c6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java @@ -1,6 +1,12 @@ package com.loopers.domain.user; +import com.loopers.interfaces.api.user.dto.UserV1Dto; + public record UpdatePasswordCommand( String loginId, String newPassword -) {} +) { + public static UpdatePasswordCommand from(String loginId, UserV1Dto.UpdatePasswordRequest request) { + return new UpdatePasswordCommand(loginId, request.newPassword()); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java index 1552ff07..ea9610b8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -1,11 +1,15 @@ package com.loopers.interfaces.api.user; import com.loopers.application.user.UserInfo; +import com.loopers.domain.user.UpdatePasswordCommand; import com.loopers.domain.user.UserService; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.user.dto.UserV1Dto; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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; @@ -22,4 +26,13 @@ public ApiResponse getMyInfo(@RequestHeader("X-Loopers-L UserInfo userInfo = userService.getMyInfo(loginId); return ApiResponse.success(UserV1Dto.UserResponse.from(userInfo)); } + + @PatchMapping("/me/password") + public ApiResponse updatePassword( + @RequestHeader("X-Loopers-LoginId") String loginId, + @Valid @RequestBody UserV1Dto.UpdatePasswordRequest request + ) { + userService.updatePassword(UpdatePasswordCommand.from(loginId, request)); + return ApiResponse.success(null); + } } \ No newline at end of file diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index 77626e49..d7dd0a9b 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -24,6 +25,8 @@ class UserServiceIntegrationTest { @Autowired private DatabaseCleanUp databaseCleanUp; + private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + @AfterEach void tearDown() { databaseCleanUp.truncateAllTables(); @@ -49,4 +52,27 @@ void getMyInfo_returnsUserInfoWithMaskedName() { () -> assertThat(result.name()).isEqualTo("๋ฐ•์ž*") ); } + + @Test + @DisplayName("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์‹œ ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ์—…๋ฐ์ดํŠธ๋œ๋‹ค.") + void updatePassword_updatesPasswordInDb() { + // arrange + String loginId = "testUser123"; + String oldPassword = bCryptPasswordEncoder.encode("OldPass1!"); + User user = UserFixture.builder() + .loginId(loginId) + .password(oldPassword) + .build(); + userJpaRepository.save(user); + + String newPassword = "NewPass1!"; + UpdatePasswordCommand command = new UpdatePasswordCommand(loginId, newPassword); + + // act + userService.updatePassword(command); + + // assert + User updatedUser = userJpaRepository.findByLoginId(loginId).orElseThrow(); + assertThat(bCryptPasswordEncoder.matches(newPassword, updatedUser.getPassword())).isTrue(); + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index 990127fa..72af6858 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -81,23 +81,6 @@ void throwsBadRequestException_whenNameIsEmpty() { assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); } - @DisplayName("name์— ๊ณต๋ฐฑ์ด ํฌํ•จ๋˜๋ฉด BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") - @Test - void throwsBadRequestException_whenNameContainsWhitespace() { - // arrange - String name = "๋ฐ• ์ž๋ฐ”"; - - // act - CoreException result = assertThrows(CoreException.class, () -> { - UserFixture.builder() - .name(name) - .build(); - }); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); - } - @DisplayName("email์ด @์™€ .์„ ํฌํ•จํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.") @Test void createsUser_whenEmailIsValid() { diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java index a1cc2a59..e06b3d1a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -17,7 +17,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -26,6 +28,7 @@ class UserV1ApiE2ETest { private static final String ENDPOINT_GET_MY_INFO = "/api/v1/users/me"; + private static final String ENDPOINT_UPDATE_PASSWORD = "/api/v1/users/me/password"; private final TestRestTemplate testRestTemplate; private final UserJpaRepository userJpaRepository; @@ -96,4 +99,93 @@ void returnsNotFound_whenUserNotExists() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } } + + @DisplayName("PATCH /api/v1/users/me/password (๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ)") + @Nested + class UpdatePassword { + + private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + + @DisplayName("์œ ํšจํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์š”์ฒญ์ด๋ฉด, 200 OK๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค.") + @Test + void returnsOk_whenValidRequest() { + // arrange + String loginId = "testUser123"; + String oldEncodedPassword = bCryptPasswordEncoder.encode("OldPass1!"); + User savedUser = UserFixture.builder() + .loginId(loginId) + .password(oldEncodedPassword) + .build(); + userJpaRepository.save(savedUser); + + String newPassword = "NewPass1!"; + UserV1Dto.UpdatePasswordRequest request = new UserV1Dto.UpdatePasswordRequest(newPassword); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", loginId); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(request, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_UPDATE_PASSWORD, HttpMethod.PATCH, requestEntity, responseType); + + // assert + User updatedUser = userJpaRepository.findByLoginId(loginId).orElseThrow(); + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(bCryptPasswordEncoder.matches(newPassword, updatedUser.getPassword())).isTrue() + ); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด, 404 Not Found๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsNotFound_whenUserNotExists() { + // arrange + UserV1Dto.UpdatePasswordRequest request = new UserV1Dto.UpdatePasswordRequest("NewPass1!"); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", "nonExistingId"); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(request, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_UPDATE_PASSWORD, HttpMethod.PATCH, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @DisplayName("ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด, 400 Bad Request๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsBadRequest_whenSamePassword() { + // arrange + String loginId = "testUser123"; + String currentPassword = "SamePass1!"; + String encodedPassword = bCryptPasswordEncoder.encode(currentPassword); + User savedUser = UserFixture.builder() + .loginId(loginId) + .password(encodedPassword) + .build(); + userJpaRepository.save(savedUser); + + UserV1Dto.UpdatePasswordRequest request = new UserV1Dto.UpdatePasswordRequest(currentPassword); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", loginId); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(request, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_UPDATE_PASSWORD, HttpMethod.PATCH, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + } } From 5febf4aaf07ac0ee8fcfdff297dde97a57c2c426 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 12:16:09 +0900 Subject: [PATCH 32/40] =?UTF-8?q?refactor:=20User=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/UserService.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 58bf795b..7501d733 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 @@ -15,17 +15,13 @@ public class UserService { @Transactional(readOnly = true) public UserInfo getMyInfo(String loginId) { - User user = userRepository.findByLoginId(loginId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); - + User user = findUserByLoginId(loginId); return UserInfo.from(user); } @Transactional public void updatePassword(UpdatePasswordCommand command) { - String loginId = command.loginId(); - User user = userRepository.findByLoginId(loginId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + User user = findUserByLoginId(command.loginId()); PasswordPolicyValidator.validate(command.newPassword(), user.getBirthDate()); @@ -35,4 +31,9 @@ public void updatePassword(UpdatePasswordCommand command) { user.updatePassword(passwordEncoder.encode(command.newPassword())); } + + private User findUserByLoginId(String loginId) { + return userRepository.findByLoginId(loginId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + } } From 37e4574a7c5fa811f74511506be47d7c72a8fba8 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 12:16:39 +0900 Subject: [PATCH 33/40] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=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/domain/user/UserService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 7501d733..95ffb8ae 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 @@ -34,6 +34,7 @@ public void updatePassword(UpdatePasswordCommand command) { private User findUserByLoginId(String loginId) { return userRepository.findByLoginId(loginId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, + "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); } } From b44396c8e86aa135e65a052363041289842da16d Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 12:26:17 +0900 Subject: [PATCH 34/40] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EB=B0=8F=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/domain/user/UserService.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 95ffb8ae..1736c3fc 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 @@ -13,26 +13,26 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - @Transactional(readOnly = true) - public UserInfo getMyInfo(String loginId) { - User user = findUserByLoginId(loginId); - return UserInfo.from(user); - } - @Transactional public void updatePassword(UpdatePasswordCommand command) { - User user = findUserByLoginId(command.loginId()); - - PasswordPolicyValidator.validate(command.newPassword(), user.getBirthDate()); + User user = getUser(command.loginId()); if (passwordEncoder.matches(command.newPassword(), user.getPassword())) { throw new CoreException(ErrorType.BAD_REQUEST, "ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } - user.updatePassword(passwordEncoder.encode(command.newPassword())); + PasswordPolicyValidator.validate(command.newPassword(), user.getBirthDate()); + String encoded = passwordEncoder.encode(command.newPassword()); + user.updatePassword(encoded); + } + + @Transactional(readOnly = true) + public UserInfo getMyInfo(String loginId) { + User user = getUser(loginId); + return UserInfo.from(user); } - private User findUserByLoginId(String loginId) { + private User getUser(String loginId) { return userRepository.findByLoginId(loginId) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")); From b468aa93893d7d9bee4d5598ba2dccbbfe705b3a Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 13:02:37 +0900 Subject: [PATCH 35/40] =?UTF-8?q?refactor:=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{domain => application}/user/UpdatePasswordCommand.java | 2 +- .../src/main/java/com/loopers/domain/user/UserService.java | 1 + .../com/loopers/interfaces/api/user/UserV1Controller.java | 4 ++-- .../com/loopers/domain/user/UserServiceIntegrationTest.java | 1 + .../test/java/com/loopers/domain/user/UserServiceTest.java | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) rename apps/commerce-api/src/main/java/com/loopers/{domain => application}/user/UpdatePasswordCommand.java (89%) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java similarity index 89% rename from apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java rename to apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java index b2ec13c6..3d99b1b0 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UpdatePasswordCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java @@ -1,4 +1,4 @@ -package com.loopers.domain.user; +package com.loopers.application.user; import com.loopers.interfaces.api.user.dto.UserV1Dto; 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 1736c3fc..77e0d9a5 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 @@ -1,5 +1,6 @@ package com.loopers.domain.user; +import com.loopers.application.user.UpdatePasswordCommand; import com.loopers.application.user.UserInfo; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java index ea9610b8..bd3c8e48 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -1,7 +1,7 @@ package com.loopers.interfaces.api.user; import com.loopers.application.user.UserInfo; -import com.loopers.domain.user.UpdatePasswordCommand; +import com.loopers.application.user.UpdatePasswordCommand; import com.loopers.domain.user.UserService; import com.loopers.interfaces.api.ApiResponse; import com.loopers.interfaces.api.user.dto.UserV1Dto; @@ -35,4 +35,4 @@ public ApiResponse updatePassword( userService.updatePassword(UpdatePasswordCommand.from(loginId, request)); return ApiResponse.success(null); } -} \ No newline at end of file +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index d7dd0a9b..051e7c67 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -1,5 +1,6 @@ package com.loopers.domain.user; +import com.loopers.application.user.UpdatePasswordCommand; import com.loopers.application.user.UserInfo; import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.utils.DatabaseCleanUp; 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 e0cec566..613ff1f9 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 @@ -1,5 +1,6 @@ package com.loopers.domain.user; +import com.loopers.application.user.UpdatePasswordCommand; import com.loopers.application.user.UserInfo; import com.loopers.infrastructure.user.BcryptPasswordEncoder; import com.loopers.support.error.CoreException; From b566e9f2e856a96389b19548759ede45748629a8 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 13:13:57 +0900 Subject: [PATCH 36/40] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=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/user/UserInfo.java | 1 + 1 file changed, 1 insertion(+) 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 7e1d4828..abc8599e 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 @@ -13,6 +13,7 @@ public static UserInfo from(User user) { user.getEmail() ); } + private static String maskLastChar(String name) { if (name == null || name.isBlank()) return name; if (name.length() == 1) return "*"; From 8b1dca951c7fb66ac9cc07fffb37a970277a03f4 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Fri, 6 Feb 2026 13:22:38 +0900 Subject: [PATCH 37/40] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20profile=20=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopers/testcontainers/MySqlTestContainersConfig.java | 1 - .../com/loopers/testcontainers/RedisTestContainersConfig.java | 1 - 2 files changed, 2 deletions(-) diff --git a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java index a4af2b43..bb56084b 100644 --- a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java +++ b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java @@ -6,7 +6,6 @@ import org.testcontainers.utility.DockerImageName; @Configuration -@Profile("test") public class MySqlTestContainersConfig { private static final MySQLContainer mySqlContainer; diff --git a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java index bbd7ab03..3a0a2f17 100644 --- a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java +++ b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java @@ -6,7 +6,6 @@ import org.testcontainers.utility.DockerImageName; @Configuration -@Profile("test") public class RedisTestContainersConfig { private static final RedisContainer redisContainer = new RedisContainer(DockerImageName.parse("redis:latest")); From 24511a7ae57086712005a6c61cfd4ceee679bc43 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 10 Feb 2026 11:20:03 +0900 Subject: [PATCH 38/40] =?UTF-8?q?refactor:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=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=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ํ—ค๋”๋กœ ๋ฐ›์•„์˜จ ์œ ์ €์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ์‹ค์ œ๋กœ ๋””๋น„ ์ €์žฅ๊ฐ’์„ ๋น„๊ตํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€ --- .../user/UpdatePasswordCommand.java | 5 ++- .../com/loopers/domain/user/UserService.java | 4 ++ .../interfaces/api/user/UserV1Controller.java | 3 +- .../user/UserServiceIntegrationTest.java | 7 ++-- .../loopers/domain/user/UserServiceTest.java | 24 ++++++++++- .../interfaces/api/UserV1ApiE2ETest.java | 42 +++++++++++++++++-- 6 files changed, 74 insertions(+), 11 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java index 3d99b1b0..ebf9f0f5 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UpdatePasswordCommand.java @@ -4,9 +4,10 @@ public record UpdatePasswordCommand( String loginId, + String currentPassword, String newPassword ) { - public static UpdatePasswordCommand from(String loginId, UserV1Dto.UpdatePasswordRequest request) { - return new UpdatePasswordCommand(loginId, request.newPassword()); + public static UpdatePasswordCommand from(String loginId, String currentPassword, UserV1Dto.UpdatePasswordRequest request) { + return new UpdatePasswordCommand(loginId, 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 77e0d9a5..160d6a90 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 @@ -18,6 +18,10 @@ public class UserService { public void updatePassword(UpdatePasswordCommand command) { User user = getUser(command.loginId()); + if (!passwordEncoder.matches(command.currentPassword(), user.getPassword())) { + throw new CoreException(ErrorType.BAD_REQUEST, "ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + if (passwordEncoder.matches(command.newPassword(), user.getPassword())) { throw new CoreException(ErrorType.BAD_REQUEST, "ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java index bd3c8e48..d4d219da 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java @@ -30,9 +30,10 @@ public ApiResponse getMyInfo(@RequestHeader("X-Loopers-L @PatchMapping("/me/password") public ApiResponse updatePassword( @RequestHeader("X-Loopers-LoginId") String loginId, + @RequestHeader("X-Loopers-LoginPw") String currentPassword, @Valid @RequestBody UserV1Dto.UpdatePasswordRequest request ) { - userService.updatePassword(UpdatePasswordCommand.from(loginId, request)); + userService.updatePassword(UpdatePasswordCommand.from(loginId, currentPassword, request)); return ApiResponse.success(null); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index 051e7c67..292d9231 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -59,15 +59,16 @@ void getMyInfo_returnsUserInfoWithMaskedName() { void updatePassword_updatesPasswordInDb() { // arrange String loginId = "testUser123"; - String oldPassword = bCryptPasswordEncoder.encode("OldPass1!"); + String currentPassword = "OldPass1!"; + String encode = bCryptPasswordEncoder.encode(currentPassword); User user = UserFixture.builder() .loginId(loginId) - .password(oldPassword) + .password(encode) .build(); userJpaRepository.save(user); String newPassword = "NewPass1!"; - UpdatePasswordCommand command = new UpdatePasswordCommand(loginId, newPassword); + UpdatePasswordCommand command = new UpdatePasswordCommand(loginId, currentPassword, newPassword); // act userService.updatePassword(command); 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 613ff1f9..bf0516c9 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 @@ -44,6 +44,28 @@ void getMyInfo_masks_last_character_of_name() { assertThat(myInfo.birthDate()).isEqualTo(user.getBirthDate()); } + @DisplayName("ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") + @Test + void throwsException_whenCurrentPasswordNotMatches() { + // given + String currentPassword = "ValidPass1!"; + String encodedPassword = passwordEncoder.encode(currentPassword); + User user = UserFixture.builder() + .password(encodedPassword) + .build(); + userRepository.save(user); + + UpdatePasswordCommand command = new UpdatePasswordCommand(user.getLoginId(), "WrongPass1!", "NewPass1!"); + + // when + CoreException result = assertThrows(CoreException.class, () -> { + userService.updatePassword(command); + }); + + // then + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + @DisplayName("์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๊ฐ™์œผ๋ฉด, BAD_REQUEST ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.") @Test void throwsException_whenNewPasswordSameAsCurrent() { @@ -55,7 +77,7 @@ void throwsException_whenNewPasswordSameAsCurrent() { .build(); userRepository.save(user); - UpdatePasswordCommand command = new UpdatePasswordCommand(user.getLoginId(), rawPassword); + UpdatePasswordCommand command = new UpdatePasswordCommand(user.getLoginId(), rawPassword, rawPassword); // when CoreException result = assertThrows(CoreException.class, () -> { diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java index e06b3d1a..66db6ea6 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -111,11 +111,12 @@ class UpdatePassword { void returnsOk_whenValidRequest() { // arrange String loginId = "testUser123"; - String oldEncodedPassword = bCryptPasswordEncoder.encode("OldPass1!"); + String currentPassword = "OldPass1!"; + String encodedPassword = bCryptPasswordEncoder.encode(currentPassword); User savedUser = UserFixture.builder() - .loginId(loginId) - .password(oldEncodedPassword) - .build(); + .loginId(loginId) + .password(encodedPassword) + .build(); userJpaRepository.save(savedUser); String newPassword = "NewPass1!"; @@ -123,6 +124,7 @@ void returnsOk_whenValidRequest() { HttpHeaders headers = new HttpHeaders(); headers.set("X-Loopers-LoginId", loginId); + headers.set("X-Loopers-LoginPw", currentPassword); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity requestEntity = new HttpEntity<>(request, headers); @@ -147,6 +149,7 @@ void returnsNotFound_whenUserNotExists() { HttpHeaders headers = new HttpHeaders(); headers.set("X-Loopers-LoginId", "nonExistingId"); + headers.set("X-Loopers-LoginPw", "OldPass1!"); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity requestEntity = new HttpEntity<>(request, headers); @@ -159,6 +162,36 @@ void returnsNotFound_whenUserNotExists() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } + @DisplayName("ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด, 400 Bad Request๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") + @Test + void returnsBadRequest_whenCurrentPasswordNotMatches() { + // arrange + String loginId = "testUser123"; + String currentPassword = "OldPass1!"; + String encodedPassword = bCryptPasswordEncoder.encode(currentPassword); + User savedUser = UserFixture.builder() + .loginId(loginId) + .password(encodedPassword) + .build(); + userJpaRepository.save(savedUser); + + UserV1Dto.UpdatePasswordRequest request = new UserV1Dto.UpdatePasswordRequest("NewPass1!"); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", loginId); + headers.set("X-Loopers-LoginPw", "WrongPass1!"); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(request, headers); + + // act + ParameterizedTypeReference> responseType = new ParameterizedTypeReference<>() {}; + ResponseEntity> response = + testRestTemplate.exchange(ENDPOINT_UPDATE_PASSWORD, HttpMethod.PATCH, requestEntity, responseType); + + // assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + @DisplayName("ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด, 400 Bad Request๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.") @Test void returnsBadRequest_whenSamePassword() { @@ -176,6 +209,7 @@ void returnsBadRequest_whenSamePassword() { HttpHeaders headers = new HttpHeaders(); headers.set("X-Loopers-LoginId", loginId); + headers.set("X-Loopers-LoginPw", currentPassword); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity requestEntity = new HttpEntity<>(request, headers); From 9f4c1dd094bf7db785a3cad121fc967745c87466 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 10 Feb 2026 11:41:05 +0900 Subject: [PATCH 39/40] =?UTF-8?q?refactor:=20User=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20loginId=20unique=20=EC=A0=9C=EC=95=BD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/domain/user/User.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index f9a27180..a2a7db14 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -4,6 +4,7 @@ import com.loopers.domain.BaseEntity; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Getter; @@ -14,8 +15,7 @@ @Entity @Table(name = "user") public class User extends BaseEntity { - - private Long id; + @Column(unique = true) private String loginId; private String password; // encoded private String name; From 7c681b9dc94c990bb47b50fc4bedd119d5e32619 Mon Sep 17 00:00:00 2001 From: hey-sion Date: Tue, 10 Feb 2026 11:51:14 +0900 Subject: [PATCH 40/40] =?UTF-8?q?refactor:=20User=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=EB=AA=85=20=EB=B3=80=EA=B2=BD(users)=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/loopers/domain/user/User.java | 2 +- .../com/loopers/testcontainers/MySqlTestContainersConfig.java | 1 - .../com/loopers/testcontainers/RedisTestContainersConfig.java | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index a2a7db14..7e57a997 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -13,7 +13,7 @@ @Getter @Entity -@Table(name = "user") +@Table(name = "users") public class User extends BaseEntity { @Column(unique = true) private String loginId; diff --git a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java index bb56084b..9c41edac 100644 --- a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java +++ b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java @@ -1,7 +1,6 @@ package com.loopers.testcontainers; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.utility.DockerImageName; diff --git a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java index 3a0a2f17..35bf94f0 100644 --- a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java +++ b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java @@ -2,7 +2,6 @@ import com.redis.testcontainers.RedisContainer; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; import org.testcontainers.utility.DockerImageName; @Configuration