From 41c4e509fa54dabb91723349b5c134dd8f6e5ccc Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:37:02 +0900
Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EA=B0=80?=
 =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?=
 =?UTF-8?q?=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../loopers/domain/members/MemberModel.java   | 106 ++++++++++++++++++
 .../domain/members/MemberRepository.java      |  11 ++
 .../loopers/domain/members/MemberService.java |  32 ++++++
 .../members/MemberJpaRepository.java          |  11 ++
 .../members/MemberRepository.java             |  12 ++
 .../members/MemberRepositoryImpl.java         |  29 +++++
 .../domain/members/MemberModelTest.java       |  84 ++++++++++++++
 .../members/MemberServiceIntegrationTest.java |  93 +++++++++++++++
 .../members/MemberServicePasswordTest.java    |  62 ++++++++++
 9 files changed, 440 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServicePasswordTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
new file mode 100644
index 000000000..e28c0ba11
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
@@ -0,0 +1,106 @@
+package com.loopers.domain.members;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.loopers.domain.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.regex.Pattern;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Entity
+@Table(name = "member")
+public class MemberModel extends BaseEntity {
+
+    private static final Pattern MEMBER_ID_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{1,10}$");
+    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$");
+    private static final DateTimeFormatter BIRTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    @Column(unique = true, nullable = false, length = 10)
+    private String memberId;
+
+    @Column(nullable = false)
+    private String name;
+
+    @Column(nullable = false)
+    private String email;
+
+    @JsonIgnore
+    @Column(nullable = false)
+    private String password;
+
+    @Column(nullable = false)
+    private LocalDate birthDate;
+
+    @Column(nullable = false)
+    private String gender;
+
+    public static MemberModel create(String memberId, String name, String email, String password, String birthDate, String gender) {
+        validateMemberId(memberId);
+        validateEmail(email);
+        LocalDate parsedBirthDate = validateBirthDate(birthDate);
+        validateGender(gender);
+
+        return new MemberModel(
+                memberId,
+                name,
+                email,
+                password,
+                parsedBirthDate,
+                gender
+        );
+    }
+
+    private static void validateMemberId(String MemberId) {
+        if (MemberId == null || MemberId.isBlank()) {
+            throw new IllegalArgumentException("사용자 ID는 필수입니다.");
+        }
+
+        if (!MEMBER_ID_PATTERN.matcher(MemberId).matches()) {
+            throw new IllegalArgumentException("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+        }
+
+    }
+
+    private static LocalDate validateBirthDate(String birthDate) {
+        if (birthDate == null || birthDate.isBlank()) {
+            throw new IllegalArgumentException("생년월일은 필수입니다.");
+        }
+
+        try {
+            LocalDate parsedDate = LocalDate.parse(birthDate, BIRTH_DATE_FORMATTER);
+
+            if (parsedDate.isAfter(LocalDate.now())) {
+                throw new IllegalArgumentException("생년월일은 미래일 수 없습니다");
+            }
+            return parsedDate;
+        } catch (DateTimeParseException e) {
+            throw new IllegalArgumentException("생년월일 형식이 올바르지 않습니다.");
+        }
+    }
+
+    private static void validateEmail(String email) {
+        if (email == null || email.isBlank()) {
+            throw new IllegalArgumentException("이메일은 필수입니다.");
+        }
+
+        if (!EMAIL_PATTERN.matcher(email).matches()) {
+            throw new IllegalArgumentException("이메일 형식이 올바르지 않습니다.");
+        }
+    }
+
+    private static void validateGender(String gender) {
+        if (gender == null || (!gender.equals("MALE") && !gender.equals("FEMALE"))) {
+            throw new IllegalArgumentException("성별은 MALE 또는 FEMALE 이어야 합니다");
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java
new file mode 100644
index 000000000..7adc82a60
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java
@@ -0,0 +1,11 @@
+package com.loopers.domain.members;
+
+import java.util.Optional;
+
+public interface MemberRepository {
+    Optional<MemberModel> findByMemberId(String memberId);
+
+    MemberModel save(MemberModel memberModel);
+
+    boolean existsByMemberId(String memberId);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
new file mode 100644
index 000000000..bcbee8167
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
@@ -0,0 +1,32 @@
+package com.loopers.domain.members;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@RequiredArgsConstructor
+@Component
+public class MemberService {
+
+    private final MemberRepository memberRepository;
+    private final PasswordEncoder passwordEncoder;
+
+    @Transactional
+    public MemberModel registerMember(String memberId, String name, String email, String password, String birthDate, String gender) {
+        if (memberRepository.existsByMemberId(memberId)) {
+            throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 사용자 ID입니다.");
+        }
+        
+        String encodedPassword = passwordEncoder.encode(password);
+        MemberModel member = MemberModel.create(memberId, name, email, encodedPassword, birthDate, gender);
+        return memberRepository.save(member);
+    }
+
+    @Transactional(readOnly = true)
+    public MemberModel getMember(String memberId) {
+        return memberRepository.findByMemberId(memberId).orElse(null);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java
new file mode 100644
index 000000000..18b9dcc3b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java
@@ -0,0 +1,11 @@
+package com.loopers.infrastructure.members;
+
+import com.loopers.domain.members.MemberModel;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface MemberJpaRepository extends JpaRepository<MemberModel, Long> {
+    Optional<MemberModel> findByMemberId(String memberId);
+    boolean existsByMemberId(String memberId);
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java
new file mode 100644
index 000000000..490a01dcd
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java
@@ -0,0 +1,12 @@
+package com.loopers.infrastructure.members;
+
+import com.loopers.domain.members.MemberModel;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface MemberRepository extends JpaRepository<MemberModel, Long> {
+    Optional<MemberModel> findByMemberId(String memberId);
+
+    boolean existsByMemberId(String memberId);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java
new file mode 100644
index 000000000..ec4ba8153
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java
@@ -0,0 +1,29 @@
+package com.loopers.infrastructure.members;
+
+import com.loopers.domain.members.MemberModel;
+import com.loopers.domain.members.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Component
+public class MemberRepositoryImpl implements MemberRepository {
+    private final MemberJpaRepository memberJpaRepository;
+
+    @Override
+    public Optional<MemberModel> findByMemberId(String memberId) {
+        return memberJpaRepository.findByMemberId(memberId);
+    }
+
+    @Override
+    public MemberModel save(MemberModel memberModel) {
+        return memberJpaRepository.save(memberModel);
+    }
+
+    @Override
+    public boolean existsByMemberId(String memberId) {
+        return memberJpaRepository.existsByMemberId(memberId);
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java
new file mode 100644
index 000000000..f30af25f5
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java
@@ -0,0 +1,84 @@
+package com.loopers.domain.members;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+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.assertj.core.api.Assertions.assertThatThrownBy;
+
+class MemberModelTest {
+
+    @DisplayName("Member 객체를 생성 시,")
+    @Nested
+    class CreateMemberTest {
+
+        @DisplayName("정상 값이 들어오면 User 객체 생성에 성공한다")
+        @Test
+        void shouldSaveMember_whenRequestIsValid() {
+            MemberModel member = MemberModel.create("test123", "홍길동", "test@example.com", "password123", "1990-01-01", "MALE");
+
+            assertThat(member).isNotNull();
+            assertThat(member.getMemberId()).isEqualTo("test123");
+            assertThat(member.getEmail()).isEqualTo("test@example.com");
+            assertThat(member.getBirthDate()).isEqualTo(java.time.LocalDate.of(1990, 1, 1));
+            assertThat(member.getGender()).isEqualTo("MALE");
+
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"abcdefghijklmn", "abc123daas456"})
+        @DisplayName("Member ID가 10자 초과이면 예외가 발생한다")
+        void shouldThrowException_whenMemberIdTooLong(String invalidMemberId) {
+            assertThatThrownBy(() ->
+                    MemberModel.create(
+                            invalidMemberId, "홍길동", "test@example.com", "password", "2002-02-02", "FEMALE"
+                    )
+            ).isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageContaining("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"aaa", "bcdef"})
+        @DisplayName("ID가 영문으로만 이루어지면 예외가 발생한다")
+        void shouldThrowException_IfMemberIdIsLetterOnly(String letterOnlyId) {
+            assertThatThrownBy(() ->
+                    MemberModel.create(letterOnlyId, "홍길동", "test@example.com", "password", "2002-02-02", "FEMALE")
+            ).isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageContaining("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"123", "44445"})
+        @DisplayName("ID가 숫자로만 이루어지면 예외가 발생한다")
+        void shouldThrowException_IfMemberIdIsNumberOnly(String numberOnlyId) {
+            assertThatThrownBy(() ->
+                    MemberModel.create(numberOnlyId, "홍길동", "test@example.com", "password", "2002-02-02", "FEMALE")
+            ).isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageContaining("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"test@", "@example.com", "test@example"})
+        @DisplayName("이메일이 xx@yy.zz 형식에 맞지 않으면, User 객체 생성에 실패한다")
+        void shouldThrowException_whenEmailFormatIsInvalid(String invalidEmail) {
+            assertThatThrownBy(() ->
+                    MemberModel.create("test123", "홍길동", invalidEmail, "password", "1990-01-01", "MALE")
+            ).isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageContaining("이메일 형식이 올바르지 않습니다.");
+        }
+
+        @ParameterizedTest
+        @ValueSource(strings = {"19900101", "1990/01/01", "90-01-01", "1990-13-01", "1990-01-32"})
+        @DisplayName("생년월일이 yyyy-MM-dd 형식에 맞지 않으면, User 객체 생성에 실패한다")
+        void shouldThrowException_whenBirthDateFormatIsInvalid(String invalidBirthDate) {
+            assertThatThrownBy(() ->
+                    MemberModel.create("test123", "홍길동", "test@example.com", "password", invalidBirthDate, "MALE")
+            ).isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageContaining("생년월일 형식이 올바르지 않습니다.");
+        }
+
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java
new file mode 100644
index 000000000..b1e75cf70
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java
@@ -0,0 +1,93 @@
+package com.loopers.domain.members;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.test.context.bean.override.mockito.MockitoSpyBean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@SpringBootTest
+class MemberServiceIntegrationTest {
+
+    @Autowired
+    private MemberService memberService;
+
+    @MockitoSpyBean
+    private MemberRepository memberRepository;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("회원 가입 시,")
+    @Nested
+    class RegisterMember {
+
+        @DisplayName("회원 가입시 User 저장이 수행된다 (spy)")
+        @Test
+        void shouldSaveMember_whenMemberRegisters() {
+            assertThat(memberRepository).isNotNull();
+            memberService.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+            verify(memberRepository, times(1)).save(any(MemberModel.class));
+
+            boolean exists = memberRepository.existsByMemberId("test123");
+            assertThat(exists).isTrue();
+        }
+
+        @DisplayName("이미 가입된 ID로 회원가입 시도 시, 실패한다")
+        @Test
+        void shouldThrowException_whenDuplicateMemberIdIsUsed() {
+            memberService.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+
+            CoreException exception = assertThrows(CoreException.class, () -> {
+                memberService.registerMember("test123", "김철수", "kim@example.com", "password", "1985-05-05", "MALE");
+            });
+
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.CONFLICT);
+        }
+    }
+
+    @DisplayName("회원 조회 시,")
+    @Nested
+    class GetMember {
+
+        @DisplayName("해당 ID의 회원이 존재할 경우, 회원 정보가 반환된다")
+        @Test
+        void shouldReturnMemberInfo_whenMemberExists() {
+            memberService.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+
+            MemberModel result = memberService.getMember("test123");
+
+            assertAll(
+                    () -> assertThat(result).isNotNull(),
+                    () -> assertThat(result.getMemberId()).isEqualTo("test123"),
+                    () -> assertThat(result.getName()).isEqualTo("홍길동"),
+                    () -> assertThat(result.getEmail()).isEqualTo("test@example.com")
+            );
+        }
+
+        @DisplayName("해당 ID의 회원이 존재하지 않을 경우, null이 반환된다")
+        @Test
+        void shouldReturnNull_whenMemberDoesNotExist() {
+            MemberModel result = memberService.getMember("nonexistent");
+
+            assertThat(result).isNull();
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServicePasswordTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServicePasswordTest.java
new file mode 100644
index 000000000..be68d1e07
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServicePasswordTest.java
@@ -0,0 +1,62 @@
+package com.loopers.domain.members;
+
+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 org.springframework.security.crypto.password.PasswordEncoder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+class MemberServicePasswordTest {
+
+    @Autowired
+    private MemberService memberService;
+
+    @Autowired
+    private MemberRepository memberRepository;
+
+    @Autowired
+    private PasswordEncoder passwordEncoder;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("회원 가입 시 비밀번호가 암호화되어 저장된다")
+    @Test
+    void shouldEncryptPassword_whenRegisterMember() {
+        String plainPassword = "password123";
+
+        memberService.registerMember("test123", "홍길동", "test@example.com", plainPassword, "1990-01-01", "MALE");
+
+        MemberModel savedMember = memberRepository.findByMemberId("test123").orElseThrow();
+
+        assertThat(savedMember.getPassword()).isNotEqualTo(plainPassword);
+        assertThat(savedMember.getPassword()).startsWith("$2a$");
+        assertThat(passwordEncoder.matches(plainPassword, savedMember.getPassword())).isTrue();
+    }
+
+    @DisplayName("같은 비밀번호라도 매번 다른 해시값이 생성된다")
+    @Test
+    void shouldGenerateDifferentHash_forSamePassword() {
+        String plainPassword = "password123";
+
+        memberService.registerMember("test1", "홍길동1", "test1@example.com", plainPassword, "1990-01-01", "MALE");
+        memberService.registerMember("test2", "홍길동2", "test2@example.com", plainPassword, "1990-01-01", "MALE");
+
+        MemberModel member1 = memberRepository.findByMemberId("test1").orElseThrow();
+        MemberModel member2 = memberRepository.findByMemberId("test2").orElseThrow();
+
+        assertThat(member1.getPassword()).isNotEqualTo(member2.getPassword());
+        assertThat(passwordEncoder.matches(plainPassword, member1.getPassword())).isTrue();
+        assertThat(passwordEncoder.matches(plainPassword, member2.getPassword())).isTrue();
+    }
+}

From 3a5db12a12c519799626a072b6bd3980a75ff928 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:37:32 +0900
Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?=
 =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/loopers/domain/points/PointModel.java | 40 +++++++++++++++
 .../domain/points/PointRepository.java        |  8 +++
 .../loopers/domain/points/PointService.java   | 27 ++++++++++
 .../points/PointJpaRepository.java            | 10 ++++
 .../points/PointRepositoryImpl.java           | 24 +++++++++
 .../points/PointServiceIntegrationTest.java   | 51 +++++++++++++++++++
 6 files changed, 160 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/points/PointModel.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/points/PointRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/points/PointService.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointJpaRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointRepositoryImpl.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointModel.java
new file mode 100644
index 000000000..862c44e5b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointModel.java
@@ -0,0 +1,40 @@
+package com.loopers.domain.points;
+
+import com.loopers.domain.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Entity
+@Table(name = "point")
+public class PointModel extends BaseEntity {
+
+    @Column(unique = true, nullable = false, length = 10)
+    private String memberId;
+
+    @Column(nullable = false)
+    private BigDecimal amount;
+
+    public static PointModel create(String memberId, BigDecimal amount) {
+        if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
+            throw new IllegalArgumentException("포인트는 0 이상이어야 합니다");
+        }
+
+        return new PointModel(memberId, amount);
+    }
+
+    public void addAmount(BigDecimal addAmount) {
+        if (addAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new IllegalArgumentException("추가하는 포인트는 0보다 커야 합니다");
+        }
+        this.amount = this.amount.add(addAmount);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointRepository.java
new file mode 100644
index 000000000..67ddc7c2c
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointRepository.java
@@ -0,0 +1,8 @@
+package com.loopers.domain.points;
+
+import java.util.Optional;
+
+public interface PointRepository {
+    Optional<PointModel> findByMemberId(String memberId);
+    PointModel save(PointModel pointModel);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointService.java
new file mode 100644
index 000000000..5436e5dd4
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointService.java
@@ -0,0 +1,27 @@
+package com.loopers.domain.points;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+
+@RequiredArgsConstructor
+@Component
+public class PointService {
+
+    private final PointRepository pointRepository;
+
+    @Transactional(readOnly = true)
+    public BigDecimal getMemberPoints(String memberId) {
+        return pointRepository.findByMemberId(memberId)
+                .map(PointModel::getAmount)
+                .orElse(null);
+    }
+
+    @Transactional
+    public PointModel initializeMemberPoints(String memberId) {
+        PointModel point = PointModel.create(memberId, BigDecimal.ZERO);
+        return pointRepository.save(point);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointJpaRepository.java
new file mode 100644
index 000000000..639ed2bf2
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointJpaRepository.java
@@ -0,0 +1,10 @@
+package com.loopers.infrastructure.points;
+
+import com.loopers.domain.points.PointModel;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface PointJpaRepository extends JpaRepository<PointModel, Long> {
+    Optional<PointModel> findByMemberId(String memberId);
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointRepositoryImpl.java
new file mode 100644
index 000000000..79193a4a3
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/points/PointRepositoryImpl.java
@@ -0,0 +1,24 @@
+package com.loopers.infrastructure.points;
+
+import com.loopers.domain.points.PointModel;
+import com.loopers.domain.points.PointRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Component
+public class PointRepositoryImpl implements PointRepository {
+    private final PointJpaRepository pointJpaRepository;
+
+    @Override
+    public Optional<PointModel> findByMemberId(String memberId) {
+        return pointJpaRepository.findByMemberId(memberId);
+    }
+
+    @Override
+    public PointModel save(PointModel pointModel) {
+        return pointJpaRepository.save(pointModel);
+    }
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java
new file mode 100644
index 000000000..ff346ca69
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java
@@ -0,0 +1,51 @@
+package com.loopers.domain.points;
+
+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 java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+class PointServiceIntegrationTest {
+    @Autowired
+    private PointService pointService;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("포인트 조회 시,")
+    @Nested
+    class GetMemberPoints {
+        
+        @DisplayName("해당 ID의 회원이 존재할 경우, 보유 포인트가 반환된다")
+        @Test
+        void shouldReturnPoints_whenMemberExists() {
+            String memberId = "test123";
+            pointService.initializeMemberPoints(memberId);
+
+            BigDecimal result = pointService.getMemberPoints(memberId);
+
+            assertThat(result).isEqualTo(BigDecimal.ZERO);
+        }
+
+        @DisplayName("해당 ID의 회원이 존재하지 않을 경우, null이 반환된다")
+        @Test
+        void shouldReturnNull_whenMemberDoesNotExist() {
+            BigDecimal result = pointService.getMemberPoints("nonexistent");
+
+            assertThat(result).isNull();
+        }
+    }
+}
\ No newline at end of file

From 06e14220845b9baaa906ae0f7960a721f55c7d6b Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:38:48 +0900
Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20API=20?=
 =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/members/MemberFacade.java     |  31 ++++
 .../application/members/MemberInfo.java       |  18 +++
 .../api/members/MemberV1ApiSpec.java          |  19 +++
 .../api/members/MemberV1Controller.java       |  58 ++++++++
 .../interfaces/api/members/MemberV1Dto.java   |  69 +++++++++
 .../members/MemberPasswordSecurityTest.java   |  89 +++++++++++
 .../api/members/MemberV1ApiE2ETest.java       | 140 ++++++++++++++++++
 7 files changed, 424 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberPasswordSecurityTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberV1ApiE2ETest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
new file mode 100644
index 000000000..f747cb5b7
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
@@ -0,0 +1,31 @@
+package com.loopers.application.members;
+
+import com.loopers.domain.members.MemberModel;
+import com.loopers.domain.members.MemberService;
+import com.loopers.domain.points.PointService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+
+@RequiredArgsConstructor
+@Component
+public class MemberFacade {
+    private final MemberService memberService;
+    private final PointService pointService;
+
+    public MemberInfo registerMember(String memberId, String name, String email, String password, String birthDate, String gender) {
+        MemberModel member = memberService.registerMember(memberId, name, email, password, birthDate, gender);
+        pointService.initializeMemberPoints(memberId);
+        return MemberInfo.from(member);
+    }
+
+    public MemberInfo getMemberInfo(String memberId) {
+        MemberModel member = memberService.getMember(memberId);
+        return member != null ? MemberInfo.from(member) : null;
+    }
+
+    public BigDecimal getMemberPoints(String memberId) {
+        return pointService.getMemberPoints(memberId);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java
new file mode 100644
index 000000000..d396a760c
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java
@@ -0,0 +1,18 @@
+package com.loopers.application.members;
+
+import com.loopers.domain.members.MemberModel;
+
+import java.time.LocalDate;
+
+public record MemberInfo(Long id, String memberId, String name, String email, LocalDate birthDate, String gender) {
+    public static MemberInfo from(MemberModel model) {
+        return new MemberInfo(
+                model.getId(),
+                model.getMemberId(),
+                model.getName(),
+                model.getEmail(),
+                model.getBirthDate(),
+                model.getGender()
+        );
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java
new file mode 100644
index 000000000..8a681bc37
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java
@@ -0,0 +1,19 @@
+package com.loopers.interfaces.api.members;
+
+import com.loopers.interfaces.api.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@Tag(name = "Member", description = "회원 관리 API")
+public interface MemberV1ApiSpec {
+
+    @Operation(summary = "회원 가입", description = "새로운 회원을 등록합니다.")
+    ApiResponse<MemberV1Dto.MemberResponse> registerMember(MemberV1Dto.MemberRegisterRequest request);
+
+    @Operation(summary = "내 정보 조회", description = "현재 로그인한 회원의 정보를 조회합니다.")
+    ApiResponse<MemberV1Dto.MemberResponse> getMemberInfo(
+            @Parameter(description = "사용자 ID", required = true) String userId
+    );
+
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java
new file mode 100644
index 000000000..b1e0a22de
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java
@@ -0,0 +1,58 @@
+package com.loopers.interfaces.api.members;
+
+import com.loopers.application.members.MemberFacade;
+import com.loopers.application.members.MemberInfo;
+import com.loopers.interfaces.api.ApiResponse;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/v1/members")
+public class MemberV1Controller implements MemberV1ApiSpec {
+
+    private final MemberFacade memberFacade;
+
+    @PostMapping("/register")
+    @Override
+    public ApiResponse<MemberV1Dto.MemberResponse> registerMember(
+            @Valid @RequestBody MemberV1Dto.MemberRegisterRequest request
+    ) {
+        MemberInfo memberInfo = memberFacade.registerMember(
+                request.memberId(),
+                request.name(),
+                request.email(),
+                request.password(),
+                request.birthDate(),
+                request.gender()
+        );
+        
+        MemberV1Dto.MemberResponse response = MemberV1Dto.MemberResponse.from(memberInfo);
+        return ApiResponse.success(response);
+    }
+
+    @GetMapping("/me")
+    @Override
+    public ApiResponse<MemberV1Dto.MemberResponse> getMemberInfo(
+            @RequestHeader("X-USER-ID") String userId
+    ) {
+        MemberInfo memberInfo = memberFacade.getMemberInfo(userId);
+        if (memberInfo == null) {
+            throw new CoreException(ErrorType.NOT_FOUND, "회원을 찾을 수 없습니다.");
+        }
+        
+        MemberV1Dto.MemberResponse response = MemberV1Dto.MemberResponse.from(memberInfo);
+        return ApiResponse.success(response);
+    }
+
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java
new file mode 100644
index 000000000..69bd64dd4
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java
@@ -0,0 +1,69 @@
+package com.loopers.interfaces.api.members;
+
+import com.loopers.application.members.MemberInfo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public class MemberV1Dto {
+
+    @Schema(description = "회원 가입 요청")
+    public record MemberRegisterRequest(
+            @Schema(description = "사용자 ID", example = "test123")
+            @NotBlank(message = "사용자 ID는 필수입니다.")
+            @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{1,10}$", message = "사용자 ID는 영문 및 숫자 10자 이내여야 합니다.")
+            String memberId,
+            @Schema(description = "이름", example = "홍길동")
+            @NotBlank(message = "이름은 필수입니다.")
+            String name,
+            @Schema(description = "이메일", example = "test@example.com")
+            @NotBlank(message = "이메일은 필수입니다.")
+            @Email(message = "이메일 형식이 올바르지 않습니다.")
+            String email,
+            @Schema(description = "비밀번호", example = "password123")
+            @NotBlank(message = "비밀번호는 필수입니다.")
+            String password,
+            @Schema(description = "생년월일", example = "1990-01-01")
+            @NotBlank(message = "생년월일은 필수입니다.")
+            @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "생년월일 형식이 올바르지 않습니다.")
+            String birthDate,
+            @Schema(description = "성별", example = "MALE")
+            @NotNull(message = "성별은 필수입니다.")
+            @Pattern(regexp = "^(MALE|FEMALE)$", message = "성별은 MALE 또는 FEMALE이어야 합니다.")
+            String gender
+    ) {}
+
+    @Schema(description = "회원 정보 응답")
+    public record MemberResponse(
+            @Schema(description = "회원 ID")
+            Long id,
+            @Schema(description = "사용자 ID")
+            String memberId,
+            @Schema(description = "이름")
+            String name,
+            @Schema(description = "이메일")
+            String email,
+            @Schema(description = "생년월일")
+            LocalDate birthDate,
+            @Schema(description = "성별")
+            String gender
+    ) {
+        public static MemberResponse from(MemberInfo memberInfo) {
+            return new MemberResponse(
+                    memberInfo.id(),
+                    memberInfo.memberId(),
+                    memberInfo.name(),
+                    memberInfo.email(),
+                    memberInfo.birthDate(),
+                    memberInfo.gender()
+            );
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberPasswordSecurityTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberPasswordSecurityTest.java
new file mode 100644
index 000000000..085cb2bc3
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberPasswordSecurityTest.java
@@ -0,0 +1,89 @@
+package com.loopers.interfaces.api.members;
+
+import com.loopers.application.members.MemberFacade;
+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 org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class MemberPasswordSecurityTest {
+
+    private final TestRestTemplate testRestTemplate;
+    private final MemberFacade memberFacade;
+    private final DatabaseCleanUp databaseCleanUp;
+
+    @Autowired
+    public MemberPasswordSecurityTest(
+            TestRestTemplate testRestTemplate,
+            MemberFacade memberFacade,
+            DatabaseCleanUp databaseCleanUp
+    ) {
+        this.testRestTemplate = testRestTemplate;
+        this.memberFacade = memberFacade;
+        this.databaseCleanUp = databaseCleanUp;
+    }
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("회원 가입 응답에 비밀번호가 포함되지 않는다")
+    @Test
+    void shouldNotIncludePassword_inRegistrationResponse() {
+        MemberV1Dto.MemberRegisterRequest request = new MemberV1Dto.MemberRegisterRequest(
+                "test123", "홍길동", "test@example.com", "secretPassword123", "1990-01-01", "MALE"
+        );
+
+        ResponseEntity<String> response = testRestTemplate.exchange(
+                "/api/v1/members/register",
+                HttpMethod.POST,
+                new HttpEntity<>(request),
+                String.class
+        );
+
+        String responseBody = response.getBody();
+
+        assertThat(responseBody).doesNotContain("password");
+        assertThat(responseBody).doesNotContain("secretPassword123");
+
+        assertThat(responseBody).contains("test123");
+        assertThat(responseBody).contains("홍길동");
+        assertThat(responseBody).contains("test@example.com");
+    }
+
+    @DisplayName("내 정보 조회 응답에 비밀번호가 포함되지 않는다")
+    @Test
+    void shouldNotIncludePassword_inMemberInfoResponse() {
+        memberFacade.registerMember("test123", "홍길동", "test@example.com", "secretPassword123", "1990-01-01", "MALE");
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-USER-ID", "test123");
+
+        ResponseEntity<String> response = testRestTemplate.exchange(
+                "/api/v1/members/me",
+                HttpMethod.GET,
+                new HttpEntity<>(null, headers),
+                String.class
+        );
+
+        String responseBody = response.getBody();
+
+        assertThat(responseBody).doesNotContain("password");
+        assertThat(responseBody).doesNotContain("secretPassword123");
+
+        assertThat(responseBody).contains("test123");
+        assertThat(responseBody).contains("홍길동");
+        assertThat(responseBody).contains("test@example.com");
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberV1ApiE2ETest.java
new file mode 100644
index 000000000..4689342cf
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/members/MemberV1ApiE2ETest.java
@@ -0,0 +1,140 @@
+package com.loopers.interfaces.api.members;
+
+import com.loopers.application.members.MemberFacade;
+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.util.Objects;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class MemberV1ApiE2ETest {
+
+    private final TestRestTemplate testRestTemplate;
+    private final MemberFacade memberFacade;
+    private final DatabaseCleanUp databaseCleanUp;
+
+    @Autowired
+    public MemberV1ApiE2ETest(
+            TestRestTemplate testRestTemplate,
+            MemberFacade memberFacade,
+            DatabaseCleanUp databaseCleanUp
+    ) {
+        this.testRestTemplate = testRestTemplate;
+        this.memberFacade = memberFacade;
+        this.databaseCleanUp = databaseCleanUp;
+    }
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("회원 가입 (POST /api/v1/members/register)")
+    @Nested
+    class MemberRegistration {
+
+        @DisplayName("유효한 정보로 가입 시 201과 회원 정보를 반환한다")
+        @Test
+        void shouldReturn201AndMemberInfo_whenValidDataProvided() {
+            MemberV1Dto.MemberRegisterRequest request = new MemberV1Dto.MemberRegisterRequest(
+                    "test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE"
+            );
+
+            ParameterizedTypeReference<ApiResponse<MemberV1Dto.MemberResponse>> responseType =
+                    new ParameterizedTypeReference<>() {
+                    };
+            ResponseEntity<ApiResponse<MemberV1Dto.MemberResponse>> response =
+                    testRestTemplate.exchange("/api/v1/members/register", HttpMethod.POST,
+                            new HttpEntity<>(request), responseType);
+
+            assertAll(
+                    () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
+                    () -> assertThat(Objects.requireNonNull(response.getBody()).data().memberId()).isEqualTo("test123"),
+                    () -> assertThat(Objects.requireNonNull(response.getBody()).data().name()).isEqualTo("홍길동"),
+                    () -> assertThat(Objects.requireNonNull(response.getBody()).data().email()).isEqualTo("test@example.com"),
+                    () -> assertThat(Objects.requireNonNull(response.getBody()).data().gender()).isEqualTo("MALE")
+            );
+        }
+
+        @DisplayName("성별이 누락된 경우 400 Bad Request를 반환한다")
+        @Test
+        void shouldReturn400_whenGenderIsMissing() {
+            MemberV1Dto.MemberRegisterRequest request = new MemberV1Dto.MemberRegisterRequest(
+                    "test123", "홍길동", "test@example.com", "password", "1990-01-01", null
+            );
+
+            ParameterizedTypeReference<ApiResponse<MemberV1Dto.MemberResponse>> responseType =
+                    new ParameterizedTypeReference<>() {
+                    };
+            ResponseEntity<ApiResponse<MemberV1Dto.MemberResponse>> response =
+                    testRestTemplate.exchange("/api/v1/members/register", HttpMethod.POST,
+                            new HttpEntity<>(request), responseType);
+
+            assertAll(
+                    () -> assertTrue(response.getStatusCode().is4xxClientError()),
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+            );
+        }
+    }
+
+    @DisplayName("내 정보 조회 (GET /api/v1/members/me)")
+    @Nested
+    class MyInfoRetrieval {
+
+        @DisplayName("유효한 사용자 ID로 조회 시 200과 회원 정보를 반환한다")
+        @Test
+        void shouldReturn200AndMemberInfo_whenValidUserIdProvided() {
+            memberFacade.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", "test123");
+
+            ParameterizedTypeReference<ApiResponse<MemberV1Dto.MemberResponse>> responseType =
+                    new ParameterizedTypeReference<>() {
+                    };
+            ResponseEntity<ApiResponse<MemberV1Dto.MemberResponse>> response =
+                    testRestTemplate.exchange("/api/v1/members/me", HttpMethod.GET,
+                            new HttpEntity<>(null, headers), responseType);
+
+            assertAll(
+                    () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
+                    () -> assertThat(response.getBody().data().memberId()).isEqualTo("test123"),
+                    () -> assertThat(response.getBody().data().name()).isEqualTo("홍길동"),
+                    () -> assertThat(response.getBody().data().email()).isEqualTo("test@example.com")
+            );
+        }
+
+        @DisplayName("존재하지 않는 사용자 ID로 조회 시 404를 반환한다")
+        @Test
+        void shouldReturn404_whenUserNotFound() {
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", "nonexistent");
+
+            ParameterizedTypeReference<ApiResponse<MemberV1Dto.MemberResponse>> responseType =
+                    new ParameterizedTypeReference<>() {
+                    };
+            ResponseEntity<ApiResponse<MemberV1Dto.MemberResponse>> response =
+                    testRestTemplate.exchange("/api/v1/members/me", HttpMethod.GET,
+                            new HttpEntity<>(null, headers), responseType);
+
+            assertAll(
+                    () -> assertTrue(response.getStatusCode().is4xxClientError()),
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND)
+            );
+        }
+    }
+
+}

From 8492cfe3a633d00cda374b078cd539c083b92c19 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:39:17 +0900
Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?=
 =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../interfaces/api/points/PointV1ApiSpec.java | 15 ++++
 .../api/points/PointV1Controller.java         | 35 ++++++++
 .../interfaces/api/points/PointV1Dto.java     | 14 +++
 .../api/points/PointV1ApiE2ETest.java         | 88 +++++++++++++++++++
 4 files changed, 152 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Controller.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/points/PointV1ApiE2ETest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1ApiSpec.java
new file mode 100644
index 000000000..001eaf99e
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1ApiSpec.java
@@ -0,0 +1,15 @@
+package com.loopers.interfaces.api.points;
+
+import com.loopers.interfaces.api.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@Tag(name = "Point", description = "포인트 관리 API")
+public interface PointV1ApiSpec {
+
+    @Operation(summary = "포인트 조회", description = "현재 로그인한 회원의 포인트를 조회합니다.")
+    ApiResponse<PointV1Dto.PointResponse> getMemberPoints(
+            @Parameter(description = "사용자 ID", required = true) String userId
+    );
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Controller.java
new file mode 100644
index 000000000..5af0340db
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Controller.java
@@ -0,0 +1,35 @@
+package com.loopers.interfaces.api.points;
+
+import com.loopers.domain.points.PointService;
+import com.loopers.interfaces.api.ApiResponse;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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;
+
+import java.math.BigDecimal;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/v1/points")
+public class PointV1Controller implements PointV1ApiSpec {
+
+    private final PointService pointService;
+
+    @GetMapping
+    @Override
+    public ApiResponse<PointV1Dto.PointResponse> getMemberPoints(
+            @RequestHeader(value = "X-USER-ID", required = false) String userId
+    ) {
+        if (userId == null || userId.isBlank()) {
+            throw new CoreException(ErrorType.MISSING_REQUEST_HEADER, "X-USER-ID 헤더는 필수입니다.");
+        }
+        
+        BigDecimal points = pointService.getMemberPoints(userId);
+        PointV1Dto.PointResponse response = new PointV1Dto.PointResponse(points);
+        return ApiResponse.success(response);
+    }
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Dto.java
new file mode 100644
index 000000000..de662c0b8
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointV1Dto.java
@@ -0,0 +1,14 @@
+package com.loopers.interfaces.api.points;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+
+public class PointV1Dto {
+
+    @Schema(description = "포인트 응답")
+    public record PointResponse(
+            @Schema(description = "보유 포인트")
+            BigDecimal points
+    ) {}
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/points/PointV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/points/PointV1ApiE2ETest.java
new file mode 100644
index 000000000..d2ba1fdd5
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/points/PointV1ApiE2ETest.java
@@ -0,0 +1,88 @@
+package com.loopers.interfaces.api.points;
+
+import com.loopers.application.members.MemberFacade;
+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.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class PointV1ApiE2ETest {
+
+    private final TestRestTemplate testRestTemplate;
+    private final MemberFacade memberFacade;
+    private final DatabaseCleanUp databaseCleanUp;
+
+    @Autowired
+    public PointV1ApiE2ETest(
+            TestRestTemplate testRestTemplate,
+            MemberFacade memberFacade,
+            DatabaseCleanUp databaseCleanUp
+    ) {
+        this.testRestTemplate = testRestTemplate;
+        this.memberFacade = memberFacade;
+        this.databaseCleanUp = databaseCleanUp;
+    }
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("포인트 조회 (GET /api/v1/points)")
+    @Nested
+    class PointInquiry {
+        
+        @DisplayName("유효한 사용자 ID로 조회 시 200과 보유 포인트를 반환한다")
+        @Test
+        void shouldReturn200AndPoints_whenValidUserIdProvided() {
+            memberFacade.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", "test123");
+
+            ParameterizedTypeReference<ApiResponse<PointV1Dto.PointResponse>> responseType = 
+                    new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<PointV1Dto.PointResponse>> response =
+                    testRestTemplate.exchange("/api/v1/points", HttpMethod.GET, 
+                            new HttpEntity<>(null, headers), responseType);
+
+            assertAll(
+                    () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
+                    () -> assertThat(response.getBody().data().points()).isEqualTo(BigDecimal.ZERO)
+            );
+        }
+
+        @DisplayName("X-USER-ID 헤더가 누락된 경우 400 Bad Request를 반환한다")
+        @Test
+        void shouldReturn400_whenUserIdHeaderIsMissing() {
+            ParameterizedTypeReference<ApiResponse<PointV1Dto.PointResponse>> responseType = 
+                    new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<PointV1Dto.PointResponse>> response =
+                    testRestTemplate.exchange("/api/v1/points", HttpMethod.GET, 
+                            new HttpEntity<>(null), responseType);
+
+            assertAll(
+                    () -> assertTrue(response.getStatusCode().is4xxClientError()),
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+            );
+        }
+    }
+}
\ No newline at end of file

From 624f6e76ad5b1ef1acbb8d4d9fa75a9f41fbd821 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:39:51 +0900
Subject: [PATCH 05/17] =?UTF-8?q?feat:=20SecurityConfig=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

---
 .../com/loopers/config/SecurityConfig.java    | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java
new file mode 100644
index 000000000..cc4ae7646
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java
@@ -0,0 +1,28 @@
+package com.loopers.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        return http
+                .csrf(AbstractHttpConfigurer::disable)
+                .authorizeHttpRequests(authz -> authz.anyRequest().permitAll())
+                .build();
+    }
+}
\ No newline at end of file

From c7385cdfa09412ccb2598b27e974fc3d941e8bd6 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:40:02 +0900
Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=ED=83=80?=
 =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/loopers/support/error/ErrorType.java   | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

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 5d142efbf..d3c212936 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
@@ -7,11 +7,16 @@
 @Getter
 @RequiredArgsConstructor
 public enum ErrorType {
-    /** 범용 에러 */
+    /**
+     * 범용 에러
+     */
     INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."),
     BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."),
     NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."),
-    CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다.");
+    CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."),
+
+    MISSING_REQUEST_HEADER(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "헤더 값은 필수입니다."),
+    INVALID_TOKEN(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "적절하지 않은 토큰 값입니다.");
 
     private final HttpStatus status;
     private final String code;

From 37a55a63f18d566276e683f07fe705f1c27f9654 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:40:25 +0900
Subject: [PATCH 07/17] =?UTF-8?q?fix:=20=EC=95=88=20=EC=93=B0=EB=8A=94=20e?=
 =?UTF-8?q?xample=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/example/ExampleFacade.java    |  17 ---
 .../application/example/ExampleInfo.java      |  13 --
 .../loopers/domain/example/ExampleModel.java  |  44 -------
 .../domain/example/ExampleRepository.java     |   7 --
 .../domain/example/ExampleService.java        |  20 ---
 .../example/ExampleJpaRepository.java         |   6 -
 .../example/ExampleRepositoryImpl.java        |  19 ---
 .../api/example/ExampleV1ApiSpec.java         |  19 ---
 .../api/example/ExampleV1Controller.java      |  28 -----
 .../interfaces/api/example/ExampleV1Dto.java  |  15 ---
 .../domain/example/ExampleModelTest.java      |  65 ----------
 .../ExampleServiceIntegrationTest.java        |  72 -----------
 .../interfaces/api/ExampleV1ApiE2ETest.java   | 114 ------------------
 13 files changed, 439 deletions(-)
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
deleted file mode 100644
index 552a9ad62..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.loopers.application.example;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.domain.example.ExampleService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleFacade {
-    private final ExampleService exampleService;
-
-    public ExampleInfo getExample(Long id) {
-        ExampleModel example = exampleService.getExample(id);
-        return ExampleInfo.from(example);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
deleted file mode 100644
index 877aba96c..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.loopers.application.example;
-
-import com.loopers.domain.example.ExampleModel;
-
-public record ExampleInfo(Long id, String name, String description) {
-    public static ExampleInfo from(ExampleModel model) {
-        return new ExampleInfo(
-            model.getId(),
-            model.getName(),
-            model.getDescription()
-        );
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
deleted file mode 100644
index c588c4a8a..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.loopers.domain.example;
-
-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;
-
-@Entity
-@Table(name = "example")
-public class ExampleModel extends BaseEntity {
-
-    private String name;
-    private String description;
-
-    protected ExampleModel() {}
-
-    public ExampleModel(String name, String description) {
-        if (name == null || name.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "이름은 비어있을 수 없습니다.");
-        }
-        if (description == null || description.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "설명은 비어있을 수 없습니다.");
-        }
-
-        this.name = name;
-        this.description = description;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void update(String newDescription) {
-        if (newDescription == null || newDescription.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "설명은 비어있을 수 없습니다.");
-        }
-        this.description = newDescription;
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
deleted file mode 100644
index 3625e5662..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.loopers.domain.example;
-
-import java.util.Optional;
-
-public interface ExampleRepository {
-    Optional<ExampleModel> find(Long id);
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
deleted file mode 100644
index c0e8431e8..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleService {
-
-    private final ExampleRepository exampleRepository;
-
-    @Transactional(readOnly = true)
-    public ExampleModel getExample(Long id) {
-        return exampleRepository.find(id)
-            .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] 예시를 찾을 수 없습니다."));
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
deleted file mode 100644
index ce6d3ead0..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.loopers.infrastructure.example;
-
-import com.loopers.domain.example.ExampleModel;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface ExampleJpaRepository extends JpaRepository<ExampleModel, Long> {}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
deleted file mode 100644
index 37f2272f0..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.loopers.infrastructure.example;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.domain.example.ExampleRepository;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.Optional;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleRepositoryImpl implements ExampleRepository {
-    private final ExampleJpaRepository exampleJpaRepository;
-
-    @Override
-    public Optional<ExampleModel> find(Long id) {
-        return exampleJpaRepository.findById(id);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
deleted file mode 100644
index 219e3101e..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-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 = "Example V1 API", description = "Loopers 예시 API 입니다.")
-public interface ExampleV1ApiSpec {
-
-    @Operation(
-        summary = "예시 조회",
-        description = "ID로 예시를 조회합니다."
-    )
-    ApiResponse<ExampleV1Dto.ExampleResponse> getExample(
-        @Schema(name = "예시 ID", description = "조회할 예시의 ID")
-        Long exampleId
-    );
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
deleted file mode 100644
index 917376016..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.application.example.ExampleFacade;
-import com.loopers.application.example.ExampleInfo;
-import com.loopers.interfaces.api.ApiResponse;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RequiredArgsConstructor
-@RestController
-@RequestMapping("/api/v1/examples")
-public class ExampleV1Controller implements ExampleV1ApiSpec {
-
-    private final ExampleFacade exampleFacade;
-
-    @GetMapping("/{exampleId}")
-    @Override
-    public ApiResponse<ExampleV1Dto.ExampleResponse> getExample(
-        @PathVariable(value = "exampleId") Long exampleId
-    ) {
-        ExampleInfo info = exampleFacade.getExample(exampleId);
-        ExampleV1Dto.ExampleResponse response = ExampleV1Dto.ExampleResponse.from(info);
-        return ApiResponse.success(response);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
deleted file mode 100644
index 4ecf0eea5..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.application.example.ExampleInfo;
-
-public class ExampleV1Dto {
-    public record ExampleResponse(Long id, String name, String description) {
-        public static ExampleResponse from(ExampleInfo info) {
-            return new ExampleResponse(
-                info.id(),
-                info.name(),
-                info.description()
-            );
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
deleted file mode 100644
index 44ca7576e..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.loopers.domain.example;
-
-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;
-
-class ExampleModelTest {
-    @DisplayName("예시 모델을 생성할 때, ")
-    @Nested
-    class Create {
-        @DisplayName("제목과 설명이 모두 주어지면, 정상적으로 생성된다.")
-        @Test
-        void createsExampleModel_whenNameAndDescriptionAreProvided() {
-            // arrange
-            String name = "제목";
-            String description = "설명";
-
-            // act
-            ExampleModel exampleModel = new ExampleModel(name, description);
-
-            // assert
-            assertAll(
-                () -> assertThat(exampleModel.getId()).isNotNull(),
-                () -> assertThat(exampleModel.getName()).isEqualTo(name),
-                () -> assertThat(exampleModel.getDescription()).isEqualTo(description)
-            );
-        }
-
-        @DisplayName("제목이 빈칸으로만 이루어져 있으면, BAD_REQUEST 예외가 발생한다.")
-        @Test
-        void throwsBadRequestException_whenTitleIsBlank() {
-            // arrange
-            String name = "   ";
-
-            // act
-            CoreException result = assertThrows(CoreException.class, () -> {
-                new ExampleModel(name, "설명");
-            });
-
-            // assert
-            assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-        }
-
-        @DisplayName("설명이 비어있으면, BAD_REQUEST 예외가 발생한다.")
-        @Test
-        void throwsBadRequestException_whenDescriptionIsEmpty() {
-            // arrange
-            String description = "";
-
-            // act
-            CoreException result = assertThrows(CoreException.class, () -> {
-                new ExampleModel("제목", description);
-            });
-
-            // assert
-            assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
deleted file mode 100644
index bbd5fdbe1..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.infrastructure.example.ExampleJpaRepository;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-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 static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-@SpringBootTest
-class ExampleServiceIntegrationTest {
-    @Autowired
-    private ExampleService exampleService;
-
-    @Autowired
-    private ExampleJpaRepository exampleJpaRepository;
-
-    @Autowired
-    private DatabaseCleanUp databaseCleanUp;
-
-    @AfterEach
-    void tearDown() {
-        databaseCleanUp.truncateAllTables();
-    }
-
-    @DisplayName("예시를 조회할 때,")
-    @Nested
-    class Get {
-        @DisplayName("존재하는 예시 ID를 주면, 해당 예시 정보를 반환한다.")
-        @Test
-        void returnsExampleInfo_whenValidIdIsProvided() {
-            // arrange
-            ExampleModel exampleModel = exampleJpaRepository.save(
-                new ExampleModel("예시 제목", "예시 설명")
-            );
-
-            // act
-            ExampleModel result = exampleService.getExample(exampleModel.getId());
-
-            // assert
-            assertAll(
-                () -> assertThat(result).isNotNull(),
-                () -> assertThat(result.getId()).isEqualTo(exampleModel.getId()),
-                () -> assertThat(result.getName()).isEqualTo(exampleModel.getName()),
-                () -> assertThat(result.getDescription()).isEqualTo(exampleModel.getDescription())
-            );
-        }
-
-        @DisplayName("존재하지 않는 예시 ID를 주면, NOT_FOUND 예외가 발생한다.")
-        @Test
-        void throwsException_whenInvalidIdIsProvided() {
-            // arrange
-            Long invalidId = 999L; // Assuming this ID does not exist
-
-            // act
-            CoreException exception = assertThrows(CoreException.class, () -> {
-                exampleService.getExample(invalidId);
-            });
-
-            // assert
-            assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
deleted file mode 100644
index 1bb3dba65..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.loopers.interfaces.api;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.infrastructure.example.ExampleJpaRepository;
-import com.loopers.interfaces.api.example.ExampleV1Dto;
-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.util.function.Function;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-class ExampleV1ApiE2ETest {
-
-    private static final Function<Long, String> ENDPOINT_GET = id -> "/api/v1/examples/" + id;
-
-    private final TestRestTemplate testRestTemplate;
-    private final ExampleJpaRepository exampleJpaRepository;
-    private final DatabaseCleanUp databaseCleanUp;
-
-    @Autowired
-    public ExampleV1ApiE2ETest(
-        TestRestTemplate testRestTemplate,
-        ExampleJpaRepository exampleJpaRepository,
-        DatabaseCleanUp databaseCleanUp
-    ) {
-        this.testRestTemplate = testRestTemplate;
-        this.exampleJpaRepository = exampleJpaRepository;
-        this.databaseCleanUp = databaseCleanUp;
-    }
-
-    @AfterEach
-    void tearDown() {
-        databaseCleanUp.truncateAllTables();
-    }
-
-    @DisplayName("GET /api/v1/examples/{id}")
-    @Nested
-    class Get {
-        @DisplayName("존재하는 예시 ID를 주면, 해당 예시 정보를 반환한다.")
-        @Test
-        void returnsExampleInfo_whenValidIdIsProvided() {
-            // arrange
-            ExampleModel exampleModel = exampleJpaRepository.save(
-                new ExampleModel("예시 제목", "예시 설명")
-            );
-            String requestUrl = ENDPOINT_GET.apply(exampleModel.getId());
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
-                () -> assertThat(response.getBody().data().id()).isEqualTo(exampleModel.getId()),
-                () -> assertThat(response.getBody().data().name()).isEqualTo(exampleModel.getName()),
-                () -> assertThat(response.getBody().data().description()).isEqualTo(exampleModel.getDescription())
-            );
-        }
-
-        @DisplayName("숫자가 아닌 ID 로 요청하면, 400 BAD_REQUEST 응답을 받는다.")
-        @Test
-        void throwsBadRequest_whenIdIsNotProvided() {
-            // arrange
-            String requestUrl = "/api/v1/examples/나나";
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is4xxClientError()),
-                () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
-            );
-        }
-
-        @DisplayName("존재하지 않는 예시 ID를 주면, 404 NOT_FOUND 응답을 받는다.")
-        @Test
-        void throwsException_whenInvalidIdIsProvided() {
-            // arrange
-            Long invalidId = -1L;
-            String requestUrl = ENDPOINT_GET.apply(invalidId);
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is4xxClientError()),
-                () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND)
-            );
-        }
-    }
-}

From 4fc9310c2262bf290f980de237b0934b6738e6c1 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:37:18 +0900
Subject: [PATCH 08/17] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?=
 =?UTF-8?q?=20=EB=AA=A8=EB=8D=B8=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - MemberModel 필드명 통일 (userId → memberId)
  - Gender enum을 members 패키지로 이동
  - validation 규칙명 통일 (USER_ID_REGEX → MEMBER_ID_REGEX)
---
 .../com/loopers/domain/members/Gender.java    |   7 ++
 .../loopers/domain/members/MemberModel.java   | 103 +++++++-----------
 .../members/MemberRepository.java             |  12 --
 3 files changed, 48 insertions(+), 74 deletions(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/members/Gender.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/Gender.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/Gender.java
new file mode 100644
index 000000000..7eb92b9ee
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/Gender.java
@@ -0,0 +1,7 @@
+package com.loopers.domain.members;
+
+public enum Gender {
+    MALE,
+    FEMALE,
+    OTHER
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
index e28c0ba11..3306e0898 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
@@ -2,10 +2,13 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 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.EnumType;
+import jakarta.persistence.Enumerated;
 import jakarta.persistence.Table;
-import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
@@ -15,92 +18,68 @@
 import java.util.regex.Pattern;
 
 @NoArgsConstructor
-@AllArgsConstructor
 @Getter
 @Entity
-@Table(name = "member")
+@Table(name = "members")
 public class MemberModel extends BaseEntity {
 
-    private static final Pattern MEMBER_ID_PATTERN = Pattern.compile("^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{1,10}$");
-    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$");
+    private static final Pattern MEMBER_ID_REGEX = Pattern.compile("^[a-zA-Z0-9]{1,10}$");
+    private static final Pattern EMAIL_REGEX = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$");
     private static final DateTimeFormatter BIRTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
 
-    @Column(unique = true, nullable = false, length = 10)
+    @Column(name = "member_id", nullable = false, unique = true, length = 10)
     private String memberId;
 
-    @Column(nullable = false)
-    private String name;
-
-    @Column(nullable = false)
+    @Column(name = "email", nullable = false)
     private String email;
 
-    @JsonIgnore
-    @Column(nullable = false)
-    private String password;
-
-    @Column(nullable = false)
+    @Column(name = "birth_date", nullable = false)
     private LocalDate birthDate;
 
-    @Column(nullable = false)
-    private String gender;
+    @Enumerated(EnumType.STRING)
+    @Column(name = "gender", nullable = false)
+    private Gender gender;
+
+    @Column(name = "password", nullable = false)
+    private String password;
 
-    public static MemberModel create(String memberId, String name, String email, String password, String birthDate, String gender) {
+    public MemberModel(String memberId, String email, String password, String birthDate, Gender gender) {
         validateMemberId(memberId);
         validateEmail(email);
-        LocalDate parsedBirthDate = validateBirthDate(birthDate);
-        validateGender(gender);
-
-        return new MemberModel(
-                memberId,
-                name,
-                email,
-                password,
-                parsedBirthDate,
-                gender
-        );
+        this.memberId = memberId;
+        this.email = email;
+        this.password = password;
+        this.birthDate = parseBirthDate(birthDate);
+        this.gender = gender;
     }
 
-    private static void validateMemberId(String MemberId) {
-        if (MemberId == null || MemberId.isBlank()) {
-            throw new IllegalArgumentException("사용자 ID는 필수입니다.");
-        }
-
-        if (!MEMBER_ID_PATTERN.matcher(MemberId).matches()) {
-            throw new IllegalArgumentException("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+    private static void validateMemberId(String memberId) {
+        if (!MEMBER_ID_REGEX.matcher(memberId).matches()) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "ID는 영문 및 숫자 10자 이내여야 합니다."
+            );
         }
-
     }
 
-    private static LocalDate validateBirthDate(String birthDate) {
-        if (birthDate == null || birthDate.isBlank()) {
-            throw new IllegalArgumentException("생년월일은 필수입니다.");
+    private static void validateEmail(String email) {
+        if (!EMAIL_REGEX.matcher(email).matches()) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "이메일은 xx@yy.zz 형식이어야 합니다."
+            );
         }
+    }
 
+    private static LocalDate parseBirthDate(String birthDate) {
         try {
-            LocalDate parsedDate = LocalDate.parse(birthDate, BIRTH_DATE_FORMATTER);
-
-            if (parsedDate.isAfter(LocalDate.now())) {
-                throw new IllegalArgumentException("생년월일은 미래일 수 없습니다");
-            }
-            return parsedDate;
+            return LocalDate.parse(birthDate, BIRTH_DATE_FORMATTER);
         } catch (DateTimeParseException e) {
-            throw new IllegalArgumentException("생년월일 형식이 올바르지 않습니다.");
-        }
-    }
-
-    private static void validateEmail(String email) {
-        if (email == null || email.isBlank()) {
-            throw new IllegalArgumentException("이메일은 필수입니다.");
-        }
-
-        if (!EMAIL_PATTERN.matcher(email).matches()) {
-            throw new IllegalArgumentException("이메일 형식이 올바르지 않습니다.");
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "생년월일은 yyyy-MM-dd 형식이어야 합니다."
+            );
         }
     }
 
-    private static void validateGender(String gender) {
-        if (gender == null || (!gender.equals("MALE") && !gender.equals("FEMALE"))) {
-            throw new IllegalArgumentException("성별은 MALE 또는 FEMALE 이어야 합니다");
-        }
-    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java
deleted file mode 100644
index 490a01dcd..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepository.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.loopers.infrastructure.members;
-
-import com.loopers.domain.members.MemberModel;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-import java.util.Optional;
-
-public interface MemberRepository extends JpaRepository<MemberModel, Long> {
-    Optional<MemberModel> findByMemberId(String memberId);
-
-    boolean existsByMemberId(String memberId);
-}

From e2d9b278a6a7f161976ba5e49a609263c60b1fc4 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:37:26 +0900
Subject: [PATCH 09/17] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B9=84=EC=8A=A4?=
 =?UTF-8?q?=20=EA=B3=84=EC=B8=B5=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20?=
 =?UTF-8?q?=ED=86=B5=EC=9D=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - User → Member 네이밍 일관성 적용
  - 포인트 관련 메서드 제거하여 책임 분리
  - findByUserId → findByMemberId로 변경
---
 .../loopers/domain/members/MemberRepository.java  |  8 ++------
 .../com/loopers/domain/members/MemberService.java | 15 +++++++++------
 .../members/MemberJpaRepository.java              |  6 +++---
 .../members/MemberRepositoryImpl.java             | 14 ++++++--------
 4 files changed, 20 insertions(+), 23 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java
index 7adc82a60..d7f4eb274 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java
@@ -1,11 +1,7 @@
 package com.loopers.domain.members;
 
-import java.util.Optional;
-
 public interface MemberRepository {
-    Optional<MemberModel> findByMemberId(String memberId);
-
-    MemberModel save(MemberModel memberModel);
-
+    MemberModel save(MemberModel member);
+    MemberModel findByMemberId(String memberId);
     boolean existsByMemberId(String memberId);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
index bcbee8167..74311ece3 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
@@ -15,18 +15,21 @@ public class MemberService {
     private final PasswordEncoder passwordEncoder;
 
     @Transactional
-    public MemberModel registerMember(String memberId, String name, String email, String password, String birthDate, String gender) {
+    public MemberModel registerMember(String memberId, String email, String password, String birthDate, Gender gender) {
         if (memberRepository.existsByMemberId(memberId)) {
-            throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 사용자 ID입니다.");
+            throw new CoreException(
+                    ErrorType.CONFLICT,
+                    "이미 가입된 ID 입니다."
+            );
         }
-        
+
         String encodedPassword = passwordEncoder.encode(password);
-        MemberModel member = MemberModel.create(memberId, name, email, encodedPassword, birthDate, gender);
+        MemberModel member = new MemberModel(memberId, email, encodedPassword, birthDate, gender);
         return memberRepository.save(member);
     }
 
     @Transactional(readOnly = true)
-    public MemberModel getMember(String memberId) {
-        return memberRepository.findByMemberId(memberId).orElse(null);
+    public MemberModel getMemberByMemberId(String memberId) {
+        return memberRepository.findByMemberId(memberId);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java
index 18b9dcc3b..2df08f60a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberJpaRepository.java
@@ -2,10 +2,10 @@
 
 import com.loopers.domain.members.MemberModel;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
 
-import java.util.Optional;
-
+@Repository
 public interface MemberJpaRepository extends JpaRepository<MemberModel, Long> {
-    Optional<MemberModel> findByMemberId(String memberId);
+    MemberModel findByMemberId(String memberId);
     boolean existsByMemberId(String memberId);
 }
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java
index ec4ba8153..ed01e7076 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/members/MemberRepositoryImpl.java
@@ -3,23 +3,21 @@
 import com.loopers.domain.members.MemberModel;
 import com.loopers.domain.members.MemberRepository;
 import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.Optional;
+import org.springframework.stereotype.Repository;
 
 @RequiredArgsConstructor
-@Component
+@Repository
 public class MemberRepositoryImpl implements MemberRepository {
     private final MemberJpaRepository memberJpaRepository;
 
     @Override
-    public Optional<MemberModel> findByMemberId(String memberId) {
-        return memberJpaRepository.findByMemberId(memberId);
+    public MemberModel save(MemberModel member) {
+        return memberJpaRepository.save(member);
     }
 
     @Override
-    public MemberModel save(MemberModel memberModel) {
-        return memberJpaRepository.save(memberModel);
+    public MemberModel findByMemberId(String memberId) {
+        return memberJpaRepository.findByMemberId(memberId);
     }
 
     @Override

From 694cf6b162607b540c0e9151773db3b977f8f734 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:37:34 +0900
Subject: [PATCH 10/17] =?UTF-8?q?refactor:=20=EC=96=B4=ED=94=8C=EB=A6=AC?=
 =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=EA=B3=84=EC=B8=B5=20=EB=A6=AC?=
 =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - MemberInfo에서 민감 정보(password) 제외
  - 포인트 관련 로직 제거하여 단일 책임 원칙 적용
  - User → Member 네이밍 통일
---
 .../application/members/MemberFacade.java     | 28 +++++++++----------
 .../application/members/MemberInfo.java       | 10 +++++--
 2 files changed, 22 insertions(+), 16 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
index f747cb5b7..69819149c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
@@ -1,31 +1,31 @@
 package com.loopers.application.members;
 
+import com.loopers.domain.members.Gender;
 import com.loopers.domain.members.MemberModel;
 import com.loopers.domain.members.MemberService;
-import com.loopers.domain.points.PointService;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
-import java.math.BigDecimal;
-
 @RequiredArgsConstructor
 @Component
 public class MemberFacade {
     private final MemberService memberService;
-    private final PointService pointService;
 
-    public MemberInfo registerMember(String memberId, String name, String email, String password, String birthDate, String gender) {
-        MemberModel member = memberService.registerMember(memberId, name, email, password, birthDate, gender);
-        pointService.initializeMemberPoints(memberId);
+    public MemberInfo registerMember(String memberId, String email, String password, String birthDate, Gender gender) {
+        MemberModel member = memberService.registerMember(memberId, email, password, birthDate, gender);
         return MemberInfo.from(member);
     }
 
-    public MemberInfo getMemberInfo(String memberId) {
-        MemberModel member = memberService.getMember(memberId);
-        return member != null ? MemberInfo.from(member) : null;
-    }
-
-    public BigDecimal getMemberPoints(String memberId) {
-        return pointService.getMemberPoints(memberId);
+    public MemberInfo getMemberByMemberId(String memberId) {
+        MemberModel member = memberService.getMemberByMemberId(memberId);
+        if (member == null) {
+            throw new CoreException(
+                    ErrorType.NOT_FOUND,
+                    "존재하지 않는 회원입니다."
+            );
+        }
+        return MemberInfo.from(member);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java
index d396a760c..c6708ba0c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java
@@ -1,15 +1,21 @@
 package com.loopers.application.members;
 
+import com.loopers.domain.members.Gender;
 import com.loopers.domain.members.MemberModel;
 
 import java.time.LocalDate;
 
-public record MemberInfo(Long id, String memberId, String name, String email, LocalDate birthDate, String gender) {
+public record MemberInfo(
+        Long id, 
+        String memberId, 
+        String email, 
+        LocalDate birthDate, 
+        Gender gender
+) {
     public static MemberInfo from(MemberModel model) {
         return new MemberInfo(
                 model.getId(),
                 model.getMemberId(),
-                model.getName(),
                 model.getEmail(),
                 model.getBirthDate(),
                 model.getGender()

From 79317389da02e84f6051849e036569825147092e Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:37:41 +0900
Subject: [PATCH 11/17] =?UTF-8?q?feat:=20API=20DTO=EC=97=90=20validation?=
 =?UTF-8?q?=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - RegisterMemberRequest에 Bean Validation 애노테이션 추가
  - Swagger 문서화를 위한 @Schema 애노테이션 추가
  - 응답 DTO에서 민감 정보 제외 보장
---
 .../interfaces/api/members/MemberV1Dto.java   | 48 +++++++++----------
 1 file changed, 23 insertions(+), 25 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java
index 69bd64dd4..bc0e9ae90 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Dto.java
@@ -1,69 +1,67 @@
 package com.loopers.interfaces.api.members;
 
 import com.loopers.application.members.MemberInfo;
+import com.loopers.domain.members.Gender;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.Email;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Pattern;
-import jakarta.validation.constraints.Size;
-
-import java.math.BigDecimal;
 import java.time.LocalDate;
 
 public class MemberV1Dto {
 
     @Schema(description = "회원 가입 요청")
-    public record MemberRegisterRequest(
-            @Schema(description = "사용자 ID", example = "test123")
-            @NotBlank(message = "사용자 ID는 필수입니다.")
-            @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{1,10}$", message = "사용자 ID는 영문 및 숫자 10자 이내여야 합니다.")
+    public record RegisterMemberRequest(
+            @Schema(description = "회원 ID", example = "test123")
+            @NotBlank(message = "회원 ID는 필수입니다.")
+            @Pattern(regexp = "^[a-zA-Z0-9]{1,10}$", message = "ID는 영문 및 숫자 10자 이내여야 합니다.")
             String memberId,
-            @Schema(description = "이름", example = "홍길동")
-            @NotBlank(message = "이름은 필수입니다.")
-            String name,
+            
             @Schema(description = "이메일", example = "test@example.com")
             @NotBlank(message = "이메일은 필수입니다.")
             @Email(message = "이메일 형식이 올바르지 않습니다.")
             String email,
+            
             @Schema(description = "비밀번호", example = "password123")
             @NotBlank(message = "비밀번호는 필수입니다.")
             String password,
+            
             @Schema(description = "생년월일", example = "1990-01-01")
             @NotBlank(message = "생년월일은 필수입니다.")
-            @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "생년월일 형식이 올바르지 않습니다.")
+            @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "생년월일은 yyyy-MM-dd 형식이어야 합니다.")
             String birthDate,
+            
             @Schema(description = "성별", example = "MALE")
             @NotNull(message = "성별은 필수입니다.")
-            @Pattern(regexp = "^(MALE|FEMALE)$", message = "성별은 MALE 또는 FEMALE이어야 합니다.")
-            String gender
+            Gender gender
     ) {}
 
     @Schema(description = "회원 정보 응답")
     public record MemberResponse(
             @Schema(description = "회원 ID")
             Long id,
-            @Schema(description = "사용자 ID")
+            
+            @Schema(description = "회원 ID")
             String memberId,
-            @Schema(description = "이름")
-            String name,
+            
             @Schema(description = "이메일")
             String email,
+            
             @Schema(description = "생년월일")
             LocalDate birthDate,
+            
             @Schema(description = "성별")
-            String gender
+            Gender gender
     ) {
-        public static MemberResponse from(MemberInfo memberInfo) {
+        public static MemberResponse from(MemberInfo info) {
             return new MemberResponse(
-                    memberInfo.id(),
-                    memberInfo.memberId(),
-                    memberInfo.name(),
-                    memberInfo.email(),
-                    memberInfo.birthDate(),
-                    memberInfo.gender()
+                    info.id(),
+                    info.memberId(),
+                    info.email(),
+                    info.birthDate(),
+                    info.gender()
             );
         }
     }
-
 }
\ No newline at end of file

From 8d25f25cc1b4bfdcbea0355405521ef73a611bc8 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:37:48 +0900
Subject: [PATCH 12/17] =?UTF-8?q?refactor:=20API=20=EC=BB=A8=ED=8A=B8?=
 =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EB=B0=8F=20=EC=8A=A4=ED=8E=99=20=EC=A0=95?=
 =?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - User → Member 네이밍 통일
  - 포인트 관련 엔드포인트 제거
  - @Valid 애노테이션 추가하여 요청 검증 활성화
---
 .../api/members/MemberV1ApiSpec.java          | 12 ++++------
 .../api/members/MemberV1Controller.java       | 23 +++++++------------
 2 files changed, 12 insertions(+), 23 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java
index 8a681bc37..29b214171 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1ApiSpec.java
@@ -2,18 +2,14 @@
 
 import com.loopers.interfaces.api.ApiResponse;
 import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 
-@Tag(name = "Member", description = "회원 관리 API")
+@Tag(name = "Member V1", description = "회원 관리 API")
 public interface MemberV1ApiSpec {
 
     @Operation(summary = "회원 가입", description = "새로운 회원을 등록합니다.")
-    ApiResponse<MemberV1Dto.MemberResponse> registerMember(MemberV1Dto.MemberRegisterRequest request);
-
-    @Operation(summary = "내 정보 조회", description = "현재 로그인한 회원의 정보를 조회합니다.")
-    ApiResponse<MemberV1Dto.MemberResponse> getMemberInfo(
-            @Parameter(description = "사용자 ID", required = true) String userId
-    );
+    ApiResponse<MemberV1Dto.MemberResponse> registerMember(MemberV1Dto.RegisterMemberRequest request);
 
+    @Operation(summary = "내 정보 조회", description = "회원 ID로 정보를 조회합니다.")
+    ApiResponse<MemberV1Dto.MemberResponse> getMemberByMemberId(String memberId);
 }
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java
index b1e0a22de..3aaad045d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/members/MemberV1Controller.java
@@ -8,29 +8,27 @@
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.math.BigDecimal;
-
 @RequiredArgsConstructor
 @RestController
-@RequestMapping("/api/v1/members")
+@RequestMapping("/api/v1")
 public class MemberV1Controller implements MemberV1ApiSpec {
 
     private final MemberFacade memberFacade;
 
-    @PostMapping("/register")
+    @PostMapping("/members")
     @Override
     public ApiResponse<MemberV1Dto.MemberResponse> registerMember(
-            @Valid @RequestBody MemberV1Dto.MemberRegisterRequest request
+            @Valid @RequestBody MemberV1Dto.RegisterMemberRequest request
     ) {
         MemberInfo memberInfo = memberFacade.registerMember(
                 request.memberId(),
-                request.name(),
                 request.email(),
                 request.password(),
                 request.birthDate(),
@@ -41,18 +39,13 @@ public ApiResponse<MemberV1Dto.MemberResponse> registerMember(
         return ApiResponse.success(response);
     }
 
-    @GetMapping("/me")
+    @GetMapping("/members/{memberId}")
     @Override
-    public ApiResponse<MemberV1Dto.MemberResponse> getMemberInfo(
-            @RequestHeader("X-USER-ID") String userId
+    public ApiResponse<MemberV1Dto.MemberResponse> getMemberByMemberId(
+            @PathVariable String memberId
     ) {
-        MemberInfo memberInfo = memberFacade.getMemberInfo(userId);
-        if (memberInfo == null) {
-            throw new CoreException(ErrorType.NOT_FOUND, "회원을 찾을 수 없습니다.");
-        }
-        
+        MemberInfo memberInfo = memberFacade.getMemberByMemberId(memberId);
         MemberV1Dto.MemberResponse response = MemberV1Dto.MemberResponse.from(memberInfo);
         return ApiResponse.success(response);
     }
-
 }
\ No newline at end of file

From ddc2defa575fb253456ec1c9dccf02d8ac906241 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:37:58 +0900
Subject: [PATCH 13/17] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
 =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - 변경된 도메인 모델 구조에 맞춰 테스트 수정
  - IllegalArgumentException → CoreException 변경
  - Gender enum 사용하도록 테스트 데이터 수정
---
 .../domain/members/MemberModelTest.java       | 45 +++++++++----------
 .../members/MemberServiceIntegrationTest.java | 18 ++++----
 2 files changed, 31 insertions(+), 32 deletions(-)

diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java
index f30af25f5..5457fa32f 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberModelTest.java
@@ -1,5 +1,7 @@
 package com.loopers.domain.members;
 
+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;
@@ -15,17 +17,16 @@ class MemberModelTest {
     @Nested
     class CreateMemberTest {
 
-        @DisplayName("정상 값이 들어오면 User 객체 생성에 성공한다")
+        @DisplayName("정상 값이 들어오면 Member 객체 생성에 성공한다")
         @Test
         void shouldSaveMember_whenRequestIsValid() {
-            MemberModel member = MemberModel.create("test123", "홍길동", "test@example.com", "password123", "1990-01-01", "MALE");
+            MemberModel member = new MemberModel("test123", "test@example.com", "password123", "1990-01-01", Gender.MALE);
 
             assertThat(member).isNotNull();
             assertThat(member.getMemberId()).isEqualTo("test123");
             assertThat(member.getEmail()).isEqualTo("test@example.com");
             assertThat(member.getBirthDate()).isEqualTo(java.time.LocalDate.of(1990, 1, 1));
-            assertThat(member.getGender()).isEqualTo("MALE");
-
+            assertThat(member.getGender()).isEqualTo(Gender.MALE);
         }
 
         @ParameterizedTest
@@ -33,11 +34,9 @@ void shouldSaveMember_whenRequestIsValid() {
         @DisplayName("Member ID가 10자 초과이면 예외가 발생한다")
         void shouldThrowException_whenMemberIdTooLong(String invalidMemberId) {
             assertThatThrownBy(() ->
-                    MemberModel.create(
-                            invalidMemberId, "홍길동", "test@example.com", "password", "2002-02-02", "FEMALE"
-                    )
-            ).isInstanceOf(IllegalArgumentException.class)
-                    .hasMessageContaining("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+                    new MemberModel(invalidMemberId, "test@example.com", "password", "2002-02-02", Gender.FEMALE)
+            ).isInstanceOf(CoreException.class)
+                    .hasMessageContaining("ID는 영문 및 숫자 10자 이내여야 합니다.");
         }
 
         @ParameterizedTest
@@ -45,9 +44,9 @@ void shouldThrowException_whenMemberIdTooLong(String invalidMemberId) {
         @DisplayName("ID가 영문으로만 이루어지면 예외가 발생한다")
         void shouldThrowException_IfMemberIdIsLetterOnly(String letterOnlyId) {
             assertThatThrownBy(() ->
-                    MemberModel.create(letterOnlyId, "홍길동", "test@example.com", "password", "2002-02-02", "FEMALE")
-            ).isInstanceOf(IllegalArgumentException.class)
-                    .hasMessageContaining("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+                    new MemberModel(letterOnlyId, "test@example.com", "password", "2002-02-02", Gender.FEMALE)
+            ).isInstanceOf(CoreException.class)
+                    .hasMessageContaining("ID는 영문 및 숫자 10자 이내여야 합니다.");
         }
 
         @ParameterizedTest
@@ -55,29 +54,29 @@ void shouldThrowException_IfMemberIdIsLetterOnly(String letterOnlyId) {
         @DisplayName("ID가 숫자로만 이루어지면 예외가 발생한다")
         void shouldThrowException_IfMemberIdIsNumberOnly(String numberOnlyId) {
             assertThatThrownBy(() ->
-                    MemberModel.create(numberOnlyId, "홍길동", "test@example.com", "password", "2002-02-02", "FEMALE")
-            ).isInstanceOf(IllegalArgumentException.class)
-                    .hasMessageContaining("사용자 ID는 영문 및 숫자 10자 이내여야 합니다.");
+                    new MemberModel(numberOnlyId, "test@example.com", "password", "2002-02-02", Gender.FEMALE)
+            ).isInstanceOf(CoreException.class)
+                    .hasMessageContaining("ID는 영문 및 숫자 10자 이내여야 합니다.");
         }
 
         @ParameterizedTest
         @ValueSource(strings = {"test@", "@example.com", "test@example"})
-        @DisplayName("이메일이 xx@yy.zz 형식에 맞지 않으면, User 객체 생성에 실패한다")
+        @DisplayName("이메일이 xx@yy.zz 형식에 맞지 않으면, Member 객체 생성에 실패한다")
         void shouldThrowException_whenEmailFormatIsInvalid(String invalidEmail) {
             assertThatThrownBy(() ->
-                    MemberModel.create("test123", "홍길동", invalidEmail, "password", "1990-01-01", "MALE")
-            ).isInstanceOf(IllegalArgumentException.class)
-                    .hasMessageContaining("이메일 형식이 올바르지 않습니다.");
+                    new MemberModel("test123", invalidEmail, "password", "1990-01-01", Gender.MALE)
+            ).isInstanceOf(CoreException.class)
+                    .hasMessageContaining("이메일은 xx@yy.zz 형식이어야 합니다.");
         }
 
         @ParameterizedTest
         @ValueSource(strings = {"19900101", "1990/01/01", "90-01-01", "1990-13-01", "1990-01-32"})
-        @DisplayName("생년월일이 yyyy-MM-dd 형식에 맞지 않으면, User 객체 생성에 실패한다")
+        @DisplayName("생년월일이 yyyy-MM-dd 형식에 맞지 않으면, Member 객체 생성에 실패한다")
         void shouldThrowException_whenBirthDateFormatIsInvalid(String invalidBirthDate) {
             assertThatThrownBy(() ->
-                    MemberModel.create("test123", "홍길동", "test@example.com", "password", invalidBirthDate, "MALE")
-            ).isInstanceOf(IllegalArgumentException.class)
-                    .hasMessageContaining("생년월일 형식이 올바르지 않습니다.");
+                    new MemberModel("test123", "test@example.com", "password", invalidBirthDate, Gender.MALE)
+            ).isInstanceOf(CoreException.class)
+                    .hasMessageContaining("생년월일은 yyyy-MM-dd 형식이어야 합니다.");
         }
 
     }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java
index b1e75cf70..c85217430 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/members/MemberServiceIntegrationTest.java
@@ -39,11 +39,11 @@ void tearDown() {
     @Nested
     class RegisterMember {
 
-        @DisplayName("회원 가입시 User 저장이 수행된다 (spy)")
+        @DisplayName("회원 가입시 Member 저장이 수행된다 (spy)")
         @Test
         void shouldSaveMember_whenMemberRegisters() {
             assertThat(memberRepository).isNotNull();
-            memberService.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+            memberService.registerMember("test123", "test@example.com", "password", "1990-01-01", Gender.MALE);
             verify(memberRepository, times(1)).save(any(MemberModel.class));
 
             boolean exists = memberRepository.existsByMemberId("test123");
@@ -53,10 +53,10 @@ void shouldSaveMember_whenMemberRegisters() {
         @DisplayName("이미 가입된 ID로 회원가입 시도 시, 실패한다")
         @Test
         void shouldThrowException_whenDuplicateMemberIdIsUsed() {
-            memberService.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+            memberService.registerMember("test123", "test@example.com", "password", "1990-01-01", Gender.MALE);
 
             CoreException exception = assertThrows(CoreException.class, () -> {
-                memberService.registerMember("test123", "김철수", "kim@example.com", "password", "1985-05-05", "MALE");
+                memberService.registerMember("test123", "kim@example.com", "password", "1985-05-05", Gender.MALE);
             });
 
             assertThat(exception.getErrorType()).isEqualTo(ErrorType.CONFLICT);
@@ -70,22 +70,22 @@ class GetMember {
         @DisplayName("해당 ID의 회원이 존재할 경우, 회원 정보가 반환된다")
         @Test
         void shouldReturnMemberInfo_whenMemberExists() {
-            memberService.registerMember("test123", "홍길동", "test@example.com", "password", "1990-01-01", "MALE");
+            memberService.registerMember("test123", "test@example.com", "password", "1990-01-01", Gender.MALE);
 
-            MemberModel result = memberService.getMember("test123");
+            MemberModel result = memberService.getMemberByMemberId("test123");
 
             assertAll(
                     () -> assertThat(result).isNotNull(),
                     () -> assertThat(result.getMemberId()).isEqualTo("test123"),
-                    () -> assertThat(result.getName()).isEqualTo("홍길동"),
-                    () -> assertThat(result.getEmail()).isEqualTo("test@example.com")
+                    () -> assertThat(result.getEmail()).isEqualTo("test@example.com"),
+                    () -> assertThat(result.getGender()).isEqualTo(Gender.MALE)
             );
         }
 
         @DisplayName("해당 ID의 회원이 존재하지 않을 경우, null이 반환된다")
         @Test
         void shouldReturnNull_whenMemberDoesNotExist() {
-            MemberModel result = memberService.getMember("nonexistent");
+            MemberModel result = memberService.getMemberByMemberId("nonexistent");
 
             assertThat(result).isNull();
         }

From cea05c15255cf0dca58dca5b40dc4c3b6dff8a9c Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:47:18 +0900
Subject: [PATCH 14/17] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?=
 =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B3=BC=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?=
 =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=EB=8A=94=20=ED=8A=B8=EB=9E=9C?=
 =?UTF-8?q?=EC=9E=AD=EC=85=98=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=9E=A5?=
 =?UTF-8?q?=EB=90=98=EC=96=B4=EC=95=BC=20=ED=95=9C=EB=8B=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/loopers/application/members/MemberFacade.java   | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
index 69819149c..527ed09d1 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
@@ -3,18 +3,23 @@
 import com.loopers.domain.members.Gender;
 import com.loopers.domain.members.MemberModel;
 import com.loopers.domain.members.MemberService;
+import com.loopers.domain.points.PointService;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 @RequiredArgsConstructor
 @Component
 public class MemberFacade {
     private final MemberService memberService;
+    private final PointService pointService;
 
+    @Transactional
     public MemberInfo registerMember(String memberId, String email, String password, String birthDate, Gender gender) {
         MemberModel member = memberService.registerMember(memberId, email, password, birthDate, gender);
+        pointService.initializeMemberPoints(memberId);
         return MemberInfo.from(member);
     }
 

From 436001f831cb8964f4b353a32dbd28b0dcbe624a Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:47:31 +0900
Subject: [PATCH 15/17] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=20?=
 =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=EC=97=90=20jsonignore=20?=
 =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/main/java/com/loopers/domain/members/MemberModel.java    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
index 3306e0898..7ba295322 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
@@ -41,6 +41,7 @@ public class MemberModel extends BaseEntity {
     private Gender gender;
 
     @Column(name = "password", nullable = false)
+    @JsonIgnore
     private String password;
 
     public MemberModel(String memberId, String email, String password, String birthDate, Gender gender) {

From df455a5d110560d6c4bd94c3c3bb4a760f5e6d8d Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:49:30 +0900
Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=20?=
 =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=9E=90=EC=97=90=20gender=20=EA=B2=80?=
 =?UTF-8?q?=EC=A6=9D=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/members/MemberModel.java  | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
index 7ba295322..b51ae5282 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
@@ -47,6 +47,8 @@ public class MemberModel extends BaseEntity {
     public MemberModel(String memberId, String email, String password, String birthDate, Gender gender) {
         validateMemberId(memberId);
         validateEmail(email);
+        validateGender(gender);
+
         this.memberId = memberId;
         this.email = email;
         this.password = password;
@@ -72,6 +74,15 @@ private static void validateEmail(String email) {
         }
     }
 
+    public static void validateGender(Gender gender) {
+        if (gender == null) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "성별은 필수입니다."
+            );
+        }
+    }
+
     private static LocalDate parseBirthDate(String birthDate) {
         try {
             return LocalDate.parse(birthDate, BIRTH_DATE_FORMATTER);

From d1c961c91cc24faae5af85ff1d4e4a496376a470 Mon Sep 17 00:00:00 2001
From: Seoyeon Lee <68765200+sylee6529@users.noreply.github.com>
Date: Thu, 13 Nov 2025 13:00:28 +0900
Subject: [PATCH 17/17] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?=
 =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B2=BD=EC=9F=81=EC=A0=81=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EC=9C=A0=EB=8B=88=ED=81=AC=20=EC=A0=9C?=
 =?UTF-8?q?=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=97=90=EB=9F=AC=20=EC=B2=98?=
 =?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/loopers/domain/members/MemberService.java    | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
index 74311ece3..109574080 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
@@ -3,6 +3,7 @@
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
+import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
@@ -16,16 +17,17 @@ public class MemberService {
 
     @Transactional
     public MemberModel registerMember(String memberId, String email, String password, String birthDate, Gender gender) {
-        if (memberRepository.existsByMemberId(memberId)) {
+        String encodedPassword = passwordEncoder.encode(password);
+        MemberModel member = new MemberModel(memberId, email, encodedPassword, birthDate, gender);
+
+        try {
+            return memberRepository.save(member);
+        } catch (DataIntegrityViolationException e) {
             throw new CoreException(
                     ErrorType.CONFLICT,
                     "이미 가입된 ID 입니다."
             );
         }
-
-        String encodedPassword = passwordEncoder.encode(password);
-        MemberModel member = new MemberModel(memberId, email, encodedPassword, birthDate, gender);
-        return memberRepository.save(member);
     }
 
     @Transactional(readOnly = true)
