diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt index a292b3f1..725864c8 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt @@ -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 @@ -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 { return chatMessageRepository.getRecentMessages(chatroomId, messageId) diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt index 1cae784e..f3236e6c 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt @@ -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 { - val projections: List - if(isCarer) { - projections = chatroomRepository.carerFindChatRooms(userId) - }else { - projections = chatroomRepository.centerFindChatRooms(userId) - } + fun findChatRoomsWithLastMessages(roomIds: Set, isCarer: Boolean): List { + 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() - ) } \ No newline at end of file diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt index 49e83959..9d300a34 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt @@ -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 @@ -21,10 +25,9 @@ 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, @@ -32,17 +35,38 @@ class ChatFacadeService( 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) } } @@ -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 } @Transactional @@ -112,36 +121,54 @@ class ChatFacadeService( return center.id } - fun getRecentMessages(chatRoomId: UUID, messageId: UUID?): List { - 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 { - 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 + } + } + } + } } } \ No newline at end of file diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/common/converter/PointConverter.kt b/idle-application/src/main/kotlin/com/swm/idle/application/common/converter/PointConverter.kt index f880769b..84caacce 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/common/converter/PointConverter.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/common/converter/PointConverter.kt @@ -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 @@ -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)) + } +} \ No newline at end of file diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/domain/CrawlingJobPostingService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/domain/CrawlingJobPostingService.kt index 5f3c23dc..09972e7d 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/domain/CrawlingJobPostingService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/domain/CrawlingJobPostingService.kt @@ -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 @@ -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) { - 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 { - 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, diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CarerJobPostingFacadeService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CarerPostingFacadeService.kt similarity index 81% rename from idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CarerJobPostingFacadeService.kt rename to idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CarerPostingFacadeService.kt index cefab582..23fd818d 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CarerJobPostingFacadeService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CarerPostingFacadeService.kt @@ -12,7 +12,6 @@ import com.swm.idle.application.user.carer.domain.CarerService import com.swm.idle.application.user.center.service.domain.CenterService import com.swm.idle.domain.common.dto.JobPostingPreviewDto import com.swm.idle.domain.common.enums.EntityStatus -import com.swm.idle.domain.user.carer.entity.jpa.Carer import com.swm.idle.support.transfer.common.CursorScrollRequest import com.swm.idle.support.transfer.jobposting.carer.CarerAppliedJobPostingScrollResponse import com.swm.idle.support.transfer.jobposting.carer.CarerJobPostingResponse @@ -23,7 +22,7 @@ import org.springframework.stereotype.Service import java.util.* @Service -class CarerJobPostingFacadeService( +class CarerPostingFacadeService( private val jobPostingWeekdayService: JobPostingWeekdayService, private val jobPostingLifeAssistanceService: JobPostingLifeAssistanceService, private val jobPostingApplyMethodService: JobPostingApplyMethodService, @@ -35,18 +34,12 @@ class CarerJobPostingFacadeService( ) { fun getJobPostingDetail(jobPostingId: UUID): CarerJobPostingResponse { - val carer = getUserAuthentication().userId.let { - carerService.getById(it) - } - - val jobPosting = jobPostingService.getById(jobPostingId) + val carer = carerService.getById(getUserAuthentication().userId) + val posting = jobPostingService.getById(jobPostingId) val distance = jobPostingService.calculateDistance( - jobPosting, - PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) + posting, + PointConverter.convertToPoint(carer) ) val weekdays = jobPostingWeekdayService.findByJobPostingId(jobPostingId)?.map { it.weekday } @@ -65,10 +58,10 @@ class CarerJobPostingFacadeService( carerId = carer.id, ) - val center = centerService.getById(jobPosting.centerId) + val center = centerService.getById(posting.centerId) return CarerJobPostingResponse.of( - jobPosting = jobPosting, + jobPosting = posting, weekdays = weekdays, lifeAssistances = lifeAssistances, applyMethods = applyMethods, @@ -81,8 +74,10 @@ class CarerJobPostingFacadeService( fun getJobPostingsInRange( request: CursorScrollRequest, - location: Point, ): CarerJobPostingScrollResponse { + val carer = carerService.getById(getUserAuthentication().userId) + val location = PointConverter.convertToPoint(carer) + val (items, next) = scrollByCarerLocationInRange( location = location, next = request.next, @@ -112,10 +107,7 @@ class CarerJobPostingFacadeService( limit = limit + 1, ) - val carerLocation = PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) + val carerLocation = PointConverter.convertToPoint(carer) for (jobPostingPreviewDto in jobPostingPreviewDtos) { jobPostingPreviewDto.distance = jobPostingService.calculateDistance( @@ -134,14 +126,11 @@ class CarerJobPostingFacadeService( return items to newNext } - fun getAppliedJobPostings( - request: CursorScrollRequest, - carerId: UUID, - ): CarerAppliedJobPostingScrollResponse { + fun getAppliedJobPostings(request: CursorScrollRequest): CarerAppliedJobPostingScrollResponse { val (items, next) = scrollByCarerApplyHistory( next = request.next, limit = request.limit, - carerId = carerId, + carerId = getUserAuthentication().userId ) return CarerAppliedJobPostingScrollResponse.from( @@ -163,14 +152,8 @@ class CarerJobPostingFacadeService( carerId = carerId, ) - val carerLocation = getUserAuthentication().userId.let { - carerService.getById(it) - }.let { - PointConverter.convertToPoint( - latitude = it.latitude.toDouble(), - longitude = it.longitude.toDouble(), - ) - } + val carer = carerService.getById(getUserAuthentication().userId) + val carerLocation = PointConverter.convertToPoint(carer) for (jobPostingPreviewDto in JobPostingPreviewDtos) { jobPostingService.calculateDistance( @@ -189,20 +172,17 @@ class CarerJobPostingFacadeService( return items to newNext } - fun getMyFavoriteJobPostings( - carer: Carer, - location: Point, - ): JobPostingFavoriteResponse { + fun getMyFavoriteJobPostings(): JobPostingFavoriteResponse { + val carer = carerService.getById(getUserAuthentication().userId) + val location = PointConverter.convertToPoint(carer) + val jobPostingPreviewDtos: List? = jobPostingService.findAllFavorites(carer.id) jobPostingPreviewDtos?.map { jobPostingPreviewDto -> val distance = jobPostingService.calculateDistance( jobPostingPreviewDto.jobPosting, - PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) + location ) JobPostingFavoriteResponse.MyFavoriteJobPostingDto.of( diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CenterJobPostingFacadeService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CenterPostingFacadeService.kt similarity index 98% rename from idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CenterJobPostingFacadeService.kt rename to idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CenterPostingFacadeService.kt index 36e83dea..8a02a3ad 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CenterJobPostingFacadeService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CenterPostingFacadeService.kt @@ -37,7 +37,7 @@ import java.util.* @Service @Transactional(readOnly = true) -class CenterJobPostingFacadeService( +class CenterPostingFacadeService( private val jobPostingService: JobPostingService, private val jobPostingLifeAssistanceService: JobPostingLifeAssistanceService, private val jobPostingWeekdayService: JobPostingWeekdayService, @@ -56,13 +56,7 @@ class CenterJobPostingFacadeService( @Transactional suspend fun create(request: CreateJobPostingRequest) { - val centerId = - centerManagerService.getById(getUserAuthentication().userId).let { - centerService.findByBusinessRegistrationNumber( - BusinessRegistrationNumber(it.centerBusinessRegistrationNumber) - )?.id - } ?: throw CenterException.NotFoundException() - + val centerId = getCenterId() val geoCodeSearchResult = geoCodeService.search(request.roadNameAddress) val jobPosting = jobPostingService.create( @@ -127,6 +121,16 @@ class CenterJobPostingFacadeService( } } + private fun getCenterId(): UUID { + val centerId = + centerManagerService.getById(getUserAuthentication().userId).let { + centerService.findByBusinessRegistrationNumber( + BusinessRegistrationNumber(it.centerBusinessRegistrationNumber) + )?.id + } ?: throw CenterException.NotFoundException() + return centerId + } + private fun createBodyMessage(jobPosting: JobPosting): String { val filteredLotNumberAddress = jobPosting.lotNumberAddress.split(" ") .take(3) @@ -140,10 +144,10 @@ class CenterJobPostingFacadeService( @Transactional fun update( - jobPostingId: UUID, + postingId: UUID, request: UpdateJobPostingRequest, ) { - val jobPosting = jobPostingService.getById(jobPostingId) + val jobPosting = jobPostingService.getById(postingId) val shouldUpdateAddress = request.roadNameAddress != null && request.lotNumberAddress != null diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CrawlingJobPostingFacadeService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CrawlingJobPostingFacadeService.kt deleted file mode 100644 index 903b7642..00000000 --- a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CrawlingJobPostingFacadeService.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.swm.idle.application.jobposting.facade - -import com.swm.idle.application.common.converter.PointConverter -import com.swm.idle.application.common.security.getUserAuthentication -import com.swm.idle.application.jobposting.domain.CrawlingJobPostingService -import com.swm.idle.application.jobposting.domain.JobPostingFavoriteService -import com.swm.idle.application.user.carer.domain.CarerService -import com.swm.idle.domain.common.dto.CrawlingJobPostingPreviewDto -import com.swm.idle.domain.common.enums.EntityStatus -import com.swm.idle.domain.user.carer.entity.jpa.Carer -import com.swm.idle.support.transfer.common.CursorScrollRequest -import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingFavoriteResponse -import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingScrollResponse -import com.swm.idle.support.transfer.jobposting.common.CrawlingJobPostingResponse -import org.locationtech.jts.geom.Point -import org.springframework.stereotype.Service -import java.util.* - -@Service -class CrawlingJobPostingFacadeService( - private val crawlingJobPostingService: CrawlingJobPostingService, - private val carerService: CarerService, - private val jobPostingFavoriteService: JobPostingFavoriteService, -) { - - fun getCrawlingJobPosting(crawlingJobPostingId: UUID): CrawlingJobPostingResponse { - val carer = getUserAuthentication().userId.let { - carerService.getById(it) - } - - val jobPosting = crawlingJobPostingService.getById(crawlingJobPostingId) - - val distance = crawlingJobPostingService.calculateDistance( - jobPosting, - PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) - ) - - return crawlingJobPostingService.getById(crawlingJobPostingId).let { - val isFavorite = - jobPostingFavoriteService.findByByJobPostingId(crawlingJobPostingId)?.let { - it.entityStatus == EntityStatus.ACTIVE - } ?: false - - CrawlingJobPostingResponse.from( - crawlingJobPosting = it, - longitude = jobPosting.location.x, - latitude = jobPosting.location.y, - isFavorite = isFavorite, - distance = distance, - ) - } - } - - fun getCrawlingJobPostingsInRange( - request: CursorScrollRequest, - location: Point, - ): CrawlingJobPostingScrollResponse { - val (items, next) = scrollByCarerLocationInRange( - location = location, - next = request.next, - limit = request.limit - ) - - return CrawlingJobPostingScrollResponse.from( - items = items, - next = next, - total = items.size, - ) - } - - fun getFavoriteCrawlingJobPostings( - carer: Carer, - location: Point, - ): CrawlingJobPostingFavoriteResponse { - val crawlingJobPostings = crawlingJobPostingService.findMyFavoritesByCarerId(carer.id) - - crawlingJobPostings?.map { crawledJobPosting -> - val distance = crawlingJobPostingService.calculateDistance( - crawledJobPosting, - PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) - ) - - CrawlingJobPostingFavoriteResponse.CrawlingJobPostingFavoriteDto.from( - crawledJobPosting = crawledJobPosting, - distance = distance, - ) - }.let { - return CrawlingJobPostingFavoriteResponse.from(it!!) - } - } - - private fun scrollByCarerLocationInRange( - location: Point, - next: UUID?, - limit: Long, - ): Pair, UUID?> { - val carer = getUserAuthentication().userId.let { - carerService.getById(it) - } - - val crawlingJobPostingPreviewDtos = crawlingJobPostingService.findAllByCarerLocationInRange( - carerId = carer.id, - location = location, - next = next, - limit = limit + 1, - ) - - val carerLocation = PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) - - for (crawlingJobPostingPreviewDto in crawlingJobPostingPreviewDtos) { - crawlingJobPostingPreviewDto.distance = crawlingJobPostingService.calculateDistance( - crawlingJobPostingPreviewDto.crawledJobPosting, - carerLocation - ) - } - - val newNext = - if (crawlingJobPostingPreviewDtos.size > limit) crawlingJobPostingPreviewDtos.last().crawledJobPosting.id else null - val items = - if (newNext == null) crawlingJobPostingPreviewDtos else crawlingJobPostingPreviewDtos.subList( - 0, - limit.toInt() - ) - return items to newNext - } - -} diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CrawlingPostingFacadeService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CrawlingPostingFacadeService.kt new file mode 100644 index 00000000..82b858e9 --- /dev/null +++ b/idle-application/src/main/kotlin/com/swm/idle/application/jobposting/facade/CrawlingPostingFacadeService.kt @@ -0,0 +1,86 @@ +package com.swm.idle.application.jobposting.facade + +import com.swm.idle.application.common.converter.PointConverter +import com.swm.idle.application.common.security.getUserAuthentication +import com.swm.idle.application.jobposting.domain.CrawlingJobPostingService +import com.swm.idle.application.jobposting.domain.JobPostingFavoriteService +import com.swm.idle.application.user.carer.domain.CarerService +import com.swm.idle.domain.common.dto.CrawlingJobPostingPreviewDto +import com.swm.idle.domain.common.enums.EntityStatus +import com.swm.idle.support.transfer.common.CrawlingCursorScrollRequest +import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingFavoriteResponse +import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingScrollResponse +import com.swm.idle.support.transfer.jobposting.common.CrawlingJobPostingResponse +import org.springframework.stereotype.Service +import java.util.* + +private const val searchingBreakCount = 3 + +@Service +class CrawlingPostingFacadeService( + private val crawlingJobPostingService: CrawlingJobPostingService, + private val carerService: CarerService, + private val jobPostingFavoriteService: JobPostingFavoriteService, +) { + companion object { + private const val MAX_SEARCH_DISTANCE = 30L + } + + fun getCrawlingPostingsInRange(request: CrawlingCursorScrollRequest): CrawlingJobPostingScrollResponse { + val carer = carerService.getById(getUserAuthentication().userId) + val location = PointConverter.convertToPoint(carer) + + var distance = request.distance + var zeroCount = 0 + var nextCursor: UUID? = request.next + + val result = mutableListOf() + while (result.size < request.limit && distance <= MAX_SEARCH_DISTANCE) { + val currentBatch = crawlingJobPostingService.findAllInRange( + next = nextCursor, + location = location, + distance = distance, + limit = request.limit - result.size.toLong() + ) + + if(currentBatch.isEmpty()) zeroCount++ + nextCursor = currentBatch + .lastOrNull() + ?.crawledJobPosting + ?.id + + result.addAll(currentBatch) + if (result.size >= request.limit + || zeroCount == searchingBreakCount) break + + distance += 1 + } + + return CrawlingJobPostingScrollResponse.from(result, distance) + } + + fun getCrawlingJobPosting(postingId: UUID): CrawlingJobPostingResponse { + val carer = carerService.getById(getUserAuthentication().userId) + val posting = crawlingJobPostingService.getById(postingId) + val distance = crawlingJobPostingService.calculateDistance(posting, PointConverter.convertToPoint(carer)) + val isFavorite = jobPostingFavoriteService.findByByJobPostingId(postingId)?.let { + it.entityStatus == EntityStatus.ACTIVE + } ?: false + + return CrawlingJobPostingResponse.from(posting, isFavorite, distance) + } + + fun getFavoriteCrawlingJobPostings(): CrawlingJobPostingFavoriteResponse { + val carer = carerService.getById(getUserAuthentication().userId) + val carerPoint = PointConverter.convertToPoint(carer) + + val crawlingJobPostings = crawlingJobPostingService.findMyFavoritesByCarerId(carer.id).orEmpty() + + val dtoList = crawlingJobPostings.map { posting -> + val distance = crawlingJobPostingService.calculateDistance(posting, carerPoint) + CrawlingJobPostingFavoriteResponse.CrawlingJobPostingFavoriteDto.from(posting, distance) + } + + return CrawlingJobPostingFavoriteResponse.from(dtoList) + } +} diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/notification/domain/DeviceTokenService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/notification/domain/DeviceTokenService.kt index c97142ee..9dd991eb 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/notification/domain/DeviceTokenService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/notification/domain/DeviceTokenService.kt @@ -27,10 +27,10 @@ class DeviceTokenService( @Transactional fun updateDeviceTokenUserId( - deviceToken: DeviceToken, - userId: UUID, + deviceTokenEntity: DeviceToken, + newToken: String, ) { - deviceToken.updateUserId(userId) + deviceTokenEntity.updateToken(newToken) } @Transactional diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/notification/facade/DeviceTokenFacadeService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/notification/facade/DeviceTokenFacadeService.kt index e7fe1c99..3b276caf 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/notification/facade/DeviceTokenFacadeService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/notification/facade/DeviceTokenFacadeService.kt @@ -15,17 +15,22 @@ class DeviceTokenFacadeService( @Transactional fun createDeviceToken(deviceToken: String, userType: UserType) { val userId = getUserAuthentication().userId - deviceTokenService.findByDeviceToken(deviceToken)?.let { - if (it.userId != userId) { - deviceTokenService.updateDeviceTokenUserId(it, userId) - } - } ?: deviceTokenService.save( - userId = userId, - deviceToken = deviceToken, - userType = userType, - ) + + val existingTokenByDevice = deviceTokenService.findByDeviceToken(deviceToken) + val existingTokenByUser = deviceTokenService.findByUserId(userId) + + if (existingTokenByDevice != null) { + deviceTokenService.deleteByDeviceToken(deviceToken) + } + + if (existingTokenByUser == null) { + deviceTokenService.save(userId, deviceToken, userType) + } else { + deviceTokenService.updateDeviceTokenUserId(existingTokenByUser, deviceToken) + } } + @Transactional fun deleteDeviceToken(deviceToken: String) { deviceTokenService.deleteByDeviceToken(deviceToken) diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/user/carer/domain/CarerService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/user/carer/domain/CarerService.kt index c66a0e81..06d4df19 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/user/carer/domain/CarerService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/user/carer/domain/CarerService.kt @@ -111,4 +111,8 @@ class CarerService( return carerQueryRepository.findAllByLocationWithinRadius(location) } + fun getByIds(carerIds: Set): List { + return carerJpaRepository.findAllById(carerIds) + } + } diff --git a/idle-application/src/main/kotlin/com/swm/idle/application/user/center/service/domain/CenterService.kt b/idle-application/src/main/kotlin/com/swm/idle/application/user/center/service/domain/CenterService.kt index 25fd6848..4ab3be34 100644 --- a/idle-application/src/main/kotlin/com/swm/idle/application/user/center/service/domain/CenterService.kt +++ b/idle-application/src/main/kotlin/com/swm/idle/application/user/center/service/domain/CenterService.kt @@ -66,4 +66,8 @@ class CenterService( ?: throw PersistenceException.ResourceNotFound("Center(id=$centerId)를 찾을 수 없습니다") } + fun getByIds(centerIds : Set) : List
{ + return centerJpaRepository.findAllById(centerIds) + } + } diff --git a/idle-batch/build.gradle.kts b/idle-batch/build.gradle.kts index 99582abf..87b9dca9 100644 --- a/idle-batch/build.gradle.kts +++ b/idle-batch/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(project(":idle-application")) implementation(project(":idle-infrastructure:client")) + implementation(libs.spring.boot.starter.data.redis) implementation(rootProject.libs.spring.boot.starter.batch) implementation(rootProject.libs.selenium.java) implementation(libs.mysql.connector.java) diff --git a/idle-batch/src/main/kotlin/com/swm/idle/batch/common/launcher/CrawlingJobLauncher.kt b/idle-batch/src/main/kotlin/com/swm/idle/batch/common/launcher/CrawlingJobLauncher.kt index 174485e8..fdee5e31 100644 --- a/idle-batch/src/main/kotlin/com/swm/idle/batch/common/launcher/CrawlingJobLauncher.kt +++ b/idle-batch/src/main/kotlin/com/swm/idle/batch/common/launcher/CrawlingJobLauncher.kt @@ -12,21 +12,23 @@ import org.springframework.stereotype.Component class CrawlingJobLauncher( private val jobLauncher: JobLauncher, private val jobRegistry: JobRegistry, - private val crawlingJobConfig: JobConfig, ) { - @Scheduled(cron = "0 0 23 * * *") + @Scheduled(cron = "0 0 22 * * *") fun scheduleJob() { val jobParameters: JobParameters = JobParametersBuilder() .addLong("timestamp", System.currentTimeMillis()) + .addLong("day", 0L) .toJobParameters() - jobLauncher.run(crawlingJobConfig.crawlingJob(), jobParameters) + jobLauncher.run(jobRegistry.getJob("crawlingJob"), jobParameters) } - fun jobStart() { + + fun jobStart(dayOffset: Long = 0) { val jobParameters: JobParameters = JobParametersBuilder() .addLong("timestamp", System.currentTimeMillis()) + .addLong("day", dayOffset) .toJobParameters() jobLauncher.run(jobRegistry.getJob("crawlingJob"), jobParameters) diff --git a/idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageParser.kt b/idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageParser.kt index ac4ae0b8..9d6a684c 100644 --- a/idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageParser.kt +++ b/idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageParser.kt @@ -9,7 +9,7 @@ import java.time.Duration import java.time.LocalDate import java.time.format.DateTimeFormatter -class WorknetPageParser { +class WorknetPageParser(dayOffset: Long = 0) { private var postingCount = 0 private var crawlingUrl: String = "" private var lastPageJobPostingCount: Int = 1 @@ -20,7 +20,7 @@ class WorknetPageParser { init { val driver = DriverInitializer.init() driver.safeUse { - getCrawlingURL() + getCrawlingURL(dayOffset) moveToPage(driver) getPostingCount(driver) calculatePageInfo() @@ -45,9 +45,10 @@ class WorknetPageParser { "pageIndex=$currentPage") } - private fun getCrawlingURL() { + private fun getCrawlingURL(dayOffset: Long) { + val targetDate = LocalDate.now().minusDays(dayOffset) crawlingUrl = CrawlerConsts.CRAWLING_TARGET_URL_FORMAT.value - .replace("{yesterday}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))) + .replace("{yesterday}", targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))) .replace("{pageIndex}", "1") } diff --git a/idle-batch/src/main/kotlin/com/swm/idle/batch/job/JobConfig.kt b/idle-batch/src/main/kotlin/com/swm/idle/batch/job/JobConfig.kt index 56147d27..11c32a65 100644 --- a/idle-batch/src/main/kotlin/com/swm/idle/batch/job/JobConfig.kt +++ b/idle-batch/src/main/kotlin/com/swm/idle/batch/job/JobConfig.kt @@ -3,7 +3,9 @@ package com.swm.idle.batch.job import com.swm.idle.batch.common.dto.CrawledJobPostingDto import com.swm.idle.batch.step.PostingProcessor import com.swm.idle.batch.step.PostingReader +import com.swm.idle.batch.step.PostingWriter import com.swm.idle.domain.jobposting.entity.jpa.CrawledJobPosting +import org.springframework.beans.factory.annotation.Value import jakarta.persistence.EntityManagerFactory import org.springframework.batch.core.Step import org.springframework.batch.core.Job @@ -14,12 +16,11 @@ import org.springframework.batch.core.configuration.annotation.StepScope import org.springframework.batch.core.job.builder.JobBuilder import org.springframework.batch.core.repository.JobRepository import org.springframework.batch.core.step.builder.StepBuilder -import org.springframework.batch.item.Chunk import org.springframework.batch.item.ItemProcessor import org.springframework.batch.item.ItemReader import org.springframework.batch.item.ItemWriter -import org.springframework.batch.item.database.JpaItemWriter import org.springframework.core.task.SimpleAsyncTaskExecutor +import org.springframework.data.redis.core.RedisTemplate import org.springframework.transaction.PlatformTransactionManager @Configuration @@ -27,25 +28,28 @@ import org.springframework.transaction.PlatformTransactionManager class JobConfig( private val jobRepository: JobRepository, private val transactionManager: PlatformTransactionManager, - private val entityManagerFactory: EntityManagerFactory + private val entityManagerFactory: EntityManagerFactory, + private val redisTemplate: RedisTemplate ) { @Bean - fun crawlingJob(): Job { + fun crawlingJob(crawlStep: Step): Job { return JobBuilder("crawlingJob", jobRepository) - .start(crawlStep()) + .start(crawlStep) .preventRestart() .build() } @Bean - fun crawlStep(): Step { + fun crawlStep( + postingReader: ItemReader>, + postingWriter: ItemWriter> + ): Step { return StepBuilder("crawlStep", jobRepository) - .chunk, - List>(1, transactionManager) - .reader(postingReader()) + .chunk, List>(1, transactionManager) + .reader(postingReader) .processor(postingProcessor()) - .writer(postingWriter()) + .writer(postingWriter) .taskExecutor(taskExecutor()) .allowStartIfComplete(true) .build() @@ -54,28 +58,30 @@ class JobConfig( @Bean fun taskExecutor(): SimpleAsyncTaskExecutor { return SimpleAsyncTaskExecutor().apply { - this.setConcurrencyLimit(4) + this.concurrencyLimit = 4 } } @Bean @StepScope - fun postingReader(): ItemReader> = PostingReader() + fun postingReader( + @Value("#{jobParameters['day']}") dayParam: Long? + ): ItemReader> { + val day = dayParam?: 0L + return PostingReader(day) + } @Bean - fun postingProcessor(): ItemProcessor, out List> = PostingProcessor() + fun postingProcessor(): ItemProcessor, out List> { + return PostingProcessor() + } @Bean - fun postingWriter(): ItemWriter> { - val jpaWriter = JpaItemWriter() - jpaWriter.setEntityManagerFactory(entityManagerFactory) - - return ItemWriter { items -> - val chunk = Chunk() - items.forEach { itemList -> - chunk.addAll(itemList) - } - jpaWriter.write(chunk) - } + @StepScope + fun postingWriter( + @Value("#{jobParameters['day']}") dayParam: Long? + ): ItemWriter> { + val day = dayParam?: 0L + return PostingWriter(entityManagerFactory, redisTemplate, day) } } \ No newline at end of file diff --git a/idle-batch/src/main/kotlin/com/swm/idle/batch/step/PostingReader.kt b/idle-batch/src/main/kotlin/com/swm/idle/batch/step/PostingReader.kt index 54f8fed2..8692282c 100644 --- a/idle-batch/src/main/kotlin/com/swm/idle/batch/step/PostingReader.kt +++ b/idle-batch/src/main/kotlin/com/swm/idle/batch/step/PostingReader.kt @@ -6,8 +6,11 @@ import com.swm.idle.batch.crawler.WorknetPostParser import org.springframework.batch.item.ItemStreamReader import java.util.concurrent.atomic.AtomicInteger -class PostingReader: ItemStreamReader> { - val pageParser: WorknetPageParser = WorknetPageParser() +class PostingReader( + dayOffset: Long = 0 +) : ItemStreamReader> { + + val pageParser: WorknetPageParser = WorknetPageParser(dayOffset) companion object { var pageIndex = AtomicInteger(1) @@ -27,4 +30,4 @@ class PostingReader: ItemStreamReader> { return posts } -} \ No newline at end of file +} diff --git a/idle-batch/src/main/kotlin/com/swm/idle/batch/step/PostingWriter.kt b/idle-batch/src/main/kotlin/com/swm/idle/batch/step/PostingWriter.kt new file mode 100644 index 00000000..6f55ad5e --- /dev/null +++ b/idle-batch/src/main/kotlin/com/swm/idle/batch/step/PostingWriter.kt @@ -0,0 +1,51 @@ +package com.swm.idle.batch.step + +import com.swm.idle.domain.jobposting.entity.jpa.CrawledJobPosting +import jakarta.persistence.EntityManagerFactory +import org.springframework.batch.item.Chunk +import org.springframework.batch.item.ItemWriter +import org.springframework.batch.item.database.JpaItemWriter +import org.springframework.data.geo.Point +import org.springframework.data.redis.core.RedisTemplate +import java.time.Duration +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class PostingWriter( + private val entityManagerFactory: EntityManagerFactory, + private val redisTemplate: RedisTemplate, + private val dayOffset: Long +) : ItemWriter> { + + private val jpaWriter = JpaItemWriter().apply { + setEntityManagerFactory(entityManagerFactory) + } + + override fun write(chunk: Chunk>) { + val flatChunk = Chunk() + chunk.forEach { itemList -> + flatChunk.addAll(itemList) + } + + jpaWriter.write(flatChunk) + + chunk.forEach { itemList -> + saveToRedis(itemList) + } + } + + private fun saveToRedis(itemList: List) { + val today = LocalDate.now() + val redisKey = "job_postings_geo_${today.format(DateTimeFormatter.BASIC_ISO_DATE)}" // 예: job_postings_geo_20240520 + + itemList.forEach { posting -> + val added = redisTemplate.opsForGeo().add( + redisKey, + Point(posting.location.x, posting.location.y), + posting.id.toString() + ) + } + + redisTemplate.expire(redisKey, Duration.ofDays(13-dayOffset)) + } +} \ No newline at end of file diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt index 28bb3098..3e627063 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt @@ -13,6 +13,7 @@ class ChatMessage( senderId: UUID, receiverId: UUID, content: String, + sequence: Long, ) : BaseEntity() { @Column(updatable = false) @@ -32,7 +33,8 @@ class ChatMessage( private set @Column(updatable = false) - val isRead: Boolean = false + var sequence: Long = sequence + private set init { require(content.isNotBlank()) { "채팅 메시지는 최소 1자 이상 입력해야 합니다." } diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisTemplate.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt similarity index 74% rename from idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisTemplate.kt rename to idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt index afdf2c72..522599b1 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisTemplate.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt @@ -6,26 +6,12 @@ import com.swm.idle.domain.chat.entity.jpa.ChatMessage import com.swm.idle.domain.chat.vo.ReadMessage import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Component -import java.time.Duration -import java.util.* @Component -class ChatRedisTemplate( +class ChatRedisPublisher( private val redisTemplate: RedisTemplate, private val objectMapper: ObjectMapper ) { - fun isChatting(userId: UUID): Boolean { - return redisTemplate.hasKey(userId.toString()) - } - - fun delete(userId: String) { - redisTemplate.delete(userId) - } - - fun setSession(userId: String, duration: Duration) { - redisTemplate.opsForValue().set(userId,"active",duration) - } - fun publish(chatMessage: ChatMessage) { val message = objectMapper.writeValueAsString( mapOf(TYPE to SEND_MESSAGE, DATA to chatMessage) diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt index 76286f35..29807840 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt @@ -21,17 +21,17 @@ class ChatRedisSubscriber( val actualJson = objectMapper.readTree(rawJson).asText() val jsonNode = objectMapper.readTree(actualJson) - when (jsonNode.get(ChatRedisTemplate.TYPE).asText()) { - ChatRedisTemplate.SEND_MESSAGE -> { + when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) { + ChatRedisPublisher.SEND_MESSAGE -> { val chatMessage: ChatMessage = objectMapper.treeToValue( - jsonNode.get(ChatRedisTemplate.DATA), ChatMessage::class.java + jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java ) eventPublisher.publishEvent(chatMessage) } - ChatRedisTemplate.READ_MESSAGE -> { + ChatRedisPublisher.READ_MESSAGE -> { val readMessage: ReadMessage = objectMapper.treeToValue( - jsonNode.get(ChatRedisTemplate.DATA), ReadMessage::class.java + jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java ) eventPublisher.publishEvent(readMessage) diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt index bdc7b2ec..edd1cc91 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt @@ -11,17 +11,6 @@ import java.util.* @Repository interface ChatMessageRepository : JpaRepository { - @Transactional - @Modifying(clearAutomatically = true) - @Query(""" - UPDATE ChatMessage cm - SET cm.isRead = true - WHERE cm.chatRoomId = :chatroomId - AND cm.isRead = false - AND cm.receiverId = :readUserId - """) - fun readByChatroomId(@Param("chatroomId") chatroomId: UUID, @Param("readUserId") readUserId: UUID) - @Query(value = """ SELECT * FROM chat_message diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRedisRepository.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRedisRepository.kt new file mode 100644 index 00000000..c165c90c --- /dev/null +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRedisRepository.kt @@ -0,0 +1,66 @@ +package com.swm.idle.domain.chat.repository + +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Component +import java.time.Duration +import java.util.* + +@Component +class ChatRedisRepository( + private val redisTemplate: RedisTemplate, +) { + + fun removeUnreadChatRoom(receiverId: UUID, chatRoomId: String) { + val key = "unread_chatroom:${receiverId}" + redisTemplate.opsForSet().remove(key, chatRoomId) + } + + fun addUnreadChatRoom(receiverId: String, chatRoomId: String) { + val key = "unread_chatroom:${receiverId}" + redisTemplate.opsForSet().add(key, chatRoomId) + } + + fun updateReadSequence(chatRoomId: String, messageSequence: String, userId: UUID) { + val key = "chatroom_read_sequence:$chatRoomId:$userId" + redisTemplate.opsForValue().set(key, messageSequence) + } + + fun getReadSequence(userId: UUID, chatRoomId: UUID) : Long{ + val key = "chatroom_read_sequence:$chatRoomId:$userId" + return redisTemplate.opsForValue().get(key)?.toLong() ?: 0L + } + + fun getChatRoomSequence(chatRoomId: String): Long { + val key = "chatroom_sequence:$chatRoomId" + return redisTemplate.opsForValue().increment(key)?:1L + } + + fun getUnreadChatRooms(userId: UUID): Set { + val key = "unread_chatroom:$userId" + val members = redisTemplate.opsForSet().members(key) ?: return emptySet() + return members.mapNotNull { it }.toSet() + } + + fun getReadSequences(userId: UUID, chatRoomIds: Set): Map { + if (chatRoomIds.isEmpty()) return emptyMap() + + val keys = chatRoomIds.map { chatRoomId -> "chatroom_read_sequence:$chatRoomId:$userId" } + val values = redisTemplate.opsForValue().multiGet(keys) ?: emptyList() + return keys.zip(values).associate { (key, value) -> + val chatRoomId = key.split(":")[1] + chatRoomId to (value?.toLongOrNull() ?: 0L) + } + } + + fun isChatting(userId: UUID): Boolean { + return redisTemplate.hasKey(userId.toString()) + } + + fun delete(userId: String) { + redisTemplate.delete(userId) + } + + fun setSession(userId: String, duration: Duration) { + redisTemplate.opsForValue().set(userId,"active",duration) + } +} diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt index e85661be..b117f8ef 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt @@ -13,83 +13,24 @@ interface ChatRoomRepository : JpaRepository { fun findByCarerIdAndCenterId(carerId: UUID, centerId: UUID): ChatRoom? - @Query( - """ - WITH FilteredChatRooms AS ( - SELECT - cr.id AS chat_room_id, - cr.center_id - FROM chat_room cr - WHERE cr.carer_id = :userId - ), - - UnreadMessageCounts AS ( - SELECT - cm.chat_room_id, - COUNT(*) AS unread_count - FROM chat_message cm - WHERE cm.chat_room_id IN (SELECT chat_room_id FROM FilteredChatRooms) - AND cm.is_read = false - AND cm.receiver_id = :userId - GROUP BY cm.chat_room_id - ) - - SELECT - fcr.chat_room_id AS chatRoomId, - fcr.center_id AS opponentId, - umc.unread_count AS unreadCount, - cm.content AS lastMessage, - cm.created_at AS lastMessageTime - FROM FilteredChatRooms fcr - JOIN UnreadMessageCounts umc ON fcr.chat_room_id = umc.chat_room_id + @Query(""" + SELECT + cr.id AS chatRoomId, + cr.carer_id AS carerId, + cr.center_id AS centerId, + cm.content AS lastMessage, + cm.created_at AS lastMessageTime, + cm.sequence AS lastSequence + FROM chat_room cr JOIN LATERAL ( - SELECT content, created_at - FROM chat_message - WHERE chat_room_id = fcr.chat_room_id AND is_read = false - ORDER BY id DESC - LIMIT 1 - ) cm; -""", nativeQuery = true - ) - fun carerFindChatRooms(@Param("userId") userId: UUID): List - - @Query( - """ - WITH FilteredChatRooms AS ( - SELECT - cr.id AS chat_room_id, - cr.carer_id - FROM chat_room cr - WHERE cr.center_id = :userId - ), - - UnreadMessageCounts AS ( - SELECT - cm.chat_room_id, - COUNT(*) AS unread_count + SELECT cm.content, cm.created_at, cm.sequence FROM chat_message cm - WHERE cm.chat_room_id IN (SELECT chat_room_id FROM FilteredChatRooms) - AND cm.is_read = false - AND cm.receiver_id = :userId - GROUP BY cm.chat_room_id - ) - - SELECT - fcr.chat_room_id AS chatRoomId, - fcr.carer_id AS opponentId, - umc.unread_count AS unreadCount, - cm.content AS lastMessage, - cm.created_at AS lastMessageTime - FROM FilteredChatRooms fcr - JOIN UnreadMessageCounts umc ON fcr.chat_room_id = umc.chat_room_id - JOIN LATERAL ( - SELECT content, created_at - FROM chat_message - WHERE chat_room_id = fcr.chat_room_id AND is_read = false - ORDER BY id DESC + WHERE cr.id = cm.chat_room_id + ORDER BY cm.id DESC LIMIT 1 - ) cm; + ) cm + WHERE cr.id IN :chatRoomIds """, nativeQuery = true ) - fun centerFindChatRooms(@Param("userId") userId: UUID): List + fun findChatRoomsWithLastMessages(@Param("chatRoomIds") chatRoomIds: Set): List } \ No newline at end of file diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfo.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfo.kt index d158213f..87536d30 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfo.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfo.kt @@ -8,32 +8,32 @@ data class ChatRoomSummaryInfo( val chatRoomId: UUID, val lastMessage: String, val lastMessageTime: LocalDateTime, - val count: Int, - val opponentId: UUID, - var opponentName: String, - var opponentProfileImageUrl: String?, + var count: Long, + var opponentId: UUID, + var opponentName: String = "알 수 없음", + var opponentProfileImageUrl: String? = null ) { - constructor(chatRoomId: ByteArray, - lastMessage: String, - lastMessageTime: LocalDateTime, - count: Int, - opponentId: ByteArray, - ) : this( - fromByteArray(chatRoomId), - lastMessage, - lastMessageTime, - count, - fromByteArray(opponentId), - "알 수 없음", - null, - ) + constructor( + chatRoomId: ByteArray, + opponentId: ByteArray, + lastMessage: String, + lastMessageTime: LocalDateTime, + count: Long + ) : this( + chatRoomId = fromByteArray(chatRoomId), + lastMessage = lastMessage, + lastMessageTime = lastMessageTime, + count= count, + opponentId =fromByteArray(opponentId), + ) companion object { fun fromByteArray(array: ByteArray): UUID { val buffer = ByteBuffer.wrap(array) - val mostSigBits = buffer.getLong() - val leastSigBits = buffer.getLong() + val mostSigBits = buffer.long + val leastSigBits = buffer.long return UUID(mostSigBits, leastSigBits) } } -} \ No newline at end of file +} + diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfoProjection.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfoProjection.kt index 00caecd8..4d965375 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfoProjection.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfoProjection.kt @@ -4,8 +4,9 @@ import java.time.LocalDateTime interface ChatRoomSummaryInfoProjection { fun getChatRoomId(): ByteArray - fun getOpponentId(): ByteArray - fun getUnreadCount(): Int + fun getCarerId(): ByteArray + fun getCenterId(): ByteArray fun getLastMessage(): String fun getLastMessageTime(): LocalDateTime + fun getLastSequence(): Long? } \ No newline at end of file diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt index 70210f92..b83011b7 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt @@ -5,4 +5,5 @@ import java.util.* data class ReadMessage( val chatRoomId: UUID, val receiverId: UUID, - val readUserId: UUID,) + val readUserId: UUID, + val sequence:Long) diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/common/dto/CrawlingJobPostingPreviewDto.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/common/dto/CrawlingJobPostingPreviewDto.kt index baba6e88..e7272048 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/common/dto/CrawlingJobPostingPreviewDto.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/common/dto/CrawlingJobPostingPreviewDto.kt @@ -10,7 +10,8 @@ data class CrawlingJobPostingPreviewDto( constructor( crawledJobPosting: CrawledJobPosting, - ) : this(crawledJobPosting = crawledJobPosting, distance = 0, isFavorite = false) + distance: Int + ) : this(crawledJobPosting = crawledJobPosting, distance = distance, isFavorite = false) constructor( crawledJobPosting: CrawledJobPosting, diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/jobposting/repository/querydsl/CrawlingJobPostingSpatialQueryRepository.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/jobposting/repository/querydsl/CrawlingJobPostingSpatialQueryRepository.kt deleted file mode 100644 index 91f495b1..00000000 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/jobposting/repository/querydsl/CrawlingJobPostingSpatialQueryRepository.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.swm.idle.domain.jobposting.repository.querydsl - -import com.querydsl.core.group.GroupBy.groupBy -import com.querydsl.core.types.Projections -import com.querydsl.core.types.dsl.BooleanExpression -import com.querydsl.core.types.dsl.Expressions -import com.querydsl.jpa.impl.JPAQueryFactory -import com.swm.idle.domain.common.dto.CrawlingJobPostingPreviewDto -import com.swm.idle.domain.common.enums.EntityStatus -import com.swm.idle.domain.jobposting.entity.jpa.QCrawledJobPosting.crawledJobPosting -import com.swm.idle.domain.jobposting.entity.jpa.QJobPostingFavorite.jobPostingFavorite -import org.locationtech.jts.geom.Point -import org.springframework.stereotype.Repository -import java.util.* - -@Repository -class CrawlingJobPostingSpatialQueryRepository( - private val jpaQueryFactory: JPAQueryFactory, -) { - - fun findAllInRange( - carerId: UUID, - location: Point, - next: UUID?, - limit: Long, - ): List { - val crawledJobPostingIds = jpaQueryFactory - .select(crawledJobPosting.id) - .from(crawledJobPosting) - .where( - isExistInRange(location) - .and(next?.let { crawledJobPosting.id.goe(it) }) - .and(crawledJobPosting.entityStatus.eq(EntityStatus.ACTIVE)) - ) - .limit(limit) - .fetch() - - if (crawledJobPostingIds.isEmpty()) { - return emptyList() - } - - return jpaQueryFactory - .select(crawledJobPosting, jobPostingFavorite) - .from(crawledJobPosting) - .leftJoin(jobPostingFavorite) - .on( - crawledJobPosting.id.eq(jobPostingFavorite.jobPostingId) - .and(jobPostingFavorite.carerId.eq(carerId)) - ) - .where(crawledJobPosting.id.`in`(crawledJobPostingIds)) - .transform( - groupBy(crawledJobPosting.id) - .list( - Projections.constructor( - CrawlingJobPostingPreviewDto::class.java, - crawledJobPosting, - jobPostingFavorite.id.isNotNull - .and(jobPostingFavorite.entityStatus.eq(EntityStatus.ACTIVE)) - ) - ) - ) - } - - private fun isExistInRange( - location: Point, - ): BooleanExpression { - return Expressions.booleanTemplate( - "ST_Contains(ST_BUFFER({0}, 5000), {1})", - location, - crawledJobPosting.location, - ) - } - -} diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/jobposting/repository/redis/RedisJobPostingRepository.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/jobposting/repository/redis/RedisJobPostingRepository.kt new file mode 100644 index 00000000..662b0b01 --- /dev/null +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/jobposting/repository/redis/RedisJobPostingRepository.kt @@ -0,0 +1,106 @@ +package com.swm.idle.domain.jobposting.repository.redis + +import org.locationtech.jts.geom.Point +import org.springframework.data.geo.Circle +import org.springframework.data.geo.Distance +import org.springframework.data.geo.GeoResults +import org.springframework.data.geo.Metrics +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.* + +@Repository +class RedisJobPostingRepository( + private val redisTemplate: RedisTemplate, +) { + companion object { + private const val REDIS_TTL_DAYS = 13L + private const val REDIS_KEY_PREFIX = "job_postings_geo_" + } + + fun findByLocationAndDistance( + location: Point, + distance: Long, + limit: Long, + next: UUID? + ): List { + val keys = makeRedisKeys() + val circle = Circle( + org.springframework.data.geo.Point(location.x, location.y), + Distance(distance.toDouble(), + Metrics.KILOMETERS) + ) + + if(next == null) {//first + val ids = findPostingIds(keys, circle) + return ids.take(limit.toInt()) + } + + return getPagedGeoUuids(keys, circle, next, limit) + } + + private fun makeRedisKeys(): List { + val today = LocalDate.now() + val keys = (0 until REDIS_TTL_DAYS).map { offset -> + val date = today.minusDays(offset) + "$REDIS_KEY_PREFIX${date.format(DateTimeFormatter.BASIC_ISO_DATE)}" + } + return keys + } + + private fun findPostingIds( + keys: List, + circle: Circle + ): MutableList { + val ids = mutableListOf() + + keys.forEach { key -> + val geoResults = redisTemplate.opsForGeo().radius(key, circle) as GeoResults> + + val uuids = geoResults.content.mapNotNull { geoLocation -> + UUID.fromString(geoLocation.content.name) + } + ids.addAll(uuids) + } + return ids + } + + fun getPagedGeoUuids( + keys: List, + circle: Circle, + next: UUID, + limit: Long + ): List { + val distanceMap = mutableListOf>() + + keys.forEach { key -> + val geoResults = redisTemplate.opsForGeo().radius(key, circle) as GeoResults> + geoResults.content.forEach { geoLocation -> + val uuid = runCatching { UUID.fromString(geoLocation.content.name) }.getOrNull() + val distance = geoLocation.distance?.value + if (uuid != null && distance != null) { + distanceMap.add(uuid to distance) + } + } + } + + val sortedIdsByDistance = distanceMap.sortedBy { it.second }.map { it.first } + + val result = mutableListOf() + var foundNext = false + + for (id in sortedIdsByDistance) { + if (foundNext) { + result.add(id) + if (result.size == limit.toInt()) break + } + + if (id == next) foundNext = true + } + + return result + } +} \ No newline at end of file diff --git a/idle-domain/src/main/kotlin/com/swm/idle/domain/notification/jpa/DeviceToken.kt b/idle-domain/src/main/kotlin/com/swm/idle/domain/notification/jpa/DeviceToken.kt index e8e42469..f2b73df6 100644 --- a/idle-domain/src/main/kotlin/com/swm/idle/domain/notification/jpa/DeviceToken.kt +++ b/idle-domain/src/main/kotlin/com/swm/idle/domain/notification/jpa/DeviceToken.kt @@ -29,8 +29,8 @@ class DeviceToken( var userType: UserType = userType private set - fun updateUserId(userId: UUID) { - this.userId = userId + fun updateToken(token: String) { + this.deviceToken = token } } diff --git a/idle-domain/src/main/resources/db/migration/V4__add_device_token_index.sql b/idle-domain/src/main/resources/db/migration/V4__add_device_token_index.sql new file mode 100644 index 00000000..f1df6eee --- /dev/null +++ b/idle-domain/src/main/resources/db/migration/V4__add_device_token_index.sql @@ -0,0 +1,2 @@ +-- V4__add_device_token_index.sql +CREATE UNIQUE INDEX idx_unique_device_token ON device_token(device_token); \ No newline at end of file diff --git a/idle-domain/src/main/resources/db/migration/V5__change_chat_message_sequence.sql b/idle-domain/src/main/resources/db/migration/V5__change_chat_message_sequence.sql new file mode 100644 index 00000000..11ebe1f0 --- /dev/null +++ b/idle-domain/src/main/resources/db/migration/V5__change_chat_message_sequence.sql @@ -0,0 +1,7 @@ +-- V5__change_chat_message_sequence.sql + +ALTER TABLE chat_message +DROP COLUMN is_read; + +ALTER TABLE chat_message + ADD COLUMN sequence BIGINT NOT NULL; diff --git a/idle-domain/src/main/resources/db/migration/V6__add_chat_room_unique_index.sql b/idle-domain/src/main/resources/db/migration/V6__add_chat_room_unique_index.sql new file mode 100644 index 00000000..0147f19b --- /dev/null +++ b/idle-domain/src/main/resources/db/migration/V6__add_chat_room_unique_index.sql @@ -0,0 +1,4 @@ +-- V6__add_chat_room_unique_index.sql + +ALTER TABLE chat_room + ADD UNIQUE KEY uniq_carer_center (carer_id, center_id); \ No newline at end of file diff --git a/idle-domain/src/test/kotlin/com/swm/idle/ChatTest.kt b/idle-domain/src/test/kotlin/com/swm/idle/ChatTest.kt deleted file mode 100644 index b2316e3e..00000000 --- a/idle-domain/src/test/kotlin/com/swm/idle/ChatTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.swm.idle - -import com.swm.idle.config.DomainTestApplication -import com.swm.idle.domain.chat.entity.jpa.ChatMessage -import com.swm.idle.domain.chat.repository.ChatMessageRepository -import io.kotest.core.spec.style.BehaviorSpec -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import java.util.UUID -import org.assertj.core.api.Assertions.assertThat -import org.springframework.test.context.ActiveProfiles - -@SpringBootTest(classes = [DomainTestApplication::class]) -@ActiveProfiles("test") -class ChatTest : BehaviorSpec() { - @Autowired - lateinit var chatMessageRepository: ChatMessageRepository - - val chatRoomId = UUID.randomUUID() - val senderId = UUID.randomUUID() - val receiverId = UUID.randomUUID() - - - init { - given("채팅 메시지가 여러개 주어졌을 때") { - val msg1 = ChatMessage(chatRoomId = chatRoomId, senderId = senderId, receiverId = receiverId, content = "1") - val msg2 = ChatMessage(chatRoomId = chatRoomId, senderId = senderId, receiverId = receiverId, content = "2") - val msg3 = ChatMessage(chatRoomId = chatRoomId, senderId = senderId, receiverId = UUID.randomUUID(), content = "3") - chatMessageRepository.saveAll(listOf(msg1, msg2, msg3)) - - `when`("readByChatroomId 메서드로 읽음 처리 하면") { - chatMessageRepository.readByChatroomId(chatRoomId, receiverId) - - then("해당 메시지들의 isRead 상태가 true로 업데이트 되어야 한다") { - val updatedMessages = chatMessageRepository.findAll() - val readMessages = updatedMessages.filter { it.receiverId == receiverId && it.chatRoomId == chatRoomId } - assertThat(readMessages).allMatch { it.isRead } - } - } - } - } -} diff --git a/idle-domain/src/test/kotlin/com/swm/idle/config/DomainTestApplication.kt b/idle-domain/src/test/kotlin/com/swm/idle/config/DomainTestApplication.kt deleted file mode 100644 index c42d4dd2..00000000 --- a/idle-domain/src/test/kotlin/com/swm/idle/config/DomainTestApplication.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.swm.idle.config - -import org.springframework.boot.autoconfigure.SpringBootApplication - -@SpringBootApplication(scanBasePackages = ["com.swm.idle.domain"]) -class DomainTestApplication diff --git a/idle-domain/src/test/kotlin/com/swm/idle/config/EmbeddedRedisConfig.kt b/idle-domain/src/test/kotlin/com/swm/idle/config/EmbeddedRedisConfig.kt deleted file mode 100644 index ce7075be..00000000 --- a/idle-domain/src/test/kotlin/com/swm/idle/config/EmbeddedRedisConfig.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.swm.idle.config - -import jakarta.annotation.PostConstruct -import jakarta.annotation.PreDestroy -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Profile -import redis.embedded.RedisServer - -@Configuration -@Profile("test") -class EmbeddedRedisConfig { - - private val redisPort = 6379 - private val redisServer = RedisServer(redisPort) - - @PostConstruct - fun startRedis() { - if (!redisServer.isActive) { - redisServer.start() - } - } - - @PreDestroy - fun stopRedis() { - redisServer.stop() - } -} diff --git a/idle-domain/src/test/kotlin/com/swm/idle/config/ProjectConfig.kt b/idle-domain/src/test/kotlin/com/swm/idle/config/ProjectConfig.kt deleted file mode 100644 index 18e80c4a..00000000 --- a/idle-domain/src/test/kotlin/com/swm/idle/config/ProjectConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.swm.idle.config - -import io.kotest.core.config.AbstractProjectConfig -import io.kotest.core.extensions.Extension -import io.kotest.extensions.spring.SpringExtension - -object ProjectConfig : AbstractProjectConfig() { - override fun extensions(): List = listOf(SpringExtension) -} diff --git a/idle-domain/src/test/resources/application-test.yml b/idle-domain/src/test/resources/application-test.yml deleted file mode 100644 index 544399d0..00000000 --- a/idle-domain/src/test/resources/application-test.yml +++ /dev/null @@ -1,18 +0,0 @@ -spring: - datasource: - url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE - driver-class-name: org.h2.Driver - username: sa - password: - jpa: - hibernate: - ddl-auto: create-drop - show-sql: true - database-platform: org.hibernate.dialect.H2Dialect - flyway: - enabled: false - data: - redis: - host: localhost - port: 6379 - password: "" \ No newline at end of file diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/api/BatchApi.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/api/BatchApi.kt index 7107dd6c..9e6e87f4 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/api/BatchApi.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/api/BatchApi.kt @@ -16,5 +16,5 @@ interface BatchApi { @Operation(summary = "배치 엔트포인트 실행 API") @GetMapping @ResponseStatus(HttpStatus.OK) - fun launchBatch() + fun launchBatch(day: Long = 0) } \ No newline at end of file diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/controller/BatchController.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/controller/BatchController.kt index f7d01d7d..70b7d11d 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/controller/BatchController.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/batch/controller/BatchController.kt @@ -8,5 +8,5 @@ import org.springframework.web.bind.annotation.RestController class BatchController( private val jobLauncher: CrawlingJobLauncher, ) : BatchApi { - override fun launchBatch() = jobLauncher.jobStart() + override fun launchBatch(day: Long) = jobLauncher.jobStart(day) } \ No newline at end of file diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt index 97f517fc..4734e1c8 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt @@ -26,7 +26,7 @@ interface ChatCarerApi { @GetMapping("/chatrooms/{chatroom-id}/messages") @ResponseStatus(HttpStatus.OK) fun recentMessages(@PathVariable(value = "chatroom-id") chatroomId: UUID, - @RequestParam(value = "message-id", required = false) messageId: UUID?): List + @RequestParam(value = "message-id", required = false) messageId: UUID?): ChatMessageResponse @Secured @Operation(summary = "보호사의 채팅방 요약 목록 조회 API") diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt index bf7dea12..0dee53bc 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt @@ -26,7 +26,7 @@ interface ChatCenterApi { @GetMapping("/chatrooms/{chatroom-id}/messages") @ResponseStatus(HttpStatus.OK) fun recentMessages(@PathVariable(value = "chatroom-id") chatroomId: UUID, - @RequestParam(value = "message-id", required = false) messageId: UUID?): List + @RequestParam(value = "message-id", required = false) messageId: UUID?): ChatMessageResponse @Secured @Operation(summary = "센터장의 채팅방 요약 목록 조회 API") diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt index a0f7c4bd..676809ba 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt @@ -1,7 +1,7 @@ package com.swm.idle.presentation.chat.config import com.swm.idle.application.common.properties.JwtTokenProperties -import com.swm.idle.domain.chat.event.ChatRedisTemplate +import com.swm.idle.domain.chat.repository.ChatRedisRepository import com.swm.idle.support.security.util.JwtTokenProvider import org.springframework.http.server.ServerHttpRequest import org.springframework.http.server.ServerHttpResponse @@ -14,7 +14,7 @@ import java.time.Duration @Component class ChatHandshakeInterceptor( private val jwtTokenProperties: JwtTokenProperties, - private val redisTemplate : ChatRedisTemplate, + private val chatRedisRepository : ChatRedisRepository, ): HandshakeInterceptor { override fun beforeHandshake( @@ -35,7 +35,7 @@ class ChatHandshakeInterceptor( val userId = claims.customClaims["userId"] as String attributes["userId"] = userId - redisTemplate.setSession(userId, Duration.ofHours(24)) + chatRedisRepository.setSession(userId, Duration.ofHours(24)) return true } diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketDisconnectListener.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketDisconnectListener.kt index fad27f95..8805c008 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketDisconnectListener.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketDisconnectListener.kt @@ -1,6 +1,6 @@ package com.swm.idle.presentation.chat.config -import com.swm.idle.domain.chat.event.ChatRedisTemplate +import com.swm.idle.domain.chat.repository.ChatRedisRepository import org.springframework.context.event.EventListener import org.springframework.messaging.simp.stomp.StompHeaderAccessor import org.springframework.stereotype.Component @@ -8,7 +8,7 @@ import org.springframework.web.socket.messaging.SessionDisconnectEvent @Component class WebSocketDisconnectListener( - private val redisTemplate: ChatRedisTemplate + private val chatRedisRepository: ChatRedisRepository, ) { @EventListener @@ -17,7 +17,7 @@ class WebSocketDisconnectListener( val userId = headers.sessionAttributes?.get("userId") as? String if (userId != null) { - redisTemplate.delete(userId) + chatRedisRepository.delete(userId) } } } diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt index 6c58b93a..dec5aa36 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt @@ -22,7 +22,7 @@ class ChatCarerController( return chatMessageService.getChatroomSummary(true) } - override fun recentMessages(chatroomId: UUID, messageId: UUID?): List { - return chatMessageService.getRecentMessages(chatroomId, messageId) + override fun recentMessages(chatroomId: UUID, messageId: UUID?): ChatMessageResponse { + return chatMessageService.getRecentMessages(chatroomId, messageId, true) } } \ No newline at end of file diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt index 9abc605c..6b3b1173 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt @@ -22,7 +22,7 @@ class ChatCenterController( return chatMessageService.getChatroomSummary(false) } - override fun recentMessages(chatroomId: UUID, messageId: UUID?): List { - return chatMessageService.getRecentMessages(chatroomId, messageId) + override fun recentMessages(chatroomId: UUID, messageId: UUID?): ChatMessageResponse { + return chatMessageService.getRecentMessages(chatroomId, messageId, false) } } \ No newline at end of file diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt index d0421a72..b5d91040 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt @@ -1,7 +1,7 @@ package com.swm.idle.presentation.chat.controller import com.swm.idle.application.chat.facade.ChatFacadeService -import com.swm.idle.support.transfer.chat.ReadChatMessagesReqeust +import com.swm.idle.support.transfer.chat.ReadChatMessageRequest import com.swm.idle.support.transfer.chat.SendChatMessageRequest import org.springframework.messaging.handler.annotation.MessageMapping import org.springframework.messaging.handler.annotation.Payload @@ -18,27 +18,27 @@ class ChatSocketController ( fun carerSendMessage(@Payload request: SendChatMessageRequest, headerAccessor: SimpMessageHeaderAccessor) { val userId = UUID.fromString(headerAccessor.sessionAttributes?.get("userId") as String) - chatMessageService.carerSend(request, userId) + chatMessageService.send(request, userId, true) } @MessageMapping("/read/carer") - fun carerRead(@Payload request: ReadChatMessagesReqeust, - headerAccessor: SimpMessageHeaderAccessor) { + fun carerRead(@Payload request: ReadChatMessageRequest, + headerAccessor: SimpMessageHeaderAccessor) { val userId = UUID.fromString(headerAccessor.sessionAttributes?.get("userId") as String) - chatMessageService.carerRead(request, userId) + chatMessageService.read(request, userId, true) } @MessageMapping("/send/center") fun centerSendMessage(@Payload request: SendChatMessageRequest, headerAccessor: SimpMessageHeaderAccessor) { val userId = UUID.fromString(headerAccessor.sessionAttributes?.get("userId") as String) - chatMessageService.centerSend(request, userId) + chatMessageService.send(request, userId, false) } @MessageMapping("/read/center") - fun centerRead(@Payload request: ReadChatMessagesReqeust, - headerAccessor: SimpMessageHeaderAccessor) { + fun centerRead(@Payload request: ReadChatMessageRequest, + headerAccessor: SimpMessageHeaderAccessor) { val userId = UUID.fromString(headerAccessor.sessionAttributes?.get("userId") as String) - chatMessageService.centerRead(request, userId) + chatMessageService.read(request, userId, false) } } \ No newline at end of file diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt index 413fea58..5c918f01 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt @@ -2,7 +2,7 @@ package com.swm.idle.presentation.chat.handler import com.swm.idle.domain.chat.entity.jpa.ChatMessage import com.swm.idle.domain.chat.vo.ReadMessage -import com.swm.idle.support.transfer.chat.ChatMessageResponse +import com.swm.idle.support.transfer.chat.ChatMessageSocketResponse import com.swm.idle.support.transfer.chat.ReadNoti import org.springframework.context.event.EventListener import org.springframework.messaging.simp.SimpMessageSendingOperations @@ -14,8 +14,8 @@ class ChatHandler( ) { @EventListener fun handleSendMessage(sendMessage: ChatMessage) { - messageTemplate.convertAndSend("/sub/${sendMessage.receiverId}", ChatMessageResponse(sendMessage)) - messageTemplate.convertAndSend("/sub/${sendMessage.senderId}", ChatMessageResponse(sendMessage)) + messageTemplate.convertAndSend("/sub/${sendMessage.receiverId}", ChatMessageSocketResponse(sendMessage)) + messageTemplate.convertAndSend("/sub/${sendMessage.senderId}", ChatMessageSocketResponse(sendMessage)) } @EventListener diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/api/CrawlingJobPostingApi.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/api/CrawlingJobPostingApi.kt index ec6b29ae..47a85dcf 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/api/CrawlingJobPostingApi.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/api/CrawlingJobPostingApi.kt @@ -1,7 +1,7 @@ package com.swm.idle.presentation.jobposting.api import com.swm.idle.presentation.common.security.annotation.Secured -import com.swm.idle.support.transfer.common.CursorScrollRequest +import com.swm.idle.support.transfer.common.CrawlingCursorScrollRequest import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingFavoriteResponse import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingScrollResponse import com.swm.idle.support.transfer.jobposting.common.CrawlingJobPostingResponse @@ -28,7 +28,7 @@ interface CrawlingJobPostingApi { @Operation(summary = "크롤링 공고 전체 조회 API") @GetMapping @ResponseStatus(HttpStatus.OK) - fun getCrawlingJobPostings(request: CursorScrollRequest): CrawlingJobPostingScrollResponse + fun getCrawlingJobPostings(request: CrawlingCursorScrollRequest): CrawlingJobPostingScrollResponse @Secured @Operation(summary = "즐겨찾기한 크롤링 공고 전체 조회 API") diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CarerJobPostingController.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CarerJobPostingController.kt index 2f97c9a8..d7421b3c 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CarerJobPostingController.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CarerJobPostingController.kt @@ -1,10 +1,8 @@ package com.swm.idle.presentation.jobposting.controller -import com.swm.idle.application.common.converter.PointConverter import com.swm.idle.application.common.security.getUserAuthentication -import com.swm.idle.application.jobposting.facade.CarerJobPostingFacadeService +import com.swm.idle.application.jobposting.facade.CarerPostingFacadeService import com.swm.idle.application.jobposting.facade.JobPostingFavoriteFacadeService -import com.swm.idle.application.user.carer.domain.CarerService import com.swm.idle.presentation.jobposting.api.CarerJobPostingApi import com.swm.idle.support.transfer.common.CursorScrollRequest import com.swm.idle.support.transfer.jobposting.carer.CarerAppliedJobPostingScrollResponse @@ -17,8 +15,7 @@ import java.util.* @RestController class CarerJobPostingController( - private val carerJobPostingFacadeService: CarerJobPostingFacadeService, - private val carerService: CarerService, + private val carerJobPostingFacadeService: CarerPostingFacadeService, private val jobPostingFavoriteFacadeService: JobPostingFavoriteFacadeService, ) : CarerJobPostingApi { @@ -27,28 +24,13 @@ class CarerJobPostingController( } override fun getJobPostings(request: CursorScrollRequest): CarerJobPostingScrollResponse { - val carer = carerService.getById(getUserAuthentication().userId) - - val location = PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) - - return carerJobPostingFacadeService.getJobPostingsInRange( - request = request, - location = location - ) + return carerJobPostingFacadeService.getJobPostingsInRange(request) } override fun getAppliedJobPostings( request: CursorScrollRequest, ): CarerAppliedJobPostingScrollResponse { - val carer = carerService.getById(getUserAuthentication().userId) - - return carerJobPostingFacadeService.getAppliedJobPostings( - request = request, - carerId = carer.id - ) + return carerJobPostingFacadeService.getAppliedJobPostings(request) } override fun createJobPostingFavorite( @@ -70,17 +52,7 @@ class CarerJobPostingController( } override fun getMyFavoriteJobPostings(): JobPostingFavoriteResponse { - val carer = carerService.getById(getUserAuthentication().userId) - - val location = PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) - - return carerJobPostingFacadeService.getMyFavoriteJobPostings( - carer = carer, - location = location, - ) + return carerJobPostingFacadeService.getMyFavoriteJobPostings() } } diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CenterJobPostingController.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CenterJobPostingController.kt index 0faaf68f..a0fc0f1d 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CenterJobPostingController.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CenterJobPostingController.kt @@ -1,6 +1,6 @@ package com.swm.idle.presentation.jobposting.controller -import com.swm.idle.application.jobposting.facade.CenterJobPostingFacadeService +import com.swm.idle.application.jobposting.facade.CenterPostingFacadeService import com.swm.idle.presentation.jobposting.api.CenterJobPostingApi import com.swm.idle.support.transfer.jobposting.center.CenterJobPostingListResponse import com.swm.idle.support.transfer.jobposting.center.CenterJobPostingResponse @@ -13,21 +13,15 @@ import java.util.* @RestController class CenterJobPostingController( - private val centerJobPostingFacadeService: CenterJobPostingFacadeService, + private val centerJobPostingFacadeService: CenterPostingFacadeService, ) : CenterJobPostingApi { override suspend fun createJobPosting(request: CreateJobPostingRequest) { centerJobPostingFacadeService.create(request = request) } - override fun updateJobPosting( - jobPostingId: UUID, - request: UpdateJobPostingRequest, - ) { - centerJobPostingFacadeService.update( - jobPostingId = jobPostingId, - request = request - ) + override fun updateJobPosting(jobPostingId: UUID, request: UpdateJobPostingRequest) { + centerJobPostingFacadeService.update(jobPostingId, request) } override fun deleteJobPosting(jobPostingId: UUID) { diff --git a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CrawlingJobPostingController.kt b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CrawlingJobPostingController.kt index 98b5e2c0..5e4de890 100644 --- a/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CrawlingJobPostingController.kt +++ b/idle-presentation/src/main/kotlin/com/swm/idle/presentation/jobposting/controller/CrawlingJobPostingController.kt @@ -1,11 +1,8 @@ package com.swm.idle.presentation.jobposting.controller -import com.swm.idle.application.common.converter.PointConverter -import com.swm.idle.application.common.security.getUserAuthentication -import com.swm.idle.application.jobposting.facade.CrawlingJobPostingFacadeService -import com.swm.idle.application.user.carer.domain.CarerService +import com.swm.idle.application.jobposting.facade.CrawlingPostingFacadeService import com.swm.idle.presentation.jobposting.api.CrawlingJobPostingApi -import com.swm.idle.support.transfer.common.CursorScrollRequest +import com.swm.idle.support.transfer.common.CrawlingCursorScrollRequest import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingFavoriteResponse import com.swm.idle.support.transfer.jobposting.carer.CrawlingJobPostingScrollResponse import com.swm.idle.support.transfer.jobposting.common.CrawlingJobPostingResponse @@ -14,40 +11,19 @@ import java.util.* @RestController class CrawlingJobPostingController( - private val crawlingJobPostingFacadeService: CrawlingJobPostingFacadeService, - private val carerService: CarerService, + private val crawlingPostingFacadeService: CrawlingPostingFacadeService, ) : CrawlingJobPostingApi { override fun getCrawlingJobPostingDetail(crawlingJobPostingId: UUID): CrawlingJobPostingResponse { - return crawlingJobPostingFacadeService.getCrawlingJobPosting(crawlingJobPostingId) + return crawlingPostingFacadeService.getCrawlingJobPosting(crawlingJobPostingId) } - override fun getCrawlingJobPostings(request: CursorScrollRequest): CrawlingJobPostingScrollResponse { - val carer = carerService.getById(getUserAuthentication().userId) - - val location = PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) - - return crawlingJobPostingFacadeService.getCrawlingJobPostingsInRange( - request = request, - location = location - ) + override fun getCrawlingJobPostings(request: CrawlingCursorScrollRequest): CrawlingJobPostingScrollResponse { + return crawlingPostingFacadeService.getCrawlingPostingsInRange(request) } override fun getFavoriteCrawlingJobPostings(): CrawlingJobPostingFavoriteResponse { - val carer = carerService.getById(getUserAuthentication().userId) - - val location = PointConverter.convertToPoint( - latitude = carer.latitude.toDouble(), - longitude = carer.longitude.toDouble(), - ) - - return crawlingJobPostingFacadeService.getFavoriteCrawlingJobPostings( - carer = carer, - location = location, - ) + return crawlingPostingFacadeService.getFavoriteCrawlingJobPostings() } } diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageInfo.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageInfo.kt new file mode 100644 index 00000000..242ecd29 --- /dev/null +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageInfo.kt @@ -0,0 +1,27 @@ +package com.swm.idle.support.transfer.chat + +import com.swm.idle.domain.chat.entity.jpa.ChatMessage +import java.time.LocalDateTime +import java.util.* + +data class ChatMessageInfo( + val type: ChatMessageType, + val id: UUID, + val chatroomId: UUID, + val senderId: UUID, + val receiverId: UUID, + val content: String, + val createdAt: LocalDateTime, + val sequence: Long, +) { + constructor(message: ChatMessage) : this( + type = ChatMessageType.MESSAGE, + id = message.id, + chatroomId = message.chatRoomId, + senderId = message.senderId, + receiverId = message.receiverId, + content = message.content, + createdAt = message.createdAt ?: LocalDateTime.now(), + sequence = message.sequence, + ) +} \ No newline at end of file diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageResponse.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageResponse.kt index cd07fc77..d7d49b30 100644 --- a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageResponse.kt +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageResponse.kt @@ -1,27 +1,7 @@ package com.swm.idle.support.transfer.chat -import com.swm.idle.domain.chat.entity.jpa.ChatMessage -import java.util.UUID -import java.time.LocalDateTime data class ChatMessageResponse( - val type: ChatMessageType, - val id: UUID, - val chatroomId: UUID, - val senderId: UUID, - val receiverId: UUID, - val content: String, - val createdAt: LocalDateTime, - val isRead: Boolean -) { - constructor(message: ChatMessage) : this( - type = ChatMessageType.MESSAGE, - id = message.id, - chatroomId = message.chatRoomId, - senderId = message.senderId, - receiverId = message.receiverId, - content = message.content, - isRead = message.isRead, - createdAt = message.createdAt ?: LocalDateTime.now(), - ) -} \ No newline at end of file + val chatMessageInfos: List, + val sequence: Long +) \ No newline at end of file diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageSocketResponse.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageSocketResponse.kt new file mode 100644 index 00000000..c54d822f --- /dev/null +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ChatMessageSocketResponse.kt @@ -0,0 +1,27 @@ +package com.swm.idle.support.transfer.chat + +import com.swm.idle.domain.chat.entity.jpa.ChatMessage +import java.time.LocalDateTime +import java.util.* + +class ChatMessageSocketResponse( + val type: ChatMessageType, + val id: UUID, + val chatroomId: UUID, + val receiverId: UUID, + val senderId: UUID, + val content: String, + val createdAt: LocalDateTime, + val sequence: Long, +) { + constructor(message: ChatMessage) : this( + type = ChatMessageType.MESSAGE, + id = message.id, + chatroomId = message.chatRoomId, + receiverId = message.receiverId, + senderId = message.senderId, + content = message.content, + createdAt = message.createdAt ?: LocalDateTime.now(), + sequence = message.sequence, + ) +} diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessagesReqeust.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessageRequest.kt similarity index 55% rename from idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessagesReqeust.kt rename to idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessageRequest.kt index db62cd10..1bfb0122 100644 --- a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessagesReqeust.kt +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessageRequest.kt @@ -3,7 +3,8 @@ package com.swm.idle.support.transfer.chat import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty -data class ReadChatMessagesReqeust @JsonCreator constructor( +data class ReadChatMessageRequest @JsonCreator constructor( @JsonProperty("chatroomId") val chatroomId: String, - @JsonProperty("opponentId")val opponentId: String + @JsonProperty("opponentId")val opponentId: String, + @JsonProperty("sequence")val sequence: String ) \ No newline at end of file diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadNoti.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadNoti.kt index 1fbf8164..d8c29ef4 100644 --- a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadNoti.kt +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadNoti.kt @@ -3,10 +3,16 @@ package com.swm.idle.support.transfer.chat import com.swm.idle.domain.chat.vo.ReadMessage import java.util.* -data class ReadNoti(val chatroomId: UUID, val readByUserId: UUID, val type: ChatMessageType) { +data class ReadNoti( + val chatroomId: UUID, + val readByUserId: UUID, + val type: ChatMessageType, + val sequence:Long +) { constructor(message: ReadMessage) : this( readByUserId = message.readUserId, chatroomId = message.chatRoomId, - type = ChatMessageType.READ + type = ChatMessageType.READ, + sequence = message.sequence ) } \ No newline at end of file diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingCursorScrollRequest.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingCursorScrollRequest.kt new file mode 100644 index 00000000..fb325091 --- /dev/null +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingCursorScrollRequest.kt @@ -0,0 +1,19 @@ +package com.swm.idle.support.transfer.common + +import io.swagger.v3.oas.annotations.Parameter +import org.springdoc.core.annotations.ParameterObject +import java.util.* + +@ParameterObject +data class CrawlingCursorScrollRequest( + @Parameter(required = false, example = "다음 요청 시에 최초로 조회되는 ID. 최초 요청시에는 null") + override val next: UUID? = null, + @Parameter(required = false, example = "조회 item 수 : default 10") + override val limit: Long = 10, + @Parameter(required = true, example = "조회 거리 : default 15km") + override val distance: Long = 15, +) : CrawlingScrollRequest( + next = next, + limit = limit, + distance = distance, +) \ No newline at end of file diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingScrollRequest.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingScrollRequest.kt new file mode 100644 index 00000000..78dec7bb --- /dev/null +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingScrollRequest.kt @@ -0,0 +1,7 @@ +package com.swm.idle.support.transfer.common + +abstract class CrawlingScrollRequest( + open val next: N, + open val limit: Long, + open val distance: Long, +) \ No newline at end of file diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingScrollResponse.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingScrollResponse.kt new file mode 100644 index 00000000..f24f3a72 --- /dev/null +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/common/CrawlingScrollResponse.kt @@ -0,0 +1,8 @@ +package com.swm.idle.support.transfer.common + +abstract class CrawlingScrollResponse( + open val items: List, + open val next: N, + open val total: Int, + open val nextDistance: Int +) diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/carer/CrawlingJobPostingScrollResponse.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/carer/CrawlingJobPostingScrollResponse.kt index e45e368a..b7a7b2ab 100644 --- a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/carer/CrawlingJobPostingScrollResponse.kt +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/carer/CrawlingJobPostingScrollResponse.kt @@ -3,22 +3,24 @@ package com.swm.idle.support.transfer.jobposting.carer import com.fasterxml.jackson.annotation.JsonProperty import com.swm.idle.domain.common.dto.CrawlingJobPostingPreviewDto import com.swm.idle.domain.jobposting.enums.JobPostingType -import com.swm.idle.support.transfer.common.ScrollResponse +import com.swm.idle.support.transfer.common.CrawlingScrollResponse import io.swagger.v3.oas.annotations.media.Schema import java.util.* @Schema( name = "CrawlingJobPostingScrollResponse", - description = "외부 구인 공고 전체 조회 API(3km 내 검색)" + description = "외부 구인 공고 전체 조회 API(1km 내 검색)" ) data class CrawlingJobPostingScrollResponse( override val items: List, override val next: UUID?, override val total: Int, -) : ScrollResponse( + override val nextDistance: Int, +) : CrawlingScrollResponse( items = items, next = next, total = total, + nextDistance = nextDistance ) { data class CrawlingJobPostingDto( @@ -40,7 +42,7 @@ data class CrawlingJobPostingScrollResponse( @Schema(description = "공고 모집 마감 기한") val applyDeadline: String, - @Schema(description = "직선 거리", example = "760(단위 : 미터)") + @Schema(description = "직선 거리 (*km 이내 위치)", example = "1(단위 : 키로미터)") val distance: Int, @get:JsonProperty("isFavorite") @@ -76,13 +78,13 @@ data class CrawlingJobPostingScrollResponse( fun from( items: List, - next: UUID?, - total: Int, + distance: Long ): CrawlingJobPostingScrollResponse { return CrawlingJobPostingScrollResponse( items = items.map(CrawlingJobPostingDto::from), - next = next, - total = total, + next = items.lastOrNull()?.crawledJobPosting?.id, + total = items.size, + nextDistance = distance.toInt() ) } diff --git a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/common/CrawlingJobPostingResponse.kt b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/common/CrawlingJobPostingResponse.kt index 766e190c..21f18c25 100644 --- a/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/common/CrawlingJobPostingResponse.kt +++ b/idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/common/CrawlingJobPostingResponse.kt @@ -75,32 +75,29 @@ data class CrawlingJobPostingResponse( ) { companion object { - fun from( - crawlingJobPosting: CrawledJobPosting, - longitude: Double, - latitude: Double, + posting: CrawledJobPosting, isFavorite: Boolean, distance: Int, ): CrawlingJobPostingResponse { return CrawlingJobPostingResponse( - id = crawlingJobPosting.id, - title = crawlingJobPosting.title, - content = crawlingJobPosting.content, - clientAddress = crawlingJobPosting.clientAddress, - longitude = longitude.toString(), - latitude = latitude.toString(), - createdAt = crawlingJobPosting.createdAt, - payInfo = crawlingJobPosting.payInfo, - workingTime = crawlingJobPosting.workTime, - workingSchedule = crawlingJobPosting.workSchedule, - applyDeadline = crawlingJobPosting.applyDeadline, - recruitmentProcess = crawlingJobPosting.recruitmentProcess, - applyMethod = crawlingJobPosting.applyMethod, - requiredDocumentation = crawlingJobPosting.requiredDocument, - centerName = crawlingJobPosting.centerName, - centerAddress = crawlingJobPosting.centerAddress, - jobPostingUrl = crawlingJobPosting.directUrl, + id = posting.id, + title = posting.title, + content = posting.content, + clientAddress = posting.clientAddress, + longitude = posting.location.x.toString(), + latitude = posting.location.y.toString(), + createdAt = posting.createdAt, + payInfo = posting.payInfo, + workingTime = posting.workTime, + workingSchedule = posting.workSchedule, + applyDeadline = posting.applyDeadline, + recruitmentProcess = posting.recruitmentProcess, + applyMethod = posting.applyMethod, + requiredDocumentation = posting.requiredDocument, + centerName = posting.centerName, + centerAddress = posting.centerAddress, + jobPostingUrl = posting.directUrl, jobPostingType = JobPostingType.WORKNET, isFavorite = isFavorite, distance = distance,