diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..5c35e65
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ mysql.8
+ true
+ com.mysql.cj.jdbc.Driver
+ jdbc:mysql://localhost:3306/smarket
+
+
+
+
+
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 3251653..2dcfedb 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -22,6 +22,11 @@
+
diff --git a/README.md b/README.md
index 982c51b..b773963 100644
--- a/README.md
+++ b/README.md
@@ -286,7 +286,7 @@ Connection Pool설정
## 기능 구현 우선순위
-1. User
+1. User + 인증
2. Product
diff --git a/user/.gitignore b/user/.gitignore
index c2065bc..ee37c22 100644
--- a/user/.gitignore
+++ b/user/.gitignore
@@ -35,3 +35,6 @@ out/
### VS Code ###
.vscode/
+
+### applicaiton.yml ###
+/src/main/resources/application.yml
diff --git a/user/build.gradle b/user/build.gradle
index 04284bf..dda23dc 100644
--- a/user/build.gradle
+++ b/user/build.gradle
@@ -26,6 +26,9 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.security:spring-security-crypto:6.4.4'
+
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
diff --git a/user/src/main/java/com/sangyunpark/user/application/UserService.java b/user/src/main/java/com/sangyunpark/user/application/UserService.java
index 537991d..9a467eb 100644
--- a/user/src/main/java/com/sangyunpark/user/application/UserService.java
+++ b/user/src/main/java/com/sangyunpark/user/application/UserService.java
@@ -1,4 +1,44 @@
package com.sangyunpark.user.application;
+import com.sangyunpark.user.application.mapper.UserMapper;
+import com.sangyunpark.user.constant.code.ErrorCode;
+import com.sangyunpark.user.domain.dto.request.UserSignupRequestDto;
+import com.sangyunpark.user.domain.dto.response.UserSelectResponseDto;
+import com.sangyunpark.user.domain.dto.request.UserSelectByEmailRequestDto;
+import com.sangyunpark.user.domain.entity.User;
+import com.sangyunpark.user.exception.BusinessException;
+import com.sangyunpark.user.infrastructure.repository.UserJpaRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static com.sangyunpark.user.application.mapper.UserMapper.*;
+import static com.sangyunpark.user.application.mapper.UserMapper.toEntity;
+
+@Service
+@RequiredArgsConstructor
public class UserService {
+
+ private final UserJpaRepository userJpaRepository;
+
+ @Transactional
+ public Long signup(final UserSignupRequestDto userSignupRequestDto) {
+
+ return (Long) userJpaRepository.findUserByEmail(userSignupRequestDto.email())
+ .map(user -> {
+ throw new BusinessException(ErrorCode.USER_DUPLICATE);
+ })
+ .orElseGet(() -> {
+ User savedUser = userJpaRepository.save(toEntity(userSignupRequestDto));
+ return savedUser.getId();
+ });
+ }
+
+ public UserSelectResponseDto findUserById(final Long userId) {
+ return userJpaRepository.findById(userId).map(UserMapper::toUserSelectResponseDto).orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
+ }
+
+ public UserSelectResponseDto findUserByEmail(final String email) {
+ return userJpaRepository.findUserByEmail(email).map(UserMapper::toUserSelectResponseDto).orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
+ }
}
diff --git a/user/src/main/java/com/sangyunpark/user/application/mapper/UserAddressMapper.java b/user/src/main/java/com/sangyunpark/user/application/mapper/UserAddressMapper.java
new file mode 100644
index 0000000..294afbc
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/application/mapper/UserAddressMapper.java
@@ -0,0 +1,15 @@
+package com.sangyunpark.user.application.mapper;
+
+import com.sangyunpark.user.domain.dto.request.UserAddressRequestDto;
+import com.sangyunpark.user.domain.entity.UserAddress;
+
+public class UserAddressMapper {
+
+ public static UserAddress toEntity(final UserAddressRequestDto dto) {
+ return UserAddress.builder()
+ .address(dto.address())
+ .defaultAddress(dto.defaultAddress())
+ .receiverName(dto.receiverName())
+ .build();
+ }
+}
diff --git a/user/src/main/java/com/sangyunpark/user/application/mapper/UserMapper.java b/user/src/main/java/com/sangyunpark/user/application/mapper/UserMapper.java
new file mode 100644
index 0000000..8094c97
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/application/mapper/UserMapper.java
@@ -0,0 +1,45 @@
+package com.sangyunpark.user.application.mapper;
+
+import com.sangyunpark.user.constant.enums.UserStatus;
+import com.sangyunpark.user.domain.dto.request.UserSignupRequestDto;
+import com.sangyunpark.user.domain.dto.response.UserSelectResponseDto;
+import com.sangyunpark.user.domain.entity.User;
+import com.sangyunpark.user.domain.entity.UserAddress;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+public class UserMapper {
+
+ private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+
+ public static User toEntity(final UserSignupRequestDto dto) {
+
+ final UserAddress userAddress = UserAddressMapper.toEntity(dto.shippingInfo());
+
+ final User user = User.builder()
+ .email(dto.email())
+ .username(dto.username())
+ .password(passwordEncoder.encode(dto.password()))
+ .registerType(dto.registerType())
+ .phoneNumber(dto.phoneNumber())
+ .userType(dto.userType())
+ .userStatus(UserStatus.ACTIVE)
+ .build();
+
+ user.addUserAddress(userAddress);
+
+ return user;
+ }
+
+ public static UserSelectResponseDto toUserSelectResponseDto(final User user) {
+ return UserSelectResponseDto.builder()
+ .id(user.getId())
+ .userType(user.getUserType())
+ .userStatus(user.getUserStatus())
+ .phoneNumber(user.getPhoneNumber())
+ .registerType(user.getRegisterType())
+ .username(user.getUsername())
+ .email(user.getEmail())
+ .build();
+ }
+}
diff --git a/user/src/main/java/com/sangyunpark/user/config/CustomConfig.java b/user/src/main/java/com/sangyunpark/user/config/CustomConfig.java
deleted file mode 100644
index 561f519..0000000
--- a/user/src/main/java/com/sangyunpark/user/config/CustomConfig.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.sangyunpark.user.config;
-
-public class CustomConfig {
-}
diff --git a/user/src/main/java/com/sangyunpark/user/config/JpaConfig.java b/user/src/main/java/com/sangyunpark/user/config/JpaConfig.java
new file mode 100644
index 0000000..4226946
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/config/JpaConfig.java
@@ -0,0 +1,9 @@
+package com.sangyunpark.user.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@Configuration
+@EnableJpaAuditing
+public class JpaConfig {
+}
diff --git a/user/src/main/java/com/sangyunpark/user/constant/code/ErrorCode.java b/user/src/main/java/com/sangyunpark/user/constant/code/ErrorCode.java
new file mode 100644
index 0000000..9c27384
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/constant/code/ErrorCode.java
@@ -0,0 +1,22 @@
+package com.sangyunpark.user.constant.code;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum ErrorCode {
+
+ USER_DUPLICATE("user_duplicate",HttpStatus.CONFLICT),
+ USER_NOT_FOUND("user_not_found", HttpStatus.NOT_FOUND),
+ INVALID_REQUEST("invalid_request", HttpStatus.BAD_REQUEST),
+
+ INTERNAL_SERVER_ERROR("internal_server_error", HttpStatus.INTERNAL_SERVER_ERROR);
+
+ private final String code;
+ private final HttpStatus status;
+
+ ErrorCode(String code, HttpStatus status) {
+ this.code = code;
+ this.status = status;
+ }
+}
diff --git a/user/src/main/java/com/sangyunpark/user/constant/enums/RegisterType.java b/user/src/main/java/com/sangyunpark/user/constant/enums/RegisterType.java
new file mode 100644
index 0000000..e3d5b79
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/constant/enums/RegisterType.java
@@ -0,0 +1,5 @@
+package com.sangyunpark.user.constant.enums;
+
+public enum RegisterType {
+ EMAIL, OAUTH
+}
diff --git a/user/src/main/java/com/sangyunpark/user/constant/enums/UserStatus.java b/user/src/main/java/com/sangyunpark/user/constant/enums/UserStatus.java
new file mode 100644
index 0000000..8de596c
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/constant/enums/UserStatus.java
@@ -0,0 +1,5 @@
+package com.sangyunpark.user.constant.enums;
+
+public enum UserStatus {
+ ACTIVE, INACTIVE, DELETED
+}
diff --git a/user/src/main/java/com/sangyunpark/user/constant/enums/UserType.java b/user/src/main/java/com/sangyunpark/user/constant/enums/UserType.java
new file mode 100644
index 0000000..b660c45
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/constant/enums/UserType.java
@@ -0,0 +1,5 @@
+package com.sangyunpark.user.constant.enums;
+
+public enum UserType {
+ NORMAL, ADMIN
+}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/dto/UserCreateRequestDto.java b/user/src/main/java/com/sangyunpark/user/domain/dto/UserCreateRequestDto.java
deleted file mode 100644
index aa01fb9..0000000
--- a/user/src/main/java/com/sangyunpark/user/domain/dto/UserCreateRequestDto.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.sangyunpark.user.domain.dto;
-
-public class UserCreateRequestDto {
-}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserAddressRequestDto.java b/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserAddressRequestDto.java
new file mode 100644
index 0000000..54bffff
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserAddressRequestDto.java
@@ -0,0 +1,15 @@
+package com.sangyunpark.user.domain.dto.request;
+
+
+import jakarta.validation.constraints.NotBlank;
+
+public record UserAddressRequestDto(
+ @NotBlank
+ String receiverName,
+
+ @NotBlank
+ String address,
+
+ boolean defaultAddress
+) {
+}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserSelectByEmailRequestDto.java b/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserSelectByEmailRequestDto.java
new file mode 100644
index 0000000..b28a686
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserSelectByEmailRequestDto.java
@@ -0,0 +1,11 @@
+package com.sangyunpark.user.domain.dto.request;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+
+public record UserSelectByEmailRequestDto(
+ @Email
+ @NotBlank
+ String email
+) {
+}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserSignupRequestDto.java b/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserSignupRequestDto.java
new file mode 100644
index 0000000..c552461
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/domain/dto/request/UserSignupRequestDto.java
@@ -0,0 +1,35 @@
+package com.sangyunpark.user.domain.dto.request;
+
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserType;
+import jakarta.validation.constraints.*;
+
+public record UserSignupRequestDto(
+
+ @Email
+ @NotBlank
+ String email,
+
+ @NotBlank
+ @Size(min = 2, max = 10)
+ String username,
+
+ @NotBlank
+ @Size(min = 8, max = 20)
+ String password,
+
+ @NotBlank
+ @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$")
+ String phoneNumber,
+
+ @NotNull
+ RegisterType registerType,
+
+ @NotNull
+ UserType userType,
+
+ @NotNull
+ UserAddressRequestDto shippingInfo
+) {
+
+}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/dto/response/ErrorResponse.java b/user/src/main/java/com/sangyunpark/user/domain/dto/response/ErrorResponse.java
new file mode 100644
index 0000000..d750210
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/domain/dto/response/ErrorResponse.java
@@ -0,0 +1,5 @@
+package com.sangyunpark.user.domain.dto.response;
+
+public record ErrorResponse(String code) {
+
+}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/dto/response/UserSelectResponseDto.java b/user/src/main/java/com/sangyunpark/user/domain/dto/response/UserSelectResponseDto.java
new file mode 100644
index 0000000..9fdbe6b
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/domain/dto/response/UserSelectResponseDto.java
@@ -0,0 +1,18 @@
+package com.sangyunpark.user.domain.dto.response;
+
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserStatus;
+import com.sangyunpark.user.constant.enums.UserType;
+import lombok.Builder;
+
+@Builder
+public record UserSelectResponseDto(
+ Long id,
+ String email,
+ String username,
+ UserType userType,
+ UserStatus userStatus,
+ RegisterType registerType,
+ String phoneNumber
+) {
+}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/dto/response/UserSignupResponseDto.java b/user/src/main/java/com/sangyunpark/user/domain/dto/response/UserSignupResponseDto.java
new file mode 100644
index 0000000..e04ffb3
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/domain/dto/response/UserSignupResponseDto.java
@@ -0,0 +1,6 @@
+package com.sangyunpark.user.domain.dto.response;
+
+public record UserSignupResponseDto(
+ Long userId
+) {
+}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/entity/User.java b/user/src/main/java/com/sangyunpark/user/domain/entity/User.java
index 549f30d..93bba68 100644
--- a/user/src/main/java/com/sangyunpark/user/domain/entity/User.java
+++ b/user/src/main/java/com/sangyunpark/user/domain/entity/User.java
@@ -1,4 +1,75 @@
package com.sangyunpark.user.domain.entity;
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserStatus;
+import com.sangyunpark.user.constant.enums.UserType;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Table(name = "users")
+@Getter
+@EntityListeners(AuditingEntityListener.class)
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
public class User {
+
+ @Id @GeneratedValue
+ @Column(name = "user_id")
+ private Long id;
+
+ @Column(unique = true, nullable = false)
+ private String email;
+
+ @Column(nullable = false)
+ private String username;
+
+ @Column(nullable = false)
+ private String password;
+
+ @Column(nullable = false)
+ @Enumerated(value = EnumType.STRING)
+ private UserType userType;
+
+ @Column(nullable = false)
+ @Enumerated(value = EnumType.STRING)
+ private UserStatus userStatus;
+
+ @Column
+ private String providerId;
+
+ @Column(nullable = false)
+ @Enumerated(value = EnumType.STRING)
+ private RegisterType registerType;
+
+ @Column(nullable = false)
+ private String phoneNumber;
+
+ @Builder.Default
+ @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
+ private List userAddress = new ArrayList<>();
+
+ @CreatedDate
+ @Column(updatable = false, nullable = false)
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ @Column(nullable = false)
+ private LocalDateTime updatedAt;
+
+ public void addUserAddress(UserAddress address) {
+ this.userAddress.add(address);
+ address.setUser(this);
+ }
}
diff --git a/user/src/main/java/com/sangyunpark/user/domain/entity/UserAddress.java b/user/src/main/java/com/sangyunpark/user/domain/entity/UserAddress.java
new file mode 100644
index 0000000..e58574e
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/domain/entity/UserAddress.java
@@ -0,0 +1,32 @@
+package com.sangyunpark.user.domain.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Table(name = "user_address")
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class UserAddress {
+
+ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "user_address_id")
+ private Long id;
+
+ @Column(nullable = false)
+ private String receiverName;
+
+ @Setter
+ @ManyToOne
+ @JoinColumn(name = "user_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), nullable = false)
+ private User user;
+
+ @Column(nullable = false)
+ private String address;
+
+ @Column(nullable = false)
+ private Boolean defaultAddress;
+
+}
\ No newline at end of file
diff --git a/user/src/main/java/com/sangyunpark/user/domain/vo/UserStatus.java b/user/src/main/java/com/sangyunpark/user/domain/vo/UserStatus.java
deleted file mode 100644
index 85ca5b8..0000000
--- a/user/src/main/java/com/sangyunpark/user/domain/vo/UserStatus.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.sangyunpark.user.domain.vo;
-
-public enum UserStatus {
-}
diff --git a/user/src/main/java/com/sangyunpark/user/exception/BusinessException.java b/user/src/main/java/com/sangyunpark/user/exception/BusinessException.java
new file mode 100644
index 0000000..f7d58b3
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/exception/BusinessException.java
@@ -0,0 +1,14 @@
+package com.sangyunpark.user.exception;
+
+import com.sangyunpark.user.constant.code.ErrorCode;
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+
+ private final ErrorCode errorCode;
+
+ public BusinessException(ErrorCode errorCode) {
+ this.errorCode = errorCode;
+ }
+}
diff --git a/user/src/main/java/com/sangyunpark/user/exception/CustomException.java b/user/src/main/java/com/sangyunpark/user/exception/CustomException.java
deleted file mode 100644
index 7cfdc15..0000000
--- a/user/src/main/java/com/sangyunpark/user/exception/CustomException.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.sangyunpark.user.exception;
-
-public class CustomException {
-}
diff --git a/user/src/main/java/com/sangyunpark/user/global/GlobalExceptionHandler.java b/user/src/main/java/com/sangyunpark/user/global/GlobalExceptionHandler.java
new file mode 100644
index 0000000..b8116aa
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/global/GlobalExceptionHandler.java
@@ -0,0 +1,31 @@
+package com.sangyunpark.user.global;
+
+import com.sangyunpark.user.constant.code.ErrorCode;
+import com.sangyunpark.user.domain.dto.response.ErrorResponse;
+import com.sangyunpark.user.exception.BusinessException;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleException(Exception ex) {
+ ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR;
+ return ResponseEntity.status(errorCode.getStatus()).body(new ErrorResponse(errorCode.getCode()));
+ }
+
+ @ExceptionHandler(BusinessException.class)
+ public ResponseEntity handleBusinessException(BusinessException ex) {
+ ErrorCode errorCode = ex.getErrorCode();
+ return ResponseEntity.status(errorCode.getStatus()).body(new ErrorResponse(errorCode.getCode()));
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) {
+ ErrorCode errorCode = ErrorCode.INVALID_REQUEST;
+ return ResponseEntity.status(errorCode.getStatus()).body(new ErrorResponse(errorCode.getCode()));
+ }
+}
diff --git a/user/src/main/java/com/sangyunpark/user/infrastructure/repository/JpaUserRepository.java b/user/src/main/java/com/sangyunpark/user/infrastructure/repository/JpaUserRepository.java
index be47e8c..0173f35 100644
--- a/user/src/main/java/com/sangyunpark/user/infrastructure/repository/JpaUserRepository.java
+++ b/user/src/main/java/com/sangyunpark/user/infrastructure/repository/JpaUserRepository.java
@@ -1,4 +1,11 @@
package com.sangyunpark.user.infrastructure.repository;
-public interface JpaUserRepository {
+import com.sangyunpark.user.domain.entity.User;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Optional;
+
+public interface JpaUserRepository extends CrudRepository {
+
+ Optional findUserByEmail(String email);
}
diff --git a/user/src/main/java/com/sangyunpark/user/infrastructure/repository/UserJpaRepository.java b/user/src/main/java/com/sangyunpark/user/infrastructure/repository/UserJpaRepository.java
new file mode 100644
index 0000000..ba2fb78
--- /dev/null
+++ b/user/src/main/java/com/sangyunpark/user/infrastructure/repository/UserJpaRepository.java
@@ -0,0 +1,11 @@
+package com.sangyunpark.user.infrastructure.repository;
+
+import com.sangyunpark.user.domain.entity.User;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Optional;
+
+public interface UserJpaRepository extends CrudRepository {
+
+ Optional findUserByEmail(String email);
+}
diff --git a/user/src/main/java/com/sangyunpark/user/presentation/UserController.java b/user/src/main/java/com/sangyunpark/user/presentation/UserController.java
index dd95ccf..c1602d7 100644
--- a/user/src/main/java/com/sangyunpark/user/presentation/UserController.java
+++ b/user/src/main/java/com/sangyunpark/user/presentation/UserController.java
@@ -1,4 +1,35 @@
package com.sangyunpark.user.presentation;
+import com.sangyunpark.user.application.UserService;
+import com.sangyunpark.user.domain.dto.request.UserSignupRequestDto;
+import com.sangyunpark.user.domain.dto.response.UserSelectResponseDto;
+import com.sangyunpark.user.domain.dto.response.UserSignupResponseDto;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Email;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/users")
+@RequiredArgsConstructor
public class UserController {
+
+ private final UserService userService;
+
+ @GetMapping("/{id}")
+ public UserSelectResponseDto findUserById(@PathVariable Long id) {
+ return userService.findUserById(id);
+ }
+
+ @GetMapping
+ public UserSelectResponseDto findUserByEmail(@RequestParam @Email String email) {
+ return userService.findUserByEmail(email);
+ }
+
+ @PostMapping
+ public UserSignupResponseDto signup(@Valid @RequestBody final UserSignupRequestDto request) {
+ Long userId = userService.signup(request);
+ return new UserSignupResponseDto(userId);
+ }
}
diff --git a/user/src/main/resources/application.properties b/user/src/main/resources/application.properties
deleted file mode 100644
index c6fc250..0000000
--- a/user/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=user
diff --git a/user/src/test/java/com/sangyunpark/user/application/UserServiceTest.java b/user/src/test/java/com/sangyunpark/user/application/UserServiceTest.java
new file mode 100644
index 0000000..df81ca4
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/application/UserServiceTest.java
@@ -0,0 +1,165 @@
+package com.sangyunpark.user.application;
+
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserStatus;
+import com.sangyunpark.user.constant.enums.UserType;
+import com.sangyunpark.user.domain.dto.request.UserAddressRequestDto;
+import com.sangyunpark.user.domain.dto.request.UserSignupRequestDto;
+import com.sangyunpark.user.domain.dto.response.UserSelectResponseDto;
+import com.sangyunpark.user.domain.entity.User;
+import com.sangyunpark.user.exception.BusinessException;
+import com.sangyunpark.user.infrastructure.repository.UserJpaRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@SuppressWarnings("NonAsciiCharacters")
+class UserServiceTest {
+
+ private UserJpaRepository userJpaRepository;
+
+ private UserService userService;
+
+ @BeforeEach
+ void setUp() {
+ userJpaRepository = mock(UserJpaRepository.class);
+ userService = new UserService(userJpaRepository);
+ }
+
+ @Test
+ @DisplayName("회원가입 요청 시 User가 저장되고 ID를 반환한다")
+ void 회원가입_성공() {
+ // given
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "test@example.com",
+ "상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ new UserAddressRequestDto("박상윤", "서울시 강남구", true)
+ );
+
+ User savedUser = User.builder()
+ .id(1L)
+ .email(dto.email())
+ .build();
+
+ when(userJpaRepository.findUserByEmail(dto.email())).thenReturn(Optional.empty());
+ when(userJpaRepository.save(any(User.class))).thenReturn(savedUser);
+
+ // when
+ Long result = userService.signup(dto);
+
+ // then
+ assertThat(result).isEqualTo(1L);
+ verify(userJpaRepository).findUserByEmail(any(String.class));
+ verify(userJpaRepository).save(any(User.class));
+ }
+
+ @Test
+ @DisplayName("중복된 이메일로 회원가입하면 예외가 발생한다")
+ void 중복된_이메일_회원가입_실패() {
+ // given
+ UserSignupRequestDto request = new UserSignupRequestDto(
+ "existing@email.com",
+ "박상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ new UserAddressRequestDto("박상윤", "서울시 강남구", true)
+ );
+
+ User existingUser = User.builder()
+ .email(request.email())
+ .username(request.username())
+ .password(request.password())
+ .phoneNumber(request.phoneNumber())
+ .registerType(request.registerType())
+ .userType(request.userType())
+ .userStatus(UserStatus.ACTIVE)
+ .build();
+
+ given(userJpaRepository.findUserByEmail(request.email()))
+ .willReturn(Optional.of(existingUser));
+
+ // when & then
+ assertThatThrownBy(() -> userService.signup(request))
+ .isInstanceOf(BusinessException.class);
+ }
+
+ @Test
+ @DisplayName("ID로 회원 조회 성공")
+ void findUserById_성공() {
+ // given
+ Long id = 1L;
+ User user = createUser();
+
+ given(userJpaRepository.findById(id)).willReturn(Optional.of(user));
+
+ // when
+ UserSelectResponseDto result = userService.findUserById(id);
+
+ // then
+ assertThat(result.email()).isEqualTo("test@example.com");
+ }
+
+ @Test
+ @DisplayName("ID로 존재하지 않는 회원 조회 시 예외 발생")
+ void findUserById_실패() {
+ Long id = 999L;
+
+ given(userJpaRepository.findById(id)).willReturn(Optional.empty());
+
+ assertThatThrownBy(() -> userService.findUserById(id))
+ .isInstanceOf(BusinessException.class);
+ }
+
+ @Test
+ @DisplayName("이메일로 회원 조회 성공")
+ void findUserByEmail_성공() {
+ // given
+ final User user = createUser();
+ String email = "test@example.com";
+
+ given(userJpaRepository.findUserByEmail(email)).willReturn(Optional.of(user));
+
+ // when
+ UserSelectResponseDto result = userService.findUserByEmail(email);
+
+ // then
+ assertThat(result.email()).isEqualTo("test@example.com");
+ verify(userJpaRepository).findUserByEmail(email);
+ }
+
+ @Test
+ @DisplayName("이메일로 존재하지 않는 회원 조회 시 예외 발생")
+ void findUserByEmail_실패() {
+ String email = "notfound@example.com";
+ given(userJpaRepository.findUserByEmail(email)).willReturn(Optional.empty());
+
+ assertThatThrownBy(() -> userService.findUserByEmail(email))
+ .isInstanceOf(BusinessException.class);
+ }
+
+ private User createUser() {
+ return User.builder()
+ .id(1L)
+ .email("test@example.com")
+ .username("상윤")
+ .password("hashed-password")
+ .phoneNumber("010-1234-5678")
+ .registerType(RegisterType.EMAIL)
+ .userType(UserType.NORMAL)
+ .userStatus(UserStatus.ACTIVE)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/user/src/test/java/com/sangyunpark/user/application/mapper/UserAddressMapperTest.java b/user/src/test/java/com/sangyunpark/user/application/mapper/UserAddressMapperTest.java
new file mode 100644
index 0000000..0f12651
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/application/mapper/UserAddressMapperTest.java
@@ -0,0 +1,30 @@
+package com.sangyunpark.user.application.mapper;
+
+import com.sangyunpark.user.domain.dto.request.UserAddressRequestDto;
+import com.sangyunpark.user.domain.entity.UserAddress;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UserAddressMapperTest {
+
+ @Test
+ @DisplayName("UserAddressRequestDto를 UserAddress 엔티티로 정상 변환한다")
+ void toEntity_변환성공() {
+ // given
+ UserAddressRequestDto dto = new UserAddressRequestDto(
+ "박상윤",
+ "경기도 부천시 소사구",
+ true
+ );
+
+ // when
+ UserAddress entity = UserAddressMapper.toEntity(dto);
+
+ // then
+ assertThat(entity.getReceiverName()).isEqualTo(dto.receiverName());
+ assertThat(entity.getAddress()).isEqualTo(dto.address());
+ assertThat(entity.getDefaultAddress()).isEqualTo(dto.defaultAddress());
+ }
+}
\ No newline at end of file
diff --git a/user/src/test/java/com/sangyunpark/user/application/mapper/UserMapperTest.java b/user/src/test/java/com/sangyunpark/user/application/mapper/UserMapperTest.java
new file mode 100644
index 0000000..5ea603b
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/application/mapper/UserMapperTest.java
@@ -0,0 +1,52 @@
+package com.sangyunpark.user.application.mapper;
+
+import com.sangyunpark.user.domain.dto.request.UserAddressRequestDto;
+import com.sangyunpark.user.domain.dto.request.UserSignupRequestDto;
+import com.sangyunpark.user.domain.entity.User;
+import com.sangyunpark.user.domain.entity.UserAddress;
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UserMapperTest {
+
+ private final UserMapper userMapper = new UserMapper();
+
+ @Test
+ @DisplayName("UserSignupRequestDto를 User 엔티티로 변환한다")
+ void UserSignupRequestDto에서_User로_변환_성공() {
+ // given
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "test@example.com",
+ "상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ new UserAddressRequestDto("박상윤", "서울시 강남구", true)
+ );
+
+ // when
+ User user = userMapper.toEntity(dto);
+
+ // then
+ assertThat(user.getEmail()).isEqualTo(dto.email());
+ assertThat(user.getUsername()).isEqualTo(dto.username());
+ assertThat(user.getPassword()).isNotEqualTo(dto.password());
+ assertThat(user.getRegisterType()).isEqualTo(dto.registerType());
+ assertThat(user.getPhoneNumber()).isEqualTo(dto.phoneNumber());
+ assertThat(user.getUserType()).isEqualTo(dto.userType());
+ assertThat(user.getUserStatus()).isNotNull();
+
+ assertThat(user.getUserAddress()).hasSize(1);
+ UserAddress address = user.getUserAddress().get(0);
+ assertThat(address.getReceiverName()).isEqualTo(dto.shippingInfo().receiverName());
+ assertThat(address.getAddress()).isEqualTo(dto.shippingInfo().address());
+ assertThat(address.getDefaultAddress()).isEqualTo(dto.shippingInfo().defaultAddress());
+
+ assertThat(address.getUser()).isEqualTo(user);
+ }
+}
\ No newline at end of file
diff --git a/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserAddressRequestDtoTest.java b/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserAddressRequestDtoTest.java
new file mode 100644
index 0000000..f877981
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserAddressRequestDtoTest.java
@@ -0,0 +1,66 @@
+package com.sangyunpark.user.domain.dto.request;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+@SuppressWarnings("NonAsciiCharacters")
+class UserAddressRequestDtoTest {
+
+ private Validator validator;
+
+ @BeforeEach
+ void setUp() {
+ try(ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
+ validator = factory.getValidator();
+ }
+ }
+
+ @Test
+ void 모든값이_정상일때_검증에_성공한다() {
+ // given
+ UserAddressRequestDto dto = new UserAddressRequestDto("박상윤", "경기도 부천시", true);
+
+ // when
+ Set> violations = validator.validate(dto);
+
+ // then
+ assertThat(violations).isEmpty();
+ }
+
+ @Test
+ void 수령인_이름이_비어있으면_검증에_실패한다() {
+ // given
+ UserAddressRequestDto dto = new UserAddressRequestDto(" ", "경기도 부천시", true);
+
+ // when, then
+ assertViolation(dto,"receiverName");
+ }
+
+ @Test
+ void 주소가_비어있으면_검증에_실패한다() {
+ // given
+ UserAddressRequestDto dto = new UserAddressRequestDto("박상윤", "", true);
+
+ // when, then
+ assertViolation(dto,"address");
+ }
+
+ private void assertViolation(UserAddressRequestDto dto, String field) {
+ Set> violations = validator.validate(dto);
+ assertThat(violations)
+ .hasSize(1)
+ .anyMatch(v ->
+ v.getPropertyPath().toString().equals(field)
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserSelectByEmailRequestDtoTest.java b/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserSelectByEmailRequestDtoTest.java
new file mode 100644
index 0000000..ece3fd7
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserSelectByEmailRequestDtoTest.java
@@ -0,0 +1,63 @@
+package com.sangyunpark.user.domain.dto.request;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UserSelectByEmailRequestDtoTest {
+
+ private Validator validator;
+
+ @BeforeEach
+ void setUp() {
+ ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+ validator = factory.getValidator();
+ }
+
+ @Test
+ @DisplayName("올바른 이메일이면 검증을 통과한다")
+ void 유효한_이메일_검증_성공() {
+ // given
+ UserSelectByEmailRequestDto dto = new UserSelectByEmailRequestDto("test@example.com");
+
+ // when
+ Set> violations = validator.validate(dto);
+
+ // then
+ assertThat(violations).isEmpty();
+ }
+
+ @Test
+ @DisplayName("이메일이 빈 값이면 검증에 실패한다")
+ void 이메일_빈값_검증_실패() {
+ // given
+ UserSelectByEmailRequestDto dto = new UserSelectByEmailRequestDto("");
+
+ // when
+ Set> violations = validator.validate(dto);
+
+ // then
+ assertThat(violations).hasSize(1);
+ }
+
+ @Test
+ @DisplayName("이메일 형식이 아니면 검증에 실패한다")
+ void 이메일_형식_검증_실패() {
+ // given
+ UserSelectByEmailRequestDto dto = new UserSelectByEmailRequestDto("invalid-email");
+
+ // when
+ Set> violations = validator.validate(dto);
+
+ // then
+ assertThat(violations).hasSize(1);
+ }
+}
\ No newline at end of file
diff --git a/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserSignupRequestDtoTest.java b/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserSignupRequestDtoTest.java
new file mode 100644
index 0000000..f5b2f36
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/domain/dto/request/UserSignupRequestDtoTest.java
@@ -0,0 +1,128 @@
+package com.sangyunpark.user.domain.dto.request;
+
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserType;
+import jakarta.validation.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SuppressWarnings("NonAsciiCharacters")
+class UserSignupRequestDtoTest {
+
+ private Validator validator;
+
+ @BeforeEach
+ void setUp() {
+ try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
+ validator = factory.getValidator();
+ }
+ }
+
+ private UserAddressRequestDto validAddress() {
+ return new UserAddressRequestDto("홍길동", "서울시 강남구", true);
+ }
+
+ @Test
+ void 모든값이_정상일때_검증에_성공한다() {
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "test@example.com",
+ "상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ validAddress()
+ );
+
+ Set> violations = validator.validate(dto);
+ assertThat(violations).isEmpty();
+ }
+
+ @Test
+ void 이메일이_비어있으면_검증에_실패한다() {
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "",
+ "상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ validAddress()
+ );
+
+ assertViolation(dto, "email");
+ }
+
+ @Test
+ void 비밀번호가_짧으면_검증에_실패한다() {
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "test@example.com",
+ "상윤",
+ "abcde",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ validAddress()
+ );
+
+ assertViolation(dto, "password");
+ }
+
+ @Test
+ void 전화번호_형식이_잘못되면_검증에_실패한다() {
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "test@example.com",
+ "상윤",
+ "password123",
+ "010123@@@@45678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ validAddress()
+ );
+
+ assertViolation(dto, "phoneNumber");
+ }
+
+ @Test
+ void 주소가_null이면_검증에_실패한다() {
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "test@example.com",
+ "상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ null
+ );
+
+ assertViolation(dto, "shippingInfo");
+ }
+
+ @Test
+ void 회원유형이_null이면_검증에_실패한다() {
+ UserSignupRequestDto dto = new UserSignupRequestDto(
+ "test@example.com",
+ "상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ null,
+ validAddress()
+ );
+
+ assertViolation(dto, "userType");
+ }
+
+ private void assertViolation(UserSignupRequestDto dto, String field) {
+ Set> violations = validator.validate(dto);
+ assertThat(violations)
+ .hasSize(1)
+ .anyMatch(v ->
+ v.getPropertyPath().toString().equals(field)
+ );
+ }
+}
\ No newline at end of file
diff --git a/user/src/test/java/com/sangyunpark/user/integration/UserIntegrationTest.java b/user/src/test/java/com/sangyunpark/user/integration/UserIntegrationTest.java
new file mode 100644
index 0000000..2e26d16
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/integration/UserIntegrationTest.java
@@ -0,0 +1,127 @@
+package com.sangyunpark.user.integration;
+
+import com.sangyunpark.user.constant.code.ErrorCode;
+import com.sangyunpark.user.domain.entity.User;
+import com.sangyunpark.user.domain.entity.UserAddress;
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserStatus;
+import com.sangyunpark.user.constant.enums.UserType;
+import com.sangyunpark.user.exception.BusinessException;
+import com.sangyunpark.user.infrastructure.repository.UserJpaRepository;
+import jakarta.transaction.Transactional;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@Transactional
+@SuppressWarnings("NonAsciiCharacters")
+public class UserIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private UserJpaRepository userRepository;
+
+ @Test
+ @DisplayName("정상적인 회원가입 요청이 오면 DB에 유저가 저장된다.")
+ void 회원가입_성공() throws Exception {
+ // given
+ String requestJson = """
+ {
+ "email": "test@example.com",
+ "username": "상윤",
+ "password": "password123",
+ "phoneNumber": "010-1234-5678",
+ "registerType": "EMAIL",
+ "userType": "NORMAL",
+ "shippingInfo": {
+ "receiverName": "홍길동",
+ "address": "서울시 강남구",
+ "defaultAddress": true
+ }
+ }
+ """;
+
+ // when & then
+ mockMvc.perform(post("/api/v1/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestJson))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.userId").exists());
+
+ User savedUser = userRepository.findUserByEmail("test@example.com")
+ .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
+
+ assertThat(savedUser.getEmail()).isEqualTo("test@example.com");
+ assertThat(savedUser.getUsername()).isEqualTo("상윤");
+ assertThat(savedUser.getPassword()).isNotEqualTo("password123");
+ assertThat(savedUser.getPhoneNumber()).isEqualTo("010-1234-5678");
+ assertThat(savedUser.getRegisterType().name()).isEqualTo("EMAIL");
+ assertThat(savedUser.getUserType().name()).isEqualTo("NORMAL");
+ assertThat(savedUser.getUserStatus().name()).isEqualTo("ACTIVE");
+
+ assertThat(savedUser.getUserAddress()).hasSize(1);
+ UserAddress address = savedUser.getUserAddress().get(0);
+ assertThat(address.getReceiverName()).isEqualTo("홍길동");
+ assertThat(address.getAddress()).isEqualTo("서울시 강남구");
+ assertThat(address.getDefaultAddress()).isTrue();
+ assertThat(address.getUser()).isEqualTo(savedUser);
+ }
+
+ @Test
+ @DisplayName("중복된 이메일로 회원가입 요청이 오면 400과 예외 메시지를 반환한다.")
+ void 회원가입_중복된_이메일_실패() throws Exception {
+ // given: 이미 존재하는 유저를 DB에 저장
+ User existingUser = User.builder()
+ .email("test@example.com")
+ .username("기존유저")
+ .password("hashedPassword")
+ .phoneNumber("010-1234-5678")
+ .registerType(RegisterType.EMAIL)
+ .userType(UserType.NORMAL)
+ .userStatus(UserStatus.ACTIVE)
+ .build();
+
+ UserAddress address = UserAddress.builder()
+ .receiverName("기존수신자")
+ .address("서울시 중구")
+ .defaultAddress(true)
+ .build();
+
+ existingUser.addUserAddress(address);
+ userRepository.save(existingUser);
+
+ String requestJson = """
+ {
+ "email": "test@example.com",
+ "username": "상윤",
+ "password": "password123",
+ "phoneNumber": "010-1234-5678",
+ "registerType": "EMAIL",
+ "userType": "NORMAL",
+ "shippingInfo": {
+ "receiverName": "홍길동",
+ "address": "서울시 강남구",
+ "defaultAddress": true
+ }
+ }
+ """;
+
+ // when & then
+ mockMvc.perform(post("/api/v1/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestJson))
+ .andExpect(status().is(ErrorCode.USER_DUPLICATE.getStatus().value()));
+ }
+}
diff --git a/user/src/test/java/com/sangyunpark/user/integration/UserQueryIntegrationTest.java b/user/src/test/java/com/sangyunpark/user/integration/UserQueryIntegrationTest.java
new file mode 100644
index 0000000..c433058
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/integration/UserQueryIntegrationTest.java
@@ -0,0 +1,107 @@
+package com.sangyunpark.user.integration;
+
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserStatus;
+import com.sangyunpark.user.constant.enums.UserType;
+import com.sangyunpark.user.domain.entity.User;
+import com.sangyunpark.user.domain.entity.UserAddress;
+import com.sangyunpark.user.infrastructure.repository.JpaUserRepository;
+import jakarta.transaction.Transactional;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.hamcrest.Matchers.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@Transactional
+@SuppressWarnings("NonAsciiCharacters")
+class UserQueryIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private JpaUserRepository userRepository;
+
+ @Test
+ @DisplayName("ID 기반 회원 조회 성공")
+ void 회원조회_ID_성공() throws Exception {
+ // given
+ User user = createAndSaveTestUser("iduser@example.com");
+
+ // when & then
+ mockMvc.perform(get("/api/v1/users/" + user.getId()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(user.getId().intValue())))
+ .andExpect(jsonPath("$.email", is("iduser@example.com")))
+ .andExpect(jsonPath("$.username", is("상윤")))
+ .andExpect(jsonPath("$.userType", is("NORMAL")))
+ .andExpect(jsonPath("$.userStatus", is("ACTIVE")))
+ .andExpect(jsonPath("$.registerType", is("EMAIL")))
+ .andExpect(jsonPath("$.phoneNumber", is("010-1234-5678")));
+ }
+
+ @Test
+ @DisplayName("ID 기반 회원 조회 실패 - 존재하지 않음")
+ void 회원조회_ID_실패() throws Exception {
+ mockMvc.perform(get("/api/v1/users/9999"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @DisplayName("이메일 기반 회원 조회 성공")
+ void 회원조회_이메일_성공() throws Exception {
+ // given
+ User user = createAndSaveTestUser("emailuser@example.com");
+
+ // when & then
+ mockMvc.perform(get("/api/v1/users?email=emailuser@example.com")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id", is(user.getId().intValue())))
+ .andExpect(jsonPath("$.email", is("emailuser@example.com")))
+ .andExpect(jsonPath("$.username", is("상윤")))
+ .andExpect(jsonPath("$.userType", is("NORMAL")))
+ .andExpect(jsonPath("$.userStatus", is("ACTIVE")))
+ .andExpect(jsonPath("$.registerType", is("EMAIL")))
+ .andExpect(jsonPath("$.phoneNumber", is("010-1234-5678")));
+ }
+
+ @Test
+ @DisplayName("이메일 기반 회원 조회 실패 - 존재하지 않음")
+ void 회원조회_이메일_실패() throws Exception {
+
+ mockMvc.perform(get("/api/v1/users?email=notfound@example.com")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound());
+ }
+
+ private User createAndSaveTestUser(String email) {
+ User user = User.builder()
+ .email(email)
+ .username("상윤")
+ .password("encodedPassword")
+ .phoneNumber("010-1234-5678")
+ .registerType(RegisterType.EMAIL)
+ .userType(UserType.NORMAL)
+ .userStatus(UserStatus.ACTIVE)
+ .build();
+
+ UserAddress address = UserAddress.builder()
+ .receiverName("홍길동")
+ .address("서울시 강남구")
+ .defaultAddress(true)
+ .build();
+
+ user.addUserAddress(address);
+ return userRepository.save(user);
+ }
+}
diff --git a/user/src/test/java/com/sangyunpark/user/presentation/UserControllerTest.java b/user/src/test/java/com/sangyunpark/user/presentation/UserControllerTest.java
new file mode 100644
index 0000000..0660587
--- /dev/null
+++ b/user/src/test/java/com/sangyunpark/user/presentation/UserControllerTest.java
@@ -0,0 +1,196 @@
+package com.sangyunpark.user.presentation;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sangyunpark.user.application.UserService;
+import com.sangyunpark.user.constant.enums.UserStatus;
+import com.sangyunpark.user.domain.dto.request.UserAddressRequestDto;
+import com.sangyunpark.user.domain.dto.request.UserSignupRequestDto;
+import com.sangyunpark.user.constant.enums.RegisterType;
+import com.sangyunpark.user.constant.enums.UserType;
+import com.sangyunpark.user.exception.BusinessException;
+import com.sangyunpark.user.domain.dto.response.UserSelectResponseDto;
+import com.sangyunpark.user.domain.dto.request.UserSelectByEmailRequestDto;
+import com.sangyunpark.user.global.GlobalExceptionHandler;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static com.sangyunpark.user.constant.code.ErrorCode.*;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SuppressWarnings("NonAsciiCharacters")
+@WebMvcTest(UserController.class)
+@Import(GlobalExceptionHandler.class)
+class UserControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockitoBean
+ private UserService userService;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Test
+ @DisplayName("회원가입 요청이 유효하면 200 OK와 userId, 메시지를 반환한다")
+ void 회원가입_성공() throws Exception {
+ // given
+ UserSignupRequestDto request = new UserSignupRequestDto(
+ "test@example.com",
+ "박상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ new UserAddressRequestDto("박상윤", "서울시 강남구", true)
+ );
+
+ Long fakeUserId = 1L;
+ given(userService.signup(any())).willReturn(fakeUserId);
+
+ // when & then
+ mockMvc.perform(post("/api/v1/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request))
+ )
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.userId", is(fakeUserId.intValue())));
+ }
+
+ @Test
+ @DisplayName("중복된_이메일이면_회원가입_실패")
+ void 중복된_이메일이면_회원가입_실패() throws Exception {
+ // given
+ UserSignupRequestDto request = new UserSignupRequestDto(
+ "existing@email.com",
+ "박상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ new UserAddressRequestDto("박상윤", "서울시 강남구", true)
+ );
+
+ given(userService.signup(any(UserSignupRequestDto.class)))
+ .willThrow(new BusinessException(USER_DUPLICATE));
+
+ // when & then
+ mockMvc.perform(post("/api/v1/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().is(USER_DUPLICATE.getStatus().value()));
+ }
+
+ @Test
+ @DisplayName("이메일이 비어있으면 400 Bad Request를 반환한다")
+ void 이메일이_비어있어_회원가입_실패() throws Exception {
+ UserSignupRequestDto request = new UserSignupRequestDto(
+ "", // invalid email
+ "박상윤",
+ "password123",
+ "010-1234-5678",
+ RegisterType.EMAIL,
+ UserType.NORMAL,
+ new UserAddressRequestDto("박상윤", "서울시 강남구", true)
+ );
+
+ mockMvc.perform(post("/api/v1/users")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().is(INVALID_REQUEST.getStatus().value()));
+ }
+
+ @Test
+ @DisplayName("이메일로 유저 조회 성공")
+ void 이메일로_유저_조회_성공() throws Exception {
+ // given
+ UserSelectByEmailRequestDto request = new UserSelectByEmailRequestDto("test@example.com");
+ UserSelectResponseDto response = UserSelectResponseDto.builder()
+ .id(1L)
+ .email("test@example.com")
+ .username("상윤")
+ .userType(UserType.NORMAL)
+ .userStatus(UserStatus.ACTIVE)
+ .registerType(RegisterType.EMAIL)
+ .phoneNumber("010-1234-5678")
+ .build();
+
+ given(userService.findUserByEmail(any(String.class))).willReturn(response);
+
+ // when & then
+ mockMvc.perform(get("/api/v1/users?email=test@example.com")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.email", is("test@example.com")))
+ .andExpect(jsonPath("$.username", is("상윤")))
+ .andExpect(jsonPath("$.userType", is("NORMAL")))
+ .andExpect(jsonPath("$.userStatus", is("ACTIVE")))
+ .andExpect(jsonPath("$.registerType", is("EMAIL")))
+ .andExpect(jsonPath("$.phoneNumber", is("010-1234-5678")));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 이메일로 유저 조회 시 예외 발생")
+ void 이메일로_유저_조회_실패() throws Exception {
+ // given
+ given(userService.findUserByEmail(any(String.class)))
+ .willThrow(new BusinessException(USER_NOT_FOUND));
+
+ // when & then
+ mockMvc.perform(get("/api/v1/users?email=nonexist@example.com")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ @DisplayName("ID로 유저 조회 성공")
+ void 유저_ID_조회_성공() throws Exception {
+ // given
+ Long userId = 1L;
+ UserSelectResponseDto response = UserSelectResponseDto.builder()
+ .id(userId)
+ .email("test@example.com")
+ .username("상윤")
+ .userType(UserType.NORMAL)
+ .userStatus(UserStatus.ACTIVE)
+ .registerType(RegisterType.EMAIL)
+ .phoneNumber("010-1234-5678")
+ .build();
+
+ given(userService.findUserById(userId)).willReturn(response);
+
+ // when & then
+ mockMvc.perform(get("/api/v1/users/{id}", userId))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.id").value(userId))
+ .andExpect(jsonPath("$.email").value("test@example.com"))
+ .andExpect(jsonPath("$.username").value("상윤"));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 ID로 유저 조회 시 404 반환")
+ void 유저_ID_조회_실패() throws Exception {
+ // given
+ Long userId = 999L;
+ given(userService.findUserById(userId))
+ .willThrow(new BusinessException(USER_NOT_FOUND));
+
+ // when & then
+ mockMvc.perform(get("/api/v1/users/{id}", userId))
+ .andExpect(status().isNotFound());
+ }
+}
\ No newline at end of file