Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1d8fca3
[IDLE-577] 사용하지 않는 파일 삭제
mjj111 May 23, 2025
5bd2770
[IDLE-578] Batch 에서 저장시 Redis에도 저장(TTL 13일)
mjj111 May 23, 2025
c33ef7d
[IDLE-578] Batch에서 공고 가져올 날짜 파라미터로 받게끔 수정
mjj111 May 23, 2025
4de2d89
[IDLE-579] 사용자 현재 위치로 Redis에서 검색 후 ID 획득하도록 수정
mjj111 May 23, 2025
245ad0c
[IDLE-580] 크롤링 공고 조회 request, response 변경
mjj111 May 23, 2025
5b88723
[IDLE-580] 획득한 공고ID 기반으로 MySQl에서 쿼리 후 결과 반환하도록 수정
mjj111 May 23, 2025
bd3d191
[IDLE-580] PointConverter 메서드 추가로 스타일변경
mjj111 May 23, 2025
74c8246
[IDLE-581] Read요청시 현재 사용자의 chatroom에 read Sequence 변경 및 unread 삭제
mjj111 May 23, 2025
65242a3
[IDLE-582] Send 요청시 사용자의 Redis chatroomid 에서 Sequence 가져와 메시지와 함께 Mys…
mjj111 May 23, 2025
ae76dfe
[IDLE-584] 채팅방 목록 조회시 Redis로 사용자가 읽지않은 채팅방 조회 및 Sequnce 기반으로 조회하여 MyS…
mjj111 May 23, 2025
8d7e010
[IDLE-585] 디바이스토큰 유니크 인덱스 할당
mjj111 May 23, 2025
eefd1d7
[IDLE-585] 채팅방 유니크 인덱스 할당
mjj111 May 23, 2025
b8328cf
[IDLE-577] 사용하지 않는 import 문 삭제
mjj111 May 24, 2025
5ce7eb5
[IDLE-578] 공고 개수가 최대 limit 만큼 되도록 수정
mjj111 May 24, 2025
69cae91
[IDLE-581] messageSequnce가 Long값으로 반환되도록 수정
mjj111 May 24, 2025
7ee886f
[IDLE-582] 응답값에 채팅 메시지에 대한 시퀀서 네이밍 일관화 sequence
mjj111 May 25, 2025
02b35a0
[IDLE-582] send 응답 값에 sequence 추가
mjj111 May 25, 2025
a454579
[IDLE-577] 마이그레이션 파일명과 주석 버전 일치
mjj111 May 25, 2025
f27f5b7
[IDLE-577] 읽음처리 Request dto 클래스명 오타 수정
mjj111 May 25, 2025
07522cc
[IDLE-577] 불필요한 print문 삭제
mjj111 May 25, 2025
1f3e712
[IDLE-577] 보조 생성자의 매개변수 순서 일치
mjj111 May 25, 2025
455fcfe
[IDLE-577] 중복된 의존성 주입 삭제
mjj111 May 25, 2025
8e870c1
[IDLE-577] 매직 넘버 상수화
mjj111 May 25, 2025
7910243
[IDLE-577] 필드명과 타입명 일치화
mjj111 May 25, 2025
dea7a18
[IDLE-577] sequnce null 허용추가
mjj111 May 25, 2025
159fb6c
[IDLE-577] null일 경우 1을 반환하도록 수정
mjj111 May 25, 2025
ed035dd
[IDLE-577] projection과 매핑을위한 alias 추가
mjj111 May 25, 2025
d4d5f41
[IDLE-577] send 요청시 본인의 채팅 sequnce를 업데이트 하도록 수정
mjj111 May 25, 2025
fac8784
[IDLE-577] RedisTemplate의 Key Value를 String으로 변경
mjj111 May 25, 2025
c60c1c1
[IDLE-577] Redis 이벤트 처리를 위한 Publisher와 Repository로써 클래스 분리
mjj111 May 25, 2025
f77453d
[IDLE-577] 안읽은 채팅방목록 삭제 메서드의 파라미터 순서 변경
mjj111 May 25, 2025
9a90d32
[IDLE-577] FCM 토큰 중복등록 예방로직 추가
mjj111 May 25, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.swm.idle.application.chat.domain

