From 99e051a702923c613c4f143bb7eeef031646aca2 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:26:36 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=200,1,2=EB=8B=A8=EA=B3=84=20controller=20/=20controll?= =?UTF-8?q?erImpl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ContractController.java | 94 +++++++++++ .../controller/ContractControllerImpl.java | 156 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/controller/ContractController.java create mode 100644 src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractController.java b/src/main/java/org/scoula/domain/contract/controller/ContractController.java new file mode 100644 index 00000000..666bc03d --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/controller/ContractController.java @@ -0,0 +1,94 @@ +package org.scoula.domain.contract.controller; + +import javax.servlet.http.HttpServletResponse; + +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.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +@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에 계약서 만들기") + ResponseEntity> saveContractMongo( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + // step 1 : start + @ApiOperation(value = "계약서 전체 조회", notes = "계약서 가져오기") + ResponseEntity> getContract( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + @ApiOperation(value = "정보조회 다음 단계로 넘어가기 Message", notes = "정보조회 마지막 단계에서 다음 단계로 넘어가기 Message") + ResponseEntity> getContractNext( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + // step 1 : finish + @ApiOperation(value = "다음 단계로 넘어가기", notes = "다음 단계 여부(true/false)를 받아서 다음 단계로 넘어가기") + ResponseEntity> nextStep( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody NextStepDTO dto); + + @ApiOperation(value = "금액 조회", notes = "금액을 조율하기 위해 금액을 조회") + ResponseEntity> getDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + @ApiOperation(value = "금액 요청", notes = "임대인이 금액을 요청") + ResponseEntity> saveDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody PaymentDTO dto); + + @ApiOperation(value = "금액 거절 ", notes = "임차인이 금액을 거절") + ResponseEntity> deleteDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + @ApiOperation(value = "금액 수락", notes = "임대인과 임차인 모두 동의") + ResponseEntity> updateDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + @ApiOperation(value = "AI 적법성 확인 from 몽고DB", notes = "몽고DB에 있는 계약서를 AI로 보내고, 적법성 받기") + ResponseEntity> getLegality( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + @ApiOperation(value = "특약을 계약서 DB에 저장 FROM 몽고DB", notes = "특약 테이블에 있는걸 계약서로 가져오기") + ResponseEntity> saveSpecialContract( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails); + + @ApiOperation(value = "바뀐 특약 수정 from 몽고DB", notes = "적법성 검사 후 수정된 특약으로 변경") + ResponseEntity> updateSpecialContract( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody SpecialContractDTO dto); + + @ApiOperation(value = "적법성 검사 후 다음단계로 넘어가기", notes = "적법성 검사 후 AI 메세지를 보낸다") + ResponseEntity> sendStep4( + @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 new file mode 100644 index 00000000..042cb134 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -0,0 +1,156 @@ +package org.scoula.domain.contract.controller; + +import javax.servlet.http.HttpServletResponse; + +import org.scoula.domain.contract.dto.*; +import org.scoula.domain.contract.service.ContractService; +import org.scoula.global.auth.dto.CustomUserDetails; +import org.scoula.global.common.dto.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@RestController +@Log4j2 +@RequiredArgsConstructor +@RequestMapping("/api/contract/{contractChatId}") +public class ContractControllerImpl implements ContractController { + + private final ContractService service; + + @Override + @GetMapping("/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( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success( + service.saveContractMongo(contractChatId, userDetails.getUserId()))); + } + + @Override + @GetMapping("") + public ResponseEntity> getContract( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success(service.getContract(contractChatId, userDetails.getUserId()))); + } + + @Override + @GetMapping("/step1") + public ResponseEntity> getContractNext( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success( + service.getContractNext(contractChatId, userDetails.getUserId()))); + } + + @Override + @PostMapping("/nextStep") + public ResponseEntity> nextStep( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody NextStepDTO dto) { + return ResponseEntity.ok( + ApiResponse.success( + service.nextStep(contractChatId, userDetails.getUserId(), dto))); + } + + @Override + @GetMapping("/price") + public ResponseEntity> getDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success( + service.getDepositPrice(contractChatId, userDetails.getUserId()))); + } + + @Override + @PostMapping("/price") + public ResponseEntity> saveDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody PaymentDTO dto) { + return ResponseEntity.ok( + ApiResponse.success( + service.saveDepositPrice(contractChatId, userDetails.getUserId(), dto))); + } + + @Override + @DeleteMapping("/price") + public ResponseEntity> deleteDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success( + service.deleteDepositPrice(contractChatId, userDetails.getUserId()))); + } + + @Override + @PatchMapping("/price") + public ResponseEntity> updateDepositPrice( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success( + service.updateDepositPrice(contractChatId, userDetails.getUserId()))); + } + + @Override + @PostMapping("/legality") + public ResponseEntity> getLegality( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success(service.getLegality(contractChatId, userDetails.getUserId()))); + } + + @Override + @PostMapping("/specialContract") + public ResponseEntity> saveSpecialContract( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success( + service.saveSpecialContract(contractChatId, userDetails.getUserId()))); + } + + @Override + @PatchMapping("/specialContract") + public ResponseEntity> updateSpecialContract( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody SpecialContractDTO dto) { + return ResponseEntity.ok( + ApiResponse.success( + service.updateSpecialContract( + contractChatId, userDetails.getUserId(), dto))); + } + + @Override + @GetMapping("/specialContract") + public ResponseEntity> sendStep4( + @PathVariable Long contractChatId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok( + ApiResponse.success(service.sendStep4(contractChatId, userDetails.getUserId()))); + } + +} From 94f6a77656c671e294a9f4f2fedd54a84b68f5f0 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:27:16 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=20exception?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/exception/ContractException.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/exception/ContractException.java diff --git a/src/main/java/org/scoula/domain/contract/exception/ContractException.java b/src/main/java/org/scoula/domain/contract/exception/ContractException.java new file mode 100644 index 00000000..c03f201a --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/exception/ContractException.java @@ -0,0 +1,21 @@ +package org.scoula.domain.contract.exception; + +import org.scoula.global.common.exception.IErrorCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ContractException implements IErrorCode { + CONTRACT_GET("CONTRACT_4001", HttpStatus.BAD_REQUEST, "mongoDB에서 값을 조회하지 못 햇습니다."), + CONTRACT_INSERT("CONTRACT_4002", HttpStatus.BAD_REQUEST, "MongoDB에 저장이 되지 않았습니다."), + CONTRACT_AI_SERVER_ERROR( + "CONTRACT_4003", HttpStatus.SERVICE_UNAVAILABLE, "AI 서버 통신 중 오류가 발생했습니다."), + CONTRACT_UPDATE("CONTRACT_4004", HttpStatus.BAD_REQUEST, "MongoDB에 수정이 되지 않았습니다"), + CONTRACt_REDIS("CONTRACT_4005", HttpStatus.BAD_REQUEST, "REDIS에 해당 정보가 없습니다."); + private final String code; + private final HttpStatus httpStatus; + private final String message; +} From 35d9d6805cec57e14031427e3d467aa837756017 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:28:04 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=20=EB=AA=BD=EA=B3=A0=20DB=20=5F=20document?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/ContractMongoDocument.java | 110 ++++++++++++++++++ .../FinalSpecialContractDocument.java | 34 ++++++ 2 files changed, 144 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java create mode 100644 src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java diff --git a/src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java b/src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java new file mode 100644 index 00000000..9c79f617 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java @@ -0,0 +1,110 @@ +package org.scoula.domain.contract.document; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.stream.Collectors; + +import org.scoula.domain.contract.dto.ContractDTO; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Document(collection = "FINAL_CONTRACT") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ContractMongoDocument { + + @Id private String id; + + @Field("contractChatId") + private Long contractChatId; + + // 제 1조 계약 당사자 + private String ownerName; + private String ownerAddr; + + @JsonInclude(JsonInclude.Include.ALWAYS) + private String ownerPhoneNum; + + private String buyerName; + private String buyerAddr; + + @JsonInclude(JsonInclude.Include.ALWAYS) + private String buyerPhoneNum; + + // 제 2조 임대물건의 표시 + private String homeAddr1; + private String homeAddr2; + private String residenceType; + private float exclusiveArea; + private int homeFloor; + + // 제 3조 임대차 기간 및 임료 + @Field("contractStartDate") + private String contractStartDate; + + @Field("contractEndDate") + private String contractEndDate; // 계산해서 넣기 + + private int depositPrice; + private int monthlyRent; + private int maintenanceFee; + + // 제 4조 특약사항 + @JsonInclude(JsonInclude.Include.ALWAYS) + private java.util.List specialContracts; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SpecialContract { + private Integer order; + private String title; + private String content; + } + + public static ContractMongoDocument toDocument(ContractDTO dto, LocalDate contractEndDate) { + return ContractMongoDocument.builder() + .contractChatId(dto.getContractChatId()) + .ownerName(dto.getOwnerName()) + .ownerAddr(dto.getOwnerAddr()) + .ownerPhoneNum(dto.getOwnerPhoneNum()) + .buyerName(dto.getBuyerName()) + .buyerAddr(dto.getBuyerAddr()) + .buyerPhoneNum(dto.getBuyerPhoneNum()) + .homeAddr1(dto.getHomeAddr1()) + .homeAddr2(dto.getHomeAddr2()) + .residenceType(dto.getResidenceType()) + .exclusiveArea(dto.getExclusiveArea()) + .homeFloor(dto.getHomeFloor()) + .contractStartDate(dto.getContractStartDate().toString()) + .contractEndDate(contractEndDate.toString()) + .depositPrice(dto.getDepositPrice()) + .monthlyRent(dto.getMonthlyRent()) + .maintenanceFee(dto.getMaintenanceFee()) + .specialContracts( + (dto.getSpecialContracts() == null + ? Collections.emptyList() + : dto.getSpecialContracts()) + .stream() + .map( + s -> + SpecialContract.builder() + .order(s.getOrder()) + .title(s.getTitle()) + .content(s.getContent()) + .build()) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java b/src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java new file mode 100644 index 00000000..cbc62a54 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java @@ -0,0 +1,34 @@ +package org.scoula.domain.contract.document; + +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Document(collection = "FINAL_SPECIAL_CONTRACT") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FinalSpecialContractDocument { + @Id private String id; + private Long contractChatId; + private Integer totalFinalClauses; + private List finalClauses; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class FinalClause { + private Integer order; + private String title; + private String content; + private Long sourceRound; + } +} From 1620865c5caeb547cf424eed1ebcb18dedc7a086 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:30:04 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=200,1,2=20=EB=8B=A8=EA=B3=84=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/contract/dto/AIMessageDTO.java | 50 ++++++++ .../domain/contract/dto/ContractDTO.java | 114 ++++++++++++++++++ .../domain/contract/dto/FinalContractDTO.java | 26 ++++ .../domain/contract/dto/LegalityDTO.java | 92 ++++++++++++++ .../domain/contract/dto/NextStepDTO.java | 21 ++++ .../domain/contract/dto/PaymentDTO.java | 31 +++++ .../contract/dto/SpecialContractDTO.java | 28 +++++ 7 files changed, 362 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java create mode 100644 src/main/java/org/scoula/domain/contract/dto/ContractDTO.java create mode 100644 src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java create mode 100644 src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java create mode 100644 src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java create mode 100644 src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java create mode 100644 src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java diff --git a/src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java b/src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java new file mode 100644 index 00000000..9a7f1ac7 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java @@ -0,0 +1,50 @@ +package org.scoula.domain.contract.dto; + +import java.time.LocalDate; + +import org.scoula.domain.contract.document.ContractMongoDocument; + +import io.swagger.annotations.ApiModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel(description = "계약채팅에 들어감 ") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AIMessageDTO { + // 임차인 이름 + private String ownerName; + // 임대인 이름 + private String buyerName; + + // 계약 기간 + private LocalDate contractStartDate; + private LocalDate contractEndDate; + + // 전월세 + private String rentType; + // 보증금 + // 월세 + // 관리비 + private int depositPrice; + private int monthlyRent; + private int maintenanceFee; + + // 적합성 분석 개수 + + public static AIMessageDTO toDTO(ContractMongoDocument document) { + return AIMessageDTO.builder() + .ownerName(document.getOwnerName()) + .buyerName(document.getBuyerName()) + .contractStartDate(LocalDate.parse(document.getContractStartDate())) + .contractEndDate(LocalDate.parse(document.getContractEndDate())) + .depositPrice(document.getDepositPrice()) + .monthlyRent(document.getMonthlyRent()) + .maintenanceFee(document.getMaintenanceFee()) + .build(); + } +} diff --git a/src/main/java/org/scoula/domain/contract/dto/ContractDTO.java b/src/main/java/org/scoula/domain/contract/dto/ContractDTO.java new file mode 100644 index 00000000..92598f4d --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/dto/ContractDTO.java @@ -0,0 +1,114 @@ +package org.scoula.domain.contract.dto; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; + +import org.scoula.domain.contract.document.ContractMongoDocument; +import org.scoula.global.common.constant.Constants; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.annotations.ApiModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel(description = "계약서에 들어가는 내용") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ContractDTO { + + private Long contractChatId; + + // 제 1조 계약 당사자 + private String ownerName; + private String ownerAddr; + private String ownerPhoneNum; + + private String buyerName; + private String buyerAddr; + private String buyerPhoneNum; + + // 제 2조 임대물건의 표시 + private String homeAddr1; + private String homeAddr2; + private String residenceType; + private float exclusiveArea; + private int homeFloor; + + // 제 3조 임대차 기간 및 임료 + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = Constants.DateTime.DEFAULT_DATE_FORMAT) + private LocalDate contractStartDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = Constants.DateTime.DEFAULT_DATE_FORMAT) + private LocalDate contractEndDate; // 계산해서 넣기 + + private int depositPrice; + private int monthlyRent; + private int maintenanceFee; + + private List specialContracts; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SpecialContractDTO { + private Integer order; + private String title; + private String content; + } + + public static ContractDTO toDTO(ContractMongoDocument document) { + return ContractDTO.builder() + .contractChatId(document.getContractChatId()) + .ownerName(document.getOwnerName()) + .ownerAddr(document.getOwnerAddr()) + .ownerPhoneNum(document.getOwnerPhoneNum()) + .buyerName(document.getBuyerName()) + .buyerAddr(document.getBuyerAddr()) + .buyerPhoneNum(document.getBuyerPhoneNum()) + .homeAddr1(document.getHomeAddr1()) + .homeAddr2(document.getHomeAddr2()) + .residenceType(document.getResidenceType()) + .exclusiveArea(document.getExclusiveArea()) + .homeFloor(document.getHomeFloor()) + .contractStartDate(LocalDate.parse(document.getContractStartDate())) + .contractEndDate(LocalDate.parse(document.getContractEndDate())) + .depositPrice(document.getDepositPrice()) + .monthlyRent(document.getMonthlyRent()) + .maintenanceFee(document.getMaintenanceFee()) + .specialContracts( + document.getSpecialContracts().stream() + .map( + documentSpecialContract -> + SpecialContractDTO.builder() + .content( + documentSpecialContract + .getContent()) + .title(documentSpecialContract.getTitle()) + .order(documentSpecialContract.getOrder()) + .build()) + .toList()) + .build(); + } + + public static ContractDTO toSpecialContractDTO(ContractMongoDocument document) { + return ContractDTO.builder() + .specialContracts( + document.getSpecialContracts().stream() + .map( + c -> + SpecialContractDTO.builder() + .order(c.getOrder()) + .title(c.getTitle()) + .content(c.getContent()) + .build()) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java b/src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java new file mode 100644 index 00000000..e28746c7 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java @@ -0,0 +1,26 @@ +package org.scoula.domain.contract.dto; + +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.annotations.ApiModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel(description = "최종 계약서") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FinalContractDTO { + + private MultipartFile ownerTaxSignature; + private MultipartFile ownerPrioritySignature; + private MultipartFile ownerContractSignature; + private MultipartFile buyerContractSignature; + + private Boolean mediation_agree; // 조정 동의 여부 + + private String contractKey; // 계약서 비밀번호 +} diff --git a/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java new file mode 100644 index 00000000..56c3f489 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java @@ -0,0 +1,92 @@ +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에서 가져온 적법성 검사") +@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; + } +} diff --git a/src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java b/src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java new file mode 100644 index 00000000..c5f2aba0 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java @@ -0,0 +1,21 @@ +package org.scoula.domain.contract.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.swagger.annotations.ApiModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel(description = "걔약서 다음 단계로 넘어가기 위한 DTO") +@JsonInclude(JsonInclude.Include.NON_NULL) +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NextStepDTO { + + private boolean owner; + private boolean buyer; +} diff --git a/src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java b/src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java new file mode 100644 index 00000000..c17ae680 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java @@ -0,0 +1,31 @@ +package org.scoula.domain.contract.dto; + +import org.scoula.domain.contract.document.ContractMongoDocument; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel(description = "금액 조율에 필요한 금액들") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentDTO { + + @ApiModelProperty(value = "보증금", example = "50000") + private int depositPrice; + + @ApiModelProperty(value = "월세", example = "50000") + private int monthlyRent; + + public static PaymentDTO toDTO(ContractMongoDocument document) { + return PaymentDTO.builder() + .depositPrice(document.getDepositPrice()) + .monthlyRent(document.getMonthlyRent()) + .build(); + } +} diff --git a/src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java b/src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java new file mode 100644 index 00000000..34d8cdd9 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java @@ -0,0 +1,28 @@ +package org.scoula.domain.contract.dto; + +import java.util.List; + +import io.swagger.annotations.ApiModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ApiModel(description = "적법성 검사 후 수정할 특약사항") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SpecialContractDTO { + private List specialClauses; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SpecialClauseDTO { + private Integer order; + private String title; + private String content; + } +} From 38955a58d7cda651983b1e88d4cba1ffdedec8cf Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:30:56 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=20mapper=20/=20xml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/mapper/ContractMapper.java | 30 +++++ .../domain/contract/mapper/ContractMapper.xml | 126 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java create mode 100644 src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml diff --git a/src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java b/src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java new file mode 100644 index 00000000..5d62fbcb --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java @@ -0,0 +1,30 @@ +package org.scoula.domain.contract.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.scoula.domain.contract.dto.ContractDTO; + +@Mapper +public interface ContractMapper { + + ContractDTO getContract(@Param("contractChatId") Long contractChatId); + + String getDuration(@Param("contractChatId") Long contractChatId); + + int updateStatus(@Param("contractChatId") Long contractChatId, @Param("step") int step); + + Long selectFinalContractId(@Param("contractChatId") Long contractChatId); + + int insertSignatureInit(@Param("contractChatId") Long contractChatId); + + int updateTaxSignature( + @Param("finalContractId") Long finalContractId, + @Param("url") String url, + @Param("hashKey") String hashKey); + + int insertFinalContract(@Param("contractChatId") Long contractChatId); + + String selectOwnerTaxSignatureUrl(@Param("finalContractId") Long finalContractId); + + boolean getDepositAdjustment(@Param("contractChatId") Long contractChatId); +} diff --git a/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml b/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml new file mode 100644 index 00000000..736e5cb3 --- /dev/null +++ b/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + UPDATE contract_chat + SET status = #{step} + WHERE contract_chat_id = #{contractChatId} + + + + + + INSERT INTO electronic_signature (contract_id, identity_verification_id, owner_id, buyer_id, created_at) + SELECT fc.contract_id, iv.identity_id, cc.owner_id, cc.buyer_id, NOW() + FROM contract_chat cc + INNER JOIN final_contract fc + ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id + LEFT JOIN identity_verification iv + ON iv.contract_id = cc.contract_chat_id + WHERE cc.contract_chat_id = 4; + + + + UPDATE electronic_signature + SET owner_tax_signature_file_url = #{url}, owner_tax_file_hash = #{hashKey}, owner_tax_signed_at = NOW() + WHERE contract_id = #{finalContractId} + + + + INSERT INTO final_contract ( + home_id, + owner_id, + buyer_id, + contract_pdf_url, + contract_pdf_hash, + contract_date, + contract_expire_date, + owner_identity_verified_at, + buyer_identity_verified_at, + owner_signed_at, + buyer_signed_at, + deposit_price, + monthly_rent, + maintenance_fee, + created_at + ) + SELECT + cc.home_id, + cc.owner_id, + cc.buyer_id, + #{contractPdfUrl}, + #{contractPdfHash}, + #{contractDate}, + #{contractExpireDate}, + oiv.identity_verified_at, + biv.identity_verified_at, + es.owner_signed_at, + bs.buyer_signed_at, + h.deposit_price, + h.monthly_rent, + h.maintenance_fee, + NOW() + FROM contract_chat cc + LEFT JOIN home h ON cc.home_id = h.home_id + LEFT JOIN identity_verification oiv ON oiv.user_id = cc.owner_id AND oiv.contract_id = cc.contract_chat_id + LEFT JOIN identity_verification biv ON biv.user_id = cc.buyer_id AND biv.contract_id = cc.contract_chat_id + LEFT JOIN electronic_signature es ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id + WHERE cc.contract_chat_id = #{contractChatId} + + + + + + \ No newline at end of file From 68445f07dca2225a7a7c2c7c836f2754a3426fb3 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:31:30 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=20=EB=AA=BD=EA=B3=A0=20DB=20repository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ContractMongoRepository.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java diff --git a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java new file mode 100644 index 00000000..b50903ad --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -0,0 +1,138 @@ +package org.scoula.domain.contract.repository; + +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import org.scoula.domain.contract.document.ContractMongoDocument; +import org.scoula.domain.contract.document.FinalSpecialContractDocument; +import org.scoula.domain.contract.dto.ContractDTO; +import org.scoula.domain.contract.dto.PaymentDTO; +import org.scoula.domain.contract.dto.SpecialContractDTO; +import org.scoula.domain.contract.exception.ContractException; +import org.scoula.global.common.exception.BusinessException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class ContractMongoRepository { + + @Autowired private MongoTemplate mongoTemplate; + + public ContractMongoDocument saveContractMongo(ContractDTO dto, LocalDate contractEndDate) { + ContractMongoDocument document = ContractMongoDocument.toDocument(dto, contractEndDate); + return mongoTemplate.insert(document); + } + + public ContractMongoDocument getContract(Long contractChatId) { + Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); + ContractMongoDocument document = + mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); + + return document; + } + + public ContractMongoDocument getDepositPrice(Long contractChatId) { + // ContractMongoDocument document = mongoTemplate.findById(contractChatId, + // ContractMongoDocument.class); + Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); + ContractMongoDocument document = + mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); + return document; + } + + public void updateDepositPrice(Long contractChatId, PaymentDTO dto) { + Query contractQuery = new Query(Criteria.where("contractChatId").is(contractChatId)); + ContractMongoDocument document = + mongoTemplate.findOne(contractQuery, ContractMongoDocument.class); + + if (document == null) { + throw new BusinessException(ContractException.CONTRACT_GET); + } + + document.setDepositPrice(dto.getDepositPrice()); + document.setMonthlyRent(dto.getMonthlyRent()); + + mongoTemplate.save(document); + } + + public void saveSpecialContract(Long contractChatId) { + // Step 1: 특약서 조회 + Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); + FinalSpecialContractDocument specialDoc = + mongoTemplate.findOne(query, FinalSpecialContractDocument.class); + + if (specialDoc == null + || specialDoc.getFinalClauses() == null + || specialDoc.getFinalClauses().isEmpty()) { + throw new BusinessException(ContractException.CONTRACT_GET); + } + + // Step 2: 기존 계약서 가져오기 + 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); + } + + // Step 3: 특약사항 매핑 + List finalClauses = specialDoc.getFinalClauses(); + finalClauses.sort(Comparator.comparing(FinalSpecialContractDocument.FinalClause::getOrder)); + + List specialClauses = + finalClauses.stream() + .map( + fc -> + ContractMongoDocument.SpecialContract.builder() + .order(fc.getOrder() + 1) + .title(fc.getTitle()) + .content(fc.getContent()) + .build()) + .collect(Collectors.toList()); + + contractDoc.setSpecialContracts(specialClauses); + + // Step 4: 저장 + mongoTemplate.save(contractDoc); + } + + public void updateSpecialContract(Long contractChatId, SpecialContractDTO dto) { + Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); + ContractMongoDocument document = mongoTemplate.findOne(query, ContractMongoDocument.class); + + if (document == null) { + throw new BusinessException(ContractException.CONTRACT_GET); + } + + List existingClauses = + document.getSpecialContracts(); + if (existingClauses == null || existingClauses.isEmpty()) { + throw new BusinessException(ContractException.CONTRACT_GET, "특약사항이 존재하지 않습니다."); + } + + List newClauses = dto.getSpecialClauses(); + if (newClauses == null || newClauses.isEmpty()) return; + + for (SpecialContractDTO.SpecialClauseDTO newClause : newClauses) { + Integer order = newClause.getOrder(); + if (order != null && order >= 0 && order < existingClauses.size()) { + ContractMongoDocument.SpecialContract target = existingClauses.get(order); + if (newClause.getContent() != null) { + target.setContent(newClause.getContent()); + } + if (newClause.getTitle() != null) { + target.setTitle(newClause.getTitle()); + } + } + } + + document.setSpecialContracts(existingClauses); + mongoTemplate.save(document); + } +} From d4e7d4de12cd08b5123a02b6d0f350b860094a2e Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:36:18 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=20config=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/scoula/global/config/DatabaseConfig.java | 4 +++- src/main/java/org/scoula/global/config/RootConfig.java | 3 ++- src/main/java/org/scoula/global/config/ServletConfig.java | 3 ++- .../java/org/scoula/global/mongodb/config/MongoConfig.java | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scoula/global/config/DatabaseConfig.java b/src/main/java/org/scoula/global/config/DatabaseConfig.java index 321d3344..fe50a550 100644 --- a/src/main/java/org/scoula/global/config/DatabaseConfig.java +++ b/src/main/java/org/scoula/global/config/DatabaseConfig.java @@ -28,7 +28,9 @@ "org.scoula.domain.precontract.mapper", "org.scoula.domain.chat.mapper", "org.scoula.domain.home.mapper", - "org.scoula.domain.mypage.mapper" + "org.scoula.domain.mypage.mapper", + "org.scoula.domain.chat.mapper", + "org.scoula.domain.contract.mapper" }) @RequiredArgsConstructor public class DatabaseConfig { diff --git a/src/main/java/org/scoula/global/config/RootConfig.java b/src/main/java/org/scoula/global/config/RootConfig.java index a688e3cc..48d7a6e4 100644 --- a/src/main/java/org/scoula/global/config/RootConfig.java +++ b/src/main/java/org/scoula/global/config/RootConfig.java @@ -50,7 +50,8 @@ "org.scoula.domain.fraud.service", "org.scoula.domain.precontract.service", "org.scoula.domain.home.service", - "org.scoula.domain.mypage.service" + "org.scoula.domain.mypage.service", + "org.scoula.domain.contract.service" }) public class RootConfig { // 각 도메인별 설정은 별도의 Config 클래스로 분리됨 diff --git a/src/main/java/org/scoula/global/config/ServletConfig.java b/src/main/java/org/scoula/global/config/ServletConfig.java index a1879a10..1ffa9c46 100644 --- a/src/main/java/org/scoula/global/config/ServletConfig.java +++ b/src/main/java/org/scoula/global/config/ServletConfig.java @@ -33,7 +33,8 @@ "org.scoula.global.oauth2.controller", "org.scoula.domain.precontract.controller", "org.scoula.domain.home.controller", - "org.scoula.domain.mypage.controller" + "org.scoula.domain.mypage.controller", + "org.scoula.domain.contract.controller" }) @RequiredArgsConstructor public class ServletConfig implements WebMvcConfigurer { diff --git a/src/main/java/org/scoula/global/mongodb/config/MongoConfig.java b/src/main/java/org/scoula/global/mongodb/config/MongoConfig.java index 66ef0d6d..717e26a5 100644 --- a/src/main/java/org/scoula/global/mongodb/config/MongoConfig.java +++ b/src/main/java/org/scoula/global/mongodb/config/MongoConfig.java @@ -28,7 +28,8 @@ @ComponentScan( basePackages = { "org.scoula.domain.chat.repository", - "org.scoula.domain.precontract.repository" + "org.scoula.domain.precontract.repository", + "org.scoula.domain.contract.repository", }) @Log4j2 @EnableTransactionManagement From 1881ab429e9c1ef167d43ca29915631e7fc1173e Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:37:54 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=20=EA=B8=B0=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scoula/domain/precontract/enums/ContractDuration.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java b/src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java index e1f570d4..70905c5f 100644 --- a/src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java +++ b/src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java @@ -8,8 +8,8 @@ public enum ContractDuration { YEAR_1("1년 계약"), YEAR_2("2년 계약"), - YEAR_3("3년 계약 "), - YEAR_4("4년 계약 "), - YEAR_5("5년 계약 "); + YEAR_3("3년 계약"), + YEAR_4("4년 계약"), + YEAR_5("5년 계약"); private final String displayName; } From 33fc274e8191a76726fd6925bff67f74d65610cd Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 15:39:54 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=84=9C=200,1,2=20=EB=8B=A8=EA=B3=84=20service=20/=20servicei?= =?UTF-8?q?mpl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/service/ContractService.java | 120 ++++ .../contract/service/ContractServiceImpl.java | 514 ++++++++++++++++++ 2 files changed, 634 insertions(+) create mode 100644 src/main/java/org/scoula/domain/contract/service/ContractService.java create mode 100644 src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java diff --git a/src/main/java/org/scoula/domain/contract/service/ContractService.java b/src/main/java/org/scoula/domain/contract/service/ContractService.java new file mode 100644 index 00000000..7930864e --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/service/ContractService.java @@ -0,0 +1,120 @@ +package org.scoula.domain.contract.service; + +import javax.servlet.http.HttpServletResponse; + +import org.scoula.domain.contract.dto.*; +import org.springframework.web.multipart.MultipartFile; + +public interface ContractService { + + /** + * step0. 임차인이 임대인을 기다릴때 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + */ + Void standByContract(Long contractChatId, Long userId); + + /** + * step1 (init) 계약서에 필요한 항목들을 가져와서 몽고 DB에 계약서 만들기 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + */ + Void saveContractMongo(Long contractChatId, Long userId); + + /** + * step1 start 계약서 가져오기 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + * @return 계약서 내용을 보내기 + */ + ContractDTO getContract(Long contractChatId, Long userId); + + /** + * step1 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + * @return 계약서 내용을 보내기 + */ + Void getContractNext(Long contractChatId, Long userId); + + /** + * step1 finish + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + * @return 계약서 내용을 보내기 + */ + Boolean nextStep(Long contractChatId, Long userId, NextStepDTO dto); + + /** + * step2 start 금액을 조율하기 위해 금액을 조회 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + */ + PaymentDTO getDepositPrice(Long contractChatId, Long userId); + + /** + * step2 금액을 레디스에 저장한다. + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + * @param dto 변경된 금액값 + */ + Void saveDepositPrice(Long contractChatId, Long userId, PaymentDTO dto); + + /** + * step2 금액을 레디스에서 삭제한다. + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + */ + Void deleteDepositPrice(Long contractChatId, Long userId); + + /** + * step2 finish 금액을 레디스에서 삭제하고, DB에 저장한다. + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + */ + Void updateDepositPrice(Long contractChatId, Long userId); + + /** + * step4 (init) 계약서를 AI로 보내고, 적법성 받기 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + * @return AI가 계약서를 보고 주는 적법성을 리턴값으로 보내기 + */ + LegalityDTO getLegality(Long contractChatId, Long userId); + + /** + * step4 start 특약을 개약 테이블에 저장하기 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 + */ + Void saveSpecialContract(Long contractChatId, Long userId); + + /** + * step4 적법성 검사 후 수정된 특약으로 변경 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 @Param dto 변경된 특약 + */ + Void updateSpecialContract(Long contractChatId, Long userId, SpecialContractDTO dto); + + /** + * step4 finish 적법성 검사 후 다음단계로 넘어가기 + * + * @param contractChatId 채팅방 아이디 + * @param userId 유저 아이디 @Parma step 계약서 단계 + */ + Void sendStep4(Long contractChatId, Long userId); + + +} diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java new file mode 100644 index 00000000..1a745467 --- /dev/null +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -0,0 +1,514 @@ +package org.scoula.domain.contract.service; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletResponse; + +import org.scoula.domain.chat.mapper.ContractChatMapper; +import org.scoula.domain.chat.service.ContractChatServiceInterface; +import org.scoula.domain.chat.vo.ContractChat; +import org.scoula.domain.contract.document.ContractMongoDocument; +import org.scoula.domain.contract.dto.*; +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.enums.ContractDuration; +import org.scoula.domain.precontract.exception.PreContractErrorCode; +import org.scoula.domain.precontract.mapper.TenantPreContractMapper; +import org.scoula.global.common.exception.BusinessException; +import org.scoula.global.common.util.UploadFiles; +import org.scoula.global.email.service.EmailServiceImpl; +import org.scoula.global.file.service.S3ServiceImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class ContractServiceImpl implements ContractService { + + private final ContractChatServiceInterface contractChatService; + + private final ContractMapper contractMapper; + private final ContractMongoRepository repository; + private final RestTemplate restTemplate; + private final TenantPreContractMapper tenantMapper; + private final ContractChatMapper contractChatMapper; + + private final RedisTemplate stringRedisTemplate; + private final S3ServiceImpl s3Service; + private final EmailServiceImpl emailService; + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES"; + private static final String SECRET_KEY = "mySuperSecretKey"; // 16글자 (128bit) ==> 환경변수에 넣기 + + @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) { + // userId 검증 + validateUserId(contractChatId, userId); + + // 이미 생성된 계약 문서가 있으면 저장 대신 안내 메시지 전송 후 종료 + ContractMongoDocument existing = repository.getContract(contractChatId); + if (existing != null) { + contractChatService.AiMessage(contractChatId, """ + 이미 생성된 계약서가 있어요. + 기존 계약서를 불러올게요. + """); + 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); + + LocalDate startDate = dto.getContractStartDate(); + + LocalDate contractEndDate = null; + if (duration == ContractDuration.YEAR_1) { + contractEndDate = startDate.plusYears(1); + } else if (duration == ContractDuration.YEAR_2) { + contractEndDate = startDate.plusYears(2); + } else if (duration == ContractDuration.YEAR_3) { + contractEndDate = startDate.plusYears(3); + } else if (duration == ContractDuration.YEAR_4) { + contractEndDate = startDate.plusYears(4); + } else if (duration == ContractDuration.YEAR_5) { + contractEndDate = startDate.plusYears(5); + } + + // mongoDB에 contract 도큐멘트를 만들어서 저장한다. + ContractMongoDocument document = repository.saveContractMongo(dto, contractEndDate); + if (document == null) { + throw new BusinessException(ContractException.CONTRACT_INSERT); + } + return null; + } + + /** {@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); + + // id로 Repository에서 값을 찾는다 + ContractMongoDocument document = repository.getContract(contractChatId); + if (document == null) { + throw new BusinessException(ContractException.CONTRACT_GET); + } + + // 찾은 값을 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.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(); + + // 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); + } + } + + /** {@inheritDoc} */ + @Override + public PaymentDTO getDepositPrice(Long contractChatId, Long userId) { + + // 스텝 변경 + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2); + + // 다음 단계 메세지 보내기 + contractChatService.AiMessage(contractChatId, "이번 단계는 '금액 조율' 단계입니다"); + + ContractMongoDocument doc = repository.getContract(contractChatId); + AIMessageDTO aiDto = AIMessageDTO.toDTO(doc); + + long contract = ChronoUnit.YEARS.between(aiDto.getContractStartDate(), aiDto.getContractEndDate()); + String rentType = tenantMapper.selectRentType(contractChatId, userId) + .orElseThrow(() -> new BusinessException(ContractException.CONTRACT_GET, "전/월세 타입 조회 실패")); + // 시작 메세지 보내기 + contractChatService.AiMessage( + contractChatId, + """ + 다음은 2단계: ‘금액 조율’ 단계입니다. + + 이 계약은 계약기간 %d년의 %s 계약입니다. + 전세 보증금은 %s, + 관리비는 %s입니다. + """.formatted( + contract, + rentType, + formatWonShort(aiDto.getDepositPrice()), + formatWonShort(aiDto.getMaintenanceFee()))); + + contractChatService.AiMessage( + contractChatId, """ + 자유롭게 채팅 후 임대인(%s)님께서 금액을 조정해주세요. 임차인(%s)님이 수락 후 해당 조건의 확정이 가능합니다. + """.formatted(aiDto.getBuyerName(), aiDto.getOwnerName())); + + // userId 검증 + validateUserId(contractChatId, userId); + + // MongoDB에서 보증금, 계약금, 잔금, 월세를 조회한다 + ContractMongoDocument document = repository.getDepositPrice(contractChatId); + if (document == null) { + throw new BusinessException(ContractException.CONTRACT_GET); + } + + // 조회된 금액을 리턴한다. + PaymentDTO dto = PaymentDTO.toDTO(document); + return dto; + } + + /** {@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); + + // 4. Redis에 저장 + stringRedisTemplate.opsForValue().set(redisKey, json); + + } catch (JsonProcessingException e) { + throw new BusinessException(ContractException.CONTRACt_REDIS, e); + } + + return null; + } + + /** {@inheritDoc} */ + @Override + public Void deleteDepositPrice(Long contractChatId, Long userId) { + // userId 검증 + validateUserId(contractChatId, userId); + + // 레디스에 내용 삭제하기 + // Redis key 정의 + String redisKey = "contract:payment:" + contractChatId; + String json = stringRedisTemplate.opsForValue().get(redisKey); + + if (json == null) { + throw new BusinessException(ContractException.CONTRACt_REDIS, "금액 정보가 Redis에 없습니다."); + } + + // Redis에서 삭제 + stringRedisTemplate.delete(redisKey); + + return null; + } + + /** {@inheritDoc} */ + @Override + public Void updateDepositPrice(Long contractChatId, Long userId) { + // Userid 검증 + validateUserId(contractChatId, userId); + + // 2. Redis에서 해당 금액 정보 가져오기 + String redisKey = "contract:payment:" + contractChatId; // value : 임대인 id -> 거절시 Delete + String json = stringRedisTemplate.opsForValue().get(redisKey); + + if (json == null) { + throw new BusinessException(ContractException.CONTRACt_REDIS, "금액 정보가 Redis에 없습니다."); + } + + try { + // 3. JSON -> DTO로 변환 + ObjectMapper objectMapper = new ObjectMapper(); + PaymentDTO dto = objectMapper.readValue(json, PaymentDTO.class); + + // 4. MongoDB에서 계약서 불러오기 + repository.updateDepositPrice(contractChatId, dto); + + // 7. Redis 값 삭제 + stringRedisTemplate.delete(redisKey); + + } catch (Exception e) { + throw new BusinessException(ContractException.CONTRACT_UPDATE, e); + } + + return null; + } + + /** {@inheritDoc} */ + @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); + } + ContractDTO dto = ContractDTO.toDTO(document); + + // 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 { + log.error(response.getBody()); + throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR); + } + + } catch (Exception e) { + log.error(e.getMessage()); + throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR, e); + } + } + + /** {@inheritDoc} */ + @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} */ + @Override + public Void updateSpecialContract(Long contractChatId, Long userId, SpecialContractDTO dto) { + // userId 검증 + validateUserId(contractChatId, userId); + + // 해당 번호에 맞는 특약을 계약서 몽고 DB에 update해서 수정한다. + try { + repository.updateSpecialContract(contractChatId, dto); + } catch (Exception e) { + throw new BusinessException(ContractException.CONTRACT_UPDATE); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public Void sendStep4(Long contractChatId, Long userId) { + return null; + } + + // Userid 검증 + public void validateUserId(Long contractChatId, Long userId) { + Long buyerId = + tenantMapper + .selectContractBuyerId(contractChatId) + .orElseThrow(() -> new BusinessException(PreContractErrorCode.TENANT_USER)); + + if (!userId.equals(buyerId)) { + throw new BusinessException(PreContractErrorCode.TENANT_USER); + } + } + + 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("만"); + 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 { + sb.append(man).append("만원"); + } + } + return sb.toString().replaceAll("\\s+", " "); + } +} From 998cad41a320a6a5c3b9d3a13679385673797e2c Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 16:58:47 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Http=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/controller/ContractControllerImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 042cb134..ed9997bb 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -23,7 +23,7 @@ public class ContractControllerImpl implements ContractController { private final ContractService service; @Override - @GetMapping("/standBy") + @PostMapping("/standBy") public ResponseEntity> standByContract( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { @@ -43,7 +43,7 @@ public ResponseEntity> saveContractMongo( } @Override - @GetMapping("") + @PostMapping("/getContract") public ResponseEntity> getContract( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { @@ -52,7 +52,7 @@ public ResponseEntity> getContract( } @Override - @GetMapping("/step1") + @PostMapping("/step1") public ResponseEntity> getContractNext( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { @@ -73,7 +73,7 @@ public ResponseEntity> nextStep( } @Override - @GetMapping("/price") + @PostMapping("/getPrice") public ResponseEntity> getDepositPrice( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails) { From 3b34b7f9b66d26ec0127f68e20f2f687263c8dc1 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Mon, 11 Aug 2025 17:04:33 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=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 | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 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 1a745467..c39c8b0b 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -416,15 +416,25 @@ public LegalityDTO getLegality(Long contractChatId, Long userId) { // 반환값을 받아오고, 그 값을 프론트에 넘겨준다. ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class); - LegalityDTO res = response.getBody(); + LegalityDTO res = response.getBody(); assert res != null; - log.warn("AI 응답 값 확인: {}",res.toString()); + log.warn("AI 응답 값 확인: {}", res.toString()); - log.warn("AI 응답 헤더 확인: {}",response.getStatusCode()); + log.warn("AI 응답 헤더 확인: {}", response.getStatusCode()); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { return response.getBody(); } else { - log.error(response.getBody()); + // 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); } From b90c82121329cd484ece747fcb7851fe0ae966c2 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Tue, 12 Aug 2025 09:10:23 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=86=EB=8A=94=20Import=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ContractController.java | 9 ++---- .../controller/ContractControllerImpl.java | 8 ++--- .../contract/service/ContractService.java | 3 -- .../contract/service/ContractServiceImpl.java | 30 ++++--------------- 4 files changed, 10 insertions(+), 40 deletions(-) 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 666bc03d..049e5d2a 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractController.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractController.java @@ -1,7 +1,7 @@ package org.scoula.domain.contract.controller; -import javax.servlet.http.HttpServletResponse; - +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; import org.scoula.domain.contract.dto.*; import org.scoula.global.auth.dto.CustomUserDetails; import org.scoula.global.common.dto.ApiResponse; @@ -9,11 +9,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.multipart.MultipartFile; - -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; @Api(tags = "계약서 API", description = "계약서 : 정보확인 / 금액 조율 / 적법성 확인") public interface ContractController { 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 ed9997bb..35831c4d 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -1,7 +1,7 @@ package org.scoula.domain.contract.controller; -import javax.servlet.http.HttpServletResponse; - +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.scoula.domain.contract.dto.*; import org.scoula.domain.contract.service.ContractService; import org.scoula.global.auth.dto.CustomUserDetails; @@ -9,10 +9,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; @RestController @Log4j2 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 7930864e..8896968a 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractService.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractService.java @@ -1,9 +1,6 @@ package org.scoula.domain.contract.service; -import javax.servlet.http.HttpServletResponse; - import org.scoula.domain.contract.dto.*; -import org.springframework.web.multipart.MultipartFile; public interface ContractService { 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 c39c8b0b..618845c4 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -1,22 +1,9 @@ package org.scoula.domain.contract.service; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.security.MessageDigest; -import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.Map; -import java.util.Set; - -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; -import javax.servlet.http.HttpServletResponse; - +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.scoula.domain.chat.mapper.ContractChatMapper; import org.scoula.domain.chat.service.ContractChatServiceInterface; import org.scoula.domain.chat.vo.ContractChat; @@ -29,7 +16,6 @@ import org.scoula.domain.precontract.exception.PreContractErrorCode; import org.scoula.domain.precontract.mapper.TenantPreContractMapper; import org.scoula.global.common.exception.BusinessException; -import org.scoula.global.common.util.UploadFiles; import org.scoula.global.email.service.EmailServiceImpl; import org.scoula.global.file.service.S3ServiceImpl; import org.springframework.beans.factory.annotation.Value; @@ -37,13 +23,9 @@ import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import org.springframework.web.multipart.MultipartFile; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; @Service @RequiredArgsConstructor From 8e65ad00f712add1ceffbb7e3e8a6001ec930900 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Tue, 12 Aug 2025 09:56:56 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/service/ChatServiceImpl.java | 11 +- .../chat/service/ChatServiceInterface.java | 2 +- .../chat/service/ContractChatServiceImpl.java | 251 +++++++++--------- .../controller/ContractController.java | 9 +- .../controller/ContractControllerImpl.java | 8 +- .../FinalSpecialContractDocument.java | 34 --- .../domain/contract/dto/AIMessageDTO.java | 4 +- .../domain/contract/dto/FinalContractDTO.java | 2 +- ...DTO.java => SpecialContractUpdateDTO.java} | 2 +- .../contract/exception/ContractException.java | 5 +- .../repository/ContractMongoRepository.java | 10 +- .../contract/service/ContractService.java | 4 +- .../contract/service/ContractServiceImpl.java | 14 +- .../domain/contract/mapper/ContractMapper.xml | 6 +- 14 files changed, 163 insertions(+), 199 deletions(-) delete mode 100644 src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java rename src/main/java/org/scoula/domain/contract/dto/{SpecialContractDTO.java => SpecialContractUpdateDTO.java} (94%) 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..368f70ab 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( 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); } 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..f12f059d 100644 --- a/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java +++ b/src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java @@ -133,22 +133,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 +281,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,7 +630,6 @@ public void createNextRoundSpecialContractDocument( .orElseThrow( () -> new IllegalArgumentException("현재 라운드의 특약 문서를 찾을 수 없습니다")); - Long newRound = currentRound + 1; log.info("새 라운드: {} → {}", currentRound, newRound); @@ -1581,107 +1578,113 @@ private List findRejectedOrders( return rejectedOrders; } + @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("완료된 특약이 없습니다."); + } - @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; - } + 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; + } private Optional findLatestRoundForOrder( Long contractChatId, Long order) { 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 049e5d2a..19d1623e 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractController.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractController.java @@ -1,7 +1,5 @@ package org.scoula.domain.contract.controller; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; import org.scoula.domain.contract.dto.*; import org.scoula.global.auth.dto.CustomUserDetails; import org.scoula.global.common.dto.ApiResponse; @@ -10,6 +8,9 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + @Api(tags = "계약서 API", description = "계약서 : 정보확인 / 금액 조율 / 적법성 확인") public interface ContractController { @@ -78,12 +79,10 @@ ResponseEntity> saveSpecialContract( ResponseEntity> updateSpecialContract( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody SpecialContractDTO dto); + @RequestBody SpecialContractUpdateDTO dto); @ApiOperation(value = "적법성 검사 후 다음단계로 넘어가기", notes = "적법성 검사 후 AI 메세지를 보낸다") ResponseEntity> sendStep4( @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 35831c4d..08a667bf 100644 --- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java +++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java @@ -1,7 +1,5 @@ package org.scoula.domain.contract.controller; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; import org.scoula.domain.contract.dto.*; import org.scoula.domain.contract.service.ContractService; import org.scoula.global.auth.dto.CustomUserDetails; @@ -10,6 +8,9 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + @RestController @Log4j2 @RequiredArgsConstructor @@ -133,7 +134,7 @@ public ResponseEntity> saveSpecialContract( public ResponseEntity> updateSpecialContract( @PathVariable Long contractChatId, @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestBody SpecialContractDTO dto) { + @RequestBody SpecialContractUpdateDTO dto) { return ResponseEntity.ok( ApiResponse.success( service.updateSpecialContract( @@ -148,5 +149,4 @@ public ResponseEntity> sendStep4( return ResponseEntity.ok( ApiResponse.success(service.sendStep4(contractChatId, userDetails.getUserId()))); } - } diff --git a/src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java b/src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java deleted file mode 100644 index cbc62a54..00000000 --- a/src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.scoula.domain.contract.document; - -import java.util.List; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Document(collection = "FINAL_SPECIAL_CONTRACT") -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class FinalSpecialContractDocument { - @Id private String id; - private Long contractChatId; - private Integer totalFinalClauses; - private List finalClauses; - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class FinalClause { - private Integer order; - private String title; - private String content; - private Long sourceRound; - } -} diff --git a/src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java b/src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java index 9a7f1ac7..f15d2e25 100644 --- a/src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java @@ -16,9 +16,9 @@ @NoArgsConstructor @AllArgsConstructor public class AIMessageDTO { - // 임차인 이름 - private String ownerName; // 임대인 이름 + private String ownerName; + // 임차인 이름 private String buyerName; // 계약 기간 diff --git a/src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java b/src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java index e28746c7..05838e4a 100644 --- a/src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java @@ -20,7 +20,7 @@ public class FinalContractDTO { private MultipartFile ownerContractSignature; private MultipartFile buyerContractSignature; - private Boolean mediation_agree; // 조정 동의 여부 + private Boolean mediationAgree; // 조정 동의 여부 private String contractKey; // 계약서 비밀번호 } diff --git a/src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java b/src/main/java/org/scoula/domain/contract/dto/SpecialContractUpdateDTO.java similarity index 94% rename from src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java rename to src/main/java/org/scoula/domain/contract/dto/SpecialContractUpdateDTO.java index 34d8cdd9..02a405da 100644 --- a/src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java +++ b/src/main/java/org/scoula/domain/contract/dto/SpecialContractUpdateDTO.java @@ -13,7 +13,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class SpecialContractDTO { +public class SpecialContractUpdateDTO { private List specialClauses; @Data diff --git a/src/main/java/org/scoula/domain/contract/exception/ContractException.java b/src/main/java/org/scoula/domain/contract/exception/ContractException.java index c03f201a..8d54bb84 100644 --- a/src/main/java/org/scoula/domain/contract/exception/ContractException.java +++ b/src/main/java/org/scoula/domain/contract/exception/ContractException.java @@ -9,13 +9,14 @@ @Getter @RequiredArgsConstructor public enum ContractException implements IErrorCode { - CONTRACT_GET("CONTRACT_4001", HttpStatus.BAD_REQUEST, "mongoDB에서 값을 조회하지 못 햇습니다."), + CONTRACT_GET("CONTRACT_4001", HttpStatus.BAD_REQUEST, "mongoDB에서 값을 조회하지 못 했습니다."), CONTRACT_INSERT("CONTRACT_4002", HttpStatus.BAD_REQUEST, "MongoDB에 저장이 되지 않았습니다."), CONTRACT_AI_SERVER_ERROR( "CONTRACT_4003", HttpStatus.SERVICE_UNAVAILABLE, "AI 서버 통신 중 오류가 발생했습니다."), CONTRACT_UPDATE("CONTRACT_4004", HttpStatus.BAD_REQUEST, "MongoDB에 수정이 되지 않았습니다"), - CONTRACt_REDIS("CONTRACT_4005", HttpStatus.BAD_REQUEST, "REDIS에 해당 정보가 없습니다."); + CONTRACT_REDIS("CONTRACT_4005", HttpStatus.BAD_REQUEST, "REDIS에 해당 정보가 없습니다."); private final String code; private final HttpStatus httpStatus; private final String message; } + 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 b50903ad..ed9f4fa7 100644 --- a/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java +++ b/src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java @@ -5,11 +5,11 @@ import java.util.List; import java.util.stream.Collectors; +import org.scoula.domain.chat.document.FinalSpecialContractDocument; import org.scoula.domain.contract.document.ContractMongoDocument; -import org.scoula.domain.contract.document.FinalSpecialContractDocument; import org.scoula.domain.contract.dto.ContractDTO; import org.scoula.domain.contract.dto.PaymentDTO; -import org.scoula.domain.contract.dto.SpecialContractDTO; +import org.scoula.domain.contract.dto.SpecialContractUpdateDTO; import org.scoula.domain.contract.exception.ContractException; import org.scoula.global.common.exception.BusinessException; import org.springframework.beans.factory.annotation.Autowired; @@ -102,7 +102,7 @@ public void saveSpecialContract(Long contractChatId) { mongoTemplate.save(contractDoc); } - public void updateSpecialContract(Long contractChatId, SpecialContractDTO dto) { + public void updateSpecialContract(Long contractChatId, SpecialContractUpdateDTO dto) { Query query = new Query(Criteria.where("contractChatId").is(contractChatId)); ContractMongoDocument document = mongoTemplate.findOne(query, ContractMongoDocument.class); @@ -116,10 +116,10 @@ public void updateSpecialContract(Long contractChatId, SpecialContractDTO dto) { throw new BusinessException(ContractException.CONTRACT_GET, "특약사항이 존재하지 않습니다."); } - List newClauses = dto.getSpecialClauses(); + List newClauses = dto.getSpecialClauses(); if (newClauses == null || newClauses.isEmpty()) return; - for (SpecialContractDTO.SpecialClauseDTO newClause : newClauses) { + for (SpecialContractUpdateDTO.SpecialClauseDTO newClause : newClauses) { Integer order = newClause.getOrder(); if (order != null && order >= 0 && order < existingClauses.size()) { ContractMongoDocument.SpecialContract target = existingClauses.get(order); 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 8896968a..a0380765 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractService.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractService.java @@ -103,7 +103,7 @@ public interface ContractService { * @param contractChatId 채팅방 아이디 * @param userId 유저 아이디 @Param dto 변경된 특약 */ - Void updateSpecialContract(Long contractChatId, Long userId, SpecialContractDTO dto); + Void updateSpecialContract(Long contractChatId, Long userId, SpecialContractUpdateDTO dto); /** * step4 finish 적법성 검사 후 다음단계로 넘어가기 @@ -112,6 +112,4 @@ public interface ContractService { * @param userId 유저 아이디 @Parma step 계약서 단계 */ Void sendStep4(Long contractChatId, Long userId); - - } 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 618845c4..71fcb65f 100644 --- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java +++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java @@ -44,10 +44,6 @@ public class ContractServiceImpl implements ContractService { private final S3ServiceImpl s3Service; private final EmailServiceImpl emailService; - private static final String ALGORITHM = "AES"; - private static final String TRANSFORMATION = "AES"; - private static final String SECRET_KEY = "mySuperSecretKey"; // 16글자 (128bit) ==> 환경변수에 넣기 - @Value("${ai.server.url:http://localhost:8000}") private String aiServerUrl; @@ -243,7 +239,7 @@ public Boolean nextStep(Long contractChatId, Long userId, NextStepDTO dto) { stringRedisTemplate.opsForValue().set(redisKey, updatedJson); return false; } catch (Exception e) { - throw new BusinessException(ContractException.CONTRACt_REDIS, e); + throw new BusinessException(ContractException.CONTRACT_REDIS, e); } } @@ -314,7 +310,7 @@ public Void saveDepositPrice(Long contractChatId, Long userId, PaymentDTO dto) { stringRedisTemplate.opsForValue().set(redisKey, json); } catch (JsonProcessingException e) { - throw new BusinessException(ContractException.CONTRACt_REDIS, e); + throw new BusinessException(ContractException.CONTRACT_REDIS, e); } return null; @@ -332,7 +328,7 @@ public Void deleteDepositPrice(Long contractChatId, Long userId) { String json = stringRedisTemplate.opsForValue().get(redisKey); if (json == null) { - throw new BusinessException(ContractException.CONTRACt_REDIS, "금액 정보가 Redis에 없습니다."); + throw new BusinessException(ContractException.CONTRACT_REDIS, "금액 정보가 Redis에 없습니다."); } // Redis에서 삭제 @@ -352,7 +348,7 @@ public Void updateDepositPrice(Long contractChatId, Long userId) { String json = stringRedisTemplate.opsForValue().get(redisKey); if (json == null) { - throw new BusinessException(ContractException.CONTRACt_REDIS, "금액 정보가 Redis에 없습니다."); + throw new BusinessException(ContractException.CONTRACT_REDIS, "금액 정보가 Redis에 없습니다."); } try { @@ -446,7 +442,7 @@ public Void saveSpecialContract(Long contractChatId, Long userId) { /** {@inheritDoc} */ @Override - public Void updateSpecialContract(Long contractChatId, Long userId, SpecialContractDTO dto) { + public Void updateSpecialContract(Long contractChatId, Long userId, SpecialContractUpdateDTO dto) { // userId 검증 validateUserId(contractChatId, userId); 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 736e5cb3..65037f6b 100644 --- a/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml +++ b/src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml @@ -64,11 +64,11 @@ WHERE cc.contract_chat_id = 4; - + UPDATE electronic_signature SET owner_tax_signature_file_url = #{url}, owner_tax_file_hash = #{hashKey}, owner_tax_signed_at = NOW() WHERE contract_id = #{finalContractId} - + INSERT INTO final_contract ( @@ -99,7 +99,7 @@ oiv.identity_verified_at, biv.identity_verified_at, es.owner_signed_at, - bs.buyer_signed_at, + es.buyer_signed_at, h.deposit_price, h.monthly_rent, h.maintenance_fee, From 47537ec9dffe094fff3db696db8a156acd5d3292 Mon Sep 17 00:00:00 2001 From: LeeEunmi Date: Tue, 12 Aug 2025 11:35:49 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=ED=8F=AC=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/scoula/domain/contract/exception/ContractException.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/scoula/domain/contract/exception/ContractException.java b/src/main/java/org/scoula/domain/contract/exception/ContractException.java index 8d54bb84..e2ad06a3 100644 --- a/src/main/java/org/scoula/domain/contract/exception/ContractException.java +++ b/src/main/java/org/scoula/domain/contract/exception/ContractException.java @@ -19,4 +19,3 @@ public enum ContractException implements IErrorCode { private final HttpStatus httpStatus; private final String message; } -