Repository: f-lab-edu/topay
Pull request #9: feat: User 도메인 기능 구현
Author: login: HongJungWan
Labels: name: userDoamin - feature
description: 유저 도메인 - 기능 구현
Created: 2025-04-05T02:07:39Z | Merged: 2025-04-05T02:13:57Z

- feat: 유저 도메인 TODO 주석 명시 
  - 기능 개발 전 구현 범위 검토 목적

<br>

- feat: SHA-256 해시 알고리즘 - 암호화 기능 구현
- test: SHA-256 해시 알고리즘 - 암호화 기능 테스트 구현
  - SHA-256 해시 암호화 테스트 - 일반 문자열
  - SHA-256 해시 암호화 테스트 - 빈 문자열 
  - SHA-256 해시 암호화 테스트 - null 입력


Diff:
diff --git a/deploy.sh b/deploy.sh
index 49fc3aa..8f4e236 100644
--- a/deploy.sh
+++ b/deploy.sh
@@ -2,9 +2,9 @@
 
 DEPLOY_DIR="$(pwd)"
 DEPLOY_FILE="$DEPLOY_DIR/deploy.txt"
-IMAGE_NAME="weakwill-api"
+IMAGE_NAME="topay-api"
 IMAGE_TAG="latest"
-CONTAINER_NAME="weakwill-api-container"
+CONTAINER_NAME="topay-api-container"
 DOCKERFILE="$DEPLOY_DIR/Dockerfile"
 
 if [ -f "$DEPLOY_FILE" ]; then