import com.swm.idle.domain.chat.entity.jpa.ChatMessage
import com.swm.idle.domain.chat.repository.ChatMessageRepository
import com.swm.idle.support.transfer.chat.ReadChatMessagesReqeust
import com.swm.idle.support.transfer.chat.SendChatMessageRequest
import jakarta.transaction.Transactional
import org.springframework.stereotype.Service
Expand All @@ -13,21 +12,17 @@ class ChatMessageService (
private val chatMessageRepository: ChatMessageRepository
){
@Transactional
fun save(request: SendChatMessageRequest, userId: UUID): ChatMessage {
fun save(request: SendChatMessageRequest, userId: UUID, sequence: Long): ChatMessage {
val message = ChatMessage(
chatRoomId = UUID.fromString(request.chatroomId),
content = request.content,
senderId = userId,
receiverId = UUID.fromString(request.receiverId),
sequence = sequence
)
return chatMessageRepository.save(message)
}

@Transactional
fun read(request: ReadChatMessagesReqeust, readUserId: UUID) {
chatMessageRepository.readByChatroomId(UUID.fromString(request.chatroomId), readUserId)
}

@Transactional
fun getRecentMessages(chatroomId: UUID, messageId: UUID): List<ChatMessage> {
return chatMessageRepository.getRecentMessages(chatroomId, messageId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,38 @@ package com.swm.idle.application.chat.domain
import com.swm.idle.domain.chat.entity.jpa.ChatRoom
import com.swm.idle.domain.chat.repository.ChatRoomRepository
import com.swm.idle.domain.chat.vo.ChatRoomSummaryInfo
import com.swm.idle.domain.chat.vo.ChatRoomSummaryInfoProjection
import org.springframework.stereotype.Service
import java.util.*

@Service
class ChatRoomService (val chatroomRepository: ChatRoomRepository){
class ChatRoomService(
val chatroomRepository: ChatRoomRepository,
){

fun create(carerId: UUID, centerId: UUID): UUID {
val existing = chatroomRepository.findByCarerIdAndCenterId(carerId, centerId)
return existing?.id ?: chatroomRepository.save(ChatRoom(carerId = carerId, centerId = centerId)).id
}

fun getById(chatRoomId: UUID): ChatRoom {
return chatroomRepository.findById(chatRoomId)
.orElseThrow()
}

fun findChatroomSummaries(userId: UUID, isCarer: Boolean): List<ChatRoomSummaryInfo> {
val projections: List<ChatRoomSummaryInfoProjection>
if(isCarer) {
projections = chatroomRepository.carerFindChatRooms(userId)
}else {
projections = chatroomRepository.centerFindChatRooms(userId)
}
fun findChatRoomsWithLastMessages(roomIds: Set<String>, isCarer: Boolean): List<ChatRoomSummaryInfo> {
val uuidSet = roomIds.map(UUID::fromString).toSet()
val projections = chatroomRepository.findChatRoomsWithLastMessages(uuidSet)

return projections.map { projection ->
mappingChatRoomSummaryInfo(projection)
val opponentId = if (isCarer) projection.getCenterId() else projection.getCarerId()
ChatRoomSummaryInfo(
projection.getChatRoomId(),
opponentId,
projection.getLastMessage(),
projection.getLastMessageTime(),
projection.getLastSequence()?:1L
)
}
}

private fun mappingChatRoomSummaryInfo(projection: ChatRoomSummaryInfoProjection) =
ChatRoomSummaryInfo(
chatRoomId = projection.getChatRoomId(),
lastMessage = projection.getLastMessage(),
lastMessageTime = projection.getLastMessageTime(),
count = projection.getUnreadCount(),
opponentId = projection.getOpponentId()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import com.swm.idle.application.notification.domain.DeviceTokenService
import com.swm.idle.application.user.carer.domain.CarerService
import com.swm.idle.application.user.center.service.domain.CenterManagerService
import com.swm.idle.application.user.center.service.domain.CenterService
import com.swm.idle.domain.chat.event.ChatRedisTemplate
import com.swm.idle.domain.chat.entity.jpa.ChatMessage
import com.swm.idle.domain.chat.event.ChatRedisPublisher
import com.swm.idle.domain.chat.repository.ChatRedisRepository
import com.swm.idle.domain.chat.vo.ChatRoomSummaryInfo
import com.swm.idle.domain.chat.vo.ReadMessage
import com.swm.idle.domain.user.carer.entity.jpa.Carer
import com.swm.idle.domain.user.center.entity.jpa.Center
import com.swm.idle.domain.user.center.entity.jpa.CenterManager
import com.swm.idle.domain.user.center.exception.CenterException
import com.swm.idle.domain.user.center.vo.BusinessRegistrationNumber
Expand All @@ -21,28 +25,48 @@ import org.springframework.transaction.annotation.Transactional
import java.util.*

@Service
@Transactional(readOnly = true)
class ChatFacadeService(
private val centerManagerService: CenterManagerService,
private val chatRedisTemplate: ChatRedisTemplate,
private val chatRedisPublisher: ChatRedisPublisher,
private val messageService: ChatMessageService,
private val notificationService: ChatNotificationService,
private val deviceTokenService: DeviceTokenService,
private val chatMessageService: ChatMessageService,
private val chatroomService: ChatRoomService,
private val centerService: CenterService,
private val carerService: CarerService,
private val chatRedisRepository: ChatRedisRepository,
) {

@Transactional
fun carerSend(request: SendChatMessageRequest, carerId: UUID) {
val message = messageService.save(request, carerId)
chatRedisTemplate.publish(message)
fun send(request: SendChatMessageRequest, inputId: UUID, isCarer: Boolean) {
val userId = if(isCarer) inputId else getCenterId(inputId)
val sequence = chatRedisRepository.getChatRoomSequence(request.chatroomId)
val message = messageService.save(request, userId, sequence)

for(manager in getManagersByCenterId(UUID.fromString(request.receiverId))) {
if (chatRedisTemplate.isChatting(manager.id)) continue
chatRedisRepository.addUnreadChatRoom(request.receiverId, request.chatroomId)
chatRedisRepository.updateReadSequence(request.chatroomId, message.sequence.toString(), userId)

val token = deviceTokenService.findByUserId(manager.id)
chatRedisPublisher.publish(message)

sendNotification(message, request, isCarer)
}

private fun sendNotification(
message: ChatMessage,
request: SendChatMessageRequest,
isCarer: Boolean
) {
if(isCarer) {
for (manager in getManagersByCenterId(UUID.fromString(request.receiverId))) {
if (chatRedisRepository.isChatting(manager.id)) continue

val token = deviceTokenService.findByUserId(manager.id) ?: continue
notificationService.send(message, request.senderName, token)
}
}else{
if (chatRedisRepository.isChatting(message.receiverId)) return
val token = deviceTokenService.findByUserId(message.receiverId) ?: return
notificationService.send(message, request.senderName, token)
}
}
Expand All @@ -54,40 +78,25 @@ class ChatFacadeService(
}

@Transactional
fun centerSend(request: SendChatMessageRequest, managerId: UUID) {
val centerId = getCenterId(managerId)
val message = messageService.save(request, centerId)
chatRedisTemplate.publish(message)

if (chatRedisTemplate.isChatting(message.receiverId)) return

val token = deviceTokenService.findByUserId(message.receiverId)
notificationService.send(message, request.senderName, token)
}

@Transactional
fun carerRead(request: ReadChatMessagesReqeust, carerId: UUID) {
messageService.read(request, carerId)
fun read(request: ReadChatMessageRequest, inputId: UUID, isCarer: Boolean) {
val userId = if(isCarer) inputId else getCenterId(inputId)
chatRedisRepository.removeUnreadChatRoom(userId ,request.chatroomId)
chatRedisRepository.updateReadSequence(request.chatroomId, request.sequence, userId)

val readMessage = ReadMessage(
chatRoomId = UUID.fromString(request.chatroomId),
receiverId = UUID.fromString(request.opponentId),
readUserId = carerId
readUserId = userId,
sequence = request.sequence.toLong()
)
chatRedisTemplate.publish(readMessage)
chatRedisPublisher.publish(readMessage)
}

@Transactional
fun centerRead(request: ReadChatMessagesReqeust, managerId: UUID) {
val centerId = getCenterId(managerId)
messageService.read(request, centerId)

val readMessage = ReadMessage(
chatRoomId = UUID.fromString(request.chatroomId),
receiverId = UUID.fromString(request.opponentId),
readUserId = centerId
)
chatRedisTemplate.publish(readMessage)
private fun getCenterId(managerId:UUID): UUID {
val manager = centerManagerService.getById(managerId)
val businessNumber = BusinessRegistrationNumber(manager.centerBusinessRegistrationNumber)
val center = centerService.findByBusinessRegistrationNumber(businessNumber)?: throw CenterException.NotFoundException()
return center.id
}
Comment on lines +95 to 100

This comment was marked as resolved.


@Transactional
Expand All @@ -112,36 +121,54 @@ class ChatFacadeService(
return center.id
}

fun getRecentMessages(chatRoomId: UUID, messageId: UUID?): List<ChatMessageResponse> {
val effectiveMessageId = messageId ?: UuidCreator.create()

return chatMessageService.getRecentMessages(chatRoomId, effectiveMessageId)
.map { ChatMessageResponse(it) }
@Transactional(readOnly = true)
fun getRecentMessages(
chatRoomId: UUID,
messageId: UUID?,
isCarer: Boolean
): ChatMessageResponse {
val lastMessageId = messageId ?: UuidCreator.create()
val messages = chatMessageService.getRecentMessages(chatRoomId, lastMessageId)
val messageInfo = messages.map { ChatMessageInfo(it) }

val chatRoom = chatroomService.getById(chatRoomId)
val opponentId = if (isCarer) chatRoom.centerId else chatRoom.carerId
val opponentReadSequence = chatRedisRepository.getReadSequence(opponentId, chatRoomId)

return ChatMessageResponse(messageInfo, opponentReadSequence)
}

@Transactional(readOnly = true)
fun getChatroomSummary(isCarer: Boolean): List<ChatRoomSummaryInfo> {
val userId: UUID = if (isCarer) getUserAuthentication().userId
else getCenterIdByAuthentication()

val summary = chatroomService.findChatroomSummaries(userId, isCarer)

return if (isCarer) {
summary.map {
val center = centerService.getById(it.opponentId)
it.copy(opponentName = center.centerName, opponentProfileImageUrl = center.profileImageUrl)
}
val userId = if (isCarer) getUserAuthentication().userId else getCenterIdByAuthentication()
val unreadChatRoomIds = chatRedisRepository.getUnreadChatRooms(userId)
val readSequences = chatRedisRepository.getReadSequences(userId, unreadChatRoomIds)
val summaries = chatroomService.findChatRoomsWithLastMessages(unreadChatRoomIds, isCarer)

val opponentIds = summaries.map { it.opponentId }.toSet()
val opponentInfoMap = if (isCarer) {
centerService.getByIds(opponentIds).associateBy { it.id }
} else {
summary.map {
val carer = carerService.getById(it.opponentId)
it.copy(opponentName = carer.name, opponentProfileImageUrl = carer.profileImageUrl)
}
carerService.getByIds(opponentIds).associateBy { it.id }
}
}

private fun getCenterId(managerId:UUID): UUID {
val manager = centerManagerService.getById(managerId)
val businessNumber = BusinessRegistrationNumber(manager.centerBusinessRegistrationNumber)
val center = centerService.findByBusinessRegistrationNumber(businessNumber)?: throw CenterException.NotFoundException()
return center.id
return summaries.map { summary ->
val readSeq = readSequences[summary.chatRoomId.toString()] ?: 0L
summary.count = (summary.count - readSeq).coerceAtLeast(1)

val opponent = opponentInfoMap[summary.opponentId]
summary.apply {
when (opponent) {
is Center -> {
opponentName = opponent.centerName
opponentProfileImageUrl = opponent.profileImageUrl
}
is Carer -> {
opponentName = opponent.name
opponentProfileImageUrl = opponent.profileImageUrl
}
}
}
}
Comment on lines +155 to +172
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

불변성을 유지하고 로직을 개선하세요.

현재 코드는 여러 문제점이 있습니다:

  1. summary 객체를 직접 변경하는 것은 부작용을 일으킬 수 있습니다.
  2. coerceAtLeast(1)로 인해 모든 메시지를 읽어도 count가 1로 유지됩니다.
  3. opponent 타입 검사에서 else 절이 없어 예상치 못한 타입일 때 처리되지 않습니다.
         return summaries.map { summary ->
             val readSeq = readSequences[summary.chatRoomId.toString()] ?: 0L
-            summary.count = (summary.count - readSeq).coerceAtLeast(1)
+            val unreadCount = maxOf(0, summary.count - readSeq)
 
             val opponent = opponentInfoMap[summary.opponentId]
-            summary.apply {
-                when (opponent) {
-                    is Center -> {
-                        opponentName = opponent.centerName
-                        opponentProfileImageUrl = opponent.profileImageUrl
-                    }
-                    is Carer -> {
-                        opponentName = opponent.name
-                        opponentProfileImageUrl = opponent.profileImageUrl
-                    }
-                }
+            val (opponentName, opponentProfileImageUrl) = when (opponent) {
+                is Center -> opponent.centerName to opponent.profileImageUrl
+                is Carer -> opponent.name to opponent.profileImageUrl
+                else -> "Unknown" to null
             }
+            
+            summary.copy(
+                count = unreadCount,
+                opponentName = opponentName,
+                opponentProfileImageUrl = opponentProfileImageUrl
+            )
         }
📝 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
return summaries.map { summary ->
val readSeq = readSequences[summary.chatRoomId.toString()] ?: 0L
summary.count = (summary.count - readSeq).coerceAtLeast(1)
val opponent = opponentInfoMap[summary.opponentId]
summary.apply {
when (opponent) {
is Center -> {
opponentName = opponent.centerName
opponentProfileImageUrl = opponent.profileImageUrl
}
is Carer -> {
opponentName = opponent.name
opponentProfileImageUrl = opponent.profileImageUrl
}
}
}
}
return summaries.map { summary ->
val readSeq = readSequences[summary.chatRoomId.toString()] ?: 0L
val unreadCount = maxOf(0, summary.count - readSeq)
val opponent = opponentInfoMap[summary.opponentId]
val (opponentName, opponentProfileImageUrl) = when (opponent) {
is Center -> opponent.centerName to opponent.profileImageUrl
is Carer -> opponent.name to opponent.profileImageUrl
else -> "Unknown" to null
}
summary.copy(
count = unreadCount,
opponentName = opponentName,
opponentProfileImageUrl = opponentProfileImageUrl
)
}
🤖 Prompt for AI Agents
In
idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt
around lines 152 to 169, avoid mutating the summary object directly to maintain
immutability by creating and returning a new summary instance with updated
fields. Fix the count calculation by using coerceAtLeast(0) instead of 1 so that
if all messages are read, the count can be zero. Add an else branch in the
opponent type check to handle unexpected types safely, possibly by setting
default values or logging a warning.

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.swm.idle.application.common.converter

import com.swm.idle.domain.user.carer.entity.jpa.Carer
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.geom.Point
Expand All @@ -14,14 +15,14 @@ object PointConverter {
SPATIAL_REFERENCE_IDENTIFIER_NUMBER
)

fun convertToPoint(
latitude: Double,
longitude: Double,
): Point {
val correctedLongitude = if (longitude > latitude) longitude else latitude
val correctedLatitude = if (longitude > latitude) latitude else longitude
fun convertToPoint(carer: Carer): Point {
val latitude = carer.latitude.toDouble()
val longitude = carer.longitude.toDouble()

return geometryFactory.createPoint(Coordinate(correctedLongitude, correctedLatitude))
return geometryFactory.createPoint(Coordinate(longitude, latitude))
}

}
fun convertToPoint(latitude: Double, longitude: Double ): Point {
return geometryFactory.createPoint(Coordinate(longitude, latitude))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.swm.idle.domain.common.dto.CrawlingJobPostingPreviewDto
import com.swm.idle.domain.common.exception.PersistenceException
import com.swm.idle.domain.jobposting.entity.jpa.CrawledJobPosting
import com.swm.idle.domain.jobposting.repository.jpa.CrawlingJobPostingJpaRepository
import com.swm.idle.domain.jobposting.repository.querydsl.CrawlingJobPostingSpatialQueryRepository
import com.swm.idle.domain.jobposting.repository.redis.RedisJobPostingRepository
import org.locationtech.jts.geom.Point
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
Expand All @@ -15,33 +15,26 @@ import java.util.*
@Transactional(readOnly = true)
class CrawlingJobPostingService(
private val crawlingJobPostingJpaRepository: CrawlingJobPostingJpaRepository,
private val crawlingJobPostingSpatialQueryRepository: CrawlingJobPostingSpatialQueryRepository,
private val redisJobPostingRepository: RedisJobPostingRepository,
) {

@Transactional
fun saveAll(crawledJobPostings: List<CrawledJobPosting>) {
crawlingJobPostingJpaRepository.saveAll(crawledJobPostings)
}

fun getById(crawlingJobPostingId: UUID): CrawledJobPosting {
return crawlingJobPostingJpaRepository.findByIdOrNull(crawlingJobPostingId)
?: throw PersistenceException.ResourceNotFound("크롤링한 구인 공고(id=$crawlingJobPostingId)를 찾을 수 없습니다")
}

fun findAllByCarerLocationInRange(
carerId: UUID,
location: Point,
fun findAllInRange(
next: UUID?,
limit: Long,
location: Point,
distance: Long,
limit: Long
): List<CrawlingJobPostingPreviewDto> {
return crawlingJobPostingSpatialQueryRepository.findAllInRange(
carerId = carerId,
location = location,
next = next,
limit = limit,
)
val postingIds = redisJobPostingRepository.findByLocationAndDistance(location, distance, limit, next)
val postings = crawlingJobPostingJpaRepository.findAllById(postingIds)
return postings.map { CrawlingJobPostingPreviewDto(it, distance.toInt()) }
}


fun calculateDistance(
crawledJobPosting: CrawledJobPosting,
carerLocation: Point,
Expand Down
Loading
Loading