From fa97a00647aba5eb070957f858a68b9d07066bee Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 11 Aug 2025 11:39:20 +0900 Subject: [PATCH 01/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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/87] =?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} + + 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 12c94950..e91beaaf 100644 --- a/src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml +++ b/src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml @@ -33,6 +33,7 @@ INNER JOIN identity_verification iv ON cc.buyer_id = iv.user_id WHERE cc.buyer_id = #{userId} + AND iv.contract_id = cc.contract_chat_id + + + + From 3755d8e68fe2744c51705c1282922b6a0fa34572 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Sun, 17 Aug 2025 18:38:41 +0900 Subject: [PATCH 41/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=B0=8F=20=EA=B8=88=EC=95=A1=EC=A1=B0?= =?UTF-8?q?=EC=9C=A8=20=EB=8B=A8=EA=B3=84=20AI=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=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 --- .../contract/service/ContractServiceImpl.java | 108 +++++++----------- 1 file changed, 44 insertions(+), 64 deletions(-) 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 0b025deb..26139a85 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -138,13 +138,15 @@ public Void getContractNext(Long contractChatId, Long userId) { contractChatService.AiMessage( contractChatId, """ - 👋🏻 안녕하세요! + 🎉 안녕하세요! 이 계약은 임대인 %s님과 임차인 %s님의 계약입니다. - 시작하기 전, 정보를 먼저 확인할게요. - 제출된 정보를 토대로 계약서를 추출할게요. + 이번 단계는 정보 확인 단계에요. """.formatted(aiDto.getOwnerName(), aiDto.getBuyerName()) ); + // 스텝 변경 + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP0); + // 2초 대기 try { Thread.sleep(2000); @@ -154,14 +156,22 @@ public Void getContractNext(Long contractChatId, Long userId) { contractChatService.AiMessageBtn(contractChatId, """ %s님과 %s님이 작성한 사전 조사를 토대로 - 정보를 추출한 결과가 다음과 같습니다. - 매물 정보, 조건을 확인하셨나요? + 정보를 추출한 결과, 👉오른쪽 계약서와 같아요. + 🏠매물 정보, 조건을 확인해주세요. 다음 단계로 넘어갈까요? - """.formatted(aiDto.getBuyerName(), aiDto.getOwnerName())); + """.formatted(aiDto.getOwnerName(), aiDto.getBuyerName())); return null; } + String step3StartMessage = "다음은 3단계: ‘특약 조율' 단계입니다.\n" + + "\n" + + "'특약'은 계약 당사자 간의 특별한 상호 합의로서 명확한 권리, 의무 관계를 명시해야 해요. \n" + + "\n" + + "하지만, 특약으로 기재했다고 모든 조항이 효력을 갖는 것이 아니에요. \n" + + "\n" + + "주택임대차보호법의 범위를 넘어서지 않도록 AI가 도와줄게요."; + @Override public Boolean nextStep(Long contractChatId, Long userId, NextStepDTO dto) { @@ -173,8 +183,11 @@ public Boolean nextStep(Long contractChatId, Long userId, NextStepDTO dto) { if (nextSteps) { boolean deposit = contractMapper.getDepositAdjustment(contractChatId); + // 스텝 변경 + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP1); + if (deposit) { - contractChatService.AiMessage(contractChatId, "다음 단계는 '금액 조율' 단계입니다"); + contractChatService.AiMessage(contractChatId, "다음은 2단계 '금액 조율' 단계입니다."); } else if (!deposit) { contractChatService.AiMessageBtn(contractChatId, """ 다음은 2단계 '금액 조율' 단계입니다. @@ -186,15 +199,21 @@ public Boolean nextStep(Long contractChatId, Long userId, NextStepDTO dto) { // 2초 대기 try { Thread.sleep(2000); + + // 다음 단계 메세지 보내기 + contractChatService.AiMessage(contractChatId, step3StartMessage); + + // 스텝 변경 + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); + + // 특약 초안 메시지 + contractChatService.AiMessageBtn(contractChatId, "특약 초안이 생성되었습니다. 각 조항을 검토하고 수락 / 거절을 선택하세요."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - // 다음 단계 메세지 보내기 - contractChatService.AiMessage(contractChatId, "이번 단계는 '금액 조율' 단계입니다"); } -// // 스텝 변경 -// contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP1); + } return nextSteps; @@ -212,15 +231,13 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { AIMessageDTO aiDto = AIMessageDTO.toDTO(doc); long contract = ChronoUnit.YEARS.between(aiDto.getContractStartDate(), aiDto.getContractEndDate()); - String rentType = tenantMapper.selectRentType(contractChatId, userId) + String rentType = tenantMapper.selectRentTypeAll(contractChatId, userId) .orElseThrow(() -> new BusinessException(ContractException.CONTRACT_GET, "전/월세 타입 조회 실패")); // 시작 메세지 보내기 contractChatService.AiMessage( contractChatId, """ - 다음은 2단계: ‘금액 조율’ 단계입니다. - 이 계약은 계약기간 %d년의 %s 계약입니다. 전세 보증금은 %s, 관리비는 %s입니다. @@ -322,63 +339,26 @@ public Void updateDepositPrice(Long contractChatId, Long userId) { // 7. Redis 값 삭제 stringRedisTemplate.delete(redisKey); + + // 스텝 변경 + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); + + // 다음 단계 메세지 보내기 + contractChatService.AiMessage(contractChatId, step3StartMessage); + + Thread.sleep(2000); + + // 특약 초안 메시지 + contractChatService.AiMessageBtn(contractChatId, "특약 초안이 생성되었습니다. 각 조항을 검토하고 수락 / 거절을 선택하세요."); + + } catch (Exception e) { throw new BusinessException(ContractException.CONTRACT_UPDATE, e); } - // 스텝 변경 - contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); - - // 다음 단계 메세지 보내기 - contractChatService.AiMessage(contractChatId, "이번 단계는 '특약 조율' 단계입니다"); - return null; } - - // 적법성 검사 -// @Override -// public ContractDTO getContracts (Long contractChatId, Long userId){ -// // userId 검증 -// validateUserId(contractChatId, userId); -// -// ContractDTO dto; -// -// // 몽고 DB에서 특약부분을 받아서 저장한다. -//// try { -// repository.saveSpecialContract(contractChatId); -// -// ContractMongoDocument document = repository.getContract(contractChatId); -// if (document == null) { -// throw new BusinessException(ContractException.CONTRACT_GET); -// } -// Long ownerContractId = contractMapper.getOwnerId(contractChatId); -// Long buyerContractId = contractMapper.getBuyerId(contractChatId); -// IdentityVerificationInfoVO ownerVO = identityVerificationService.getDecryptedVerificationInfo(contractChatId, ownerContractId); -// IdentityVerificationInfoVO buyerVO = identityVerificationService.getDecryptedVerificationInfo(contractChatId, buyerContractId); -// -// -// // 찾은 값을 Dto에 넣고 반환하기 -// dto = ContractDTO.toDTO(document, ownerVO, buyerVO); -//// } catch (Exception e) { -//// // 예외 로그 기록 및 사용자에게 전달할 메시지 등 처리 -//// log.error("특약사항 저장 실패 ❌", e); -//// throw new BusinessException(ContractException.CONTRACT_INSERT, e); -//// } -// -//// ContractMongoDocument document = repository.getContract(contractChatId); -//// if (document == null) { -//// throw new BusinessException(ContractException.CONTRACT_GET); -//// } -//// -//// // 찾은 값을 Dto에 넣고 반환하기 -//// ContractDTO dto = ContractDTO.toDTO(document); -// -//// ContractDTO dto = getContract(contractChatId, userId); -// -// return dto; -// } - @Override public Void saveSpecialContract(Long contractChatId, Long userId) { // userId 검증 From 56bb583809b08be3127e86c42f53c9033918f59c Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 00:30:52 +0900 Subject: [PATCH 42/87] =?UTF-8?q?=E2=9C=A8=20feat:=20serviceImpl=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 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 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 414c81d3..208a5d69 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -17,6 +17,7 @@ import org.scoula.domain.chat.repository.SpecialContractMongoRepository; 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.service.PreContractDataService; import org.scoula.global.common.exception.BusinessException; import org.scoula.global.common.exception.EntityNotFoundException; @@ -48,6 +49,7 @@ public class ContractChatServiceImpl implements ContractChatServiceInterface { private final Map> contractChatOnlineUsers = new ConcurrentHashMap<>(); private final RedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper = new ObjectMapper(); + private final ContractService contractService; @Autowired private SpecialContractMongoRepository specialContractMongoRepository; @Value("${front.base.url}") @@ -2449,11 +2451,14 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; AiMessage(contractChatId, confirmationMessage); - AiMessageNext( - contractChatId, - "다음은 마지막 4단계: ‘적법성 검토' 단계입니다.\n" - + "\n" - + "해당 계약 내용을 기준으로 법률적 적합성을 분석할게요. 잠시만 기다려주세요."); + /// api/contract/{contractChatId}/save/special-contract + // [적법성 검사] 계약서 1 몽고DB에 특약 저장 + contractService.saveSpecialContract(contractChatId, buyerId); + + AiMessageNext(contractChatId, "다음은 마지막 4단계: ‘적법성 검토' 단계입니다."); + AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); + // api/contract/{contractChatId}/legality + contractService.getLegality(contractChatId, buyerId); return Map.of( "message", From 3af890df1e77cbf27bd156dad29fadae808a9d11 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 00:31:00 +0900 Subject: [PATCH 43/87] =?UTF-8?q?=E2=9C=A8=20feat:=20controllerImpl=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 --- .../contract/controller/ContractControllerImpl.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index 7c94e640..a767c340 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -100,16 +100,6 @@ public ResponseEntity> updateDepositPrice( service.updateDepositPrice(contractChatId, userDetails.getUserId()))); } - // @Override - // @PostMapping("/getContracts") - // public ResponseEntity> getContracts( - // @PathVariable Long contractChatId, - // @AuthenticationPrincipal CustomUserDetails userDetails) { - // return ResponseEntity.ok( - // ApiResponse.success(service.getContract(contractChatId, - // userDetails.getUserId()))); - // } - @Override @PostMapping("/save/special-contract") public ResponseEntity> saveSpecialContract( From ef4331512e68febf184403341d4cb3fca7de86e4 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 00:31:16 +0900 Subject: [PATCH 44/87] =?UTF-8?q?=E2=9C=A8=20feat:=20serviceImpl=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/contract/service/ContractServiceImpl.java | 1 + 1 file changed, 1 insertion(+) 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 26139a85..436ddfe9 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -539,6 +539,7 @@ public Void updateBuyerLegality(Long contractChatId, Long userId, SpecialContrac return null; } + // 임차인 거절 @Override @Transactional From 4165b832f5eba4326608d60cd10d12dcfcf48af6 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 00:31:35 +0900 Subject: [PATCH 45/87] =?UTF-8?q?=E2=9C=A8=20feat:=20serviceImpl=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 --- .../precontract/service/TenantPreContractServiceImpl.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java index 20ff89c4..a425eb76 100644 --- a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java @@ -2,11 +2,6 @@ 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.chat.dto.ChatMessageRequestDto; import org.scoula.domain.chat.mapper.ChatRoomMapper; import org.scoula.domain.chat.mapper.ContractChatMapper; From a3b5d930e7d1f0e71d6940b1eab8ea324dcf09f9 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 01:01:30 +0900 Subject: [PATCH 46/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B4=80=EB=A0=A8=20Service?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/service/ContractFixService.java | 154 ++++++++++++++++++ .../service/ContractFixServiceInterface.java | 10 ++ 2 files changed, 164 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/service/ContractFixService.java create mode 100644 src/main/java/org/scoula/domain/contract/service/ContractFixServiceInterface.java diff --git a/src/main/java/org/scoula/domain/contract/service/ContractFixService.java b/src/main/java/org/scoula/domain/contract/service/ContractFixService.java new file mode 100644 index 00000000..2a2cdd17 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/service/ContractFixService.java @@ -0,0 +1,154 @@ +package org.scoula.domain.contract.service; + +import org.scoula.domain.contract.document.ContractMongoDocument; +import org.scoula.domain.contract.dto.ContractDTO; +import org.scoula.domain.contract.dto.LegalityDTO; +import org.scoula.domain.contract.exception.ContractException; +import org.scoula.domain.contract.mapper.ContractMapper; +import org.scoula.domain.contract.repository.ContractMongoRepository; +import org.scoula.domain.precontract.exception.PreContractErrorCode; +import org.scoula.domain.precontract.mapper.TenantPreContractMapper; +import org.scoula.domain.precontract.service.IdentityVerificationService; +import org.scoula.domain.precontract.vo.IdentityVerificationInfoVO; +import org.scoula.global.common.exception.BusinessException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class ContractFixService implements ContractFixServiceInterface { + private final ContractMongoRepository repository; + private final RestTemplate restTemplate; + private final IdentityVerificationService identityVerificationService; + + private final ContractMapper contractMapper; + private final TenantPreContractMapper tenantMapper; + + @Value("${ai.server.url:http://localhost:8000}") + private String aiServerUrl; + + @Override + public LegalityDTO getLegality(Long contractChatId, Long userId) { + // userId 검증 + validateUserId(contractChatId, userId); + + // MongoDB에서 전체 부분을 조회한다 + ContractMongoDocument document = repository.getContract(contractChatId); + if (document == null) { + throw new BusinessException(ContractException.CONTRACT_GET); + } + + Long ownerContractId = contractMapper.getOwnerId(contractChatId); + Long buyerContractId = contractMapper.getBuyerId(contractChatId); + IdentityVerificationInfoVO ownerVO = + identityVerificationService.getDecryptedVerificationInfo( + contractChatId, ownerContractId); + IdentityVerificationInfoVO buyerVO = + identityVerificationService.getDecryptedVerificationInfo( + contractChatId, buyerContractId); + + ContractDTO dto = ContractDTO.toDTO(document, ownerVO, buyerVO); + + // AI + try { + // AI로 해당 데이터를 넘긴다 (restTemplate 사용) + String url = aiServerUrl + "/api/contract/validate"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity = new HttpEntity<>(dto, headers); + + // 반환값을 받아오고, 그 값을 프론트에 넘겨준다. + ResponseEntity response = + restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class); + LegalityDTO res = response.getBody(); + assert res != null; + log.warn("AI 응답 값 확인: {}", res.toString()); + + log.warn("AI 응답 헤더 확인: {}", response.getStatusCode()); + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + return response.getBody(); + } else { + // Sanitize response body before logging to prevent log injection + String responseBodyStr; + try { + ObjectMapper objectMapper = new ObjectMapper(); + responseBodyStr = objectMapper.writeValueAsString(response.getBody()); + } catch (Exception ex) { + responseBodyStr = String.valueOf(response.getBody()); + } + // Remove newlines and carriage returns + responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " "); + log.error(responseBodyStr); + throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR); + } + + } catch (Exception e) { + log.error(e.getMessage()); + throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR, e); + } + } + + @Override + public Void saveSpecialContract(Long contractChatId, Long userId) { + // userId 검증 + validateUserId(contractChatId, userId); + // 몽고 DB에서 특약부분을 받아서 저장한다. + try { + repository.saveSpecialContract(contractChatId); + } catch (Exception e) { + // 예외 로그 기록 및 사용자에게 전달할 메시지 등 처리 + log.error("특약사항 저장 실패 ❌", e); + throw new BusinessException(ContractException.CONTRACT_INSERT, e); + } + return null; + } + + public void validateUserId(Long contractChatId, Long userId) { + + if (userId == null) { + throw new BusinessException(PreContractErrorCode.TENANT_USER); + } + + Long ownerContractId = contractMapper.getOwnerId(contractChatId); + Long buyerContractId = contractMapper.getBuyerId(contractChatId); + + if (userId.equals(ownerContractId)) { + validateIsOwner(contractChatId, userId); + return; + } + + if (userId.equals(buyerContractId)) { + Long buyerId = + tenantMapper + .selectContractBuyerId(contractChatId) + .orElseThrow( + () -> new BusinessException(PreContractErrorCode.TENANT_USER)); + + if (!userId.equals(buyerId)) { + throw new BusinessException(PreContractErrorCode.TENANT_USER); + } + return; + } + + throw new BusinessException(PreContractErrorCode.TENANT_USER); + } + + 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); + } + } +} diff --git a/src/main/java/org/scoula/domain/contract/service/ContractFixServiceInterface.java b/src/main/java/org/scoula/domain/contract/service/ContractFixServiceInterface.java new file mode 100644 index 00000000..3e81a7c9 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/service/ContractFixServiceInterface.java @@ -0,0 +1,10 @@ +package org.scoula.domain.contract.service; + +import org.scoula.domain.contract.dto.LegalityDTO; + +public interface ContractFixServiceInterface { + + Void saveSpecialContract(Long contractChatId, Long userId); + + LegalityDTO getLegality(Long contractChatId, Long userId); +} From 6b9f27834da5a4526156637ff40ce66efac16365 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 01:01:41 +0900 Subject: [PATCH 47/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B4=80=EB=A0=A8=20Service?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/service/ContractService.java | 19 ----- .../contract/service/ContractServiceImpl.java | 81 ++----------------- 2 files changed, 7 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/scoula/domain/contract/service/ContractService.java b/src/main/java/org/scoula/domain/contract/service/ContractService.java index e875661d..03f334c7 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractService.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractService.java @@ -72,25 +72,6 @@ public interface ContractService { */ Void updateDepositPrice(Long contractChatId, Long userId); - // /** - // * @param contractChatId 채팅방 아이디 - // * @param userId 유저 아이디 - // * @return 계약서 내용을 보내기 - // */ - // ContractDTO getContracts(Long contractChatId, Long userId); - - /** * step4 start 특약을 개약 테이블에 저장하기 * * @param contractChatId 채팅방 아이디 * @param userId 유저 아이디 */ - Void saveSpecialContract(Long contractChatId, Long userId); - - /** - * step4 (init) 계약서를 AI로 보내고, 적법성 받기 - * - * @param contractChatId 채팅방 아이디 - * @param userId 유저 아이디 - * @return AI가 계약서를 보고 주는 적법성을 리턴값으로 보내기 - */ - LegalityDTO getLegality(Long contractChatId, Long userId); - /** * step4 적법성 검사 후 수정된 특약으로 변경 * 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 436ddfe9..5cf9e6c4 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.context.annotation.Lazy; import org.scoula.domain.chat.mapper.ContractChatMapper; import org.scoula.domain.chat.service.ContractChatServiceInterface; import org.scoula.domain.chat.vo.ContractChat; @@ -38,22 +39,20 @@ @Log4j2 public class ContractServiceImpl implements ContractService { - private final ContractChatServiceInterface contractChatService; + private final @Lazy ContractChatServiceInterface contractChatService; - private final ContractMapper contractMapper; private final ContractMongoRepository repository; - private final RestTemplate restTemplate; - private final TenantPreContractMapper tenantMapper; - private final ContractChatMapper contractChatMapper; private final IdentityVerificationService identityVerificationService; + private final ContractChatMapper contractChatMapper; + private final ContractMapper contractMapper; + private final TenantPreContractMapper tenantMapper; private final ObjectMapper objectMapper = new ObjectMapper(); private final RedisTemplate stringRedisTemplate; private final S3ServiceImpl s3Service; private final EmailServiceImpl emailService; - @Value("${ai.server.url:http://localhost:8000}") - private String aiServerUrl; + /** {@inheritDoc} */ @Override @@ -359,77 +358,11 @@ public Void updateDepositPrice(Long contractChatId, Long userId) { return null; } - @Override - public Void saveSpecialContract(Long contractChatId, Long userId) { - // userId 검증 - validateUserId(contractChatId, userId); - // 몽고 DB에서 특약부분을 받아서 저장한다. - try { repository.saveSpecialContract(contractChatId); } catch (Exception e) { - // 예외 로그 기록 및 사용자에게 전달할 메시지 등 처리 - log.error("특약사항 저장 실패 ❌", e); - throw new BusinessException(ContractException.CONTRACT_INSERT, e); } - return null; - } + /** {@inheritDoc} */ // ai로 적법성 검사하기 -> 암호화 풀어서 보내기 - @Override - public LegalityDTO getLegality(Long contractChatId, Long userId) { - // userId 검증 - validateUserId(contractChatId, userId); - - // MongoDB에서 전체 부분을 조회한다 - ContractMongoDocument document = repository.getContract(contractChatId); - if (document == null) { - throw new BusinessException(ContractException.CONTRACT_GET); - } - Long ownerContractId = contractMapper.getOwnerId(contractChatId); - Long buyerContractId = contractMapper.getBuyerId(contractChatId); - IdentityVerificationInfoVO ownerVO = identityVerificationService.getDecryptedVerificationInfo(contractChatId, ownerContractId); - IdentityVerificationInfoVO buyerVO = identityVerificationService.getDecryptedVerificationInfo(contractChatId, buyerContractId); - - ContractDTO dto = ContractDTO.toDTO(document, ownerVO, buyerVO); - - // AI - try { - // AI로 해당 데이터를 넘긴다 (restTemplate 사용) - String url = aiServerUrl + "/api/contract/validate"; - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity requestEntity = new HttpEntity<>(dto, headers); - - // 반환값을 받아오고, 그 값을 프론트에 넘겨준다. - ResponseEntity response = - restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class); - LegalityDTO res = response.getBody(); - assert res != null; - log.warn("AI 응답 값 확인: {}", res.toString()); - - log.warn("AI 응답 헤더 확인: {}", response.getStatusCode()); - if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { - return response.getBody(); - } else { - // Sanitize response body before logging to prevent log injection - String responseBodyStr; - try { - ObjectMapper objectMapper = new ObjectMapper(); - responseBodyStr = objectMapper.writeValueAsString(response.getBody()); - } catch (Exception ex) { - responseBodyStr = String.valueOf(response.getBody()); - } - // Remove newlines and carriage returns - responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " "); - log.error(responseBodyStr); - throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR); - } - - } catch (Exception e) { - log.error(e.getMessage()); - throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR, e); - } - } // 임대인 삭제 @Override From f4de98f5fcc774dee24e9199d5c493ad1f6f580c Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 01:01:58 +0900 Subject: [PATCH 48/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B4=80=EB=A0=A8=20Service?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=EB=90=9C=20=EB=A1=9C=EC=A7=81=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/controller/ContractControllerImpl.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index a767c340..ae990e94 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -1,6 +1,8 @@ package org.scoula.domain.contract.controller; +import org.scoula.domain.chat.service.ContractChatServiceInterface; import org.scoula.domain.contract.dto.*; +import org.scoula.domain.contract.service.ContractFixServiceInterface; import org.scoula.domain.contract.service.ContractService; import org.scoula.global.auth.dto.CustomUserDetails; import org.scoula.global.common.dto.ApiResponse; @@ -17,7 +19,10 @@ @RequestMapping("/api/contract/{contractChatId}") public class ContractControllerImpl implements ContractController { + private final ContractFixServiceInterface contractFixService; + private final ContractService service; + private final ContractChatServiceInterface contractChatService; @Override @PostMapping("") @@ -107,7 +112,8 @@ public ResponseEntity> saveSpecialContract( @AuthenticationPrincipal CustomUserDetails userDetails) { return ResponseEntity.ok( ApiResponse.success( - service.saveSpecialContract(contractChatId, userDetails.getUserId()))); + contractFixService.saveSpecialContract( + contractChatId, userDetails.getUserId()))); } @Override @@ -116,7 +122,8 @@ public ResponseEntity> getLegality( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { return ResponseEntity.ok( - ApiResponse.success(service.getLegality(contractChatId, userDetails.getUserId()))); + ApiResponse.success( + contractFixService.getLegality(contractChatId, userDetails.getUserId()))); } @Override From ec571c6806c6bacfb7435dcff876a72c5356c834 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 01:02:30 +0900 Subject: [PATCH 49/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B4=80=EB=A0=A8=20Service?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=EB=90=9C=20=EB=A1=9C=EC=A7=81=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ContractChatServiceImpl.java | 98 +++++++++++++++++-- 1 file changed, 91 insertions(+), 7 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 208a5d69..85ef479f 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -17,13 +17,14 @@ import org.scoula.domain.chat.repository.SpecialContractMongoRepository; 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.contract.service.ContractFixServiceInterface; import org.scoula.domain.precontract.service.PreContractDataService; 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.http.*; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -45,11 +46,10 @@ public class ContractChatServiceImpl implements ContractChatServiceInterface { private final ChatServiceInterface chatService; private final AiClauseImproveService aiClauseImproveService; private final PreContractDataService preContractDataService; - + private final ContractFixServiceInterface contractFixService; private final Map> contractChatOnlineUsers = new ConcurrentHashMap<>(); private final RedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper = new ObjectMapper(); - private final ContractService contractService; @Autowired private SpecialContractMongoRepository specialContractMongoRepository; @Value("${front.base.url}") @@ -2451,14 +2451,98 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; AiMessage(contractChatId, confirmationMessage); - /// api/contract/{contractChatId}/save/special-contract + // [적법성 검사] 계약서 1 몽고DB에 특약 저장 - contractService.saveSpecialContract(contractChatId, buyerId); + contractFixService.saveSpecialContract(contractChatId, buyerId); - AiMessageNext(contractChatId, "다음은 마지막 4단계: ‘적법성 검토' 단계입니다."); + AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); + // api/contract/{contractChatId}/legality - contractService.getLegality(contractChatId, buyerId); + try { + Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); + + if (legalityResponse instanceof Map) { + Map responseMap = (Map) legalityResponse; + Map data = (Map) responseMap.get("data"); + if (data != null) { + Map innerData = (Map) data.get("data"); + if (innerData != null) { + List> violations = + (List>) innerData.get("violations"); + + if (violations != null && !violations.isEmpty()) { + AiMessage(contractChatId, "적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + + for (int i = 0; i < violations.size(); i++) { + Map violation = violations.get(i); + + String violationType = (String) violation.get("violation_type"); + String lawName = (String) violation.get("law_name"); + String violationContent = + (String) violation.get("violation_content"); + String explanation = (String) violation.get("explanation"); + String improvementExample = + (String) violation.get("improvement_example"); + String legalBasis = (String) violation.get("legal_basis"); + String originalClause = (String) violation.get("original_clause"); + + StringBuilder violationMessage = new StringBuilder(); + violationMessage.append(String.format("문제점 %d\n", i + 1)); + violationMessage.append( + String.format( + "위반 유형: %s\n", + violationType != null ? violationType : "정보 없음")); + violationMessage.append( + String.format( + "관련 법령: %s\n", + lawName != null ? lawName : "정보 없음")); + violationMessage.append( + String.format( + "위반 내용: %s\n", + violationContent != null + ? violationContent + : "정보 없음")); + violationMessage.append( + String.format( + "설명: %s\n", + explanation != null ? explanation : "정보 없음")); + + if (originalClause != null && !originalClause.trim().isEmpty()) { + violationMessage.append( + String.format("문제가 된 조항: %s\n", originalClause)); + } + + if (improvementExample != null + && !improvementExample.trim().isEmpty()) { + violationMessage.append( + String.format("개선 방안: %s\n", improvementExample)); + } + + if (legalBasis != null && !legalBasis.trim().isEmpty()) { + violationMessage.append(String.format("법적 근거: %s", legalBasis)); + } + + AiMessage(contractChatId, violationMessage.toString()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); + } else { + AiMessage(contractChatId, "적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + } + } + } + } + } catch (Exception e) { + log.error("적법성 검사 결과 처리 중 오류 발생", e); + AiMessage(contractChatId, "적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); + } return Map.of( "message", From b421fbc5c028ab51f368c77d92f4e6337ff30004 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 01:37:23 +0900 Subject: [PATCH 50/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=209996=20Ai=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ContractChatServiceImpl.java | 147 ++++++++++-------- 1 file changed, 81 insertions(+), 66 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 85ef479f..cd47521f 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -197,6 +197,24 @@ public void AiMessageBtn(Long contractChatId, String content) { messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, aiMessage); } + public void AiMessageLegal(Long contractChatId, String content) { + final Long ai = 9996L; + + 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); + } + + /** {@inheritDoc} */ @Override public List getContractMessages(Long contractChatId) { @@ -2460,84 +2478,81 @@ public Map acceptFinalContractConfirmation(Long contractChatId, // api/contract/{contractChatId}/legality try { + log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); + log.info("적법성 검사 응답: {}", legalityResponse); if (legalityResponse instanceof Map) { Map responseMap = (Map) legalityResponse; - Map data = (Map) responseMap.get("data"); - if (data != null) { - Map innerData = (Map) data.get("data"); - if (innerData != null) { - List> violations = - (List>) innerData.get("violations"); - - if (violations != null && !violations.isEmpty()) { - AiMessage(contractChatId, "적법성 검사 결과, 일부 문제점이 발견되었습니다:"); - - for (int i = 0; i < violations.size(); i++) { - Map violation = violations.get(i); - - String violationType = (String) violation.get("violation_type"); - String lawName = (String) violation.get("law_name"); - String violationContent = - (String) violation.get("violation_content"); - String explanation = (String) violation.get("explanation"); - String improvementExample = - (String) violation.get("improvement_example"); - String legalBasis = (String) violation.get("legal_basis"); - String originalClause = (String) violation.get("original_clause"); - - StringBuilder violationMessage = new StringBuilder(); - violationMessage.append(String.format("문제점 %d\n", i + 1)); - violationMessage.append( - String.format( - "위반 유형: %s\n", - violationType != null ? violationType : "정보 없음")); - violationMessage.append( - String.format( - "관련 법령: %s\n", - lawName != null ? lawName : "정보 없음")); - violationMessage.append( - String.format( - "위반 내용: %s\n", - violationContent != null - ? violationContent - : "정보 없음")); - violationMessage.append( - String.format( - "설명: %s\n", - explanation != null ? explanation : "정보 없음")); - - if (originalClause != null && !originalClause.trim().isEmpty()) { - violationMessage.append( - String.format("문제가 된 조항: %s\n", originalClause)); - } - - if (improvementExample != null - && !improvementExample.trim().isEmpty()) { - violationMessage.append( - String.format("개선 방안: %s\n", improvementExample)); - } - if (legalBasis != null && !legalBasis.trim().isEmpty()) { - violationMessage.append(String.format("법적 근거: %s", legalBasis)); - } + Object violationsObj = responseMap.get("violations"); + log.info("violations 객체: {}", violationsObj); + + if (violationsObj instanceof List) { + List> violations = (List>) violationsObj; + log.info("violations 크기: {}", violations.size()); + + if (!violations.isEmpty()) { + log.info("위반 사항 발견됨: {}개", violations.size()); + AiMessage(contractChatId, "적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + + for (int i = 0; i < violations.size(); i++) { + Map violation = violations.get(i); + log.info("위반 사항 {}: {}", i + 1, violation); + + String violationType = (String) violation.get("violation_type"); + String lawName = (String) violation.get("law_name"); + String violationContent = (String) violation.get("violation_content"); + String explanation = (String) violation.get("explanation"); + String improvementExample = (String) violation.get("improvement_example"); + String legalBasis = (String) violation.get("legal_basis"); + String originalClause = (String) violation.get("original_clause"); + + StringBuilder violationMessage = new StringBuilder(); + violationMessage.append(String.format("문제점 %d\n", i + 1)); + violationMessage.append(String.format("위반 유형: %s\n", + violationType != null ? violationType : "정보 없음")); + violationMessage.append(String.format("관련 법령: %s\n", + lawName != null ? lawName : "정보 없음")); + violationMessage.append(String.format("위반 내용: %s\n", + violationContent != null ? violationContent : "정보 없음")); + violationMessage.append(String.format("설명: %s\n", + explanation != null ? explanation : "정보 없음")); + + if (originalClause != null && !originalClause.trim().isEmpty()) { + violationMessage.append(String.format("문제가 된 조항: %s\n", originalClause)); + } - AiMessage(contractChatId, violationMessage.toString()); + if (improvementExample != null && !improvementExample.trim().isEmpty()) { + violationMessage.append(String.format("개선 방안: %s\n", improvementExample)); + } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + if (legalBasis != null && !legalBasis.trim().isEmpty()) { + violationMessage.append(String.format("법적 근거: %s", legalBasis)); } - AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); - } else { - AiMessage(contractChatId, "적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + log.info("전송할 메시지: {}", violationMessage.toString()); + AiMessageLegal(contractChatId, violationMessage.toString()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } + + AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); + } else { + log.info("위반 사항 없음 - 빈 배열"); + AiMessage(contractChatId, "적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); } + } else { + log.warn("violations가 List 타입이 아님: {}", violationsObj != null ? violationsObj.getClass() : "null"); + AiMessage(contractChatId, "적법성 검사 응답 형식이 올바르지 않습니다."); } + } else { + log.warn("응답이 Map 타입이 아님: {}", legalityResponse != null ? legalityResponse.getClass() : "null"); + AiMessage(contractChatId, "적법성 검사 응답 형식이 올바르지 않습니다."); } } catch (Exception e) { log.error("적법성 검사 결과 처리 중 오류 발생", e); From eae54206388914ce9fd76c6ab22bde8e02b64ac9 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 01:58:29 +0900 Subject: [PATCH 51/87] =?UTF-8?q?=E2=9C=A8=20feat:=20order=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/contract/repository/ContractMongoRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java index ed9f4fa7..dfed3010 100644 --- a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -90,7 +90,7 @@ public void saveSpecialContract(Long contractChatId) { .map( fc -> ContractMongoDocument.SpecialContract.builder() - .order(fc.getOrder() + 1) + .order(fc.getOrder()) .title(fc.getTitle()) .content(fc.getContent()) .build()) From 9a5322c1abc3cd9ee637fffaad1bf292ba9cec64 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 02:41:44 +0900 Subject: [PATCH 52/87] =?UTF-8?q?=E2=9C=A8=20feat:=20final=5Fcontract?= =?UTF-8?q?=EC=9D=98=20=ED=8A=B9=EC=95=BD=20=EB=82=B4=EC=9A=A9=20=EC=A7=80?= =?UTF-8?q?=EC=9A=B0=EA=B8=B0=20=EB=A9=94=EC=84=9C=EB=93=9C=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 --- .../repository/ContractMongoRepository.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java index ed9f4fa7..e502194e 100644 --- a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -1,6 +1,7 @@ package org.scoula.domain.contract.repository; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -101,6 +102,23 @@ public void saveSpecialContract(Long contractChatId) { // Step 4: 저장 mongoTemplate.save(contractDoc); } + public void clearSpecialContracts(Long contractChatId) { + Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); + ContractMongoDocument contractDoc = + mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); + + if (contractDoc == null) { + throw new BusinessException(ContractException.CONTRACT_GET, "계약서를 찾을 수 없습니다."); + } + + // 특약 내용을 빈 리스트로 설정 + contractDoc.setSpecialContracts(new ArrayList<>()); + + // 저장 + mongoTemplate.save(contractDoc); + + System.out.println("특약 내용 삭제 완료 - contractChatId: " + contractChatId); + } public void updateSpecialContract(Long contractChatId, SpecialContractUpdateDTO dto) { Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); From 120729333b448341ec2e3d0e06cfdadb2446bbf4 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 02:42:09 +0900 Subject: [PATCH 53/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EB=82=B4=EC=9A=A9=20=ED=99=95=EC=A0=95=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EB=B0=8F=20=EC=88=98=EB=9D=BD=20api=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 --- .../controller/ContractController.java | 15 +++++ .../controller/ContractControllerImpl.java | 62 ++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractController.java b/src/main/java/org/scoula/domain/contract/controller/ContractController.java index b5d892f2..05def41c 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractController.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractController.java @@ -1,16 +1,21 @@ package org.scoula.domain.contract.controller; +import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto; import org.scoula.domain.contract.dto.*; import org.scoula.global.auth.dto.CustomUserDetails; import org.scoula.global.common.dto.ApiResponse; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import java.util.Map; + @Api(tags = "계약서 API", description = "계약서 : 정보확인 / 금액 조율 / 적법성 확인") public interface ContractController { @@ -114,4 +119,14 @@ ResponseEntity> updateSpecialContract( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody SpecialContractUpdateDTO dto); + + @ApiOperation(value = "최종 특약 확정 요청 (임대인)", notes = "임대인이 최종 특약서에 대한 확정을 요청합니다.") + ResponseEntity> requestFinalContract( + @PathVariable Long contractChatId, Authentication authentication); + + @ApiOperation(value = "최종 특약 확정 수락 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 수락합니다.") + ResponseEntity>> acceptFinalContract( + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication); } diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index ae990e94..a69f18c1 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -1,18 +1,29 @@ package org.scoula.domain.contract.controller; +import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto; +import org.scoula.domain.chat.exception.ChatErrorCode; import org.scoula.domain.chat.service.ContractChatServiceInterface; +import org.scoula.domain.chat.vo.ContractChat; import org.scoula.domain.contract.dto.*; import org.scoula.domain.contract.service.ContractFixServiceInterface; import org.scoula.domain.contract.service.ContractService; +import org.scoula.domain.user.service.UserServiceInterface; +import org.scoula.domain.user.vo.User; import org.scoula.global.auth.dto.CustomUserDetails; import org.scoula.global.common.dto.ApiResponse; +import org.scoula.global.common.exception.BusinessException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import java.util.Map; +import java.util.Optional; + @RestController @Log4j2 @RequiredArgsConstructor @@ -20,10 +31,20 @@ public class ContractControllerImpl implements ContractController { private final ContractFixServiceInterface contractFixService; - + private final UserServiceInterface userService; private final ContractService service; private final ContractChatServiceInterface contractChatService; + private Long getUserIdFromAuthentication(Authentication authentication) { + String currentUserEmail = authentication.getName(); + Optional currentUserOpt = userService.findByEmail(currentUserEmail); + + if (currentUserOpt.isEmpty()) { + throw new BusinessException(ChatErrorCode.USER_NOT_FOUND); + } + + return currentUserOpt.get().getUserId(); + } @Override @PostMapping("") public ResponseEntity> saveContractMongo( @@ -188,4 +209,43 @@ public ResponseEntity> sendStep4( return ResponseEntity.ok( ApiResponse.success(service.sendStep4(contractChatId, userDetails.getUserId()))); } + @Override + @PostMapping("/specialContract/final-request") + public ResponseEntity> requestFinalContract( + @PathVariable Long contractChatId, Authentication authentication) { + + try { + Long userId = getUserIdFromAuthentication(authentication); + contractChatService.requestFinalContract(contractChatId, userId); + return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); + + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 요청 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } + } + + @Override + @PostMapping("/specialContract/final-accept") + public ResponseEntity>> acceptFinalContract( + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication) { + + try { + Long userId = getUserIdFromAuthentication(authentication); + Map result = contractChatService.acceptFinalContract(contractChatId, userId); + return ResponseEntity.ok(ApiResponse.success(result)); + + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } + } } From 72694f8379de0e1f4bd1d24edef41f436743a249 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 02:42:17 +0900 Subject: [PATCH 54/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EB=82=B4=EC=9A=A9=20=ED=99=95=EC=A0=95=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EB=B0=8F=20=EC=88=98=EB=9D=BD=20=EB=A1=9C?= =?UTF-8?q?=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 | 67 +++++++++++++++++++ .../service/ContractChatServiceInterface.java | 6 ++ 2 files changed, 73 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 cd47521f..51d34520 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -17,6 +17,7 @@ import org.scoula.domain.chat.repository.SpecialContractMongoRepository; import org.scoula.domain.chat.vo.ChatRoom; import org.scoula.domain.chat.vo.ContractChat; +import org.scoula.domain.contract.repository.ContractMongoRepository; import org.scoula.domain.contract.service.ContractFixServiceInterface; import org.scoula.domain.precontract.service.PreContractDataService; import org.scoula.global.common.exception.BusinessException; @@ -44,6 +45,7 @@ public class ContractChatServiceImpl implements ContractChatServiceInterface { private final ContractChatMessageRepository contractChatMessageRepository; private final SimpMessagingTemplate messagingTemplate; private final ChatServiceInterface chatService; + private final ContractMongoRepository contractMongoRepository; private final AiClauseImproveService aiClauseImproveService; private final PreContractDataService preContractDataService; private final ContractFixServiceInterface contractFixService; @@ -2778,4 +2780,69 @@ private void broadcastPresence(Long contractChatId) { "buyerId", c.getBuyerId()); messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, payload); } + @Override + public void requestFinalContract(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("최종 특약서가 생성되지 않았습니다."); + } + + AiMessageBtn(contractChatId, "임대인이 최종 특약 확정을 요청하였습니다"); + + String key = "final-contract:request:" + 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); + } + + @Override + public Map acceptFinalContract(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:request:" + 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, "확정 요청 정보가 유효하지 않습니다."); + } + contractMongoRepository.clearSpecialContracts(contractChatId); + contractMongoRepository.saveSpecialContract(contractChatId); + AiMessage(contractChatId,"계약이 수락되었습니다."); + return Map.of(); + } } 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 8824ea28..e9f554d9 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java @@ -301,4 +301,10 @@ Map respondToFinalContractDeletionRequest( String getContractChatRoomUrl(Long chatRoomId); String getContractChatStatus(ContractChat.ContractStatus status); + + /** 임대인이 최종 특약 확정 요청 */ + void requestFinalContract(Long contractChatId, Long ownerId); + + /** 임차인이 최종 특약 확정 수락 */ + Map acceptFinalContract(Long contractChatId, Long buyerId); } From 76db63e3b6053e72b7c6257f914268ef7647fc8e Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 02:57:50 +0900 Subject: [PATCH 55/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=EC=A1=B0=EC=82=AC=20url=20HomeId=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/scoula/domain/chat/service/ChatServiceImpl.java | 2 +- .../precontract/service/TenantPreContractServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 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 480f9869..1aaa73b4 100644 --- a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java @@ -839,7 +839,7 @@ public Long acceptContractRequest(Long chatRoomId, Long userId) { .build(); handleChatMessage(acceptMessage); - String contractChatUrl = URL + PRECONTRACTURL + (contractChatRoomId.toString()) + BUYERURL; + String contractChatUrl = URL + PRECONTRACTURL + (contractChatRoomId.toString()) + BUYERURL+"&homeId="+(originalChatRoom.getHomeId()); ChatMessageRequestDto linkMessage = ChatMessageRequestDto.builder() diff --git a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java index a425eb76..0e2e07ec 100644 --- a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java @@ -367,7 +367,7 @@ public Void saveMongoDB(Long contractChatId, Long userId) { contractChat.getOwnerId(), contractChat.getBuyerId(), contractChat.getHomeId()); - String contractChatUrls = URL + precontractUrl + (contractChatId.toString()) + ownerUrl; + String contractChatUrls = URL + precontractUrl + (contractChatId.toString()) + ownerUrl+"&homeId="+(contractChat.getHomeId().toString()); ChatMessageRequestDto linkMessages = ChatMessageRequestDto.builder() .chatRoomId(chatRoom.getChatRoomId()) From eed4753a9fa53748df45f85753c8803daaeb0906 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 03:23:02 +0900 Subject: [PATCH 56/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=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 --- .../chat/service/ContractChatServiceImpl.java | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 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 51d34520..b6ce40f4 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -17,6 +17,7 @@ import org.scoula.domain.chat.repository.SpecialContractMongoRepository; import org.scoula.domain.chat.vo.ChatRoom; import org.scoula.domain.chat.vo.ContractChat; +import org.scoula.domain.contract.dto.LegalityDTO; import org.scoula.domain.contract.repository.ContractMongoRepository; import org.scoula.domain.contract.service.ContractFixServiceInterface; import org.scoula.domain.precontract.service.PreContractDataService; @@ -2484,14 +2485,12 @@ public Map acceptFinalContractConfirmation(Long contractChatId, Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); log.info("적법성 검사 응답: {}", legalityResponse); - if (legalityResponse instanceof Map) { - Map responseMap = (Map) legalityResponse; - - Object violationsObj = responseMap.get("violations"); - log.info("violations 객체: {}", violationsObj); + if (legalityResponse instanceof LegalityDTO) { + LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; + log.info("LegalityDTO로 응답 파싱 성공"); - if (violationsObj instanceof List) { - List> violations = (List>) violationsObj; + if (legalityDTO.getData() != null && legalityDTO.getData().getViolations() != null) { + List violations = legalityDTO.getData().getViolations(); log.info("violations 크기: {}", violations.size()); if (!violations.isEmpty()) { @@ -2499,42 +2498,34 @@ public Map acceptFinalContractConfirmation(Long contractChatId, AiMessage(contractChatId, "적법성 검사 결과, 일부 문제점이 발견되었습니다:"); for (int i = 0; i < violations.size(); i++) { - Map violation = violations.get(i); + LegalityDTO.Violation violation = violations.get(i); log.info("위반 사항 {}: {}", i + 1, violation); - String violationType = (String) violation.get("violation_type"); - String lawName = (String) violation.get("law_name"); - String violationContent = (String) violation.get("violation_content"); - String explanation = (String) violation.get("explanation"); - String improvementExample = (String) violation.get("improvement_example"); - String legalBasis = (String) violation.get("legal_basis"); - String originalClause = (String) violation.get("original_clause"); - StringBuilder violationMessage = new StringBuilder(); violationMessage.append(String.format("문제점 %d\n", i + 1)); violationMessage.append(String.format("위반 유형: %s\n", - violationType != null ? violationType : "정보 없음")); + violation.getViolationType() != null ? violation.getViolationType() : "정보 없음")); violationMessage.append(String.format("관련 법령: %s\n", - lawName != null ? lawName : "정보 없음")); + violation.getLawName() != null ? violation.getLawName() : "정보 없음")); violationMessage.append(String.format("위반 내용: %s\n", - violationContent != null ? violationContent : "정보 없음")); + violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); violationMessage.append(String.format("설명: %s\n", - explanation != null ? explanation : "정보 없음")); + violation.getExplanation() != null ? violation.getExplanation() : "정보 없음")); - if (originalClause != null && !originalClause.trim().isEmpty()) { - violationMessage.append(String.format("문제가 된 조항: %s\n", originalClause)); + if (violation.getOriginalClause() != null && !violation.getOriginalClause().trim().isEmpty()) { + violationMessage.append(String.format("문제가 된 조항: %s\n", violation.getOriginalClause())); } - if (improvementExample != null && !improvementExample.trim().isEmpty()) { - violationMessage.append(String.format("개선 방안: %s\n", improvementExample)); + if (violation.getImprovementExample() != null && !violation.getImprovementExample().trim().isEmpty()) { + violationMessage.append(String.format("개선 방안: %s\n", violation.getImprovementExample())); } - if (legalBasis != null && !legalBasis.trim().isEmpty()) { - violationMessage.append(String.format("법적 근거: %s", legalBasis)); + if (violation.getLegalBasis() != null && !violation.getLegalBasis().trim().isEmpty()) { + violationMessage.append(String.format("법적 근거: %s", violation.getLegalBasis())); } log.info("전송할 메시지: {}", violationMessage.toString()); - AiMessageLegal(contractChatId, violationMessage.toString()); + AiMessage(contractChatId, violationMessage.toString()); try { Thread.sleep(1000); @@ -2546,21 +2537,36 @@ public Map acceptFinalContractConfirmation(Long contractChatId, AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); } else { log.info("위반 사항 없음 - 빈 배열"); - AiMessage(contractChatId, "적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); } } else { - log.warn("violations가 List 타입이 아님: {}", violationsObj != null ? violationsObj.getClass() : "null"); - AiMessage(contractChatId, "적법성 검사 응답 형식이 올바르지 않습니다."); + log.warn("LegalityDTO의 data 또는 violations가 null"); + AiMessage(contractChatId, "❌ 적법성 검사 데이터를 처리할 수 없습니다."); + } + } else if (legalityResponse instanceof Map) { + // 기존 Map 처리 로직도 유지 (혹시 다른 형태로 올 수도 있으니) + Map responseMap = (Map) legalityResponse; + Object violationsObj = responseMap.get("violations"); + + if (violationsObj instanceof List) { + List> violations = (List>) violationsObj; + + if (!violations.isEmpty()) { + AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + + // 기존 Map 처리 로직... + } else { + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + } } } else { - log.warn("응답이 Map 타입이 아님: {}", legalityResponse != null ? legalityResponse.getClass() : "null"); - AiMessage(contractChatId, "적법성 검사 응답 형식이 올바르지 않습니다."); + log.warn("응답 타입을 인식할 수 없음: {}", legalityResponse != null ? legalityResponse.getClass() : "null"); + AiMessage(contractChatId, "❌ 적법성 검사 응답 형식을 인식할 수 없습니다."); } } catch (Exception e) { log.error("적법성 검사 결과 처리 중 오류 발생", e); - AiMessage(contractChatId, "적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); + AiMessage(contractChatId, "❌ 적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); } - return Map.of( "message", "최종 특약서가 확정되었습니다.", From f7fb5079cf5624f938cf7ec03aa1365897c14922 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 03:23:13 +0900 Subject: [PATCH 57/87] =?UTF-8?q?=E2=9C=A8=20feat:=20Tread=20=ED=83=80?= =?UTF-8?q?=EC=9E=84=20=EC=8A=AC=EB=A6=BD=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/scoula/domain/contract/service/ContractServiceImpl.java | 1 + 1 file changed, 1 insertion(+) 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 5cf9e6c4..36455796 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -204,6 +204,7 @@ public Boolean nextStep(Long contractChatId, Long userId, NextStepDTO dto) { // 스텝 변경 contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); + Thread.sleep(2000); // 특약 초안 메시지 contractChatService.AiMessageBtn(contractChatId, "특약 초안이 생성되었습니다. 각 조항을 검토하고 수락 / 거절을 선택하세요."); From 8cc1078602ff9b94dbb28a76b14cac97bfdfae83 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 03:41:29 +0900 Subject: [PATCH 58/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=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 --- .../chat/service/ContractChatServiceImpl.java | 7 +- .../domain/contract/dto/LegalityDTO.java | 125 +++++++----------- 2 files changed, 51 insertions(+), 81 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 b6ce40f4..e0a7268d 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2472,7 +2472,6 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; AiMessage(contractChatId, confirmationMessage); - // [적법성 검사] 계약서 1 몽고DB에 특약 저장 contractFixService.saveSpecialContract(contractChatId, buyerId); @@ -2489,8 +2488,8 @@ public Map acceptFinalContractConfirmation(Long contractChatId, LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; log.info("LegalityDTO로 응답 파싱 성공"); - if (legalityDTO.getData() != null && legalityDTO.getData().getViolations() != null) { - List violations = legalityDTO.getData().getViolations(); + if (legalityDTO.getViolations() != null) { // getData() 제거 + List violations = legalityDTO.getViolations(); log.info("violations 크기: {}", violations.size()); if (!violations.isEmpty()) { @@ -2540,7 +2539,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); } } else { - log.warn("LegalityDTO의 data 또는 violations가 null"); + log.warn("LegalityDTO의 violations가 null"); AiMessage(contractChatId, "❌ 적법성 검사 데이터를 처리할 수 없습니다."); } } else if (legalityResponse instanceof Map) { diff --git a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java index 56c3f489..e91151c0 100644 --- a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java @@ -1,92 +1,63 @@ package org.scoula.domain.contract.dto; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - -import io.swagger.annotations.ApiModel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -@ApiModel(description = "AI에서 가져온 적법성 검사") +import java.util.List; + @Data @JsonInclude(JsonInclude.Include.NON_NULL) @Builder @NoArgsConstructor @AllArgsConstructor public class LegalityDTO { - // ⬇⬇ 샘플 JSON의 최상위 구조와 동일 - private Boolean success; - private String message; - private Payload data; // 기존 Data → Payload로 명칭만 변경 (상관없음) - private Object error; // null 또는 객체/문자열일 수 있어 Object 권장 - private String timestamp; // "2025-08-11T14:39:41" 같은 문자열 - - @lombok.Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Payload { - private Boolean success; - - @JsonProperty("contract_chat_id") - private Long contractChatId; - - @JsonProperty("validation_status") - private String validationStatus; - - @JsonProperty("total_violations") - private Integer totalViolations; - - @JsonProperty("violation_summary") - private ViolationSummary violationSummary; // 샘플엔 없지만 올 수 있으니 optional - - private List violations; - - @JsonProperty("validated_at") - private String validatedAt; - - private String recommendation; // optional - } - - @lombok.Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class ViolationSummary { - @JsonProperty("illegal_count") - private Integer illegalCount; - - @JsonProperty("caution_count") - private Integer cautionCount; - } - - @lombok.Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Violation { - @JsonProperty("violation_type") - private String violationType; - - @JsonProperty("law_name") - private String lawName; - - @JsonProperty("violation_content") - private String violationContent; - - private String explanation; - - @JsonProperty("legal_basis") - private String legalBasis; - - @JsonProperty("improvement_example") - private String improvementExample; - - @JsonProperty("original_clause") - private String originalClause; - } -} + private Boolean success; + private String message; + + @JsonProperty("contract_chat_id") + private Long contractChatId; + + @JsonProperty("validation_status") + private String validationStatus; + + @JsonProperty("total_violations") + private Integer totalViolations; + + private List violations; // 최상위에 바로 위치 + + @JsonProperty("validated_at") + private String validatedAt; + + private Object error; + private String timestamp; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Violation { + @JsonProperty("violation_type") + private String violationType; + + @JsonProperty("law_name") + private String lawName; + + @JsonProperty("violation_content") + private String violationContent; + + private String explanation; + + @JsonProperty("legal_basis") + private String legalBasis; + + @JsonProperty("improvement_example") + private String improvementExample; + + @JsonProperty("original_clause") + private String originalClause; + } +} \ No newline at end of file From d28cb1522095919bc1e498603a1e3be40b272e88 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 03:43:52 +0900 Subject: [PATCH 59/87] =?UTF-8?q?=E2=9C=A8=20feat:=20contractChatId=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../precontract/service/TenantPreContractServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java index a425eb76..14af5909 100644 --- a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java @@ -85,7 +85,7 @@ public TenantInitRespDTO saveTenantInfo(Long contractChatId, Long userId) { // 1-1. identity_id 가져오기 Long identityId = tenantMapper - .selectIdentityId(userId) + .selectIdentityId(contractChatId, userId) .orElseThrow( () -> new BusinessException(PreContractErrorCode.TENANT_SELECT)); From 99fc0122e75cf444ad3b8fde33a88e0f80cbeb63 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 04:00:18 +0900 Subject: [PATCH 60/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=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 --- .../chat/service/ContractChatServiceImpl.java | 304 +++++++++--------- .../domain/contract/dto/LegalityDTO.java | 32 +- 2 files changed, 176 insertions(+), 160 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 e0a7268d..c61e3bb4 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2428,154 +2428,162 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) stringRedisTemplate.opsForValue().set(key, value); } - @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 = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; - - AiMessage(contractChatId, confirmationMessage); - // [적법성 검사] 계약서 1 몽고DB에 특약 저장 - contractFixService.saveSpecialContract(contractChatId, buyerId); - - AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); - AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); - - // api/contract/{contractChatId}/legality - try { - log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); - Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); - log.info("적법성 검사 응답: {}", legalityResponse); - - if (legalityResponse instanceof LegalityDTO) { - LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; - log.info("LegalityDTO로 응답 파싱 성공"); - - if (legalityDTO.getViolations() != null) { // getData() 제거 - List violations = legalityDTO.getViolations(); - log.info("violations 크기: {}", violations.size()); - - if (!violations.isEmpty()) { - log.info("위반 사항 발견됨: {}개", violations.size()); - AiMessage(contractChatId, "적법성 검사 결과, 일부 문제점이 발견되었습니다:"); - - for (int i = 0; i < violations.size(); i++) { - LegalityDTO.Violation violation = violations.get(i); - log.info("위반 사항 {}: {}", i + 1, violation); - - StringBuilder violationMessage = new StringBuilder(); - violationMessage.append(String.format("문제점 %d\n", i + 1)); - violationMessage.append(String.format("위반 유형: %s\n", - violation.getViolationType() != null ? violation.getViolationType() : "정보 없음")); - violationMessage.append(String.format("관련 법령: %s\n", - violation.getLawName() != null ? violation.getLawName() : "정보 없음")); - violationMessage.append(String.format("위반 내용: %s\n", - violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); - violationMessage.append(String.format("설명: %s\n", - violation.getExplanation() != null ? violation.getExplanation() : "정보 없음")); - - if (violation.getOriginalClause() != null && !violation.getOriginalClause().trim().isEmpty()) { - violationMessage.append(String.format("문제가 된 조항: %s\n", violation.getOriginalClause())); - } - - if (violation.getImprovementExample() != null && !violation.getImprovementExample().trim().isEmpty()) { - violationMessage.append(String.format("개선 방안: %s\n", violation.getImprovementExample())); - } - - if (violation.getLegalBasis() != null && !violation.getLegalBasis().trim().isEmpty()) { - violationMessage.append(String.format("법적 근거: %s", violation.getLegalBasis())); - } - - log.info("전송할 메시지: {}", violationMessage.toString()); - AiMessage(contractChatId, violationMessage.toString()); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); - } else { - log.info("위반 사항 없음 - 빈 배열"); - AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - } - } else { - log.warn("LegalityDTO의 violations가 null"); - AiMessage(contractChatId, "❌ 적법성 검사 데이터를 처리할 수 없습니다."); - } - } else if (legalityResponse instanceof Map) { - // 기존 Map 처리 로직도 유지 (혹시 다른 형태로 올 수도 있으니) - Map responseMap = (Map) legalityResponse; - Object violationsObj = responseMap.get("violations"); - - if (violationsObj instanceof List) { - List> violations = (List>) violationsObj; - - if (!violations.isEmpty()) { - AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); - - // 기존 Map 처리 로직... - } else { - AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - } - } - } else { - log.warn("응답 타입을 인식할 수 없음: {}", legalityResponse != null ? legalityResponse.getClass() : "null"); - AiMessage(contractChatId, "❌ 적법성 검사 응답 형식을 인식할 수 없습니다."); - } - } catch (Exception e) { - log.error("적법성 검사 결과 처리 중 오류 발생", e); - AiMessage(contractChatId, "❌ 적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); - } - return Map.of( - "message", - "최종 특약서가 확정되었습니다.", - "status", - "COMPLETED", - "finalContractId", - finalContract.getId(), - "totalFinalClauses", - finalContract.getTotalFinalClauses()); - } + @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 = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; + + AiMessage(contractChatId, confirmationMessage); + // [적법성 검사] 계약서 1 몽고DB에 특약 저장 + contractFixService.saveSpecialContract(contractChatId, buyerId); + + AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); + AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); + + // api/contract/{contractChatId}/legality + try { + log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); + Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); + log.info("적법성 검사 응답: {}", legalityResponse); + + if (legalityResponse instanceof LegalityDTO) { + LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; + log.info("LegalityDTO로 응답 파싱 성공"); + + // 디버깅용 로그 추가 (중첩 구조로 수정) + log.info("=== LegalityDTO 필드 확인 ==="); + log.info("success: {}", legalityDTO.getSuccess()); + log.info("message: {}", legalityDTO.getMessage()); + log.info("error: {}", legalityDTO.getError()); + log.info("timestamp: {}", legalityDTO.getTimestamp()); + log.info("data: {}", legalityDTO.getData()); + + if (legalityDTO.getData() != null) { + log.info("=== Payload 확인 ==="); + log.info("contractChatId: {}", legalityDTO.getData().getContractChatId()); + log.info("validationStatus: {}", legalityDTO.getData().getValidationStatus()); + log.info("totalViolations: {}", legalityDTO.getData().getTotalViolations()); + log.info("violations: {}", legalityDTO.getData().getViolations()); + log.info("validatedAt: {}", legalityDTO.getData().getValidatedAt()); + } + + // violations 처리 (중첩 구조로 접근) + if (legalityDTO.getData() != null && legalityDTO.getData().getViolations() != null && !legalityDTO.getData().getViolations().isEmpty()) { + List violations = legalityDTO.getData().getViolations(); + log.info("위반 사항 발견됨: {}개", violations.size()); + AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + + for (int i = 0; i < violations.size(); i++) { + LegalityDTO.Violation violation = violations.get(i); + log.info("위반 사항 {}: {}", i + 1, violation); + + StringBuilder violationMessage = new StringBuilder(); + violationMessage.append(String.format("📋 문제점 %d\n", i + 1)); + violationMessage.append(String.format("🚨 위반 유형: %s\n", + violation.getViolationType() != null ? violation.getViolationType() : "정보 없음")); + violationMessage.append(String.format("📖 관련 법령: %s\n", + violation.getLawName() != null ? violation.getLawName() : "정보 없음")); + violationMessage.append(String.format("⚠️ 위반 내용: %s\n", + violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); + violationMessage.append(String.format("💡 설명: %s\n", + violation.getExplanation() != null ? violation.getExplanation() : "정보 없음")); + + if (violation.getOriginalClause() != null && !violation.getOriginalClause().trim().isEmpty()) { + violationMessage.append(String.format("📝 문제가 된 조항: %s\n", violation.getOriginalClause())); + } + + if (violation.getImprovementExample() != null && !violation.getImprovementExample().trim().isEmpty()) { + violationMessage.append(String.format("✅ 개선 방안: %s\n", violation.getImprovementExample())); + } + + if (violation.getLegalBasis() != null && !violation.getLegalBasis().trim().isEmpty()) { + violationMessage.append(String.format("📚 법적 근거: %s", violation.getLegalBasis())); + } + + log.info("전송할 메시지: {}", violationMessage.toString()); + AiMessage(contractChatId, violationMessage.toString()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); + } else { + log.info("위반 사항 없음"); + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + } + } else if (legalityResponse instanceof Map) { + // 기존 Map 처리 로직 + Map responseMap = (Map) legalityResponse; + Object violationsObj = responseMap.get("violations"); + + if (violationsObj instanceof List) { + List> violations = (List>) violationsObj; + if (!violations.isEmpty()) { + AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + } else { + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + } + } + } else { + log.warn("응답 타입을 인식할 수 없음: {}", legalityResponse != null ? legalityResponse.getClass() : "null"); + AiMessage(contractChatId, "❌ 적법성 검사 응답 형식을 인식할 수 없습니다."); + } + } catch (Exception e) { + log.error("적법성 검사 결과 처리 중 오류 발생", e); + AiMessage(contractChatId, "❌ 적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); + } + return Map.of( + "message", + "최종 특약서가 확정되었습니다.", + "status", + "COMPLETED", + "finalContractId", + finalContract.getId(), + "totalFinalClauses", + finalContract.getTotalFinalClauses()); + } @Override public void rejectFinalContractConfirmation(Long contractChatId, Long buyerId) { diff --git a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java index e91151c0..b5e2bb15 100644 --- a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java @@ -7,6 +7,7 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.ArrayList; import java.util.List; @Data @@ -17,23 +18,30 @@ public class LegalityDTO { private Boolean success; private String message; + private Payload data; // 다시 Payload 구조로 + private Object error; + private String timestamp; - @JsonProperty("contract_chat_id") - private Long contractChatId; - - @JsonProperty("validation_status") - private String validationStatus; + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Payload { + @JsonProperty("contract_chat_id") + private Long contractChatId; - @JsonProperty("total_violations") - private Integer totalViolations; + @JsonProperty("validation_status") + private String validationStatus; - private List violations; // 최상위에 바로 위치 + @JsonProperty("total_violations") + private Integer totalViolations; - @JsonProperty("validated_at") - private String validatedAt; + @Builder.Default + private List violations = new ArrayList<>(); // 기본값 설정 - private Object error; - private String timestamp; + @JsonProperty("validated_at") + private String validatedAt; + } @Data @Builder From 423bac7a4c3461a60e3e3964266c4f7707925e44 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 04:06:21 +0900 Subject: [PATCH 61/87] =?UTF-8?q?=E2=9C=A8=20feat:=20LegalityDTO=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 --- src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java index b5e2bb15..d3347876 100644 --- a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java @@ -11,7 +11,6 @@ import java.util.List; @Data -@JsonInclude(JsonInclude.Include.NON_NULL) @Builder @NoArgsConstructor @AllArgsConstructor From c42c72805371e104409342cbee4ccd4be2d05943 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 04:31:00 +0900 Subject: [PATCH 62/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=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/service/ChatServiceImpl.java | 8 +- .../chat/service/ContractChatServiceImpl.java | 378 ++++++++++-------- .../controller/ContractController.java | 9 +- .../controller/ContractControllerImpl.java | 68 ++-- .../domain/contract/dto/LegalityDTO.java | 113 +++--- .../repository/ContractMongoRepository.java | 27 +- 6 files changed, 320 insertions(+), 283 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 1aaa73b4..7cb96e0c 100644 --- a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java @@ -839,7 +839,13 @@ public Long acceptContractRequest(Long chatRoomId, Long userId) { .build(); handleChatMessage(acceptMessage); - String contractChatUrl = URL + PRECONTRACTURL + (contractChatRoomId.toString()) + BUYERURL+"&homeId="+(originalChatRoom.getHomeId()); + String contractChatUrl = + URL + + PRECONTRACTURL + + (contractChatRoomId.toString()) + + BUYERURL + + "&homeId=" + + (originalChatRoom.getHomeId()); 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 c61e3bb4..e508d029 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -200,23 +200,22 @@ public void AiMessageBtn(Long contractChatId, String content) { messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, aiMessage); } - public void AiMessageLegal(Long contractChatId, String content) { - final Long ai = 9996L; + public void AiMessageLegal(Long contractChatId, String content) { + final Long ai = 9996L; - 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); - } + 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); + } /** {@inheritDoc} */ @Override @@ -2428,162 +2427,192 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) stringRedisTemplate.opsForValue().set(key, value); } - @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 = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; - - AiMessage(contractChatId, confirmationMessage); - // [적법성 검사] 계약서 1 몽고DB에 특약 저장 - contractFixService.saveSpecialContract(contractChatId, buyerId); - - AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); - AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); - - // api/contract/{contractChatId}/legality - try { - log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); - Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); - log.info("적법성 검사 응답: {}", legalityResponse); - - if (legalityResponse instanceof LegalityDTO) { - LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; - log.info("LegalityDTO로 응답 파싱 성공"); - - // 디버깅용 로그 추가 (중첩 구조로 수정) - log.info("=== LegalityDTO 필드 확인 ==="); - log.info("success: {}", legalityDTO.getSuccess()); - log.info("message: {}", legalityDTO.getMessage()); - log.info("error: {}", legalityDTO.getError()); - log.info("timestamp: {}", legalityDTO.getTimestamp()); - log.info("data: {}", legalityDTO.getData()); - - if (legalityDTO.getData() != null) { - log.info("=== Payload 확인 ==="); - log.info("contractChatId: {}", legalityDTO.getData().getContractChatId()); - log.info("validationStatus: {}", legalityDTO.getData().getValidationStatus()); - log.info("totalViolations: {}", legalityDTO.getData().getTotalViolations()); - log.info("violations: {}", legalityDTO.getData().getViolations()); - log.info("validatedAt: {}", legalityDTO.getData().getValidatedAt()); - } - - // violations 처리 (중첩 구조로 접근) - if (legalityDTO.getData() != null && legalityDTO.getData().getViolations() != null && !legalityDTO.getData().getViolations().isEmpty()) { - List violations = legalityDTO.getData().getViolations(); - log.info("위반 사항 발견됨: {}개", violations.size()); - AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); - - for (int i = 0; i < violations.size(); i++) { - LegalityDTO.Violation violation = violations.get(i); - log.info("위반 사항 {}: {}", i + 1, violation); - - StringBuilder violationMessage = new StringBuilder(); - violationMessage.append(String.format("📋 문제점 %d\n", i + 1)); - violationMessage.append(String.format("🚨 위반 유형: %s\n", - violation.getViolationType() != null ? violation.getViolationType() : "정보 없음")); - violationMessage.append(String.format("📖 관련 법령: %s\n", - violation.getLawName() != null ? violation.getLawName() : "정보 없음")); - violationMessage.append(String.format("⚠️ 위반 내용: %s\n", - violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); - violationMessage.append(String.format("💡 설명: %s\n", - violation.getExplanation() != null ? violation.getExplanation() : "정보 없음")); - - if (violation.getOriginalClause() != null && !violation.getOriginalClause().trim().isEmpty()) { - violationMessage.append(String.format("📝 문제가 된 조항: %s\n", violation.getOriginalClause())); - } - - if (violation.getImprovementExample() != null && !violation.getImprovementExample().trim().isEmpty()) { - violationMessage.append(String.format("✅ 개선 방안: %s\n", violation.getImprovementExample())); - } - - if (violation.getLegalBasis() != null && !violation.getLegalBasis().trim().isEmpty()) { - violationMessage.append(String.format("📚 법적 근거: %s", violation.getLegalBasis())); - } - - log.info("전송할 메시지: {}", violationMessage.toString()); - AiMessage(contractChatId, violationMessage.toString()); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); - } else { - log.info("위반 사항 없음"); - AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - } - } else if (legalityResponse instanceof Map) { - // 기존 Map 처리 로직 - Map responseMap = (Map) legalityResponse; - Object violationsObj = responseMap.get("violations"); - - if (violationsObj instanceof List) { - List> violations = (List>) violationsObj; - if (!violations.isEmpty()) { - AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); - } else { - AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - } - } - } else { - log.warn("응답 타입을 인식할 수 없음: {}", legalityResponse != null ? legalityResponse.getClass() : "null"); - AiMessage(contractChatId, "❌ 적법성 검사 응답 형식을 인식할 수 없습니다."); - } - } catch (Exception e) { - log.error("적법성 검사 결과 처리 중 오류 발생", e); - AiMessage(contractChatId, "❌ 적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); - } - return Map.of( - "message", - "최종 특약서가 확정되었습니다.", - "status", - "COMPLETED", - "finalContractId", - finalContract.getId(), - "totalFinalClauses", - finalContract.getTotalFinalClauses()); - } + @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 = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; + + AiMessage(contractChatId, confirmationMessage); + // [적법성 검사] 계약서 1 몽고DB에 특약 저장 + contractFixService.saveSpecialContract(contractChatId, buyerId); + + AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); + AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); + + // api/contract/{contractChatId}/legality + try { + log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); + Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); + log.info("적법성 검사 응답: {}", legalityResponse); + + if (legalityResponse instanceof LegalityDTO) { + LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; + log.info("LegalityDTO로 응답 파싱 성공"); + + // 디버깅용 로그 추가 (중첩 구조로 수정) + log.info("=== LegalityDTO 필드 확인 ==="); + log.info("success: {}", legalityDTO.getSuccess()); + log.info("message: {}", legalityDTO.getMessage()); + log.info("error: {}", legalityDTO.getError()); + log.info("timestamp: {}", legalityDTO.getTimestamp()); + log.info("data: {}", legalityDTO.getData()); + + if (legalityDTO.getData() != null) { + log.info("=== Payload 확인 ==="); + log.info("contractChatId: {}", legalityDTO.getData().getContractChatId()); + log.info("validationStatus: {}", legalityDTO.getData().getValidationStatus()); + log.info("totalViolations: {}", legalityDTO.getData().getTotalViolations()); + log.info("violations: {}", legalityDTO.getData().getViolations()); + log.info("validatedAt: {}", legalityDTO.getData().getValidatedAt()); + } + + // violations 처리 (중첩 구조로 접근) + if (legalityDTO.getData() != null + && legalityDTO.getData().getViolations() != null + && !legalityDTO.getData().getViolations().isEmpty()) { + List violations = legalityDTO.getData().getViolations(); + log.info("위반 사항 발견됨: {}개", violations.size()); + AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + + for (int i = 0; i < violations.size(); i++) { + LegalityDTO.Violation violation = violations.get(i); + log.info("위반 사항 {}: {}", i + 1, violation); + + StringBuilder violationMessage = new StringBuilder(); + violationMessage.append(String.format("📋 문제점 %d\n", i + 1)); + violationMessage.append( + String.format( + "🚨 위반 유형: %s\n", + violation.getViolationType() != null + ? violation.getViolationType() + : "정보 없음")); + violationMessage.append( + String.format( + "📖 관련 법령: %s\n", + violation.getLawName() != null + ? violation.getLawName() + : "정보 없음")); + violationMessage.append( + String.format( + "⚠️ 위반 내용: %s\n", + violation.getViolationContent() != null + ? violation.getViolationContent() + : "정보 없음")); + violationMessage.append( + String.format( + "💡 설명: %s\n", + violation.getExplanation() != null + ? violation.getExplanation() + : "정보 없음")); + + if (violation.getOriginalClause() != null + && !violation.getOriginalClause().trim().isEmpty()) { + violationMessage.append( + String.format( + "📝 문제가 된 조항: %s\n", violation.getOriginalClause())); + } + + if (violation.getImprovementExample() != null + && !violation.getImprovementExample().trim().isEmpty()) { + violationMessage.append( + String.format( + "✅ 개선 방안: %s\n", violation.getImprovementExample())); + } + + if (violation.getLegalBasis() != null + && !violation.getLegalBasis().trim().isEmpty()) { + violationMessage.append( + String.format("📚 법적 근거: %s", violation.getLegalBasis())); + } + + log.info("전송할 메시지: {}", violationMessage.toString()); + AiMessageLegal(contractChatId, violationMessage.toString()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + AiMessage(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); + } else { + log.info("위반 사항 없음"); + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + AiMessage(contractChatId,"최종 계약서 서명하러 갈꼐요!"); + } + } else if (legalityResponse instanceof Map) { + // 기존 Map 처리 로직 + Map responseMap = (Map) legalityResponse; + Object violationsObj = responseMap.get("violations"); + + if (violationsObj instanceof List) { + List> violations = + (List>) violationsObj; + if (!violations.isEmpty()) { + AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + } else { + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + } + } + } else { + log.warn( + "응답 타입을 인식할 수 없음: {}", + legalityResponse != null ? legalityResponse.getClass() : "null"); + AiMessage(contractChatId, "❌ 적법성 검사 응답 형식을 인식할 수 없습니다."); + } + } catch (Exception e) { + log.error("적법성 검사 결과 처리 중 오류 발생", e); + AiMessage(contractChatId, "❌ 적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); + } + return Map.of( + "message", + "최종 특약서가 확정되었습니다.", + "status", + "COMPLETED", + "finalContractId", + finalContract.getId(), + "totalFinalClauses", + finalContract.getTotalFinalClauses()); + } @Override public void rejectFinalContractConfirmation(Long contractChatId, Long buyerId) { @@ -2793,8 +2822,9 @@ private void broadcastPresence(Long contractChatId) { "buyerId", c.getBuyerId()); messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, payload); } + @Override - public void requestFinalContract(Long contractChatId, Long ownerId){ + public void requestFinalContract(Long contractChatId, Long ownerId) { ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); if (contractChat == null) { throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); @@ -2824,7 +2854,7 @@ public void requestFinalContract(Long contractChatId, Long ownerId){ } @Override - public Map acceptFinalContract(Long contractChatId, Long buyerId){ + public Map acceptFinalContract(Long contractChatId, Long buyerId) { if (!isUserInContractChat(contractChatId, buyerId)) { throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); } @@ -2855,7 +2885,7 @@ public Map acceptFinalContract(Long contractChatId, Long buyerId } contractMongoRepository.clearSpecialContracts(contractChatId); contractMongoRepository.saveSpecialContract(contractChatId); - AiMessage(contractChatId,"계약이 수락되었습니다."); + AiMessage(contractChatId, "최종 계약서 서명하러 갈꼐요!"); return Map.of(); } } diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractController.java b/src/main/java/org/scoula/domain/contract/controller/ContractController.java index 05def41c..42a91c76 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractController.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractController.java @@ -1,5 +1,7 @@ package org.scoula.domain.contract.controller; +import java.util.Map; + import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto; import org.scoula.domain.contract.dto.*; import org.scoula.global.auth.dto.CustomUserDetails; @@ -8,14 +10,11 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import java.util.Map; - @Api(tags = "계약서 API", description = "계약서 : 정보확인 / 금액 조율 / 적법성 확인") public interface ContractController { @@ -120,11 +119,11 @@ ResponseEntity> updateSpecialContract( @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody SpecialContractUpdateDTO dto); - @ApiOperation(value = "최종 특약 확정 요청 (임대인)", notes = "임대인이 최종 특약서에 대한 확정을 요청합니다.") + @ApiOperation(value = "최종 계약서 확정 요청 (임대인)", notes = "임대인이 최종 특약서에 대한 확정을 요청합니다.") ResponseEntity> requestFinalContract( @PathVariable Long contractChatId, Authentication authentication); - @ApiOperation(value = "최종 특약 확정 수락 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 수락합니다.") + @ApiOperation(value = "최종 계약서 확정 수락 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 수락합니다.") ResponseEntity>> acceptFinalContract( @PathVariable Long contractChatId, @RequestBody FinalContractDeletionResponseDto responseDto, diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index a69f18c1..456f22bb 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -1,9 +1,11 @@ package org.scoula.domain.contract.controller; +import java.util.Map; +import java.util.Optional; + import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto; import org.scoula.domain.chat.exception.ChatErrorCode; import org.scoula.domain.chat.service.ContractChatServiceInterface; -import org.scoula.domain.chat.vo.ContractChat; import org.scoula.domain.contract.dto.*; import org.scoula.domain.contract.service.ContractFixServiceInterface; import org.scoula.domain.contract.service.ContractService; @@ -21,9 +23,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import java.util.Map; -import java.util.Optional; - @RestController @Log4j2 @RequiredArgsConstructor @@ -36,15 +35,16 @@ public class ContractControllerImpl implements ContractController { private final ContractChatServiceInterface contractChatService; private Long getUserIdFromAuthentication(Authentication authentication) { - String currentUserEmail = authentication.getName(); - Optional currentUserOpt = userService.findByEmail(currentUserEmail); + String currentUserEmail = authentication.getName(); + Optional currentUserOpt = userService.findByEmail(currentUserEmail); - if (currentUserOpt.isEmpty()) { - throw new BusinessException(ChatErrorCode.USER_NOT_FOUND); - } + if (currentUserOpt.isEmpty()) { + throw new BusinessException(ChatErrorCode.USER_NOT_FOUND); + } - return currentUserOpt.get().getUserId(); + return currentUserOpt.get().getUserId(); } + @Override @PostMapping("") public ResponseEntity> saveContractMongo( @@ -209,23 +209,24 @@ public ResponseEntity> sendStep4( return ResponseEntity.ok( ApiResponse.success(service.sendStep4(contractChatId, userDetails.getUserId()))); } + @Override @PostMapping("/specialContract/final-request") public ResponseEntity> requestFinalContract( @PathVariable Long contractChatId, Authentication authentication) { - try { - Long userId = getUserIdFromAuthentication(authentication); - contractChatService.requestFinalContract(contractChatId, userId); - return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); + try { + Long userId = getUserIdFromAuthentication(authentication); + contractChatService.requestFinalContract(contractChatId, userId); + return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); - } catch (BusinessException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("최종 특약서 확정 요청 처리 중 오류 발생", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("서버 오류가 발생했습니다.")); - } + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 요청 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } } @Override @@ -235,17 +236,18 @@ public ResponseEntity>> acceptFinalContract( @RequestBody FinalContractDeletionResponseDto responseDto, Authentication authentication) { - try { - Long userId = getUserIdFromAuthentication(authentication); - Map result = contractChatService.acceptFinalContract(contractChatId, userId); - return ResponseEntity.ok(ApiResponse.success(result)); - - } catch (BusinessException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("서버 오류가 발생했습니다.")); - } + try { + Long userId = getUserIdFromAuthentication(authentication); + Map result = + contractChatService.acceptFinalContract(contractChatId, userId); + return ResponseEntity.ok(ApiResponse.success(result)); + + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } } } diff --git a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java index d3347876..e4abd69f 100644 --- a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java @@ -1,70 +1,69 @@ package org.scoula.domain.contract.dto; -import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.List; - @Data @Builder @NoArgsConstructor @AllArgsConstructor public class LegalityDTO { - private Boolean success; - private String message; - private Payload data; // 다시 Payload 구조로 - private Object error; - private String timestamp; - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Payload { - @JsonProperty("contract_chat_id") - private Long contractChatId; - - @JsonProperty("validation_status") - private String validationStatus; - - @JsonProperty("total_violations") - private Integer totalViolations; - - @Builder.Default - private List violations = new ArrayList<>(); // 기본값 설정 - - @JsonProperty("validated_at") - private String validatedAt; - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Violation { - @JsonProperty("violation_type") - private String violationType; - - @JsonProperty("law_name") - private String lawName; - - @JsonProperty("violation_content") - private String violationContent; - - private String explanation; - - @JsonProperty("legal_basis") - private String legalBasis; - - @JsonProperty("improvement_example") - private String improvementExample; - - @JsonProperty("original_clause") - private String originalClause; - } -} \ No newline at end of file + private Boolean success; + private String message; + private Payload data; // 다시 Payload 구조로 + private Object error; + private String timestamp; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Payload { + @JsonProperty("contract_chat_id") + private Long contractChatId; + + @JsonProperty("validation_status") + private String validationStatus; + + @JsonProperty("total_violations") + private Integer totalViolations; + + @Builder.Default private List violations = new ArrayList<>(); // 기본값 설정 + + @JsonProperty("validated_at") + private String validatedAt; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Violation { + @JsonProperty("violation_type") + private String violationType; + + @JsonProperty("law_name") + private String lawName; + + @JsonProperty("violation_content") + private String violationContent; + + private String explanation; + + @JsonProperty("legal_basis") + private String legalBasis; + + @JsonProperty("improvement_example") + private String improvementExample; + + @JsonProperty("original_clause") + private String originalClause; + } +} diff --git a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java index e502194e..740ef0e1 100644 --- a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -102,23 +102,24 @@ public void saveSpecialContract(Long contractChatId) { // Step 4: 저장 mongoTemplate.save(contractDoc); } - public void clearSpecialContracts(Long contractChatId) { - Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); - ContractMongoDocument contractDoc = - mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); - if (contractDoc == null) { - throw new BusinessException(ContractException.CONTRACT_GET, "계약서를 찾을 수 없습니다."); - } + public void clearSpecialContracts(Long contractChatId) { + Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); + ContractMongoDocument contractDoc = + mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); + + if (contractDoc == null) { + throw new BusinessException(ContractException.CONTRACT_GET, "계약서를 찾을 수 없습니다."); + } - // 특약 내용을 빈 리스트로 설정 - contractDoc.setSpecialContracts(new ArrayList<>()); + // 특약 내용을 빈 리스트로 설정 + contractDoc.setSpecialContracts(new ArrayList<>()); - // 저장 - mongoTemplate.save(contractDoc); + // 저장 + mongoTemplate.save(contractDoc); - System.out.println("특약 내용 삭제 완료 - contractChatId: " + contractChatId); - } + System.out.println("특약 내용 삭제 완료 - contractChatId: " + contractChatId); + } public void updateSpecialContract(Long contractChatId, SpecialContractUpdateDTO dto) { Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); From 1fa88fd4a2dd4ea5ef0ada5ed0feb74f2a16798a Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 04:31:42 +0900 Subject: [PATCH 63/87] =?UTF-8?q?=E2=9C=A8=20feat:=20Url=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EC=97=90=20HomeId=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../precontract/service/TenantPreContractServiceImpl.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java index 0e2e07ec..deeeca94 100644 --- a/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java @@ -367,7 +367,13 @@ public Void saveMongoDB(Long contractChatId, Long userId) { contractChat.getOwnerId(), contractChat.getBuyerId(), contractChat.getHomeId()); - String contractChatUrls = URL + precontractUrl + (contractChatId.toString()) + ownerUrl+"&homeId="+(contractChat.getHomeId().toString()); + String contractChatUrls = + URL + + precontractUrl + + (contractChatId.toString()) + + ownerUrl + + "&homeId=" + + (contractChat.getHomeId().toString()); ChatMessageRequestDto linkMessages = ChatMessageRequestDto.builder() .chatRoomId(chatRoom.getChatRoomId()) From 56d3d85e3febb077ccc0f5aa883a5591c1568d3d Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 04:45:07 +0900 Subject: [PATCH 64/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A0=81=EB=B2=95?= =?UTF-8?q?=EC=84=B1=20=EC=95=88=EB=82=B4=20=EB=A9=94=EC=8B=9C=EC=A7=80=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 | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 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 e508d029..e4411923 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2517,28 +2517,32 @@ public Map acceptFinalContractConfirmation(Long contractChatId, log.info("위반 사항 {}: {}", i + 1, violation); StringBuilder violationMessage = new StringBuilder(); - violationMessage.append(String.format("📋 문제점 %d\n", i + 1)); violationMessage.append( + // 위반 유형 String.format( - "🚨 위반 유형: %s\n", + "%s\n", violation.getViolationType() != null ? violation.getViolationType() : "정보 없음")); violationMessage.append( + // 관련 법령 String.format( - "📖 관련 법령: %s\n", + "%s\n" + "\n", violation.getLawName() != null ? violation.getLawName() : "정보 없음")); + violationMessage.append( + // 위반 내용 String.format( - "⚠️ 위반 내용: %s\n", + i + ". %s\n" + "\n", violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); violationMessage.append( + // 설명 String.format( - "💡 설명: %s\n", + "%s\n" + "\n", violation.getExplanation() != null ? violation.getExplanation() : "정보 없음")); @@ -2547,21 +2551,22 @@ public Map acceptFinalContractConfirmation(Long contractChatId, && !violation.getOriginalClause().trim().isEmpty()) { violationMessage.append( String.format( - "📝 문제가 된 조항: %s\n", violation.getOriginalClause())); + "📝 문제가 된 조항\n %s\n", violation.getOriginalClause())); + } + + if (violation.getLegalBasis() != null + && !violation.getLegalBasis().trim().isEmpty()) { + violationMessage.append( + String.format("📚 법적 근거\n %s\n", violation.getLegalBasis())); } if (violation.getImprovementExample() != null && !violation.getImprovementExample().trim().isEmpty()) { violationMessage.append( String.format( - "✅ 개선 방안: %s\n", violation.getImprovementExample())); + "✅ 개선 방안\n %s\n", violation.getImprovementExample())); } - if (violation.getLegalBasis() != null - && !violation.getLegalBasis().trim().isEmpty()) { - violationMessage.append( - String.format("📚 법적 근거: %s", violation.getLegalBasis())); - } log.info("전송할 메시지: {}", violationMessage.toString()); AiMessageLegal(contractChatId, violationMessage.toString()); From d910c7767962ce07c33704237b768613d8ae09ea Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 04:56:04 +0900 Subject: [PATCH 65/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EC=98=A4=EB=8D=94=20=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/contract/repository/ContractMongoRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java index 740ef0e1..f75ed014 100644 --- a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -91,7 +91,7 @@ public void saveSpecialContract(Long contractChatId) { .map( fc -> ContractMongoDocument.SpecialContract.builder() - .order(fc.getOrder() + 1) + .order(fc.getOrder() ) .title(fc.getTitle()) .content(fc.getContent()) .build()) From 808286a6f53cb0b8fa2cd9c789f7eb836513c841 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 05:12:36 +0900 Subject: [PATCH 66/87] =?UTF-8?q?=E2=9C=A8=20feat:=20identityId=20?= =?UTF-8?q?=EA=B0=92=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=EC=97=90=20contractChatId=20=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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 73db2431..ebb8b431 100644 --- a/src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java +++ b/src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java @@ -24,7 +24,8 @@ public interface TenantPreContractMapper { // =============== 사기 위험도 확인 & 기본 세팅 ================== // identity_verification에서 identity_id 가져오기 - Optional selectIdentityId(@Param("userId") Long userId); + Optional selectIdentityId( + @Param("contractChatId") Long contractChatId, @Param("userId") Long userId); // risk_check에 맞는 risk_id가 있는지 확인하기 Optional selectRiskId( From 0f465a919b983ff1a74d1c2493b1bdccb1bc5bbc Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 05:35:43 +0900 Subject: [PATCH 67/87] =?UTF-8?q?=E2=9C=A8=20feat:=20code=20formatting?= 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, 1 insertion(+), 2 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 e4411923..9d0e53ac 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2567,7 +2567,6 @@ public Map acceptFinalContractConfirmation(Long contractChatId, "✅ 개선 방안\n %s\n", violation.getImprovementExample())); } - log.info("전송할 메시지: {}", violationMessage.toString()); AiMessageLegal(contractChatId, violationMessage.toString()); @@ -2582,7 +2581,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, } else { log.info("위반 사항 없음"); AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - AiMessage(contractChatId,"최종 계약서 서명하러 갈꼐요!"); + AiMessage(contractChatId, "최종 계약서 서명하러 갈꼐요!"); } } else if (legalityResponse instanceof Map) { // 기존 Map 처리 로직 From 8fda12b7e91f67a9e8e3f036de03527d137547d6 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 05:36:00 +0900 Subject: [PATCH 68/87] =?UTF-8?q?=E2=9C=A8=20feat:=20RestApi=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/contract/controller/ContractControllerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index 456f22bb..953c0b4d 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -96,7 +96,7 @@ public ResponseEntity> getDepositPrice( } @Override - @PostMapping("/price") + @PostMapping("/price/request") public ResponseEntity> saveDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails, @@ -107,7 +107,7 @@ public ResponseEntity> saveDepositPrice( } @Override - @DeleteMapping("/price") + @PostMapping("/price/reject") public ResponseEntity> deleteDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { @@ -117,7 +117,7 @@ public ResponseEntity> deleteDepositPrice( } @Override - @PatchMapping("/price") + @PatchMapping("/price/accept") public ResponseEntity> updateDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { From c928f60b206b80f2dd5cafccf1a01432927f4e86 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 05:36:14 +0900 Subject: [PATCH 69/87] =?UTF-8?q?=E2=9C=A8=20feat:=20order=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B0=98=ED=99=98=20=EC=98=A4=EB=A5=98=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/contract/repository/ContractMongoRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java index f75ed014..8b8b3681 100644 --- a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -91,7 +91,7 @@ public void saveSpecialContract(Long contractChatId) { .map( fc -> ContractMongoDocument.SpecialContract.builder() - .order(fc.getOrder() ) + .order(fc.getOrder()) .title(fc.getTitle()) .content(fc.getContent()) .build()) From 4e46d4912368ac9bbfe426bf334bea686587f4b1 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 05:36:28 +0900 Subject: [PATCH 70/87] =?UTF-8?q?=E2=9C=A8=20feat:=20rentType=20=ED=95=9C?= =?UTF-8?q?=EA=B8=80=EB=A1=9C=20=EB=B0=98=ED=99=98=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 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 36455796..3c5046a9 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -233,7 +233,12 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { long contract = ChronoUnit.YEARS.between(aiDto.getContractStartDate(), aiDto.getContractEndDate()); String rentType = tenantMapper.selectRentTypeAll(contractChatId, userId) .orElseThrow(() -> new BusinessException(ContractException.CONTRACT_GET, "전/월세 타입 조회 실패")); - +String rentTypeKr; + if(rentType.equals("JEONSE")){ + rentTypeKr = "전세"; + }else{ + rentTypeKr="월세"; + } // 시작 메세지 보내기 contractChatService.AiMessage( contractChatId, @@ -243,7 +248,7 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { 관리비는 %s입니다. """.formatted( contract, - rentType, + rentTypeKr, formatWonShort(aiDto.getDepositPrice()), formatWonShort(aiDto.getMaintenanceFee()))); From ebc609922ca8354e0eff5713c1d0d76d44e9cbd5 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 05:38:10 +0900 Subject: [PATCH 71/87] =?UTF-8?q?=E2=9C=A8=20feat:=20code=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/scoula/domain/contract/service/ContractServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 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 3c5046a9..2a60763e 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -233,7 +233,7 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { long contract = ChronoUnit.YEARS.between(aiDto.getContractStartDate(), aiDto.getContractEndDate()); String rentType = tenantMapper.selectRentTypeAll(contractChatId, userId) .orElseThrow(() -> new BusinessException(ContractException.CONTRACT_GET, "전/월세 타입 조회 실패")); -String rentTypeKr; + String rentTypeKr; if(rentType.equals("JEONSE")){ rentTypeKr = "전세"; }else{ From 60daa008cf74fb90907d7576a305508a15e9dec4 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 05:43:34 +0900 Subject: [PATCH 72/87] =?UTF-8?q?=E2=9C=A8=20feat:=20CORS=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/scoula/global/config/ServletConfig.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/org/scoula/global/config/ServletConfig.java b/src/main/java/org/scoula/global/config/ServletConfig.java index 1354df14..4b08e4fd 100644 --- a/src/main/java/org/scoula/global/config/ServletConfig.java +++ b/src/main/java/org/scoula/global/config/ServletConfig.java @@ -14,6 +14,7 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.StandardServletMultipartResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -95,4 +96,20 @@ public LocalDate convert(String source) { } }); } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins( + "http://localhost:5173", + "http://localhost:8080", + "https://itzeep.ariogi.kr", + "https://www.itzeep.ariogi.kr", + "http://itzeep.ariogi.kr", + "http://www.itzeep.ariogi.kr") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } } From 691c9fb31c15ba9d0dee0789a6453b54c12f10e8 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Mon, 18 Aug 2025 05:47:30 +0900 Subject: [PATCH 73/87] =?UTF-8?q?=E2=9C=A8=20feat:=20pull=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/service/ChatServiceImpl.java | 8 +- .../chat/service/ContractChatServiceImpl.java | 382 ++++++++++-------- .../controller/ContractController.java | 9 +- .../controller/ContractControllerImpl.java | 6 +- .../domain/contract/dto/LegalityDTO.java | 113 +++--- .../repository/ContractMongoRepository.java | 27 +- .../contract/service/ContractServiceImpl.java | 9 +- 7 files changed, 299 insertions(+), 255 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 1aaa73b4..7cb96e0c 100644 --- a/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java @@ -839,7 +839,13 @@ public Long acceptContractRequest(Long chatRoomId, Long userId) { .build(); handleChatMessage(acceptMessage); - String contractChatUrl = URL + PRECONTRACTURL + (contractChatRoomId.toString()) + BUYERURL+"&homeId="+(originalChatRoom.getHomeId()); + String contractChatUrl = + URL + + PRECONTRACTURL + + (contractChatRoomId.toString()) + + BUYERURL + + "&homeId=" + + (originalChatRoom.getHomeId()); 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 c61e3bb4..9d0e53ac 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -200,23 +200,22 @@ public void AiMessageBtn(Long contractChatId, String content) { messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, aiMessage); } - public void AiMessageLegal(Long contractChatId, String content) { - final Long ai = 9996L; + public void AiMessageLegal(Long contractChatId, String content) { + final Long ai = 9996L; - 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); - } + 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); + } /** {@inheritDoc} */ @Override @@ -2428,162 +2427,196 @@ public void requestFinalContractConfirmation(Long contractChatId, Long ownerId) stringRedisTemplate.opsForValue().set(key, value); } - @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 = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; - - AiMessage(contractChatId, confirmationMessage); - // [적법성 검사] 계약서 1 몽고DB에 특약 저장 - contractFixService.saveSpecialContract(contractChatId, buyerId); - - AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); - AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); - - // api/contract/{contractChatId}/legality - try { - log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); - Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); - log.info("적법성 검사 응답: {}", legalityResponse); - - if (legalityResponse instanceof LegalityDTO) { - LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; - log.info("LegalityDTO로 응답 파싱 성공"); - - // 디버깅용 로그 추가 (중첩 구조로 수정) - log.info("=== LegalityDTO 필드 확인 ==="); - log.info("success: {}", legalityDTO.getSuccess()); - log.info("message: {}", legalityDTO.getMessage()); - log.info("error: {}", legalityDTO.getError()); - log.info("timestamp: {}", legalityDTO.getTimestamp()); - log.info("data: {}", legalityDTO.getData()); - - if (legalityDTO.getData() != null) { - log.info("=== Payload 확인 ==="); - log.info("contractChatId: {}", legalityDTO.getData().getContractChatId()); - log.info("validationStatus: {}", legalityDTO.getData().getValidationStatus()); - log.info("totalViolations: {}", legalityDTO.getData().getTotalViolations()); - log.info("violations: {}", legalityDTO.getData().getViolations()); - log.info("validatedAt: {}", legalityDTO.getData().getValidatedAt()); - } - - // violations 처리 (중첩 구조로 접근) - if (legalityDTO.getData() != null && legalityDTO.getData().getViolations() != null && !legalityDTO.getData().getViolations().isEmpty()) { - List violations = legalityDTO.getData().getViolations(); - log.info("위반 사항 발견됨: {}개", violations.size()); - AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); - - for (int i = 0; i < violations.size(); i++) { - LegalityDTO.Violation violation = violations.get(i); - log.info("위반 사항 {}: {}", i + 1, violation); - - StringBuilder violationMessage = new StringBuilder(); - violationMessage.append(String.format("📋 문제점 %d\n", i + 1)); - violationMessage.append(String.format("🚨 위반 유형: %s\n", - violation.getViolationType() != null ? violation.getViolationType() : "정보 없음")); - violationMessage.append(String.format("📖 관련 법령: %s\n", - violation.getLawName() != null ? violation.getLawName() : "정보 없음")); - violationMessage.append(String.format("⚠️ 위반 내용: %s\n", - violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); - violationMessage.append(String.format("💡 설명: %s\n", - violation.getExplanation() != null ? violation.getExplanation() : "정보 없음")); - - if (violation.getOriginalClause() != null && !violation.getOriginalClause().trim().isEmpty()) { - violationMessage.append(String.format("📝 문제가 된 조항: %s\n", violation.getOriginalClause())); - } - - if (violation.getImprovementExample() != null && !violation.getImprovementExample().trim().isEmpty()) { - violationMessage.append(String.format("✅ 개선 방안: %s\n", violation.getImprovementExample())); - } - - if (violation.getLegalBasis() != null && !violation.getLegalBasis().trim().isEmpty()) { - violationMessage.append(String.format("📚 법적 근거: %s", violation.getLegalBasis())); - } - - log.info("전송할 메시지: {}", violationMessage.toString()); - AiMessage(contractChatId, violationMessage.toString()); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - AiMessageBtn(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); - } else { - log.info("위반 사항 없음"); - AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - } - } else if (legalityResponse instanceof Map) { - // 기존 Map 처리 로직 - Map responseMap = (Map) legalityResponse; - Object violationsObj = responseMap.get("violations"); - - if (violationsObj instanceof List) { - List> violations = (List>) violationsObj; - if (!violations.isEmpty()) { - AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); - } else { - AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - } - } - } else { - log.warn("응답 타입을 인식할 수 없음: {}", legalityResponse != null ? legalityResponse.getClass() : "null"); - AiMessage(contractChatId, "❌ 적법성 검사 응답 형식을 인식할 수 없습니다."); - } - } catch (Exception e) { - log.error("적법성 검사 결과 처리 중 오류 발생", e); - AiMessage(contractChatId, "❌ 적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); - } - return Map.of( - "message", - "최종 특약서가 확정되었습니다.", - "status", - "COMPLETED", - "finalContractId", - finalContract.getId(), - "totalFinalClauses", - finalContract.getTotalFinalClauses()); - } + @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 = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; + + AiMessage(contractChatId, confirmationMessage); + // [적법성 검사] 계약서 1 몽고DB에 특약 저장 + contractFixService.saveSpecialContract(contractChatId, buyerId); + + AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); + AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); + + // api/contract/{contractChatId}/legality + try { + log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); + Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); + log.info("적법성 검사 응답: {}", legalityResponse); + + if (legalityResponse instanceof LegalityDTO) { + LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; + log.info("LegalityDTO로 응답 파싱 성공"); + + // 디버깅용 로그 추가 (중첩 구조로 수정) + log.info("=== LegalityDTO 필드 확인 ==="); + log.info("success: {}", legalityDTO.getSuccess()); + log.info("message: {}", legalityDTO.getMessage()); + log.info("error: {}", legalityDTO.getError()); + log.info("timestamp: {}", legalityDTO.getTimestamp()); + log.info("data: {}", legalityDTO.getData()); + + if (legalityDTO.getData() != null) { + log.info("=== Payload 확인 ==="); + log.info("contractChatId: {}", legalityDTO.getData().getContractChatId()); + log.info("validationStatus: {}", legalityDTO.getData().getValidationStatus()); + log.info("totalViolations: {}", legalityDTO.getData().getTotalViolations()); + log.info("violations: {}", legalityDTO.getData().getViolations()); + log.info("validatedAt: {}", legalityDTO.getData().getValidatedAt()); + } + + // violations 처리 (중첩 구조로 접근) + if (legalityDTO.getData() != null + && legalityDTO.getData().getViolations() != null + && !legalityDTO.getData().getViolations().isEmpty()) { + List violations = legalityDTO.getData().getViolations(); + log.info("위반 사항 발견됨: {}개", violations.size()); + AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + + for (int i = 0; i < violations.size(); i++) { + LegalityDTO.Violation violation = violations.get(i); + log.info("위반 사항 {}: {}", i + 1, violation); + + StringBuilder violationMessage = new StringBuilder(); + violationMessage.append( + // 위반 유형 + String.format( + "%s\n", + violation.getViolationType() != null + ? violation.getViolationType() + : "정보 없음")); + violationMessage.append( + // 관련 법령 + String.format( + "%s\n" + "\n", + violation.getLawName() != null + ? violation.getLawName() + : "정보 없음")); + + violationMessage.append( + // 위반 내용 + String.format( + i + ". %s\n" + "\n", + violation.getViolationContent() != null + ? violation.getViolationContent() + : "정보 없음")); + violationMessage.append( + // 설명 + String.format( + "%s\n" + "\n", + violation.getExplanation() != null + ? violation.getExplanation() + : "정보 없음")); + + if (violation.getOriginalClause() != null + && !violation.getOriginalClause().trim().isEmpty()) { + violationMessage.append( + String.format( + "📝 문제가 된 조항\n %s\n", violation.getOriginalClause())); + } + + if (violation.getLegalBasis() != null + && !violation.getLegalBasis().trim().isEmpty()) { + violationMessage.append( + String.format("📚 법적 근거\n %s\n", violation.getLegalBasis())); + } + + if (violation.getImprovementExample() != null + && !violation.getImprovementExample().trim().isEmpty()) { + violationMessage.append( + String.format( + "✅ 개선 방안\n %s\n", violation.getImprovementExample())); + } + + log.info("전송할 메시지: {}", violationMessage.toString()); + AiMessageLegal(contractChatId, violationMessage.toString()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + AiMessage(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); + } else { + log.info("위반 사항 없음"); + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + AiMessage(contractChatId, "최종 계약서 서명하러 갈꼐요!"); + } + } else if (legalityResponse instanceof Map) { + // 기존 Map 처리 로직 + Map responseMap = (Map) legalityResponse; + Object violationsObj = responseMap.get("violations"); + + if (violationsObj instanceof List) { + List> violations = + (List>) violationsObj; + if (!violations.isEmpty()) { + AiMessage(contractChatId, "⚠️ 적법성 검사 결과, 일부 문제점이 발견되었습니다:"); + } else { + AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + } + } + } else { + log.warn( + "응답 타입을 인식할 수 없음: {}", + legalityResponse != null ? legalityResponse.getClass() : "null"); + AiMessage(contractChatId, "❌ 적법성 검사 응답 형식을 인식할 수 없습니다."); + } + } catch (Exception e) { + log.error("적법성 검사 결과 처리 중 오류 발생", e); + AiMessage(contractChatId, "❌ 적법성 검사 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); + } + return Map.of( + "message", + "최종 특약서가 확정되었습니다.", + "status", + "COMPLETED", + "finalContractId", + finalContract.getId(), + "totalFinalClauses", + finalContract.getTotalFinalClauses()); + } @Override public void rejectFinalContractConfirmation(Long contractChatId, Long buyerId) { @@ -2793,8 +2826,9 @@ private void broadcastPresence(Long contractChatId) { "buyerId", c.getBuyerId()); messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, payload); } + @Override - public void requestFinalContract(Long contractChatId, Long ownerId){ + public void requestFinalContract(Long contractChatId, Long ownerId) { ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); if (contractChat == null) { throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); @@ -2824,7 +2858,7 @@ public void requestFinalContract(Long contractChatId, Long ownerId){ } @Override - public Map acceptFinalContract(Long contractChatId, Long buyerId){ + public Map acceptFinalContract(Long contractChatId, Long buyerId) { if (!isUserInContractChat(contractChatId, buyerId)) { throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); } @@ -2855,7 +2889,7 @@ public Map acceptFinalContract(Long contractChatId, Long buyerId } contractMongoRepository.clearSpecialContracts(contractChatId); contractMongoRepository.saveSpecialContract(contractChatId); - AiMessage(contractChatId,"계약이 수락되었습니다."); + AiMessage(contractChatId, "최종 계약서 서명하러 갈꼐요!"); return Map.of(); } } diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractController.java b/src/main/java/org/scoula/domain/contract/controller/ContractController.java index 05def41c..42a91c76 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractController.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractController.java @@ -1,5 +1,7 @@ package org.scoula.domain.contract.controller; +import java.util.Map; + import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto; import org.scoula.domain.contract.dto.*; import org.scoula.global.auth.dto.CustomUserDetails; @@ -8,14 +10,11 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import java.util.Map; - @Api(tags = "계약서 API", description = "계약서 : 정보확인 / 금액 조율 / 적법성 확인") public interface ContractController { @@ -120,11 +119,11 @@ ResponseEntity> updateSpecialContract( @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody SpecialContractUpdateDTO dto); - @ApiOperation(value = "최종 특약 확정 요청 (임대인)", notes = "임대인이 최종 특약서에 대한 확정을 요청합니다.") + @ApiOperation(value = "최종 계약서 확정 요청 (임대인)", notes = "임대인이 최종 특약서에 대한 확정을 요청합니다.") ResponseEntity> requestFinalContract( @PathVariable Long contractChatId, Authentication authentication); - @ApiOperation(value = "최종 특약 확정 수락 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 수락합니다.") + @ApiOperation(value = "최종 계약서 확정 수락 (임차인)", notes = "임차인이 임대인의 최종 특약서 확정 요청을 수락합니다.") ResponseEntity>> acceptFinalContract( @PathVariable Long contractChatId, @RequestBody FinalContractDeletionResponseDto responseDto, diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index a69f18c1..a3193bd7 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -96,7 +96,7 @@ public ResponseEntity> getDepositPrice( } @Override - @PostMapping("/price") + @PostMapping("/price/request") public ResponseEntity> saveDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails, @@ -107,7 +107,7 @@ public ResponseEntity> saveDepositPrice( } @Override - @DeleteMapping("/price") + @PostMapping("/price/reject") public ResponseEntity> deleteDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { @@ -117,7 +117,7 @@ public ResponseEntity> deleteDepositPrice( } @Override - @PatchMapping("/price") + @PatchMapping("/price/accept") public ResponseEntity> updateDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { diff --git a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java index d3347876..e4abd69f 100644 --- a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java @@ -1,70 +1,69 @@ package org.scoula.domain.contract.dto; -import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.List; - @Data @Builder @NoArgsConstructor @AllArgsConstructor public class LegalityDTO { - private Boolean success; - private String message; - private Payload data; // 다시 Payload 구조로 - private Object error; - private String timestamp; - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Payload { - @JsonProperty("contract_chat_id") - private Long contractChatId; - - @JsonProperty("validation_status") - private String validationStatus; - - @JsonProperty("total_violations") - private Integer totalViolations; - - @Builder.Default - private List violations = new ArrayList<>(); // 기본값 설정 - - @JsonProperty("validated_at") - private String validatedAt; - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class Violation { - @JsonProperty("violation_type") - private String violationType; - - @JsonProperty("law_name") - private String lawName; - - @JsonProperty("violation_content") - private String violationContent; - - private String explanation; - - @JsonProperty("legal_basis") - private String legalBasis; - - @JsonProperty("improvement_example") - private String improvementExample; - - @JsonProperty("original_clause") - private String originalClause; - } -} \ No newline at end of file + private Boolean success; + private String message; + private Payload data; // 다시 Payload 구조로 + private Object error; + private String timestamp; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Payload { + @JsonProperty("contract_chat_id") + private Long contractChatId; + + @JsonProperty("validation_status") + private String validationStatus; + + @JsonProperty("total_violations") + private Integer totalViolations; + + @Builder.Default private List violations = new ArrayList<>(); // 기본값 설정 + + @JsonProperty("validated_at") + private String validatedAt; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Violation { + @JsonProperty("violation_type") + private String violationType; + + @JsonProperty("law_name") + private String lawName; + + @JsonProperty("violation_content") + private String violationContent; + + private String explanation; + + @JsonProperty("legal_basis") + private String legalBasis; + + @JsonProperty("improvement_example") + private String improvementExample; + + @JsonProperty("original_clause") + private String originalClause; + } +} diff --git a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java index 5107171f..8b8b3681 100644 --- a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -102,23 +102,24 @@ public void saveSpecialContract(Long contractChatId) { // Step 4: 저장 mongoTemplate.save(contractDoc); } - public void clearSpecialContracts(Long contractChatId) { - Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); - ContractMongoDocument contractDoc = - mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); - if (contractDoc == null) { - throw new BusinessException(ContractException.CONTRACT_GET, "계약서를 찾을 수 없습니다."); - } + public void clearSpecialContracts(Long contractChatId) { + Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); + ContractMongoDocument contractDoc = + mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); + + if (contractDoc == null) { + throw new BusinessException(ContractException.CONTRACT_GET, "계약서를 찾을 수 없습니다."); + } - // 특약 내용을 빈 리스트로 설정 - contractDoc.setSpecialContracts(new ArrayList<>()); + // 특약 내용을 빈 리스트로 설정 + contractDoc.setSpecialContracts(new ArrayList<>()); - // 저장 - mongoTemplate.save(contractDoc); + // 저장 + mongoTemplate.save(contractDoc); - System.out.println("특약 내용 삭제 완료 - contractChatId: " + contractChatId); - } + System.out.println("특약 내용 삭제 완료 - contractChatId: " + contractChatId); + } public void updateSpecialContract(Long contractChatId, SpecialContractUpdateDTO dto) { Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); 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 36455796..2a60763e 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -233,7 +233,12 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { long contract = ChronoUnit.YEARS.between(aiDto.getContractStartDate(), aiDto.getContractEndDate()); String rentType = tenantMapper.selectRentTypeAll(contractChatId, userId) .orElseThrow(() -> new BusinessException(ContractException.CONTRACT_GET, "전/월세 타입 조회 실패")); - + String rentTypeKr; + if(rentType.equals("JEONSE")){ + rentTypeKr = "전세"; + }else{ + rentTypeKr="월세"; + } // 시작 메세지 보내기 contractChatService.AiMessage( contractChatId, @@ -243,7 +248,7 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { 관리비는 %s입니다. """.formatted( contract, - rentType, + rentTypeKr, formatWonShort(aiDto.getDepositPrice()), formatWonShort(aiDto.getMaintenanceFee()))); From e1911b1c8f8db152e6041fa36f2ae0dfdd2ff6c9 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 05:56:48 +0900 Subject: [PATCH 74/87] =?UTF-8?q?=E2=9C=A8=20feat:=20code=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ContractControllerImpl.java | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index a3193bd7..953c0b4d 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -1,9 +1,11 @@ package org.scoula.domain.contract.controller; +import java.util.Map; +import java.util.Optional; + import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto; import org.scoula.domain.chat.exception.ChatErrorCode; import org.scoula.domain.chat.service.ContractChatServiceInterface; -import org.scoula.domain.chat.vo.ContractChat; import org.scoula.domain.contract.dto.*; import org.scoula.domain.contract.service.ContractFixServiceInterface; import org.scoula.domain.contract.service.ContractService; @@ -21,9 +23,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import java.util.Map; -import java.util.Optional; - @RestController @Log4j2 @RequiredArgsConstructor @@ -36,15 +35,16 @@ public class ContractControllerImpl implements ContractController { private final ContractChatServiceInterface contractChatService; private Long getUserIdFromAuthentication(Authentication authentication) { - String currentUserEmail = authentication.getName(); - Optional currentUserOpt = userService.findByEmail(currentUserEmail); + String currentUserEmail = authentication.getName(); + Optional currentUserOpt = userService.findByEmail(currentUserEmail); - if (currentUserOpt.isEmpty()) { - throw new BusinessException(ChatErrorCode.USER_NOT_FOUND); - } + if (currentUserOpt.isEmpty()) { + throw new BusinessException(ChatErrorCode.USER_NOT_FOUND); + } - return currentUserOpt.get().getUserId(); + return currentUserOpt.get().getUserId(); } + @Override @PostMapping("") public ResponseEntity> saveContractMongo( @@ -209,23 +209,24 @@ public ResponseEntity> sendStep4( return ResponseEntity.ok( ApiResponse.success(service.sendStep4(contractChatId, userDetails.getUserId()))); } + @Override @PostMapping("/specialContract/final-request") public ResponseEntity> requestFinalContract( @PathVariable Long contractChatId, Authentication authentication) { - try { - Long userId = getUserIdFromAuthentication(authentication); - contractChatService.requestFinalContract(contractChatId, userId); - return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); + try { + Long userId = getUserIdFromAuthentication(authentication); + contractChatService.requestFinalContract(contractChatId, userId); + return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); - } catch (BusinessException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("최종 특약서 확정 요청 처리 중 오류 발생", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("서버 오류가 발생했습니다.")); - } + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 요청 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } } @Override @@ -235,17 +236,18 @@ public ResponseEntity>> acceptFinalContract( @RequestBody FinalContractDeletionResponseDto responseDto, Authentication authentication) { - try { - Long userId = getUserIdFromAuthentication(authentication); - Map result = contractChatService.acceptFinalContract(contractChatId, userId); - return ResponseEntity.ok(ApiResponse.success(result)); - - } catch (BusinessException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("서버 오류가 발생했습니다.")); - } + try { + Long userId = getUserIdFromAuthentication(authentication); + Map result = + contractChatService.acceptFinalContract(contractChatId, userId); + return ResponseEntity.ok(ApiResponse.success(result)); + + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } } } From cbee6767b71435ccc2be9bd45ea242ba74d2968d Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 05:57:06 +0900 Subject: [PATCH 75/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?formatting=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/service/ContractServiceImpl.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) 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 2a60763e..47d735e0 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -244,7 +244,7 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { contractChatId, """ 이 계약은 계약기간 %d년의 %s 계약입니다. - 전세 보증금은 %s, + 보증금은 %s, 관리비는 %s입니다. """.formatted( contract, @@ -614,28 +614,32 @@ public Boolean nextSteps(Long contractChatId, Long userId, NextStepDTO dto) { private static String formatWonShort(int amount) { if (amount == 0) return "0원"; + long eok = amount / 100_000_000; // 억 long man = (amount % 100_000_000) / 10_000; // 만원 단위 StringBuilder sb = new StringBuilder(); + if (eok > 0) { sb.append(eok).append("억"); - long cheon = man / 1000; // 천만원 단위 - long remainMan = man % 1000; - if (cheon > 0) sb.append(" ").append(cheon).append("천"); - if (cheon == 0 && remainMan > 0) sb.append(" ").append(remainMan).append("만"); + if (man > 0) { + sb.append(" ").append(man).append("만"); + } sb.append("원"); } else { - if (man >= 1000) { - long cheon = man / 1000; - long remainMan = man % 1000; - sb.append(cheon).append("천"); - if (remainMan > 0) sb.append(" ").append(remainMan).append("만"); - sb.append("원"); - } else { + if (man > 0) { sb.append(man).append("만원"); + } else { + // 만원 미만인 경우 + long cheon = (amount % 10_000) / 1_000; + if (cheon > 0) { + sb.append(cheon).append("천원"); + } else { + sb.append(amount).append("원"); + } } } - return sb.toString().replaceAll("\\s+", " "); + + return sb.toString(); } } From 6fff55cbbb64d661dadf233bdfc950b9b0a02dcf Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 06:32:25 +0900 Subject: [PATCH 76/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95=20=EB=A9=94=EC=84=9C=EB=93=9C=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 --- .../contract/service/ContractServiceImpl.java | 117 +++++++++++------- 1 file changed, 70 insertions(+), 47 deletions(-) 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 47d735e0..e4ffc814 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -275,28 +275,37 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { return dto; } - /** {@inheritDoc} */ - @Override - public Void saveDepositPrice(Long contractChatId, Long userId, PaymentDTO dto) { - // Userid 검증 - validateUserId(contractChatId, userId); + /** {@inheritDoc} */ + @Override + public Void saveDepositPrice(Long contractChatId, Long userId, PaymentDTO dto) { + // Userid 검증 + validateUserId(contractChatId, userId); - // 레디스에 내용 저장하기 / value 값 넛기 - String redisKey = "contract:payment:" + contractChatId; - try { - // 3. DTO를 JSON 문자열로 변환 - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writeValueAsString(dto); + String redisKey = "contract:payment:" + contractChatId; - // 4. Redis에 저장 - stringRedisTemplate.opsForValue().set(redisKey, json); + String paymentValue = dto.getDepositPrice() + "," + dto.getMonthlyRent(); + stringRedisTemplate.opsForValue().set(redisKey, paymentValue); - } catch (JsonProcessingException e) { - throw new BusinessException(ContractException.CONTRACT_REDIS, e); - } + Long ownerId = contractMapper.getOwnerId(contractChatId); - return null; - } + String userRole = userId.equals(ownerId) ? "임대인" : "임차인"; + + String depositFormatted = formatWonShort(dto.getDepositPrice()); + String monthlyRentFormatted = formatWonShort(dto.getMonthlyRent()); + + String message; + if (dto.getMonthlyRent() > 0) { + message = String.format("%s이 보증금 %s, 월세 %s로 금액 조정을 요청했습니다.", + userRole, depositFormatted, monthlyRentFormatted); + } else { + message = String.format("%s이 전세금 %s로 금액 조정을 요청했습니다.", + userRole, depositFormatted); + } + + contractChatService.AiMessage(contractChatId, message); + + return null; + } /** {@inheritDoc} */ @Override @@ -319,51 +328,65 @@ public Void deleteDepositPrice(Long contractChatId, Long userId) { return null; } - /** {@inheritDoc} */ - @Override - public Void updateDepositPrice(Long contractChatId, Long userId) { - // Userid 검증 - validateUserId(contractChatId, userId); + /** {@inheritDoc} */ + @Override + public Void updateDepositPrice(Long contractChatId, Long userId) { + validateUserId(contractChatId, userId); - // 2. Redis에서 해당 금액 정보 가져오기 - String redisKey = "contract:payment:" + contractChatId; // value : 임대인 id -> 거절시 Delete - String json = stringRedisTemplate.opsForValue().get(redisKey); + String redisKey = "contract:payment:" + contractChatId; + String paymentValue = stringRedisTemplate.opsForValue().get(redisKey); - if (json == null) { - throw new BusinessException(ContractException.CONTRACT_REDIS, "금액 정보가 Redis에 없습니다."); - } + if (paymentValue == null) { + throw new BusinessException(ContractException.CONTRACT_REDIS, "금액 정보가 Redis에 없습니다."); + } - try { - // 3. JSON -> DTO로 변환 - ObjectMapper objectMapper = new ObjectMapper(); - PaymentDTO dto = objectMapper.readValue(json, PaymentDTO.class); + try { + String[] amounts = paymentValue.split(","); + if (amounts.length != 2) { + throw new BusinessException(ContractException.CONTRACT_REDIS, "Redis 금액 데이터 형식이 올바르지 않습니다."); + } + + int depositPrice = Integer.parseInt(amounts[0]); + int monthlyRent = Integer.parseInt(amounts[1]); + + PaymentDTO dto = PaymentDTO.builder() + .depositPrice(depositPrice) + .monthlyRent(monthlyRent) + .build(); - // 4. MongoDB에서 계약서 불러오기 - repository.updateDepositPrice(contractChatId, dto); + repository.updateDepositPrice(contractChatId, dto); - // 7. Redis 값 삭제 - stringRedisTemplate.delete(redisKey); + String depositFormatted = formatWonShort(depositPrice); + String monthlyRentFormatted = formatWonShort(monthlyRent); + + String acceptMessage; + if (monthlyRent > 0) { + acceptMessage = String.format("금액 조정이 수락되었습니다!\n보증금: %s\n월세: %s", + depositFormatted, monthlyRentFormatted); + } else { + acceptMessage = String.format("금액 조정이 수락되었습니다!\n전세금: %s", depositFormatted); + } + contractChatService.AiMessage(contractChatId, acceptMessage); + + stringRedisTemplate.delete(redisKey); - // 스텝 변경 contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); - // 다음 단계 메세지 보내기 contractChatService.AiMessage(contractChatId, step3StartMessage); Thread.sleep(2000); - // 특약 초안 메시지 contractChatService.AiMessageBtn(contractChatId, "특약 초안이 생성되었습니다. 각 조항을 검토하고 수락 / 거절을 선택하세요."); + } catch (NumberFormatException e) { + throw new BusinessException(ContractException.CONTRACT_REDIS, "Redis의 금액 데이터를 파싱할 수 없습니다.", e); + } catch (Exception e) { + throw new BusinessException(ContractException.CONTRACT_UPDATE, e); + } - } catch (Exception e) { - throw new BusinessException(ContractException.CONTRACT_UPDATE, e); - } - - return null; - } - + return null; + } /** {@inheritDoc} */ From 0303eabab613e14a5124330df2e30346cf10f6b5 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 06:33:36 +0900 Subject: [PATCH 77/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9C=84=EB=B0=98=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B0=98=ED=99=98=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 9d0e53ac..2b0d4ef4 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2535,7 +2535,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, violationMessage.append( // 위반 내용 String.format( - i + ". %s\n" + "\n", + (i+1) + ". %s\n" + "\n", violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); From efcba54032e1d95aa3a411b7fa2a0afe7ea11915 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 06:56:29 +0900 Subject: [PATCH 78/87] =?UTF-8?q?=E2=9C=A8=20feat:=20AiMessage=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 --- .../scoula/domain/chat/service/ContractChatServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 2b0d4ef4..7dc79bf9 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2535,7 +2535,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, violationMessage.append( // 위반 내용 String.format( - (i+1) + ". %s\n" + "\n", + (i + 1) + ". %s\n" + "\n", violation.getViolationContent() != null ? violation.getViolationContent() : "정보 없음")); @@ -2581,7 +2581,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, } else { log.info("위반 사항 없음"); AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); - AiMessage(contractChatId, "최종 계약서 서명하러 갈꼐요!"); + AiMessage(contractChatId, "최종 계약서 서명하러 갈께요!"); } } else if (legalityResponse instanceof Map) { // 기존 Map 처리 로직 From 29be11f40ef06d7d0f30064f53c63d45ec910abb Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 08:33:13 +0900 Subject: [PATCH 79/87] =?UTF-8?q?=E2=9C=A8=20feat:=20AiMessage=20sleep=202?= =?UTF-8?q?=EC=B4=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ContractChatServiceImpl.java | 17 ++++++++++++++++- .../contract/service/ContractServiceImpl.java | 4 +++- 2 files changed, 19 insertions(+), 2 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 7dc79bf9..84f2abb1 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2471,10 +2471,20 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String confirmationMessage = "🎉 임차인이 최종 특약서를 수락했습니다! 특약서가 확정되었습니다."; AiMessage(contractChatId, confirmationMessage); + // [적법성 검사] 계약서 1 몽고DB에 특약 저장 contractFixService.saveSpecialContract(contractChatId, buyerId); - + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } AiMessageNext(contractChatId, "다음은 마지막 4단계: '적법성 검토' 단계입니다."); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } AiMessage(contractChatId, "AI가 지금까지 작성된 계약서의 적법성을 분석중이에요!\n 잠시만 기다려주세요!"); // api/contract/{contractChatId}/legality @@ -2581,6 +2591,11 @@ public Map acceptFinalContractConfirmation(Long contractChatId, } else { log.info("위반 사항 없음"); AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } AiMessage(contractChatId, "최종 계약서 서명하러 갈께요!"); } } else if (legalityResponse instanceof Map) { 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 e4ffc814..91828df4 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -254,7 +254,7 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { // 대기 try { - Thread.sleep(1000); + Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -372,6 +372,8 @@ public Void updateDepositPrice(Long contractChatId, Long userId) { stringRedisTemplate.delete(redisKey); contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); + Thread.sleep(2000); + contractChatService.AiMessage(contractChatId, step3StartMessage); From 5012bd5a5874850b8772c8119b0bed47cf08fbb8 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 08:57:21 +0900 Subject: [PATCH 80/87] =?UTF-8?q?=E2=9C=A8=20feat:=20AiMessage=20sleep=202?= =?UTF-8?q?=EC=B4=88=20=EC=B6=94=EA=B0=80?= 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 84f2abb1..5a607896 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2581,7 +2581,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, AiMessageLegal(contractChatId, violationMessage.toString()); try { - Thread.sleep(1000); + Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } From fb0270951f280451754c2fab9430aa822e56057f Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 11:07:03 +0900 Subject: [PATCH 81/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EC=88=98=EB=9D=BD=20=EB=B0=8F=20=EA=B1=B0?= =?UTF-8?q?=EC=A0=88=20=EB=A1=9C=EC=A7=81=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 | 69 +++++++++++-------- .../service/ContractChatServiceInterface.java | 2 +- .../controller/ContractControllerImpl.java | 43 ++++++------ 3 files changed, 62 insertions(+), 52 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 5a607896..97886c51 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2587,7 +2587,7 @@ public Map acceptFinalContractConfirmation(Long contractChatId, } } - AiMessage(contractChatId, "위 문제점들을 검토하시고 필요시 수정 요청을 해주세요."); + AiMessage(contractChatId, "위 문제점들을 검토하시고 필요시 임대인께서 수정 요청을 해주세요."); } else { log.info("위반 사항 없음"); AiMessage(contractChatId, "✅ 적법성 검사 완료! 계약서에 법적 문제가 발견되지 않았습니다."); @@ -2872,39 +2872,48 @@ public void requestFinalContract(Long contractChatId, Long ownerId) { stringRedisTemplate.opsForValue().set(key, value); } - @Override - public Map acceptFinalContract(Long contractChatId, Long buyerId) { - if (!isUserInContractChat(contractChatId, buyerId)) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } + @Override + public Map acceptFinalContract(Long contractChatId, Long buyerId, Boolean isAccepted) { + if (!isUserInContractChat(contractChatId, buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } - ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); - if (contractChat == null) { - throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); - } + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } - Long ownerId = contractChat.getOwnerId(); + Long ownerId = contractChat.getOwnerId(); - if (!buyerId.equals(contractChat.getBuyerId())) { - throw new BusinessException( - ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 확정 수락을 할 수 있습니다."); - } + if (!buyerId.equals(contractChat.getBuyerId())) { + throw new BusinessException( + ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 확정 수락을 할 수 있습니다."); + } - String redisKey = "final-contract:request:" + contractChatId; - String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + String redisKey = "final-contract:request:" + contractChatId; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); - if (storedOwnerId == null) { - throw new BusinessException( - ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "확정 요청이 존재하지 않습니다."); - } + 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, "확정 요청 정보가 유효하지 않습니다."); - } - contractMongoRepository.clearSpecialContracts(contractChatId); - contractMongoRepository.saveSpecialContract(contractChatId); - AiMessage(contractChatId, "최종 계약서 서명하러 갈꼐요!"); - return Map.of(); - } + if (!storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_INVALID, "확정 요청 정보가 유효하지 않습니다."); + } + + // Redis 키 삭제 + stringRedisTemplate.delete(redisKey); + + if (isAccepted) { + contractMongoRepository.clearSpecialContracts(contractChatId); + contractMongoRepository.saveSpecialContract(contractChatId); + AiMessage(contractChatId, "임차인이 최종 계약서를 수락했습니다! 계약서 서명하러 갈께요!"); + } else { + AiMessage(contractChatId, "임차인이 최종 계약서를 거절했습니다. 추가 협상이 필요합니다."); + } + + return Map.of("accepted", isAccepted); + } } 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 e9f554d9..2b47dee3 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java @@ -306,5 +306,5 @@ Map respondToFinalContractDeletionRequest( void requestFinalContract(Long contractChatId, Long ownerId); /** 임차인이 최종 특약 확정 수락 */ - Map acceptFinalContract(Long contractChatId, Long buyerId); + Map acceptFinalContract(Long contractChatId, Long buyerId, Boolean isAccepted); } diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index 953c0b4d..5afc94cd 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -229,25 +229,26 @@ public ResponseEntity> requestFinalContract( } } - @Override - @PostMapping("/specialContract/final-accept") - public ResponseEntity>> acceptFinalContract( - @PathVariable Long contractChatId, - @RequestBody FinalContractDeletionResponseDto responseDto, - Authentication authentication) { - - try { - Long userId = getUserIdFromAuthentication(authentication); - Map result = - contractChatService.acceptFinalContract(contractChatId, userId); - return ResponseEntity.ok(ApiResponse.success(result)); - - } catch (BusinessException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("서버 오류가 발생했습니다.")); - } - } + @Override + @PostMapping("/specialContract/final-accept") + public ResponseEntity>> acceptFinalContract( + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication) { + + try { + Long userId = getUserIdFromAuthentication(authentication); + + Map result = + contractChatService.acceptFinalContract(contractChatId, userId, responseDto.isAccepted()); + return ResponseEntity.ok(ApiResponse.success(result)); + + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } + } } From d51f424ba705fc12a0486c8c82411e2bc822f601 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 11:07:41 +0900 Subject: [PATCH 82/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EC=88=98=EB=9D=BD=20=EB=B0=8F=20=EA=B1=B0?= =?UTF-8?q?=EC=A0=88=20=EB=A1=9C=EC=A7=81=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 | 1 - 1 file changed, 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 97886c51..e55b2fa2 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2903,7 +2903,6 @@ public Map acceptFinalContract(Long contractChatId, Long buyerId ChatErrorCode.CONTRACT_END_REQUEST_INVALID, "확정 요청 정보가 유효하지 않습니다."); } - // Redis 키 삭제 stringRedisTemplate.delete(redisKey); if (isAccepted) { From 7040a7f47feac05f5c602e39553f7abe9dc07e15 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 11:11:42 +0900 Subject: [PATCH 83/87] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EC=88=98=EB=9D=BD=20=EB=B0=8F=20=EA=B1=B0?= =?UTF-8?q?=EC=A0=88=20=EB=A1=9C=EC=A7=81=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 e55b2fa2..24bf4d10 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2860,7 +2860,7 @@ public void requestFinalContract(Long contractChatId, Long ownerId) { throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다."); } - AiMessageBtn(contractChatId, "임대인이 최종 특약 확정을 요청하였습니다"); + AiMessageBtn(contractChatId, "임대인이 최종 계약서 확인을 요청하였습니다"); String key = "final-contract:request:" + contractChatId; String existingValue = stringRedisTemplate.opsForValue().get(key); From 75362f3afa6cc7f3f8be39bc8262ed35b483b600 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 11:24:05 +0900 Subject: [PATCH 84/87] =?UTF-8?q?=E2=9C=A8=20feat:=20CORS=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scoula/global/config/ServletConfig.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/scoula/global/config/ServletConfig.java b/src/main/java/org/scoula/global/config/ServletConfig.java index 4b08e4fd..2c5c8adf 100644 --- a/src/main/java/org/scoula/global/config/ServletConfig.java +++ b/src/main/java/org/scoula/global/config/ServletConfig.java @@ -97,19 +97,19 @@ public LocalDate convert(String source) { }); } - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins( - "http://localhost:5173", - "http://localhost:8080", - "https://itzeep.ariogi.kr", - "https://www.itzeep.ariogi.kr", - "http://itzeep.ariogi.kr", - "http://www.itzeep.ariogi.kr") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") - .allowedHeaders("*") - .allowCredentials(true) - .maxAge(3600); - } +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// registry.addMapping("/**") +// .allowedOrigins( +// "http://localhost:5173", +// "http://localhost:8080", +// "https://itzeep.ariogi.kr", +// "https://www.itzeep.ariogi.kr", +// "http://itzeep.ariogi.kr", +// "http://www.itzeep.ariogi.kr") +// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") +// .allowedHeaders("*") +// .allowCredentials(true) +// .maxAge(3600); +// } } From f34d3e8a17506cbd09e9d23f41da69bc21215af2 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 11:37:30 +0900 Subject: [PATCH 85/87] =?UTF-8?q?=E2=99=BB=20refactor:CodeQL=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 --- .../chat/service/ContractChatServiceImpl.java | 36 +++++++------------ .../contract/service/ContractFixService.java | 15 ++++++-- .../websocket/config/WebSocketConfig.java | 5 --- 3 files changed, 25 insertions(+), 31 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 24bf4d10..79ea7318 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2491,29 +2491,19 @@ public Map acceptFinalContractConfirmation(Long contractChatId, try { log.info("적법성 검사 API 호출 시작 - contractChatId: {}", contractChatId); Object legalityResponse = contractFixService.getLegality(contractChatId, buyerId); - log.info("적법성 검사 응답: {}", legalityResponse); - + String sanitizedLegalityResponse; + try { + ObjectMapper objectMapper = new ObjectMapper(); + sanitizedLegalityResponse = objectMapper.writeValueAsString(legalityResponse); + } catch (Exception ex) { + sanitizedLegalityResponse = String.valueOf(legalityResponse); + } + sanitizedLegalityResponse = sanitizedLegalityResponse.replaceAll("[\\r\\n]", " "); + log.info("적법성 검사 응답: {}", sanitizedLegalityResponse); if (legalityResponse instanceof LegalityDTO) { LegalityDTO legalityDTO = (LegalityDTO) legalityResponse; log.info("LegalityDTO로 응답 파싱 성공"); - // 디버깅용 로그 추가 (중첩 구조로 수정) - log.info("=== LegalityDTO 필드 확인 ==="); - log.info("success: {}", legalityDTO.getSuccess()); - log.info("message: {}", legalityDTO.getMessage()); - log.info("error: {}", legalityDTO.getError()); - log.info("timestamp: {}", legalityDTO.getTimestamp()); - log.info("data: {}", legalityDTO.getData()); - - if (legalityDTO.getData() != null) { - log.info("=== Payload 확인 ==="); - log.info("contractChatId: {}", legalityDTO.getData().getContractChatId()); - log.info("validationStatus: {}", legalityDTO.getData().getValidationStatus()); - log.info("totalViolations: {}", legalityDTO.getData().getTotalViolations()); - log.info("violations: {}", legalityDTO.getData().getViolations()); - log.info("validatedAt: {}", legalityDTO.getData().getValidatedAt()); - } - // violations 처리 (중첩 구조로 접근) if (legalityDTO.getData() != null && legalityDTO.getData().getViolations() != null @@ -2524,8 +2514,8 @@ public Map acceptFinalContractConfirmation(Long contractChatId, for (int i = 0; i < violations.size(); i++) { LegalityDTO.Violation violation = violations.get(i); - log.info("위반 사항 {}: {}", i + 1, violation); - + String sanitizedViolation = violation == null ? "null" : violation.toString().replaceAll("[\\r\\n]", " "); + log.info("위반 사항 {}: {}", i + 1, sanitizedViolation); StringBuilder violationMessage = new StringBuilder(); violationMessage.append( // 위반 유형 @@ -2576,8 +2566,8 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String.format( "✅ 개선 방안\n %s\n", violation.getImprovementExample())); } - - log.info("전송할 메시지: {}", violationMessage.toString()); + String sanitizedMessage = violationMessage.toString().replaceAll("[\\r\\n]", " "); + log.info("전송할 메시지: {}", sanitizedMessage); AiMessageLegal(contractChatId, violationMessage.toString()); try { diff --git a/src/main/java/org/scoula/domain/contract/service/ContractFixService.java b/src/main/java/org/scoula/domain/contract/service/ContractFixService.java index 2a2cdd17..1084f801 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractFixService.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractFixService.java @@ -71,8 +71,17 @@ public LegalityDTO getLegality(Long contractChatId, Long userId) { restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class); LegalityDTO res = response.getBody(); assert res != null; - log.warn("AI 응답 값 확인: {}", res.toString()); + String resStr; + try { + ObjectMapper objectMapper = new ObjectMapper(); + resStr = objectMapper.writeValueAsString(res); + } catch (Exception ex) { + resStr = res.toString(); + } + // Remove newlines and carriage returns + resStr = resStr.replaceAll("[\\r\\n]", " "); + log.warn("AI 응답 값 확인: {}", resStr); log.warn("AI 응답 헤더 확인: {}", response.getStatusCode()); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { return response.getBody(); @@ -86,8 +95,8 @@ public LegalityDTO getLegality(Long contractChatId, Long userId) { responseBodyStr = String.valueOf(response.getBody()); } // Remove newlines and carriage returns - responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " "); - log.error(responseBodyStr); + responseBodyStr = responseBodyStr.replaceAll("[\\p{Cntrl}]", " "); + log.error("AI server error response (sanitized): {}", responseBodyStr); throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR); } diff --git a/src/main/java/org/scoula/global/websocket/config/WebSocketConfig.java b/src/main/java/org/scoula/global/websocket/config/WebSocketConfig.java index b98e9c82..b66bed3e 100644 --- a/src/main/java/org/scoula/global/websocket/config/WebSocketConfig.java +++ b/src/main/java/org/scoula/global/websocket/config/WebSocketConfig.java @@ -91,15 +91,11 @@ public Message preSend(Message message, MessageChannel channel) { new String( java.util.Base64.getDecoder() .decode(parts[1])); - log.info("🔍 JWT 페이로드: {}", payload); - // 페이로드에서 sub (사용자 이메일) 추출 if (payload.contains("\"sub\"")) { String[] subParts = payload.split("\"sub\":\""); if (subParts.length > 1) { String userEmail = subParts[1].split("\"")[0]; - log.info("🔍 JWT에서 추출한 사용자: {}", userEmail); - // Principal 설정 UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( @@ -124,7 +120,6 @@ public Message preSend(Message message, MessageChannel channel) { new UsernamePasswordAuthenticationToken( userId, null, Collections.emptyList()); accessor.setUser(auth); - log.info("✅ WebSocket 인증 성공 (User-Id) - UserId: {}", userId); } log.info( From 0a26ad386bc7ed1e5577feac3f631a76f0885758 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 11:45:49 +0900 Subject: [PATCH 86/87] =?UTF-8?q?=E2=99=BB=20refactor:CodeQL=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 --- .../chat/service/ContractChatServiceImpl.java | 85 +++++++++---------- .../controller/ContractControllerImpl.java | 45 +++++----- .../scoula/global/config/ServletConfig.java | 31 ++++--- 3 files changed, 80 insertions(+), 81 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 79ea7318..811ee384 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2514,16 +2514,13 @@ public Map acceptFinalContractConfirmation(Long contractChatId, for (int i = 0; i < violations.size(); i++) { LegalityDTO.Violation violation = violations.get(i); - String sanitizedViolation = violation == null ? "null" : violation.toString().replaceAll("[\\r\\n]", " "); + String sanitizedViolation = + violation == null + ? "null" + : violation.toString().replaceAll("[\\r\\n]", " "); log.info("위반 사항 {}: {}", i + 1, sanitizedViolation); StringBuilder violationMessage = new StringBuilder(); - violationMessage.append( - // 위반 유형 - String.format( - "%s\n", - violation.getViolationType() != null - ? violation.getViolationType() - : "정보 없음")); + violationMessage.append( // 관련 법령 String.format( @@ -2566,7 +2563,8 @@ public Map acceptFinalContractConfirmation(Long contractChatId, String.format( "✅ 개선 방안\n %s\n", violation.getImprovementExample())); } - String sanitizedMessage = violationMessage.toString().replaceAll("[\\r\\n]", " "); + String sanitizedMessage = + violationMessage.toString().replaceAll("[\\r\\n]", " "); log.info("전송할 메시지: {}", sanitizedMessage); AiMessageLegal(contractChatId, violationMessage.toString()); @@ -2862,47 +2860,48 @@ public void requestFinalContract(Long contractChatId, Long ownerId) { stringRedisTemplate.opsForValue().set(key, value); } - @Override - public Map acceptFinalContract(Long contractChatId, Long buyerId, Boolean isAccepted) { - if (!isUserInContractChat(contractChatId, buyerId)) { - throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); - } + @Override + public Map acceptFinalContract( + Long contractChatId, Long buyerId, Boolean isAccepted) { + if (!isUserInContractChat(contractChatId, buyerId)) { + throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED); + } - ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); - if (contractChat == null) { - throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); - } + ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId); + } - Long ownerId = contractChat.getOwnerId(); + Long ownerId = contractChat.getOwnerId(); - if (!buyerId.equals(contractChat.getBuyerId())) { - throw new BusinessException( - ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 확정 수락을 할 수 있습니다."); - } + if (!buyerId.equals(contractChat.getBuyerId())) { + throw new BusinessException( + ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 확정 수락을 할 수 있습니다."); + } - String redisKey = "final-contract:request:" + contractChatId; - String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); + String redisKey = "final-contract:request:" + contractChatId; + String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey); - if (storedOwnerId == null) { - throw new BusinessException( - ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "확정 요청이 존재하지 않습니다."); - } + 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, "확정 요청 정보가 유효하지 않습니다."); - } + if (!storedOwnerId.equals(ownerId.toString())) { + throw new BusinessException( + ChatErrorCode.CONTRACT_END_REQUEST_INVALID, "확정 요청 정보가 유효하지 않습니다."); + } - stringRedisTemplate.delete(redisKey); + stringRedisTemplate.delete(redisKey); - if (isAccepted) { - contractMongoRepository.clearSpecialContracts(contractChatId); - contractMongoRepository.saveSpecialContract(contractChatId); - AiMessage(contractChatId, "임차인이 최종 계약서를 수락했습니다! 계약서 서명하러 갈께요!"); - } else { - AiMessage(contractChatId, "임차인이 최종 계약서를 거절했습니다. 추가 협상이 필요합니다."); - } + if (isAccepted) { + contractMongoRepository.clearSpecialContracts(contractChatId); + contractMongoRepository.saveSpecialContract(contractChatId); + AiMessage(contractChatId, "임차인이 최종 계약서를 수락했습니다! 계약서 서명하러 갈께요!"); + } else { + AiMessage(contractChatId, "임차인이 최종 계약서를 거절했습니다. 추가 협상이 필요합니다."); + } - return Map.of("accepted", isAccepted); - } + return Map.of("accepted", isAccepted); + } } diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java index 5afc94cd..25842dca 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -229,26 +229,27 @@ public ResponseEntity> requestFinalContract( } } - @Override - @PostMapping("/specialContract/final-accept") - public ResponseEntity>> acceptFinalContract( - @PathVariable Long contractChatId, - @RequestBody FinalContractDeletionResponseDto responseDto, - Authentication authentication) { - - try { - Long userId = getUserIdFromAuthentication(authentication); - - Map result = - contractChatService.acceptFinalContract(contractChatId, userId, responseDto.isAccepted()); - return ResponseEntity.ok(ApiResponse.success(result)); - - } catch (BusinessException e) { - return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); - } catch (Exception e) { - log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("서버 오류가 발생했습니다.")); - } - } + @Override + @PostMapping("/specialContract/final-accept") + public ResponseEntity>> acceptFinalContract( + @PathVariable Long contractChatId, + @RequestBody FinalContractDeletionResponseDto responseDto, + Authentication authentication) { + + try { + Long userId = getUserIdFromAuthentication(authentication); + + Map result = + contractChatService.acceptFinalContract( + contractChatId, userId, responseDto.isAccepted()); + return ResponseEntity.ok(ApiResponse.success(result)); + + } catch (BusinessException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage())); + } catch (Exception e) { + log.error("최종 특약서 확정 수락 처리 중 오류 발생", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("서버 오류가 발생했습니다.")); + } + } } diff --git a/src/main/java/org/scoula/global/config/ServletConfig.java b/src/main/java/org/scoula/global/config/ServletConfig.java index 2c5c8adf..9905ff7d 100644 --- a/src/main/java/org/scoula/global/config/ServletConfig.java +++ b/src/main/java/org/scoula/global/config/ServletConfig.java @@ -14,7 +14,6 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.StandardServletMultipartResolver; -import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -97,19 +96,19 @@ public LocalDate convert(String source) { }); } -// @Override -// public void addCorsMappings(CorsRegistry registry) { -// registry.addMapping("/**") -// .allowedOrigins( -// "http://localhost:5173", -// "http://localhost:8080", -// "https://itzeep.ariogi.kr", -// "https://www.itzeep.ariogi.kr", -// "http://itzeep.ariogi.kr", -// "http://www.itzeep.ariogi.kr") -// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") -// .allowedHeaders("*") -// .allowCredentials(true) -// .maxAge(3600); -// } + // @Override + // public void addCorsMappings(CorsRegistry registry) { + // registry.addMapping("/**") + // .allowedOrigins( + // "http://localhost:5173", + // "http://localhost:8080", + // "https://itzeep.ariogi.kr", + // "https://www.itzeep.ariogi.kr", + // "http://itzeep.ariogi.kr", + // "http://www.itzeep.ariogi.kr") + // .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") + // .allowedHeaders("*") + // .allowCredentials(true) + // .maxAge(3600); + // } } From fa5904f8921cd782e87a39ca1f6617e726c7c3f7 Mon Sep 17 00:00:00 2001 From: leeedongjaee Date: Mon, 18 Aug 2025 13:00:26 +0900 Subject: [PATCH 87/87] =?UTF-8?q?=E2=9C=A8=20feat:=20COMPLETE=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=B0=98=ED=99=98=20URL=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/service/ContractChatServiceImpl.java | 10 ++++++++-- .../java/org/scoula/domain/chat/vo/ContractChat.java | 3 ++- 2 files changed, 10 insertions(+), 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 811ee384..aa212110 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -2090,6 +2090,8 @@ public String getContractChatStatus(ContractChat.ContractStatus status) { return "?step=3&round=3"; case ROUND4: return "?step=3&round=4"; + case COMPLETE: + return "?step=3&round=4"; default: return null; } @@ -2806,8 +2808,11 @@ public String getContractChatRoomUrl(Long chatRoomId) { } Long contractChatRoomId = contractChatId.getContractChatId(); String param = getContractChatStatus(contractChatId.getStatus()); - - return baseUrl + contractChatUrl + contractChatRoomId.toString() + param; + if (contractChatId.getStatus() == ContractChat.ContractStatus.COMPLETE) { + return baseUrl + contractChatUrl + "complete/" + (contractChatRoomId.toString()); + } else { + return baseUrl + contractChatUrl + contractChatRoomId.toString() + param; + } } private void broadcastPresence(Long contractChatId) { @@ -2897,6 +2902,7 @@ public Map acceptFinalContract( if (isAccepted) { contractMongoRepository.clearSpecialContracts(contractChatId); contractMongoRepository.saveSpecialContract(contractChatId); + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.COMPLETE); AiMessage(contractChatId, "임차인이 최종 계약서를 수락했습니다! 계약서 서명하러 갈께요!"); } else { AiMessage(contractChatId, "임차인이 최종 계약서를 거절했습니다. 추가 협상이 필요합니다."); 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 756ed828..56e53b77 100644 --- a/src/main/java/org/scoula/domain/chat/vo/ContractChat.java +++ b/src/main/java/org/scoula/domain/chat/vo/ContractChat.java @@ -30,7 +30,8 @@ public enum ContractStatus { ROUND2, ROUND3, ROUND4, - STEP4 + STEP4, + COMPLETE } // 현재 라운드 번호 계산 메서드