diff --git a/src/main/java/com/topay/common/utils/encrytion/EncryptionService.java b/src/main/java/com/topay/common/utils/encrytion/EncryptionService.java
new file mode 100644
index 0000000..b592922
--- /dev/null
+++ b/src/main/java/com/topay/common/utils/encrytion/EncryptionService.java
@@ -0,0 +1,6 @@
+package com.topay.common.utils.encryption;
+public interface EncryptionService {
+
+    public String encrypt(String s);
+
+}
diff --git a/src/main/java/com/topay/common/utils/encrytion/SHA256EncryptionService.java b/src/main/java/com/topay/common/utils/encrytion/SHA256EncryptionService.java
new file mode 100644
index 0000000..dc6f70f
--- /dev/null
+++ b/src/main/java/com/topay/common/utils/encrytion/SHA256EncryptionService.java
@@ -0,0 +1,36 @@
+package com.topay.common.utils.encryption;
+import org.springframework.stereotype.Component;
+
+import java.security.MessageDigest;
+
+/**
+ * 입력 문자열을 SHA-256 해시 알고리즘으로 암호화한다.
+ * <p>
+ * 1. 문자열을 바이트 배열로 변환 후, SHA-256 해시 계산
+ * 2. 계산된 해시 바이트 배열을 16진수 문자열로 변환하여 반환
+ * 3. 암호화 도중 예외 발생 시 원본 문자열 반환
+ */
+@Component
+public class SHA256EncryptionService implements EncryptionService {
+
+    @Override
+    public String encrypt(String string) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+
+            byte[] passBytes = string.getBytes();
+            md.reset();
+
+            byte[] digested = md.digest(passBytes);
+            StringBuilder sb = new StringBuilder();
+
+            for (byte b : digested) {
+                sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            return string;
+        }
+    }
+
+}
diff --git a/src/main/java/com/topay/user/controller/UserController.java b/src/main/java/com/topay/user/controller/UserController.java
new file mode 100644
index 0000000..2af91d3
--- /dev/null
+++ b/src/main/java/com/topay/user/controller/UserController.java
@@ -0,0 +1,26 @@
+package com.topay.user.controller;
+
+import com.topay.user.service.UserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RequestMapping("/users")
+@RestController
+public class UserController {
+    private final UserService userService;
+
+    // TODO: 회원 가입 API 엔드포인트
+
+    // TODO: 이메일 중복 체크 API 엔드포인트
+
+    // TODO: 사용자 조회 API 엔드포인트
+
+    // TODO: 특정 사용자 검색 API 엔드포인트
+
+    // TODO: 로그인 API 엔드포인트
+
+    // TODO: 로그아웃 API 엔드포인트
+
+}
diff --git a/src/main/java/com/topay/user/controller/.gitkeep b/src/main/java/com/topay/user/controller/dto/.gitkeep
similarity index 100%
rename from src/main/java/com/topay/user/controller/.gitkeep
rename to src/main/java/com/topay/user/controller/dto/.gitkeep
diff --git a/src/main/java/com/topay/user/domain/.gitkeep b/src/main/java/com/topay/user/domain/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/java/com/topay/user/domain/Account.java b/src/main/java/com/topay/user/domain/Account.java
new file mode 100644
index 0000000..b23b3f3
--- /dev/null
+++ b/src/main/java/com/topay/user/domain/Account.java
@@ -0,0 +1,21 @@
+package com.topay.user.domain;
+
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Embeddable
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+public class Account {
+
+    private String bankName;
+
+    private String accountNumber;
+
+    private String depositor;
+
+}
diff --git a/src/main/java/com/topay/user/domain/BaseTimeEntity.java b/src/main/java/com/topay/user/domain/BaseTimeEntity.java
new file mode 100644
index 0000000..b7c7370
--- /dev/null
+++ b/src/main/java/com/topay/user/domain/BaseTimeEntity.java
@@ -0,0 +1,25 @@
+package com.topay.user.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@Getter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class BaseTimeEntity {
+
+    @CreatedDate
+    @Column(updatable = false)
+    private LocalDateTime createdDate;
+
+    @LastModifiedDate
+    private LocalDateTime modifiedDate;
+
+}
diff --git a/src/main/java/com/topay/user/domain/Level.java b/src/main/java/com/topay/user/domain/Level.java
new file mode 100644
index 0000000..50dc990
--- /dev/null
+++ b/src/main/java/com/topay/user/domain/Level.java
@@ -0,0 +1,7 @@
+package com.topay.user.domain;
+
+public enum Level {
+
+    UN_AUTH, AUTH, ADMIN
+
+}
diff --git a/src/main/java/com/topay/user/domain/UserEntity.java b/src/main/java/com/topay/user/domain/UserEntity.java
new file mode 100644
index 0000000..da6c4d3
--- /dev/null
+++ b/src/main/java/com/topay/user/domain/UserEntity.java
@@ -0,0 +1,46 @@
+package com.topay.user.domain;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Entity
+public class UserEntity extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue
+    @Column(name = "USER_ID")
+    private Long id;
+
+    private String name;
+
+    @Column(unique = true, updatable = false)
+    private String email;
+
+    private String password;
+
+    @Embedded
+    private Account account;
+
+    private Level level;
+
+    public static UserEntity of(String name, String email, Account account, Level level) {
+        return new UserEntity(null, name, email, account, level);
+    }
+
+    public UserEntity(Long id, String name, String email, Account account, Level level) {
+        this.id = id;
+        this.name = name;
+        this.email = email;
+        this.account = account;
+        this.level = level;
+    }
+
+    // TODO: 유저 관련 비즈니스 행위를 본 도메인에서 구현한다.
+
+}
diff --git a/src/main/java/com/topay/user/repository/.gitkeep b/src/main/java/com/topay/user/repository/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/java/com/topay/user/repository/UserRepository.java b/src/main/java/com/topay/user/repository/UserRepository.java
new file mode 100644
index 0000000..b2fc899
--- /dev/null
+++ b/src/main/java/com/topay/user/repository/UserRepository.java
@@ -0,0 +1,10 @@
+package com.topay.user.repository;
+
+import com.topay.user.domain.UserEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserRepository extends JpaRepository<UserEntity, Long> {
+
+    // TODO: 사용자 도메인에 특화된 커스텀 쿼리 메소드 추가 (예: 이메일로 사용자 조회, 사용자명 검색 등)
+
+}
diff --git a/src/main/java/com/topay/user/service/.gitkeep b/src/main/java/com/topay/user/service/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/java/com/topay/user/service/UserService.java b/src/main/java/com/topay/user/service/UserService.java
new file mode 100644
index 0000000..64fcc42
--- /dev/null
+++ b/src/main/java/com/topay/user/service/UserService.java
@@ -0,0 +1,26 @@
+package com.topay.user.service;
+
+import com.topay.common.utils.encrytion.EncryptionService;
+import com.topay.user.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class UserService {
+    private final UserRepository userRepository;
+    private final EncryptionService encryptionService;
+
+    // TODO: 회원 가입 기능
+
+    // TODO: 이메일 중복 체크 기능
+
+    // TODO: 사용자 조회 기능
+
+    // TODO: 특정 사용자 검색 기능
+
+    // TODO: ...
+
+}
diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml
index 99a6e22..9128924 100644
--- a/src/main/resources/application-prd.yml
+++ b/src/main/resources/application-prd.yml
@@ -2,6 +2,10 @@ spring:
   config:
     activate:
       on-profile: prd
