From aac83c1b02c6790d85ae84153a5878b53e7edaaa Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Tue, 15 Jul 2025 06:52:19 +0900
Subject: [PATCH 1/8] feat: user controller

---
 .../interfaces/api/user/UserV1ApiSpec.java    | 19 ++++++++
 .../interfaces/api/user/UserV1Controller.java | 17 +++++++
 .../interfaces/api/user/UserV1Dto.java        |  9 ++++
 .../interfaces/api/UserV1ApiE2ETest.java      | 46 +++++++++++++++++++
 4 files changed, 91 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
new file mode 100644
index 000000000..fb5ceda69
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
@@ -0,0 +1,19 @@
+package com.loopers.interfaces.api.user;
+
+import com.loopers.interfaces.api.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@Tag(name = "User V1 API", description = "Loopers 사용자 API 입니다.")
+public interface UserV1ApiSpec {
+    @Operation(
+        summary = "회원 가입",
+        description = "사용자를 등록합니다. 성공 시 생성된 사용자 정보를 반환합니다."
+    )
+    ApiResponse<UserV1Dto.UserResponse> registerUser(
+            @Schema(name = "예시 ID", description = "조회할 예시의 ID")
+            UserV1Dto.UserRequest userV1Dto
+    );
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
new file mode 100644
index 000000000..7455f9032
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
@@ -0,0 +1,17 @@
+package com.loopers.interfaces.api.user;
+
+
+import com.loopers.interfaces.api.ApiResponse;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/users")
+public class UserV1Controller implements UserV1ApiSpec{
+
+    @Override
+    @PostMapping
+    public ApiResponse<UserV1Dto.UserResponse> registerUser(
+            @RequestBody UserV1Dto.UserRequest userV1Dto) {
+        return null;
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
new file mode 100644
index 000000000..7ffa42a83
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
@@ -0,0 +1,9 @@
+package com.loopers.interfaces.api.user;
+
+public class UserV1Dto {
+    public record UserRequest(){
+    }
+    public record UserResponse(){
+
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
new file mode 100644
index 000000000..c55a6d91b
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
@@ -0,0 +1,46 @@
+package com.loopers.interfaces.api;
+
+import com.loopers.interfaces.api.user.UserV1Dto;
+import org.junit.jupiter.api.Disabled;
+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 static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class UserV1ApiE2ETest {
+
+    private final TestRestTemplate testRestTemplate;
+    @Autowired
+    public UserV1ApiE2ETest(TestRestTemplate testRestTemplate) {
+        this.testRestTemplate = testRestTemplate;
+    }
+
+    @Nested
+    @DisplayName("POST /api/v1/users")
+    class post{
+        @DisplayName("회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.")
+        @Test
+        void returnsCreatedUserInfo_whenRegistrationIsSuccessful() {
+             ParameterizedTypeReference<ApiResponse<UserV1Dto.UserResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<UserV1Dto.UserResponse>> response =
+                    testRestTemplate.exchange("/api/v1/users", HttpMethod.POST, new HttpEntity<>(null),responseType);
+            System.out.println(response.getBody());
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK)
+            );
+        }
+    }
+
+}

From 0b8077b823c39809bedb4c9b83106fd6302d7afb Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Tue, 15 Jul 2025 16:58:38 +0900
Subject: [PATCH 2/8] feat: user controller

---
 .../com/loopers/interfaces/api/user/UserV1Dto.java  |  9 ++++++++-
 .../loopers/interfaces/api/UserV1ApiE2ETest.java    | 13 +++++++++++--
 2 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
index 7ffa42a83..87a1ea2ee 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
@@ -1,7 +1,14 @@
 package com.loopers.interfaces.api.user;
 
 public class UserV1Dto {
-    public record UserRequest(){
+    public record UserRequest(
+            String loginId,
+            String password,
+            String name,
+            String email,
+            String gender,
+            String phoneNumber
+    ){
     }
     public record UserResponse(){
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
index c55a6d91b..b0a2108b7 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
@@ -33,12 +33,21 @@ class post{
         @DisplayName("회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.")
         @Test
         void returnsCreatedUserInfo_whenRegistrationIsSuccessful() {
+            UserV1Dto.UserRequest request  = new UserV1Dto.UserRequest(
+                    "testUser",
+                    "password123",
+                    "Test User",
+                    "test@naver.com",
+                    "m",
+                    "010-1234-5678"
+            );
              ParameterizedTypeReference<ApiResponse<UserV1Dto.UserResponse>> responseType = new ParameterizedTypeReference<>() {};
             ResponseEntity<ApiResponse<UserV1Dto.UserResponse>> response =
-                    testRestTemplate.exchange("/api/v1/users", HttpMethod.POST, new HttpEntity<>(null),responseType);
+                    testRestTemplate.exchange("/api/v1/users", HttpMethod.POST, new HttpEntity<>(request),responseType);
             System.out.println(response.getBody());
             assertAll(
-                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK)
+                    () -> assertNull(response)
+//                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK)
             );
         }
     }

From 5b4d7796e9e999f3e08b63884a5e72099cf686e3 Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Thu, 17 Jul 2025 21:30:47 +0900
Subject: [PATCH 3/8] feat: create user signup

---
 .../loopers/application/user/UserCommand.java |  44 ++++++++
 .../loopers/application/user/UserFacade.java  |  35 ++++++
 .../domain/example/ExampleRepository.java     |   1 +
 .../com/loopers/domain/user/UserModel.java    |  53 +++++++++
 .../loopers/domain/user/UserRepository.java   |  22 ++++
 .../com/loopers/domain/user/UserService.java  |  26 +++++
 .../interfaces/api/ApiControllerAdvice.java   |  11 ++
 .../interfaces/api/user/UserV1ApiSpec.java    |  13 ++-
 .../interfaces/api/user/UserV1Controller.java |  36 +++++-
 .../interfaces/api/user/UserV1Dto.java        |  42 +++++--
 .../com/loopers/domain/user/UseModelTest.java | 105 ++++++++++++++++++
 .../loopers/domain/user/UserServiceTest.java  |  57 ++++++++++
 .../interfaces/api/UserV1ApiE2ETest.java      |  96 +++++++++++++---
 13 files changed, 514 insertions(+), 27 deletions(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UseModelTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java
new file mode 100644
index 000000000..06082b4e5
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java
@@ -0,0 +1,44 @@
+package com.loopers.application.user;
+
+import com.loopers.domain.user.UserModel;
+import com.loopers.interfaces.api.user.UserV1Dto;
+
+public class UserCommand {
+    public record SignUp(
+            String loginId,
+            String email,
+            String birth,
+            Gender gender){
+        public static SignUp from(UserV1Dto.SignUpRequest request) {
+            return new SignUp(
+                    request.loginId(),
+                    request.email(),
+                    request.birth(),
+                    Gender.valueOf(request.gender().name())
+            );
+        }
+    }
+    public record UserInfo(
+            String loginId,
+            String email,
+            String birth,
+            Gender gender){
+        public static UserInfo from(UserModel user) {
+            return new UserInfo(
+                    user.getLoginId(),
+                    user.getEmail(),
+                    user.getBirth(),
+                    Gender.valueOf(user.getGrender())
+            );
+        }
+    }
+
+
+    public enum Gender {
+        M,
+        F;
+        public static Gender from(UserV1Dto.GenderResponse gender) {
+            return Gender.valueOf(gender.name());
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
new file mode 100644
index 000000000..c4835da86
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
@@ -0,0 +1,35 @@
+package com.loopers.application.user;
+
+import com.loopers.domain.user.UserModel;
+import com.loopers.domain.user.UserService;
+import com.loopers.interfaces.api.user.UserV1Dto;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.springframework.stereotype.Component;
+
+@Component
+public class UserFacade {
+    private final UserService userService;
+
+    public UserFacade(UserService userService) {
+        this.userService = userService;
+    }
+
+    public UserCommand.UserInfo signUpUser(UserV1Dto.SignUpRequest request) {
+
+        UserCommand.SignUp command = UserCommand.SignUp.from(request);
+
+        if (userService.isLoginIdExists(command.loginId())) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "이미 존재하는 ID입니다.");
+        }
+
+        UserModel user = new UserModel(
+                command.loginId(),
+                command.email(),
+                command.birth(),
+                command.gender().toString()
+        );
+        UserModel saved = userService.save(user);
+        return UserCommand.UserInfo.from(saved);
+    }
+}
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
index 3625e5662..83b04f41c 100644
--- 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
@@ -3,5 +3,6 @@
 import java.util.Optional;
 
 public interface ExampleRepository {
+
     Optional<ExampleModel> find(Long id);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java
new file mode 100644
index 000000000..3327a06f4
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java
@@ -0,0 +1,53 @@
+package com.loopers.domain.user;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+@Getter@Setter
+public class UserModel extends BaseEntity {
+
+    private String loginId;
+    private String email;
+    private String birth;
+    private String grender;
+
+
+    private static final String REGEX = "^[a-zA-Z0-9]{1,10}$";
+    private static final String EREGEX = "^\\S+@\\S+\\.\\S+$";
+
+
+
+    public UserModel(String loginId, String email, String birth, String grender) {
+        if(!loginId.matches(REGEX)) {
+            throw new CoreException(ErrorType.BAD_REQUEST,"loginId는 영문 및 숫자 10자 이내로 입력해야 합니다.");
+        }
+        if(!email.matches(EREGEX)) {
+            throw new CoreException(ErrorType.BAD_REQUEST,"email은 올바른 형식으로 입력해야 합니다.");
+        }
+        if(!validBirth(birth)) {
+            throw new CoreException(ErrorType.BAD_REQUEST,"birth는 yyyy-MM-dd 형식으로 입력해야 합니다.");
+        }
+        this.loginId = loginId;
+        this.email = email;
+        this.birth = birth;
+        this.grender = grender;
+    }
+    private boolean validBirth(String birth) {
+        try {
+            LocalDate localDate = LocalDate.parse(birth, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+            if(localDate.isAfter(LocalDate.now())){
+                throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 현재 날짜 이전이어야 합니다.");
+            }
+            return true;
+        } catch (DateTimeParseException e) {
+            return false;
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
new file mode 100644
index 000000000..38a4fab4a
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
@@ -0,0 +1,22 @@
+package com.loopers.domain.user;
+
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class UserRepository {
+
+    private static final Map<String, UserModel> db = new ConcurrentHashMap<>();
+
+    public UserModel save(UserModel userModel){
+        db.put(userModel.getLoginId(), userModel);
+        return db.get(userModel.getLoginId());
+    }
+    public boolean existsByLoginId(String loginId) {
+        return db.containsKey(loginId);
+    }
+
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
new file mode 100644
index 000000000..c3aacef29
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
@@ -0,0 +1,26 @@
+package com.loopers.domain.user;
+
+import com.loopers.interfaces.api.user.UserV1Dto;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.springframework.stereotype.Component;
+
+@Component
+public class UserService {
+    private final UserRepository userRepository;
+
+    public UserService(UserRepository userRepository) {
+        this.userRepository = userRepository;
+    }
+
+    public UserModel save(UserModel user) {
+        try {
+            return userRepository.save(user);
+        }catch (Exception e){
+            throw new CoreException(ErrorType.INTERNAL_ERROR);
+        }
+    }
+    public boolean isLoginIdExists(String loginId) {
+        return userRepository.existsByLoginId(loginId);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
index 20b2809c8..d7f71accd 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
@@ -8,6 +8,7 @@
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -112,6 +113,15 @@ public ResponseEntity<ApiResponse<?>> handle(Throwable e) {
         log.error("Exception : {}", e.getMessage(), e);
         return failureResponse(ErrorType.INTERNAL_ERROR, null);
     }
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public ResponseEntity<ApiResponse<?>> handleValidation(MethodArgumentNotValidException e) {
+        String message = e.getBindingResult()
+                .getFieldErrors()
+                .stream()
+                .map(fe -> String.format("%s: %s", fe.getField(), fe.getDefaultMessage()))
+                .collect(Collectors.joining(", "));
+        return failureResponse(ErrorType.BAD_REQUEST, message);
+    }
 
     private String extractMissingParameter(String message) {
         Pattern pattern = Pattern.compile("'(.+?)'");
@@ -123,4 +133,5 @@ private ResponseEntity<ApiResponse<?>> failureResponse(ErrorType errorType, Stri
         return ResponseEntity.status(errorType.getStatus())
             .body(ApiResponse.fail(errorType.getCode(), errorMessage != null ? errorMessage : errorType.getMessage()));
     }
+
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
index fb5ceda69..5d7eb252b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
@@ -4,7 +4,7 @@
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
 
 @Tag(name = "User V1 API", description = "Loopers 사용자 API 입니다.")
 public interface UserV1ApiSpec {
@@ -12,8 +12,15 @@ public interface UserV1ApiSpec {
         summary = "회원 가입",
         description = "사용자를 등록합니다. 성공 시 생성된 사용자 정보를 반환합니다."
     )
-    ApiResponse<UserV1Dto.UserResponse> registerUser(
+    ApiResponse<UserV1Dto.SignUpResponse> signUp(
             @Schema(name = "예시 ID", description = "조회할 예시의 ID")
-            UserV1Dto.UserRequest userV1Dto
+            UserV1Dto.SignUpRequest userV1Dto
+    );
+    @Operation(
+        summary = "내 정보 조회",
+        description = "사용자의 정보를 조회합니다. 성공 시 사용자 정보를 반환합니다."
+    )
+    ApiResponse<UserV1Dto.GetMeResponse> getMe(
+            @RequestHeader("X-USER-ID") String userId
     );
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
index 7455f9032..660a823e7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
@@ -1,17 +1,47 @@
 package com.loopers.interfaces.api.user;
 
 
+import com.loopers.application.user.UserCommand;
+import com.loopers.application.user.UserFacade;
 import com.loopers.interfaces.api.ApiResponse;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("/api/v1/users")
 public class UserV1Controller implements UserV1ApiSpec{
+    private final UserFacade userFacade;
+
+    public UserV1Controller(UserFacade userFacade) {
+        this.userFacade = userFacade;
+    }
 
     @Override
     @PostMapping
-    public ApiResponse<UserV1Dto.UserResponse> registerUser(
-            @RequestBody UserV1Dto.UserRequest userV1Dto) {
-        return null;
+    public ApiResponse<UserV1Dto.SignUpResponse> signUp(
+           @Valid @RequestBody UserV1Dto.SignUpRequest userV1Dto) {
+        UserV1Dto.SignUpResponse response = UserV1Dto.SignUpResponse.from(userFacade.signUpUser(userV1Dto));
+        return ApiResponse.success(response);
     }
+    @Override
+    @GetMapping(value = "/me")
+    public ApiResponse<UserV1Dto.GetMeResponse> getMe(
+            @NotNull @NotBlank @RequestHeader("X-USER-ID") String userId) {
+        String loginId = "TestUserId";
+        if(!loginId.equals(userId)){
+            throw new CoreException(ErrorType.BAD_REQUEST,"아이디가 일치하지않아요");// 예시로 고정된 값 사용
+        }
+        return ApiResponse.success(
+                new UserV1Dto.GetMeResponse(
+                        userId
+                )
+        );
+
+    }
+
 }
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
index 87a1ea2ee..3dccf03d3 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
@@ -1,16 +1,42 @@
 package com.loopers.interfaces.api.user;
 
+import com.loopers.application.user.UserCommand;
+import com.loopers.domain.user.UserModel;
+import jakarta.validation.constraints.NotNull;
+
 public class UserV1Dto {
-    public record UserRequest(
+    public record SignUpRequest(
+            @NotNull
             String loginId,
-            String password,
-            String name,
+            @NotNull
             String email,
-            String gender,
-            String phoneNumber
-    ){
-    }
-    public record UserResponse(){
+            @NotNull
+            String birth,
+            @NotNull
+            GenderResponse gender
+    ){}
+    public record SignUpResponse(
 
+            String loginId,
+            String email,
+            String birth,
+            GenderResponse gender
+    ) {
+        public static SignUpResponse from(UserCommand.UserInfo userInfo) {
+            return new SignUpResponse(
+                    userInfo.loginId(),
+                    userInfo.email(),
+                    userInfo.birth(),
+                    GenderResponse.valueOf(userInfo.gender().name())
+            );
+        }
+    }
+    public record GetMeResponse(
+            String loginId
+    ) {}
+    public enum GenderResponse {
+        M,
+        F
+        ;
     }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UseModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UseModelTest.java
new file mode 100644
index 000000000..ac79f81f6
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UseModelTest.java
@@ -0,0 +1,105 @@
+package com.loopers.domain.user;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class UseModelTest {
+
+    @Nested
+    @DisplayName("")
+    class create {
+        @DisplayName("ID, 이메일, 생년월일, 성별이 모두 주어지면, User 객체를 정상적으로 생성한다.")
+        @Test
+        void createsUserModel_whenAllFieldsAreProvided() {
+            // arrange
+            String loginId = "test123";
+            String email = "test@test.net";
+            String birth = "2000-08-04";
+            String grender = "M"; // 성별 M
+            // act
+            UserModel userModel = new UserModel(loginId, email, birth, grender);
+
+            // assert
+            assertAll(
+                    ()-> assertThat(userModel).isNotNull(),
+                    () -> assertThat(userModel.getLoginId()).isEqualTo(loginId),
+                    () -> assertThat(userModel.getEmail()).isEqualTo(email),
+                    () -> assertThat(userModel.getBirth()).isEqualTo(birth),
+                    () -> assertThat(userModel.getGrender()).isEqualTo(grender)
+            );
+        }
+
+        @DisplayName("ID 가 영문 및 숫자 10자 이내 형식에 맞지 않으면, User 객체 생성에 실패한다.")
+        @Test
+        void throwsException_whenIdIsInvalid() {
+            // arrange
+            String loginId = "invalid_id_1234567890";
+            String email = "test@test.net";
+            String birth = "2000-08-04";
+            String grender = "M"; // 성별 M
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () -> {
+                new UserModel(loginId, email, birth, grender);
+            });
+
+            // assert
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+        }
+        @DisplayName("이메일이 xx@yy.zz 형식에 맞지 않으면, User 객체 생성에 실패한다.")
+        @Test
+        void throwsException_whenEmailIsInvalid() {
+            String loginId = "invali23";
+            String email = "test@testnet";
+            String birth = "2000-08-04";
+            String grender = "M"; // 성별 M
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () -> {
+                new UserModel(loginId, email, birth, grender);
+            });
+
+            // assert
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+        }
+        @DisplayName("생년월일이 yyyy-MM-dd 형식에 맞지 않으면, User 객체 생성에 실패한다.")
+        @Test
+        void throwsException_whenBirthIsInvalid() {
+            String loginId = "invali23";
+            String email = "test@test.net";
+            String birth = "20000804";
+            String grender = "M"; // 성별 M
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () -> {
+                new UserModel(loginId, email, birth, grender);
+            });
+
+            // assert
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+        }
+        @DisplayName("생년월일이 yyyy-MM-dd 형식은 맞지만 없는 날짜거나 미래의 날짜인경우에, User 객체 생성에 실패한다.")
+        @Test
+        void throwsException_whenBirthIsInvalid_2() {
+            String loginId = "invali23";
+            String email = "test@test.net";
+            String birth = "29999-08-04"; // 미래의 날짜
+            String grender = "M"; // 성별 M
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () -> {
+                new UserModel(loginId, email, birth, grender);
+            });
+
+            // assert
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
new file mode 100644
index 000000000..ae602f4a4
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
@@ -0,0 +1,57 @@
+package com.loopers.domain.user;
+
+import com.loopers.application.user.UserFacade;
+import com.loopers.interfaces.api.user.UserV1Dto;
+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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
+
+@SpringBootTest
+public class UserServiceTest {
+    @Autowired
+    private UserFacade userFacade;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @DisplayName(" 회원 가입시,")
+    @Nested
+    class get {
+        @DisplayName("이미 가입된 Id가 있는 경우 실패 한다.")
+        @Test
+        void throwsException_whenIdAlreadyExists() {
+            // arrange
+            String loginId = "test123";
+            String email = "test@test.net";
+            String birth = "2000-08-04";
+            String grender = "M"; // 성별 M
+            UserModel userModel = new UserModel(loginId, email, birth, grender);
+
+            UserV1Dto.SignUpRequest request = new UserV1Dto.SignUpRequest(
+                userModel.getLoginId(),
+                userModel.getEmail(),
+                userModel.getBirth(),
+                UserV1Dto.GenderResponse.valueOf(userModel.getGrender())
+            );
+
+            userRepository.save(userModel);
+
+            // act
+            CoreException exception = assertThrows(
+                CoreException.class, () -> userFacade.signUpUser(request));
+
+            //assert
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+        }
+    }
+
+
+
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
index b0a2108b7..e283d9362 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
@@ -1,7 +1,6 @@
 package com.loopers.interfaces.api;
 
 import com.loopers.interfaces.api.user.UserV1Dto;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -13,14 +12,13 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.ResponseBody;
 
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 public class UserV1ApiE2ETest {
-
     private final TestRestTemplate testRestTemplate;
     @Autowired
     public UserV1ApiE2ETest(TestRestTemplate testRestTemplate) {
@@ -30,26 +28,98 @@ public UserV1ApiE2ETest(TestRestTemplate testRestTemplate) {
     @Nested
     @DisplayName("POST /api/v1/users")
     class post{
+        static String ENDPOINT = "/api/v1/users";
         @DisplayName("회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.")
         @Test
         void returnsCreatedUserInfo_whenRegistrationIsSuccessful() {
-            UserV1Dto.UserRequest request  = new UserV1Dto.UserRequest(
+
+            UserV1Dto.SignUpRequest request  = new UserV1Dto.SignUpRequest(
                     "testUser",
-                    "password123",
-                    "Test User",
                     "test@naver.com",
-                    "m",
-                    "010-1234-5678"
+                    "2000-08-04",
+                    UserV1Dto.GenderResponse.M // 성별 M
             );
-             ParameterizedTypeReference<ApiResponse<UserV1Dto.UserResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<UserV1Dto.UserResponse>> response =
-                    testRestTemplate.exchange("/api/v1/users", HttpMethod.POST, new HttpEntity<>(request),responseType);
+             ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.POST, new HttpEntity<>(request),responseType);
             System.out.println(response.getBody());
             assertAll(
-                    () -> assertNull(response)
-//                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK)
+                    () -> assertThat(response.getBody()).isNotNull(),
+                    () -> assertThat(response.getBody().meta().result()).isEqualTo(ApiResponse.Metadata.Result.SUCCESS),
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK)
+
+            );
+        }
+        @DisplayName("회원 가입 시에 성별이 없을 경우, `400 Bad Request` 응답을 반환한다.")
+        @Test
+        void returnsBadRequest_whenRegistrationIsNotSuccessful() {
+            //arrange
+            UserV1Dto.SignUpRequest request  = new UserV1Dto.SignUpRequest(
+                    "testUser",
+                    "test@test.net",
+                    "2000-08-04",
+                    null // 성별이 없을 경우
             );
+            //act
+            ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.POST, new HttpEntity<>(request),responseType);
+            //assert
+            System.out.println(response.getStatusCode());
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+                    );
+
         }
     }
+    @Nested
+    @DisplayName("GET /api/v1/users/me")
+    class get {
+        static String ENDPOINT = "/api/v1/users/me";
+
+        @DisplayName("사용자 정보를 조회할 때, 올바른 사용자 ID를 제공하면 해당 사용자 정보를 반환한다.")
+        @Test
+        void returnsUserInfo_whenValidUserIdIsProvided() {
+            // arrange
+            String userId = "TestUserId";
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId );
+
+            HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+            // act
+            ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, responseType);
+
+            // assert
+            assertAll(
+                    () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
+                    () -> assertThat(response.getBody().data().loginId()).isEqualTo(userId)
+            );
+        }
+
+        @DisplayName("사용자 정보를 조회할 때, 사용자 ID가 제공되지 않으면 `400 Bad Request` 응답을 반환한다.")
+        @Test
+        void returnsBadRequest_whenUserIdIsNotProvided() {
+            // arrange
+            String userId = "";
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId );
+
+            HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+            // act
+            ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, responseType);
+            System.out.println(response.getStatusCode());
+            // assert
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+            );
+        }
+    }
+
 
 }

From 817a746e5b7065bc0cc456e438cc79665691ddbb Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Thu, 17 Jul 2025 22:24:24 +0900
Subject: [PATCH 4/8] feat: create get me

---
 .../loopers/application/user/UserCommand.java |  1 +
 .../loopers/application/user/UserFacade.java  | 10 +++++
 .../loopers/domain/user/UserRepository.java   |  8 ++++
 .../com/loopers/domain/user/UserService.java  |  7 ++++
 .../interfaces/api/user/UserV1ApiSpec.java    |  2 +-
 .../interfaces/api/user/UserV1Controller.java | 19 ++++++----
 .../interfaces/api/user/UserV1Dto.java        | 16 +++++++-
 .../loopers/domain/user/UserServiceTest.java  | 38 +++++++++++++++++++
 .../interfaces/api/UserV1ApiE2ETest.java      |  4 +-
 9 files changed, 93 insertions(+), 12 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java
index 06082b4e5..99398b7ac 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserCommand.java
@@ -3,6 +3,7 @@
 import com.loopers.domain.user.UserModel;
 import com.loopers.interfaces.api.user.UserV1Dto;
 
+
 public class UserCommand {
     public record SignUp(
             String loginId,
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
index c4835da86..a2e5b92cd 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
@@ -7,6 +7,8 @@
 import com.loopers.support.error.ErrorType;
 import org.springframework.stereotype.Component;
 
+import java.util.Optional;
+
 @Component
 public class UserFacade {
     private final UserService userService;
@@ -32,4 +34,12 @@ public UserCommand.UserInfo signUpUser(UserV1Dto.SignUpRequest request) {
         UserModel saved = userService.save(user);
         return UserCommand.UserInfo.from(saved);
     }
+    public UserCommand.UserInfo getUserById(String userId) {
+        if(userId == null || userId.isBlank()) {
+            return null;
+        }
+        return userService.findByLoginId(userId)
+                .map(UserCommand.UserInfo::from)
+                .orElse(null);
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
index 38a4fab4a..39d7c7c9c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
@@ -3,12 +3,17 @@
 import org.springframework.stereotype.Component;
 
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 
 @Component
 public class UserRepository {
 
     private static final Map<String, UserModel> db = new ConcurrentHashMap<>();
+    public UserRepository() {
+        UserModel testUser = new UserModel("admin", "admin@example.com", "1990-01-01", "M");
+        db.put(testUser.getLoginId(), testUser);
+    }
 
     public UserModel save(UserModel userModel){
         db.put(userModel.getLoginId(), userModel);
@@ -18,5 +23,8 @@ public boolean existsByLoginId(String loginId) {
         return db.containsKey(loginId);
     }
 
+    public Optional<UserModel> findByLoginId(String loginId) {
+        return Optional.ofNullable(db.get(loginId));
+    }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
index c3aacef29..139707436 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
@@ -3,8 +3,11 @@
 import com.loopers.interfaces.api.user.UserV1Dto;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
+import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.stereotype.Component;
 
+import java.util.Optional;
+
 @Component
 public class UserService {
     private final UserRepository userRepository;
@@ -23,4 +26,8 @@ public UserModel save(UserModel user) {
     public boolean isLoginIdExists(String loginId) {
         return userRepository.existsByLoginId(loginId);
     }
+
+    public Optional<UserModel> findByLoginId(String loginId) {
+        return userRepository.findByLoginId(loginId);
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
index 5d7eb252b..874a1ebca 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java
@@ -21,6 +21,6 @@ ApiResponse<UserV1Dto.SignUpResponse> signUp(
         description = "사용자의 정보를 조회합니다. 성공 시 사용자 정보를 반환합니다."
     )
     ApiResponse<UserV1Dto.GetMeResponse> getMe(
-            @RequestHeader("X-USER-ID") String userId
+            String userId
     );
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
index 660a823e7..1ab475523 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java
@@ -11,6 +11,8 @@
 import jakarta.validation.constraints.NotNull;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Optional;
+
 @RestController
 @RequestMapping("/api/v1/users")
 public class UserV1Controller implements UserV1ApiSpec{
@@ -30,15 +32,16 @@ public ApiResponse<UserV1Dto.SignUpResponse> signUp(
     @Override
     @GetMapping(value = "/me")
     public ApiResponse<UserV1Dto.GetMeResponse> getMe(
-            @NotNull @NotBlank @RequestHeader("X-USER-ID") String userId) {
-        String loginId = "TestUserId";
-        if(!loginId.equals(userId)){
-            throw new CoreException(ErrorType.BAD_REQUEST,"아이디가 일치하지않아요");// 예시로 고정된 값 사용
-        }
+            @RequestHeader("X-USER-ID") String userId) {
+       UserV1Dto.GetMeResponse response =
+               UserV1Dto.GetMeResponse.from(
+                       Optional.ofNullable(
+                               userFacade.getUserById(userId)
+                               ).orElseThrow(
+                                 () -> new CoreException(ErrorType.BAD_REQUEST, "사용자를 찾을 수 없습니다.")
+                       ));
         return ApiResponse.success(
-                new UserV1Dto.GetMeResponse(
-                        userId
-                )
+                response
         );
 
     }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
index 3dccf03d3..a6e0337fb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java
@@ -32,8 +32,20 @@ public static SignUpResponse from(UserCommand.UserInfo userInfo) {
         }
     }
     public record GetMeResponse(
-            String loginId
-    ) {}
+            String loginId,
+            String email,
+            String birth,
+            GenderResponse gender
+    ) {
+        public static GetMeResponse from(UserCommand.UserInfo userInfo) {
+            return new GetMeResponse(
+                    userInfo.loginId(),
+                    userInfo.email(),
+                    userInfo.birth(),
+                    GenderResponse.valueOf(userInfo.gender().name())
+            );
+        }
+    }
     public enum GenderResponse {
         M,
         F
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
index ae602f4a4..895b88f85 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
@@ -1,5 +1,6 @@
 package com.loopers.domain.user;
 
+import com.loopers.application.user.UserCommand;
 import com.loopers.application.user.UserFacade;
 import com.loopers.interfaces.api.user.UserV1Dto;
 import com.loopers.support.error.CoreException;
@@ -50,6 +51,43 @@ void throwsException_whenIdAlreadyExists() {
             //assert
             assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
         }
+        
+        @DisplayName("해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.")
+        @Test
+        void returnsUserInfo_whenUserExists() {
+            // arrange
+            String loginId = "test123";
+            String email = "test@test.net";
+            String birth = "2000-08-04";
+            String grender = "M"; // 성별 M
+            UserModel userModel = new UserModel(loginId, email, birth, grender);
+
+            userRepository.save(userModel);
+            
+            // act
+            UserCommand.UserInfo userInfo = userFacade.getUserById(userModel.getLoginId());
+            
+            // assert
+            assertThat(userInfo).isNotNull();
+            assertThat(userInfo.loginId()).isEqualTo(loginId);
+            assertThat(userInfo.email()).isEqualTo(email);
+            assertThat(userInfo.birth()).isEqualTo(birth);
+            assertThat(userInfo.gender()).isEqualTo(UserCommand.Gender.M);
+        }
+
+        @DisplayName("해당 ID 의 회원이 존재하지 않을 경우,  null 이 반환된다.")
+        @Test
+        void throwsException_whenUserDoesNotExist() {
+            // arrange
+            String nonExistentLoginId = "nonExistent123";
+
+            // act
+            UserCommand.UserInfo info = userFacade.getUserById(nonExistentLoginId);
+
+            // assert
+            assertThat(info).isNull();
+        }
+
     }
 
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
index e283d9362..50ae43dce 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
@@ -1,5 +1,6 @@
 package com.loopers.interfaces.api;
 
+import com.loopers.domain.user.UserModel;
 import com.loopers.interfaces.api.user.UserV1Dto;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -81,7 +82,8 @@ class get {
         @Test
         void returnsUserInfo_whenValidUserIdIsProvided() {
             // arrange
-            String userId = "TestUserId";
+            String userId = "admin";
+
             HttpHeaders headers = new HttpHeaders();
             headers.set("X-USER-ID", userId );
 

From 3589d3b5b52835c3205533158f5a76db8ad7418c Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Thu, 17 Jul 2025 22:43:05 +0900
Subject: [PATCH 5/8] =?UTF-8?q?test:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?=
 =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../user/UserFacadeIntegrationTest.java       | 119 ++++++++++++++++++
 .../loopers/domain/user/UserServiceTest.java  |  34 -----
 2 files changed, 119 insertions(+), 34 deletions(-)
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeIntegrationTest.java

diff --git a/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeIntegrationTest.java
new file mode 100644
index 000000000..8cc25a94e
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeIntegrationTest.java
@@ -0,0 +1,119 @@
+package com.loopers.application.user;
+
+import com.loopers.domain.user.UserModel;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.domain.user.UserService;
+import com.loopers.interfaces.api.user.UserV1Dto;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.assertj.core.api.AssertionsForClassTypes;
+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.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+
+@SpringBootTest
+public class UserFacadeIntegrationTest {
+
+    @Autowired
+    private UserFacade userFacade;
+
+    @MockitoSpyBean
+    private UserService userService;
+
+    @MockitoSpyBean
+    private UserRepository userRepository;
+
+    @Nested
+    @DisplayName("[통합 테스트]회원 가입 시")
+    class SignUp {
+        
+        @DisplayName("회원 가입시 User 저장이 수행된다.")
+        @Test
+        void saveUser_whenSignUpIsSuccessful() {
+            // arrange
+            UserV1Dto.SignUpRequest request = new UserV1Dto.SignUpRequest(
+                    "newUser123",
+                    "new@example.com",
+                    "1995-05-15",
+                    UserV1Dto.GenderResponse.F
+            );
+
+            // act
+            UserCommand.UserInfo result = userFacade.signUpUser(request);
+
+            // assert
+            assertThat(result).isNotNull();
+            assertThat(result.loginId()).isEqualTo("newUser123");
+            assertThat(result.email()).isEqualTo("new@example.com");
+            assertThat(result.birth()).isEqualTo("1995-05-15");
+            assertThat(result.gender()).isEqualTo(UserCommand.Gender.F);
+            
+            verify(userService).save(any());
+            verify(userRepository).save(any());
+        }
+
+        @DisplayName("이미 가입된 ID 로 회원가입 시도 시, 실패한다.")
+        @Test
+        void throwsException_whenLoginIdAlreadyExists() {
+            // arrange
+            UserV1Dto.SignUpRequest request = new UserV1Dto.SignUpRequest(
+                    "admin", // 이미 존재하는 ID
+                    "admin2@example.com",
+                    "1990-01-01",
+                    UserV1Dto.GenderResponse.M
+            );
+
+            // act
+            CoreException exception = assertThrows(
+                    CoreException.class,
+                    () -> userFacade.signUpUser(request)
+            );
+            // assert
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+            assertThat(exception.getMessage()).contains("이미 존재하는 ID입니다");
+        }
+    }
+    @DisplayName("해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.")
+    @Test
+    void returnsUserInfo_whenUserExists() {
+        // arrange
+        String loginId = "test123";
+        String email = "test@test.net";
+        String birth = "2000-08-04";
+        String grender = "M"; // 성별 M
+        UserModel userModel = new UserModel(loginId, email, birth, grender);
+
+        userRepository.save(userModel);
+
+        // act
+        UserCommand.UserInfo userInfo = userFacade.getUserById(userModel.getLoginId());
+
+        // assert
+        AssertionsForClassTypes.assertThat(userInfo).isNotNull();
+        AssertionsForClassTypes.assertThat(userInfo.loginId()).isEqualTo(loginId);
+        AssertionsForClassTypes.assertThat(userInfo.email()).isEqualTo(email);
+        AssertionsForClassTypes.assertThat(userInfo.birth()).isEqualTo(birth);
+        AssertionsForClassTypes.assertThat(userInfo.gender()).isEqualTo(UserCommand.Gender.M);
+    }
+
+    @DisplayName("해당 ID 의 회원이 존재하지 않을 경우,  null 이 반환된다.")
+    @Test
+    void throwsException_whenUserDoesNotExist() {
+        // arrange
+        String nonExistentLoginId = "nonExistent123";
+
+        // act
+        UserCommand.UserInfo info = userFacade.getUserById(nonExistentLoginId);
+
+        // assert
+        AssertionsForClassTypes.assertThat(info).isNull();
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
index 895b88f85..998e47f26 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java
@@ -52,41 +52,7 @@ void throwsException_whenIdAlreadyExists() {
             assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
         }
         
-        @DisplayName("해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.")
-        @Test
-        void returnsUserInfo_whenUserExists() {
-            // arrange
-            String loginId = "test123";
-            String email = "test@test.net";
-            String birth = "2000-08-04";
-            String grender = "M"; // 성별 M
-            UserModel userModel = new UserModel(loginId, email, birth, grender);
 
-            userRepository.save(userModel);
-            
-            // act
-            UserCommand.UserInfo userInfo = userFacade.getUserById(userModel.getLoginId());
-            
-            // assert
-            assertThat(userInfo).isNotNull();
-            assertThat(userInfo.loginId()).isEqualTo(loginId);
-            assertThat(userInfo.email()).isEqualTo(email);
-            assertThat(userInfo.birth()).isEqualTo(birth);
-            assertThat(userInfo.gender()).isEqualTo(UserCommand.Gender.M);
-        }
-
-        @DisplayName("해당 ID 의 회원이 존재하지 않을 경우,  null 이 반환된다.")
-        @Test
-        void throwsException_whenUserDoesNotExist() {
-            // arrange
-            String nonExistentLoginId = "nonExistent123";
-
-            // act
-            UserCommand.UserInfo info = userFacade.getUserById(nonExistentLoginId);
-
-            // assert
-            assertThat(info).isNull();
-        }
 
     }
 

From 438e442a475f637e194bdd695d6531ade126bbfe Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Fri, 18 Jul 2025 01:40:29 +0900
Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?=
 =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20add?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/points/PointsCommand.java     |  22 +++
 .../application/points/PointsFacade.java      |  34 +++++
 .../loopers/domain/points/PointsModel.java    |  20 +++
 .../domain/points/PointsRepository.java       |  27 ++++
 .../loopers/domain/points/PointsService.java  |  36 +++++
 .../interfaces/api/ApiControllerAdvice.java   |   8 +
 .../api/points/PointsV1ApiSpec.java           |  26 ++++
 .../api/points/PointsV1Controller.java        |  52 +++++++
 .../interfaces/api/points/PointsV1Dto.java    |  26 ++++
 .../user/PointsFacadeIntegrationTest.java     | 119 +++++++++++++++
 .../interfaces/api/PointsV1ApiE2RTest.java    | 141 ++++++++++++++++++
 .../interfaces/api/UserV1ApiE2ETest.java      |  19 ++-
 12 files changed, 529 insertions(+), 1 deletion(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/points/PointsCommand.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/points/PointsModel.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/points/PointsCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/points/PointsCommand.java
new file mode 100644
index 000000000..f76940aec
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/points/PointsCommand.java
@@ -0,0 +1,22 @@
+package com.loopers.application.points;
+
+import com.loopers.domain.points.PointsModel;
+import com.loopers.domain.user.UserModel;
+import com.loopers.interfaces.api.points.PointsV1Dto;
+import com.loopers.interfaces.api.user.UserV1Dto;
+
+import java.math.BigDecimal;
+
+
+public class PointsCommand {
+    public record PointInfo(
+            String loginId,
+            BigDecimal amount){
+        public static PointInfo from(PointsModel pointsModel) {
+            return new PointInfo(
+                    pointsModel.getUserId(),
+                    pointsModel.getAmount()
+            );
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java
new file mode 100644
index 000000000..311a4c865
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java
@@ -0,0 +1,34 @@
+package com.loopers.application.points;
+
+import com.loopers.application.user.UserCommand;
+import com.loopers.domain.points.PointsModel;
+import com.loopers.domain.points.PointsService;
+import com.loopers.domain.user.UserModel;
+import com.loopers.domain.user.UserService;
+import com.loopers.interfaces.api.user.UserV1Dto;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PointsFacade {
+    private final PointsService pointsService;
+    private final UserService userService;
+
+    public PointsFacade(PointsService pointsService, UserService userService) {
+        this.pointsService = pointsService;
+        this.userService = userService;
+    }
+
+    public PointsCommand.PointInfo getPointInfo(String loginId) {
+        if(!userService.isLoginIdExists(loginId)){
+            throw new CoreException(ErrorType.BAD_REQUEST, "사용자를 찾을 수 없습니다.");
+        }
+        PointsModel pointsModel = pointsService.getByLoginId(loginId);
+        if(pointsModel == null) {
+            throw new CoreException(ErrorType.NOT_FOUND);
+        }
+        return PointsCommand.PointInfo.from(pointsModel);
+
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsModel.java
new file mode 100644
index 000000000..309b8a4d4
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsModel.java
@@ -0,0 +1,20 @@
+package com.loopers.domain.points;
+
+import com.loopers.domain.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Getter@Setter
+public class PointsModel extends BaseEntity {
+
+    private String userId;
+    private BigDecimal amount;
+
+
+    public PointsModel(String loginId, BigDecimal zero) {
+        this.userId = loginId;
+        this.amount = zero;
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java
new file mode 100644
index 000000000..21cfcbeb7
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java
@@ -0,0 +1,27 @@
+package com.loopers.domain.points;
+
+import com.loopers.domain.user.UserModel;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class PointsRepository {
+
+    private static final Map<String, PointsModel> db = new ConcurrentHashMap<>();
+
+    public PointsModel save(PointsModel pointsModel) {
+        db.put(pointsModel.getUserId(), pointsModel);
+        return db.get(pointsModel.getUserId());
+    }
+    public boolean existsByLoginId(String loginId) {
+        return db.containsKey(loginId);
+    }
+
+    public Optional<PointsModel> findByLoginId(String loginId) {
+        return Optional.ofNullable(db.get(loginId));
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java
new file mode 100644
index 000000000..99b180928
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java
@@ -0,0 +1,36 @@
+package com.loopers.domain.points;
+
+import com.loopers.domain.user.UserModel;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+@Component
+public class PointsService {
+    private final PointsRepository pointsRepository;
+
+    public PointsService(PointsRepository pointsRepository) {
+        this.pointsRepository = pointsRepository;
+    }
+
+    public PointsModel save(PointsModel user) {
+        try {
+            return pointsRepository.save(user);
+        }catch (Exception e){
+            throw new CoreException(ErrorType.INTERNAL_ERROR);
+        }
+    }
+
+    public PointsModel getByLoginId(String loginId) {
+        if(!pointsRepository.existsByLoginId(loginId)){
+            pointsRepository.save(
+                    new PointsModel(loginId, BigDecimal.ZERO)
+            );
+        }
+        return pointsRepository.findByLoginId(loginId).orElse(null);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
index d7f71accd..b4928fc94 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
@@ -10,6 +10,7 @@
 import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.MissingRequestHeaderException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@@ -47,6 +48,13 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestPara
         return failureResponse(ErrorType.BAD_REQUEST, message);
     }
 
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingRequestHeaderException e) {
+        String headerName = e.getHeaderName();
+        String message = String.format("필수 요청 헤더 '%s'가 누락되었습니다.", headerName);
+        return failureResponse(ErrorType.BAD_REQUEST, message);
+    }
+
     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableException e) {
         String errorMessage;
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1ApiSpec.java
new file mode 100644
index 000000000..40187c165
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1ApiSpec.java
@@ -0,0 +1,26 @@
+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.tags.Tag;
+
+import java.math.BigDecimal;
+
+@Tag(name = "Points V1 API", description = "Loopers 포인트 API 입니다.")
+public interface PointsV1ApiSpec {
+    @Operation(
+        summary = "포인트 조회",
+        description = "사용자의 보유 포인트를 조회합니다. 성공 시 보유 포인트를 반환합니다."
+    )
+    ApiResponse<PointsV1Dto.PointsResponse> getPoints(
+            String userId
+    );
+    @Operation(
+        summary = "포인트 충전",
+        description = "사용자의 포인트를 충전합니다. 성공 시 충전된 포인트를 반환합니다."
+    )
+    ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
+            String userId,
+            PointsV1Dto.PointsRequest pointsV1Request
+    );
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java
new file mode 100644
index 000000000..8d4c3d551
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java
@@ -0,0 +1,52 @@
+package com.loopers.interfaces.api.points;
+
+import com.loopers.application.points.PointsFacade;
+import com.loopers.interfaces.api.ApiResponse;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+
+@RestController
+@RequestMapping("/api/v1/points")
+public class PointsV1Controller implements PointsV1ApiSpec {
+    private final PointsFacade pointsFacade;
+
+    public PointsV1Controller(PointsFacade pointsFacade) {
+        this.pointsFacade = pointsFacade;
+    }
+
+    @Override
+    @GetMapping
+    public ApiResponse<PointsV1Dto.PointsResponse> getPoints(
+            @RequestHeader("X-USER-ID") String userId) {
+        if(userId == null || userId.isBlank()) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID가 필요합니다.");
+        }
+        return ApiResponse.success(
+                PointsV1Dto.PointsResponse
+                        .from(pointsFacade.getPointInfo(userId))
+        );
+
+    }
+    @Override
+    @PostMapping("/charge")
+    public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
+            @RequestHeader("X-USER-ID") String userId,
+            @RequestBody PointsV1Dto.PointsRequest pointsV1Request) {
+        BigDecimal amount = pointsV1Request.amount();
+        if(userId == null || userId.isBlank() || !userId.equals("admin")) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID가 필요합니다.");
+        }
+
+        if(amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "충전 금액은 0보다 커야 합니다.");
+        }
+        return ApiResponse.success(
+                PointsV1Dto.PointsResponse.of(amount)
+        );
+    }
+
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Dto.java
new file mode 100644
index 000000000..a230a5d84
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Dto.java
@@ -0,0 +1,26 @@
+package com.loopers.interfaces.api.points;
+
+import com.loopers.application.points.PointsCommand;
+
+import java.math.BigDecimal;
+
+public class PointsV1Dto {
+    public record PointsRequest(
+        BigDecimal amount
+    ) {
+        public static PointsRequest of(BigDecimal amount) {
+            return new PointsRequest(amount);
+        }
+    }
+    public record PointsResponse(
+        BigDecimal amount
+    ) {
+        public static PointsResponse of(BigDecimal points) {
+            return new PointsResponse(points);
+        }
+
+        public static PointsResponse from(PointsCommand.PointInfo pointInfo) {
+            return new PointsResponse(pointInfo.amount());
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java
new file mode 100644
index 000000000..8c85230f5
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java
@@ -0,0 +1,119 @@
+package com.loopers.application.user;
+
+import com.loopers.domain.user.UserModel;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.domain.user.UserService;
+import com.loopers.interfaces.api.user.UserV1Dto;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.assertj.core.api.AssertionsForClassTypes;
+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.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+
+@SpringBootTest
+public class PointsFacadeIntegrationTest {
+
+    @Autowired
+    private UserFacade userFacade;
+
+    @MockitoSpyBean
+    private UserService userService;
+
+    @MockitoSpyBean
+    private UserRepository userRepository;
+
+    @Nested
+    @DisplayName("[통합 테스트]회원 가입 시")
+    class SignUp {
+
+        @DisplayName("회원 가입시 User 저장이 수행된다.")
+        @Test
+        void saveUser_whenSignUpIsSuccessful() {
+            // arrange
+            UserV1Dto.SignUpRequest request = new UserV1Dto.SignUpRequest(
+                    "newUser123",
+                    "new@example.com",
+                    "1995-05-15",
+                    UserV1Dto.GenderResponse.F
+            );
+
+            // act
+            UserCommand.UserInfo result = userFacade.signUpUser(request);
+
+            // assert
+            assertThat(result).isNotNull();
+            assertThat(result.loginId()).isEqualTo("newUser123");
+            assertThat(result.email()).isEqualTo("new@example.com");
+            assertThat(result.birth()).isEqualTo("1995-05-15");
+            assertThat(result.gender()).isEqualTo(UserCommand.Gender.F);
+
+            verify(userService).save(any());
+            verify(userRepository).save(any());
+        }
+
+        @DisplayName("이미 가입된 ID 로 회원가입 시도 시, 실패한다.")
+        @Test
+        void throwsException_whenLoginIdAlreadyExists() {
+            // arrange
+            UserV1Dto.SignUpRequest request = new UserV1Dto.SignUpRequest(
+                    "admin", // 이미 존재하는 ID
+                    "admin2@example.com",
+                    "1990-01-01",
+                    UserV1Dto.GenderResponse.M
+            );
+
+            // act
+            CoreException exception = assertThrows(
+                    CoreException.class,
+                    () -> userFacade.signUpUser(request)
+            );
+            // assert
+            assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+            assertThat(exception.getMessage()).contains("이미 존재하는 ID입니다");
+        }
+    }
+    @DisplayName("해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.")
+    @Test
+    void returnsUserInfo_whenUserExists() {
+        // arrange
+        String loginId = "test123";
+        String email = "test@test.net";
+        String birth = "2000-08-04";
+        String grender = "M"; // 성별 M
+        UserModel userModel = new UserModel(loginId, email, birth, grender);
+
+        userRepository.save(userModel);
+
+        // act
+        UserCommand.UserInfo userInfo = userFacade.getUserById(userModel.getLoginId());
+
+        // assert
+        AssertionsForClassTypes.assertThat(userInfo).isNotNull();
+        AssertionsForClassTypes.assertThat(userInfo.loginId()).isEqualTo(loginId);
+        AssertionsForClassTypes.assertThat(userInfo.email()).isEqualTo(email);
+        AssertionsForClassTypes.assertThat(userInfo.birth()).isEqualTo(birth);
+        AssertionsForClassTypes.assertThat(userInfo.gender()).isEqualTo(UserCommand.Gender.M);
+    }
+
+    @DisplayName("해당 ID 의 회원이 존재하지 않을 경우,  null 이 반환된다.")
+    @Test
+    void throwsException_whenUserDoesNotExist() {
+        // arrange
+        String nonExistentLoginId = "nonExistent123";
+
+        // act
+        UserCommand.UserInfo info = userFacade.getUserById(nonExistentLoginId);
+
+        // assert
+        AssertionsForClassTypes.assertThat(info).isNull();
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java
new file mode 100644
index 000000000..96be659a1
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java
@@ -0,0 +1,141 @@
+package com.loopers.interfaces.api;
+
+import com.loopers.interfaces.api.points.PointsV1ApiSpec;
+import com.loopers.interfaces.api.points.PointsV1Dto;
+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.math.BigDecimal;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.domain.user.UserModel;
+import org.junit.jupiter.api.BeforeEach;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+public class PointsV1ApiE2RTest {
+    private final TestRestTemplate testRestTemplate;
+    
+    @Autowired
+    private UserRepository userRepository;
+    
+    @Autowired
+    public PointsV1ApiE2RTest(TestRestTemplate testRestTemplate) {
+        this.testRestTemplate = testRestTemplate;
+    }
+    
+    @BeforeEach
+    void setUp() {
+        // 테스트 전에 admin 사용자가 있는지 확인하고 없으면 생성
+        if (!userRepository.existsByLoginId("admin")) {
+            UserModel adminUser = new UserModel("admin", "admin@example.com", "1990-01-01", "M");
+            userRepository.save(adminUser);
+        }
+    }
+
+    @Nested
+    @DisplayName("GET /api/v1/points")
+    class get {
+        static String ENDPOINT = "/api/v1/points";
+
+        @DisplayName("포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.")
+        @Test
+        void returnsPoints_whenPointRetrievalIsSuccessful() {
+            // arrange
+            String userId = "admin";
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId );
+
+            HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+            // act
+            ParameterizedTypeReference<ApiResponse<PointsV1Dto.PointsResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<PointsV1Dto.PointsResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, responseType);
+
+            // assert
+            assertAll(
+                    ()-> assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(),
+                    ()-> assertThat(response.getBody()).isNotNull(),
+                    ()-> assertThat(response.getBody().data().amount()).isEqualTo("0")
+            );
+        }
+        @DisplayName("X-USER-ID 헤더가 없을 경우, 400 Bad Request 응답을 반환한다.")
+        @Test
+        void  returnsBadRequest_whenPointRetrievalIsNotSuccessful() {
+            // arrange
+            HttpHeaders headers = new HttpHeaders();
+
+            HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+            // act
+            ParameterizedTypeReference<ApiResponse<PointsV1Dto.PointsResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<PointsV1Dto.PointsResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, responseType);
+
+            // assert
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+            );
+        }
+    }
+    @Nested
+    @DisplayName("POST /api/v1/points")
+    class post{
+        static String ENDPOINT = "/api/v1/points/charge";
+
+        @DisplayName("존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다.")
+        @Test
+        void returnsPoints_whenPointRetrievalIsSuccessful() {
+            // arrange
+            String userId = "admin";
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId );
+            PointsV1Dto.PointsRequest pointsV1Request = new PointsV1Dto.PointsRequest(new BigDecimal("1000.00"));
+
+            HttpEntity<PointsV1Dto.PointsRequest> entity = new HttpEntity<>(pointsV1Request, headers);
+
+            // act
+            ParameterizedTypeReference<ApiResponse<PointsV1Dto.PointsResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<PointsV1Dto.PointsResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.POST, entity, responseType);
+            System.out.println(response.getBody());
+            // assert
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK),
+                    () -> assertThat(response.getBody()).isNotNull(),
+                    () -> assertThat(response.getBody().data().amount()).isEqualTo("1000.00")
+            );
+        }
+        @DisplayName("존재하지 않는 유저로 요청할 경우, 404 Not Found 응답을 반환한다.")
+        @Test
+        void returnsNotFound_whenUserDoesNotExist() {
+            // arrange
+            String userId = "Testadmin";
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId );
+            PointsV1Dto.PointsRequest pointsV1Request = new PointsV1Dto.PointsRequest(new BigDecimal("1000.00"));
+
+            HttpEntity<PointsV1Dto.PointsRequest> entity = new HttpEntity<>(pointsV1Request, headers);
+
+            // act
+            ParameterizedTypeReference<ApiResponse<PointsV1Dto.PointsResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<PointsV1Dto.PointsResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.POST, entity, responseType);
+            // assert
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+            );
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
index 50ae43dce..3779d44af 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
@@ -115,7 +115,24 @@ void returnsBadRequest_whenUserIdIsNotProvided() {
             ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
             ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
                     testRestTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, responseType);
-            System.out.println(response.getStatusCode());
+            // assert
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+            );
+        }
+
+        @DisplayName("사용자 정보를 조회할 때, X-USER-ID 헤더가 없으면 `400 Bad Request` 응답을 반환한다.")
+        @Test
+        void returnsBadRequest_whenUserIdHeaderIsMissing() {
+            // arrange
+            HttpHeaders headers = new HttpHeaders();
+            HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+            // act
+            ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, responseType);
+
             // assert
             assertAll(
                     () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)

From 52d8c0778eb18bd8c60ce66cef97eddbe6c2e578 Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Fri, 18 Jul 2025 02:25:11 +0900
Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?=
 =?UTF-8?q?=EC=B6=A9=20add?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/points/PointsFacade.java      |  16 ++-
 .../domain/points/PointsRepository.java       |   6 +
 .../loopers/domain/points/PointsService.java  |  12 ++
 .../api/points/PointsV1Controller.java        |  21 ++--
 .../user/PointsFacadeIntegrationTest.java     | 114 ++++++++----------
 .../domain/points/PointsServiceTest.java      |  54 +++++++++
 .../interfaces/api/PointsV1ApiE2RTest.java    |  11 +-
 7 files changed, 150 insertions(+), 84 deletions(-)
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/points/PointsServiceTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java
index 311a4c865..1bdced2cf 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/points/PointsFacade.java
@@ -10,6 +10,8 @@
 import com.loopers.support.error.ErrorType;
 import org.springframework.stereotype.Component;
 
+import java.math.BigDecimal;
+
 @Component
 public class PointsFacade {
     private final PointsService pointsService;
@@ -22,13 +24,23 @@ public PointsFacade(PointsService pointsService, UserService userService) {
 
     public PointsCommand.PointInfo getPointInfo(String loginId) {
         if(!userService.isLoginIdExists(loginId)){
-            throw new CoreException(ErrorType.BAD_REQUEST, "사용자를 찾을 수 없습니다.");
+            return null;
         }
         PointsModel pointsModel = pointsService.getByLoginId(loginId);
         if(pointsModel == null) {
-            throw new CoreException(ErrorType.NOT_FOUND);
+            throw new CoreException(ErrorType.INTERNAL_ERROR);
         }
         return PointsCommand.PointInfo.from(pointsModel);
 
     }
+
+    public PointsCommand.PointInfo chargePoints(String loginId, BigDecimal amount) {
+        if(!userService.isLoginIdExists(loginId)){
+            throw new CoreException(ErrorType.BAD_REQUEST, "사용자를 찾을 수 없습니다.");
+        }
+        pointsService.chargePoints(loginId, amount);
+        return PointsCommand.PointInfo.from(
+                pointsService.getByLoginId(loginId)
+        );
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java
index 21cfcbeb7..5f9fb23b3 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsRepository.java
@@ -3,6 +3,7 @@
 import com.loopers.domain.user.UserModel;
 import org.springframework.stereotype.Component;
 
+import java.math.BigDecimal;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
@@ -12,6 +13,11 @@ public class PointsRepository {
 
     private static final Map<String, PointsModel> db = new ConcurrentHashMap<>();
 
+    public PointsRepository() {
+        PointsModel initialPoints = new PointsModel("admin", BigDecimal.ZERO);
+        db.put(initialPoints.getUserId(), initialPoints);
+    }
+
     public PointsModel save(PointsModel pointsModel) {
         db.put(pointsModel.getUserId(), pointsModel);
         return db.get(pointsModel.getUserId());
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java
index 99b180928..b5b8993c1 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/points/PointsService.java
@@ -33,4 +33,16 @@ public PointsModel getByLoginId(String loginId) {
         }
         return pointsRepository.findByLoginId(loginId).orElse(null);
     }
+
+    public PointsModel chargePoints(String loginId, BigDecimal chargeAmount) {
+        if (chargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "충전 금액은 0보다 커야 합니다.");
+        }
+        PointsModel currentPoints = getByLoginId(loginId);
+        
+        BigDecimal newAmount = currentPoints.getAmount().add(chargeAmount);
+        
+        PointsModel updatedPoints = new PointsModel(loginId, newAmount);
+        return save(updatedPoints);
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java
index 8d4c3d551..e923635ca 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/points/PointsV1Controller.java
@@ -1,6 +1,7 @@
 package com.loopers.interfaces.api.points;
 
 import com.loopers.application.points.PointsFacade;
+import com.loopers.application.points.PointsCommand;
 import com.loopers.interfaces.api.ApiResponse;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
@@ -22,12 +23,13 @@ public PointsV1Controller(PointsFacade pointsFacade) {
     @GetMapping
     public ApiResponse<PointsV1Dto.PointsResponse> getPoints(
             @RequestHeader("X-USER-ID") String userId) {
-        if(userId == null || userId.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID가 필요합니다.");
+        PointsCommand.PointInfo pointInfo = pointsFacade.getPointInfo(userId);
+        if (pointInfo == null) {
+            throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다.");
         }
+
         return ApiResponse.success(
-                PointsV1Dto.PointsResponse
-                        .from(pointsFacade.getPointInfo(userId))
+                PointsV1Dto.PointsResponse.from(pointInfo)
         );
 
     }
@@ -37,15 +39,10 @@ public ApiResponse<PointsV1Dto.PointsResponse> chargePoints(
             @RequestHeader("X-USER-ID") String userId,
             @RequestBody PointsV1Dto.PointsRequest pointsV1Request) {
         BigDecimal amount = pointsV1Request.amount();
-        if(userId == null || userId.isBlank() || !userId.equals("admin")) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID가 필요합니다.");
-        }
-
-        if(amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "충전 금액은 0보다 커야 합니다.");
-        }
         return ApiResponse.success(
-                PointsV1Dto.PointsResponse.of(amount)
+                PointsV1Dto.PointsResponse.from(
+                        pointsFacade.chargePoints(userId, amount)
+                )
         );
     }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java
index 8c85230f5..530b5ef92 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/user/PointsFacadeIntegrationTest.java
@@ -1,5 +1,9 @@
 package com.loopers.application.user;
 
+import com.loopers.application.points.PointsCommand;
+import com.loopers.application.points.PointsFacade;
+import com.loopers.domain.points.PointsRepository;
+import com.loopers.domain.points.PointsService;
 import com.loopers.domain.user.UserModel;
 import com.loopers.domain.user.UserRepository;
 import com.loopers.domain.user.UserService;
@@ -14,7 +18,10 @@
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
 
+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.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
@@ -23,97 +30,70 @@
 public class PointsFacadeIntegrationTest {
 
     @Autowired
-    private UserFacade userFacade;
+    private PointsFacade pointsFacade;
 
     @MockitoSpyBean
-    private UserService userService;
+    private PointsService pointsService;
 
     @MockitoSpyBean
-    private UserRepository userRepository;
+    private PointsRepository pointsRepository;
 
     @Nested
-    @DisplayName("[통합 테스트]회원 가입 시")
-    class SignUp {
+    @DisplayName("[통합 테스트]포인트 조회시")
+    class get {
 
-        @DisplayName("회원 가입시 User 저장이 수행된다.")
+        @DisplayName("해당 ID 의 회원이 존재할 경우, 보유 포인트가 반환된다.")
         @Test
-        void saveUser_whenSignUpIsSuccessful() {
+        void returnsPoints_whenUserExists() {
             // arrange
-            UserV1Dto.SignUpRequest request = new UserV1Dto.SignUpRequest(
-                    "newUser123",
-                    "new@example.com",
-                    "1995-05-15",
-                    UserV1Dto.GenderResponse.F
-            );
+            String loginId = "admin";
 
             // act
-            UserCommand.UserInfo result = userFacade.signUpUser(request);
+            PointsCommand.PointInfo pointInfo = pointsFacade.getPointInfo(loginId);
 
             // assert
-            assertThat(result).isNotNull();
-            assertThat(result.loginId()).isEqualTo("newUser123");
-            assertThat(result.email()).isEqualTo("new@example.com");
-            assertThat(result.birth()).isEqualTo("1995-05-15");
-            assertThat(result.gender()).isEqualTo(UserCommand.Gender.F);
-
-            verify(userService).save(any());
-            verify(userRepository).save(any());
+            assertAll(
+                    ()-> assertThat(pointInfo.amount()).isNotNull()
+            );
         }
 
-        @DisplayName("이미 가입된 ID 로 회원가입 시도 시, 실패한다.")
+        @DisplayName("해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.")
         @Test
-        void throwsException_whenLoginIdAlreadyExists() {
+        void returnsNull_whenUserDoesNotExist() {
             // arrange
-            UserV1Dto.SignUpRequest request = new UserV1Dto.SignUpRequest(
-                    "admin", // 이미 존재하는 ID
-                    "admin2@example.com",
-                    "1990-01-01",
-                    UserV1Dto.GenderResponse.M
+            String nonExistentLoginId = "nonExistent123";
+            BigDecimal chargeAmount = BigDecimal.valueOf(100);
+
+            // act & assert
+            PointsCommand.PointInfo pointInfo =
+                    pointsFacade.getPointInfo(nonExistentLoginId);
+
+            // assert
+            assertAll(
+                    () -> assertThat(pointInfo).isNull()
             );
+        }
 
-            // act
+
+    }
+    @Nested
+    @DisplayName("[통합 테스트]포인트 충전시")
+    class post {
+        @DisplayName("존재하지 않는 유저 ID 로 충전을 시도한 경우, 실패한다.")
+        @Test
+        void throwsException_whenChargingPointsForNonExistentUser() {
+            // arrange
+            String nonExistentLoginId = "nonExistent123";
+            BigDecimal chargeAmount = BigDecimal.valueOf(100);
+
+            // act & assert
             CoreException exception = assertThrows(
                     CoreException.class,
-                    () -> userFacade.signUpUser(request)
+                    () -> pointsFacade.chargePoints(nonExistentLoginId, chargeAmount)
             );
+
             // assert
             assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-            assertThat(exception.getMessage()).contains("이미 존재하는 ID입니다");
         }
     }
-    @DisplayName("해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.")
-    @Test
-    void returnsUserInfo_whenUserExists() {
-        // arrange
-        String loginId = "test123";
-        String email = "test@test.net";
-        String birth = "2000-08-04";
-        String grender = "M"; // 성별 M
-        UserModel userModel = new UserModel(loginId, email, birth, grender);
-
-        userRepository.save(userModel);
-
-        // act
-        UserCommand.UserInfo userInfo = userFacade.getUserById(userModel.getLoginId());
-
-        // assert
-        AssertionsForClassTypes.assertThat(userInfo).isNotNull();
-        AssertionsForClassTypes.assertThat(userInfo.loginId()).isEqualTo(loginId);
-        AssertionsForClassTypes.assertThat(userInfo.email()).isEqualTo(email);
-        AssertionsForClassTypes.assertThat(userInfo.birth()).isEqualTo(birth);
-        AssertionsForClassTypes.assertThat(userInfo.gender()).isEqualTo(UserCommand.Gender.M);
-    }
-
-    @DisplayName("해당 ID 의 회원이 존재하지 않을 경우,  null 이 반환된다.")
-    @Test
-    void throwsException_whenUserDoesNotExist() {
-        // arrange
-        String nonExistentLoginId = "nonExistent123";
-
-        // act
-        UserCommand.UserInfo info = userFacade.getUserById(nonExistentLoginId);
-
-        // assert
-        AssertionsForClassTypes.assertThat(info).isNull();
-    }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/points/PointsServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointsServiceTest.java
new file mode 100644
index 000000000..71f265549
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointsServiceTest.java
@@ -0,0 +1,54 @@
+package com.loopers.domain.points;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+public class PointsServiceTest {
+    
+    @Mock
+    private PointsRepository pointsRepository;
+    
+    @InjectMocks
+    private PointsService pointsService;
+    
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+    }
+
+    @DisplayName("0 이하의 정수로 포인트를 충전 시 실패한다.")
+    @Test
+    void chargePointsWithZeroOrNegativeValue() {
+        // arrange
+        BigDecimal invalidAmount = BigDecimal.valueOf(-100);
+        String loginId = "testUser";
+
+        // act
+        CoreException exception = assertThrows(
+            CoreException.class, 
+            () -> pointsService.chargePoints(loginId, invalidAmount)
+        );
+
+        // assert
+        assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
+
+        // verify: repository 호출되지 않음
+        verify(pointsRepository, never()).save(any());
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java
index 96be659a1..f314cecd9 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointsV1ApiE2RTest.java
@@ -18,8 +18,12 @@
 import com.loopers.domain.user.UserRepository;
 import com.loopers.domain.user.UserModel;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
 
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
 public class PointsV1ApiE2RTest {
     private final TestRestTemplate testRestTemplate;
     
@@ -65,7 +69,7 @@ void returnsPoints_whenPointRetrievalIsSuccessful() {
             assertAll(
                     ()-> assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(),
                     ()-> assertThat(response.getBody()).isNotNull(),
-                    ()-> assertThat(response.getBody().data().amount()).isEqualTo("0")
+                    ()-> assertThat(response.getBody().data().amount()).isEqualTo(new BigDecimal("1000.00"))
             );
         }
         @DisplayName("X-USER-ID 헤더가 없을 경우, 400 Bad Request 응답을 반환한다.")
@@ -97,10 +101,11 @@ class post{
         void returnsPoints_whenPointRetrievalIsSuccessful() {
             // arrange
             String userId = "admin";
+            String points = "1000.00";
 
             HttpHeaders headers = new HttpHeaders();
             headers.set("X-USER-ID", userId );
-            PointsV1Dto.PointsRequest pointsV1Request = new PointsV1Dto.PointsRequest(new BigDecimal("1000.00"));
+            PointsV1Dto.PointsRequest pointsV1Request = new PointsV1Dto.PointsRequest(new BigDecimal(points));
 
             HttpEntity<PointsV1Dto.PointsRequest> entity = new HttpEntity<>(pointsV1Request, headers);
 
@@ -113,7 +118,7 @@ void returnsPoints_whenPointRetrievalIsSuccessful() {
             assertAll(
                     () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK),
                     () -> assertThat(response.getBody()).isNotNull(),
-                    () -> assertThat(response.getBody().data().amount()).isEqualTo("1000.00")
+                    () -> assertThat(response.getBody().data().amount()).isEqualTo(points)
             );
         }
         @DisplayName("존재하지 않는 유저로 요청할 경우, 404 Not Found 응답을 반환한다.")

From 624be1dd76eeb1a15832fc808c1161ccb7663a22 Mon Sep 17 00:00:00 2001
From: su <su@herit.net>
Date: Fri, 18 Jul 2025 15:56:19 +0900
Subject: [PATCH 8/8] =?UTF-8?q?test:=20user=20=EB=88=84=EB=9D=BD=20test=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../interfaces/api/UserV1ApiE2ETest.java      | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
index 3779d44af..161d287dc 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java
@@ -128,6 +128,25 @@ void returnsBadRequest_whenUserIdHeaderIsMissing() {
             HttpHeaders headers = new HttpHeaders();
             HttpEntity<Void> entity = new HttpEntity<>(headers);
 
+            // act
+            ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
+            ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
+                    testRestTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, responseType);
+
+            // assert
+            assertAll(
+                    () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
+            );
+        }
+        @DisplayName("존재하지 않는 ID 로 조회할 경우, 404 Not Found 응답을 반환한다.")
+        @Test
+        void returnsNotFound_whenUserDoesNotExist() {
+            // arrange
+            String userId = "amdin123";
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId );
+            HttpEntity<Void> entity = new HttpEntity<>(headers);
+
             // act
             ParameterizedTypeReference<ApiResponse<UserV1Dto.SignUpResponse>> responseType = new ParameterizedTypeReference<>() {};
             ResponseEntity<ApiResponse<UserV1Dto.SignUpResponse>> response =
