From fa97a00647aba5eb070957f858a68b9d07066bee Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 11:39:20 +0900 Subject: [PATCH 01/30] =?UTF-8?q?=E2=9C=A8=20feat:=20code=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/service/ChatServiceImpl.java | 32 ++++++++++++------- .../chat/service/ChatServiceInterface.java | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java index f831f2f7..ce63c099 100644 --- a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java @@ -446,11 +446,12 @@ public boolean existsChatRoom(Long ownerId, Long buyerId, Long propertyId) { return existing != null; } - @Override - public Long findExistingChatRoom(Long ownerId, Long buyerId, Long propertyId) { - ChatRoom existingRoom = chatRoomMapper.findByUserAndHome(ownerId, buyerId, propertyId); - return existingRoom.getChatRoomId(); - } + @Override + public Long findExistingChatRoom(Long ownerId, Long buyerId, Long propertyId) { + ChatRoom existingRoom = chatRoomMapper.findByUserAndHome(ownerId, buyerId, propertyId); + return existingRoom.getChatRoomId(); + } + /** {@inheritDoc} */ @Override public List> getChatMediaFiles( @@ -820,22 +821,31 @@ public Long acceptContractRequest(Long chatRoomId, Long userId) { handleChatMessage(acceptMessage); String contractChatUrl = "http://localhost:5173/pre-contract/" - + contractChatRoomId.toString() - + "/owner?step=1" - + "/n" - + "http://localhost:5173/pre-contract/" + contractChatRoomId.toString() + "/buyer?step=1"; ChatMessageRequestDto linkMessage = ChatMessageRequestDto.builder() .chatRoomId(chatRoomId) - .senderId(userId) + .senderId(originalChatRoom.getOwnerId()) .receiverId(originalChatRoom.getBuyerId()) .content(contractChatUrl) - .type("TEXT") + .type("URLLINK") .build(); handleChatMessage(linkMessage); + String contractChatUrls = + "http://localhost:5173/pre-contract/" + + contractChatRoomId.toString() + + "/owner?step=1"; + ChatMessageRequestDto linkMessages = + ChatMessageRequestDto.builder() + .chatRoomId(chatRoomId) + .senderId(originalChatRoom.getBuyerId()) + .receiverId(originalChatRoom.getOwnerId()) + .content(contractChatUrls) + .type("URLLINK") + .build(); + handleChatMessage(linkMessages); return contractChatRoomId; } diff --git a/src/main/java/org/scoula/domain/chat/service/ChatServiceInterface.java b/src/main/java/org/scoula/domain/chat/service/ChatServiceInterface.java index 916bb36e..a7309851 100644 --- a/src/main/java/org/scoula/domain/chat/service/ChatServiceInterface.java +++ b/src/main/java/org/scoula/domain/chat/service/ChatServiceInterface.java @@ -196,6 +196,6 @@ List> getChatMediaFiles( * @param userId 거절하는 사용자 ID (소유자여야 함) */ void rejectContractRequest(Long chatRoomId, Long userId); - Long findExistingChatRoom(Long ownerId, Long buyerId, Long propertyId); + Long findExistingChatRoom(Long ownerId, Long buyerId, Long propertyId); } From ecb06773e5e65ddcec704a7873aaa49b961cd27d Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 11:39:49 +0900 Subject: [PATCH 02/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=ED=8A=B9=EC=95=BD=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EB=B0=8F=20=EC=88=98=EB=9D=BD=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ContractChatController.java | 67 +- .../ContractChatControllerImpl.java | 288 +++++- .../chat/service/ContractChatServiceImpl.java | 829 +++++++++++++++--- .../service/ContractChatServiceInterface.java | 32 +- 4 files changed, 1060 insertions(+), 156 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java index e2c28964..c972f9ff 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java @@ -7,8 +7,7 @@ import org.scoula.domain.chat.document.ContractChatDocument; import org.scoula.domain.chat.document.FinalSpecialContractDocument; import org.scoula.domain.chat.document.SpecialContractFixDocument; -import org.scoula.domain.chat.dto.ContractChatMessageRequestDto; -import org.scoula.domain.chat.dto.SpecialContractUserViewDto; +import org.scoula.domain.chat.dto.*; import org.scoula.domain.chat.dto.ai.ClauseImproveResponseDto; import org.scoula.global.common.dto.ApiResponse; import org.springframework.http.ResponseEntity; @@ -143,4 +142,68 @@ ResponseEntity> sendAiMessage( @GetMapping("/final-contract/{contractChatId}") ResponseEntity> getFinalSpecialContract( @PathVariable Long contractChatId, Authentication authentication); + + @ApiOperation(value = "최종 특약서 수정 요청 (임대인)", notes = "임대인이 최종 특약서의 특정 조항 수정을 요청합니다.") + @PostMapping("/final-contract/{contractChatId}/modification-request") + ResponseEntity> requestFinalContractModification( + @PathVariable Long contractChatId, + @RequestBody FinalContractModificationRequestDto requestDto, + Authentication authentication); + + @ApiOperation(value = "최종 특약서 수정 요청 응답 (임차인)", notes = "임차인이 수정 요청을 수락 또는 거절합니다.") + @PostMapping("/final-contract/{contractChatId}/modification-response") + ResponseEntity> respondToModificationRequest( + @PathVariable Long contractChatId, + @RequestBody FinalContractModificationResponseDto responseDto, + Authentication authentication); + + @ApiOperation(value = "특정 수정 요청 조회", notes = "특정 조항에 대한 대기중인 수정 요청을 조회합니다.") + @GetMapping("/final-contract/{contractChatId}/modification-request/{clauseOrder}") + ResponseEntity> getPendingModificationRequest( + @PathVariable Long contractChatId, + @PathVariable Integer clauseOrder, + Authentication authentication); + + @ApiOperation(value = "대기중인 수정 요청 확인", notes = "특정 조항에 대해 대기중인 수정 요청이 있는지 확인합니다.") + @GetMapping("/final-contract/{contractChatId}/has-pending-request/{clauseOrder}") + ResponseEntity> hasPendingModificationRequest( + @PathVariable Long contractChatId, + @PathVariable Integer clauseOrder, + Authentication authentication); + + @ApiOperation(value = "최종 특약 확정 요청 (임대인)", notes = "임대인이 최종 특약서에 대한 확정을 요청합니다.") + @PostMapping("/{contractChatId}/final-contract/request-confirmation") + ResponseEntity> requestFinalContractConfirmation( + @PathVariable Long contractChatId, Authentication authentication); + + @ApiOperation(value = "최종 특약 확정 수락 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 수락합니다.") + @PostMapping("/{contractChatId}/final-contract/accept-confirmation") + ResponseEntity>> acceptFinalContractConfirmation( + @PathVariable Long contractChatId, Authentication authentication); + + @ApiOperation(value = "최종 특약 확정 거절 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 거절합니다.") + @PostMapping("/{contractChatId}/final-contract/reject-confirmation") + ResponseEntity> rejectFinalContractConfirmation( + @PathVariable Long contractChatId, Authentication authentication); + + @ApiOperation(value = "최종 특약 삭제 요청 (임대인)", notes = "임대인이 최종 특약서의 특정 조항 삭제를 요청합니다.") + @PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}") + ResponseEntity> requestFinalContractDeletion( + @PathVariable Long contractChatId, + @PathVariable Integer clauseOrder, + Authentication authentication); + + @ApiOperation(value = "최종 특약 삭제 수락 (임차인)", notes = "임차인이 임대인의 최종 특약 삭제 요청을 수락합니다.") + @PostMapping("/final-contract/{contractChatId}/accept-deletion/{clauseOrder}") + ResponseEntity>> acceptFinalContractDeletion( + @PathVariable Long contractChatId, + @PathVariable Integer clauseOrder, + Authentication authentication); + + @ApiOperation(value = "최종 특약 삭제 거절 (임차인)", notes = "임차인이 임대인의 최종 특약 삭제 요청을 거절합니다.") + @PostMapping("/final-contract/{contractChatId}/reject-deletion/{clauseOrder}") + ResponseEntity> rejectFinalContractDeletion( + @PathVariable Long contractChatId, + @PathVariable Integer clauseOrder, + Authentication authentication); } diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java index 38b3d4dc..464c50e0 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java @@ -9,8 +9,7 @@ import org.scoula.domain.chat.document.ContractChatDocument; import org.scoula.domain.chat.document.FinalSpecialContractDocument; import org.scoula.domain.chat.document.SpecialContractFixDocument; -import org.scoula.domain.chat.dto.ContractChatMessageRequestDto; -import org.scoula.domain.chat.dto.SpecialContractUserViewDto; +import org.scoula.domain.chat.dto.*; import org.scoula.domain.chat.dto.ai.ClauseImproveResponseDto; import org.scoula.domain.chat.exception.ChatErrorCode; import org.scoula.domain.chat.mapper.ContractChatMapper; @@ -152,7 +151,6 @@ public ResponseEntity>> getContractMessag User currentUser = currentUserOpt.get(); Long userId = currentUser.getUserId(); - // 권한 확인을 컨트롤러에서 직접 수행 if (!contractChatService.isUserInContractChat(contractChatId, userId)) { throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); } @@ -283,13 +281,6 @@ public ResponseEntity> setEndPointAndExpor boolean importClause = contractChatService.setEndPointAndExport(contractChatId, userId, order); - // - // log.info("메시지 내용:"); - // System.out.println(importClause); - // - // ApiResponse response = - // ApiResponse.success(importClause, "특약 대화가 성공적으로 내보내졌습니다."); - return ResponseEntity.ok(ApiResponse.success()); } catch (Exception e) { log.error("특약 대화 내보내기 실패", e); @@ -449,9 +440,6 @@ public ResponseEntity>> getContractChatInfo( } // 특약 관련 메서드 - // 수정된 ContractChatControllerImpl.java 구현 메서드들 - // ApiResponse 사용법을 기존 프로젝트에 맞게 수정 - @Override @PostMapping("/special-contracts/{contractChatId}/submit-selection") public ResponseEntity> submitSpecialContractSelection( @@ -646,7 +634,6 @@ public ResponseEntity> completeSpecialCo } } - // 컨트롤러에 추가 @Override @GetMapping("/special-contract/{contractChatId}/incomplete") public ResponseEntity>> @@ -703,4 +690,277 @@ public ResponseEntity> getFinalSpecial .body(ApiResponse.error("INTERNAL_ERROR", "최종 특약서 조회 중 오류가 발생했습니다.")); } } + + @PostMapping("/final-contract/{contractChatId}/modification-request") + public ResponseEntity> requestFinalContractModification( + @PathVariable Long contractChatId, + @RequestBody FinalContractModificationRequestDto requestDto, + Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + + requestDto.setContractChatId(contractChatId); + + ModificationRequestData result = + contractChatService.requestFinalContractModification( + contractChatId, userId, requestDto); + + return ResponseEntity.ok(ApiResponse.success(result, "수정 요청이 성공적으로 전송되었습니다.")); + + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("REQUEST_FAILED", e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 수정 요청 실패", e); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다.")); + } + } + + @PostMapping("/final-contract/{contractChatId}/modification-response") + public ResponseEntity> respondToModificationRequest( + @PathVariable Long contractChatId, + @RequestBody FinalContractModificationResponseDto responseDto, + Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + + responseDto.setContractChatId(contractChatId); + + FinalSpecialContractDocument result = + contractChatService.respondToModificationRequest( + contractChatId, userId, responseDto); + + String message = responseDto.isAccepted() ? "수정 요청을 수락했습니다." : "수정 요청을 거절했습니다."; + + return ResponseEntity.ok(ApiResponse.success(result, message)); + + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("RESPONSE_FAILED", e.getMessage())); + } catch (Exception e) { + log.error("수정 요청 응답 실패", e); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다.")); + } + } + + @GetMapping("/final-contract/{contractChatId}/modification-request/{clauseOrder}") + public ResponseEntity> getPendingModificationRequest( + @PathVariable Long contractChatId, + @PathVariable Integer clauseOrder, + Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + + if (!contractChatService.isUserInContractChat(contractChatId, userId)) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); + } + + ModificationRequestData result = + contractChatService.getPendingModificationRequest(contractChatId, clauseOrder); + + if (result == null) { + return ResponseEntity.ok(ApiResponse.success(null, "대기중인 수정 요청이 없습니다.")); + } + + return ResponseEntity.ok(ApiResponse.success(result, "수정 요청 조회 성공")); + + } catch (Exception e) { + log.error("수정 요청 조회 실패", e); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("INTERNAL_ERROR", "조회 중 오류가 발생했습니다.")); + } + } + + @Override + @GetMapping("/final-contract/{contractChatId}/has-pending-request/{clauseOrder}") + public ResponseEntity> hasPendingModificationRequest( + @PathVariable Long contractChatId, + @PathVariable Integer clauseOrder, + Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + + if (!contractChatService.isUserInContractChat(contractChatId, userId)) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); + } + + boolean hasPending = + contractChatService.hasPendingModificationRequest(contractChatId, clauseOrder); + + return ResponseEntity.ok(ApiResponse.success(hasPending, "대기중인 수정 요청 확인 완료")); + + } catch (Exception e) { + log.error("대기중인 수정 요청 확인 실패", e); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("INTERNAL_ERROR", "확인 중 오류가 발생했습니다.")); + } + } + + @Override + @PostMapping("/{contractChatId}/final-contract/request-confirmation") + public ResponseEntity> requestFinalContractConfirmation( + @PathVariable Long contractChatId, Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + contractChatService.requestFinalContractConfirmation(contractChatId, userId); + + return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); + } catch (Exception e) { + log.error("최종 특약 확정 요청 실패", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("최종 특약 확정 요청에 실패했습니다: " + e.getMessage())); + } + } + + @Override + @PostMapping("/{contractChatId}/final-contract/accept-confirmation") + public ResponseEntity>> acceptFinalContractConfirmation( + @PathVariable Long contractChatId, Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + ContractChat contractChat = + contractChatService.getContractChatInfo(contractChatId, userId); + Long buyerId = contractChat.getBuyerId(); + Long ownerId = contractChat.getOwnerId(); + + if (!userId.equals(buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + String redisKey = "final-contract:confirmation:" + contractChatId; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + Map result = + contractChatService.acceptFinalContractConfirmation(contractChatId, userId); + + return ResponseEntity.ok(ApiResponse.success(result, "최종 특약서가 확정되었습니다.")); + } catch (Exception e) { + log.error("최종 특약 확정 수락 실패", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("최종 특약 확정 수락에 실패했습니다: " + e.getMessage())); + } + } + + @Override + @PostMapping("/{contractChatId}/final-contract/reject-confirmation") + public ResponseEntity> rejectFinalContractConfirmation( + @PathVariable Long contractChatId, Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + + ContractChat contractChat = + contractChatService.getContractChatInfo(contractChatId, userId); + Long buyerId = contractChat.getBuyerId(); + Long ownerId = contractChat.getOwnerId(); + + if (!userId.equals(buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + String redisKey = "final-contract:confirmation:" + contractChatId; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + contractChatService.rejectFinalContractConfirmation(contractChatId, userId); + + return ResponseEntity.ok(ApiResponse.success("최종 특약 확정을 거절했습니다.")); + } catch (Exception e) { + log.error("최종 특약 확정 거절 실패", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("최종 특약 확정 거절에 실패했습니다: " + e.getMessage())); + } + } + + @Override + public ResponseEntity> requestFinalContractDeletion( + Long contractChatId, Integer clauseOrder, Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder); + + return ResponseEntity.ok( + ApiResponse.success( + String.format("특약 %d번 삭제 요청이 임차인에게 전송되었습니다.", clauseOrder))); + } catch (Exception e) { + log.error("최종 특약 삭제 요청 실패", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage())); + } + } + + @Override + public ResponseEntity>> acceptFinalContractDeletion( + Long contractChatId, Integer clauseOrder, Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + ContractChat contractChat = + contractChatService.getContractChatInfo(contractChatId, userId); + Long buyerId = contractChat.getBuyerId(); + Long ownerId = contractChat.getOwnerId(); + + if (!userId.equals(buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + Map result = + contractChatService.acceptFinalContractDeletion( + contractChatId, userId, clauseOrder); + + return ResponseEntity.ok(ApiResponse.success(result, "특약이 삭제되었습니다.")); + } catch (Exception e) { + log.error("최종 특약 삭제 수락 실패", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("최종 특약 삭제 수락에 실패했습니다: " + e.getMessage())); + } + } + + @Override + public ResponseEntity> rejectFinalContractDeletion( + Long contractChatId, Integer clauseOrder, Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + + ContractChat contractChat = + contractChatService.getContractChatInfo(contractChatId, userId); + Long buyerId = contractChat.getBuyerId(); + Long ownerId = contractChat.getOwnerId(); + + if (!userId.equals(buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + contractChatService.rejectFinalContractDeletion(contractChatId, userId, clauseOrder); + + return ResponseEntity.ok(ApiResponse.success("특약 삭제 요청을 거절했습니다.")); + } catch (Exception e) { + log.error("최종 특약 삭제 거절 실패", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("최종 특약 삭제 거절에 실패했습니다: " + e.getMessage())); + } + } } diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index d2d4a7af..1039ea91 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -1,14 +1,13 @@ package org.scoula.domain.chat.service; +import java.time.Duration; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.scoula.domain.chat.document.*; -import org.scoula.domain.chat.dto.ContentDataDto; -import org.scoula.domain.chat.dto.ContractChatMessageRequestDto; -import org.scoula.domain.chat.dto.SpecialContractUserViewDto; +import org.scoula.domain.chat.dto.*; import org.scoula.domain.chat.dto.ai.ClauseImproveRequestDto; import org.scoula.domain.chat.dto.ai.ClauseImproveResponseDto; import org.scoula.domain.chat.exception.ChatErrorCode; @@ -28,6 +27,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -46,6 +47,7 @@ public class ContractChatServiceImpl implements ContractChatServiceInterface { private final Map> contractChatOnlineUsers = new ConcurrentHashMap<>(); private final RedisTemplate stringRedisTemplate; + private final ObjectMapper objectMapper = new ObjectMapper(); @Autowired private SpecialContractMongoRepository specialContractMongoRepository; /** {@inheritDoc} */ @@ -133,22 +135,23 @@ public void AiMessage(Long contractChatId, String content) { contractChatMapper.updateLastMessage(contractChatId, content); messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, aiMessage); } - public void AiMessageNext(Long contractChatId, String content) { - final Long ai = 9997L; - - ContractChatDocument aiMessage = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(ai) - .receiverId(null) - .content(content) - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(aiMessage); - contractChatMapper.updateLastMessage(contractChatId, content); - messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, aiMessage); - } + + public void AiMessageNext(Long contractChatId, String content) { + final Long ai = 9997L; + + ContractChatDocument aiMessage = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(ai) + .receiverId(null) + .content(content) + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(aiMessage); + contractChatMapper.updateLastMessage(contractChatId, content); + messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, aiMessage); + } public void AiMessageBtn(Long contractChatId, String content) { final Long ai = 9998L; @@ -280,14 +283,11 @@ public boolean setEndPointAndExport(Long contractChatId, Long userId, Long order String result = sb.toString(); + SpecialContractFixDocument improveClauseRequest = + updateRecentData(contractChatId, order, result); + ClauseImproveResponseDto improveClauseResponse = getAiClauseImprove(improveClauseRequest); - - SpecialContractFixDocument improveClauseRequest = - updateRecentData(contractChatId, order, result); - ClauseImproveResponseDto improveClauseResponse = - getAiClauseImprove(improveClauseRequest); - - updateSpecialClause(contractChatId, improveClauseResponse); + updateSpecialClause(contractChatId, improveClauseResponse); checkAndIncrementRoundIfComplete(contractChatId); return true; @@ -632,14 +632,11 @@ public void createNextRoundSpecialContractDocument( .orElseThrow( () -> new IllegalArgumentException("현재 라운드의 특약 문서를 찾을 수 없습니다")); - Long newRound = currentRound + 1; log.info("새 라운드: {} → {}", currentRound, newRound); - // 이전 라운드에서 통과된 특약들도 찾아서 포함 List allPassedOrders = new ArrayList<>(passedOrders); - // 이미 완료된 특약들(isPassed=true)도 추가로 가져와서 포함 List completedContracts = specialContractMongoRepository.findByContractChatIdAndIsPassed( contractChatId, true); @@ -659,7 +656,6 @@ public void createNextRoundSpecialContractDocument( Long orderLong = Long.valueOf(order); if (allPassedOrders.contains(orderLong)) { - // 통과된 특약들을 복사 (이전 라운드에서 완료된 것들 포함) Optional clauseOpt = findBestClauseForOrder(contractChatId, orderLong); @@ -724,7 +720,6 @@ public void createNextRoundSpecialContractDocument( newClauses.add(emptyClause); log.info("거부된 특약 {}번 빈 껍데기 생성 완료", order); } else { - // 유지되는 특약들 latestDocument.getClauses().stream() .filter(clause -> clause.getOrder().equals(orderInteger)) .findFirst() @@ -799,10 +794,8 @@ public void createNextRoundSpecialContractDocument( .collect(Collectors.toList())); } - /** 특정 특약 번호에 대해 가장 최신의 완성된 조항을 찾는 메서드 가장 높은 라운드부터 역순으로 검색하여 내용이 있는 조항을 반환 */ private Optional findBestClauseForOrder( Long contractChatId, Long order) { - // 4라운드부터 1라운드까지 역순으로 검색 for (Long round = 4L; round >= 1L; round--) { Optional docOpt = specialContractMongoRepository @@ -967,7 +960,6 @@ public Object submitUserSelection( return processRoundResults(contractChatId, document, currentStatus, isOwner); } - /** 현재 상태에 따른 선택 가능한 특약들 반환 */ private List getAvailableOrders( Long contractChatId, ContractChat.ContractStatus status) { if (status == ContractChat.ContractStatus.STEP0 @@ -983,7 +975,6 @@ private List getAvailableOrders( } } - /** 라운드별 결과 처리 (기존 로직 + 라운드 진행) */ @Transactional public Object processRoundResults( Long contractChatId, @@ -1193,7 +1184,7 @@ public Map getAllRoundsSpecialContract(Long contractChatId, Long String userRole = isOwner ? "owner" : "tenant"; - Map allRounds = new HashMap<>(); + Map allRounds = new LinkedHashMap<>(); int availableRounds = 0; for (Long round = 1L; round <= 4L; round++) { @@ -1560,7 +1551,6 @@ public List getIncompleteSpecialContractsByChat( contractChatId, false); } - /** 빈 ContentDataDto 생성 헬퍼 메서드 */ private ContentDataDto createEmptyContentData() { return ContentDataDto.builder().title("").content("").messages("").build(); } @@ -1581,107 +1571,137 @@ private List findRejectedOrders( return rejectedOrders; } + @Override + @Transactional + public FinalSpecialContractDocument saveFinalSpecialContract(Long contractChatId) { + log.info("=== 최종 특약 저장 시작 ==="); + log.info("contractChatId: {}", contractChatId); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + SpecialContractDocument latestDocument = null; + Long latestRound = null; + + for (Long round = 4L; round >= 1L; round--) { + Optional docOpt = + specialContractMongoRepository + .findSpecialContractDocumentByContractChatIdAndRound( + contractChatId, round); + + if (docOpt.isPresent()) { + latestDocument = docOpt.get(); + latestRound = round; + log.info("가장 최근 라운드 발견: {}", round); + break; + } + } - @Override - @Transactional - public FinalSpecialContractDocument saveFinalSpecialContract(Long contractChatId) { - ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); - ContractChat.ContractStatus currentStatus = contractChat.getStatus(); - - boolean isThirdRoundComplete = (currentStatus == ContractChat.ContractStatus.ROUND3); - - List finalClauses = new ArrayList<>(); - - if (isThirdRoundComplete) { - log.info("=== 3회차 수정 완료 - 4라운드 데이터에서 최종 특약 생성 ==="); - - Optional round4DocOpt = - specialContractMongoRepository.findSpecialContractDocumentByContractChatIdAndRound(contractChatId, 4L); - - if (round4DocOpt.isPresent()) { - SpecialContractDocument round4Doc = round4DocOpt.get(); - - for (SpecialContractDocument.Clause clause : round4Doc.getClauses()) { - if (clause.getTitle() != null && !clause.getTitle().trim().isEmpty() && - clause.getContent() != null && !clause.getContent().trim().isEmpty()) { - - FinalSpecialContractDocument.FinalClause finalClause = - FinalSpecialContractDocument.FinalClause.builder() - .order(clause.getOrder()) - .title(clause.getTitle()) - .content(clause.getContent()) - .build(); - - finalClauses.add(finalClause); - log.info("4라운드에서 특약 {}번 최종 저장: {}", clause.getOrder(), clause.getTitle()); - } - } - } - } else { - log.info("=== 모든 특약 완료 - 완료된 특약들만 최종 저장 ==="); - - List incompleteContracts = - specialContractMongoRepository.findByContractChatIdAndIsPassed(contractChatId, false); - - if (!incompleteContracts.isEmpty()) { - throw new IllegalStateException( - "아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다."); - } - - List completedContracts = - specialContractMongoRepository.findByContractChatIdAndIsPassed(contractChatId, true); - - if (completedContracts.isEmpty()) { - throw new IllegalStateException("완료된 특약이 없습니다."); - } - - for (SpecialContractFixDocument completedContract : completedContracts) { - Long order = completedContract.getOrder(); - - Optional latestRoundDoc = - findLatestRoundForOrder(contractChatId, order); - - if (latestRoundDoc.isPresent()) { - SpecialContractDocument doc = latestRoundDoc.get(); - - doc.getClauses().stream() - .filter(clause -> clause.getOrder().equals(order.intValue())) - .findFirst() - .ifPresent( - clause -> { - FinalSpecialContractDocument.FinalClause finalClause = - FinalSpecialContractDocument.FinalClause.builder() - .order(clause.getOrder()) - .title(clause.getTitle()) - .content(clause.getContent()) - .build(); - - finalClauses.add(finalClause); - log.info( - "특약 {}번 최종 저장 완료 - sourceRound: {}", - order, - doc.getRound()); - }); - } - } - } - - FinalSpecialContractDocument finalDocument = - FinalSpecialContractDocument.builder() - .contractChatId(contractChatId) - .totalFinalClauses(finalClauses.size()) - .finalClauses(finalClauses) - .build(); - - FinalSpecialContractDocument savedDocument = - specialContractMongoRepository.saveFinalSpecialContract(finalDocument); - - log.info("최종 특약 저장 완료 - 총 {}개 조항 (방식: {})", - finalClauses.size(), - isThirdRoundComplete ? "3회차 완료" : "모든 특약 완료"); - - return savedDocument; - } + if (latestDocument == null) { + throw new IllegalStateException("특약 문서를 찾을 수 없습니다: " + contractChatId); + } + + List finalClauses = new ArrayList<>(); + + for (SpecialContractDocument.Clause clause : latestDocument.getClauses()) { + if (clause.getOrder() != null) { + String title = clause.getTitle(); + String content = clause.getContent(); + + if (title != null + && !title.trim().isEmpty() + && content != null + && !content.trim().isEmpty()) { + + FinalSpecialContractDocument.FinalClause finalClause = + FinalSpecialContractDocument.FinalClause.builder() + .order(clause.getOrder()) + .title(title.trim()) + .content(content.trim()) + .build(); + + finalClauses.add(finalClause); + log.info("특약 {}번 저장 완료 (라운드 {}): {}", clause.getOrder(), latestRound, title); + } else { + boolean foundInPreviousRound = false; + for (Long searchRound = latestRound - 1; searchRound >= 1L; searchRound--) { + Optional prevDocOpt = + specialContractMongoRepository + .findSpecialContractDocumentByContractChatIdAndRound( + contractChatId, searchRound); + + if (prevDocOpt.isPresent()) { + SpecialContractDocument prevDoc = prevDocOpt.get(); + + for (SpecialContractDocument.Clause prevClause : prevDoc.getClauses()) { + if (prevClause.getOrder() != null + && prevClause.getOrder().equals(clause.getOrder())) { + + String prevTitle = prevClause.getTitle(); + String prevContent = prevClause.getContent(); + + if (prevTitle != null + && !prevTitle.trim().isEmpty() + && prevContent != null + && !prevContent.trim().isEmpty()) { + + FinalSpecialContractDocument.FinalClause finalClause = + FinalSpecialContractDocument.FinalClause.builder() + .order(prevClause.getOrder()) + .title(prevTitle.trim()) + .content(prevContent.trim()) + .build(); + + finalClauses.add(finalClause); + log.info( + "특약 {}번 저장 완료 (이전 라운드 {}): {}", + prevClause.getOrder(), + searchRound, + prevTitle); + foundInPreviousRound = true; + break; + } + } + } + + if (foundInPreviousRound) { + break; + } + } + } + + if (!foundInPreviousRound) { + log.info("특약 {}번: 모든 라운드에서 유효한 내용을 찾을 수 없음 - 건너뜀", clause.getOrder()); + } + } + } + } + + finalClauses.sort((a, b) -> Integer.compare(a.getOrder(), b.getOrder())); + + log.info("최종 저장될 특약 개수: {}", finalClauses.size()); + for (FinalSpecialContractDocument.FinalClause clause : finalClauses) { + log.info("- 특약 {}번: {}", clause.getOrder(), clause.getTitle()); + } + + FinalSpecialContractDocument finalDocument = + FinalSpecialContractDocument.builder() + .contractChatId(contractChatId) + .totalFinalClauses(finalClauses.size()) + .finalClauses(finalClauses) + .build(); + + FinalSpecialContractDocument savedDocument = + specialContractMongoRepository.saveFinalSpecialContract(finalDocument); + + log.info("=== 최종 특약 저장 완료 ==="); + log.info("저장된 문서 ID: {}", savedDocument.getId()); + log.info("총 특약 개수: {}", savedDocument.getTotalFinalClauses()); + + return savedDocument; + } private Optional findLatestRoundForOrder( Long contractChatId, Long order) { @@ -1776,7 +1796,6 @@ public void checkAndIncrementRoundIfComplete(Long contractChatId) { } } - /** 최종 라운드(4차) 완료 체크 및 자동 완료 처리 */ @Transactional public void checkFinalRoundCompletion(Long contractChatId) { log.info("=== 최종 라운드(4차) 완료 체크 시작 ==="); @@ -1921,4 +1940,538 @@ private String getRoundIncrementMessage(ContractChat.ContractStatus status) { return "새로운 협상 라운드가 시작됩니다."; } } + + @Override + @Transactional + public ModificationRequestData requestFinalContractModification( + Long contractChatId, Long ownerId, FinalContractModificationRequestDto requestDto) { + + log.info("=== 최종 특약서 수정 요청 시작 ==="); + log.info( + "contractChatId: {}, ownerId: {}, clauseOrder: {}", + contractChatId, + ownerId, + requestDto.getClauseOrder()); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + if (!ownerId.equals(contractChat.getOwnerId())) { + throw new BusinessException( + ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임대인만 수정 요청할 수 있습니다."); + } + + Optional finalContractOpt = + specialContractMongoRepository.findFinalContractByContractChatId(contractChatId); + + if (finalContractOpt.isEmpty()) { + throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다."); + } + + FinalSpecialContractDocument finalContract = finalContractOpt.get(); + boolean clauseExists = + finalContract.getFinalClauses().stream() + .anyMatch(clause -> clause.getOrder().equals(requestDto.getClauseOrder())); + + if (!clauseExists) { + throw new IllegalArgumentException( + "해당 특약 조항을 찾을 수 없습니다: " + requestDto.getClauseOrder()); + } + + String redisKey = + "final-contract:modification:" + contractChatId + ":" + requestDto.getClauseOrder(); + + String existingRequest = stringRedisTemplate.opsForValue().get(redisKey); + if (existingRequest != null) { + throw new IllegalArgumentException("해당 조항에 대한 수정 요청이 이미 대기중입니다."); + } + + ModificationRequestData requestData = + ModificationRequestData.builder() + .contractChatId(contractChatId) + .clauseOrder(requestDto.getClauseOrder()) + .newTitle(requestDto.getNewTitle()) + .newContent(requestDto.getNewContent()) + .requesterId(ownerId) + .createdAt(LocalDateTime.now().toString()) + .build(); + + try { + String jsonData = objectMapper.writeValueAsString(requestData); + stringRedisTemplate.opsForValue().set(redisKey, jsonData, Duration.ofHours(24)); + + String notificationMessage = + String.format("임대인이 특약 %d번 수정을 요청했습니다.", requestDto.getClauseOrder()); + + ContractChatDocument requestMessage = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(ownerId) + .receiverId(contractChat.getBuyerId()) + .content(notificationMessage) + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(requestMessage); + messagingTemplate.convertAndSend( + "/topic/contract-chat/" + contractChatId, requestMessage); + + log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey); + return requestData; + + } catch (Exception e) { + log.error("수정 요청 저장 실패", e); + throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다."); + } + } + + @Override + @Transactional + public FinalSpecialContractDocument respondToModificationRequest( + Long contractChatId, Long buyerId, FinalContractModificationResponseDto responseDto) { + + log.info("=== 수정 요청 응답 처리 시작 ==="); + log.info( + "contractChatId: {}, buyerId: {}, accepted: {}", + contractChatId, + buyerId, + responseDto.isAccepted()); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + if (!buyerId.equals(contractChat.getBuyerId())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 응답할 수 있습니다."); + } + + String redisKey = + "final-contract:modification:" + + contractChatId + + ":" + + responseDto.getClauseOrder(); + String requestDataJson = stringRedisTemplate.opsForValue().get(redisKey); + + if (requestDataJson == null) { + throw new IllegalArgumentException("해당 조항에 대한 대기중인 수정 요청이 없습니다."); + } + + try { + ModificationRequestData requestData = + objectMapper.readValue(requestDataJson, ModificationRequestData.class); + + FinalSpecialContractDocument finalContract = + specialContractMongoRepository + .findFinalContractByContractChatId(contractChatId) + .orElseThrow(() -> new IllegalArgumentException("최종 특약서를 찾을 수 없습니다.")); + + String resultMessage; + + if (responseDto.isAccepted()) { + List updatedClauses = + finalContract.getFinalClauses().stream() + .map( + clause -> { + if (clause.getOrder() + .equals(responseDto.getClauseOrder())) { + return FinalSpecialContractDocument.FinalClause + .builder() + .order(clause.getOrder()) + .title(requestData.getNewTitle()) + .content(requestData.getNewContent()) + .build(); + } + return clause; + }) + .collect(Collectors.toList()); + + finalContract.setFinalClauses(updatedClauses); + + specialContractMongoRepository.saveFinalSpecialContract(finalContract); + + resultMessage = + String.format( + "임차인이 특약 %d번 수정 요청을 수락했습니다. 특약이 변경되었습니다.", + responseDto.getClauseOrder()); + + log.info("수정 수락 - 최종 특약서 업데이트 완료"); + + } else { + resultMessage = + String.format( + "임차인이 특약 %d번 수정 요청을 거절했습니다. 기존 특약이 유지됩니다.", + responseDto.getClauseOrder()); + + log.info("수정 거절 - 기존 특약서 유지"); + } + + stringRedisTemplate.delete(redisKey); + + ContractChatDocument responseMessage = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(buyerId) + .receiverId(contractChat.getOwnerId()) + .content(resultMessage) + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(responseMessage); + messagingTemplate.convertAndSend( + "/topic/contract-chat/" + contractChatId, responseMessage); + + return finalContract; + + } catch (Exception e) { + log.error("수정 요청 응답 처리 실패", e); + throw new RuntimeException("응답 처리 중 오류가 발생했습니다."); + } + } + + @Override + public ModificationRequestData getPendingModificationRequest( + Long contractChatId, Integer clauseOrder) { + String redisKey = "final-contract:modification:" + contractChatId + ":" + clauseOrder; + String requestDataJson = stringRedisTemplate.opsForValue().get(redisKey); + + if (requestDataJson == null) { + return null; + } + + try { + return objectMapper.readValue(requestDataJson, ModificationRequestData.class); + } catch (Exception e) { + log.error("수정 요청 데이터 파싱 실패", e); + return null; + } + } + + @Override + public boolean hasPendingModificationRequest(Long contractChatId, Integer clauseOrder) { + String redisKey = "final-contract:modification:" + contractChatId + ":" + clauseOrder; + return stringRedisTemplate.hasKey(redisKey); + } + + @Override + @Transactional + public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) { + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + if (!ownerId.equals(contractChat.getOwnerId())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + Optional finalContractOpt = + specialContractMongoRepository.findFinalContractByContractChatId(contractChatId); + + if (finalContractOpt.isEmpty()) { + throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다."); + } + + ContractChatDocument confirmationRequestMessage = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(ownerId) + .receiverId(contractChat.getBuyerId()) + .content("임대인이 최종 특약서 확정을 요청했습니다.") + .sendTime(LocalDateTime.now().toString()) + .build(); + + String key = "final-contract:confirmation:" + contractChatId; + String existingValue = stringRedisTemplate.opsForValue().get(key); + if (existingValue != null) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_ALREADY_EXISTS, "이미 확정 요청이 진행 중입니다."); + } + String value = ownerId.toString(); + stringRedisTemplate.opsForValue().set(key, value); + + contractChatMessageRepository.saveMessage(confirmationRequestMessage); + + messagingTemplate.convertAndSend( + "/topic/contract-chat/" + contractChatId, confirmationRequestMessage); + } + + @Override + @Transactional + public Map acceptFinalContractConfirmation(Long contractChatId, Long buyerId) { + if (!isUserInContractChat(contractChatId, buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + Long ownerId = contractChat.getOwnerId(); + + if (!buyerId.equals(contractChat.getBuyerId())) { + throw new BusinessException( + ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 확정 수락을 할 수 있습니다."); + } + + String redisKey = "final-contract:confirmation:" + contractChatId; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "확정 요청이 존재하지 않습니다."); + } + + if (!storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_INVALID, "확정 요청 정보가 유효하지 않습니다."); + } + + FinalSpecialContractDocument finalContract = + specialContractMongoRepository + .findFinalContractByContractChatId(contractChatId) + .orElseThrow(() -> new IllegalArgumentException("최종 특약서를 찾을 수 없습니다.")); + + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP4); + + stringRedisTemplate.delete(redisKey); + + String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; + + ContractChatDocument successMessage = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(buyerId) + .receiverId(ownerId) + .content(confirmationMessage) + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(successMessage); + messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, successMessage); + + return Map.of( + "message", + "최종 특약서가 확정되었습니다.", + "status", + "COMPLETED", + "finalContractId", + finalContract.getId(), + "totalFinalClauses", + finalContract.getTotalFinalClauses()); + } + + @Override + public void rejectFinalContractConfirmation(Long contractChatId, Long buyerId) { + String redisKey = "final-contract:confirmation:" + contractChatId; + stringRedisTemplate.delete(redisKey); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + ContractChatDocument rejectNotification = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(buyerId) + .receiverId(contractChat.getOwnerId()) + .content("임차인이 최종 특약서 확정을 거절했습니다.") + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(rejectNotification); + + messagingTemplate.convertAndSend( + "/topic/contract-chat/" + contractChatId, rejectNotification); + } + + @Override + @Transactional + public void requestFinalContractDeletion( + Long contractChatId, Long ownerId, Integer clauseOrder) { + log.info("=== 최종 특약 삭제 요청 시작 ==="); + log.info( + "contractChatId: {}, ownerId: {}, clauseOrder: {}", + contractChatId, + ownerId, + clauseOrder); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + if (!ownerId.equals(contractChat.getOwnerId())) { + throw new BusinessException( + ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임대인만 삭제 요청할 수 있습니다."); + } + + Optional finalContractOpt = + specialContractMongoRepository.findFinalContractByContractChatId(contractChatId); + + if (finalContractOpt.isEmpty()) { + throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다."); + } + + FinalSpecialContractDocument finalContract = finalContractOpt.get(); + boolean clauseExists = + finalContract.getFinalClauses().stream() + .anyMatch(clause -> clause.getOrder().equals(clauseOrder)); + + if (!clauseExists) { + throw new IllegalArgumentException("해당 특약 조항을 찾을 수 없습니다: " + clauseOrder); + } + + String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; + + String existingRequest = stringRedisTemplate.opsForValue().get(redisKey); + if (existingRequest != null) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_ALREADY_EXISTS, "이미 삭제 요청이 진행 중입니다."); + } + + stringRedisTemplate.opsForValue().set(redisKey, ownerId.toString(), Duration.ofHours(24)); + + String notificationMessage = String.format("임대인이 특약 %d번 삭제를 요청했습니다.", clauseOrder); + + ContractChatDocument requestMessage = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(ownerId) + .receiverId(contractChat.getBuyerId()) + .content(notificationMessage) + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(requestMessage); + messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, requestMessage); + + log.info("삭제 요청 Redis 저장 완료 - key: {}, value: {}", redisKey, ownerId); + } + + @Override + @Transactional + public Map acceptFinalContractDeletion( + Long contractChatId, Long buyerId, Integer clauseOrder) { + log.info("=== 최종 특약 삭제 수락 처리 시작 ==="); + log.info( + "contractChatId: {}, buyerId: {}, clauseOrder: {}", + contractChatId, + buyerId, + clauseOrder); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + if (!buyerId.equals(contractChat.getBuyerId())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 응답할 수 있습니다."); + } + + String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "삭제 요청이 존재하지 않습니다."); + } + + if (!storedOwnerId.equals(contractChat.getOwnerId().toString())) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_INVALID, "삭제 요청 정보가 유효하지 않습니다."); + } + + FinalSpecialContractDocument finalContract = + specialContractMongoRepository + .findFinalContractByContractChatId(contractChatId) + .orElseThrow(() -> new IllegalArgumentException("최종 특약서를 찾을 수 없습니다.")); + + List updatedClauses = + finalContract.getFinalClauses().stream() + .filter(clause -> !clause.getOrder().equals(clauseOrder)) + .collect(Collectors.toList()); + + finalContract.setFinalClauses(updatedClauses); + finalContract.setTotalFinalClauses(updatedClauses.size()); + + specialContractMongoRepository.saveFinalSpecialContract(finalContract); + + stringRedisTemplate.delete(redisKey); + + String confirmationMessage = + String.format("임차인이 특약 %d번 삭제 요청을 수락했습니다. 특약이 삭제되었습니다.", clauseOrder); + + ContractChatDocument successMessage = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(buyerId) + .receiverId(contractChat.getOwnerId()) + .content(confirmationMessage) + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(successMessage); + messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, successMessage); + + log.info("특약 {}번 삭제 완료 - contractChatId: {}", clauseOrder, contractChatId); + + return Map.of( + "message", + "특약이 삭제되었습니다.", + "deletedClauseOrder", + clauseOrder, + "finalContractId", + finalContract.getId(), + "remainingClauses", + finalContract.getTotalFinalClauses()); + } + + @Override + @Transactional + public void rejectFinalContractDeletion( + Long contractChatId, Long buyerId, Integer clauseOrder) { + log.info("=== 최종 특약 삭제 거절 처리 시작 ==="); + log.info( + "contractChatId: {}, buyerId: {}, clauseOrder: {}", + contractChatId, + buyerId, + clauseOrder); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + if (!buyerId.equals(contractChat.getBuyerId())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 응답할 수 있습니다."); + } + + String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "삭제 요청이 존재하지 않습니다."); + } + + stringRedisTemplate.delete(redisKey); + + String rejectionMessage = + String.format("임차인이 특약 %d번 삭제 요청을 거절했습니다. 기존 특약이 유지됩니다.", clauseOrder); + + ContractChatDocument rejectionDoc = + ContractChatDocument.builder() + .contractChatId(contractChatId.toString()) + .senderId(buyerId) + .receiverId(contractChat.getOwnerId()) + .content(rejectionMessage) + .sendTime(LocalDateTime.now().toString()) + .build(); + + contractChatMessageRepository.saveMessage(rejectionDoc); + messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, rejectionDoc); + + log.info("특약 {}번 삭제 거절 완료 - contractChatId: {}", clauseOrder, contractChatId); + } } diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java index 845247aa..87ea30dd 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java @@ -6,8 +6,7 @@ import org.scoula.domain.chat.document.ContractChatDocument; import org.scoula.domain.chat.document.FinalSpecialContractDocument; import org.scoula.domain.chat.document.SpecialContractFixDocument; -import org.scoula.domain.chat.dto.ContractChatMessageRequestDto; -import org.scoula.domain.chat.dto.SpecialContractUserViewDto; +import org.scoula.domain.chat.dto.*; import org.scoula.domain.chat.vo.ContractChat; import org.scoula.global.common.exception.BusinessException; import org.scoula.global.common.exception.EntityNotFoundException; @@ -259,4 +258,33 @@ void createNextRoundSpecialContractDocument( void AiMessage(Long contractChatId, String content); void AiMessageBtn(Long contractChatId, String content); + + ModificationRequestData requestFinalContractModification( + Long contractChatId, Long ownerId, FinalContractModificationRequestDto requestDto); + + FinalSpecialContractDocument respondToModificationRequest( + Long contractChatId, Long buyerId, FinalContractModificationResponseDto responseDto); + + ModificationRequestData getPendingModificationRequest(Long contractChatId, Integer clauseOrder); + + boolean hasPendingModificationRequest(Long contractChatId, Integer clauseOrder); + + /** 임대인이 최종 특약 확정 요청 */ + void requestFinalContractConfirmation(Long contractChatId, Long ownerId); + + /** 임차인이 최종 특약 확정 수락 */ + Map acceptFinalContractConfirmation(Long contractChatId, Long buyerId); + + /** 임차인이 최종 특약 확정 거절 */ + void rejectFinalContractConfirmation(Long contractChatId, Long buyerId); + + /** 임대인이 최종 특약 삭제 요청 */ + void requestFinalContractDeletion(Long contractChatId, Long ownerId, Integer clauseOrder); + + /** 임차인이 최종 특약 삭제 수락 */ + Map acceptFinalContractDeletion( + Long contractChatId, Long buyerId, Integer clauseOrder); + + /** 임차인이 최종 특약 삭제 거절 */ + void rejectFinalContractDeletion(Long contractChatId, Long buyerId, Integer clauseOrder); } From 474fe5ee1c29b35becdfdbf4e86af036b9a83823 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 11:40:01 +0900 Subject: [PATCH 03/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=ED=8A=B9=EC=95=BD=20=EC=82=AD=EC=A0=9C=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/SpecialContractMongoRepository.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java b/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java index a2005f78..b349193a 100644 --- a/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java @@ -115,10 +115,13 @@ public void deleteSpecialContract(SpecialContractFixDocument document) { mongoTemplate.remove(document); } - /** contractChatId로 특약 문서 삭제 */ - public void deleteByContractChatId(Long contractChatId) { + public void deleteFinalContractClause(Long contractChatId, Integer clauseOrder) { Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); - mongoTemplate.remove(query, SpecialContractFixDocument.class); + Update update = + new Update() + .pull("finalClauses", Query.query(Criteria.where("order").is(clauseOrder))); + + mongoTemplate.updateFirst(query, update, "FINAL_SPECIAL_CONTRACT"); } /** contractChatId로 SpecialContractDocument (원본 특약 문서) 조회 */ From 45f4d113cc36522af07582d2e691f17ec60402eb Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 11:40:17 +0900 Subject: [PATCH 04/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=ED=8A=B9=EC=95=BD=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20Dto=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinalContractModificationRequestDto.java | 15 +++++++++++++++ .../FinalContractModificationResponseDto.java | 14 ++++++++++++++ .../chat/dto/ModificationRequestData.java | 17 +++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java create mode 100644 src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java create mode 100644 src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java diff --git a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java new file mode 100644 index 00000000..47362fb8 --- /dev/null +++ b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java @@ -0,0 +1,15 @@ +package org.scoula.domain.chat.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FinalContractModificationRequestDto { + private Long contractChatId; + private Integer clauseOrder; + private String newTitle; + private String newContent; +} diff --git a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java new file mode 100644 index 00000000..45103f00 --- /dev/null +++ b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java @@ -0,0 +1,14 @@ +package org.scoula.domain.chat.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FinalContractModificationResponseDto { + private Long contractChatId; + private Integer clauseOrder; + private boolean accepted; +} diff --git a/src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java b/src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java new file mode 100644 index 00000000..69400a5c --- /dev/null +++ b/src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java @@ -0,0 +1,17 @@ +package org.scoula.domain.chat.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ModificationRequestData { + private Long contractChatId; + private Integer clauseOrder; + private String newTitle; + private String newContent; + private Long requesterId; + private String createdAt; +} From 242a53f645a35b6d9b43862cea5da6c434add341 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 16:41:43 +0900 Subject: [PATCH 05/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20=ED=8A=B9=EC=95=BD=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20AI=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ContractChatServiceImpl.java | 116 +++--------------- 1 file changed, 17 insertions(+), 99 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index 1039ea91..f06862fd 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2002,22 +2002,16 @@ public ModificationRequestData requestFinalContractModification( String jsonData = objectMapper.writeValueAsString(requestData); stringRedisTemplate.opsForValue().set(redisKey, jsonData, Duration.ofHours(24)); - String notificationMessage = - String.format("임대인이 특약 %d번 수정을 요청했습니다.", requestDto.getClauseOrder()); - - ContractChatDocument requestMessage = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(ownerId) - .receiverId(contractChat.getBuyerId()) - .content(notificationMessage) - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(requestMessage); - messagingTemplate.convertAndSend( - "/topic/contract-chat/" + contractChatId, requestMessage); - + String notificationMessage = String.format( + "임대인이 특약 %d번 수정을 요청했습니다.\n\n" + + "📝 수정 제목: %s\n" + + "✏️ 수정 내용: %s\n\n", + requestDto.getClauseOrder(), + requestDto.getNewTitle(), + requestDto.getNewContent() + ); + + AiMessageBtn(contractChatId,notificationMessage); log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey); return requestData; @@ -2110,19 +2104,7 @@ public FinalSpecialContractDocument respondToModificationRequest( stringRedisTemplate.delete(redisKey); - ContractChatDocument responseMessage = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(buyerId) - .receiverId(contractChat.getOwnerId()) - .content(resultMessage) - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(responseMessage); - messagingTemplate.convertAndSend( - "/topic/contract-chat/" + contractChatId, responseMessage); - + AiMessage(contractChatId,resultMessage); return finalContract; } catch (Exception e) { @@ -2174,14 +2156,7 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다."); } - ContractChatDocument confirmationRequestMessage = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(ownerId) - .receiverId(contractChat.getBuyerId()) - .content("임대인이 최종 특약서 확정을 요청했습니다.") - .sendTime(LocalDateTime.now().toString()) - .build(); + AiMessage(contractChatId,"특약을 수락하였습니다"); String key = "final-contract:confirmation:" + contractChatId; String existingValue = stringRedisTemplate.opsForValue().get(key); @@ -2192,10 +2167,6 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) String value = ownerId.toString(); stringRedisTemplate.opsForValue().set(key, value); - contractChatMessageRepository.saveMessage(confirmationRequestMessage); - - messagingTemplate.convertAndSend( - "/topic/contract-chat/" + contractChatId, confirmationRequestMessage); } @Override @@ -2241,17 +2212,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; - ContractChatDocument successMessage = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(buyerId) - .receiverId(ownerId) - .content(confirmationMessage) - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(successMessage); - messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, successMessage); + AiMessage(contractChatId,confirmationMessage); return Map.of( "message", @@ -2269,20 +2230,7 @@ public void rejectFinalContractConfirmation(Long contractChatId, Long buyerId) { String redisKey = "final-contract:confirmation:" + contractChatId; stringRedisTemplate.delete(redisKey); - ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); - ContractChatDocument rejectNotification = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(buyerId) - .receiverId(contractChat.getOwnerId()) - .content("임차인이 최종 특약서 확정을 거절했습니다.") - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(rejectNotification); - - messagingTemplate.convertAndSend( - "/topic/contract-chat/" + contractChatId, rejectNotification); + AiMessage(contractChatId,"임차인이 수정을 거절하였습니다."); } @Override @@ -2334,17 +2282,7 @@ public void requestFinalContractDeletion( String notificationMessage = String.format("임대인이 특약 %d번 삭제를 요청했습니다.", clauseOrder); - ContractChatDocument requestMessage = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(ownerId) - .receiverId(contractChat.getBuyerId()) - .content(notificationMessage) - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(requestMessage); - messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, requestMessage); + AiMessage(contractChatId,notificationMessage); log.info("삭제 요청 Redis 저장 완료 - key: {}, value: {}", redisKey, ownerId); } @@ -2402,17 +2340,7 @@ public Map acceptFinalContractDeletion( String confirmationMessage = String.format("임차인이 특약 %d번 삭제 요청을 수락했습니다. 특약이 삭제되었습니다.", clauseOrder); - ContractChatDocument successMessage = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(buyerId) - .receiverId(contractChat.getOwnerId()) - .content(confirmationMessage) - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(successMessage); - messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, successMessage); + AiMessage(contractChatId,confirmationMessage); log.info("특약 {}번 삭제 완료 - contractChatId: {}", clauseOrder, contractChatId); @@ -2460,17 +2388,7 @@ public void rejectFinalContractDeletion( String rejectionMessage = String.format("임차인이 특약 %d번 삭제 요청을 거절했습니다. 기존 특약이 유지됩니다.", clauseOrder); - ContractChatDocument rejectionDoc = - ContractChatDocument.builder() - .contractChatId(contractChatId.toString()) - .senderId(buyerId) - .receiverId(contractChat.getOwnerId()) - .content(rejectionMessage) - .sendTime(LocalDateTime.now().toString()) - .build(); - - contractChatMessageRepository.saveMessage(rejectionDoc); - messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, rejectionDoc); + AiMessage(contractChatId,rejectionMessage); log.info("특약 {}번 삭제 거절 완료 - contractChatId: {}", clauseOrder, contractChatId); } From d522b0bcadc7d7d5794a92a6f98c21c3d642757c Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 21:54:55 +0900 Subject: [PATCH 06/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20AI=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ContractChatServiceImpl.java | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index f06862fd..f1a5ce67 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2002,16 +2002,14 @@ public ModificationRequestData requestFinalContractModification( String jsonData = objectMapper.writeValueAsString(requestData); stringRedisTemplate.opsForValue().set(redisKey, jsonData, Duration.ofHours(24)); - String notificationMessage = String.format( - "임대인이 특약 %d번 수정을 요청했습니다.\n\n" + - "📝 수정 제목: %s\n" + - "✏️ 수정 내용: %s\n\n", - requestDto.getClauseOrder(), - requestDto.getNewTitle(), - requestDto.getNewContent() - ); - - AiMessageBtn(contractChatId,notificationMessage); + String notificationMessage = + String.format( + "임대인이 특약 %d번 수정을 요청했습니다.\n\n" + "📝 수정 제목: %s\n" + "✏️ 수정 내용: %s\n\n", + requestDto.getClauseOrder(), + requestDto.getNewTitle(), + requestDto.getNewContent()); + + AiMessageBtn(contractChatId, notificationMessage); log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey); return requestData; @@ -2104,7 +2102,7 @@ public FinalSpecialContractDocument respondToModificationRequest( stringRedisTemplate.delete(redisKey); - AiMessage(contractChatId,resultMessage); + AiMessage(contractChatId, resultMessage); return finalContract; } catch (Exception e) { @@ -2156,7 +2154,7 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다."); } - AiMessage(contractChatId,"특약을 수락하였습니다"); + AiMessageBtn(contractChatId, "최종 특약을 요청하였습니다"); String key = "final-contract:confirmation:" + contractChatId; String existingValue = stringRedisTemplate.opsForValue().get(key); @@ -2166,7 +2164,6 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) } String value = ownerId.toString(); stringRedisTemplate.opsForValue().set(key, value); - } @Override @@ -2212,7 +2209,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; - AiMessage(contractChatId,confirmationMessage); + AiMessageBtn(contractChatId, confirmationMessage); return Map.of( "message", @@ -2230,7 +2227,7 @@ public void rejectFinalContractConfirmation(Long contractChatId, Long buyerId) { String redisKey = "final-contract:confirmation:" + contractChatId; stringRedisTemplate.delete(redisKey); - AiMessage(contractChatId,"임차인이 수정을 거절하였습니다."); + AiMessage(contractChatId, "임차인이 수정을 거절하였습니다."); } @Override @@ -2282,7 +2279,7 @@ public void requestFinalContractDeletion( String notificationMessage = String.format("임대인이 특약 %d번 삭제를 요청했습니다.", clauseOrder); - AiMessage(contractChatId,notificationMessage); + AiMessageBtn(contractChatId, notificationMessage); log.info("삭제 요청 Redis 저장 완료 - key: {}, value: {}", redisKey, ownerId); } @@ -2340,7 +2337,7 @@ public Map acceptFinalContractDeletion( String confirmationMessage = String.format("임차인이 특약 %d번 삭제 요청을 수락했습니다. 특약이 삭제되었습니다.", clauseOrder); - AiMessage(contractChatId,confirmationMessage); + AiMessage(contractChatId, confirmationMessage); log.info("특약 {}번 삭제 완료 - contractChatId: {}", clauseOrder, contractChatId); @@ -2388,7 +2385,7 @@ public void rejectFinalContractDeletion( String rejectionMessage = String.format("임차인이 특약 %d번 삭제 요청을 거절했습니다. 기존 특약이 유지됩니다.", clauseOrder); - AiMessage(contractChatId,rejectionMessage); + AiMessage(contractChatId, rejectionMessage); log.info("특약 {}번 삭제 거절 완료 - contractChatId: {}", clauseOrder, contractChatId); } From eb5af907e90d676e0ce769239d5de27c976363e4 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 21:56:40 +0900 Subject: [PATCH 07/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20AI=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/scoula/domain/chat/service/ContractChatServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index f1a5ce67..f764683e 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2209,7 +2209,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; - AiMessageBtn(contractChatId, confirmationMessage); + AiMessageNext(contractChatId, confirmationMessage); return Map.of( "message", From 2c1a980bb3ff343be8ddd95ee2a1f60b569f6e51 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 23:15:43 +0900 Subject: [PATCH 08/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20Order=EB=B2=88=ED=98=B8=20=EC=A4=91=EB=B3=B5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/dto/FinalContractModificationRequestDto.java | 1 - .../domain/chat/dto/FinalContractModificationResponseDto.java | 1 - .../org/scoula/domain/chat/dto/ModificationRequestData.java | 2 -- 3 files changed, 4 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java index 47362fb8..ddbae65d 100644 --- a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java +++ b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java @@ -8,7 +8,6 @@ @AllArgsConstructor @Builder public class FinalContractModificationRequestDto { - private Long contractChatId; private Integer clauseOrder; private String newTitle; private String newContent; diff --git a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java index 45103f00..662f394b 100644 --- a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java +++ b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java @@ -8,7 +8,6 @@ @AllArgsConstructor @Builder public class FinalContractModificationResponseDto { - private Long contractChatId; private Integer clauseOrder; private boolean accepted; } diff --git a/src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java b/src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java index 69400a5c..0d17ba90 100644 --- a/src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java +++ b/src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java @@ -8,8 +8,6 @@ @AllArgsConstructor @Builder public class ModificationRequestData { - private Long contractChatId; - private Integer clauseOrder; private String newTitle; private String newContent; private Long requesterId; From 587fc74af611c7b2a5f9c74dd559e9ffd347375d Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 23:15:48 +0900 Subject: [PATCH 09/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20Order=EB=B2=88=ED=98=B8=20=EC=A4=91=EB=B3=B5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/controller/ContractChatControllerImpl.java | 4 ---- .../scoula/domain/chat/service/ContractChatServiceImpl.java | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java index 464c50e0..0e004e5c 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java @@ -699,8 +699,6 @@ public ResponseEntity> requestFinalContract try { Long userId = getUserIdFromAuthentication(authentication); - requestDto.setContractChatId(contractChatId); - ModificationRequestData result = contractChatService.requestFinalContractModification( contractChatId, userId, requestDto); @@ -725,8 +723,6 @@ public ResponseEntity> respondToModifi try { Long userId = getUserIdFromAuthentication(authentication); - responseDto.setContractChatId(contractChatId); - FinalSpecialContractDocument result = contractChatService.respondToModificationRequest( contractChatId, userId, responseDto); diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index f764683e..3af2b576 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -1990,8 +1990,6 @@ public ModificationRequestData requestFinalContractModification( ModificationRequestData requestData = ModificationRequestData.builder() - .contractChatId(contractChatId) - .clauseOrder(requestDto.getClauseOrder()) .newTitle(requestDto.getNewTitle()) .newContent(requestDto.getNewContent()) .requesterId(ownerId) From e96ddc0d9194936b2e51501cb4c2e3475ac056d7 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 13:13:30 +0900 Subject: [PATCH 10/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20Order=EB=B2=88=ED=98=B8=20=EC=97=86=EC=9D=B4=20=EC=88=98?= =?UTF-8?q?=EB=9D=BD=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ContractChatController.java | 25 ++- .../ContractChatControllerImpl.java | 101 +++++----- .../dto/FinalContractDeletionResponseDto.java | 13 ++ .../FinalContractModificationResponseDto.java | 1 - .../chat/service/ContractChatServiceImpl.java | 184 +++++++++++++++--- .../service/ContractChatServiceInterface.java | 8 + 6 files changed, 243 insertions(+), 89 deletions(-) create mode 100644 src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java index c972f9ff..7a352ab1 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java @@ -133,6 +133,14 @@ ResponseEntity>> getCompletedSpecia ResponseEntity>> getIncompleteSpecialContracts( @PathVariable Long contractChatId, Authentication authentication); + @ApiOperation( + value = "메시지 미포함 미완료 특약 문서 목록 조회", + notes = "메시지 내역이 포함되지 않은 미완료된 모든 특약 문서를 조회합니다.") + @GetMapping("/special-contract/{contractChatId}/incomplete/now") + ResponseEntity>> + getIncompleteSpecialContractsWithoutMessage( + @PathVariable Long contractChatId, Authentication authentication); + @ApiOperation(value = "특약 대화 시작 AI 메시지", notes = "선택된 특약 번호 대화를 시작한다는 메시지를 AI가 전송합니다.") @PostMapping("/special-contract/{contractChatId}/ai") ResponseEntity> sendAiMessage( @@ -193,17 +201,8 @@ ResponseEntity> requestFinalContractDeletion( @PathVariable Integer clauseOrder, Authentication authentication); - @ApiOperation(value = "최종 특약 삭제 수락 (임차인)", notes = "임차인이 임대인의 최종 특약 삭제 요청을 수락합니다.") - @PostMapping("/final-contract/{contractChatId}/accept-deletion/{clauseOrder}") - ResponseEntity>> acceptFinalContractDeletion( - @PathVariable Long contractChatId, - @PathVariable Integer clauseOrder, - Authentication authentication); - - @ApiOperation(value = "최종 특약 삭제 거절 (임차인)", notes = "임차인이 임대인의 최종 특약 삭제 요청을 거절합니다.") - @PostMapping("/final-contract/{contractChatId}/reject-deletion/{clauseOrder}") - ResponseEntity> rejectFinalContractDeletion( - @PathVariable Long contractChatId, - @PathVariable Integer clauseOrder, - Authentication authentication); + @ApiOperation(value = "현재 스텝 조회", notes = "현재 진행 상황을 조회합니다.") + @GetMapping("/{contractChatId}/status") + ResponseEntity> getContractStatus( + @PathVariable Long contractChatId, Authentication authentication); } diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java index 0e004e5c..5612165f 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java @@ -652,6 +652,25 @@ public ResponseEntity> completeSpecialCo } } + @Override + @GetMapping("/special-contract/{contractChatId}/incomplete/now") + public ResponseEntity>> + getIncompleteSpecialContractsWithoutMessage( + @PathVariable Long contractChatId, Authentication authentication) { + + try { + Long userId = getUserIdFromAuthentication(authentication); + List result = + contractChatService.getIncompleteSpecialContractsWithoutMessage( + contractChatId, userId); + + return ResponseEntity.ok(ApiResponse.success(result, "메시지가 없는 미완료 특약 목록 조회 성공")); + } catch (Exception e) { + return ResponseEntity.internalServerError() + .body(ApiResponse.error("INTERNAL_ERROR", "미완료 특약 목록 조회 중 오류가 발생했습니다.")); + } + } + @Override @PostMapping("/special-contract/{contractChatId}/ai") public ResponseEntity> sendAiMessage( @@ -895,68 +914,54 @@ public ResponseEntity> requestFinalContractDeletion( } } - @Override - public ResponseEntity>> acceptFinalContractDeletion( - Long contractChatId, Integer clauseOrder, Authentication authentication) { + @PostMapping("/final-contract/{contractChatId}/deletion-response") + public ResponseEntity>> respondToFinalContractDeletion( + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication) { try { Long userId = getUserIdFromAuthentication(authentication); - ContractChat contractChat = - contractChatService.getContractChatInfo(contractChatId, userId); - Long buyerId = contractChat.getBuyerId(); - Long ownerId = contractChat.getOwnerId(); - - if (!userId.equals(buyerId)) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } - String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; - String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + Map result = + contractChatService.respondToFinalContractDeletionRequest( + contractChatId, userId, responseDto); - if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } + String message = responseDto.isAccepted() ? "삭제 요청을 수락했습니다." : "삭제 요청을 거절했습니다."; - Map result = - contractChatService.acceptFinalContractDeletion( - contractChatId, userId, clauseOrder); + return ResponseEntity.ok(ApiResponse.success(result, message)); - return ResponseEntity.ok(ApiResponse.success(result, "특약이 삭제되었습니다.")); - } catch (Exception e) { - log.error("최종 특약 삭제 수락 실패", e); + } catch (IllegalArgumentException e) { return ResponseEntity.badRequest() - .body(ApiResponse.error("최종 특약 삭제 수락에 실패했습니다: " + e.getMessage())); + .body(ApiResponse.error("RESPONSE_FAILED", e.getMessage())); + } catch (Exception e) { + log.error("삭제 요청 응답 실패", e); + return ResponseEntity.internalServerError() + .body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다.")); } } @Override - public ResponseEntity> rejectFinalContractDeletion( - Long contractChatId, Integer clauseOrder, Authentication authentication) { - try { - Long userId = getUserIdFromAuthentication(authentication); - - ContractChat contractChat = - contractChatService.getContractChatInfo(contractChatId, userId); - Long buyerId = contractChat.getBuyerId(); - Long ownerId = contractChat.getOwnerId(); - - if (!userId.equals(buyerId)) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } - - String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; - String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); - - if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } + @GetMapping("/{contractChatId}/status") + public ResponseEntity> getContractStatus( + @PathVariable Long contractChatId, Authentication authentication) { + Long userId = getUserIdFromAuthentication(authentication); + if (!contractChatService.isUserInContractChat(contractChatId, userId)) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); + } - contractChatService.rejectFinalContractDeletion(contractChatId, userId, clauseOrder); + ContractChat contractChat = contractChatService.getContractChatInfo(contractChatId, userId); + if (contractChat == null) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("NOT_FOUND", "해당 계약 채팅방을 찾을 수 없습니다.")); + } - return ResponseEntity.ok(ApiResponse.success("특약 삭제 요청을 거절했습니다.")); - } catch (Exception e) { - log.error("최종 특약 삭제 거절 실패", e); + String statusParam = contractChatService.getContractStatusParam(contractChatId, userId); + if (statusParam == null) { return ResponseEntity.badRequest() - .body(ApiResponse.error("최종 특약 삭제 거절에 실패했습니다: " + e.getMessage())); + .body(ApiResponse.error("INVALID_STATUS", "유효하지 않은 계약 상태입니다.")); } + + return ResponseEntity.ok(ApiResponse.success(statusParam, "계약 상태 조회 성공")); } } diff --git a/src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java b/src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java new file mode 100644 index 00000000..623dc36c --- /dev/null +++ b/src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java @@ -0,0 +1,13 @@ +package org.scoula.domain.chat.dto; + +import lombok.*; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class FinalContractDeletionResponseDto { + private boolean accepted; +} diff --git a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java index 662f394b..9859bce6 100644 --- a/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java +++ b/src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java @@ -8,6 +8,5 @@ @AllArgsConstructor @Builder public class FinalContractModificationResponseDto { - private Integer clauseOrder; private boolean accepted; } diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index 3af2b576..fb95acaf 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -1551,6 +1551,17 @@ public List getIncompleteSpecialContractsByChat( contractChatId, false); } + @Override + public List getIncompleteSpecialContractsWithoutMessage( + Long contractChatId, Long userId) { + if (!isUserInContractChat(contractChatId, userId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + return specialContractMongoRepository + .findByContractChatIdAndIsPassedAndRecentDataMessagesEmpty(contractChatId, false); + } + private ContentDataDto createEmptyContentData() { return ContentDataDto.builder().title("").content("").messages("").build(); } @@ -1928,6 +1939,33 @@ private Long getNextRoundNumber(ContractChat.ContractStatus status) { } } + private String getContractChatStatus(ContractChat.ContractStatus status) { + switch (status) { + case STEP0: + return "?step=1"; + case STEP1: + return "?step=2"; + case STEP2: + return "?step=3"; + case ROUND0: + return "?step=3&round=0"; + case ROUND1: + return "?step=3&round=1"; + case ROUND2: + return "?step=3&round=2"; + case ROUND3: + return "?step=3&round=3"; + default: + return null; + } + } + + @Override + public String getContractStatusParam(Long contractChatId, Long userId) { + ContractChat contractChat = getContractChatInfo(contractChatId, userId); + return getContractChatStatus(contractChat.getStatus()); + } + private String getRoundIncrementMessage(ContractChat.ContractStatus status) { switch (status) { case ROUND1: @@ -1941,6 +1979,83 @@ private String getRoundIncrementMessage(ContractChat.ContractStatus status) { } } + @Override + @Transactional + public Map respondToFinalContractDeletionRequest( + Long contractChatId, Long buyerId, FinalContractDeletionResponseDto responseDto) { + + log.info("=== 삭제 요청 응답 처리 시작 ==="); + log.info( + "contractChatId: {}, buyerId: {}, accepted: {}", + contractChatId, + buyerId, + responseDto.isAccepted()); + + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } + + if (!buyerId.equals(contractChat.getBuyerId())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 응답할 수 있습니다."); + } + + String redisKey = + "final-contract:deletion:" + contractChatId + ":" + contractChat.getOwnerId(); + String clauseOrderStr = stringRedisTemplate.opsForValue().get(redisKey); + + if (clauseOrderStr == null) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "삭제 요청이 존재하지 않습니다."); + } + + Integer clauseOrder = Integer.parseInt(clauseOrderStr); + + Map result = new HashMap<>(); + String resultMessage; + + if (responseDto.isAccepted()) { + // 삭제 수락 로직 + FinalSpecialContractDocument finalContract = + specialContractMongoRepository + .findFinalContractByContractChatId(contractChatId) + .orElseThrow(() -> new IllegalArgumentException("최종 특약서를 찾을 수 없습니다.")); + + List updatedClauses = + finalContract.getFinalClauses().stream() + .filter(clause -> !clause.getOrder().equals(clauseOrder)) + .collect(Collectors.toList()); + + finalContract.setFinalClauses(updatedClauses); + finalContract.setTotalFinalClauses(updatedClauses.size()); + + specialContractMongoRepository.saveFinalSpecialContract(finalContract); + + resultMessage = String.format("임차인이 특약 %d번 삭제 요청을 수락했습니다. 특약이 삭제되었습니다.", clauseOrder); + + result.put("message", "특약이 삭제되었습니다."); + result.put("deletedClauseOrder", clauseOrder); + result.put("finalContractId", finalContract.getId()); + result.put("remainingClauses", finalContract.getTotalFinalClauses()); + + log.info("특약 {}번 삭제 완료 - contractChatId: {}", clauseOrder, contractChatId); + + } else { + // 삭제 거절 + resultMessage = String.format("임차인이 특약 %d번 삭제 요청을 거절했습니다. 기존 특약이 유지됩니다.", clauseOrder); + + result.put("message", "삭제 요청을 거절했습니다."); + result.put("clauseOrder", clauseOrder); + + log.info("특약 {}번 삭제 거절 완료 - contractChatId: {}", clauseOrder, contractChatId); + } + + stringRedisTemplate.delete(redisKey); + AiMessage(contractChatId, resultMessage); + + return result; + } + @Override @Transactional public ModificationRequestData requestFinalContractModification( @@ -1980,8 +2095,7 @@ public ModificationRequestData requestFinalContractModification( "해당 특약 조항을 찾을 수 없습니다: " + requestDto.getClauseOrder()); } - String redisKey = - "final-contract:modification:" + contractChatId + ":" + requestDto.getClauseOrder(); + String redisKey = "final-contract:modification:" + contractChatId + ":" + ownerId; String existingRequest = stringRedisTemplate.opsForValue().get(redisKey); if (existingRequest != null) { @@ -1998,7 +2112,11 @@ public ModificationRequestData requestFinalContractModification( try { String jsonData = objectMapper.writeValueAsString(requestData); - stringRedisTemplate.opsForValue().set(redisKey, jsonData, Duration.ofHours(24)); + String valueData = + String.format( + "{\"clauseOrder\":%d,\"requestData\":%s}", + requestDto.getClauseOrder(), jsonData); + stringRedisTemplate.opsForValue().set(redisKey, valueData, Duration.ofHours(24)); String notificationMessage = String.format( @@ -2039,17 +2157,19 @@ public FinalSpecialContractDocument respondToModificationRequest( } String redisKey = - "final-contract:modification:" - + contractChatId - + ":" - + responseDto.getClauseOrder(); - String requestDataJson = stringRedisTemplate.opsForValue().get(redisKey); + "final-contract:modification:" + contractChatId + ":" + contractChat.getOwnerId(); + String valueDataJson = stringRedisTemplate.opsForValue().get(redisKey); - if (requestDataJson == null) { - throw new IllegalArgumentException("해당 조항에 대한 대기중인 수정 요청이 없습니다."); + if (valueDataJson == null) { + throw new IllegalArgumentException("대기중인 수정 요청이 없습니다."); } try { + // JSON에서 clauseOrder와 requestData 추출 + com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(valueDataJson); + Integer clauseOrder = rootNode.get("clauseOrder").asInt(); + String requestDataJson = rootNode.get("requestData").toString(); + ModificationRequestData requestData = objectMapper.readValue(requestDataJson, ModificationRequestData.class); @@ -2065,8 +2185,7 @@ public FinalSpecialContractDocument respondToModificationRequest( finalContract.getFinalClauses().stream() .map( clause -> { - if (clause.getOrder() - .equals(responseDto.getClauseOrder())) { + if (clause.getOrder().equals(clauseOrder)) { return FinalSpecialContractDocument.FinalClause .builder() .order(clause.getOrder()) @@ -2079,27 +2198,19 @@ public FinalSpecialContractDocument respondToModificationRequest( .collect(Collectors.toList()); finalContract.setFinalClauses(updatedClauses); - specialContractMongoRepository.saveFinalSpecialContract(finalContract); resultMessage = - String.format( - "임차인이 특약 %d번 수정 요청을 수락했습니다. 특약이 변경되었습니다.", - responseDto.getClauseOrder()); - + String.format("임차인이 특약 %d번 수정 요청을 수락했습니다. 특약이 변경되었습니다.", clauseOrder); log.info("수정 수락 - 최종 특약서 업데이트 완료"); } else { resultMessage = - String.format( - "임차인이 특약 %d번 수정 요청을 거절했습니다. 기존 특약이 유지됩니다.", - responseDto.getClauseOrder()); - + String.format("임차인이 특약 %d번 수정 요청을 거절했습니다. 기존 특약이 유지됩니다.", clauseOrder); log.info("수정 거절 - 기존 특약서 유지"); } stringRedisTemplate.delete(redisKey); - AiMessage(contractChatId, resultMessage); return finalContract; @@ -2112,15 +2223,32 @@ public FinalSpecialContractDocument respondToModificationRequest( @Override public ModificationRequestData getPendingModificationRequest( Long contractChatId, Integer clauseOrder) { - String redisKey = "final-contract:modification:" + contractChatId + ":" + clauseOrder; - String requestDataJson = stringRedisTemplate.opsForValue().get(redisKey); + // 기존 방식 대신 임대인의 요청을 찾도록 수정 + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + return null; + } - if (requestDataJson == null) { + String redisKey = + "final-contract:modification:" + contractChatId + ":" + contractChat.getOwnerId(); + String valueDataJson = stringRedisTemplate.opsForValue().get(redisKey); + + if (valueDataJson == null) { return null; } try { + com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(valueDataJson); + Integer storedClauseOrder = rootNode.get("clauseOrder").asInt(); + + // 요청한 clauseOrder와 저장된 clauseOrder가 일치하는지 확인 + if (!clauseOrder.equals(storedClauseOrder)) { + return null; + } + + String requestDataJson = rootNode.get("requestData").toString(); return objectMapper.readValue(requestDataJson, ModificationRequestData.class); + } catch (Exception e) { log.error("수정 요청 데이터 파싱 실패", e); return null; @@ -2265,7 +2393,7 @@ public void requestFinalContractDeletion( throw new IllegalArgumentException("해당 특약 조항을 찾을 수 없습니다: " + clauseOrder); } - String redisKey = "final-contract:deletion:" + contractChatId + ":" + clauseOrder; + String redisKey = "final-contract:deletion:" + contractChatId + ":" + ownerId; String existingRequest = stringRedisTemplate.opsForValue().get(redisKey); if (existingRequest != null) { @@ -2273,7 +2401,9 @@ public void requestFinalContractDeletion( ChatErrorCode.CONTRACT_END_REQUEST_ALREADY_EXISTS, "이미 삭제 요청이 진행 중입니다."); } - stringRedisTemplate.opsForValue().set(redisKey, ownerId.toString(), Duration.ofHours(24)); + stringRedisTemplate + .opsForValue() + .set(redisKey, clauseOrder.toString(), Duration.ofHours(24)); String notificationMessage = String.format("임대인이 특약 %d번 삭제를 요청했습니다.", clauseOrder); diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java index 87ea30dd..9739523f 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java @@ -244,6 +244,9 @@ public interface ContractChatServiceInterface { List getIncompleteSpecialContractsByChat( Long contractChatId, Long userId); + List getIncompleteSpecialContractsWithoutMessage( + Long contractChatId, Long userId); + List proceedAllIncompleteToNextRound(Long contractChatId); void createNextRoundSpecialContractDocument( @@ -285,6 +288,11 @@ FinalSpecialContractDocument respondToModificationRequest( Map acceptFinalContractDeletion( Long contractChatId, Long buyerId, Integer clauseOrder); + String getContractStatusParam(Long contractChatId, Long userId); + /** 임차인이 최종 특약 삭제 거절 */ void rejectFinalContractDeletion(Long contractChatId, Long buyerId, Integer clauseOrder); + + Map respondToFinalContractDeletionRequest( + Long contractChatId, Long buyerId, FinalContractDeletionResponseDto responseDto); } From ad74d4974c551d75a950a9542680d8445826870e Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 13:13:35 +0900 Subject: [PATCH 11/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20Order=EB=B2=88=ED=98=B8=20=EC=97=86=EC=9D=B4=20=EC=88=98?= =?UTF-8?q?=EB=9D=BD=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SpecialContractMongoRepository.java | 158 ++---------------- 1 file changed, 15 insertions(+), 143 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java b/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java index b349193a..c35a81e9 100644 --- a/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java @@ -7,9 +7,6 @@ import org.scoula.domain.chat.document.SpecialContractDocument; import org.scoula.domain.chat.document.SpecialContractFixDocument; import org.scoula.domain.chat.document.SpecialContractSelectionDocument; -import org.scoula.domain.chat.dto.SpecialContractUserViewDto; -import org.scoula.domain.chat.mapper.ContractChatMapper; -import org.scoula.domain.chat.vo.ContractChat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; @@ -47,44 +44,17 @@ public Optional findByContractChatId(Long contractCh return Optional.ofNullable(result); } - /** contractChatId 존재 여부 확인 */ - public boolean existsByContractChatId(Long contractChatId) { - Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); - return mongoTemplate.exists(query, SpecialContractFixDocument.class); - } - /** 특약 문서 업데이트 */ public SpecialContractFixDocument updateSpecialContract(SpecialContractFixDocument document) { return mongoTemplate.save(document); } - /** 특정 라운드의 특약 문서들 조회 */ - public List findByRound(Long round) { - Query query = new Query(Criteria.where("round").is(round)); - return mongoTemplate.find(query, SpecialContractFixDocument.class); - } - /** 완료 여부로 특약 문서들 조회 */ public List findByIsPassed(Boolean isPassed) { Query query = new Query(Criteria.where("isPassed").is(isPassed)); return mongoTemplate.find(query, SpecialContractFixDocument.class); } - /** order 순으로 정렬하여 모든 특약 문서 조회 */ - public List findAllByOrderByOrderAsc() { - Query query = new Query(); - query.with( - org.springframework.data.domain.Sort.by( - org.springframework.data.domain.Sort.Direction.ASC, "order")); - return mongoTemplate.find(query, SpecialContractFixDocument.class); - } - - /** 특정 order의 특약 문서들 조회 */ - public List findByOrder(Long order) { - Query query = new Query(Criteria.where("order").is(order)); - return mongoTemplate.find(query, SpecialContractFixDocument.class); - } - public Optional findByContractChatIdAndOrder( Long contractChatId, Long order) { Query query = @@ -95,119 +65,6 @@ public Optional findByContractChatIdAndOrder( return Optional.ofNullable(result); } - public Optional findByContractChatIdAndOrderAndRound( - Long contractChatId, Long order, Long round) { - Query query = - new Query( - Criteria.where("contractChatId") - .is(contractChatId) - .and("order") - .is(order) - .and("round") - .is(round)); - SpecialContractFixDocument result = - mongoTemplate.findOne(query, SpecialContractFixDocument.class); - return Optional.ofNullable(result); - } - - /** 특약 문서 삭제 */ - public void deleteSpecialContract(SpecialContractFixDocument document) { - mongoTemplate.remove(document); - } - - public void deleteFinalContractClause(Long contractChatId, Integer clauseOrder) { - Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); - Update update = - new Update() - .pull("finalClauses", Query.query(Criteria.where("order").is(clauseOrder))); - - mongoTemplate.updateFirst(query, update, "FINAL_SPECIAL_CONTRACT"); - } - - /** contractChatId로 SpecialContractDocument (원본 특약 문서) 조회 */ - public Optional findSpecialContractDocumentByContractChatId( - Long contractChatId) { - Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); - SpecialContractDocument result = - mongoTemplate.findOne(query, SpecialContractDocument.class); - return Optional.ofNullable(result); - } - - /** 사용자 역할별 특약 문서 조회 (ID 포함) */ - public SpecialContractUserViewDto getSpecialContractForUserWithIds( - Long contractChatId, Long userId, ContractChatMapper contractChatMapper) { - // 1. Raw Document로 조회 - Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); - org.bson.Document rawDocument = - mongoTemplate.findOne(query, org.bson.Document.class, "SPECIAL_CONTRACT"); - - if (rawDocument == null) { - throw new IllegalArgumentException("해당 특약 문서를 찾을 수 없습니다: " + contractChatId); - } - - // 2. 사용자 역할 확인 - ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); - if (contractChat == null) { - throw new IllegalArgumentException("계약 채팅방을 찾을 수 없습니다."); - } - - boolean isOwner = userId.equals(contractChat.getOwnerId()); - boolean isTenant = userId.equals(contractChat.getBuyerId()); - - if (!isOwner && !isTenant) { - throw new IllegalArgumentException("해당 계약 채팅방에 접근 권한이 없습니다."); - } - - String userRole = isOwner ? "owner" : "tenant"; - - // 3. Raw Document에서 데이터 추출 - Long docContractChatId = rawDocument.getLong("contractChatId"); - ContractChat contractChats = contractChatMapper.findByContractChatId(contractChatId); - Long round = contractChats.getCurrentRound(); - Integer totalClauses = rawDocument.getInteger("totalClauses"); - - @SuppressWarnings("unchecked") - java.util.List clausesDocs = - (java.util.List) rawDocument.get("clauses"); - - java.util.List userClauses = - new java.util.ArrayList<>(); - - for (org.bson.Document clauseDoc : clausesDocs) { - Integer clauseId = clauseDoc.getInteger("_id"); // MongoDB의 _id 필드 - String title = clauseDoc.getString("title"); - String content = clauseDoc.getString("content"); - - org.bson.Document assessmentDoc = (org.bson.Document) clauseDoc.get("assessment"); - org.bson.Document userEvalDoc = - isOwner - ? (org.bson.Document) assessmentDoc.get("owner") - : (org.bson.Document) assessmentDoc.get("tenant"); - - String level = userEvalDoc.getString("level"); - String reason = userEvalDoc.getString("reason"); - - SpecialContractUserViewDto.ClauseUserView clauseView = - SpecialContractUserViewDto.ClauseUserView.builder() - .id(clauseId) - .title(title) - .content(content) - .level(level) - .reason(reason) - .build(); - - userClauses.add(clauseView); - } - - return SpecialContractUserViewDto.builder() - .contractChatId(docContractChatId) - .round(round) - .totalClauses(totalClauses) - .userRole(userRole) - .clauses(userClauses) - .build(); - } - public List findByContractChatIdAndIsPassed( Long contractChatId, Boolean isPassed) { Query query = @@ -219,6 +76,21 @@ public List findByContractChatIdAndIsPassed( return mongoTemplate.find(query, SpecialContractFixDocument.class); } + public List + findByContractChatIdAndIsPassedAndRecentDataMessagesEmpty( + Long contractChatId, Boolean isPassed) { + Criteria criteria = + Criteria.where("contractChatId") + .is(contractChatId) + .and("isPassed") + .is(isPassed) + .orOperator( + Criteria.where("recentData.messages").exists(false), + Criteria.where("recentData.messages").regex("^\\s*$")); + Query query = new Query(criteria); + return mongoTemplate.find(query, SpecialContractFixDocument.class); + } + public Optional findSpecialContractDocumentByContractChatIdAndRound( Long contractChatId, Long round) { Query query = From ea212611526a5a294094070e8c55e65bca5e760e Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 13:33:06 +0900 Subject: [PATCH 12/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20Controller=20=EC=A4=91=EB=B3=B5=20=EC=84=A0=EC=96=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ContractChatController.java | 12 ++++++++++++ .../chat/controller/ContractChatControllerImpl.java | 12 ++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java index 7a352ab1..bb20ee83 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java @@ -205,4 +205,16 @@ ResponseEntity> requestFinalContractDeletion( @GetMapping("/{contractChatId}/status") ResponseEntity> getContractStatus( @PathVariable Long contractChatId, Authentication authentication); + + @ApiOperation(value = "전체 라운드 특약 문서 조회", notes = "모든 라운드의 특약 문서를 조회합니다.") + @GetMapping("/special-contract/{contractChatId}/all-rounds") + ResponseEntity>> getAllRoundsSpecialContract( + @PathVariable Long contractChatId, Authentication authentication); + + @ApiOperation(value = "최종 특약 삭제 요청 응답", notes = "임차인이 삭제 요청을 수락 또는 거절합니다.") + @PostMapping("/final-contract/{contractChatId}/deletion-response") + ResponseEntity>> respondToFinalContractDeletion( + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication); } diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java index 5612165f..04e7e109 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java @@ -484,7 +484,7 @@ public ResponseEntity> createSpecialCont .body(ApiResponse.error("INTERNAL_ERROR", "특약 문서 생성 중 오류가 발생했습니다.")); } } - + @Override @GetMapping("/special-contract/{contractChatId}/all-rounds") public ResponseEntity>> getAllRoundsSpecialContract( @PathVariable Long contractChatId, Authentication authentication) { @@ -535,7 +535,7 @@ public ResponseEntity> getSpecialContrac .body(ApiResponse.error("INTERNAL_ERROR", "특약 문서 조회 중 오류가 발생했습니다.")); } } - + @Override @PutMapping("/special-contract/{contractChatId}/recent") public ResponseEntity> updateRecentData( @PathVariable Long contractChatId, @@ -709,7 +709,7 @@ public ResponseEntity> getFinalSpecial .body(ApiResponse.error("INTERNAL_ERROR", "최종 특약서 조회 중 오류가 발생했습니다.")); } } - + @Override @PostMapping("/final-contract/{contractChatId}/modification-request") public ResponseEntity> requestFinalContractModification( @PathVariable Long contractChatId, @@ -733,7 +733,7 @@ public ResponseEntity> requestFinalContract .body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다.")); } } - + @Override @PostMapping("/final-contract/{contractChatId}/modification-response") public ResponseEntity> respondToModificationRequest( @PathVariable Long contractChatId, @@ -759,7 +759,7 @@ public ResponseEntity> respondToModifi .body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다.")); } } - + @Override @GetMapping("/final-contract/{contractChatId}/modification-request/{clauseOrder}") public ResponseEntity> getPendingModificationRequest( @PathVariable Long contractChatId, @@ -913,7 +913,7 @@ public ResponseEntity> requestFinalContractDeletion( .body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage())); } } - + @Override @PostMapping("/final-contract/{contractChatId}/deletion-response") public ResponseEntity>> respondToFinalContractDeletion( @PathVariable Long contractChatId, From f9a5a47c92a16112c4b905fca06a7e4d63e923fc Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 13:47:57 +0900 Subject: [PATCH 13/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?ROUND4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scoula/domain/chat/service/ContractChatServiceImpl.java | 5 +++++ src/main/java/org/scoula/domain/chat/vo/ContractChat.java | 1 + 2 files changed, 6 insertions(+) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index fb95acaf..78cdff80 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -1011,6 +1011,7 @@ public Object processRoundResults( saveFinalSpecialContract(contractChatId); AiMessage(contractChatId, "모든 특약에 동의하셨습니다! 최종 특약서가 생성되었습니다."); + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.ROUND4); log.info("초안에서 최종 특약 저장 완료 - finalContractId: {}", finalContract.getId()); @@ -1058,6 +1059,7 @@ public Object processRoundResults( saveFinalSpecialContract(contractChatId); AiMessageNext(contractChatId, "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다."); + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.ROUND4); return Map.of( "message", @@ -1866,6 +1868,7 @@ public void checkFinalRoundCompletion(Long contractChatId) { + "총 " + finalContract.getTotalFinalClauses() + "개의 특약이 확정되었습니다."); + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.ROUND4); log.info( "최종 특약 자동 저장 완료 - finalContractId: {}, 총 {}개 조항", @@ -1955,6 +1958,8 @@ private String getContractChatStatus(ContractChat.ContractStatus status) { return "?step=3&round=2"; case ROUND3: return "?step=3&round=3"; + case ROUND4: + return "?step=3&round=4"; default: return null; } diff --git a/src/main/java/org/scoula/domain/chat/vo/ContractChat.java b/src/main/java/org/scoula/domain/chat/vo/ContractChat.java index 3ae0b67e..0823dbad 100644 --- a/src/main/java/org/scoula/domain/chat/vo/ContractChat.java +++ b/src/main/java/org/scoula/domain/chat/vo/ContractChat.java @@ -29,6 +29,7 @@ public enum ContractStatus { ROUND1, ROUND2, ROUND3, + ROUND4, STEP4 } From 55b892ba2bf9fd53fbf1b1eeb86549985276d0a7 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 14:12:51 +0900 Subject: [PATCH 14/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=95=BD=20?= =?UTF-8?q?=ED=99=95=EC=A0=95=20=EC=88=98=EB=9D=BD=20=EB=98=90=EB=8A=94=20?= =?UTF-8?q?=EA=B1=B0=EC=A0=88=20API=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ContractChatController.java | 9 +- .../ContractChatControllerImpl.java | 120 ++++++++---------- .../chat/service/ContractChatServiceImpl.java | 10 +- 3 files changed, 63 insertions(+), 76 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java index bb20ee83..1085f441 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java @@ -187,12 +187,9 @@ ResponseEntity> requestFinalContractConfirmation( @ApiOperation(value = "최종 특약 확정 수락 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 수락합니다.") @PostMapping("/{contractChatId}/final-contract/accept-confirmation") ResponseEntity>> acceptFinalContractConfirmation( - @PathVariable Long contractChatId, Authentication authentication); - - @ApiOperation(value = "최종 특약 확정 거절 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 거절합니다.") - @PostMapping("/{contractChatId}/final-contract/reject-confirmation") - ResponseEntity> rejectFinalContractConfirmation( - @PathVariable Long contractChatId, Authentication authentication); + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication); @ApiOperation(value = "최종 특약 삭제 요청 (임대인)", notes = "임대인이 최종 특약서의 특정 조항 삭제를 요청합니다.") @PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}") diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java index 04e7e109..8d79c9a2 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java @@ -484,6 +484,7 @@ public ResponseEntity> createSpecialCont .body(ApiResponse.error("INTERNAL_ERROR", "특약 문서 생성 중 오류가 발생했습니다.")); } } + @Override @GetMapping("/special-contract/{contractChatId}/all-rounds") public ResponseEntity>> getAllRoundsSpecialContract( @@ -535,6 +536,7 @@ public ResponseEntity> getSpecialContrac .body(ApiResponse.error("INTERNAL_ERROR", "특약 문서 조회 중 오류가 발생했습니다.")); } } + @Override @PutMapping("/special-contract/{contractChatId}/recent") public ResponseEntity> updateRecentData( @@ -709,6 +711,7 @@ public ResponseEntity> getFinalSpecial .body(ApiResponse.error("INTERNAL_ERROR", "최종 특약서 조회 중 오류가 발생했습니다.")); } } + @Override @PostMapping("/final-contract/{contractChatId}/modification-request") public ResponseEntity> requestFinalContractModification( @@ -733,6 +736,7 @@ public ResponseEntity> requestFinalContract .body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다.")); } } + @Override @PostMapping("/final-contract/{contractChatId}/modification-response") public ResponseEntity> respondToModificationRequest( @@ -759,6 +763,7 @@ public ResponseEntity> respondToModifi .body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다.")); } } + @Override @GetMapping("/final-contract/{contractChatId}/modification-request/{clauseOrder}") public ResponseEntity> getPendingModificationRequest( @@ -831,72 +836,6 @@ public ResponseEntity> requestFinalContractConfirmation( } } - @Override - @PostMapping("/{contractChatId}/final-contract/accept-confirmation") - public ResponseEntity>> acceptFinalContractConfirmation( - @PathVariable Long contractChatId, Authentication authentication) { - try { - Long userId = getUserIdFromAuthentication(authentication); - ContractChat contractChat = - contractChatService.getContractChatInfo(contractChatId, userId); - Long buyerId = contractChat.getBuyerId(); - Long ownerId = contractChat.getOwnerId(); - - if (!userId.equals(buyerId)) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } - - String redisKey = "final-contract:confirmation:" + contractChatId; - String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); - - if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } - - Map result = - contractChatService.acceptFinalContractConfirmation(contractChatId, userId); - - return ResponseEntity.ok(ApiResponse.success(result, "최종 특약서가 확정되었습니다.")); - } catch (Exception e) { - log.error("최종 특약 확정 수락 실패", e); - return ResponseEntity.badRequest() - .body(ApiResponse.error("최종 특약 확정 수락에 실패했습니다: " + e.getMessage())); - } - } - - @Override - @PostMapping("/{contractChatId}/final-contract/reject-confirmation") - public ResponseEntity> rejectFinalContractConfirmation( - @PathVariable Long contractChatId, Authentication authentication) { - try { - Long userId = getUserIdFromAuthentication(authentication); - - ContractChat contractChat = - contractChatService.getContractChatInfo(contractChatId, userId); - Long buyerId = contractChat.getBuyerId(); - Long ownerId = contractChat.getOwnerId(); - - if (!userId.equals(buyerId)) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } - - String redisKey = "final-contract:confirmation:" + contractChatId; - String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); - - if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } - - contractChatService.rejectFinalContractConfirmation(contractChatId, userId); - - return ResponseEntity.ok(ApiResponse.success("최종 특약 확정을 거절했습니다.")); - } catch (Exception e) { - log.error("최종 특약 확정 거절 실패", e); - return ResponseEntity.badRequest() - .body(ApiResponse.error("최종 특약 확정 거절에 실패했습니다: " + e.getMessage())); - } - } - @Override public ResponseEntity> requestFinalContractDeletion( Long contractChatId, Integer clauseOrder, Authentication authentication) { @@ -913,6 +852,7 @@ public ResponseEntity> requestFinalContractDeletion( .body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage())); } } + @Override @PostMapping("/final-contract/{contractChatId}/deletion-response") public ResponseEntity>> respondToFinalContractDeletion( @@ -940,6 +880,54 @@ public ResponseEntity>> respondToFinalContractDe } } + @Override + @PostMapping("/{contractChatId}/final-contract/accept-confirmation") + public ResponseEntity>> acceptFinalContractConfirmation( + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication) { + try { + Long userId = getUserIdFromAuthentication(authentication); + ContractChat contractChat = + contractChatService.getContractChatInfo(contractChatId, userId); + Long buyerId = contractChat.getBuyerId(); + Long ownerId = contractChat.getOwnerId(); + + if (!userId.equals(buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + String redisKey = "final-contract:confirmation:" + contractChatId; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + + if (storedOwnerId == null || !storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } + + Map result; + String message; + + if (responseDto.isAccepted()) { + result = + contractChatService.acceptFinalContractConfirmation(contractChatId, userId); + message = "최종 특약서가 확정되었습니다."; + } else { + contractChatService.rejectFinalContractConfirmation(contractChatId, userId); + result = + Map.of( + "message", "최종 특약 확정을 거절했습니다.", + "status", "REJECTED"); + message = "최종 특약 확정을 거절했습니다."; + } + + return ResponseEntity.ok(ApiResponse.success(result, message)); + } catch (Exception e) { + log.error("최종 특약 확정 응답 실패", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("최종 특약 확정 응답에 실패했습니다: " + e.getMessage())); + } + } + @Override @GetMapping("/{contractChatId}/status") public ResponseEntity> getContractStatus( diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index 78cdff80..b0ff963e 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -1011,7 +1011,8 @@ public Object processRoundResults( saveFinalSpecialContract(contractChatId); AiMessage(contractChatId, "모든 특약에 동의하셨습니다! 최종 특약서가 생성되었습니다."); - contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.ROUND4); + contractChatMapper.updateStatus( + contractChatId, ContractChat.ContractStatus.ROUND4); log.info("초안에서 최종 특약 저장 완료 - finalContractId: {}", finalContract.getId()); @@ -1059,7 +1060,8 @@ public Object processRoundResults( saveFinalSpecialContract(contractChatId); AiMessageNext(contractChatId, "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다."); - contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.ROUND4); + contractChatMapper.updateStatus( + contractChatId, ContractChat.ContractStatus.ROUND4); return Map.of( "message", @@ -1958,8 +1960,8 @@ private String getContractChatStatus(ContractChat.ContractStatus status) { return "?step=3&round=2"; case ROUND3: return "?step=3&round=3"; - case ROUND4: - return "?step=3&round=4"; + case ROUND4: + return "?step=3&round=4"; default: return null; } From 7f1b129f3fe07e9773d16e57529114e96f18dd13 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 15:35:57 +0900 Subject: [PATCH 15/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=ED=8A=B9=EC=95=BD=20=EC=A0=80=EC=9E=A5=20AI=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scoula/domain/chat/service/ContractChatServiceImpl.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index b0ff963e..6e09cd13 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -1879,9 +1879,6 @@ public void checkFinalRoundCompletion(Long contractChatId) { } catch (Exception e) { log.error("최종 특약 자동 저장 실패", e); - AiMessage( - contractChatId, - "모든 특약 협상이 완료되었지만 최종 특약서 생성 중 오류가 발생했습니다. " + "관리자에게 문의해주세요."); } } else { log.info("아직 4차 라운드의 모든 특약이 작성되지 않음"); From ccb3a13889d1c2f08635ad85ebae75ca76b0dce3 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 17:06:57 +0900 Subject: [PATCH 16/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=ED=8A=B9=EC=95=BD=20=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ContractChatServiceImpl.java | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index 6e09cd13..a65a1d77 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -1055,26 +1055,32 @@ public Object processRoundResults( "message", "특약 협상이 시작됩니다.", "completed", true, "createdOrders", createdOrders); } else { if (rejectedOrders.isEmpty()) { - try { - FinalSpecialContractDocument finalContract = - saveFinalSpecialContract(contractChatId); - - AiMessageNext(contractChatId, "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다."); - contractChatMapper.updateStatus( - contractChatId, ContractChat.ContractStatus.ROUND4); - - return Map.of( - "message", - "모든 특약이 완료되었습니다!", - "completed", - true, - "finalContractId", - finalContract.getId(), - "totalFinalClauses", - finalContract.getTotalFinalClauses()); - } catch (Exception e) { - log.error("최종 특약 저장 실패", e); - return Map.of("message", "특약은 완료되었지만 최종 저장 중 오류가 발생했습니다.", "completed", true); + List remainingIncompleteContracts = + specialContractMongoRepository.findByContractChatIdAndIsPassed( + contractChatId, false); + if (remainingIncompleteContracts.isEmpty()) { + try { + FinalSpecialContractDocument finalContract = + saveFinalSpecialContract(contractChatId); + + AiMessageNext(contractChatId, "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다."); + contractChatMapper.updateStatus( + contractChatId, ContractChat.ContractStatus.ROUND4); + + return Map.of( + "message", + "모든 특약이 완료되었습니다!", + "completed", + true, + "finalContractId", + finalContract.getId(), + "totalFinalClauses", + finalContract.getTotalFinalClauses()); + } catch (Exception e) { + log.error("최종 특약 저장 실패", e); + return Map.of( + "message", "특약은 완료되었지만 최종 저장 중 오류가 발생했습니다.", "completed", true); + } } } From 087601f84468189b28e32d5401f61f3b3397bc51 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Tue, 12 Aug 2025 17:10:41 +0900 Subject: [PATCH 17/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=ED=8A=B9=EC=95=BD=20=EC=A0=80=EC=9E=A5=20AI=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/scoula/domain/chat/service/ContractChatServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index a65a1d77..42614f8f 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2290,7 +2290,7 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다."); } - AiMessageBtn(contractChatId, "최종 특약을 요청하였습니다"); + AiMessageBtn(contractChatId, "임대인이 최종 특약 확정을 요청하였습니다"); String key = "final-contract:confirmation:" + contractChatId; String existingValue = stringRedisTemplate.opsForValue().get(key); From fd35ed2e3deec4434c9b39ba55893150944a0f5e Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 13:12:03 +0900 Subject: [PATCH 18/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9E=84=EB=8C=80?= =?UTF-8?q?=EC=9D=B8=20=EC=82=AC=EC=A0=84=EC=A1=B0=EC=82=AC=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scoula/domain/chat/service/ChatServiceImpl.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java index ce63c099..dda78442 100644 --- a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java @@ -833,19 +833,6 @@ public Long acceptContractRequest(Long chatRoomId, Long userId) { .type("URLLINK") .build(); handleChatMessage(linkMessage); - String contractChatUrls = - "http://localhost:5173/pre-contract/" - + contractChatRoomId.toString() - + "/owner?step=1"; - ChatMessageRequestDto linkMessages = - ChatMessageRequestDto.builder() - .chatRoomId(chatRoomId) - .senderId(originalChatRoom.getBuyerId()) - .receiverId(originalChatRoom.getOwnerId()) - .content(contractChatUrls) - .type("URLLINK") - .build(); - handleChatMessage(linkMessages); return contractChatRoomId; } From b6acc6338e088f7650495359711d38358ea126e0 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 13:12:36 +0900 Subject: [PATCH 19/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EB=A7=81=ED=81=AC=20=EC=A0=9C=EA=B3=B5=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ContractChatController.java | 5 +++++ .../controller/ContractChatControllerImpl.java | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java index 1085f441..93b67354 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatController.java @@ -214,4 +214,9 @@ ResponseEntity>> respondToFinalContractDeletion( @PathVariable Long contractChatId, @RequestBody FinalContractDeletionResponseDto responseDto, Authentication authentication); + + @ApiOperation(value = "계약 채팅방 URL 이동", notes = "계약 채팅방 URL로 이동하는 API") + @GetMapping("/{chatRoomId}/moveContractChat") + ResponseEntity> moveContractChat( + @PathVariable Long chatRoomId, Authentication authentication); } diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java index 8d79c9a2..ded5f5b4 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java @@ -73,6 +73,22 @@ private Long getUserIdFromAuthentication(Authentication authentication) { return currentUserOpt.get().getUserId(); } + @Override + @GetMapping("/{chatRoomId}/moveContractChat") + public ResponseEntity> moveContractChat( + @PathVariable Long chatRoomId, Authentication authentication) { + String currentUserEmail = authentication.getName(); + Optional currentUserOpt = userService.findByEmail(currentUserEmail); + + if (currentUserOpt.isEmpty()) { + throw new BusinessException(ChatErrorCode.USER_NOT_FOUND); + } + User currentUser = currentUserOpt.get(); + Long userId = currentUser.getUserId(); + String url = contractChatService.getContractChatRoomUrl(chatRoomId); + return ResponseEntity.ok(ApiResponse.success(url)); + } + @Override @PostMapping("/rooms") public ResponseEntity> createContractChat( From 5b57e285cd3c78b1d7a393f83edf78049a3ba4b1 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 13:13:15 +0900 Subject: [PATCH 20/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EB=A7=81=ED=81=AC=20=EC=A0=9C=EA=B3=B5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ContractChatServiceImpl.java | 30 ++++++++++++++++--- .../service/ContractChatServiceInterface.java | 6 ++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index 42614f8f..e2675040 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -21,7 +21,6 @@ import org.scoula.global.common.exception.BusinessException; import org.scoula.global.common.exception.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @@ -41,7 +40,7 @@ public class ContractChatServiceImpl implements ContractChatServiceInterface { private final ChatRoomMapper chatRoomMapper; private final ContractChatMessageRepository contractChatMessageRepository; private final SimpMessagingTemplate messagingTemplate; - @Lazy private final ChatServiceInterface chatService; + private final ChatServiceInterface chatService; private final AiClauseImproveService aiClauseImproveService; private final PreContractDataService preContractDataService; @@ -1947,7 +1946,7 @@ private Long getNextRoundNumber(ContractChat.ContractStatus status) { } } - private String getContractChatStatus(ContractChat.ContractStatus status) { + public String getContractChatStatus(ContractChat.ContractStatus status) { switch (status) { case STEP0: return "?step=1"; @@ -2345,7 +2344,12 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; - AiMessageNext(contractChatId, confirmationMessage); + AiMessage(contractChatId, confirmationMessage); + AiMessageNext( + contractChatId, + "다음은 마지막 4단계: ‘적법성 검토' 단계입니다.\n" + + "\n" + + "해당 계약 내용을 기준으로 법률적 적합성을 분석할게요. 잠시만 기다려주세요."); return Map.of( "message", @@ -2527,4 +2531,22 @@ public void rejectFinalContractDeletion( log.info("특약 {}번 삭제 거절 완료 - contractChatId: {}", clauseOrder, contractChatId); } + + public String getContractChatRoomUrl(Long chatRoomId) { + ChatRoom chatRoom = chatRoomMapper.findById(chatRoomId); + if (chatRoom == null) { + log.error("채팅방을 찾을 수 없음: {}", chatRoomId); + throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND); + } + ContractChat contractChatId = + contractChatMapper.findByUserAndHome( + chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId()); + if (contractChatId == null) { + log.error("채팅방을 찾을 수 없음: {}", chatRoomId); + throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND); + } + Long contractChatRoomId = contractChatId.getContractChatId(); + String param = getContractChatStatus(contractChatId.getStatus()); + return "http://localhost:5173/contract/" + contractChatRoomId + param; + } } diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java index 9739523f..8824ea28 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java @@ -262,6 +262,8 @@ void createNextRoundSpecialContractDocument( void AiMessageBtn(Long contractChatId, String content); + void AiMessageNext(Long contractChatId, String content); + ModificationRequestData requestFinalContractModification( Long contractChatId, Long ownerId, FinalContractModificationRequestDto requestDto); @@ -295,4 +297,8 @@ Map acceptFinalContractDeletion( Map respondToFinalContractDeletionRequest( Long contractChatId, Long buyerId, FinalContractDeletionResponseDto responseDto); + + String getContractChatRoomUrl(Long chatRoomId); + + String getContractChatStatus(ContractChat.ContractStatus status); } From fa09a0f0d2f4acaa8c2ab1de152db48d292ebc6d Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 13:13:26 +0900 Subject: [PATCH 21/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=EC=A1=B0=EC=82=AC=20=EC=95=88=EB=82=B4=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/OwnerPreContractServiceImpl.java | 21 ++++++++++++++++ .../service/PreContractServiceImpl.java | 24 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java index 5545014e..3fd418db 100644 --- a/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java @@ -7,6 +7,11 @@ import java.util.stream.Collectors; import org.scoula.domain.chat.document.SpecialContractDocument; +import org.scoula.domain.chat.dto.ChatMessageRequestDto; +import org.scoula.domain.chat.mapper.ContractChatMapper; +import org.scoula.domain.chat.service.ChatServiceInterface; +import org.scoula.domain.chat.service.ContractChatServiceInterface; +import org.scoula.domain.chat.vo.ContractChat; import org.scoula.domain.precontract.document.ContractDocumentMongoDocument; import org.scoula.domain.precontract.document.OwnerMongoDocument; import org.scoula.domain.precontract.dto.ai.ClauseRecommendRequestDto; @@ -52,6 +57,9 @@ public class OwnerPreContractServiceImpl implements OwnerPreContractService { private final MongoTemplate mongoTemplate; private final ObjectMapper objectMapper; private final AesCryptoUtil aesCryptoUtil; + private final ChatServiceInterface chatService; + private final ContractChatServiceInterface contractChatService; + private final ContractChatMapper contractChatMapper; @Override public Void requireVerification( @@ -411,6 +419,19 @@ public Void saveMongoDB(Long contractChatId, Long userId) { saveOwnerDocument(dto); processAiClauseRecommendation(contractChatId, userId, dto); + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + contractChatService.getContractChatStatus(contractChat.getStatus()); + String contractChatUrls = "http://localhost:5173/contract/" + contractChatId.toString(); + ChatMessageRequestDto linkMessages = + ChatMessageRequestDto.builder() + .chatRoomId(contractChatId) + .senderId(contractChat.getBuyerId()) + .receiverId(contractChat.getOwnerId()) + .content(contractChatUrls) + .type("URLLINK") + .build(); + chatService.handleChatMessage(linkMessages); + return null; } diff --git a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java index acb06ed0..69172e04 100644 --- a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java @@ -2,6 +2,11 @@ import java.util.Optional; +import org.scoula.domain.chat.dto.ChatMessageRequestDto; +import org.scoula.domain.chat.mapper.ContractChatMapper; +import org.scoula.domain.chat.service.ChatServiceInterface; +import org.scoula.domain.chat.service.ContractChatServiceInterface; +import org.scoula.domain.chat.vo.ContractChat; import org.scoula.domain.precontract.dto.tenant.*; import org.scoula.domain.precontract.enums.RentType; import org.scoula.domain.precontract.exception.PreContractErrorCode; @@ -25,6 +30,9 @@ public class PreContractServiceImpl implements PreContractService { private final TenantPreContractMapper tenantMapper; private final TenantMongoRepository mongoRepository; + private final ChatServiceInterface chatService; + private final ContractChatMapper contractChatMapper; + private final ContractChatServiceInterface contractChatService; // =============== 사기 위험도 확인 & 기본 세팅 ================== @@ -343,6 +351,22 @@ public Void saveMongoDB(Long contractChatId, Long userId) { throw new BusinessException(PreContractErrorCode.TENANT_INSERT, e); } + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + + String contractChatUrls = + "http://localhost:5173/pre-contract/" + contractChatId.toString() + "/owner?step=1"; + ChatMessageRequestDto linkMessages = + ChatMessageRequestDto.builder() + .chatRoomId(contractChatId) + .senderId(contractChat.getBuyerId()) + .receiverId(contractChat.getOwnerId()) + .content(contractChatUrls) + .type("URLLINK") + .build(); + contractChatService.AiMessage(contractChatId, "안녕하세요!\n" + "임대인이 입장하면 바로 계약서 작성을 시작할게요."); + contractChatService.AiMessageBtn(contractChatId, "기다리는 동안 \n" + "어려운 법률 용어와 법률 팁을 알아볼까요?"); + chatService.handleChatMessage(linkMessages); + return null; } } From e36cfdaeb044f9d4c61b7187c71f62fa464e79fe Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 15:10:24 +0900 Subject: [PATCH 22/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=95=98=EB=93=9C?= =?UTF-8?q?=EC=BD=94=EB=94=A9=20URL=20=EC=A3=BC=EC=86=8C=20properties=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/service/ChatServiceImpl.java | 24 +++++++++++++++---- .../chat/service/ContractChatServiceImpl.java | 6 ++++- .../service/PreContractServiceImpl.java | 8 +++++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java index dda78442..16856149 100644 --- a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java @@ -20,6 +20,7 @@ import org.scoula.domain.user.vo.User; import org.scoula.global.common.exception.BusinessException; import org.scoula.global.file.service.S3ServiceInterface; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @@ -52,6 +53,9 @@ public class ChatServiceImpl implements ChatServiceInterface { private final ContractChatMapper contractChatMapper; private final RedisTemplate stringRedisTemplate; + @Value("${app.url.contract.precontract.buyer}") + private String URL; + /** {@inheritDoc} */ @Override @Transactional @@ -100,7 +104,20 @@ public void handleChatMessage(ChatMessageRequestDto dto) { mongoRepository.saveMessage(dto.getChatRoomId(), message); - String preview = dto.getType().equals("TEXT") ? dto.getContent() : "[파일]"; + String preview; + switch (dto.getType()) { + case "TEXT": + preview = dto.getContent(); + break; + case "URLLINK": + preview = "[링크]"; + break; + case "FILE": + preview = "[파일]"; + break; + default: + preview = "[메시지]"; + } LocalDateTime now = LocalDateTime.now(); chatRoomMapper.updateLastMessage(dto.getChatRoomId(), preview, now); @@ -819,10 +836,7 @@ public Long acceptContractRequest(Long chatRoomId, Long userId) { .build(); handleChatMessage(acceptMessage); - String contractChatUrl = - "http://localhost:5173/pre-contract/" - + contractChatRoomId.toString() - + "/buyer?step=1"; + String contractChatUrl = URL.replace("{contractChatId}", contractChatRoomId.toString()); ChatMessageRequestDto linkMessage = ChatMessageRequestDto.builder() diff --git a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java index e2675040..ab4d41bc 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -21,6 +21,7 @@ import org.scoula.global.common.exception.BusinessException; import org.scoula.global.common.exception.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @@ -49,6 +50,9 @@ public class ContractChatServiceImpl implements ContractChatServiceInterface { private final ObjectMapper objectMapper = new ObjectMapper(); @Autowired private SpecialContractMongoRepository specialContractMongoRepository; + @Value("${url.contract.url}") + private String contractUrl; + /** {@inheritDoc} */ @Override @Transactional @@ -2547,6 +2551,6 @@ public String getContractChatRoomUrl(Long chatRoomId) { } Long contractChatRoomId = contractChatId.getContractChatId(); String param = getContractChatStatus(contractChatId.getStatus()); - return "http://localhost:5173/contract/" + contractChatRoomId + param; + return contractUrl + contractChatRoomId + param; } } diff --git a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java index 69172e04..504423c3 100644 --- a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java @@ -16,6 +16,7 @@ import org.scoula.domain.precontract.vo.TenantPreContractCheckVO; import org.scoula.domain.precontract.vo.TenantWolseInfoVO; import org.scoula.global.common.exception.BusinessException; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +35,9 @@ public class PreContractServiceImpl implements PreContractService { private final ContractChatMapper contractChatMapper; private final ContractChatServiceInterface contractChatService; + @Value("${app.url.contract.precontract.owner}") + private String URL; + // =============== 사기 위험도 확인 & 기본 세팅 ================== /** {@inheritDoc} */ @@ -353,8 +357,8 @@ public Void saveMongoDB(Long contractChatId, Long userId) { ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); - String contractChatUrls = - "http://localhost:5173/pre-contract/" + contractChatId.toString() + "/owner?step=1"; + String contractChatUrls = URL.replace("{contractChatId}", contractChatId.toString()); + ChatMessageRequestDto linkMessages = ChatMessageRequestDto.builder() .chatRoomId(contractChatId) From d02cfc4ed55be6e28b53c2e77ed6b565f640e546 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 15:11:09 +0900 Subject: [PATCH 23/30] =?UTF-8?q?=E2=9C=A8=20feat:=20ROUND4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/scoula/domain/chat/vo/ContractChat.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/chat/vo/ContractChat.java b/src/main/java/org/scoula/domain/chat/vo/ContractChat.java index 0823dbad..756ed828 100644 --- a/src/main/java/org/scoula/domain/chat/vo/ContractChat.java +++ b/src/main/java/org/scoula/domain/chat/vo/ContractChat.java @@ -46,6 +46,8 @@ public Long getCurrentRound() { return 3L; case ROUND3: return 4L; + case ROUND4: + return 5L; default: return 1L; } @@ -57,6 +59,7 @@ public boolean isInRound() { && (status == ContractStatus.ROUND0 || status == ContractStatus.ROUND1 || status == ContractStatus.ROUND2 - || status == ContractStatus.ROUND3); + || status == ContractStatus.ROUND3 + || status == ContractStatus.ROUND4); } } From f2bce82267e9bf0d5407d080badd185f25262909 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 15:11:20 +0900 Subject: [PATCH 24/30] =?UTF-8?q?=E2=9C=A8=20feat:=20UserId=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ContractChatControllerImpl.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java index ded5f5b4..8472c06f 100644 --- a/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java +++ b/src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java @@ -77,14 +77,7 @@ private Long getUserIdFromAuthentication(Authentication authentication) { @GetMapping("/{chatRoomId}/moveContractChat") public ResponseEntity> moveContractChat( @PathVariable Long chatRoomId, Authentication authentication) { - String currentUserEmail = authentication.getName(); - Optional currentUserOpt = userService.findByEmail(currentUserEmail); - - if (currentUserOpt.isEmpty()) { - throw new BusinessException(ChatErrorCode.USER_NOT_FOUND); - } - User currentUser = currentUserOpt.get(); - Long userId = currentUser.getUserId(); + Long userId = getUserIdFromAuthentication(authentication); String url = contractChatService.getContractChatRoomUrl(chatRoomId); return ResponseEntity.ok(ApiResponse.success(url)); } @@ -853,6 +846,7 @@ public ResponseEntity> requestFinalContractConfirmation( } @Override + @PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}") public ResponseEntity> requestFinalContractDeletion( Long contractChatId, Integer clauseOrder, Authentication authentication) { try { From fc5811fce7ee61901fde000481a12e924dd6d4dd Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 15:11:30 +0900 Subject: [PATCH 25/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9E=84=EB=8C=80?= =?UTF-8?q?=EC=9D=B8=20=EC=9D=B8=EC=A6=9D=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/contract/service/ContractServiceImpl.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java index 71fcb65f..a72f2d1a 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -82,7 +82,7 @@ public Void standByContract(Long contractChatId, Long userId) { @Override public Void saveContractMongo(Long contractChatId, Long userId) { // userId 검증 - validateUserId(contractChatId, userId); + validateIsOwner(contractChatId, userId); // 이미 생성된 계약 문서가 있으면 저장 대신 안내 메시지 전송 후 종료 ContractMongoDocument existing = repository.getContract(contractChatId); @@ -473,6 +473,14 @@ public void validateUserId(Long contractChatId, Long userId) { } } + public void validateIsOwner(Long contractChatId, Long userId) { + Long ownerId= + tenantMapper.selectContractOwnerId(contractChatId).orElseThrow(() -> new BusinessException(PreContractErrorCode.TENANT_USER)); + if (!userId.equals(ownerId)) { + throw new BusinessException(PreContractErrorCode.TENANT_USER); + } + } + private static String formatWonShort(int amount) { if (amount == 0) return "0원"; long eok = amount / 100_000_000; // 억 From 3e5e73d35538cf6ffbcd3561be7100935a67f529 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 15:11:36 +0900 Subject: [PATCH 26/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9E=84=EB=8C=80?= =?UTF-8?q?=EC=9D=B8=20=EC=9D=B8=EC=A6=9D=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/OwnerPreContractServiceImpl.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java index 3fd418db..ffc91736 100644 --- a/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java @@ -8,10 +8,13 @@ import org.scoula.domain.chat.document.SpecialContractDocument; import org.scoula.domain.chat.dto.ChatMessageRequestDto; +import org.scoula.domain.chat.mapper.ChatRoomMapper; import org.scoula.domain.chat.mapper.ContractChatMapper; import org.scoula.domain.chat.service.ChatServiceInterface; import org.scoula.domain.chat.service.ContractChatServiceInterface; +import org.scoula.domain.chat.vo.ChatRoom; import org.scoula.domain.chat.vo.ContractChat; +import org.scoula.domain.contract.service.ContractService; import org.scoula.domain.precontract.document.ContractDocumentMongoDocument; import org.scoula.domain.precontract.document.OwnerMongoDocument; import org.scoula.domain.precontract.dto.ai.ClauseRecommendRequestDto; @@ -60,6 +63,8 @@ public class OwnerPreContractServiceImpl implements OwnerPreContractService { private final ChatServiceInterface chatService; private final ContractChatServiceInterface contractChatService; private final ContractChatMapper contractChatMapper; + private final ContractService contractService; + private final ChatRoomMapper chatRoomMapper; @Override public Void requireVerification( @@ -420,18 +425,23 @@ public Void saveMongoDB(Long contractChatId, Long userId) { processAiClauseRecommendation(contractChatId, userId, dto); ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + ChatRoom chatRoom = + chatRoomMapper.findByUserAndHome( + contractChat.getOwnerId(), + contractChat.getBuyerId(), + contractChat.getHomeId()); contractChatService.getContractChatStatus(contractChat.getStatus()); - String contractChatUrls = "http://localhost:5173/contract/" + contractChatId.toString(); ChatMessageRequestDto linkMessages = ChatMessageRequestDto.builder() - .chatRoomId(contractChatId) + .chatRoomId(chatRoom.getChatRoomId()) .senderId(contractChat.getBuyerId()) .receiverId(contractChat.getOwnerId()) - .content(contractChatUrls) + .content("계약 채팅방 URL") .type("URLLINK") .build(); chatService.handleChatMessage(linkMessages); - + contractChatService.AiMessage(contractChatId, "임대인꼐서 입장하셨습니다! \uD83E\uDD1D 이제 계약을 시작합니다."); + contractService.saveContractMongo(contractChatId, userId); return null; } @@ -592,19 +602,17 @@ private ClauseRecommendRequestDto buildClauseRecommendRequest( ClauseRecommendRequestDto.OwnerData ownerRequestData = buildOwnerData(ownerData); ClauseRecommendRequestDto.TenantData tenantData = buildTenantData(ownerData); - return null; + return ClauseRecommendRequestDto.builder() + .ocrData(ocrData) + .ownerData(ownerRequestData) + .tenantData(tenantData) + .build(); } private ClauseRecommendRequestDto.OcrData buildOcrData( ContractDocumentMongoDocument contractDocument) { if (contractDocument == null) { - return ClauseRecommendRequestDto.OcrData.builder() - .extractedAt(null) - .fileName(null) - .rawText(null) - .source(null) - .specialTerms(null) - .build(); + return null; } return ClauseRecommendRequestDto.OcrData.builder() From f507e0e755a1d2f28bd465c7ec0adaecc8d9e202 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Wed, 13 Aug 2025 15:11:43 +0900 Subject: [PATCH 27/30] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9E=84=EB=8C=80?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EC=B6=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/precontract/mapper/TenantPreContractMapper.java | 2 ++ .../domain/precontract/mapper/TenantPreContractMapper.xml | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java b/src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java index e13f1d76..75d1f395 100644 --- a/src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java +++ b/src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java @@ -17,6 +17,8 @@ public interface TenantPreContractMapper { Optional selectContractBuyerId(@Param("contractChatId") Long contractChatId); + Optional selectContractOwnerId(@Param("contractChatId") Long contractChatId); + Optional selectBuyerId(@Param("contractChatId") Long contractChatId); // =============== 사기 위험도 확인 & 기본 세팅 ================== diff --git a/src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml b/src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml index 38ed3a74..12c94950 100644 --- a/src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml +++ b/src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml @@ -11,6 +11,12 @@ WHERE contract_chat_id = #{contractChatId} + +