+  jpa:
+    hibernate:
+      ddl-auto: none
+      format_sql: true
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: ${DB_URL}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index a3fdfd5..933af60 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,6 +2,10 @@ spring:
   config:
     activate:
       on-profile: local
+  jpa:
+    hibernate:
+      ddl-auto: create-drop
+      format_sql: true
   datasource:
     driver-class-name: org.h2.Driver
     url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
diff --git a/src/test/java/com/topay/.gitkeep b/src/test/java/com/topay/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/test/java/com/topay/common/utils/encrytion/SHA256EncryptionServiceTest.java b/src/test/java/com/topay/common/utils/encrytion/SHA256EncryptionServiceTest.java
new file mode 100644
index 0000000..227d0dd
--- /dev/null
+++ b/src/test/java/com/topay/common/utils/encrytion/SHA256EncryptionServiceTest.java
@@ -0,0 +1,58 @@
+package com.topay.common.utils.encryption;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class SHA256EncryptionServiceTest {
+    private SHA256EncryptionService encryptionService;
+
+    @BeforeEach
+    void setUp() {
+        encryptionService = new SHA256EncryptionService();
+    }
+
+    @Test
+    @DisplayName("SHA-256 해시 암호화 테스트 - 일반 문자열")
+    void testEncryptWithValidInput() {
+        // given
+        String input = "password";
+        String expected = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
+
+        // when
+        String actual = encryptionService.encrypt(input);
+
+        // then
+        assertEquals(expected, actual, "SHA-256 해시가 예상 결과와 일치해야 한다.");
+    }
+
+    @Test
+    @DisplayName("SHA-256 해시 암호화 테스트 - 빈 문자열")
+    void testEncryptWithEmptyString() {
+        // given
+        String input = "";
+        String expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+
+        // when
+        String actual = encryptionService.encrypt(input);
+
+        // then
+        assertEquals(expected, actual, "빈 문자열에 대한 SHA-256 해시가 예상 결과와 일치해야 한다.");
+    }
+
+    @Test
+    @DisplayName("SHA-256 해시 암호화 테스트 - null 입력")
+    void testEncryptWithNull() {
+        // given
+        String input = null;
+
+        // when
+        String actual = encryptionService.encrypt(input);
+
+        // then
+        assertNull(actual, "null 입력 시, 결과도 null이어야 한다.");
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/topay/user/domain/UserEntityTest.java b/src/test/java/com/topay/user/domain/UserEntityTest.java
new file mode 100644
index 0000000..8b81e99
--- /dev/null
+++ b/src/test/java/com/topay/user/domain/UserEntityTest.java
@@ -0,0 +1,40 @@
+package com.topay.user.domain;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class UserEntityTest {
+
+    private String name;
+    private String email;
+    private Account account;
+    private Level level;
+
+    @BeforeEach
+    void setUp() {
+        // given
+        name = "Hong Jung Wan";
+        email = "jungwan.hong@example.com";
+        account = new Account("MyBank", "1234567890", "Hong Jung Wan");
+        level = Level.UN_AUTH;
+    }
+
+    @Test
+    @DisplayName("UserEntity 객체 생성 테스트")
+    void testUserEntityOf() {
+        // when
+        UserEntity user = UserEntity.of(name, email, account, level);
+
+        // then
+        assertNull(user.getId(), "새로 생성된 UserEntity의 id는 null이어야 한다.");
+        assertEquals(name, user.getName(), "이름이 일치해야 한다.");
+        assertEquals(email, user.getEmail(), "이메일이 일치해야 한다.");
+        assertEquals(account, user.getAccount(), "계좌 정보가 일치해야 한다.");
+        assertEquals(level, user.getLevel(), "레벨이 일치해야 한다.");
+    }
+
+}
diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
new file mode 100644
index 0000000..933af60
--- /dev/null
+++ b/src/test/resources/application.yml
@@ -0,0 +1,19 @@
+spring:
+  config:
+    activate:
+      on-profile: local
+  jpa:
+    hibernate:
+      ddl-auto: create-drop
+      format_sql: true
+  datasource:
+    driver-class-name: org.h2.Driver
+    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+    username: sa
+    password:
+  h2:
+    console:
+      enabled: true
+
+server:
+  port: 8080
\ No newline at end of file


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.

<details>
<summary>Files not reviewed (1)</summary>

* **deploy.sh**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/main/java/com/topay/user/domain/UserEntity.java:36**
* Duplicate all-args constructor detected alongside the @AllArgsConstructor annotation; consider removing one to prevent ambiguity.
```
public UserEntity(Long id, String name, String email, Account account, Level level) {
```
**src/main/java/com/topay/common/utils/encrytion/SHA256EncryptionService.java:18**
* Consider adding an explicit null check for the input 's' to avoid potential NullPointerExceptions instead of relying on exception handling for null values.
```
public String encrypt(String s) {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-05T02:08:31Z

Review comments:
diff_hunk: @@ -0,0 +1,59 @@
+package com.topay.common.utils.encrytion;
+
path: src/test/java/com/topay/common/utils/encrytion/SHA256EncryptionServiceTest.java
user: login: Copilot
body: The package name 'encrytion' appears to be misspelled; consider renaming it to 'encryption' to avoid confusion.
```suggestion
package com.topay.common.utils.encryption;
```
created_at: 2025-04-05T02:08:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,37 @@
+package com.topay.common.utils.encrytion;
+
path: src/main/java/com/topay/common/utils/encrytion/SHA256EncryptionService.java
user: login: Copilot
body: The package name 'encrytion' appears to be misspelled; consider renaming it to 'encryption'.
```suggestion
package com.topay.common.utils.encryption;
```
created_at: 2025-04-05T02:08:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,7 @@
+package com.topay.common.utils.encrytion;
+
path: src/main/java/com/topay/common/utils/encrytion/EncryptionService.java
user: login: Copilot
body: The package name 'encrytion' appears to be misspelled; consider renaming it to 'encryption'.
```suggestion
package com.topay.common.utils.encryption;
```
created_at: 2025-04-05T02:08:31Z
author_association: NONE

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-05T02:13:29Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=f-lab-edu_topay&pullRequest=9) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [16 New issues](https://sonarcloud.io/project/issues?id=f-lab-edu_topay&pullRequest=9&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=f-lab-edu_topay&pullRequest=9&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=f-lab-edu_topay&pullRequest=9&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=f-lab-edu_topay&pullRequest=9&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=f-lab-edu_topay&pullRequest=9&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=f-lab-edu_topay&pullRequest=9)


author_association: NONE