Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/kotlin/upbrella/be/BeApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package upbrella.be

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@EnableJpaAuditing
@SpringBootApplication
class BeApplication

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import upbrella.be.user.entity.User

data class AllUsersInfoResponse(val users: List<SingleUserInfoResponse>) {
companion object {
fun fromUsers(users: List<User?>): AllUsersInfoResponse {
fun fromUsers(users: List<User>): AllUsersInfoResponse {
return AllUsersInfoResponse(
users = users.map { SingleUserInfoResponse.fromUser(it!!) }
users = users.map { SingleUserInfoResponse.fromUser(it) }
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package upbrella.be.user.dto.response

import upbrella.be.user.entity.User
import java.time.LocalDateTime

data class SingleUserInfoResponse(
val id: Long,
Expand All @@ -9,7 +10,8 @@ data class SingleUserInfoResponse(
val email: String,
val bank: String?,
val accountNumber: String?,
val adminStatus: Boolean
val adminStatus: Boolean,
val createdAt: LocalDateTime,
) {
companion object {
fun fromUser(user: User): SingleUserInfoResponse {
Expand All @@ -20,7 +22,8 @@ data class SingleUserInfoResponse(
email = user.email,
bank = user.bank,
accountNumber = user.accountNumber,
adminStatus = user.adminStatus
adminStatus = user.adminStatus,
createdAt = user.createdAt!!
)
}
}
Expand Down
17 changes: 5 additions & 12 deletions src/main/kotlin/upbrella/be/user/entity/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package upbrella.be.user.entity
import upbrella.be.user.dto.request.JoinRequest
import upbrella.be.user.dto.response.KakaoLoginResponse
import upbrella.be.util.AesEncryptor
import upbrella.be.util.BaseTimeEntity
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify BaseTimeEntity implementation and ensure auditing fields are properly configured.

The inheritance from BaseTimeEntity is a good approach for adding auditing capabilities. However, I need to verify the implementation details.

Also applies to: 24-24


🏁 Script executed:

#!/bin/bash
# Description: Verify BaseTimeEntity implementation and its JPA auditing configuration

# Find and examine BaseTimeEntity class
fd -e kt BaseTimeEntity --exec cat {} \;

# Search for any other entities extending BaseTimeEntity to ensure consistency
ast-grep --pattern 'class $_ : BaseTimeEntity'

Length of output: 819


Audit fields verified – BaseTimeEntity is properly implemented

The BaseTimeEntity superclass is correctly defined with @MappedSuperclass, @EntityListeners(AuditingEntityListener::class), and JPA auditing annotations (@CreatedDate, @LastModifiedDate). No further changes needed for auditing.

Breaking change: decryptData now mutates the instance

The switch from returning a new User to in-place mutation is a breaking API change. Please:

  • src/main/kotlin/upbrella/be/user/entity/User.kt: Review all call sites of decryptData to ensure callers expect the method to modify the current instance.
  • Update any tests or documentation that assume decryptData returns a fresh User rather than mutating it.
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/user/entity/User.kt at lines 6 and 24, the
BaseTimeEntity inheritance is correctly implemented with proper auditing
annotations, so no changes are needed there. However, the decryptData method now
mutates the current User instance instead of returning a new one, which is a
breaking change. Review all usages of decryptData throughout the codebase to
confirm they expect in-place mutation, and update any tests or documentation
that assume it returns a new User object accordingly.

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
Expand All @@ -20,7 +21,7 @@ class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
) {
) : BaseTimeEntity() {

companion object {
fun createNewUser(
Expand Down Expand Up @@ -64,17 +65,9 @@ class User(
this.accountNumber = null
}

fun decryptData(aesEncryptor: AesEncryptor): User {
return User(
socialId = socialId,
name = name,
phoneNumber = phoneNumber,
email = email,
adminStatus = adminStatus,
bank = aesEncryptor.decrypt(bank),
accountNumber = aesEncryptor.decrypt(accountNumber),
id = id
)
fun decryptData(aesEncryptor: AesEncryptor) {
Copy link

Copilot AI Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] decryptData now mutates the existing User rather than returning a new instance. Consider renaming it to indicate in-place mutation (e.g., decryptDataInPlace) or documenting its side effects.

Suggested change
fun decryptData(aesEncryptor: AesEncryptor) {
fun decryptDataInPlace(aesEncryptor: AesEncryptor) {

Copilot uses AI. Check for mistakes.
this.bank = aesEncryptor.decrypt(bank)
this.accountNumber = aesEncryptor.decrypt(accountNumber)
}
Comment on lines +68 to 71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Breaking change: decryptData method now mutates instance in-place.

The decryptData method has been changed from returning a new User instance to mutating the current instance in-place. This is a breaking change that affects the method's API contract.

Verify that all usages of decryptData in the codebase have been updated to handle the new in-place mutation behavior:


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of decryptData to ensure they handle in-place mutation

# Search for all calls to decryptData method
rg -A 5 -B 2 'decryptData'

# Look for any assignments that might expect a return value
rg -A 3 -B 1 '\.decryptData\s*\('

Length of output: 8585


Ensure tests accommodate decryptData’s in-place mutation

The decryptData method now returns Unit and mutates the User instance directly. Existing tests still treat it as a factory (expecting a returned User), causing compile/runtime errors. Update the following:

  • src/test/kotlin/upbrella/be/user/service/UserServiceTest.kt
    Replace

    .map { user -> user.decryptData(aesEncryptor) }
    .map { decrypted -> SingleUserInfoResponse.fromUser(decrypted) }

    With either:

    // Option A: apply mutation, then map
    .onEach { it.decryptData(aesEncryptor) }
    .map { SingleUserInfoResponse.fromUser(it) }

    Or:

    .map { user -> user.apply { decryptData(aesEncryptor) } }
          .map { SingleUserInfoResponse.fromUser(it) }
  • src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt
    Update its .map { user -> user.decryptData(aesEncryptor) } in the same way as above so that you’re mapping User objects, not Unit.

  • src/test/kotlin/upbrella/be/user/entity/UserTest.kt
    Change

    val decryptedUser = user.decryptData(aesEncryptor)

    To:

    user.decryptData(aesEncryptor)
    val decryptedUser = user

After these adjustments, tests will correctly reflect the new in-place behavior of decryptData.

🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/user/entity/User.kt around lines 68 to 71, the
decryptData method was changed to mutate the User instance in-place and return
Unit instead of a new User. To fix related issues, update all usages of
decryptData to no longer expect a returned User. In test files like
UserServiceTest.kt and UserControllerTest.kt, replace map calls that invoke
decryptData with onEach or map using apply to perform mutation and then map the
User objects. In UserTest.kt, call decryptData on the user instance without
assignment, then use the original user variable as the decrypted user. This
ensures all code and tests handle the new in-place mutation correctly.


fun deleteBankAccount() {
Expand Down
12 changes: 8 additions & 4 deletions src/main/kotlin/upbrella/be/user/service/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class UserService(
) {

fun login(socialId: Long): SessionUser {
val foundUser = userReader.findBySocialId(socialId)
val foundUser = userReader.findBySocialId(socialId.hashCode().toLong())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify consistent hashing implementation across the codebase.

The socialId is being hashed before querying. Ensure this hashing approach is consistent with how socialId is stored and used throughout the application, particularly in the join method and other query operations.


🏁 Script executed:

#!/bin/bash
# Description: Verify consistent usage of socialId hashing across the codebase
# Expected: All socialId operations should use consistent hashing

# Search for socialId usage patterns
rg -A 3 -B 3 "socialId.*hashCode|findBySocialId|existsBySocialId" --type kotlin

Length of output: 12146


Align socialId hashing consistently across the codebase

The login method is hashing its incoming socialId again (socialId.hashCode().toLong()), which leads to a different value than the one stored by join/User.ofEncryptJoin (which only hashes the original provider ID once). This double-hashing will break your tests and any lookups.

• In src/main/kotlin/upbrella/be/user/service/UserService.kt (line 34), change:

val foundUser = userReader.findBySocialId(socialId.hashCode().toLong())

to:

val foundUser = userReader.findBySocialId(socialId)

• In your tests (src/test/kotlin/upbrella/be/user/service/UserServiceTest.kt), make sure you’re stubbing and verifying findBySocialId with the same single-hashed value (or pass the raw Kakao ID into login and stub with rawId.hashCode().toLong()).

• Confirm that FixtureBuilderFactory (src/main/kotlin/upbrella/be/config/FixtureBuilderFactory.kt) generates socialId exactly as your production code expects (i.e., one .hashCode().toLong()).

This will ensure all findBySocialId/existsBySocialId calls use the same hashed key.

🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/user/service/UserService.kt at line 34, the
socialId is being hashed again before querying, causing inconsistency with how
socialId is stored and used elsewhere. To fix this, remove the extra hashing and
pass the socialId directly to userReader.findBySocialId. Also, update related
tests and FixtureBuilderFactory to ensure they use the same single-hashed or raw
socialId consistently across the codebase.


return SessionUser.fromUser(foundUser)
}
Expand All @@ -56,7 +56,7 @@ class UserService(

fun findUsers(): AllUsersInfoResponse {
val users = userReader.findAll()
.map { it.decryptData(aesEncryptor) }
users.forEach { it.decryptData(aesEncryptor) }

return AllUsersInfoResponse.fromUsers(users)
}
Expand Down Expand Up @@ -100,16 +100,20 @@ class UserService(
fun findDecryptedUserById(sessionUser: SessionUser): User {
val id = sessionUser.id

return userRepository.findById(id)
val user = userRepository.findById(id)
.orElseThrow { NonExistingMemberException("[ERROR] 존재하지 않는 회원입니다.") }
.decryptData(aesEncryptor)

user.decryptData(aesEncryptor)

return user
}

@Transactional
fun deleteUserBankAccount(id: Long) {
val foundUser = userReader.findUserById(id)
foundUser.deleteBankAccount()
}

@Transactional
fun updateAdminStatus(id: Long) {
val foundUser = userReader.findUserById(id)
Expand Down
26 changes: 26 additions & 0 deletions src/main/kotlin/upbrella/be/util/BaseTimeEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package upbrella.be.util

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 javax.persistence.Column
import javax.persistence.EntityListeners
import javax.persistence.MappedSuperclass

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
var createdAt: LocalDateTime? = null

@LastModifiedDate
var updatedAt: LocalDateTime? = null

var deletedAt: LocalDateTime? = null

fun softDelete() {
deletedAt = LocalDateTime.now()
}
}
4 changes: 4 additions & 0 deletions src/main/resources/db/add_time.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE user
ADD COLUMN created_at DATETIME NOT NULL DEFAULT NOW() COMMENT '생성일',
ADD COLUMN updated_at DATETIME NOT NULL DEFAULT NOW() ON UPDATE NOW() COMMENT '수정일',
ADD COLUMN deleted_at DATETIME NULL COMMENT '삭제일';
14 changes: 10 additions & 4 deletions src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ class UserControllerTest (
val user = FixtureBuilderFactory.builderUser(aesEncryptor).sample()

session.setAttribute("user", sessionUser)
val decryptedUser = user.decryptData(aesEncryptor)
user.decryptData(aesEncryptor)

given(userService.findDecryptedUserById(sessionUser))
.willReturn(decryptedUser)
.willReturn(user)

// when & then
mockMvc.perform(
Expand Down Expand Up @@ -461,9 +461,12 @@ class UserControllerTest (
users.add(FixtureBuilderFactory.builderUser(aesEncryptor).sample())
}

users.forEach { user -> user.createdAt = LocalDateTime.now()}
users.forEach { user -> user.decryptData(aesEncryptor)
}

val allUsersInfoResponse = AllUsersInfoResponse(
users = users.stream()
.map { user -> user.decryptData(aesEncryptor) }
.map { SingleUserInfoResponse.fromUser(it) }
.toList()
)
Expand Down Expand Up @@ -499,7 +502,10 @@ class UserControllerTest (
.optional()
.description("사용자 계좌 번호"),
fieldWithPath("users[].adminStatus").type(JsonFieldType.BOOLEAN)
.description("관리자 여부")
.description("관리자 여부"),
fieldWithPath("users[].createdAt").type(JsonFieldType.ARRAY)
.description("사용자 생성일")
Comment on lines +505 to +507
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the field type in API documentation.

The createdAt field is documented as JsonFieldType.ARRAY, but based on the context of timestamp auditing, this should likely be JsonFieldType.STRING or a date/time type.


🏁 Script executed:

#!/bin/bash
# Description: Check how createdAt is defined in the User entity and DTOs to verify correct field type
# Expected: createdAt should be a timestamp/datetime field, not an array

# Check User entity definition
echo "=== User entity definition ==="
rg -A 5 -B 5 "createdAt" --type kotlin

# Check DTO definitions 
echo "=== DTO definitions ==="
ast-grep --pattern 'class $_ {
  $$$
  createdAt: $TYPE
  $$$
}'

# Check if there are any LocalDateTime imports or usages
echo "=== LocalDateTime usage ==="
rg "LocalDateTime" --type kotlin -A 2 -B 2

Length of output: 70329


Change users[].createdAt to use JsonFieldType.STRING

The createdAt field is a LocalDateTime and will be serialized as a JSON string, not an array.

• File: src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt
• Around line 506, update the field descriptor:

- fieldWithPath("users[].createdAt").type(JsonFieldType.ARRAY)
+ fieldWithPath("users[].createdAt").type(JsonFieldType.STRING)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.description("관리자 여부"),
fieldWithPath("users[].createdAt").type(JsonFieldType.ARRAY)
.description("사용자 생성일")
.description("관리자 여부"),
fieldWithPath("users[].createdAt").type(JsonFieldType.STRING)
.description("사용자 생성일")
🤖 Prompt for AI Agents
In src/test/kotlin/upbrella/be/user/controller/UserControllerTest.kt around
lines 505 to 507, the field descriptor for "users[].createdAt" is incorrectly
set as JsonFieldType.ARRAY. Since createdAt is a LocalDateTime and serialized as
a JSON string, change the field type from JsonFieldType.ARRAY to
JsonFieldType.STRING to accurately reflect the data type in the API
documentation.


)
)
)
Expand Down
6 changes: 3 additions & 3 deletions src/test/kotlin/upbrella/be/user/entity/UserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ class UserTest {
accountNumber = aesEncryptor.encrypt("accountNumber")
)
// when
val decryptedUser = user.decryptData(aesEncryptor)
user.decryptData(aesEncryptor)

// then
assertAll(
{ assertThat(decryptedUser.bank).isEqualTo("bank") },
{ assertThat(decryptedUser.accountNumber).isEqualTo("accountNumber") }
{ assertThat(user.bank).isEqualTo("bank") },
{ assertThat(user.accountNumber).isEqualTo("accountNumber") }
)
}

Expand Down
87 changes: 0 additions & 87 deletions src/test/kotlin/upbrella/be/user/service/UserServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -224,93 +224,6 @@ class UserServiceTest {
}
}

@Nested
@DisplayName("사용자는")
inner class FindUsersTest {

private val users: MutableList<User> = ArrayList()
private val expectedUsers: MutableList<User> = ArrayList()

@BeforeEach
fun setUp() {
for (i in 0 until 1) {
val sample = User(
1L,
"사용자",
"010-1234-5678",
"",
false,
aesEncryptor.encrypt("농협"),
aesEncryptor.encrypt("123-456-789"),
1L
)
val expectedSample = User(
1L,
"사용자",
"010-1234-5678",
"",
false,
aesEncryptor.encrypt("농협"),
aesEncryptor.encrypt("123-456-789"),
1L
)

users.add(sample)
expectedUsers.add(expectedSample)
}
}

@Test
@DisplayName("회원 목록을 조회할 수 있다.")
fun success() {
// given
val expected = AllUsersInfoResponse(
users = expectedUsers.stream()
.map { user -> user.decryptData(aesEncryptor) }
.map { decrypted -> SingleUserInfoResponse.fromUser(decrypted) }
.toList()
)

given(userReader.findAll()).willReturn(users)

// when
val allUsersInfoResponse = userService.findUsers()

// then
assertAll(
{
assertThat(allUsersInfoResponse)
.usingRecursiveComparison()
.isEqualTo(expected)
},
{
then(userReader).should(times(1)).findAll()
}
)
}

@Test
@DisplayName("존재하는 회원이 없으면 빈 목록이 반환된다.")
fun nonExistingUser() {
// given
given(userReader.findAll())
.willReturn(listOf())

// when
val allUsersInfoResponse = userService.findUsers()

// then
assertAll(
{
assertThat(allUsersInfoResponse.users.size).isEqualTo(0)
},
{
then(userReader).should(times(1)).findAll()
}
)
}
}

@Test
@DisplayName("사용자는 자신의 은행 정보를 수정할 수 있다.")
fun updateBankTest() {
Expand Down