From f7acbe649d339cc51a7d88c32d28562dfe8af81f Mon Sep 17 00:00:00 2001 From: seonmin Date: Thu, 5 Feb 2026 17:13:06 +0900 Subject: [PATCH 01/11] =?UTF-8?q?CLAUDE=20md=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a93afa17 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,38 @@ +## 개발 규칙 +### 진행 Workflow - 증강 코딩 +- **대원칙** : 방향성 및 주요 의사 결정은 개발자에게 제안만 할 수 있으며, 최종 승인된 사항을 기반으로 작업을 수행. +- **중간 결과 보고** : AI 가 반복적인 동작을 하거나, 요청하지 않은 기능을 구현, 테스트 삭제를 임의로 진행할 경우 개발자가 개입. +- **설계 주도권 유지** : AI 가 임의판단을 하지 않고, 방향성에 대한 제안 등을 진행할 수 있으나 개발자의 승인을 받은 후 수행. + +### 개발 Workflow - TDD (Red > Green > Refactor) +- 모든 테스트는 3A 원칙으로 작성할 것 (Arrange - Act - Assert) +#### 1. Red Phase : 실패하는 테스트 먼저 작성 +- 요구사항을 만족하는 기능 테스트 케이스 작성 +- 테스트 예시 +#### 2. Green Phase : 테스트를 통과하는 코드 작성 +- Red Phase 의 테스트가 모두 통과할 수 있는 코드 작성 +- 오버엔지니어링 금지 +#### 3. Refactor Phase : 불필요한 코드 제거 및 품질 개선 +- 불필요한 private 함수 지양, 객체지향적 코드 작성 +- unused import 제거 +- 성능 최적화 +- 모든 테스트 케이스가 통과해야 함 + +## 주의사항 +### 1. Never Do +- 실제 동작하지 않는 코드, 불필요한 Mock 데이터를 이요한 구현을 하지 말 것 +- null-safety 하지 않게 코드 작성하지 말 것 (Java 의 경우, Optional 을 활용할 것) +- println 코드 남기지 말 것 +- 당장 필요하지 않은 코드는 만들지 말 것 + +### 2. Recommendation +- 실제 API 를 호출해 확인하는 E2E 테스트 코드 작성 +- 재사용 가능한 객체 설계 +- 성능 최적화에 대한 대안 및 제안 +- 개발 완료된 API 의 경우, `.http/**.http` 에 분류해 작성 + +### 3. Priority +1. 실제 동작하는 해결책만 고려 +2. null-safety, thread-safety 고려 +3. 테스트 가능한 구조로 설계 +4. 기존 코드 패턴 분석 후 일관성 유지 \ No newline at end of file From 70c7861929b336fd6d5c178cf12ad117c1b32aac Mon Sep 17 00:00:00 2001 From: seonmin Date: Thu, 5 Feb 2026 17:28:37 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat(signup):=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/commerce-api/build.gradle.kts | 3 + .../com/loopers/domain/user/BirthDate.java | 49 ++++++++++ .../java/com/loopers/domain/user/Email.java | 51 +++++++++++ .../java/com/loopers/domain/user/LoginId.java | 51 +++++++++++ .../java/com/loopers/domain/user/Name.java | 52 +++++++++++ .../com/loopers/domain/user/Password.java | 43 +++++++++ .../loopers/domain/user/PasswordEncoder.java | 8 ++ .../java/com/loopers/domain/user/User.java | 86 ++++++++++++++++++ .../loopers/domain/user/UserRepository.java | 12 +++ .../com/loopers/domain/user/UserService.java | 40 +++++++++ .../user/BcryptPasswordEncoder.java | 21 +++++ .../user/UserJpaRepository.java | 11 +++ .../user/UserRepositoryImpl.java | 26 ++++++ .../loopers/domain/user/BirthDateTest.java | 37 ++++++++ .../com/loopers/domain/user/EmailTest.java | 34 +++++++ .../com/loopers/domain/user/LoginIdTest.java | 47 ++++++++++ .../com/loopers/domain/user/NameTest.java | 45 ++++++++++ .../com/loopers/domain/user/PasswordTest.java | 56 ++++++++++++ .../loopers/domain/user/UserServiceTest.java | 89 +++++++++++++++++++ .../com/loopers/domain/user/UserTest.java | 58 ++++++++++++ .../user/UserRepositoryImplTest.java | 52 +++++++++++ 21 files changed, 871 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/LoginId.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/Password.java 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/BcryptPasswordEncoder.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/BirthDateTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/EmailTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/LoginIdTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/PasswordTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/infrastructure/user/UserRepositoryImplTest.java diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts index 03ce68f0..9ad4d8ea 100644 --- a/apps/commerce-api/build.gradle.kts +++ b/apps/commerce-api/build.gradle.kts @@ -6,6 +6,9 @@ dependencies { implementation(project(":supports:logging")) implementation(project(":supports:monitoring")) + // security + implementation("org.springframework.security:spring-security-crypto") + // web implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-actuator") diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java new file mode 100644 index 00000000..ce7cf7f4 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java @@ -0,0 +1,49 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.Objects; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BirthDate { + + private LocalDate value; + + private BirthDate(LocalDate value) { + this.value = value; + } + + public static BirthDate from(LocalDate value) { + if (value == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 비어있을 수 없습니다."); + } + + if (value.isAfter(LocalDate.now())) { + throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 오늘 이전 날짜만 가능합니다."); + } + + return new BirthDate(value); + } + + public LocalDate asLocalDate() { + return value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + BirthDate birthDate = (BirthDate) o; + return Objects.equals(value, birthDate.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java new file mode 100644 index 00000000..710b903b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java @@ -0,0 +1,51 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.regex.Pattern; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Email { + + private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); + + private String value; + + private Email(String value) { + this.value = value; + } + + public static Email from(String value) { + if (value == null || value.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 비어있을 수 없습니다."); + } + + if (!EMAIL_PATTERN.matcher(value).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "이메일 형식이 올바르지 않습니다."); + } + + return new Email(value); + } + + public String asString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Email email = (Email) o; + return Objects.equals(value, email.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/LoginId.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/LoginId.java new file mode 100644 index 00000000..0195600c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/LoginId.java @@ -0,0 +1,51 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.regex.Pattern; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginId { + + private static final Pattern LOGIN_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{5,20}$"); + + private String value; + + private LoginId(String value) { + this.value = value; + } + + public static LoginId from(String value) { + if (value == null || value.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "로그인ID는 비어있을 수 없습니다."); + } + + if (!LOGIN_ID_PATTERN.matcher(value).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "로그인ID는 5~20자의 영문과 숫자로만 이루어져야 합니다."); + } + + return new LoginId(value); + } + + public String asString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + LoginId loginId = (LoginId) o; + return Objects.equals(value, loginId.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java new file mode 100644 index 00000000..ba67c180 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java @@ -0,0 +1,52 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Objects; +import java.util.regex.Pattern; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Name { + + private static final Pattern NAME_PATTERN = Pattern.compile("^[가-힣]{2,6}$"); + + private String value; + + private Name(String value) { + this.value = value; + } + + + public static Name from(String value) { + if (value == null || value.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "이름은 비어있을 수 없습니다."); + } + + if (!NAME_PATTERN.matcher(value).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "이름은 한글 2~6자만 가능합니다."); + } + + return new Name(value); + } + + public String asString() { + return value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Name name = (Name) o; + return Objects.equals(value, name.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/Password.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/Password.java new file mode 100644 index 00000000..882ad17b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/Password.java @@ -0,0 +1,43 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.regex.Pattern; + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Password { + + private static final Pattern PASSWORD_PATTERN = Pattern.compile("^[a-zA-Z0-9`~!@#$%^&*|'\";:\\\\₩?]{8,16}$"); + + private String value; + + private Password(String value) { + this.value = value; + } + + public static Password of(String value, PasswordEncoder encoder) { + if (value == null || value.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 비어있을 수 없습니다."); + } + + if (!PASSWORD_PATTERN.matcher(value).matches()) { + throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8~16자의 영문대소문자, 숫자, 특수문자만 가능합니다."); + } + + return new Password(encoder.encode(value)); + } + + public String asString() { + return value; + } + + // Password는 저장 시 암호화 되므로 Password 간의 동일성/동등성은 PasswordEncoder 가 확인하는 것으로 한다. + public boolean matches(String raw, PasswordEncoder encoder) { + return encoder.matches(raw, value); + } +} 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..f19bd47c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/PasswordEncoder.java @@ -0,0 +1,8 @@ +package com.loopers.domain.user; + +public interface PasswordEncoder { + + String encode(String raw); + + boolean matches(String raw, String encoded); +} 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..1b823a6c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -0,0 +1,86 @@ +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.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "Users") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseEntity { + + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "login_id", nullable = false, unique = true)) + private LoginId loginId; + + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "password", nullable = false)) + private Password password; + + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "name", nullable = false)) + private Name name; + + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "birth_date", nullable = false)) + private BirthDate birthDate; + + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "email", nullable = false)) + private Email email; + + private User(LoginId loginId, Password password, Name name, BirthDate birthDate, Email email) { + this.loginId = loginId; + this.password = password; + this.name = name; + this.birthDate = birthDate; + this.email = email; + } + + public static User create(LoginId loginId, Password password, Name name, BirthDate birthDate, Email email) { + if (loginId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "로그인ID는 필수입니다."); + } + + if (password == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 필수입니다."); + } + + if (name == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "이름은 필수입니다."); + } + + if (birthDate == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 필수입니다."); + } + + if (email == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 필수입니다."); + } + + return new User(loginId, password, name, birthDate, email); + } + + public LoginId loginId() { + return loginId; + } + + public Password password() { + return password; + } + + public Name name() { + return name; + } + + public BirthDate birthDate() { + return birthDate; + } + + public Email email() { + return email; + } +} 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..1a611c08 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -0,0 +1,12 @@ +package com.loopers.domain.user; + +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +public interface UserRepository { + + User save(User user); + + Optional findByLoginId(LoginId 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 new file mode 100644 index 00000000..60cb8b56 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -0,0 +1,40 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + + +@Service +@RequiredArgsConstructor +public class UserService { + + private final PasswordEncoder passwordEncoder; + private final UserRepository userRepository; + + public LoginId signup(String loginId, String password, String name, LocalDate birthDate, String email) { + LoginId id = LoginId.from(loginId); + + if (userRepository.findByLoginId(id).isPresent()) { + throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인ID입니다."); + } + + String birthDateWithDash = birthDate.toString(); + String birthDateWithOutDash = birthDateWithDash.replace("-", ""); + + if (password.contains(birthDateWithDash) || password.contains(birthDateWithOutDash)) { + throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 생년월일을 포함할 수 없습니다."); + } + + User user = User.create(id + , Password.of(password, passwordEncoder) + , Name.from(name) + , BirthDate.from(birthDate) + , Email.from(email)); + + return user.loginId(); + } +} 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..0df1e80a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/BcryptPasswordEncoder.java @@ -0,0 +1,21 @@ +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 raw) { + return encoder.encode(raw); + } + + @Override + public boolean matches(String raw, String encoded) { + return encoder.matches(raw, encoded); + } +} 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..eafbcba1 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -0,0 +1,11 @@ +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 findByLoginIdValue(String value); +} 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..dd6ebd3d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -0,0 +1,26 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.LoginId; +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 User save(User user) { + return userJpaRepository.save(user); + } + + @Override + public Optional findByLoginId(LoginId loginId) { + return userJpaRepository.findByLoginIdValue(loginId.asString()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/BirthDateTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/BirthDateTest.java new file mode 100644 index 00000000..7d33e056 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/BirthDateTest.java @@ -0,0 +1,37 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class BirthDateTest { + + @Test + void 생년월일이_미래면_BAD_REQUEST를_던진다() { + // given + LocalDate value = LocalDate.now().plusDays(1); + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + BirthDate.from(value); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 생년월일이_오늘날짜_이전이면_정상적으로_생성() { + // given + LocalDate value = LocalDate.of(1996, 11, 22); + + // when + BirthDate birthDate = BirthDate.from(value); + + // then + assertThat(birthDate.asLocalDate()).isEqualTo(value); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/EmailTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/EmailTest.java new file mode 100644 index 00000000..f8deb129 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/EmailTest.java @@ -0,0 +1,34 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +public class EmailTest { + + @Test + void 이메일_형식을_따르지_않으면_BAD_REQUEST를_던진다() { + // given + String value = "testloopers.im"; + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + Email.from(value); + }); + } + + @Test + void 이메일_형식을_따르면_객체생성_성공() { + // given + String value = "test@loopers.im"; + + // when + Email email = Email.from(value); + + // then + assertThat(email.asString()).isEqualTo(value); + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/LoginIdTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/LoginIdTest.java new file mode 100644 index 00000000..34f0eeb0 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/LoginIdTest.java @@ -0,0 +1,47 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class LoginIdTest { + + @Test + void 로그인ID는_한글이_포함되면_BAD_REQUSET를_던진다() { + // given + String value = "루퍼스123loopers"; + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + LoginId.from(value); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 로그인ID가_영문과_숫자로만_이루어지면_객체생성_성공() { + // given + String value = "loopers123"; + + // when + LoginId loginId = LoginId.from(value); + + // then + assertThat(loginId.asString()).isEqualTo(value); + } + + @ParameterizedTest + @ValueSource(strings = {"loop", "loopersloopersloopers"}) + void 로그인ID는_5자미만_20자초과이면_BAD_REQUEST를_던진다(String value) { + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + LoginId.from(value); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java new file mode 100644 index 00000000..18f04d54 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java @@ -0,0 +1,45 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class NameTest { + + @ParameterizedTest + @ValueSource(strings = {"김", "김가나다라마바"}) + void 이름이_2자미만_6자초과이면_BAD_REQUEST를_던진다(String value) { + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + Name.from(value); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @ParameterizedTest + @ValueSource(strings = {"김loop", "김123"}) + void 이름에_영어나_숫자가_포함되면_BAD_REQUEST를_던진다(String value) { + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + Name.from(value); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 이름이_한글_2자이상_6자이하이면_객체생성_성공() { + // given + String value = "루퍼스"; + + // when + Name name = Name.from(value); + + // then + assertThat(name.asString()).isEqualTo(value); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/PasswordTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/PasswordTest.java new file mode 100644 index 00000000..73ad2e99 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/PasswordTest.java @@ -0,0 +1,56 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PasswordTest { + + @ParameterizedTest + @ValueSource(strings = {"1234567", "12345678123456789"}) + void 비밀번호가_8자미만_16자초과이면_BAD_REQUEST를_던진다(String value) { + // given + PasswordEncoder encoder = mock(PasswordEncoder.class); + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + Password.of(value, encoder); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 비밀번호에_한글이_포함되면_BAD_REQUEST를_던진다() { + // given + String value = "비밀번호486"; + PasswordEncoder encoder = mock(PasswordEncoder.class); + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + Password.of(value, encoder); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + + @Test + void 유효한_비밀번호이면_암호화해서_객체생성_성공() { + // given + String value = "loopers123"; + PasswordEncoder encoder = mock(PasswordEncoder.class); + when(encoder.encode(value)).thenReturn("encrypted"); + + // when + Password password = Password.of(value, encoder); + + // then + assertThat(password.asString()).isEqualTo("encrypted"); + } +} 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..b6f80f08 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -0,0 +1,89 @@ +package com.loopers.domain.user; + +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.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UserServiceTest { + + UserService userService; + PasswordEncoder passwordEncoder; + UserRepository userRepository; + + @BeforeEach + void beforeEach() { + passwordEncoder = mock(PasswordEncoder.class); + userRepository = mock(UserRepository.class); + userService = new UserService(passwordEncoder, userRepository); + } + + @DisplayName("회원가입 시, ") + @Nested + class Signup { + @Test + void 중복된_로그인ID면_CONFLICT를_던진다() { + // given + String loginId = "loopers123"; + String password = "loopers123!@"; + String name = "루퍼스"; + LocalDate birthDate = LocalDate.of(1996, 11, 22); + String email = "test@loopers.im"; + + when(userRepository.findByLoginId(LoginId.from(loginId))).thenReturn(Optional.of(mock(User.class))); + + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + userService.signup(loginId, password, name, birthDate, email); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.CONFLICT); + } + + @Test + void 생년월일이_비밀번호에_포함되면_BAD_REQUEST를_던진다() { + // given + String loginId = "loopers123"; + String password = "lo19961122@"; + String name = "루퍼스"; + LocalDate birthDate = LocalDate.of(1996, 11, 22); + String email = "test@loopers.im"; + + when(userRepository.findByLoginId(LoginId.from(loginId))).thenReturn(Optional.empty()); + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + userService.signup(loginId, password, name, birthDate, email); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 정상적으로_되면_회원객체를_생성해서_반환() { + // given + String loginId = "loopers123"; + String password = "loopers123!@"; + String name = "루퍼스"; + LocalDate birthDate = LocalDate.of(1996, 11, 22); + String email = "test@loopers.im"; + + when(userRepository.findByLoginId(LoginId.from(loginId))).thenReturn(Optional.empty()); + + // when + LoginId returnLoginId = userService.signup(loginId, password, name, birthDate, email); + + // then + assertThat(returnLoginId).isEqualTo(LoginId.from(loginId)); + } + } + +} 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..87714c93 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -0,0 +1,58 @@ +package com.loopers.domain.user; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UserTest { + + @Test + void 로그인ID_비밀번호_이름_생년월일_이메일이_모두_주어지면_정상적으로_생성() { + // given + PasswordEncoder encoder = mock(PasswordEncoder.class); + when(encoder.encode("loopers123")).thenReturn("encrypted"); + when(encoder.matches("loopers123", "encrypted")).thenReturn(true); + + LoginId loginId = LoginId.from("loopers"); + Password password = Password.of("loopers123", encoder); + Name name = Name.from("루퍼스"); + BirthDate birthDate = BirthDate.from(LocalDate.of(1996, 11, 22)); + Email email = Email.from("tester@loopers.im"); + + // when + User user = User.create(loginId, password, name, birthDate, email); + + // then + assertAll( + () -> assertThat(user.loginId()).isEqualTo(loginId), + () -> assertThat(user.password().matches("loopers123", encoder)).isTrue(), + () -> assertThat(user.name()).isEqualTo(name), + () -> assertThat(user.birthDate()).isEqualTo(birthDate), + () -> assertThat(user.email()).isEqualTo(email) + ); + } + + @Test + void 로그인ID_비밀번호_이름_생년월일_이메일_중_하나라도_null이면_BAD_REQUEST를_던진다() { + // given + PasswordEncoder encoder = mock(PasswordEncoder.class); + + LoginId loginId = LoginId.from("loopers"); + Password password = Password.of("loopers123", encoder); + Name name = null; + BirthDate birthDate = BirthDate.from(LocalDate.of(1996, 11, 22)); + Email email = Email.from("tester@loopers.im"); + + // when-then + CoreException result = assertThrows(CoreException.class, () -> { + User.create(loginId, password, name, birthDate, email); + }); + assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/infrastructure/user/UserRepositoryImplTest.java b/apps/commerce-api/src/test/java/com/loopers/infrastructure/user/UserRepositoryImplTest.java new file mode 100644 index 00000000..580fa10b --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/infrastructure/user/UserRepositoryImplTest.java @@ -0,0 +1,52 @@ +package com.loopers.infrastructure.user; + +import com.loopers.domain.user.*; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@SpringBootTest +@Transactional +class UserRepositoryImplTest { + + @Autowired + UserRepositoryImpl userRepository; + + @Test + void 존재하는_loginId로_조회하면_User를_반환() { + // given + PasswordEncoder encoder = mock(PasswordEncoder.class); + when(encoder.encode("loopers123")).thenReturn("encrypted"); + + LoginId loginId = LoginId.from("loopers"); + Password password = Password.of("loopers123", encoder); + Name name = Name.from("루퍼스"); + BirthDate birthDate = BirthDate.from(LocalDate.of(1996, 11, 22)); + Email email = Email.from("test@loopers.im"); + + User user = User.create(loginId, password, name, birthDate, email); + userRepository.save(user); + + // when + Optional findUser = userRepository.findByLoginId(loginId); + + // then + assertThat(findUser).isPresent(); + } + + @Test + void 존재하지_않는_loginId로_조회하면_빈_Optional_반환() { + // given-when + Optional findUser = userRepository.findByLoginId(LoginId.from("loop123")); + + // then + assertThat(findUser).isEmpty(); + } +} From 2a0de079c54beb6d8c3599a44351584755a6cab5 Mon Sep 17 00:00:00 2001 From: seonmin Date: Thu, 5 Feb 2026 21:12:06 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat(signup):=20API=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/User.java | 2 +- .../com/loopers/domain/user/UserService.java | 2 + .../interfaces/api/user/UserApiSpec.java | 20 +++ .../interfaces/api/user/UserController.java | 33 +++++ .../loopers/interfaces/api/user/UserDto.java | 17 +++ .../user/UserServiceIntegrationTest.java | 70 +++++++++++ .../interfaces/api/user/UserApiE2ETest.java | 119 ++++++++++++++++++ .../api/user/UserControllerTest.java | 63 ++++++++++ 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.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/user/UserApiE2ETest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.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 1b823a6c..fb362e3a 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 @@ -8,7 +8,7 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "Users") +@Table(name = "users") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class User extends BaseEntity { 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 60cb8b56..a67a47e3 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 @@ -35,6 +35,8 @@ public LoginId signup(String loginId, String password, String name, LocalDate bi , BirthDate.from(birthDate) , Email.from(email)); + userRepository.save(user); + return user.loginId(); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java new file mode 100644 index 00000000..c6a89283 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java @@ -0,0 +1,20 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "User API", description = "회원 서비스 API") +public interface UserApiSpec { + + @Operation( + summary = "회원 가입", + description = "로그인ID, 비밀번호, 이름, 생년월일, 이메일을 입력해서 회원가입을 합니다." + ) + ApiResponse signup( + @Schema(name = "회원가입 요청", description = "회원가입 시 필요한 정보") + UserDto.SignupRequest signRequest + ); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java new file mode 100644 index 00000000..8290b3c7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java @@ -0,0 +1,33 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.domain.user.LoginId; +import com.loopers.domain.user.UserService; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController implements UserApiSpec { + + private final UserService userService; + + @PostMapping + @Override + public ApiResponse signup(@RequestBody UserDto.SignupRequest signRequest) { + LoginId loginId = userService.signup( + signRequest.loginId(), + signRequest.password(), + signRequest.name(), + signRequest.birthDate(), + signRequest.email() + ); + + UserDto.SignupResponse response = UserDto.SignupResponse.from(loginId); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java new file mode 100644 index 00000000..ee02c4da --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java @@ -0,0 +1,17 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.domain.user.LoginId; + +import java.time.LocalDate; + +public class UserDto { + + public record SignupRequest(String loginId, String password, String name, LocalDate birthDate, String email) { + } + + public record SignupResponse(String loginId) { + public static SignupResponse from(LoginId loginId) { + return new SignupResponse(loginId.asString()); + } + } +} 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..42fbc66f --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -0,0 +1,70 @@ +package com.loopers.domain.user; + +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class UserServiceIntegrationTest { + + @Autowired + UserService userService; + + @Autowired + UserJpaRepository userJpaRepository; + + @Autowired + DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("회원가입 시,") + @Nested + class Signup { + + @Test + void 성공하면_LoginId를_반환하고_DB에_저장된다() { + // Arrange + String loginId = "loopers123"; + String password = "loopers123!@"; + String name = "루퍼스"; + LocalDate birthDate = LocalDate.of(1996, 11, 22); + String email = "test@loopers.im"; + + // Act + LoginId result = userService.signup(loginId, password, name, birthDate, email); + + // Assert + assertThat(result).isEqualTo(LoginId.from(loginId)); + + Optional savedUser = userJpaRepository.findByLoginIdValue(loginId); + assertThat(savedUser).isPresent(); + } + + @Test + void 중복된_로그인ID면_CONFLICT를_던진다() { + // Arrange + String loginId = "loopers123"; + userService.signup(loginId, "loopers123!@", "루퍼스", LocalDate.of(1996, 11, 22), "test@loopers.im"); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.signup(loginId, "otherPass123!", "다른이름", LocalDate.of(2000, 1, 1), "other@loopers.im"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.CONFLICT); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java new file mode 100644 index 00000000..322a484f --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java @@ -0,0 +1,119 @@ +package com.loopers.interfaces.api.user; + +import com.loopers.interfaces.api.ApiResponse; +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.*; + +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 UserApiE2ETest { + + private static final String ENDPOINT = "/api/users"; + + @Autowired + TestRestTemplate testRestTemplate; + + @Autowired + DatabaseCleanUp databaseCleanUp; + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("POST /api/users") + @Nested + class Signup { + + @Test + void 정상_요청이면_200_OK와_loginId를_반환한다() { + // Arrange + UserDto.SignupRequest request = new UserDto.SignupRequest( + "loopers123", "loopers123!@", "루퍼스", + LocalDate.of(1996, 11, 22), "test@loopers.im" + ); + + // Act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(request, jsonHeaders()), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(response.getBody().meta().result()).isEqualTo(ApiResponse.Metadata.Result.SUCCESS), + () -> assertThat(response.getBody().data().loginId()).isEqualTo("loopers123") + ); + } + + @Test + void 중복된_로그인ID면_409_CONFLICT를_반환한다() { + // Arrange + UserDto.SignupRequest first = new UserDto.SignupRequest( + "loopers123", "loopers123!@", "루퍼스", + LocalDate.of(1996, 11, 22), "test@loopers.im" + ); + testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(first, jsonHeaders()), + new ParameterizedTypeReference>() {} + ); + + UserDto.SignupRequest duplicate = new UserDto.SignupRequest( + "loopers123", "otherPass123!", "다른이름", + LocalDate.of(2000, 1, 1), "other@loopers.im" + ); + + // Act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(duplicate, jsonHeaders()), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT), + () -> assertThat(response.getBody().meta().result()).isEqualTo(ApiResponse.Metadata.Result.FAIL) + ); + } + + @Test + void 잘못된_입력값이면_400_BAD_REQUEST를_반환한다() { + // Arrange + UserDto.SignupRequest request = new UserDto.SignupRequest( + "ab", "loopers123!@", "루퍼스", + LocalDate.of(1996, 11, 22), "test@loopers.im" + ); + + // Act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(request, jsonHeaders()), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST), + () -> assertThat(response.getBody().meta().result()).isEqualTo(ApiResponse.Metadata.Result.FAIL) + ); + } + } + + private HttpHeaders jsonHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java new file mode 100644 index 00000000..4175cd4c --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java @@ -0,0 +1,63 @@ +package com.loopers.interfaces.api.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.loopers.domain.user.LoginId; +import com.loopers.domain.user.UserService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @MockitoBean + UserService userService; + + @DisplayName("회원가입 시,") + @Nested + class Signup { + + @Test + void 성공하면_200_OK() throws Exception { + // given + UserDto.SignupRequest request = new UserDto.SignupRequest( + "looper123", + "password123", + "루퍼스", + LocalDate.of(1996, 11, 22), + "test@loopers.im" + ); + + when(userService.signup(request.loginId(), request.password(), request.name(), request.birthDate(), request.email())) + .thenReturn(LoginId.from("looper123")); + + String content = objectMapper.writeValueAsString(request); + + // when-then + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.loginId").value("looper123")); + } + } + +} From 2c0219941c637d160d56fad2f9a9b80448691dbd Mon Sep 17 00:00:00 2001 From: seonmin Date: Thu, 5 Feb 2026 23:29:22 +0900 Subject: [PATCH 04/11] =?UTF-8?q?chore:=20claude=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20ignore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ CLAUDE.md | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5a979af6..71d6abd4 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ out/ ### Kotlin ### .kotlin + +### CLAUDE ### +.claude diff --git a/CLAUDE.md b/CLAUDE.md index a93afa17..5ec31561 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,4 +35,7 @@ 1. 실제 동작하는 해결책만 고려 2. null-safety, thread-safety 고려 3. 테스트 가능한 구조로 설계 -4. 기존 코드 패턴 분석 후 일관성 유지 \ No newline at end of file +4. 기존 코드 패턴 분석 후 일관성 유지 + +## 소통규칙 +- 개발자의 의사를 확인해야 할 질문이 필요한 경우 사운드를 내서 알람을 준다 \ No newline at end of file From 79a0dbd6541558b390685e98096f1ecf5da4d58b Mon Sep 17 00:00:00 2001 From: seonmin Date: Thu, 5 Feb 2026 23:30:04 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat(my-info):=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/Name.java | 4 ++ .../com/loopers/domain/user/UserInfo.java | 15 +++++ .../com/loopers/domain/user/UserService.java | 13 ++++ .../com/loopers/support/error/ErrorType.java | 1 + .../com/loopers/domain/user/NameTest.java | 12 ++++ .../loopers/domain/user/UserServiceTest.java | 61 +++++++++++++++++++ 6 files changed, 106 insertions(+) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserInfo.java diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java index ba67c180..7208215a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/Name.java @@ -38,6 +38,10 @@ public String asString() { return value; } + public String masked() { + return value.substring(0, value.length() - 1) + "*"; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserInfo.java new file mode 100644 index 00000000..3f48de19 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserInfo.java @@ -0,0 +1,15 @@ +package com.loopers.domain.user; + +import java.time.LocalDate; + +public record UserInfo(String loginId, String maskedName, LocalDate birthDate, String email) { + + public static UserInfo from(User user) { + return new UserInfo( + user.loginId().asString(), + user.name().masked(), + user.birthDate().asLocalDate(), + user.email().asString() + ); + } +} 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 a67a47e3..6e3f1e11 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 @@ -39,4 +39,17 @@ public LoginId signup(String loginId, String password, String name, LocalDate bi return user.loginId(); } + + public UserInfo getMyInfo(String loginId, String password) { + LoginId id = LoginId.from(loginId); + + User user = userRepository.findByLoginId(id) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 사용자입니다.")); + + if (!user.password().matches(password, passwordEncoder)) { + throw new CoreException(ErrorType.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."); + } + + return UserInfo.from(user); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java index 5d142efb..1d3a7c9e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java +++ b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java @@ -11,6 +11,7 @@ public enum ErrorType { INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."), BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."), NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED.getReasonPhrase(), "인증에 실패했습니다."), CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."); private final HttpStatus status; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java index 18f04d54..c2109501 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/NameTest.java @@ -42,4 +42,16 @@ class NameTest { // then assertThat(name.asString()).isEqualTo(value); } + + @Test + void 마스킹하면_마지막_글자가_별표로_대체된다() { + // Arrange + Name name = Name.from("루퍼스"); + + // Act + String masked = name.masked(); + + // Assert + assertThat(masked).isEqualTo("루퍼*"); + } } 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 b6f80f08..0322e7aa 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 @@ -86,4 +86,65 @@ class Signup { } } + @DisplayName("내 정보 조회 시, ") + @Nested + class GetMyInfo { + + @Test + void 존재하지_않는_loginId면_NOT_FOUND를_던진다() { + // Arrange + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.empty()); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.getMyInfo("loopers123", "loopers123!@"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + + @Test + void 비밀번호가_불일치하면_UNAUTHORIZED를_던진다() { + // Arrange + User user = User.create( + LoginId.from("loopers123"), + Password.of("loopers123!@", passwordEncoder), + Name.from("루퍼스"), + BirthDate.from(LocalDate.of(1996, 11, 22)), + Email.from("test@loopers.im") + ); + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("wrongPass123", user.password().asString())).thenReturn(false); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.getMyInfo("loopers123", "wrongPass123"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED); + } + + @Test + void 정상_인증이면_UserInfo를_반환한다() { + // Arrange + when(passwordEncoder.encode("loopers123!@")).thenReturn("encoded"); + User user = User.create( + LoginId.from("loopers123"), + Password.of("loopers123!@", passwordEncoder), + Name.from("루퍼스"), + BirthDate.from(LocalDate.of(1996, 11, 22)), + Email.from("test@loopers.im") + ); + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("loopers123!@", "encoded")).thenReturn(true); + + // Act + UserInfo result = userService.getMyInfo("loopers123", "loopers123!@"); + + // Assert + assertThat(result.loginId()).isEqualTo("loopers123"); + assertThat(result.maskedName()).isEqualTo("루퍼*"); + assertThat(result.birthDate()).isEqualTo(LocalDate.of(1996, 11, 22)); + assertThat(result.email()).isEqualTo("test@loopers.im"); + } + } + } From 42cceaa9d9d26efb4c9cd6f2f72ff7503d3a1799 Mon Sep 17 00:00:00 2001 From: seonmin Date: Thu, 5 Feb 2026 23:41:29 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat(my-info):=20API=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/user/UserController.java | 14 ++++++++--- .../loopers/interfaces/api/user/UserDto.java | 12 +++++++++ .../api/user/UserControllerTest.java | 25 +++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java index 8290b3c7..b4f85428 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java @@ -4,10 +4,8 @@ import com.loopers.domain.user.UserService; import com.loopers.interfaces.api.ApiResponse; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import com.loopers.domain.user.UserInfo; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/users") @@ -30,4 +28,12 @@ public ApiResponse signup(@RequestBody UserDto.SignupReq UserDto.SignupResponse response = UserDto.SignupResponse.from(loginId); return ApiResponse.success(response); } + + @GetMapping("/me") + public ApiResponse getMyInfo( + @RequestHeader("X-Loopers-LoginId") String loginId, + @RequestHeader("X-Loopers-LoginPw") String password) { + UserInfo userInfo = userService.getMyInfo(loginId, password); + return ApiResponse.success(UserDto.MyInfoResponse.from(userInfo)); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java index ee02c4da..0dcc842f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java @@ -1,6 +1,7 @@ package com.loopers.interfaces.api.user; import com.loopers.domain.user.LoginId; +import com.loopers.domain.user.UserInfo; import java.time.LocalDate; @@ -14,4 +15,15 @@ public static SignupResponse from(LoginId loginId) { return new SignupResponse(loginId.asString()); } } + + public record MyInfoResponse(String loginId, String name, LocalDate birthDate, String email) { + public static MyInfoResponse from(UserInfo userInfo) { + return new MyInfoResponse( + userInfo.loginId(), + userInfo.maskedName(), + userInfo.birthDate(), + userInfo.email() + ); + } + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java index 4175cd4c..65106b0a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java @@ -14,7 +14,10 @@ import java.time.LocalDate; +import com.loopers.domain.user.UserInfo; + import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -60,4 +63,26 @@ class Signup { } } + @DisplayName("내 정보 조회 시,") + @Nested + class GetMyInfo { + + @Test + void 성공하면_200_OK와_유저정보를_반환한다() throws Exception { + // Arrange + UserInfo userInfo = new UserInfo("loopers123", "루퍼*", LocalDate.of(1996, 11, 22), "test@loopers.im"); + when(userService.getMyInfo("loopers123", "loopers123!@")).thenReturn(userInfo); + + // Act & Assert + mockMvc.perform(get("/api/users/me") + .header("X-Loopers-LoginId", "loopers123") + .header("X-Loopers-LoginPw", "loopers123!@")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.loginId").value("loopers123")) + .andExpect(jsonPath("$.data.name").value("루퍼*")) + .andExpect(jsonPath("$.data.birthDate").value("1996-11-22")) + .andExpect(jsonPath("$.data.email").value("test@loopers.im")); + } + } + } From 798b687c9af1530a2df06480a111f34e8374e205 Mon Sep 17 00:00:00 2001 From: seonmin Date: Thu, 5 Feb 2026 23:56:28 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat(my-info):=20=ED=86=B5=ED=95=A9/E2E?= =?UTF-8?q?=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 --- .../user/UserServiceIntegrationTest.java | 41 ++++++++++ .../interfaces/api/user/UserApiE2ETest.java | 80 +++++++++++++++++++ 2 files changed, 121 insertions(+) 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 42fbc66f..f80be58e 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 @@ -67,4 +67,45 @@ class Signup { assertThat(exception.getErrorType()).isEqualTo(ErrorType.CONFLICT); } } + + @DisplayName("내 정보 조회 시,") + @Nested + class GetMyInfo { + + @Test + void 정상_인증이면_UserInfo를_반환한다() { + // Arrange + userService.signup("loopers123", "loopers123!@", "루퍼스", LocalDate.of(1996, 11, 22), "test@loopers.im"); + + // Act + UserInfo result = userService.getMyInfo("loopers123", "loopers123!@"); + + // Assert + assertThat(result.loginId()).isEqualTo("loopers123"); + assertThat(result.maskedName()).isEqualTo("루퍼*"); + assertThat(result.birthDate()).isEqualTo(LocalDate.of(1996, 11, 22)); + assertThat(result.email()).isEqualTo("test@loopers.im"); + } + + @Test + void 존재하지_않는_loginId면_NOT_FOUND를_던진다() { + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.getMyInfo("nonexist12", "loopers123!@"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + + @Test + void 비밀번호가_불일치하면_UNAUTHORIZED를_던진다() { + // Arrange + userService.signup("loopers123", "loopers123!@", "루퍼스", LocalDate.of(1996, 11, 22), "test@loopers.im"); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.getMyInfo("loopers123", "wrongPass123!"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED); + } + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java index 322a484f..aaf9ce92 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java @@ -111,6 +111,86 @@ class Signup { } } + @DisplayName("GET /api/users/me") + @Nested + class GetMyInfo { + + @Test + void 정상_인증이면_200_OK와_유저정보를_반환한다() { + // Arrange + UserDto.SignupRequest signupRequest = new UserDto.SignupRequest( + "loopers123", "loopers123!@", "루퍼스", + LocalDate.of(1996, 11, 22), "test@loopers.im" + ); + testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(signupRequest, jsonHeaders()), + new ParameterizedTypeReference>() {} + ); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", "loopers123"); + headers.set("X-Loopers-LoginPw", "loopers123!@"); + + // Act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT + "/me", HttpMethod.GET, new HttpEntity<>(null, headers), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(response.getBody().data().loginId()).isEqualTo("loopers123"), + () -> assertThat(response.getBody().data().name()).isEqualTo("루퍼*"), + () -> assertThat(response.getBody().data().birthDate()).isEqualTo(LocalDate.of(1996, 11, 22)), + () -> assertThat(response.getBody().data().email()).isEqualTo("test@loopers.im") + ); + } + + @Test + void 존재하지_않는_loginId면_404_NOT_FOUND를_반환한다() { + // Arrange + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", "nonexist12"); + headers.set("X-Loopers-LoginPw", "loopers123!@"); + + // Act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT + "/me", HttpMethod.GET, new HttpEntity<>(null, headers), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void 비밀번호가_불일치하면_401_UNAUTHORIZED를_반환한다() { + // Arrange + UserDto.SignupRequest signupRequest = new UserDto.SignupRequest( + "loopers123", "loopers123!@", "루퍼스", + LocalDate.of(1996, 11, 22), "test@loopers.im" + ); + testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(signupRequest, jsonHeaders()), + new ParameterizedTypeReference>() {} + ); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Loopers-LoginId", "loopers123"); + headers.set("X-Loopers-LoginPw", "wrongPass123!"); + + // Act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT + "/me", HttpMethod.GET, new HttpEntity<>(null, headers), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + } + private HttpHeaders jsonHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); From a94f74ef78d22c9ceeb7ff13fae1f844a016368e Mon Sep 17 00:00:00 2001 From: seonmin Date: Fri, 6 Feb 2026 00:21:02 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat(change-pw):=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopers/domain/user/User.java | 4 + .../com/loopers/domain/user/UserService.java | 39 +++++++-- .../loopers/domain/user/UserServiceTest.java | 84 +++++++++++++++++++ 3 files changed, 120 insertions(+), 7 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 fb362e3a..284eb769 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 @@ -83,4 +83,8 @@ public BirthDate birthDate() { public Email email() { return email; } + + public void changePassword(Password newPassword) { + this.password = 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 6e3f1e11..a1efe5d8 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 @@ -4,6 +4,7 @@ import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -15,6 +16,7 @@ public class UserService { private final PasswordEncoder passwordEncoder; private final UserRepository userRepository; + @Transactional public LoginId signup(String loginId, String password, String name, LocalDate birthDate, String email) { LoginId id = LoginId.from(loginId); @@ -22,12 +24,7 @@ public LoginId signup(String loginId, String password, String name, LocalDate bi throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인ID입니다."); } - String birthDateWithDash = birthDate.toString(); - String birthDateWithOutDash = birthDateWithDash.replace("-", ""); - - if (password.contains(birthDateWithDash) || password.contains(birthDateWithOutDash)) { - throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 생년월일을 포함할 수 없습니다."); - } + validatePasswordNotContainsBirthDate(password, birthDate); User user = User.create(id , Password.of(password, passwordEncoder) @@ -40,7 +37,26 @@ public LoginId signup(String loginId, String password, String name, LocalDate bi return user.loginId(); } + @Transactional(readOnly = true) public UserInfo getMyInfo(String loginId, String password) { + User user = authenticate(loginId, password); + return UserInfo.from(user); + } + + @Transactional + public void changePassword(String loginId, String currentPassword, String newPassword) { + User user = authenticate(loginId, currentPassword); + + if (user.password().matches(newPassword, passwordEncoder)) { + throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호는 사용할 수 없습니다."); + } + + validatePasswordNotContainsBirthDate(newPassword, user.birthDate().asLocalDate()); + + user.changePassword(Password.of(newPassword, passwordEncoder)); + } + + private User authenticate(String loginId, String password) { LoginId id = LoginId.from(loginId); User user = userRepository.findByLoginId(id) @@ -50,6 +66,15 @@ public UserInfo getMyInfo(String loginId, String password) { throw new CoreException(ErrorType.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."); } - return UserInfo.from(user); + return user; + } + + private void validatePasswordNotContainsBirthDate(String password, LocalDate birthDate) { + String birthDateWithDash = birthDate.toString(); + String birthDateWithoutDash = birthDateWithDash.replace("-", ""); + + if (password.contains(birthDateWithDash) || password.contains(birthDateWithoutDash)) { + throw new CoreException(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 0322e7aa..c948fb72 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 @@ -147,4 +147,88 @@ class GetMyInfo { } } + @DisplayName("비밀번호 수정 시, ") + @Nested + class ChangePassword { + + User user; + + @BeforeEach + void setUp() { + when(passwordEncoder.encode("loopers123!@")).thenReturn("encoded"); + user = User.create( + LoginId.from("loopers123"), + Password.of("loopers123!@", passwordEncoder), + Name.from("루퍼스"), + BirthDate.from(LocalDate.of(1996, 11, 22)), + Email.from("test@loopers.im") + ); + } + + @Test + void 존재하지_않는_loginId면_NOT_FOUND를_던진다() { + // Arrange + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.empty()); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("loopers123", "loopers123!@", "newPass1234!"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + + @Test + void 기존_비밀번호가_불일치하면_UNAUTHORIZED를_던진다() { + // Arrange + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("wrongPass123", "encoded")).thenReturn(false); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("loopers123", "wrongPass123", "newPass1234!"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED); + } + + @Test + void 새_비밀번호가_기존과_동일하면_BAD_REQUEST를_던진다() { + // Arrange + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("loopers123!@", "encoded")).thenReturn(true); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("loopers123", "loopers123!@", "loopers123!@"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 새_비밀번호에_생년월일이_포함되면_BAD_REQUEST를_던진다() { + // Arrange + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("loopers123!@", "encoded")).thenReturn(true); + + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("loopers123", "loopers123!@", "ab19961122!"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 정상_변경이면_비밀번호가_변경된다() { + // Arrange + when(userRepository.findByLoginId(LoginId.from("loopers123"))).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("loopers123!@", "encoded")).thenReturn(true); + when(passwordEncoder.encode("newPass1234!")).thenReturn("newEncoded"); + + // Act + userService.changePassword("loopers123", "loopers123!@", "newPass1234!"); + + // Assert + assertThat(user.password().asString()).isEqualTo("newEncoded"); + } + } + } From b291a816ba47fea982d6b20abfc13224adaa3329 Mon Sep 17 00:00:00 2001 From: seonmin Date: Fri, 6 Feb 2026 01:03:38 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat(change-pw):=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interfaces/api/user/UserController.java | 9 +++++++ .../loopers/interfaces/api/user/UserDto.java | 3 +++ .../api/user/UserControllerTest.java | 26 +++++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java index b4f85428..86e4e254 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java @@ -36,4 +36,13 @@ public ApiResponse getMyInfo( UserInfo userInfo = userService.getMyInfo(loginId, password); return ApiResponse.success(UserDto.MyInfoResponse.from(userInfo)); } + + @PatchMapping("/me/password") + public ApiResponse changePassword( + @RequestHeader("X-Loopers-LoginId") String loginId, + @RequestHeader("X-Loopers-LoginPw") String currentPassword, + @RequestBody UserDto.ChangePasswordRequest request) { + userService.changePassword(loginId, currentPassword, request.newPassword()); + return ApiResponse.success(); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java index 0dcc842f..8c4563cd 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java @@ -16,6 +16,9 @@ public static SignupResponse from(LoginId loginId) { } } + public record ChangePasswordRequest(String newPassword) { + } + public record MyInfoResponse(String loginId, String name, LocalDate birthDate, String email) { public static MyInfoResponse from(UserInfo userInfo) { return new MyInfoResponse( diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java index 65106b0a..59cf3bd6 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java @@ -16,9 +16,9 @@ import com.loopers.domain.user.UserInfo; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -85,4 +85,26 @@ class GetMyInfo { } } + @DisplayName("비밀번호 수정 시,") + @Nested + class ChangePassword { + + @Test + void 성공하면_200_OK를_반환한다() throws Exception { + // Arrange + UserDto.ChangePasswordRequest request = new UserDto.ChangePasswordRequest("newPass1234!"); + doNothing().when(userService).changePassword("loopers123", "loopers123!@", "newPass1234!"); + + String content = objectMapper.writeValueAsString(request); + + // Act & Assert + mockMvc.perform(patch("/api/users/me/password") + .header("X-Loopers-LoginId", "loopers123") + .header("X-Loopers-LoginPw", "loopers123!@") + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isOk()); + } + } + } From dc15642b20feca8ca4ea038a92cba7554a29286a Mon Sep 17 00:00:00 2001 From: seonmin Date: Fri, 6 Feb 2026 01:03:52 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat(change-pw):=20=ED=86=B5=ED=95=A9/E2E?= =?UTF-8?q?=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 --- .../user/UserServiceIntegrationTest.java | 56 +++++++++++++++ .../interfaces/api/user/UserApiE2ETest.java | 70 +++++++++++++++++++ 2 files changed, 126 insertions(+) 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 f80be58e..01109fd9 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 @@ -108,4 +108,60 @@ class GetMyInfo { assertThat(exception.getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED); } } + + @DisplayName("비밀번호 변경 시,") + @Nested + class ChangePassword { + + @BeforeEach + void setUp() { + userService.signup("loopers123", "loopers123!@", "루퍼스", LocalDate.of(1996, 11, 22), "test@loopers.im"); + } + + @Test + void 정상_변경이면_새_비밀번호로_인증할_수_있다() { + // Act + userService.changePassword("loopers123", "loopers123!@", "newPass1234!"); + + // Assert - 새 비밀번호로 조회 성공 + UserInfo result = userService.getMyInfo("loopers123", "newPass1234!"); + assertThat(result.loginId()).isEqualTo("loopers123"); + } + + @Test + void 존재하지_않는_loginId면_NOT_FOUND를_던진다() { + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("nonexist12", "loopers123!@", "newPass1234!"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND); + } + + @Test + void 기존_비밀번호가_불일치하면_UNAUTHORIZED를_던진다() { + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("loopers123", "wrongPass123!", "newPass1234!"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED); + } + + @Test + void 새_비밀번호가_기존과_동일하면_BAD_REQUEST를_던진다() { + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("loopers123", "loopers123!@", "loopers123!@"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + + @Test + void 새_비밀번호에_생년월일이_포함되면_BAD_REQUEST를_던진다() { + // Act & Assert + CoreException exception = assertThrows(CoreException.class, () -> { + userService.changePassword("loopers123", "loopers123!@", "ab19961122!"); + }); + assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); + } + } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java index aaf9ce92..7159ccb6 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java @@ -191,6 +191,76 @@ class GetMyInfo { } } + @DisplayName("PATCH /api/users/me/password") + @Nested + class ChangePassword { + + @Test + void 정상_변경이면_200_OK를_반환한다() { + // Arrange - 회원가입 + UserDto.SignupRequest signupRequest = new UserDto.SignupRequest( + "loopers123", "loopers123!@", "루퍼스", + LocalDate.of(1996, 11, 22), "test@loopers.im" + ); + testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(signupRequest, jsonHeaders()), + new ParameterizedTypeReference>() {} + ); + + HttpHeaders headers = jsonHeaders(); + headers.set("X-Loopers-LoginId", "loopers123"); + headers.set("X-Loopers-LoginPw", "loopers123!@"); + UserDto.ChangePasswordRequest request = new UserDto.ChangePasswordRequest("newPass1234!"); + + // Act + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT + "/me/password", HttpMethod.PATCH, new HttpEntity<>(request, headers), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void 변경_후_새_비밀번호로_인증할_수_있다() { + // Arrange - 회원가입 후 비밀번호 변경 + UserDto.SignupRequest signupRequest = new UserDto.SignupRequest( + "loopers123", "loopers123!@", "루퍼스", + LocalDate.of(1996, 11, 22), "test@loopers.im" + ); + testRestTemplate.exchange( + ENDPOINT, HttpMethod.POST, new HttpEntity<>(signupRequest, jsonHeaders()), + new ParameterizedTypeReference>() {} + ); + + HttpHeaders changeHeaders = jsonHeaders(); + changeHeaders.set("X-Loopers-LoginId", "loopers123"); + changeHeaders.set("X-Loopers-LoginPw", "loopers123!@"); + testRestTemplate.exchange( + ENDPOINT + "/me/password", HttpMethod.PATCH, + new HttpEntity<>(new UserDto.ChangePasswordRequest("newPass1234!"), changeHeaders), + new ParameterizedTypeReference>() {} + ); + + // Act - 새 비밀번호로 내 정보 조회 + HttpHeaders newHeaders = new HttpHeaders(); + newHeaders.set("X-Loopers-LoginId", "loopers123"); + newHeaders.set("X-Loopers-LoginPw", "newPass1234!"); + + ResponseEntity> response = testRestTemplate.exchange( + ENDPOINT + "/me", HttpMethod.GET, new HttpEntity<>(null, newHeaders), + new ParameterizedTypeReference<>() {} + ); + + // Assert + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(response.getBody().data().loginId()).isEqualTo("loopers123") + ); + } + } + private HttpHeaders jsonHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); From b38b69113fbdb2fa9b514154e0a49394c681010b Mon Sep 17 00:00:00 2001 From: seonmin Date: Fri, 6 Feb 2026 02:25:25 +0900 Subject: [PATCH 11/11] =?UTF-8?q?feat(signup):=20api=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20status=20201=20=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 --- .../java/com/loopers/interfaces/api/user/UserController.java | 2 ++ .../java/com/loopers/interfaces/api/user/UserApiE2ETest.java | 4 ++-- .../com/loopers/interfaces/api/user/UserControllerTest.java | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java index 86e4e254..cb5e53cc 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java @@ -5,6 +5,7 @@ import com.loopers.interfaces.api.ApiResponse; import lombok.RequiredArgsConstructor; import com.loopers.domain.user.UserInfo; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @RestController @@ -14,6 +15,7 @@ public class UserController implements UserApiSpec { private final UserService userService; + @ResponseStatus(HttpStatus.CREATED) @PostMapping @Override public ApiResponse signup(@RequestBody UserDto.SignupRequest signRequest) { diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java index 7159ccb6..9ef41037 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java @@ -38,7 +38,7 @@ void tearDown() { class Signup { @Test - void 정상_요청이면_200_OK와_loginId를_반환한다() { + void 정상_요청이면_201_CREATED와_loginId를_반환한다() { // Arrange UserDto.SignupRequest request = new UserDto.SignupRequest( "loopers123", "loopers123!@", "루퍼스", @@ -53,7 +53,7 @@ class Signup { // Assert assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED), () -> assertThat(response.getBody().meta().result()).isEqualTo(ApiResponse.Metadata.Result.SUCCESS), () -> assertThat(response.getBody().data().loginId()).isEqualTo("loopers123") ); diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java index 59cf3bd6..4896ac67 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserControllerTest.java @@ -39,7 +39,7 @@ class UserControllerTest { class Signup { @Test - void 성공하면_200_OK() throws Exception { + void 성공하면_201_CREATED() throws Exception { // given UserDto.SignupRequest request = new UserDto.SignupRequest( "looper123", @@ -58,7 +58,7 @@ class Signup { mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(content)) - .andExpect(status().isOk()) + .andExpect(status().isCreated()) .andExpect(jsonPath("$.data.loginId").value("looper123")); } }