Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9d77953
delete :: deviceToken 필드 삭제#629
1210thatman Jan 19, 2026
e8e597a
add :: entity 및 mapper 추가#629
1210thatman Jan 19, 2026
2f604d7
add :: entity 및 mapper 추가#629
1210thatman Jan 19, 2026
a99c966
add :: deviceTokenRepository#629
1210thatman Jan 19, 2026
b1276a6
add :: adminDeviceTokenPort추가#629
1210thatman Jan 19, 2026
487a460
add :: AdminDeviceTokenAdapter#629
1210thatman Jan 19, 2026
a8fe51b
modify: domain에 있던 deviceToken 로직을 adapter로 이전
1210thatman Feb 3, 2026
2e1ff88
add: 필요한 port 설계 및 구현
1210thatman Feb 3, 2026
01cf305
add: UserDeviceTokenPersistencePort
1210thatman Feb 3, 2026
6b203d3
chore: 필요없는 로직 삭제
1210thatman Feb 3, 2026
061b8e6
add: AdminSignUpService 수정
1210thatman Feb 5, 2026
1eeb05f
modify: ChangeApplicationStatusService의 deviceToken관련 로직 수정#629
1210thatman Feb 5, 2026
102b2db
modify: ChangeEarlyReturnStatusService에서 deviceToken 관련 로직 수정#629
1210thatman Feb 5, 2026
5c0d4d1
modify: CreateEarlyReturnService에서 deviceToken 관련 로직 수정#629
1210thatman Feb 6, 2026
c80ecca
modify: NotificationWeekendMealService에서 deviceToken 관련 로직 수정#629
1210thatman Feb 6, 2026
d5d87c5
modify: SendNotificationSelfStudyTeacher에서 deviceToken 관련 로직 수정#629
1210thatman Feb 6, 2026
6a381d8
modify: UserLoginService에서 deviceToken 관련 로직 수정#629
1210thatman Feb 6, 2026
d81290a
modify: UserMapper에서 deviceToken 관련 로직 수정#629
1210thatman Feb 6, 2026
205ad1e
modify: UserSignUpService에서 deviceToken 관련 로직 수정#629
1210thatman Feb 6, 2026
aa1d5c4
modify: osType을 nullable하게 변경
1210thatman Feb 7, 2026
966b13b
modify: UUID 생성 로직 추가
1210thatman Feb 7, 2026
f64d872
modify: 중복 deviceToken 제거 로직 추가
1210thatman Feb 7, 2026
1b65d2e
Merge branch 'develop' into 629-devicetoken을-별도-테이블로-분리
1210thatman Feb 7, 2026
f97d122
chore: lint 해결
1210thatman Feb 7, 2026
ea5a484
Merge branch '629-devicetoken을-별도-테이블로-분리' of https://github.com/DSM-…
1210thatman Feb 7, 2026
4891799
feat: 문법 오류 수정
1210thatman Feb 7, 2026
6ee01da
feat: 중첩된 let문 간소화#629
1210thatman Feb 19, 2026
8877904
feat: try-catch문 적용#629
1210thatman Feb 19, 2026
1a08fcb
feat: N+1 문제 해결#629
1210thatman Feb 19, 2026
6c9759f
feat: git rebase를 abort
1210thatman Feb 20, 2026
b923f90
feat: deviceToken 컬럼에 unique constraint 추가#629
1210thatman Feb 20, 2026
24f571f
Merge branch 'develop' into 629-devicetoken을-별도-테이블로-분리
1210thatman Feb 20, 2026
615a953
chore: 마지막 줄 밑줄 추가
1210thatman Feb 20, 2026
d854e10
feat :: os Type을 not null하게 수정
1210thatman Mar 17, 2026
728e5d0
feat :: id 업데이트 로직 수정
1210thatman Mar 17, 2026
9c6686d
modify :: 토큰 소유자 변경 정책을 adapter에서 service로 이동
1210thatman Mar 18, 2026
0141290
naming :: update 메서드 이름 수정
1210thatman Mar 18, 2026
cb0c70d
add :: deviceTokenTable 쿼리 작성
1210thatman Mar 19, 2026
7f5408c
err :: weekend_meal 로유 수정
1210thatman Mar 20, 2026
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
8 changes: 1 addition & 7 deletions src/main/kotlin/dsm/pick2024/domain/admin/domain/Admin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ data class Admin(
val grade: Int?,
val classNum: Int?,
val password: String,
val deviceToken: String?,
val adminId: String,
val role: Role
) {

fun updateDeviceToken(deviceToken: String?): Admin {
return this.copy(deviceToken = deviceToken)
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ class AdminJpaEntity(
@Column(name = "class_num", columnDefinition = "int(4)")
val classNum: Int? = null,

@Column(name = "device_token", nullable = true)
val deviceToken: String? = null,

@Enumerated(value = EnumType.STRING)
@Column(nullable = false)
val role: Role
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class AdminMapper : GenericMapper<AdminJpaEntity, Admin> {
grade = grade,
classNum = classNum,
adminId = adminId,
role = role,
deviceToken = deviceToken
role = role
)
}

Expand All @@ -29,8 +28,7 @@ class AdminMapper : GenericMapper<AdminJpaEntity, Admin> {
grade = grade,
classNum = classNum,
adminId = adminId,
role = role,
deviceToken = deviceToken
role = role
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package dsm.pick2024.domain.admin.presentation.dto.request

import dsm.pick2024.domain.devicetoken.enum.OSType

data class AdminLoginRequest(
val adminId: String,
val password: String,
val deviceToken: String?
val deviceToken: String?,
val os: OSType?
Copy link
Member

Choose a reason for hiding this comment

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

os를 nullable type으로 설정하신 이유가 있을까요? 저는 nullable이 아니어도 괜찮을 것 같습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

os의 경우 시뮬레이터에서 값을 넘겨줄 수 없다고 생각해서 저렇게 했던 것 같습니다.
수정하겠습니다

)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dsm.pick2024.domain.admin.presentation.dto.request

import dsm.pick2024.domain.devicetoken.enum.OSType
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size

Expand All @@ -21,5 +22,7 @@ data class AdminSignUpRequest(
val deviceToken: String,
@field:NotBlank
@field:Size(min = 6, max = 6)
val secretKey: String
val secretKey: String,
@field:NotBlank
val os: OSType
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package dsm.pick2024.domain.admin.service

import dsm.pick2024.domain.admin.port.`in`.AdminFinderUseCase
import dsm.pick2024.domain.admin.port.`in`.AdminLoginUseCase
import dsm.pick2024.domain.admin.port.out.AdminSavePort
import dsm.pick2024.domain.admin.presentation.dto.request.AdminLoginRequest
import dsm.pick2024.domain.devicetoken.domain.AdminDeviceToken
import dsm.pick2024.domain.devicetoken.port.out.AdminDeviceTokenPort
import dsm.pick2024.domain.user.exception.PasswordMissMatchException
import dsm.pick2024.global.security.jwt.JwtTokenProvider
import dsm.pick2024.global.security.jwt.dto.TokenResponse
Expand All @@ -18,7 +19,7 @@ class AdminLoginService(
private val adminFinderUseCase: AdminFinderUseCase,
private val passwordEncoder: PasswordEncoder,
private val jwtTokenProvider: JwtTokenProvider,
private val adminSavePort: AdminSavePort
private val adminDeviceTokenPort: AdminDeviceTokenPort
) : AdminLoginUseCase {

@Transactional
Expand All @@ -29,8 +30,14 @@ class AdminLoginService(
throw PasswordMissMatchException
}

if (request.deviceToken != null || admin.deviceToken != "") {
adminSavePort.save(admin.updateDeviceToken(request.deviceToken))
request.deviceToken?.let { token ->
adminDeviceTokenPort.save(
AdminDeviceToken(
adminId = admin.id,
deviceToken = token,
os = request.os
)
)
}

return jwtTokenProvider.generateToken(request.adminId, admin.role.name)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dsm.pick2024.domain.admin.service

import dsm.pick2024.domain.devicetoken.domain.AdminDeviceToken
import dsm.pick2024.domain.admin.domain.Admin
import dsm.pick2024.domain.admin.exception.RegisteredClassAndGrade
import dsm.pick2024.domain.admin.exception.SecretKeyMissMatchException
Expand All @@ -9,6 +10,7 @@ import dsm.pick2024.domain.admin.port.out.ExistsByAdminIdPort
import dsm.pick2024.domain.admin.port.out.QueryAdminPort
import dsm.pick2024.domain.admin.presentation.dto.request.AdminSignUpRequest
import dsm.pick2024.domain.admin.properties.AdminProperties
import dsm.pick2024.domain.devicetoken.port.out.AdminDeviceTokenPort
import dsm.pick2024.domain.mail.port.`in`.VerifyMailUseCase
import dsm.pick2024.domain.user.entity.enums.Role
import dsm.pick2024.domain.user.exception.DuplicateUserException
Expand All @@ -25,21 +27,29 @@ class AdminSignUpService(
private val jwtTokenProvider: JwtTokenProvider,
private val adminSavePort: AdminSavePort,
private val adminProperties: AdminProperties,
private val queryAdminPort: QueryAdminPort
private val queryAdminPort: QueryAdminPort,
private val adminDeviceTokenPort: AdminDeviceTokenPort
) : AdminSignUpUseCase {
override fun execute(request: AdminSignUpRequest): TokenResponse {
val encodedPassword = passwordEncoder.encode(request.password)

checkDuplicateAccountId(request.accountId)

checkSecretKey(request.secretKey)

checkRegisteredGradeAndClass(request.grade, request.classNum)

verifyMailUseCase.verifyAndConsume(request.code, request.accountId)

val user = request.toEntity(encodedPassword)
adminSavePort.save(user)
val admin = request.toEntity(encodedPassword)
val savedAdmin = adminSavePort.save(admin)

request.deviceToken?.let { token ->
adminDeviceTokenPort.save(
AdminDeviceToken(
adminId = savedAdmin.id,
deviceToken = token,
os = request.os
)
)
}

return jwtTokenProvider.generateToken(request.accountId, Role.SCH.name)
}
Expand Down Expand Up @@ -72,8 +82,7 @@ class AdminSignUpService(
name = this.name,
grade = this.grade,
classNum = this.classNum,
role = Role.SCH,
deviceToken = this.deviceToken
role = Role.SCH
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dsm.pick2024.domain.application.port.`in`.ApplicationUseCase
import dsm.pick2024.domain.application.port.out.ExistsApplicationPort
import dsm.pick2024.domain.application.port.out.SaveApplicationPort
import dsm.pick2024.domain.application.presentation.dto.request.ApplicationRequest
import dsm.pick2024.domain.devicetoken.port.out.QueryAdminDeviceTokenPort
import dsm.pick2024.domain.fcm.dto.request.FcmRequest
import dsm.pick2024.domain.main.port.`in`.MainUseCase
import dsm.pick2024.domain.outbox.domain.Outbox
Expand All @@ -27,7 +28,8 @@ class ApplicationService(
private val userFacadeUseCase: UserFacadeUseCase,
private val adminFinderUseCase: AdminFinderUseCase,
private val saveOutboxPort: SaveOutboxPort,
private val mainUseCase: MainUseCase
private val mainUseCase: MainUseCase,
private val queryAdminDeviceTokenPort: QueryAdminDeviceTokenPort
) : ApplicationUseCase {

@Transactional
Expand All @@ -54,21 +56,26 @@ class ApplicationService(
)
)

val deviceToken = adminFinderUseCase.findByGradeAndClassNumOrThrow(
val admin = adminFinderUseCase.findByGradeAndClassNumOrThrow(
grade = user.grade,
classNum = user.classNum
).deviceToken
)

val tokens = queryAdminDeviceTokenPort.findAllByAdminId(admin.id)
.map { it.deviceToken }

saveOutboxPort.saveOutbox(
Outbox(
payload = FcmRequest(
tokens = listOf(deviceToken),
title = "[PiCK] ${user.grade}학년 ${user.classNum}반 ${user.num}번 ${user.name} 학생이 외출을 신청했습니다.",
body = "사유: ${request.reason}"
),
eventType = EventType.NOTIFICATION
if (tokens.isNotEmpty()) {
saveOutboxPort.saveOutbox(
Outbox(
payload = FcmRequest(
tokens = tokens,
title = "[PiCK] ${user.grade}학년 ${user.classNum}반 ${user.num}번 ${user.name} 학생이 외출을 신청했습니다.",
body = "사유: ${request.reason}"
),
eventType = EventType.NOTIFICATION
)
)
)
}

mainUseCase.sendEvent(user.id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import dsm.pick2024.domain.application.port.`in`.ChangeApplicationStatusUseCase
import dsm.pick2024.domain.application.presentation.dto.request.ApplicationStatusRequest
import dsm.pick2024.domain.application.service.processor.ApplicationApprovalProcessor
import dsm.pick2024.domain.application.service.processor.ApplicationRejectionProcessor
import dsm.pick2024.domain.devicetoken.port.out.QueryUserDeviceTokenPort
import dsm.pick2024.domain.user.port.`in`.UserFacadeUseCase
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -18,7 +19,8 @@ class ChangeApplicationStatusService(
private val userFacadeUseCase: UserFacadeUseCase,
private val applicationFinderUseCase: ApplicationFinderUseCase,
private val approvalProcessor: ApplicationApprovalProcessor,
private val rejectionProcessor: ApplicationRejectionProcessor
private val rejectionProcessor: ApplicationRejectionProcessor,
private val queryUserDeviceTokenPort: QueryUserDeviceTokenPort
) : ChangeApplicationStatusUseCase {

@Transactional
Expand All @@ -28,10 +30,9 @@ class ChangeApplicationStatusService(
applications.forEach {
if (it.status != Status.QUIET) throw AlreadyApplyingForPicnicException
}
val deviceTokens = applications.mapNotNull {
userFacadeUseCase.getUserById(
it.userId
).deviceToken
val deviceTokens = applications.flatMap { application ->
queryUserDeviceTokenPort.findAllByUserId(application.userId)
.map { it.deviceToken }
}
if (request.status == Status.OK) {
approvalProcessor.process(applications, admin.name, deviceTokens)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dsm.pick2024.domain.devicetoken.domain

import dsm.pick2024.domain.devicetoken.enum.OSType
import dsm.pick2024.global.annotation.Aggregate
import java.util.UUID

@Aggregate
data class AdminDeviceToken(
val id: UUID = UUID(0, 0),
val adminId: UUID,
val deviceToken: String,
val os: OSType
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for AdminDeviceTokenJpaEntity file
fd "AdminDeviceTokenJpaEntity" --type f

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 152


🏁 Script executed:

# If not found by exact name, search for files with AdminDeviceToken pattern
fd "AdminDeviceToken" --type f | grep -i "jpa\|entity\|persist"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 351


🏁 Script executed:

# Search for any constraint-related annotations in the domain/devicetoken directory
find . -path "*devicetoken*" -type f -name "*.kt" | head -20

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 1623


🏁 Script executed:

# Search for unique constraint definitions in the entire codebase
rg "@UniqueConstraint|@Column.*unique" --type kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 95


🏁 Script executed:

# Read the AdminDeviceTokenJpaEntity file to check for constraints
cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/entity/AdminDeviceTokenJpaEntity.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 902


🏁 Script executed:

# Search for unique constraint definitions across the codebase
rg "@UniqueConstraint|@Column.*unique" --type-list | head -5

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 458


🏁 Script executed:

# Try searching with a simpler pattern
rg "UniqueConstraint|unique.*=.*true"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 926


🏁 Script executed:

# Look for database migration or schema files
find . -path "*migration*" -o -path "*flyway*" -o -path "*liquibase*" | grep -i "devicetoken\|admin"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 130


🏁 Script executed:

# Read the migration file that contains admin entity mapping
cat -n src/main/resources/db/migration/V2_entity_mapping_admin.sql

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 725


🏁 Script executed:

# Search for the admin device token table definition in migration files
rg "tbl_admin_device_token" -A 10 -B 2

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 2851


🏁 Script executed:

# Check if there are any other migration files that might contain device token constraints
find src/main/resources/db -name "*.sql" -type f | xargs grep -l "device_token\|admin_device" 2>/dev/null

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 53


🏁 Script executed:

# Search for all SQL migration files
find src/main/resources/db -name "*.sql" -type f

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 255


🏁 Script executed:

# Check the content of all migration files related to device tokens
find src/main/resources/db -name "*.sql" -type f -exec grep -l "device_token\|admin_device\|user_device" {} \;

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Get a list of all migration files
ls -la src/main/resources/db/migration/

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 418


🏁 Script executed:

# Check content of all migration files
cat -n src/main/resources/db/migration/V2_entity_mapping_admin.sql

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 725


🏁 Script executed:

cat -n src/main/resources/db/migration/V2_entity_mapping_user.sql

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 1825


🏁 Script executed:

cat -n src/main/resources/db/migration/V2_add_move_column_to_classroom.sql

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 189


🏁 Script executed:

# Search for any reference to device token or unique constraints in the entire codebase
rg "tbl_admin_device_token|tbl_user_device_token"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 317


🏁 Script executed:

# Check if there are any application.yml or application.properties files that might configure schema
find . -name "application*.yml" -o -name "application*.properties" | xargs grep -l "jpa\|hibernate" 2>/dev/null | head -5

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Read the UserDeviceTokenJpaEntity to check the entity name conflict
cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/entity/UserDeviceTokenJpaEntity.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 889


🏁 Script executed:

# Search for references to these entities in repositories
cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/persistence/repository/AdminDeviceTokenRepository.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 596


🏁 Script executed:

# Check if there are initial schema files that might define the device token tables
find src -name "schema*.sql" -o -name "init*.sql" | head -10

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Search for any Flyway or Hibernate schema generation configuration
rg "spring.jpa.hibernate.ddl-auto|spring.jpa.show-sql|spring.flyway"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Check UserDeviceTokenRepository to understand the full scope of the issue
cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/persistence/repository/UserDeviceTokenRepository.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 589


🏁 Script executed:

# Search for any CREATE TABLE statements in migration files or elsewhere
rg "CREATE TABLE.*device_token"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Check if there's a V1 migration or initial schema
find src -name "V1*.sql" -o -name "schema.sql" -o -name "init.sql" 2>/dev/null

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Check application configuration files
find src/main/resources -name "application*.yml" -o -name "application*.properties"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Look for any configuration that might show schema/ddl settings
cat src/main/resources/application.yml 2>/dev/null || cat src/main/resources/application.properties 2>/dev/null | head -50

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Search for any unique constraint patterns in the JPA entities
rg "@Table|@UniqueConstraint"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 165


🏁 Script executed:

# Check if there's a V1 migration file or any file that creates these tables
find src/main/resources/db -type f | sort

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 255


🏁 Script executed:

# Look at BaseUUIDEntity to understand the inheritance
cat -n src/main/kotlin/dsm/pick2024/global/base/BaseUUIDEntity.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 787


🏁 Script executed:

# Search for any uniqueness validation logic in service or adapter classes
rg "findByDeviceToken|duplicate|unique" --type-list

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 458


🏁 Script executed:

# Simpler search for uniqueness logic
rg "findByDeviceToken|duplicate|unique"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 1632


🏁 Script executed:

# Check the persistence adapter to understand how uniqueness is handled
cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/persistence/AdminDeviceTokenPersistenceAdapter.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 1945


🏁 Script executed:

# Check UserDeviceTokenPersistenceAdapter as well
cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/persistence/UserDeviceTokenPersistenceAdapter.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 1907


🏁 Script executed:

# Confirm the critical entity name duplication and check if Hibernate schema auto-generation is enabled
find src/main/resources -name "*.yml" -o -name "*.properties" | xargs cat 2>/dev/null | grep -A 5 -B 5 "hibernate\|jpa\|ddl"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

# Let's look for application configuration in META-INF or other locations
find . -name "*.yml" -o -name "*.properties" -o -name "*.yaml" 2>/dev/null | head -20

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 366


🏁 Script executed:

# Check what exists in resources directory
ls -la src/main/resources/ 2>/dev/null

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 355


🏁 Script executed:

# Check the application.yaml for JPA and Hibernate settings
cat -n src/main/resources/application.yaml

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 5047


엔티티 이름 충돌 오류 및 DB 제약조건 부재 수정 필수

중대한 문제를 발견했습니다:

  1. 엔티티 이름 충돌 (Critical): UserDeviceTokenJpaEntityAdminDeviceTokenJpaEntity 모두 @Entity(name = "tbl_admin_device_token")를 사용 중입니다. 두 엔티티가 동일한 테이블 이름을 가지면 런타임 오류 또는 데이터 혼합이 발생합니다. 각 엔티티에 고유한 테이블 이름을 부여해야 합니다.

  2. DB 레벨 유니크 제약조건 부재: 마이그레이션 파일에서 device token 테이블이 생성되지 않으며, 엔티티에도 @UniqueConstraintunique=true 설정이 없습니다. ddl-auto: none 설정으로 인해 Hibernate도 자동 생성하지 않습니다.

  3. 불충분한 애플리케이션 레벨 중복 방지: findByDeviceToken(token)은 토큰만으로 조회하며 adminId 검증을 포함하지 않아 (adminId, deviceToken) 조합의 중복을 방지할 수 없습니다. 또한 동시성 환경에서 레이스 컨디션에 취약합니다.

마이그레이션 파일에서 device token 테이블을 명시적으로 생성하고, 각 엔티티에 적절한 유니크 제약조건을 추가하세요.

🤖 Prompt for AI Agents
In `@src/main/kotlin/dsm/pick2024/domain/devicetoken/domain/AdminDeviceToken.kt`
around lines 7 - 13, Two fixes: give each JPA entity a unique table name to
avoid collision and add a DB-level unique constraint for (adminId, deviceToken),
and change repository lookup to include adminId to avoid race conditions. Update
AdminDeviceTokenJpaEntity and UserDeviceTokenJpaEntity to use distinct
`@Entity`(name=...) / `@Table`(name=..., uniqueConstraints=...) values and add a
UniqueConstraint for ("admin_id","device_token"); add or change repository
method findByAdminIdAndDeviceToken(...) (or update usages of findByDeviceToken
to pass adminId) and handle DataIntegrityViolationException on insert to map DB
uniqueness violations. Also add a migration SQL that explicitly creates the
device token table with the same unique index/constraint so ddl-auto:none
deployments are safe.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dsm.pick2024.domain.devicetoken.domain

import dsm.pick2024.domain.devicetoken.enum.OSType
import dsm.pick2024.global.annotation.Aggregate
import java.util.UUID

@Aggregate
data class UserDeviceToken(
val id: UUID = UUID(0, 0),
val userId: UUID,
val deviceToken: String,
val os: OSType
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dsm.pick2024.domain.devicetoken.entity

import dsm.pick2024.domain.devicetoken.enum.OSType
import dsm.pick2024.global.base.BaseUUIDEntity
import java.util.UUID
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated

@Entity(name = "tbl_admin_device_token")
class AdminDeviceTokenJpaEntity(
id: UUID,

@Column(name = "admin_id", nullable = false)
val adminId: UUID,

@Column(name = "device_token", nullable = false)
val deviceToken: String,

@Enumerated(EnumType.STRING)
@Column(name = "os", nullable = false)
val os: OSType
) : BaseUUIDEntity(id)
Comment on lines +12 to +24
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find the relevant files
echo "=== Finding AdminDeviceToken related files ==="
fd "AdminDeviceToken" --type f --extension kt

echo "=== Checking for BaseUUIDEntity to understand parent class ==="
fd "BaseUUIDEntity" --type f --extension kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 928


🏁 Script executed:

#!/bin/bash
# Check the main entity file
echo "=== AdminDeviceTokenJpaEntity content ==="
find . -name "AdminDeviceTokenJpaEntity.kt" -type f -exec cat {} \;

echo -e "\n=== Looking for unique constraints or database configuration ==="
# Search for `@Table` or `@Index` annotations related to AdminDeviceToken
rg -n "@Table|@Index|@UniqueConstraint" --type=kt | rg -i "admindevicetoken|admin_device"

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 886


🏁 Script executed:

#!/bin/bash
# Search for repository and persistence adapter
echo "=== AdminDeviceTokenRepository ==="
find . -name "*AdminDeviceToken*Repository*" -type f -exec cat {} \;

echo -e "\n=== AdminDeviceTokenPersistenceAdapter ==="
find . -name "*AdminDeviceToken*Adapter*" -type f -exec cat {} \;

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 2202


🏁 Script executed:

#!/bin/bash
# Search for SaveAdminDeviceTokenPort and related ports
echo "=== Port interfaces ==="
find . -name "*AdminDeviceToken*Port*" -type f -exec cat {} \;

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 692


🏁 Script executed:

#!/bin/bash
# Search for token saving logic in login/authentication context
echo "=== Login service or use case handling token save ==="
rg -n "saveAdminDeviceToken|AdminDeviceToken|deviceToken" --type=kt -C 3 | head -100

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 166


🏁 Script executed:

#!/bin/bash
cat $(find . -name "AdminDeviceTokenMapper.kt" -type f)

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 1387


🏁 Script executed:

#!/bin/bash
# Also check the domain model
cat $(find . -name "AdminDeviceToken.kt" -path "*/domain/*" -type f)

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 382


🏁 Script executed:

#!/bin/bash
# Check for any login/authentication code that saves tokens
rg -l "save.*AdminDeviceToken\|AdminDeviceTokenPort" --type=kotlin

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


(adminId, deviceToken) 조합에 대한 유니크 제약 조건 누락 및 데이터 무결성 문제

현재 구현에는 심각한 결함이 있습니다:

  1. AdminDeviceTokenRepository.findByDeviceToken(token)adminId를 고려하지 않고 검색하며, resolveEntity() 메서드에서 이를 이용해 기존 엔티티를 찾습니다. 결과적으로 관리자 A가 저장한 토큰을 관리자 B가 저장하려 하면, 관리자 A의 레코드를 조회하게 되어 다른 관리자의 토큰을 덮어쓸 위험이 있습니다.

  2. AdminDeviceTokenMapper.updateEntity()는 입력받은 domain 파라미터를 전혀 사용하지 않고 기존 엔티티를 그대로 반환하므로, 실제로는 업데이트가 이루어지지 않습니다.

  3. 데이터베이스 및 엔티티 레벨에서 (adminId, deviceToken) 조합에 대한 유니크 제약이 없습니다.

필수 수정 사항:

  • Repository에 findByAdminIdAndDeviceToken(adminId: UUID, deviceToken: String) 메서드 추가
  • 엔티티에 @Table(uniqueConstraints = [...]) 또는 @Index 추가
  • updateEntity()에서 새로운 domain 데이터를 실제로 적용
🤖 Prompt for AI Agents
In
`@src/main/kotlin/dsm/pick2024/domain/devicetoken/entity/AdminDeviceTokenJpaEntity.kt`
around lines 12 - 24, Add a unique constraint on the AdminDeviceTokenJpaEntity
for the (adminId, deviceToken) pair (e.g., via `@Table`(uniqueConstraints = ...))
and/or an index to enforce DB-level uniqueness; add a repository method
findByAdminIdAndDeviceToken(adminId: UUID, deviceToken: String) to replace the
existing findByDeviceToken usage; update resolveEntity() to call
findByAdminIdAndDeviceToken(...) so lookups consider adminId; and fix
AdminDeviceTokenMapper.updateEntity(entity, domain) to actually copy fields from
the incoming domain (deviceToken, os, etc.) into the entity before returning so
updates are applied.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dsm.pick2024.domain.devicetoken.entity

import dsm.pick2024.domain.devicetoken.enum.OSType
import dsm.pick2024.global.base.BaseUUIDEntity
import java.util.UUID
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated

@Entity(name = "tbl_user_device_token")
class UserDeviceTokenJpaEntity(
id: UUID,

@Column(name = "user_id", nullable = false)
val userId: UUID,

@Column(name = "device_token")
val deviceToken: String,

@Enumerated(EnumType.STRING)
@Column(name = "os", nullable = false)
val os: OSType
Comment on lines +21 to +23
Copy link

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd src/main/kotlin/dsm/pick2024/domain/devicetoken && find . -name "*.kt" -type f

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 759


🏁 Script executed:

rg -n "nullable\s*=\s*(true|false)" --type=kotlin -B5 -A1 src/main/kotlin/dsm/pick2024/domain/devicetoken/

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 3153


🏁 Script executed:

rg -n "class.*DeviceToken\|os\s*:\s*OSType\|osType" --type=kotlin src/main/kotlin/dsm/pick2024/domain/devicetoken/

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 51


🏁 Script executed:

cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/domain/UserDeviceToken.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 471


🏁 Script executed:

cat -n src/main/kotlin/dsm/pick2024/domain/devicetoken/domain/AdminDeviceToken.kt

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 473


커밋 메시지와 실제 코드 구현이 불일치합니다.

커밋 메시지에서 "osType을 nullable하게 변경"이라고 명시했으나, UserDeviceTokenJpaEntity와 AdminDeviceTokenJpaEntity 모두에서 os 필드는 nullable = false로 설정되어 있습니다. 도메인 클래스(UserDeviceToken, AdminDeviceToken)에서도 os: OSType은 non-nullable입니다. 의도된 변경 사항인지, 또는 커밋 메시지가 잘못되었는지 확인이 필요합니다.

🤖 Prompt for AI Agents
In
`@src/main/kotlin/dsm/pick2024/domain/devicetoken/entity/UserDeviceTokenJpaEntity.kt`
around lines 21 - 23, Commit message and code disagree about making os nullable;
inspect intent and either make os nullable across JPA and domain or revert the
commit. If making nullable, change the JPA entities UserDeviceTokenJpaEntity and
AdminDeviceTokenJpaEntity by setting `@Column`(name = "os", nullable = true) and
update the Kotlin property type from OSType to OSType? (nullable) and adjust
constructors/mappers that convert between the JPA entities and domain classes;
also update the domain classes UserDeviceToken and AdminDeviceToken to use
OSType? and handle possible null values in any code that reads os. If the commit
was wrong, instead revert the commit so os remains non-nullable everywhere
(leave `@Column`(nullable = false) and domain types as OSType).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

entity 저장단계에서는 null인 애들은 저장이 되지 않고, 실제 값이 있는 애들만 저장이 되는 로직이라 이렇게 작성됨

Copy link

Choose a reason for hiding this comment

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

@1210thatman 설명 감사합니다!

저장 로직에서 null 값을 필터링하여 실제 값이 있는 경우만 저장하는 구조이므로, 엔티티 레벨에서는 nullable = false로 설정하는 것이 맞네요. API 요청 단계에서는 nullable로 받지만, 실제 DB에 저장되는 시점에는 항상 값이 존재하도록 보장하는 설계 방식으로 이해했습니다.

커밋 메시지는 아마도 요청 DTO 레벨에서의 nullable 변경을 의미하는 것으로 보이며, 엔티티 제약 조건은 의도대로 non-nullable을 유지하고 있습니다. 👍


📝 Learnings were identified and not saved (knowledge base disabled). Enable


) : BaseUUIDEntity(id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dsm.pick2024.domain.devicetoken.enum

enum class OSType {
AOS,
IOS,
ADMIN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dsm.pick2024.domain.devicetoken.exception

import dsm.pick2024.global.error.exception.ErrorCode
import dsm.pick2024.global.error.exception.PickException

object EntityStateException : PickException(
ErrorCode.ENTITY_STATE_EXCEPTION
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package dsm.pick2024.domain.devicetoken.mapper

import dsm.pick2024.domain.devicetoken.domain.AdminDeviceToken
import dsm.pick2024.domain.devicetoken.entity.AdminDeviceTokenJpaEntity
import dsm.pick2024.domain.devicetoken.exception.EntityStateException
import dsm.pick2024.global.base.GenericMapper
import org.springframework.stereotype.Component

@Component
class AdminDeviceTokenMapper :
GenericMapper<AdminDeviceTokenJpaEntity, AdminDeviceToken> {
override fun toEntity(domain: AdminDeviceToken) = domain.run {
AdminDeviceTokenJpaEntity(
id = id,
adminId = adminId,
deviceToken = deviceToken,
os = os
)
}

override fun toDomain(entity: AdminDeviceTokenJpaEntity) = entity.run {
AdminDeviceToken(
id = id!!,
adminId = adminId,
deviceToken = deviceToken,
os = os
)
}

fun updateEntity(
existingEntity: AdminDeviceTokenJpaEntity,
domain: AdminDeviceToken
): AdminDeviceTokenJpaEntity {
return AdminDeviceTokenJpaEntity(
id = existingEntity.id ?: throw EntityStateException,
Copy link
Member

Choose a reason for hiding this comment

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

조회했던 Entity의 id가 없는 상황은 고려하지 않아도 될 것 같습니다.. 애초에 DB에서 PK가 존재할 수 없는 경우는 없고 생기더라도 서버에서 값을 검증하는게 아닌 DB가 픽 서버 외엔 변경되지 않도록 만드는게 더 나을듯 합니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

adminId = existingEntity.adminId,
deviceToken = existingEntity.deviceToken,
os = existingEntity.os
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

updateEntity에서 domain 파라미터가 완전히 무시됩니다

updateEntity 메서드가 domain의 값을 전혀 사용하지 않고 existingEntity의 필드만으로 새 엔티티를 생성합니다. 이로 인해 동일한 deviceToken으로 재등록 시 (예: OS 변경, adminId 변경) 실제 업데이트가 발생하지 않습니다.

업데이트 의도에 맞게 domain의 필드를 반영해야 합니다.

수정 제안: domain 값 반영
     fun updateEntity(
         existingEntity: AdminDeviceTokenJpaEntity,
         domain: AdminDeviceToken
     ): AdminDeviceTokenJpaEntity {
         return AdminDeviceTokenJpaEntity(
             id = existingEntity.id ?: throw EntityStateException,
-            adminId = existingEntity.adminId,
-            deviceToken = existingEntity.deviceToken,
-            os = existingEntity.os
+            adminId = domain.adminId,
+            deviceToken = domain.deviceToken,
+            os = domain.os
         )
     }
🤖 Prompt for AI Agents
In
`@src/main/kotlin/dsm/pick2024/domain/devicetoken/mapper/AdminDeviceTokenMapper.kt`
around lines 30 - 40, The updateEntity function in AdminDeviceTokenMapper
currently ignores the incoming domain object and recreates the JPA entity using
only existingEntity fields; change AdminDeviceTokenMapper.updateEntity to
preserve the existingEntity.id (throwing EntityStateException if null) but copy
updated values from the domain parameter into the new AdminDeviceTokenJpaEntity
(set adminId = domain.adminId, deviceToken = domain.deviceToken, os = domain.os)
so the JPA entity reflects the intended updates from AdminDeviceToken.

Copy link
Member

Choose a reason for hiding this comment

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

update에서 같은 id의 새 엔티티 객체를 생성하는 로직을 Mapper에 두신 이유가 있을까요? 기존 픽 코드에서는 해당 도메인에 update 메서드를 두고 기존 객체에서 바꿀 컬럼을 인자로 받아 copy()하는 방식을 사용하고 있습니다.

}
Loading