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 19d1623e..f93aa7af 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractController.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractController.java @@ -14,53 +14,49 @@ @Api(tags = "계약서 API", description = "계약서 : 정보확인 / 금액 조율 / 적법성 확인") public interface ContractController { - // step 0 - @ApiOperation(value = "임차인 대기 메세지", notes = "step0의 AI 메세지") - ResponseEntity> standByContract( - @PathVariable Long contractChatId, - @AuthenticationPrincipal CustomUserDetails userDetails); - // step 1 (init) - @ApiOperation(value = "계약서 몽고DB에 저장", notes = "계약서에 필요한 항목들을 가져와서 몽고 DB에 계약서 만들기") + @ApiOperation(value = "[계약전_임차인] 계약서를 몽고DB에 저장", notes = "계약서에 필요한 항목들을 가져와서 몽고 DB에 계약서 만들기") ResponseEntity> saveContractMongo( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails); // step 1 : start - @ApiOperation(value = "계약서 전체 조회", notes = "계약서 가져오기") + @ApiOperation(value = "[계약서 _ 정보 조회 1] 계약서 전체 조회", notes = "계약서 가져오기") ResponseEntity> getContract( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails); - @ApiOperation(value = "정보조회 다음 단계로 넘어가기 Message", notes = "정보조회 마지막 단계에서 다음 단계로 넘어가기 Message") + @ApiOperation(value = "[채팅 _ 정보 조회 1] 정보 조회 시작", notes = "정보조회 마지막 단계에서 다음 단계로 넘어가기 Message") ResponseEntity> getContractNext( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails); // step 1 : finish - @ApiOperation(value = "다음 단계로 넘어가기", notes = "다음 단계 여부(true/false)를 받아서 다음 단계로 넘어가기") + @ApiOperation( + value = "[채팅 _ 정보 조회 2] 정보 조회에서 다음단계로 가기", + notes = "다음 단계 여부(true/false)를 받아서 다음 단계로 넘어가기") ResponseEntity> nextStep( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody NextStepDTO dto); - @ApiOperation(value = "금액 조회", notes = "금액을 조율하기 위해 금액을 조회") + @ApiOperation(value = "[채팅 _ 금액 조회 1]", notes = "금액을 조율하기 위해 금액을 조회") ResponseEntity> getDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails); - @ApiOperation(value = "금액 요청", notes = "임대인이 금액을 요청") + @ApiOperation(value = "[채팅 _ 금액 요청 2]", notes = "임대인이 금액을 요청") ResponseEntity> saveDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody PaymentDTO dto); - @ApiOperation(value = "금액 거절 ", notes = "임차인이 금액을 거절") + @ApiOperation(value = "[채팅 _ 금액 거절 3]", notes = "임차인이 금액을 거절") ResponseEntity> deleteDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails); - @ApiOperation(value = "금액 수락", notes = "임대인과 임차인 모두 동의") + @ApiOperation(value = "[채팅 _ 금액 수락 4]", notes = "임대인과 임차인 모두 동의") ResponseEntity> updateDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails); 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 08a667bf..e9ba5a1d 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -19,16 +19,6 @@ public class ContractControllerImpl implements ContractController { private final ContractService service; - @Override - @PostMapping("/standBy") - public ResponseEntity> standByContract( - @PathVariable Long contractChatId, - @AuthenticationPrincipal CustomUserDetails userDetails) { - return ResponseEntity.ok( - ApiResponse.success( - service.standByContract(contractChatId, userDetails.getUserId()))); - } - @Override @PostMapping("") public ResponseEntity> saveContractMongo( diff --git a/src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java b/src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java index 5d62fbcb..86ba0dc1 100644 --- a/src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java +++ b/src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java @@ -7,6 +7,10 @@ @Mapper public interface ContractMapper { + Long getOwnerId(@Param("contractChatId") Long contractChatId); + + Long getBuyerId(@Param("contractChatId") Long contractChatId); + ContractDTO getContract(@Param("contractChatId") Long contractChatId); String getDuration(@Param("contractChatId") Long contractChatId); 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 a0380765..4dab7963 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractService.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractService.java @@ -4,14 +4,6 @@ public interface ContractService { - /** - * step0. 임차인이 임대인을 기다릴때 - * - * @param contractChatId 채팅방 아이디 - * @param userId 유저 아이디 - */ - Void standByContract(Long contractChatId, Long userId); - /** * step1 (init) 계약서에 필요한 항목들을 가져와서 몽고 DB에 계약서 만들기 * 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 a72f2d1a..fc687798 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -47,37 +47,6 @@ public class ContractServiceImpl implements ContractService { @Value("${ai.server.url:http://localhost:8000}") private String aiServerUrl; - /** {@inheritDoc} */ - @Override - public Void standByContract(Long contractChatId, Long userId) { - - // 시작 메세지 보내기 - contractChatService.AiMessage(contractChatId, """ - 안녕하세요! - 임대인이 입장하면 바로 계약서 작성을 시작할게요. - """); - - // 2초 - // 잠깐의 텀 (2초) - try { - Thread.sleep(2000); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - log.warn("standByContract sleep interrupted", ie); - } - - - contractChatService.AiMessageBtn(contractChatId, """ - 기다리는 동안 - 어려운 법률 용어와 법률 팁을 알아볼까요? - """); - - // contract에 매퍼로 스텝 추가하기 - contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP0); - - return null; - } - /** {@inheritDoc} */ @Override public Void saveContractMongo(Long contractChatId, Long userId) { @@ -87,29 +56,13 @@ public Void saveContractMongo(Long contractChatId, Long userId) { // 이미 생성된 계약 문서가 있으면 저장 대신 안내 메시지 전송 후 종료 ContractMongoDocument existing = repository.getContract(contractChatId); if (existing != null) { - contractChatService.AiMessage(contractChatId, """ - 이미 생성된 계약서가 있어요. - 기존 계약서를 불러올게요. - """); + contractChatService.AiMessage(contractChatId, " 이미 생성된 계약서가 있어요.\n" + "기존 계약서를 불러올게요."); return null; } // 계약서에 들어갈 내용들을 mapper로 가져오기 ContractDTO dto = contractMapper.getContract(contractChatId); -// // 특약이 null이면 빈 리스트로 세팅 -// if (dto.getSpecialContracts() == null) { -// dto.setSpecialContracts(Collections.emptyList()); -// } -// -// // 전화번호가 null이면 빈 문자열로 세팅 -// if (dto.getOwnerPhoneNum() == null) { -// dto.setOwnerPhoneNum(""); -// } -// if (dto.getBuyerPhoneNum() == null) { -// dto.setBuyerPhoneNum(""); -// } - // 계약 끝나는 기간 String durationStr = contractMapper.getDuration(contractChatId); ContractDuration duration = ContractDuration.valueOf(durationStr); @@ -138,29 +91,10 @@ public Void saveContractMongo(Long contractChatId, Long userId) { } /** {@inheritDoc} */ + // 계약서 조회하기 @Override public ContractDTO getContract(Long contractChatId, Long userId) { - // 스텝 변경 - contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP1); - - // 다음 단계 메세지 보내기 - contractChatService.AiMessage(contractChatId, "이번 단계는 '정보 확인' 단계입니다"); - - ContractMongoDocument doc = repository.getContract(contractChatId); - AIMessageDTO aiDto = AIMessageDTO.toDTO(doc); - - // 시작 메세지 보내기 - contractChatService.AiMessage( - contractChatId, - """ - 👋🏻 안녕하세요! - 이 계약은 임대인 %s님과 임차인 %s님의 계약입니다. 시작하기 전, 정보를 먼저 확인할게요. - - 제출된 정보를 토대로 계약서를 추출할게요. - """.formatted(aiDto.getOwnerName(), aiDto.getBuyerName()) - ); - // userId 검증 validateUserId(contractChatId, userId); @@ -173,85 +107,92 @@ public ContractDTO getContract(Long contractChatId, Long userId) { // 찾은 값을 Dto에 넣고 반환하기 ContractDTO dto = ContractDTO.toDTO(document); - boolean deposit = contractMapper.getDepositAdjustment(contractChatId); - - if (!deposit) { - contractChatService.AiMessageBtn(contractChatId, """ - 다음은 2단계 '금액 조율' 단계입니다. - - 두 분 모두 금액 조율 의사가 없으므로, - 다음 단계로 자동으로 넘어갑니다. - """); - } - return dto; } @Override + // 해당 스텝 메세지 & 다음 단계로 넘어가는지 public Void getContractNext(Long contractChatId, Long userId) { + // userId 검증 validateUserId(contractChatId, userId); ContractMongoDocument doc = repository.getContract(contractChatId); AIMessageDTO aiDto = AIMessageDTO.toDTO(doc); + // 시작 메세지 보내기 + contractChatService.AiMessage( + contractChatId, + """ + 👋🏻 안녕하세요! + 이 계약은 임대인 %s님과 임차인 %s님의 계약입니다. + 시작하기 전, 정보를 먼저 확인할게요. + 제출된 정보를 토대로 계약서를 추출할게요. + """.formatted(aiDto.getOwnerName(), aiDto.getBuyerName()) + ); + + // 2초 대기 + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + contractChatService.AiMessageBtn(contractChatId, """ %s님과 %s님이 작성한 사전 조사를 토대로 정보를 추출한 결과가 다음과 같습니다. 매물 정보, 조건을 확인하셨나요? 다음 단계로 넘어갈까요? """.formatted(aiDto.getBuyerName(), aiDto.getOwnerName())); + return null; } @Override public Boolean nextStep(Long contractChatId, Long userId, NextStepDTO dto) { - ContractChat.ContractStatus step = contractChatMapper.getStatus(contractChatId); - // Redis Key: 계약별 step 상태를 저장 - String redisKey = String.format("contract:%s:%d", step.name(), contractChatId); - - try { - ObjectMapper objectMapper = new ObjectMapper(); - - // 1) 기존 상태 로드 (없으면 기본값 생성) - String currentJson = stringRedisTemplate.opsForValue().get(redisKey); - NextStepDTO state = (currentJson != null) - ? objectMapper.readValue(currentJson, NextStepDTO.class) - : new NextStepDTO(); + // userId 검증 + validateUserId(contractChatId, userId); - // 2) 이번 요청 값 반영 (이제 step은 DTO에서 받지 않음, DB 상태는 필요 시 별도 조회) - if (dto.isOwner()) { - state.setOwner(true); - } - if (dto.isBuyer()) { - state.setBuyer(true); + Boolean nextSteps = nextSteps(contractChatId, userId, dto); + + if (nextSteps) { + boolean deposit = contractMapper.getDepositAdjustment(contractChatId); + + if (deposit) { + contractChatService.AiMessage(contractChatId, "다음 단계는 '금액 조율' 단계입니다"); + } else if (!deposit) { + contractChatService.AiMessageBtn(contractChatId, """ + 다음은 2단계 '금액 조율' 단계입니다. + + 두 분 모두 금액 조율 의사가 없으므로, + 다음 단계로 자동으로 넘어갑니다. + """); + + // 2초 대기 + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 다음 단계 메세지 보내기 + contractChatService.AiMessage(contractChatId, "이번 단계는 '금액 조율' 단계입니다"); } + // 스텝 변경 + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP1); + } - // 3) 두 사람이 모두 true면 -> 키 삭제하고 true 반환 - if (state.isOwner() && state.isBuyer()) { - stringRedisTemplate.delete(redisKey); - return true; - } + return nextSteps; - // 4) 아직 한쪽만 true면 -> 상태 저장하고 false 반환 - String updatedJson = objectMapper.writeValueAsString(state); - stringRedisTemplate.opsForValue().set(redisKey, updatedJson); - return false; - } catch (Exception e) { - throw new BusinessException(ContractException.CONTRACT_REDIS, e); - } } /** {@inheritDoc} */ @Override public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { - // 스텝 변경 - contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); - - // 다음 단계 메세지 보내기 - contractChatService.AiMessage(contractChatId, "이번 단계는 '금액 조율' 단계입니다"); + // userId 검증 + validateUserId(contractChatId, userId); ContractMongoDocument doc = repository.getContract(contractChatId); AIMessageDTO aiDto = AIMessageDTO.toDTO(doc); @@ -259,6 +200,7 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { long contract = ChronoUnit.YEARS.between(aiDto.getContractStartDate(), aiDto.getContractEndDate()); String rentType = tenantMapper.selectRentType(contractChatId, userId) .orElseThrow(() -> new BusinessException(ContractException.CONTRACT_GET, "전/월세 타입 조회 실패")); + // 시작 메세지 보내기 contractChatService.AiMessage( contractChatId, @@ -274,14 +216,18 @@ public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { formatWonShort(aiDto.getDepositPrice()), formatWonShort(aiDto.getMaintenanceFee()))); + // 대기 + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + contractChatService.AiMessage( contractChatId, """ 자유롭게 채팅 후 임대인(%s)님께서 금액을 조정해주세요. 임차인(%s)님이 수락 후 해당 조건의 확정이 가능합니다. """.formatted(aiDto.getBuyerName(), aiDto.getOwnerName())); - // userId 검증 - validateUserId(contractChatId, userId); - // MongoDB에서 보증금, 계약금, 잔금, 월세를 조회한다 ContractMongoDocument document = repository.getDepositPrice(contractChatId); if (document == null) { @@ -366,6 +312,12 @@ public Void updateDepositPrice(Long contractChatId, Long userId) { throw new BusinessException(ContractException.CONTRACT_UPDATE, e); } + // 스텝 변경 + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); + + // 다음 단계 메세지 보내기 + contractChatService.AiMessage(contractChatId, "이번 단계는 '특약 조율' 단계입니다"); + return null; } @@ -463,14 +415,31 @@ public Void sendStep4(Long contractChatId, Long userId) { // Userid 검증 public void validateUserId(Long contractChatId, Long userId) { - Long buyerId = - tenantMapper - .selectContractBuyerId(contractChatId) - .orElseThrow(() -> new BusinessException(PreContractErrorCode.TENANT_USER)); - if (!userId.equals(buyerId)) { + 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) { @@ -481,6 +450,43 @@ public void validateIsOwner(Long contractChatId, Long userId) { } } + public Boolean nextSteps(Long contractChatId, Long userId, NextStepDTO dto) { + ContractChat.ContractStatus step = contractChatMapper.getStatus(contractChatId); + // Redis Key: 계약별 step 상태를 저장 + String redisKey = String.format("contract:%s:%d", step.name(), contractChatId); + + try { + ObjectMapper objectMapper = new ObjectMapper(); + + // 1) 기존 상태 로드 (없으면 기본값 생성) + String currentJson = stringRedisTemplate.opsForValue().get(redisKey); + NextStepDTO state = (currentJson != null) + ? objectMapper.readValue(currentJson, NextStepDTO.class) + : new NextStepDTO(); + + // 2) 이번 요청 값 반영 (이제 step은 DTO에서 받지 않음, DB 상태는 필요 시 별도 조회) + if (dto.isOwner()) { + state.setOwner(true); + } + if (dto.isBuyer()) { + state.setBuyer(true); + } + + // 3) 두 사람이 모두 true면 -> 키 삭제하고 true 반환 + if (state.isOwner() && state.isBuyer()) { + stringRedisTemplate.delete(redisKey); + return true; + } + + // 4) 아직 한쪽만 true면 -> 상태 저장하고 false 반환 + String updatedJson = objectMapper.writeValueAsString(state); + stringRedisTemplate.opsForValue().set(redisKey, updatedJson); + return false; + } catch (Exception e) { + throw new BusinessException(ContractException.CONTRACT_REDIS, e); + } + } + private static String formatWonShort(int amount) { if (amount == 0) return "0원"; long eok = amount / 100_000_000; // 억 diff --git a/src/main/java/org/scoula/domain/precontract/controller/TenantPreContractControllerImpl.java b/src/main/java/org/scoula/domain/precontract/controller/TenantPreContractControllerImpl.java index 1faf1bcd..957319b6 100644 --- a/src/main/java/org/scoula/domain/precontract/controller/TenantPreContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/precontract/controller/TenantPreContractControllerImpl.java @@ -1,7 +1,7 @@ package org.scoula.domain.precontract.controller; import org.scoula.domain.precontract.dto.tenant.*; -import org.scoula.domain.precontract.service.PreContractService; +import org.scoula.domain.precontract.service.TenantPreContractService; import org.scoula.global.auth.dto.CustomUserDetails; import org.scoula.global.common.dto.ApiResponse; import org.springframework.http.ResponseEntity; @@ -17,7 +17,7 @@ @Log4j2 public class TenantPreContractControllerImpl implements TenantPreContractController { - private final PreContractService service; + private final TenantPreContractService service; // =============== 사기 위험도 확인 & 기본 세팅 ================== 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 2e2ce728..ea9a5cb7 100644 --- a/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java @@ -443,6 +443,7 @@ public Void saveMongoDB(Long contractChatId, Long userId) { contractChatService.AiMessage( contractChatId, "\uD83D\uDC4B 임대인께서 입장하셨습니다! \n" + "지금부터 계약을 진행하겠습니다."); contractService.saveContractMongo(contractChatId, userId); + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP0); return null; } diff --git a/src/main/java/org/scoula/domain/precontract/service/PreContractService.java b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractService.java similarity index 98% rename from src/main/java/org/scoula/domain/precontract/service/PreContractService.java rename to src/main/java/org/scoula/domain/precontract/service/TenantPreContractService.java index 8183b328..72d32e28 100644 --- a/src/main/java/org/scoula/domain/precontract/service/PreContractService.java +++ b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractService.java @@ -2,7 +2,7 @@ import org.scoula.domain.precontract.dto.tenant.*; -public interface PreContractService { +public interface TenantPreContractService { // =============== 사기 위험도 확인 & 기본 세팅 ================== diff --git a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java similarity index 99% rename from src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java rename to src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java index 39b2e3fb..883a1afa 100644 --- a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/precontract/service/TenantPreContractServiceImpl.java @@ -27,7 +27,7 @@ @Service @RequiredArgsConstructor @Log4j2 -public class PreContractServiceImpl implements PreContractService { +public class TenantPreContractServiceImpl implements TenantPreContractService { private final TenantPreContractMapper tenantMapper; private final TenantMongoRepository mongoRepository; diff --git a/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml b/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml index 65037f6b..10aa8332 100644 --- a/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml +++ b/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml @@ -4,25 +4,33 @@ + + + +