Skip to content

[Feat] 계약서 0,1,2 단계 #76

Merged
MeongW merged 15 commits intodevelopfrom
feat/contract
Aug 12, 2025
Merged

[Feat] 계약서 0,1,2 단계 #76
MeongW merged 15 commits intodevelopfrom
feat/contract

Conversation

@minnieming
Copy link
Contributor

@minnieming minnieming commented Aug 11, 2025

🚀 관련 이슈

🔑 주요 변경사항

계약서 작성
0. 대기

  1. 정보 확인
  2. 금액 조율

✔️ 체크 리스트

  • Merge 하려는 브랜치가 올바른가? (main branch에 실수로 PR 생성 금지)
  • Merge 하려는 PR 및 Commit들을 로컬에서 실행했을 때 에러가 발생하지 않았는가?
  • 라벨을 등록했는가?
  • 리뷰어를 지정했는가?

📢 To Reviewers

📸 스크린샷 or 실행영상

↗️ 개선 사항

Summary by CodeRabbit

  • New Features

    • 계약 라이프사이클 전면 추가: 생성·조회·단계 진행·단계별 안내 메시지 제공
    • 보증금·월세 관리: 조회/저장/수정/삭제 및 임시 상태 관리
    • 특약사항 관리: 작성·저장·일괄 수정·라운드 통합 최종본 생성
    • AI 적법성 검사 및 상세 위반 리포트 제공
    • 전자서명·최종계약 관련 서명 업로드·동의·비밀번호 입력 지원
  • Chores

    • 애플리케이션 컴포넌트·매퍼·리포지토리 스캔 범위 확장으로 신규 모듈 활성화
    • 구성 서브모듈 포인터 업데이트

@minnieming minnieming requested a review from MeongW August 11, 2025 07:23
@minnieming minnieming self-assigned this Aug 11, 2025
@minnieming minnieming added the ✨ feature 새로운 기능 요청 label Aug 11, 2025
@minnieming minnieming linked an issue Aug 11, 2025 that may be closed by this pull request
5 tasks
@coderabbitai
Copy link

coderabbitai bot commented Aug 11, 2025

Walkthrough

계약 도메인 신규 모듈을 추가하고 컨트롤러·서비스·리포지토리·매퍼·DTO·예외 등을 도입했습니다. MongoDB에 계약서를 저장·조회·갱신하고 Redis로 금액 조율 상태를 관리하며 외부 AI 서버에 적법성 검증을 요청합니다.

Changes

Cohort / File(s) Summary
Submodule
config-submodule
서브모듈 커밋 포인터 업데이트.
Controller
src/main/java/org/scoula/domain/contract/controller/ContractController.java, src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
계약 REST API 인터페이스 및 구현 추가(13개 엔드포인트). 각 엔드포인트는 ContractService로 위임.
Service
src/main/java/org/scoula/domain/contract/service/ContractService.java, src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
계약 워크플로우 서비스 인터페이스 및 구현 추가. Redis 상태 관리, Mongo 저장/조회, AI 연동, 특약 저장/수정 로직 포함.
Mongo Document & Repository
src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java, src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java
Mongo 문서 모델 추가 및 MongoTemplate 기반 저장/조회/업데이트·특약 처리 로직 구현.
DTOs
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java, .../AIMessageDTO.java, .../PaymentDTO.java, .../LegalityDTO.java, .../NextStepDTO.java, .../FinalContractDTO.java, .../SpecialContractUpdateDTO.java
계약·금액·적법성·단계·최종서명·특약수정 등의 DTO 추가 및 Document↔DTO 변환 메서드.
MyBatis Mapper & XML
src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java, src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
계약 조회·기간조회·상태업데이트·최종계약/전자서명 관련 SQL 매핑 및 메퍼 인터페이스 추가.
Config (Component/Mapper/Mongo Scan)
src/main/java/org/scoula/global/config/DatabaseConfig.java, .../RootConfig.java, .../ServletConfig.java, .../mongodb/config/MongoConfig.java
contract 패키지에 대한 컴포넌트/매퍼/몽고 리포지토리 스캔 경로 추가.
Exceptions
src/main/java/org/scoula/domain/contract/exception/ContractException.java
계약 도메인 전용 에러 코드(enum) 추가.
Precontract Enum
src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java
YEAR_3/4/5 displayName에서 불필요 공백 제거.
Chat 서비스 변경(내부)
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java, .../ChatServiceImpl.java, .../ChatServiceInterface.java
특약 최종 생성 로직 보강 및 내부 포맷팅/정렬/로깅 정리. 일부 포맷팅 변경만 포함.

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant Controller as ContractControllerImpl
  participant Service as ContractServiceImpl
  participant Mapper as ContractMapper
  participant Mongo as ContractMongoRepository
  participant Redis as RedisTemplate
  participant AI as AI Server

  Client->>Controller: POST /api/contract/{id}/getContract
  Controller->>Service: getContract(id, userId)
  Service->>Mapper: getContract(), getDuration(), updateStatus()
  Service->>Mongo: getContract() / saveContractMongo()
  Service->>Redis: read/write step/payment state
  Service->>AI: POST /api/contract/validate (ContractDTO)
  AI-->>Service: LegalityDTO
  Service-->>Controller: DTOs / Void / Boolean
  Controller-->>Client: 200 ApiResponse<...>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
계약서 단계 수정 (status 업데이트) [#53]
계약서 몽고DB 저장 및 전체 조회 [#46]
금액 조율: 조회/요청-Redis 저장/거절-삭제/동의-저장 [#47]
적법성: 전체 조회→AI 전송→결과 저장·반영 [#49] Mongo에 AI 결과를 명시적으로 저장하는 코드 경로는 불분명함.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
insertSignatureInit SQL uses hard-coded WHERE contract_chat_id = 4 (src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml) 하드코딩된 파라미터는 기능 요구사항(이슈)과 무관하며 실행 시 잘못된 동작을 유발할 수 있음.
Submodule pointer update (config-submodule) 서브모듈 커밋 포인터 변경은 링크된 이슈의 기능 목표와 직접 관련 없음.

Possibly related PRs

Suggested reviewers

  • MeongW
  • Whatdoyumin

Poem

(•ㅅ•)🐇
몽고에 계약서 한 장 놓고,
레디스엔 제안값 살짝 숨겨두고,
AI에게 묻고 답해 받아와,
특약은 또 다듬어 넣었네,
깡총—다음 스텝으로 뛰어갑니다!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e65ad0 and 47537ec.

📒 Files selected for processing (1)
  • src/main/java/org/scoula/domain/contract/exception/ContractException.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/scoula/domain/contract/exception/ContractException.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL Security Analysis (java)
  • GitHub Check: Test Coverage
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/contract

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 26

🔭 Outside diff range comments (1)
src/main/java/org/scoula/domain/contract/service/ContractService.java (1)

1-121: Spotless 포맷팅 적용 실패 – ContractServiceImpl.java 파일이 수렴되지 않음

./gradlew :spotlessApply 실행 시 아래 메시지가 출력되며 해당 파일의 포맷팅이 적용되지 않았습니다:

Skipping '/home/jailuser/git/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java' because it does not converge.  Run {@code spotlessDiagnose} to understand why

조치를 위해 아래 단계를 진행해주세요:

  • 문제 원인 진단:
    ./gradlew spotlessDiagnose
  • 진단 결과를 기반으로 파일 포맷을 수정한 뒤, 다시 포맷터를 적용합니다:
    ./gradlew :spotlessApply
🧹 Nitpick comments (20)
src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java (1)

11-13: 국제화/표시문구 분리 고려

표시문구가 고정 문자열이면 다국어/운영 중 변경에 취약합니다. enum 코드(name)로 계약·저장·비교를 수행하고, displayName은 메시지 소스로 분리하거나 응답에 code+label를 함께 노출하는 방식을 고려해 주세요.

src/main/java/org/scoula/global/config/DatabaseConfig.java (1)

32-34: MapperScan 패키지 중복 항목 정리 필요

org.scoula.domain.chat.mapper가 두 번 나옵니다(라인 29, 32). 동작에는 큰 문제 없지만 혼란을 줄이기 위해 중복은 제거하세요.

다음과 같이 중복 라인을 제거하면 됩니다.

 @MapperScan(
           basePackages = {
               "org.scoula.domain.user.mapper",
               "org.scoula.domain.fraud.mapper",
               "org.scoula.domain.precontract.mapper",
-              "org.scoula.domain.chat.mapper",
               "org.scoula.domain.home.mapper",
               "org.scoula.domain.mypage.mapper",
-              "org.scoula.domain.chat.mapper",
+              "org.scoula.domain.chat.mapper",
               "org.scoula.domain.contract.mapper"
           })
src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1)

19-31: 금액 필드 유효성 제약 및 null 방어 로직 추가 권장

  • 음수 방지를 위해 최소값 제약을 추가하세요.
  • toDTO에서 document가 null일 때의 방어 로직을 고려하세요(상위 레이어에서 이미 보장한다면 주석으로 의도 명시).
 package org.scoula.domain.contract.dto;

 import org.scoula.domain.contract.document.ContractMongoDocument;
+import jakarta.validation.constraints.Min;

 @ApiModel(description = "금액 조율에 필요한 금액들")
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
 public class PaymentDTO {

       @ApiModelProperty(value = "보증금", example = "50000")
-      private int depositPrice;
+      @Min(0)
+      private int depositPrice;

       @ApiModelProperty(value = "월세", example = "50000")
-      private int monthlyRent;
+      @Min(0)
+      private int monthlyRent;

       public static PaymentDTO toDTO(ContractMongoDocument document) {
-          return PaymentDTO.builder()
+          // document는 상위 레이어에서 유효성이 보장된다고 가정
+          return PaymentDTO.builder()
                   .depositPrice(document.getDepositPrice())
                   .monthlyRent(document.getMonthlyRent())
                   .build();
       }
 }

또한 장기적으로는 금액 타입에 BigDecimal 사용을 검토해 보세요(필요 정밀도/범위에 따라).

src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1)

11-11: 오타 수정 필요: "걔약서" → "계약서"

@apimodel description에 오타가 있습니다.

-@ApiModel(description = "걔약서 다음 단계로 넘어가기 위한 DTO")
+@ApiModel(description = "계약서 다음 단계로 넘어가기 위한 DTO")
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1)

96-111: 리스트 수집 방식 일관성 개선

두 메서드에서 서로 다른 리스트 수집 방식을 사용합니다:

  • toDTO: toList() (불변 리스트)
  • toSpecialContractDTO: Collectors.toList() (가변 리스트)

일관성을 위해 통일하는 것이 좋습니다.

-                            .collect(Collectors.toList()))
+                            .toList())
src/main/java/org/scoula/domain/contract/service/ContractService.java (2)

107-107: Javadoc 파라미터 설명 오타 수정

@Param 대신 @param을 사용해야 합니다.

-* @param userId 유저 아이디 @Param dto 변경된 특약
+* @param userId 유저 아이디
+* @param dto 변경된 특약

115-115: Javadoc 파라미터 설명 오타 수정

@Parma 대신 @param을 사용해야 합니다.

-* @param userId 유저 아이디 @Parma step 계약서 단계
+* @param userId 유저 아이디
src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (2)

119-120: 명시적인 null 체크 추가

Early return 전에 명시적인 null 체크를 추가하면 코드 의도가 더 명확해집니다.

-          List<SpecialContractDTO.SpecialClauseDTO> newClauses = dto.getSpecialClauses();
-          if (newClauses == null || newClauses.isEmpty()) return;
+          List<SpecialContractDTO.SpecialClauseDTO> newClauses = dto.getSpecialClauses();
+          if (newClauses == null || newClauses.isEmpty()) {
+              return; // 업데이트할 특약사항이 없음
+          }

63-103: 트랜잭션 처리 고려

saveSpecialContract 메서드에서 여러 MongoDB 문서를 조회하고 업데이트합니다. MongoDB 트랜잭션을 사용하여 데이터 일관성을 보장하는 것을 고려해보세요.

MongoDB 4.0 이상에서는 다중 문서 트랜잭션을 지원합니다. @Transactional 어노테이션을 추가하거나 MongoTransactionManager를 구성하여 트랜잭션 처리를 구현할 수 있습니다.

src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (2)

117-123: POST 대신 GET 메서드 사용 고려

getLegality 메서드가 데이터를 조회하는 용도라면 POST보다 GET이 RESTful 관례에 더 적합합니다.

-      @PostMapping("/legality")
+      @GetMapping("/legality")
       public ResponseEntity<ApiResponse<LegalityDTO>> getLegality(

148-154: 엔드포인트 경로와 HTTP 메서드 불일치

sendStep4 메서드가 GET으로 매핑되어 있지만 경로가 /specialContract입니다. 메서드명과 기능을 고려하면 다른 경로가 더 적절해 보입니다.

-      @GetMapping("/specialContract")
+      @PostMapping("/step4/complete")
       public ResponseEntity<ApiResponse<Void>> sendStep4(
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (4)

21-21: 주석 스타일 개선 필요

화살표를 사용한 주석보다는 표준 JavaDoc이나 일반 주석 형식을 사용하세요.

-      // ⬇⬇ 샘플 JSON의 최상위 구조와 동일
+      // 샘플 JSON의 최상위 구조와 동일

24-26: 타입 안정성 개선 권장

error 필드가 Object 타입으로 정의되어 있어 타입 안정성이 부족합니다. 구체적인 에러 DTO 클래스를 정의하거나 최소한 Map<String, Object>로 제한하는 것을 고려해보세요.

timestamp 필드는 String 대신 LocalDateTime을 사용하고 Jackson의 @JsonFormat 어노테이션으로 포맷을 지정하는 것이 더 타입 안전합니다.

+import java.time.LocalDateTime;
+import com.fasterxml.jackson.annotation.JsonFormat;

 public class LegalityDTO {
       private Boolean success;
       private String message;
       private Payload data;
-      private Object error;
-      private String timestamp;
+      private Map<String, Object> error;
+      @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
+      private LocalDateTime timestamp;

28-28: 어노테이션 사용 일관성

@lombok.Data 대신 import 문을 사용하여 @Data로 통일하는 것이 일관성 있습니다.

-      @lombok.Data
+      @Data

49-50: 날짜 타입 개선 필요

validatedAt 필드가 String으로 정의되어 있습니다. LocalDateTime을 사용하면 타입 안정성이 향상됩니다.

-          @JsonProperty("validated_at")
-          private String validatedAt;
+          @JsonProperty("validated_at")
+          @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
+          private LocalDateTime validatedAt;
src/main/java/org/scoula/domain/contract/controller/ContractController.java (1)

93-94: 불필요한 빈 줄 제거

파일 끝에 불필요한 빈 줄이 있습니다.

-
-
 }
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (4)

141-152: 반복적인 if-else 체인 개선

ContractDuration enum에 메서드를 추가하여 코드를 간소화할 수 있습니다.

-          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);
-          }
+          LocalDate contractEndDate = duration.calculateEndDate(startDate);

ContractDuration enum에 다음 메서드 추가:

public LocalDate calculateEndDate(LocalDate startDate) {
    return switch (this) {
        case YEAR_1 -> startDate.plusYears(1);
        case YEAR_2 -> startDate.plusYears(2);
        case YEAR_3 -> startDate.plusYears(3);
        case YEAR_4 -> startDate.plusYears(4);
        case YEAR_5 -> startDate.plusYears(5);
    };
}

424-429: 중복된 상태 체크 및 로깅

응답 body를 이미 사용했는데 다시 체크하고 있습니다.

-              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);
-              }
+              log.info("AI 응답 상태: {}", response.getStatusCode());
+              return res;

471-474: 미구현 메서드

sendStep4 메서드가 구현되지 않았습니다. TODO 주석을 추가하거나 구현을 완료하세요.

 @Override
 public Void sendStep4(Long contractChatId, Long userId) {
+      // TODO: 구현 필요 - 적법성 검사 후 다음 단계로 진행
+      throw new UnsupportedOperationException("sendStep4 메서드가 아직 구현되지 않았습니다");
-      return null;
 }

이 메서드의 구현을 도와드릴까요? 새 이슈를 생성할 수 있습니다.


477-486: 메서드 접근 제어자 검토

validateUserId 메서드가 public으로 선언되어 있는데, 내부에서만 사용된다면 private으로 변경을 고려하세요. 테스트를 위해 public이 필요하다면 package-private으로 변경할 수 있습니다.

-      public void validateUserId(Long contractChatId, Long userId) {
+      private void validateUserId(Long contractChatId, Long userId) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73a592a and 28a3ad2.

📒 Files selected for processing (23)
  • config-submodule (1 hunks)
  • src/main/java/org/scoula/domain/contract/controller/ContractController.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/exception/ContractException.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractService.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1 hunks)
  • src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java (1 hunks)
  • src/main/java/org/scoula/global/config/DatabaseConfig.java (1 hunks)
  • src/main/java/org/scoula/global/config/RootConfig.java (1 hunks)
  • src/main/java/org/scoula/global/config/ServletConfig.java (1 hunks)
  • src/main/java/org/scoula/global/mongodb/config/MongoConfig.java (1 hunks)
  • src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (12)
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (1)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (1)
src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java (2)
  • Document (13-34)
  • Data (24-33)
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (1)
src/main/java/org/scoula/global/common/exception/BusinessException.java (1)
  • BusinessException (4-21)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1)
src/main/java/org/scoula/global/common/constant/Constants.java (2)
  • Constants (4-94)
  • DateTime (75-80)
src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (2)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1)
  • ApiModel (11-21)
src/main/java/org/scoula/domain/contract/controller/ContractController.java (1)
src/main/java/org/scoula/domain/chat/controller/AiClauseImproveTestController.java (1)
  • Api (24-151)
src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java (1)
src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (2)
  • Document (19-110)
  • Data (66-74)
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (2)
src/main/java/org/scoula/global/common/constant/Constants.java (1)
  • File (20-29)
src/main/java/org/scoula/global/common/exception/BusinessException.java (1)
  • BusinessException (4-21)
src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (2)
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1)
  • ApiModel (13-50)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (3)
src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (1)
  • ApiModel (11-26)
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (1)
  • ApiModel (14-92)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (2)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (1)
  • Data (19-27)
🪛 GitHub Actions: CI
src/main/java/org/scoula/domain/contract/controller/ContractController.java

[error] 1-1: Spotless format violations detected in ContractController.java. Run './gradlew :spotlessApply' to fix formatting.

src/main/java/org/scoula/domain/contract/service/ContractService.java

[error] 1-1: Spotless format violations detected in ContractService.java. Run './gradlew :spotlessApply' to fix formatting.

src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java

[error] 1-1: Spotless format violations detected in ContractControllerImpl.java. Run './gradlew :spotlessApply' to fix formatting.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CodeQL Security Analysis (java)
🔇 Additional comments (8)
src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java (2)

11-13: 표시명 트레일링 공백 제거 잘하셨습니다.

UI/응답 표기 일관성이 개선되었습니다.


11-13: 하드코딩 문자열 의존성 확인 결과

3년 계약, 4년 계약, 5년 계약 문자열은 현재 ContractDuration enum 정의부에서만 사용되고 있습니다.
displayName 기반의 equals/contains 비교도 검색되지 않았습니다.

추가로 아래 영역을 반드시 검토해 주세요:

  • API 응답 직렬화 시 해당 문자열을 함께 내려주는 부분 여부
  • DB 저장 값으로서의 활용(매핑 컬럼 등)
  • 클라이언트/서버 하드코딩 비교, 스냅샷 테스트, 문서 예시 등에 의존성 존재 여부

위 확인 후 문제가 없으면 머지 진행 부탁드립니다.

src/main/java/org/scoula/global/mongodb/config/MongoConfig.java (1)

31-33: 스캔 범위에 contract.repository 추가: OK

MongoTemplate를 사용하는 수동 Repository 패턴과 정합적입니다. 계약 도메인 저장소 빈 탐지에 필요한 최소 변경으로 보이며 문제 없습니다.

src/main/java/org/scoula/global/config/RootConfig.java (1)

56-56: 서비스 스캔 범위 확장: OK

계약 도메인 서비스 빈 탐지를 위한 적절한 추가입니다. 다른 설정(Database/Mongo/Servlet)과 일관적입니다.

src/main/java/org/scoula/global/config/ServletConfig.java (1)

36-37: LGTM! 컨트롤러 패키지 스캔 설정이 적절합니다.

계약 도메인 컨트롤러를 위한 컴포넌트 스캔 설정이 올바르게 추가되었습니다.

src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1)

19-20: 원시 boolean 타입 사용 적합

NextStepDTO의 owner/buyer 필드는 단순 플래그로, 요청 바디에서 true인 경우에만 상태를 업데이트하고 false(또는 미전달)는 무시하도록 구현되어 있습니다. 즉, null과 false를 구분할 필요 없이 기본값(false)로 처리해도 의도한 동작과 일치하므로 wrapper 타입(Boolean)으로 변경할 필요가 없습니다.

src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java (1)

1-31: LGTM!

MyBatis Mapper 인터페이스가 깔끔하게 구현되었습니다. 파라미터 바인딩을 위한 @Param 어노테이션 사용이 적절합니다.

src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (1)

1-157: Spotless 포맷팅 완료 확인

./gradlew :spotlessApply 실행 결과, 변경된 컨트롤러 파일을 포함하여 포맷팅이 성공적으로 적용되었습니다. 추가 조치가 필요하지 않습니다.

@@ -1 +1 @@
Subproject commit 3a3e2d7703faf1cfe63cbcddf3c4cfb12f917dcf
Subproject commit 821c2c2ab5a06ba5e9f3e7ed6f23041d5c36bbec
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

서브모듈 포인터 변경 — 의도 확인 및 CI/동기화 점검이 필요합니다

  • 코드 변경은 없지만 외부 서브모듈 커밋을 새 SHA로 고정했습니다. 해당 커밋이 원격에서 유효한지, CI에서 서브모듈을 재귀적으로 가져오도록 설정돼 있는지 확인해 주세요.
  • PR 설명에 “왜” 서브모듈을 갱신했는지(환경/설정 변화, 버그 픽스 등), 예상 영향 범위를 간단히 남겨두면 추적성에 도움이 됩니다.
  • 재현성 관점에서 현재처럼 SHA pin은 좋습니다. 다만 서브모듈 저장소에서 force-push가 발생하지 않도록 운영 규칙을 확인하세요.

다음 스크립트로 서브모듈 설정과 원격 커밋 존재 여부, CI의 submodule 처리 여부를 점검할 수 있습니다.

  • PR 본문에 서브모듈 갱신 사유와 영향 범위를 1~2줄로 보강해 주세요.

  • 필요하시면 CI에서 actions/checkout에 submodules: true 추가, 또는 빌드 스크립트에 git submodule update --init --recursive 보강 패치를 제안드리겠습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Submodules defined in .gitmodules =="
if ! git config -f .gitmodules --get-regexp 'submodule\..*\.path' >/dev/null 2>&1; then
  echo "No submodules configured."
  exit 0
fi

git config -f .gitmodules --get-regexp 'submodule\..*\.path'

echo
echo "== Verifying each submodule's pinned commit exists on its remote =="
while read -r key path; do
  name="${key#submodule.}"; name="${name%.path}"
  url="$(git config -f .gitmodules "submodule.${name}.url")"
  sha="$(git ls-tree HEAD "$path" | awk '{print $3}')"
  echo "-- $name"
  echo "path: $path"
  echo "url : $url"
  echo "sha : $sha"
  if git ls-remote "$url" | grep -q "$sha"; then
    echo "OK: SHA exists on remote."
  else
    echo "ERROR: SHA not found on remote (builds may fail)."
  fi
  echo
done < <(git config -f .gitmodules --get-regexp 'submodule\..*\.path')

echo "== Checking CI for submodule handling =="
# GitHub Actions: actions/checkout submodules: true 또는 git submodule update --init --recursive 검색
if fd -a .github/workflows -t f >/dev/null 2>&1; then
  fd -a .github/workflows -t f | while read -r wf; do
    echo ">> $wf"
    rg -n "submodules:|git submodule|--recurse-submodules" "$wf" || true
    echo
  done
else
  echo "No .github/workflows found or fd not available."
fi

Length of output: 642


프로세스 서브스티튜션(/dev/fd/… No such file or directory) 오류로 인해 SHA 검증이 실패했습니다. 아래 스크립트로 다시 확인해주세요.


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== .gitmodules에 정의된 서브모듈 =="
entries=$(git config -f .gitmodules --get-regexp 'submodule\..*\.path' || true)
if [ -z "$entries" ]; then
  echo "서브모듈 설정 없음."
  exit 0
fi
echo "$entries"
echo

echo "== 각 서브모듈의 SHA가 원격에 존재하는지 확인 =="
while IFS= read -r entry; do
  key=${entry%% *}
  path=${entry#* }
  name=${key#submodule.}; name=${name%.path}
  url=$(git config -f .gitmodules "submodule.${name}.url")
  sha=$(git ls-tree HEAD "$path" | awk '{print $3}')
  echo "-- $name --"
  echo "path: $path"
  echo "url : $url"
  echo "sha : $sha"
  if git ls-remote "$url" | grep -q "$sha"; then
    echo "OK: SHA 존재"
  else
    echo "ERROR: SHA 미존재 (빌드 실패 가능)"
  fi
  echo
done <<<"$entries"

echo "== CI 워크플로우에서 서브모듈 처리 설정 확인 =="
if [ -d .github/workflows ]; then
  find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print |
    while read -r wf; do
      echo "[$wf]"
      grep -HnE 'actions/checkout.*submodules:|git submodule|--recurse-submodules' "$wf" || echo "  (설정 없음)"
      echo
    done
else
  echo ".github/workflows 디렉토리 없음."
fi

Length of output: 3353


서브모듈 설정 및 원격 커밋 유효성 검증 요청

  • .gitmodules에 정의된 config 서브모듈의 SHA가 HEAD 트리에 반영되어 있지 않습니다.
  • config-submodule은 SHA(821c2c2ab5a06ba5e9f3e7ed6f23041d5c36bbec)가 있으나 https://github.com/ITZEEP/backend-config.git 원격에서 “repository not found” 오류가 발생했습니다.
  • CI 워크플로우에서는 git submodule update --init --recursivegit submodule update --remote 명령을 실행하지만, actions/checkoutsubmodules: true 설정은 적용되어 있지 않습니다.

조치 항목:

  1. .gitmodules의 URL과 SHA 설정을 올바른 값으로 수정하고, 누락된 config 서브모듈 커밋을 HEAD에 반영해주세요.
  2. 원격 저장소 존재 여부 및 접근 권한(토큰·SSH) 문제를 해결하세요.
  3. CI 설정에 actions/checkout 단계에서 submodules: true 옵션을 추가하거나, 인증된 방식으로 서브모듈을 초기화하도록 구성해주세요.
  4. PR 본문에 서브모듈 업데이트 사유(버그 픽스, 환경 변경 등)와 영향 범위를 1~2줄로 명시해 주세요.
🤖 Prompt for AI Agents
In config-submodule around lines 1 to 1: the .gitmodules entry for the `config`
submodule points to a missing/invalid remote and the recorded SHA
(821c2c2ab5a06ba5e9f3e7ed6f23041d5c36bbec) is not present in HEAD; update
.gitmodules to the correct repository URL and correct commit SHA, add and commit
the updated submodule reference in the superproject so the submodule SHA is
reflected in HEAD, verify the remote repository exists and fix access (provide a
valid HTTPS token or SSH key and ensure the URL uses the correct protocol), and
update CI to initialize submodules correctly by adding `submodules: true` to the
actions/checkout step or performing an authenticated `git submodule update
--init --recursive` (with credentials) so CI can fetch the submodule; finally,
add a 1–2 line note in the PR body stating why the submodule was updated and the
expected impact.

Comment on lines +76 to +109
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.<ContractDTO.SpecialContractDTO>emptyList()
: dto.getSpecialContracts())
.stream()
.map(
s ->
SpecialContract.builder()
.order(s.getOrder())
.title(s.getTitle())
.content(s.getContent())
.build())
.collect(Collectors.toList()))
.build();
}
Copy link

@coderabbitai coderabbitai bot Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

toDocument 메서드 개선 필요

메서드에 몇 가지 개선사항이 있습니다:

  1. DTO null 체크 누락
  2. 날짜 변환 시 null 체크 누락
  3. Raw type 사용으로 인한 컴파일러 경고
 public static ContractMongoDocument toDocument(ContractDTO dto, LocalDate contractEndDate) {
+    if (dto == null) {
+        throw new IllegalArgumentException("ContractDTO cannot be null");
+    }
     return ContractMongoDocument.builder()
             .contractChatId(dto.getContractChatId())
             // ... other fields ...
-            .contractStartDate(dto.getContractStartDate().toString())
-            .contractEndDate(contractEndDate.toString())
+            .contractStartDate(dto.getContractStartDate() != null ? dto.getContractStartDate().toString() : null)
+            .contractEndDate(contractEndDate != null ? contractEndDate.toString() : null)
             // ... other fields ...
             .specialContracts(
                     (dto.getSpecialContracts() == null
-                                    ? Collections.<ContractDTO.SpecialContractDTO>emptyList()
+                                    ? Collections.emptyList()
                                     : dto.getSpecialContracts())
                             .stream()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.<ContractDTO.SpecialContractDTO>emptyList()
: dto.getSpecialContracts())
.stream()
.map(
s ->
SpecialContract.builder()
.order(s.getOrder())
.title(s.getTitle())
.content(s.getContent())
.build())
.collect(Collectors.toList()))
.build();
}
public static ContractMongoDocument toDocument(ContractDTO dto, LocalDate contractEndDate) {
if (dto == null) {
throw new IllegalArgumentException("ContractDTO cannot be null");
}
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() != null
? dto.getContractStartDate().toString()
: null)
.contractEndDate(
contractEndDate != null
? contractEndDate.toString()
: null)
.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();
}
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java
around lines 76 to 109, the toDocument method needs null-safety and proper
generics: validate dto is not null (throw IllegalArgumentException or return
null as project convention) before accessing fields; guard contractStartDate and
the passed contractEndDate with null checks before calling toString() (use
conditional expressions to set null or empty string or formatted value per
project style); replace the raw
Collections.<ContractDTO.SpecialContractDTO>emptyList() pattern with a typed
empty list (Collections.emptyList() with proper generic inference or
Collections.<ContractDTO.SpecialContractDTO>emptyList() typed correctly) and
ensure the stream mapping handles null specialContracts safely (e.g., use
Optional.ofNullable(dto.getSpecialContracts()).orElse(Collections.emptyList()));
and keep building the document only after these checks so no NPEs or compiler
warnings occur.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@minnieming 개선사항을 적용해주셔서 감사합니다! null 안전성과 제네릭 타입 처리가 이제 더 안전하고 깔끔해졌네요. 👍

\(^▽^)/ 깔끔하게 수정 완료!


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines 13 to 33
@Document(collection = "FINAL_SPECIAL_CONTRACT")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinalSpecialContractDocument {
@Id private String id;
private Long contractChatId;
private Integer totalFinalClauses;
private List<FinalClause> finalClauses;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FinalClause {
private Integer order;
private String title;
private String content;
private Long sourceRound;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

조회 키 인덱스 및 제약 추가 권장

contractChatId 기반 조회가 핵심이면 인덱스를 추가하세요. 단일 계약별 1 문서만 허용한다면 unique 제약도 고려하세요.

 import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.index.Indexed;

 @Document(collection = "FINAL_SPECIAL_CONTRACT")
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
 public class FinalSpecialContractDocument {
-      @Id private String id;
-      private Long contractChatId;
+      @Id private String id;
+      @Indexed(unique = true)
+      private Long contractChatId;
       private Integer totalFinalClauses;
       private List<FinalClause> finalClauses;

또한 FinalClause.order는 순서를 의미하므로 clauseOrder 등으로의 리네이밍을 고려하면 가독성이 향상됩니다(선택).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Document(collection = "FINAL_SPECIAL_CONTRACT")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinalSpecialContractDocument {
@Id private String id;
private Long contractChatId;
private Integer totalFinalClauses;
private List<FinalClause> finalClauses;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FinalClause {
private Integer order;
private String title;
private String content;
private Long sourceRound;
}
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.index.Indexed;
@Document(collection = "FINAL_SPECIAL_CONTRACT")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FinalSpecialContractDocument {
@Id private String id;
@Indexed(unique = true)
private Long contractChatId;
private Integer totalFinalClauses;
private List<FinalClause> finalClauses;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FinalClause {
private Integer order;
private String title;
private String content;
private Long sourceRound;
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java
around lines 13 to 33, add a MongoDB index on contractChatId (and make it unique
if only one document per contract is allowed) by annotating the field or class
appropriately (e.g., @Indexed or @CompoundIndex with unique=true) so queries by
contractChatId are fast and constrained; optionally rename FinalClause.order to
clauseOrder (update the field, constructor/builder, getters/setters/usages) to
improve clarity if you choose to change the identifier across the codebase.

Comment on lines +56 to +65
<insert id = "insertSignatureInit">
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;
</insert>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

하드코딩된 값 제거 필요

insertSignatureInit 쿼리에서 contract_chat_id = 4가 하드코딩되어 있습니다. 파라미터를 사용해야 합니다.

-        WHERE cc.contract_chat_id = 4;
+        WHERE cc.contract_chat_id = #{contractChatId}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<insert id = "insertSignatureInit">
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;
</insert>
<insert id = "insertSignatureInit">
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 = #{contractChatId}
</insert>
🤖 Prompt for AI Agents
In src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
around lines 56 to 65, the INSERT statement hardcodes "cc.contract_chat_id = 4";
change it to use a parameter (e.g. cc.contract_chat_id = #{contractChatId}) and
update the mapper method signature/parameterType accordingly so the
contractChatId is passed in from the caller; ensure the parameter name matches
the mapper interface and any calling code is updated to supply the value.

Comment on lines +108 to +113
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}
</insert>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

SQL 문법 오류: 정의되지 않은 테이블 별칭 참조

Line 111에서 fc 별칭이 정의되지 않았는데 참조되고 있습니다. electronic_signature 조인 조건이 잘못되었습니다.

-            LEFT JOIN electronic_signature es ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id
+            LEFT JOIN final_contract fc ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id
+            LEFT JOIN electronic_signature es ON fc.contract_id = es.contract_id
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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}
</insert>
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
+ LEFT JOIN final_contract fc ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id
+ LEFT JOIN electronic_signature es ON fc.contract_id = es.contract_id
WHERE cc.contract_chat_id = #{contractChatId}
</insert>
🤖 Prompt for AI Agents
In src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
around lines 108 to 113, the LEFT JOIN for electronic_signature uses an
undefined alias `fc` in the ON clause; change the ON conditions to reference the
defined alias `es` (e.g., ON cc.home_id = es.home_id AND cc.owner_id =
es.owner_id) so the join uses the correct table alias and removes the undefined
reference.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🔭 Outside diff range comments (3)
src/main/java/org/scoula/global/config/RootConfig.java (2)

35-57: ContractMongoRepository 스캔 누락 가능성: basePackages에 repository 패키지도 포함하세요

현재 basePackages에 org.scoula.domain.contract.service만 추가되어 있습니다. ContractMongoRepositoryorg.scoula.domain.contract.repository에 있으므로 컴포넌트 스캔에 포함되지 않아 빈 등록이 누락될 위험이 큽니다. 서비스에서 주입 시 NoSuchBeanDefinitionException이 날 수 있습니다.

권장:

  • 넓게 스캔: org.scoula.domain.contract 전체 패키지를 추가
  • 또는 최소한 org.scoula.domain.contract.repository도 함께 추가

예시 Diff:

-              "org.scoula.domain.contract.service"
+              "org.scoula.domain.contract"

35-57: MyBatis 매퍼 스캔 설정 확인 결과

– DatabaseConfig.java(24–33)
• @MapperScan(basePackages = {…,"org.scoula.domain.contract.mapper"})로 ContractMapper 인터페이스 스캔은 정상 설정되어 있습니다.
– XML 매퍼(.xml) 스캔 설정 누락
• DatabaseConfig#sqlSessionFactoryBean에 setMapperLocations(...) 호출이 없고
• mybatis-config.xml에도 <mappers> 섹션이 정의되어 있지 않습니다.

따라서 XML 매퍼 파일을 로드하도록 아래 중 하나를 추가해야 합니다:

  • DatabaseConfig.java에서
sqlSessionFactoryBean.setMapperLocations(
  new PathMatchingResourcePatternResolver()
    .getResources("classpath:/org/scoula/domain/**/mapper/*.xml")
);
  • 또는 mybatis-config.xml에
<mappers>
  <mapper resource="org/scoula/domain/contract/mapper/ContractMapper.xml"/>
  <!-- 다른 매퍼들도 동일하게 추가 -->
</mappers>
src/main/java/org/scoula/domain/contract/controller/ContractController.java (1)

1-95: Spotless 포맷팅 위반 감지됨

파이프라인에서 포맷팅 위반이 감지되었습니다. 포맷팅을 수정하려면 ./gradlew :spotlessApply를 실행하세요.

♻️ Duplicate comments (5)
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (3)

335-335: 오타 수정: 예외 상수명

여기서도 동일한 오타가 있습니다.

-             throw new BusinessException(ContractException.CONTRACt_REDIS, e);
+             throw new BusinessException(ContractException.CONTRACT_REDIS, e);

353-353: 오타 수정: 예외 상수명

여러 곳에서 동일한 오타가 반복됩니다.

-             throw new BusinessException(ContractException.CONTRACt_REDIS, "금액 정보가 Redis에 없습니다.");
+             throw new BusinessException(ContractException.CONTRACT_REDIS, "금액 정보가 Redis에 없습니다.");

Also applies to: 373-373


420-421: 보안: 로그 인젝션 취약점

사용자 입력값을 직접 로그에 기록하면 로그 인젝션 공격에 취약합니다. 입력값을 검증하거나 이스케이프 처리해야 합니다.

-             log.warn("AI 응답 값 확인: {}",res.toString());
+             log.warn("AI 응답 값 확인: {}", sanitizeForLogging(res.toString()));

-             log.error(response.getBody());
+             log.error("AI 서버 오류 응답: {}", sanitizeForLogging(String.valueOf(response.getBody())));

로그 삭제 또는 sanitize 메서드 추가:

private String sanitizeForLogging(String input) {
    if (input == null) return "null";
    return input.replaceAll("[\r\n]", "_");
}

Also applies to: 427-427

src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (2)

1-157: Spotless 포맷팅 위반 감지됨

파이프라인에서 포맷팅 위반이 감지되었습니다. 포맷팅을 수정하려면 ./gradlew :spotlessApply를 실행하세요.


36-43: 보안: CSRF 보호 필요

POST 엔드포인트들이 CSRF 공격에 취약할 수 있습니다. Spring Security의 CSRF 보호가 활성화되어 있는지 확인하거나, 상태 변경 작업에 대해 적절한 보호 메커니즘을 구현해야 합니다.

CSRF 보호 설정을 확인하기 위해 Spring Security 설정을 검토해야 합니다:

#!/bin/bash
# Spring Security 설정에서 CSRF 보호 상태 확인
rg -A 10 "csrf\(\)" --glob "**/*Security*.java"
rg -A 10 "@EnableWebSecurity" --glob "*.java"

Also applies to: 65-73, 86-94, 117-123, 126-133

🧹 Nitpick comments (23)
src/main/java/org/scoula/domain/contract/exception/ContractException.java (1)

12-13: 오탈자 및 표현/케이스 통일 제안 (MongoDB, 맞춤법)

한글 맞춤법 및 명사 표기 통일을 제안합니다.

-      CONTRACT_GET("CONTRACT_4001", HttpStatus.BAD_REQUEST, "mongoDB에서 값을 조회하지 못 햇습니다."),
-      CONTRACT_INSERT("CONTRACT_4002", HttpStatus.BAD_REQUEST, "MongoDB에 저장이 되지 않았습니다."),
+      CONTRACT_GET("CONTRACT_4001", HttpStatus.BAD_REQUEST, "MongoDB에서 값을 조회하지 못했습니다."),
+      CONTRACT_INSERT("CONTRACT_4002", HttpStatus.BAD_REQUEST, "MongoDB 저장에 실패했습니다."),
src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (2)

11-16: null 필드 응답 제외로 페이로드 최소화(@JsonInclude)

DTO에 @JsonInclude(JsonInclude.Include.NON_NULL) 추가를 권장합니다. 선택 사항이지만 API 응답을 깔끔하게 유지합니다.

 @ApiModel(description = "최종 계약서")
+@JsonInclude(JsonInclude.Include.NON_NULL)
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
 public class FinalContractDTO {

11-11: Swagger v2(@apimodel) → OpenAPI 3(@Schema) 마이그레이션 제안

프로젝트가 springdoc(OpenAPI 3)를 사용한다면 @apimodel 대신 @Schema로 전환을 고려하세요. 유지보수/도구 호환성이 좋아집니다. 현재 스택 유지 시 본 코멘트는 무시해도 됩니다.

config-submodule (1)

1-1: 운영 제안: 서브모듈 버전 관리 안정화(태그 고정·변경 요약 링크)

서브모듈을 임의 SHA로 고정하면 추적성이 떨어집니다. 다음을 권장합니다.

  • 서브모듈 저장소에 릴리즈 태그(예: config-vX.Y.Z) 생성 후 태그로 포인터 고정.
  • PR 설명에 서브모듈 변경점 요약과 커밋/릴리즈 링크 추가(스캔 확장 포함 여부 명시).

릴리즈 흐름 정리 필요하면 가이드를 제안드리겠습니다.

src/main/java/org/scoula/global/config/DatabaseConfig.java (1)

32-33: 중복된 Mapper 패키지 항목 제거 제안

org.scoula.domain.chat.mapper가 중복 스캔되고 있습니다. 기능 영향은 없지만 노이즈이므로 제거를 권장합니다.

적용 예시:

               "org.scoula.domain.home.mapper",
-              "org.scoula.domain.mypage.mapper",
-              "org.scoula.domain.chat.mapper",
-              "org.scoula.domain.contract.mapper"
+              "org.scoula.domain.mypage.mapper",
+              "org.scoula.domain.contract.mapper"
src/main/java/org/scoula/global/config/ServletConfig.java (1)

36-37: 중복 Component 스캔 경로 정리

이미 "org.scoula.domain" 전체를 스캔하므로, 구체 컨트롤러 패키지 항목은 중복입니다. 유지보수성 차원에서 정리를 권장합니다.

정리 예시:

               "org.scoula.domain.precontract.controller",
               "org.scoula.domain.home.controller",
-              "org.scoula.domain.mypage.controller",
-              "org.scoula.domain.contract.controller"
+              // "org.scoula.domain"에 포함되어 중복 스캔되므로 제거
src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java (1)

1-34: 조회 성능/안정성 개선: 인덱스 및 리스트 기본값 설정 권장

  • contractChatId로 조회할 가능성이 높아 보입니다. 인덱스를 추가하면 조회 성능이 향상됩니다.
  • finalClauses가 null이 될 수 있어 NPE 여지가 있습니다. 빌더 기본값을 비어있는 리스트로 두는 것을 권장합니다.

적용 예시:

 package org.scoula.domain.contract.document;

 import java.util.List;
+import java.util.Collections;

 import org.springframework.data.annotation.Id;
 import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.index.Indexed;

 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;
+    @Indexed
+    private Long contractChatId;
     private Integer totalFinalClauses;
-    private List<FinalClause> finalClauses;
+    @Builder.Default
+    private List<FinalClause> finalClauses = Collections.emptyList();

     @Data
     @Builder
     @NoArgsConstructor
     @AllArgsConstructor
     public static class FinalClause {
         private Integer order;
         private String title;
         private String content;
         private Long sourceRound;
     }
 }
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (1)

16-27: 명칭 충돌/혼동 가능성: DTO 이름 변경 또는 명시적 사용 권장

상위 DTO 이름 SpecialContractDTOContractDTO.SpecialContractDTO와 동일하여, 코드 가독성/임포트 충돌(명시적 정규명 사용 필요) 가능성이 있습니다. API 표면이 안정되기 전에 다음 중 하나를 권장합니다.

  • 상위 DTO명을 SpecialContractUpdateDTO 또는 SpecialClausesDTO로 변경
  • 또는 사용하는 쪽에서 항상 정규명(ContractDTO.SpecialContractDTO, org.scoula.domain.contract.dto.SpecialContractDTO)으로 명시

또한 기본 입력 검증을 고려해 주세요(예: specialClauses 비어있지 않음, 각 항목 order/title/content 필수).

src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1)

11-21: 오타 수정 및 사소한 정리

  • 설명 오타: "걔약서" → "계약서"
  • primitive boolean에는 @JsonInclude(Include.NON_NULL)가 영향이 없습니다. 둘 중 하나를 고려해 주세요:
    • 필드 타입을 Boolean으로 바꾸어 null 생략 효과를 활용
    • 그대로 둘 경우 어노테이션 유지 필요성 재검토

오타 수정 예시:

-@ApiModel(description = "걔약서 다음 단계로 넘어가기 위한 DTO")
+@ApiModel(description = "계약서 다음 단계로 넘어가기 위한 DTO")
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (1)

1-93: JSON 역직렬화 내구성 강화: unknown 필드 무시 추가 권장

외부 응답 스키마가 변경될 수 있으므로 @JsonIgnoreProperties(ignoreUnknown = true)를 상위 및 중첩 클래스에 적용하면 안전합니다. 또한 중첩 클래스에서도 @Data를 통일적으로 사용하면 일관성이 좋아집니다.

적용 예시:

 package org.scoula.domain.contract.dto;

 import java.util.List;

 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

 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)
+@JsonIgnoreProperties(ignoreUnknown = true)
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
 public class LegalityDTO {
@@
-    @lombok.Data
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
     @Builder
     @NoArgsConstructor
     @AllArgsConstructor
     public static class Payload {
@@
-    @lombok.Data
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
     @Builder
     @NoArgsConstructor
     @AllArgsConstructor
     public static class ViolationSummary {
@@
-    @lombok.Data
+    @Data
+    @JsonIgnoreProperties(ignoreUnknown = true)
     @Builder
     @NoArgsConstructor
     @AllArgsConstructor
     public static class Violation {
src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1)

25-30: toDTO null 방어와 확장성 보강 제안

document가 null이거나 필드 누락 시 NPE 위험이 있습니다. 방어 코드와 필드 확장 대비를 권장합니다.

예시 Diff:

-      public static PaymentDTO toDTO(ContractMongoDocument document) {
-          return PaymentDTO.builder()
-                  .depositPrice(document.getDepositPrice())
-                  .monthlyRent(document.getMonthlyRent())
-                  .build();
-      }
+      public static PaymentDTO toDTO(ContractMongoDocument document) {
+          if (document == null) return PaymentDTO.builder().build();
+          return PaymentDTO.builder()
+                  .depositPrice(document.getDepositPrice())
+                  .monthlyRent(document.getMonthlyRent())
+                  .build();
+      }
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (2)

19-23: 필드 주석 정정(임대인/임차인)

현재 주석이 역전되어 있습니다. 일반적으로 owner = 임대인, buyer = 임차인 입니다. 주석을 교정해 혼동을 줄여주세요.

-      // 임차인 이름
+      // 임대인 이름
       private String ownerName;
-      // 임대인 이름
+      // 임차인 이름
       private String buyerName;

43-44: 날짜 파싱을 명시적 포맷으로 고정(선택)

현재 LocalDate.parse(...)는 기본 ISO에 의존합니다. 저장 시 toString()이 ISO이긴 하지만, 명시적으로 DateTimeFormatter.ISO_LOCAL_DATE를 사용하는 편이 안전합니다.

-                  .contractStartDate(LocalDate.parse(document.getContractStartDate()))
-                  .contractEndDate(LocalDate.parse(document.getContractEndDate()))
+                  .contractStartDate(LocalDate.parse(document.getContractStartDate(), DateTimeFormatter.ISO_LOCAL_DATE))
+                  .contractEndDate(LocalDate.parse(document.getContractEndDate(), DateTimeFormatter.ISO_LOCAL_DATE))

필요 시 DateTimeFormatter import 추가.

src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (1)

52-56: 문서 내 날짜를 String으로 저장하는 설계 재검토(선택)

contractStartDate/contractEndDate를 String으로 저장하고 DTO 변환 시 파싱합니다. 스키마/인덱스/타입 안전성을 고려하면 Mongo에서도 LocalDate(또는 Date) 타입 저장이 더 안전합니다. 기존 데이터 호환이 필요하다면 마이그레이션 전략을 별도 이슈로 관리하는 것을 권장합니다.

필요 시 @FieldConverters 기반으로 LocalDate <-> Date 변환 설정까지 제안 드릴 수 있습니다.

src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1)

100-113: JDK 의존성/일관성: toList vs Collectors.toList 통일

toDTOstream().toList()를, 본 메서드는 Collectors.toList()를 사용합니다. 프로젝트 JDK 버전에 맞춰 하나로 통일하세요. JDK 17 이상이면 toList() 통일 권장.

src/main/java/org/scoula/domain/contract/service/ContractService.java (1)

107-109: Javadoc @param 태그 오타 수정

@Param/@Parma가 javadoc 태그로 잘못 표기되어 있습니다. @param으로 정정해 주세요.

-       * @param contractChatId 채팅방 아이디
-       * @param userId 유저 아이디 @Param dto 변경된 특약
+       * @param contractChatId 채팅방 아이디
+       * @param userId 유저 아이디
+       * @param dto 변경된 특약
-       * @param userId 유저 아이디 @Parma step 계약서 단계
+       * @param userId 유저 아이디
+       * @param step 계약서 단계

Also applies to: 115-117

src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (3)

24-24: 필드 주입 대신 생성자 주입 권장

@Autowired 필드 주입은 테스트/불변성에 불리합니다. 생성자 주입으로 전환해 주세요.

-      @Autowired private MongoTemplate mongoTemplate;
+      private final MongoTemplate mongoTemplate;
+
+      public ContractMongoRepository(MongoTemplate mongoTemplate) {
+          this.mongoTemplate = mongoTemplate;
+      }

39-46: 중복 로직/주석 정리: getDepositPrice → getContract 재사용

getDepositPricegetContract와 동일 쿼리를 수행합니다. 주석 처리된 코드도 남아 있습니다. 중복 제거와 주석 정리를 권장합니다.

-      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 ContractMongoDocument getDepositPrice(Long contractChatId) {
+          return getContract(contractChatId);
+      }

119-131: 경계값/검증 로깅 보강(선택)

범위를 벗어난 order는 조용히 무시됩니다. 추적을 위해 로깅이나 예외 처리(요구사항에 따라)를 고려해 주세요.

src/main/java/org/scoula/domain/contract/controller/ContractController.java (1)

93-94: 불필요한 빈 줄 제거 필요

파일 끝에 불필요한 빈 줄이 있습니다.

-
-
 }
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (2)

84-89: Thread.sleep 대신 비동기 처리 고려

Thread.sleep(2000)은 현재 스레드를 블로킹하여 성능 문제를 일으킬 수 있습니다. 비동기 처리나 스케줄링을 고려해 보세요.

더 나은 방법:

  1. CompletableFuture.delayedExecutor를 사용한 비동기 처리
  2. Spring의 @Async 어노테이션 활용
  3. 메시지 큐를 통한 지연 처리
-         try {
-             Thread.sleep(2000);
-         } catch (InterruptedException ie) {
-             Thread.currentThread().interrupt();
-             log.warn("standByContract sleep interrupted", ie);
-         }
+         // 비동기로 2초 후 메시지 전송
+         CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS)
+             .execute(() -> {
+                 contractChatService.AiMessageBtn(contractChatId, """
+                 기다리는 동안
+                 어려운 법률 용어와 법률 팁을 알아볼까요?
+                 """);
+             });

470-474: 미구현 메서드: sendStep4

sendStep4 메서드가 아직 구현되지 않았습니다. TODO 주석을 추가하거나 구현 계획을 명시해야 합니다.

 @Override
 public Void sendStep4(Long contractChatId, Long userId) {
+     // TODO: Step 4 구현 필요 - 적법성 검사 후 다음 단계로 진행
+     log.warn("sendStep4 메서드가 아직 구현되지 않았습니다.");
      return null;
 }

이 메서드의 구현을 도와드릴까요? 새로운 이슈를 생성하여 추적할 수 있습니다.

src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (1)

3-3: 사용하지 않는 import 제거 필요

HttpServletResponseMultipartFile은 사용되지 않으므로 제거해야 합니다.

-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;

Also applies to: 12-12

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73a592a and 28a3ad2.

📒 Files selected for processing (23)
  • config-submodule (1 hunks)
  • src/main/java/org/scoula/domain/contract/controller/ContractController.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/exception/ContractException.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractService.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1 hunks)
  • src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java (1 hunks)
  • src/main/java/org/scoula/global/config/DatabaseConfig.java (1 hunks)
  • src/main/java/org/scoula/global/config/RootConfig.java (1 hunks)
  • src/main/java/org/scoula/global/config/ServletConfig.java (1 hunks)
  • src/main/java/org/scoula/global/mongodb/config/MongoConfig.java (1 hunks)
  • src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (11)
src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (6)
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1)
  • ApiModel (13-50)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (1)
  • ApiModel (14-92)
src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1)
  • ApiModel (11-21)
src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1)
  • ApiModel (12-31)
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (2)
  • ApiModel (11-28)
  • Data (19-27)
src/main/java/org/scoula/domain/contract/controller/ContractController.java (1)
src/main/java/org/scoula/domain/chat/controller/AiClauseImproveTestController.java (1)
  • Api (24-151)
src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java (1)
src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (2)
  • Document (19-110)
  • Data (66-74)
src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (1)
src/main/java/org/scoula/global/common/exception/BusinessException.java (1)
  • BusinessException (4-21)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (6)
src/main/java/org/scoula/global/common/constant/Constants.java (2)
  • Constants (4-94)
  • DateTime (75-80)
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1)
  • ApiModel (13-50)
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (1)
  • ApiModel (14-92)
src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1)
  • ApiModel (11-21)
src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1)
  • ApiModel (12-31)
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (2)
  • ApiModel (11-28)
  • Data (19-27)
src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (1)
src/main/java/org/scoula/domain/contract/document/FinalSpecialContractDocument.java (2)
  • Document (13-34)
  • Data (24-33)
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (1)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (2)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1)
  • ApiModel (12-31)
src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (6)
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1)
  • ApiModel (13-50)
src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (1)
  • ApiModel (11-26)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (4)
  • ApiModel (14-92)
  • lombok (28-53)
  • lombok (55-65)
  • lombok (67-91)
src/main/java/org/scoula/domain/contract/dto/PaymentDTO.java (1)
  • ApiModel (12-31)
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (2)
  • ApiModel (11-28)
  • Data (19-27)
src/main/java/org/scoula/domain/contract/dto/LegalityDTO.java (3)
src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (2)
  • ApiModel (18-114)
  • Data (56-64)
src/main/java/org/scoula/domain/contract/dto/NextStepDTO.java (1)
  • ApiModel (11-21)
src/main/java/org/scoula/domain/contract/dto/SpecialContractDTO.java (2)
  • ApiModel (11-28)
  • Data (19-27)
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (2)
src/main/java/org/scoula/global/common/constant/Constants.java (1)
  • File (20-29)
src/main/java/org/scoula/global/common/exception/BusinessException.java (1)
  • BusinessException (4-21)
🪛 GitHub Actions: CI
src/main/java/org/scoula/domain/contract/controller/ContractController.java

[error] 1-1: Spotless formatting violations detected. Run './gradlew :spotlessApply' to fix formatting in this file.

src/main/java/org/scoula/domain/contract/service/ContractService.java

[error] 1-1: Spotless formatting violations detected. Run './gradlew :spotlessApply' to fix formatting in this file.

src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java

[error] 1-1: Spotless formatting violations detected. Run './gradlew :spotlessApply' to fix formatting in this file.

🪛 GitHub Check: CodeQL
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java

[failure] 24-24: HTTP request type unprotected from CSRF
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.


[failure] 44-44: HTTP request type unprotected from CSRF
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.


[failure] 53-53: HTTP request type unprotected from CSRF
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.


[failure] 74-74: HTTP request type unprotected from CSRF
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent state-changing action.

src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

[failure] 421-421: Log Injection
This log entry depends on a user-provided value.


[failure] 427-427: Log Injection
This log entry depends on a user-provided value.

🔇 Additional comments (6)
src/main/java/org/scoula/domain/contract/exception/ContractException.java (1)

9-10: Lombok 사용 적절

@Getter, @requiredargsconstructor로 IErrorCode 게터 충족 목적에 부합합니다.

src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (1)

11-26: FinalContractDTO 엔드포인트 사용 및 multipart 설정 확인 필요

현재 코드베이스에서 FinalContractDTO를 파라미터로 사용하는 컨트롤러 메서드를 찾을 수 없습니다. 아래 사항을 수동으로 확인해주세요:

  • FinalContractDTO를 파라미터로 받는 @PostMapping, @PutMapping, @PatchMapping, @RequestMapping 메서드가 실제로 존재하는지
  • 해당 메서드에 consumes = "multipart/form-data" 설정이 되어 있는지
  • DTO 내부의 MultipartFile 필드들이 @ModelAttribute 또는 @RequestPart로 정상 바인딩되는지 테스트

필요 시 다음 스크립트를 활용해 검증할 수 있습니다:

# FinalContractDTO가 사용된 매핑 메서드 검색
rg -n '@(PostMapping|PutMapping|PatchMapping|RequestMapping)' -A3 src/main/java | rg -C2 'FinalContractDTO'

# consumes 설정 검색
rg -n 'consumes *= *"multipart/form-data"' src/main/java
config-submodule (1)

1-1: 서브모듈 커밋 자동 검증 실패: 원격 저장소 접근 불가
스크립트가 config-submodule 리포지토리를 클론할 수 없어 자동으로 스캔 설정 포함 여부를 확인할 수 없습니다. 아래를 수동으로 점검해 주세요:

  • 새 서브모듈 커밋(821c2c2ab5a06ba5e9f3e7ed6f23041d5c36bbec)의
    • DatabaseConfig.java, RootConfig.java, ServletConfig.java, MongoConfig.java 파일에
    • org.scoula.domain.contract.(mapper|service|controller|repository) 패키지 스캔 설정이 추가되었는지
  • 구 커밋(3a3e2d7703faf1cfe63cbcddf3c4cfb12f917dcf)을 체크아웃하여 git diff로 변경 내역 확인
  • 누락된 설정이 있다면 메인 리포지토리에서 빈 미탐지로 인한 런타임 오류가 발생할 수 있으므로,
    필요한 스캔 설정을 커밋에 반영하거나 서브모듈 커밋을 재조정해 주세요

추가로, CI 파이프라인 단계에서 서브모듈 스캔 설정 유무를 자동 확인하는 스크립트를 도입할 것을 권장드립니다.

src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java (1)

11-13: 표기 정리 LGTM

트레일링 공백 제거로 표시 문자열 일관성이 좋아졌습니다. 변경 영향도 낮습니다.

src/main/java/org/scoula/global/mongodb/config/MongoConfig.java (1)

31-33: 스캔 범위 확장 LGTM

Mongo 레이어에 precontractcontract 저장소 스캔 추가가 의도와 일치합니다.

src/main/java/org/scoula/domain/contract/service/ContractService.java (1)

1-120: Spotless 포맷팅 위반 해결 필요

CI가 Spotless 위반을 보고했습니다. 로컬에서 ./gradlew :spotlessApply 실행 후 커밋해 주세요.

Comment on lines +28 to +35
// 전월세
private String rentType;
// 보증금
// 월세
// 관리비
private int depositPrice;
private int monthlyRent;
private int maintenanceFee;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

rentType 미매핑: 소스 필드 유무 확인 후 매핑/제거 결정 필요

rentType 필드가 선언되어 있으나 toDTO에서 설정되지 않습니다. Mongo Document에 대응 필드가 없다면 제거하거나, 존재한다면 매핑을 추가해야 합니다. 미설정 필드는 API 소비자에게 혼란을 줄 수 있습니다.

옵션:

  • Document에 전월세 정보가 있다면 매핑 추가
           return AIMessageDTO.builder()
                   .ownerName(document.getOwnerName())
                   .buyerName(document.getBuyerName())
+                  .rentType(document.getRentType()) // 존재 시
                   ...
  • 없다면 필드 제거 또는 Swagger 설명에 "미사용/미설정" 명시

Also applies to: 39-49


rentType 필드 매핑 검증을 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
set -e

# 1. AIMessageDTO.builder 호출 지점 검색
rg --context 3 "AIMessageDTO.builder" -n src/main/java

# 2. toDTO 메서드 검색
rg --context 3 "toDTO" -n src/main/java

# 3. Document 클래스에 getRentType 메서드 존재 여부 확인
rg --context 2 "getRentType" -n src/main/java

# 4. Document 클래스에서 rentType 필드 정의 검색
rg --context 1 "private .*rentType" -n src/main/java

Length of output: 18574


AIMessageDTO rentType 필드 미매핑: 제거 혹은 매핑 구현 필요

  • 위치
    • src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java
      • 29행: private String rentType;
      • toDTO(ContractMongoDocument) 내 Builder 호출부에 .rentType(...) 미포함
  • 원인
    • ContractMongoDocument에 rentType 필드·getter 정의가 없어 DTO가 항상 null 반환

수정 방안(택1):

  1. ContractMongoDocument에 private String rentType + getRentType() 추가 후 Builder에 매핑
  2. AIMessageDTO에서 rentType 필드 제거 및 Swagger 주석 정리

예시(diff):

-   // 전월세
-   private String rentType;
   return AIMessageDTO.builder()
           .ownerName(document.getOwnerName())
           .buyerName(document.getBuyerName())
+          .rentType(document.getRentType()) // Document에 필드 추가 후

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java around lines
28–35, the DTO declares private String rentType but the
toDTO(ContractMongoDocument) builder does not set it and ContractMongoDocument
has no rentType/getter so the DTO is always null; fix by either (A) add private
String rentType and a public getRentType() to ContractMongoDocument and update
the toDTO builder to call .rentType(contract.getRentType()), or (B) remove the
rentType field from AIMessageDTO and clean up related Swagger comments and
usages; after change, update any mapping/tests that reference rentType.

Comment on lines +86 to +97
document.getSpecialContracts().stream()
.map(
documentSpecialContract ->
SpecialContractDTO.builder()
.content(
documentSpecialContract
.getContent())
.title(documentSpecialContract.getTitle())
.order(documentSpecialContract.getOrder())
.build())
.toList())
.build();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

specialContracts null 방어 필요

document.getSpecialContracts()가 null일 경우 NPE가 발생합니다. 빈 리스트로 대체해 주세요.

-                  .specialContracts(
-                          document.getSpecialContracts().stream()
+                  .specialContracts(
+                          (document.getSpecialContracts() == null ? java.util.List.<ContractDTO.SpecialContractDTO>of() : document.getSpecialContracts())
+                                  .stream()
                                   .map(
                                       ...
-                                  .toList())
+                                  .collect(java.util.stream.Collectors.toList()))

또는 Optional.ofNullable(...).orElse(List.of()) 사용.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
document.getSpecialContracts().stream()
.map(
documentSpecialContract ->
SpecialContractDTO.builder()
.content(
documentSpecialContract
.getContent())
.title(documentSpecialContract.getTitle())
.order(documentSpecialContract.getOrder())
.build())
.toList())
.build();
.specialContracts(
(document.getSpecialContracts() == null
? java.util.List.<ContractDTO.SpecialContractDTO>of()
: document.getSpecialContracts())
.stream()
.map(
documentSpecialContract ->
SpecialContractDTO.builder()
.content(
documentSpecialContract.getContent())
.title(documentSpecialContract.getTitle())
.order(documentSpecialContract.getOrder())
.build())
.collect(java.util.stream.Collectors.toList()))
.build();
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/dto/ContractDTO.java around lines 86
to 97, the call to document.getSpecialContracts() can be null and cause an NPE;
guard this by replacing the null with an empty list before streaming — e.g. use
Optional.ofNullable(document.getSpecialContracts()).orElse(List.of()) or a
null-check that assigns Collections.emptyList(), then continue mapping to
SpecialContractDTO as before.

Comment on lines +5 to +9
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

필요한 Jackson/Lombok import 추가 제안

아래 변경이 후속 제안(@JsonInclude, @JsonProperty, @ToString.Exclude) 적용에 필요합니다.

 import io.swagger.annotations.ApiModel;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import lombok.ToString;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import io.swagger.annotations.ApiModel;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java around
lines 5 to 9, add the necessary Jackson and Lombok imports to support upcoming
annotations: include com.fasterxml.jackson.annotation.JsonInclude and
com.fasterxml.jackson.annotation.JsonProperty for JSON control, and import
lombok.ToString.Exclude (or lombok.ToString if you prefer class-level import) to
allow use of @ToString.Exclude on fields; add these import lines at the top of
the file so the subsequent @JsonInclude, @JsonProperty and @ToString.Exclude
annotations compile.

Comment on lines +18 to +21
private MultipartFile ownerTaxSignature;
private MultipartFile ownerPrioritySignature;
private MultipartFile ownerContractSignature;
private MultipartFile buyerContractSignature;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

대용량 파일 필드 로깅 노출 방지: toString 제외

Lombok @DaTa의 toString에 MultipartFile이 포함되면 로그/에러메시지로 대용량/메타데이터가 노출될 수 있습니다. @ToString.Exclude 추가를 권장합니다.

-      private MultipartFile ownerTaxSignature;
-      private MultipartFile ownerPrioritySignature;
-      private MultipartFile ownerContractSignature;
-      private MultipartFile buyerContractSignature;
+      @ToString.Exclude
+      private MultipartFile ownerTaxSignature;
+      @ToString.Exclude
+      private MultipartFile ownerPrioritySignature;
+      @ToString.Exclude
+      private MultipartFile ownerContractSignature;
+      @ToString.Exclude
+      private MultipartFile buyerContractSignature;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private MultipartFile ownerTaxSignature;
private MultipartFile ownerPrioritySignature;
private MultipartFile ownerContractSignature;
private MultipartFile buyerContractSignature;
@ToString.Exclude
private MultipartFile ownerTaxSignature;
@ToString.Exclude
private MultipartFile ownerPrioritySignature;
@ToString.Exclude
private MultipartFile ownerContractSignature;
@ToString.Exclude
private MultipartFile buyerContractSignature;
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java around
lines 18 to 21, the MultipartFile fields are included in Lombok-generated
toString via @Data causing potential large file/metadata leakage; annotate each
of the four fields (ownerTaxSignature, ownerPrioritySignature,
ownerContractSignature, buyerContractSignature) with @ToString.Exclude to
exclude them from the generated toString (and add the Lombok import if not
already present), then recompile to verify no warnings.

💡 Verification agent

🧩 Analysis chain

용어(owner/buyer) 일관성 검증 필요

코드베이스 전반에서 owner/buyer가 임대인/임차인 중 누구를 의미하는지 혼선 소지가 있습니다. 동일 의미로 일관되게 쓰였는지 확인 바랍니다. 불일치 시 문서/필드명 정정이 필요합니다.

다음 스크립트로 관련 사용처를 훑어보세요.


🏁 Script executed:

#!/bin/bash
# owner/buyer/임대인/임차인 용어 사용 위치 확인
rg -n -A2 -B2 $'\\bowner\\b|\\bbuyer\\b|임대인|임차인' src/main/java

Length of output: 79368


owner/buyer 용어 혼선 발견 – 필드명·주석을 일치시켜주세요

AIMessageDTO에서 ownerName(현재 주석: 임차인 이름), buyerName(현재 주석: 임대인 이름)이 실제 역할과 반대로 사용되고 있습니다. 아래 위치를 확인하여 필드명 또는 주석을 올바르게 교체하고, 관련 호출부도 함께 수정해 주세요.

• src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java:19-22

// 잘못된 매핑 예시
// 임차인 이름
private String ownerName;
// 임대인 이름
private String buyerName;

• src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java:178-183

// 현재 .formatted(aiDto.getBuyerName(), aiDto.getOwnerName()) 형태로
// 임대인→buyerName, 임차인→ownerName을 전달하고 있어
// 필드명이 바뀌면 호출부도 반드시 함께 업데이트해야 합니다.
String message = String.format(
    "이 계약은 임대인 %s님과 임차인 %s님의 계약입니다.",
    aiDto.getBuyerName(), aiDto.getOwnerName()
);

위 두 곳을 수정해 owner(임대인)/buyer(임차인) 의미가 코드 전체에서 일관되도록 정리 바랍니다.

🤖 Prompt for AI Agents
src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (lines 18-21)
and also fix related DTO/service files:
src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (lines 19-22) and
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (lines
178-183): the owner/buyer terms and comments are inconsistent (ownerName
currently commented as tenant and buyerName as landlord), causing swapped
semantics; standardize on owner = 임대인 (landlord) and buyer = 임차인 (tenant),
update AIMessageDTO field names or at minimum their comments to reflect this
convention, rename or keep FinalContractDTO signature fields consistent (e.g.,
owner* for landlord signatures, buyer* for tenant signatures), then update all
call sites including ContractServiceImpl to use the corrected getters (swap
parameters in the String.format call if needed) and run compilation/tests to
update any affected usages.

private MultipartFile ownerContractSignature;
private MultipartFile buyerContractSignature;

private Boolean mediation_agree; // 조정 동의 여부
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

네이밍/타입 일관화: mediation_agree → mediationAgree, Boolean → boolean

다른 DTO들은 camelCase를 사용합니다(예: PaymentDTO.depositPrice). 스네이크 케이스 필드는 내부 camelCase로 바꾸고 외부 호환성은 @JsonProperty로 유지하세요. 또한 Boolean 대신 원시 boolean 사용으로 불필요한 null 방지.

-      private Boolean mediation_agree; // 조정 동의 여부
+      @JsonProperty("mediation_agree")
+      private boolean mediationAgree; // 조정 동의 여부
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Boolean mediation_agree; // 조정 동의 여부
@JsonProperty("mediation_agree")
private boolean mediationAgree; // 조정 동의 여부
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java around
line 23, the field mediation_agree uses snake_case and boxed Boolean; rename the
field to mediationAgree (camelCase) and change its type to the primitive boolean
to avoid unnecessary nulls, and add @JsonProperty("mediation_agree") on the
field (or its accessor) to preserve external JSON compatibility; update any
getters/setters/usages to the new name and type accordingly.

Comment on lines +56 to +65
<insert id = "insertSignatureInit">
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;
</insert>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

하드코딩된 contract_chat_id 값 제거 필요

insertSignatureInit 쿼리에서 WHERE cc.contract_chat_id = 4로 하드코딩된 값이 있습니다. 이는 테스트 코드이거나 버그일 가능성이 높습니다. 파라미터를 사용하도록 수정해야 합니다.

-        WHERE cc.contract_chat_id = 4;
+        WHERE cc.contract_chat_id = #{contractChatId};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<insert id = "insertSignatureInit">
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;
</insert>
<insert id = "insertSignatureInit">
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 = #{contractChatId};
</insert>
🤖 Prompt for AI Agents
In src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
around lines 56 to 65, the INSERT uses a hardcoded WHERE cc.contract_chat_id =
4; change this to accept a parameter (e.g. WHERE cc.contract_chat_id =
#{contractChatId}) and update the <insert> declaration to include an appropriate
parameterType (e.g. parameterType="long" or the DTO that contains
contractChatId); ensure any Java caller/mapper interface passes the
contractChatId argument when invoking insertSignatureInit.

Comment on lines 73 to 113
<insert id="insertFinalContract">
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}
</insert>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

SQL 구문 오류: 정의되지 않은 테이블 별칭 사용

insertFinalContract 쿼리의 111번 줄에서 fc 별칭이 정의되지 않았는데 사용되고 있습니다. 이로 인해 런타임 SQL 오류가 발생할 것입니다.

             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
+            LEFT JOIN electronic_signature es ON es.contract_id = (
+                SELECT fc.contract_id 
+                FROM final_contract fc 
+                WHERE fc.home_id = cc.home_id AND fc.owner_id = cc.owner_id
+            )
                                            WHERE cc.contract_chat_id = #{contractChatId}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<insert id="insertFinalContract">
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}
</insert>
<insert id="insertFinalContract">
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 es.contract_id = (
SELECT fc.contract_id
FROM final_contract fc
WHERE fc.home_id = cc.home_id
AND fc.owner_id = cc.owner_id
)
WHERE cc.contract_chat_id = #{contractChatId}
</insert>
🤖 Prompt for AI Agents
In src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
around lines 73 to 113, the LEFT JOIN for electronic_signature uses an undefined
alias `fc` in the ON clause causing an SQL error; change the ON clause to
reference the defined electronic_signature alias (`es`) — i.e. use cc.home_id =
es.home_id AND cc.owner_id = es.owner_id — so the join references the correct
table alias.

restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class);
LegalityDTO res = response.getBody();
assert res != null;
log.warn("AI 응답 값 확인: {}", res.toString());

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fix the log injection vulnerability, we should sanitize the string representation of res before logging it. Specifically, we should remove any newline (\n) and carriage return (\r) characters from the string, as these can be used to forge new log entries. The best way to do this is to convert res to a string (using either toString() or, preferably, JSON serialization for a more predictable format), then replace all newline and carriage return characters with spaces or another safe character. This should be done directly before the log statement on line 399. No changes to existing functionality are required, and the fix should be limited to the region around the vulnerable log statement. If ObjectMapper is already imported (as it is), we can use it for serialization.

Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -398,3 +398,12 @@
               assert res != null;
-              log.warn("AI 응답 값 확인: {}", res.toString());
+              // Sanitize the string representation to prevent log injection
+              String resStr;
+              try {
+                  ObjectMapper objectMapper = new ObjectMapper();
+                  resStr = objectMapper.writeValueAsString(res);
+              } catch (Exception ex) {
+                  resStr = res.toString();
+              }
+              resStr = resStr.replaceAll("[\\r\\n]", " ");
+              log.warn("AI 응답 값 확인: {}", resStr);
 
EOF
@@ -398,3 +398,12 @@
assert res != null;
log.warn("AI 응답 값 확인: {}", res.toString());
// Sanitize the string representation to prevent log injection
String resStr;
try {
ObjectMapper objectMapper = new ObjectMapper();
resStr = objectMapper.writeValueAsString(res);
} catch (Exception ex) {
resStr = res.toString();
}
resStr = resStr.replaceAll("[\\r\\n]", " ");
log.warn("AI 응답 값 확인: {}", resStr);

Copilot is powered by AI and may make mistakes. Always verify output.
}
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
log.error(responseBodyStr);

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 7 months ago

To fully mitigate log injection, all user-controlled or external data logged should be sanitized to remove not only newlines and carriage returns, but also other control characters and log formatting tokens that could be abused. The best approach is to implement a utility method that sanitizes log messages by removing or escaping all non-printable characters and any characters that could affect log structure (such as tab, form feed, and log formatting tokens like {} or %). This method should be applied to responseBodyStr before logging. The fix should be implemented within the same file, above or within the relevant class, and the log statement on line 415 should use the sanitized value.

Required changes:

  • Add a private static method (e.g., sanitizeForLog) to sanitize log messages.
  • Use this method to sanitize responseBodyStr before logging it on line 415.

Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -413,3 +413,3 @@
                   // Remove newlines and carriage returns
-                  responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
+                  responseBodyStr = sanitizeForLog(responseBodyStr);
                   log.error(responseBodyStr);
@@ -423,2 +423,17 @@
       }
+
+      /**
+       * Sanitizes a string for safe logging by removing control characters and log formatting tokens.
+       * This helps prevent log injection attacks.
+       */
+      private static String sanitizeForLog(String input) {
+          if (input == null) return null;
+          // Remove control characters (except tab and space), and log formatting tokens
+          // Remove: \r, \n, \f, \b, \u0000-\u001F, \u007F, and curly braces used in log formatting
+          return input
+                  .replaceAll("[\\r\\n\\f\\b\\u0000-\\u001F\\u007F]", " ")
+                  .replace("{", "\\{")
+                  .replace("}", "\\}")
+                  .replace("%", "%%");
+      }
 
EOF
@@ -413,3 +413,3 @@
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
responseBodyStr = sanitizeForLog(responseBodyStr);
log.error(responseBodyStr);
@@ -423,2 +423,17 @@
}

/**
* Sanitizes a string for safe logging by removing control characters and log formatting tokens.
* This helps prevent log injection attacks.
*/
private static String sanitizeForLog(String input) {
if (input == null) return null;
// Remove control characters (except tab and space), and log formatting tokens
// Remove: \r, \n, \f, \b, \u0000-\u001F, \u007F, and curly braces used in log formatting
return input
.replaceAll("[\\r\\n\\f\\b\\u0000-\\u001F\\u007F]", " ")
.replace("{", "\\{")
.replace("}", "\\}")
.replace("%", "%%");
}

Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🔭 Outside diff range comments (1)
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)

1689-1712: Null 안전성: findLatestRoundForOrder 내 NPE 가능성

clause.getTitle().isEmpty()clause.getContent().isEmpty() 호출 전에 null 체크가 없습니다. NPE 발생 가능성이 높습니다. trim() 기반으로 null/공백을 함께 검사하세요.

-                                  .anyMatch(
-                                          clause ->
-                                                  clause.getOrder().equals(order.intValue())
-                                                          && !clause.getTitle().isEmpty()
-                                                          && !clause.getContent().isEmpty());
+                                  .anyMatch(clause -> {
+                                      if (!clause.getOrder().equals(order.intValue())) return false;
+                                      String t = clause.getTitle();
+                                      String c = clause.getContent();
+                                      return t != null && !t.trim().isEmpty()
+                                              && c != null && !c.trim().isEmpty();
+                                  });
♻️ Duplicate comments (16)
src/main/java/org/scoula/domain/contract/exception/ContractException.java (2)

11-11: Enum 명칭이 ‘Exception’으로 끝나 혼동 — ‘ContractErrorCode’로 리네이밍 제안

이 타입은 예외가 아니라 에러코드 컨테이너입니다. 프로젝트 컨벤션에 맞춰 ContractErrorCode로 변경하면 가독성과 의도가 명확해집니다. 파일명과 참조도 함께 업데이트해야 합니다.

-public enum ContractException implements IErrorCode {
+public enum ContractErrorCode implements IErrorCode {
  • 파일명: ContractException.java → ContractErrorCode.java
  • 사용처 전역 치환: ContractException.* → ContractErrorCode.*

12-17: HTTP 상태코드 재매핑 + 메시지 오탈자/대소문자/마침표 정리 제안

현재 대부분의 케이스가 400(BAD_REQUEST)로 매핑되어 있어 도메인 의도(미존재 vs 서버 실패 vs 외부 장애)와 불일치합니다. 또한 메시지에 오탈자/대소문자/띄어쓰기 불일치가 있습니다. 아래와 같이 일괄 정리하는 것을 제안합니다.

  • 조회/미존재: 404 NOT_FOUND
  • 저장/수정 실패(서버 내부 처리 실패): 500 INTERNAL_SERVER_ERROR
  • 외부 AI 서버 장애: 503 SERVICE_UNAVAILABLE 유지
  • 메시지: “mongoDB”→“MongoDB”, “못 했습니다.”→“못했습니다.”, “저장이 되지”→“저장되지”, 문장부호 일관화

적용 diff:

-      CONTRACT_GET("CONTRACT_4001", HttpStatus.BAD_REQUEST, "mongoDB에서 값을 조회하지 못 했습니다."),
-      CONTRACT_INSERT("CONTRACT_4002", HttpStatus.BAD_REQUEST, "MongoDB에 저장이 되지 않았습니다."),
+      CONTRACT_GET("CONTRACT_4001", HttpStatus.NOT_FOUND, "MongoDB에서 값을 조회하지 못했습니다."),
+      CONTRACT_INSERT("CONTRACT_4002", HttpStatus.INTERNAL_SERVER_ERROR, "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_UPDATE("CONTRACT_4004", HttpStatus.INTERNAL_SERVER_ERROR, "MongoDB에 수정되지 않았습니다."),
+      CONTRACT_REDIS("CONTRACT_4005", HttpStatus.NOT_FOUND, "Redis에 해당 정보가 없습니다.");
src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml (2)

56-66: 하드코딩된 contract_chat_id=4 제거 필요 (런타임 장애 유발 가능)

insertSignatureInit에 하드코딩된 4가 남아 있습니다. 파라미터를 사용하도록 교체해야 합니다. 이전 리뷰에서도 동일 지적이 있었고 아직 반영되지 않았습니다.

아래처럼 파라미터로 변경하세요(공백/스타일도 정리):

-    <insert id = "insertSignatureInit">
+    <insert id="insertSignatureInit">
@@
-        WHERE cc.contract_chat_id = 4;
+        WHERE cc.contract_chat_id = #{contractChatId};
     </insert>

또한, 현재 identity_verification iv 조인이 사용자 기준으로 한정되지 않아(소유자/세입자 각각 한 행씩 존재할 수 있음) 다중 행 삽입 또는 비의도적 매핑이 발생할 수 있습니다. 스키마가 사용자 단위라면 아래처럼 한정하는 것을 권장합니다(모델에 맞게 owner/buyer 중 선택):

-                 LEFT JOIN identity_verification iv
-                           ON iv.contract_id = cc.contract_chat_id
+                 LEFT JOIN identity_verification iv
+                           ON iv.contract_id = cc.contract_chat_id
+                          AND iv.user_id = cc.owner_id

필요 시, 중복 삽입 방지를 위해 UNIQUE 제약과 함께 INSERT IGNORE / ON DUPLICATE KEY UPDATE 전략도 고려하세요.


73-113: fc 별칭 미정의로 인한 SQL 오류 및 생성 순서 상의 논리적 결함

insertFinalContract 내 Line 111에서 정의되지 않은 fc 별칭을 참조하고 있습니다. 또한 전자서명(electronic_signature)은 final_contract의 PK를 외래키로 참조하는 구조가 일반적이므로, 계약서 생성 전에 전자서명 조인을 통해 owner_signed_at, buyer_signed_at을 끌어오는 현재 흐름은 순서상 맞지 않습니다.

즉시 가능한 최소 수정(서명 시간은 최초 INSERT 시 NULL로 두고 이후 업데이트로 반영):

@@
-            es.owner_signed_at,
-            es.buyer_signed_at,
+            NULL AS owner_signed_at,
+            NULL AS buyer_signed_at,
@@
-            LEFT JOIN electronic_signature es ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id
+            -- 전자서명은 final_contract 생성 이후 별도 업데이트에서 반영

권장 구조(트랜잭션 내 2-step):

  1. final_contract INSERT 시 생성된 PK 회수
  • MyBatis에서 useGeneratedKeys를 활성화
  1. 전자서명 초기 레코드 INSERT 시 위 PK 사용

예시(참고용 XML 스니펫):

<insert id="insertFinalContract" useGeneratedKeys="true" keyProperty="contractId">
  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,
    NULL, NULL,
    h.deposit_price, h.monthly_rent, h.maintenance_fee,
    NOW()
  FROM ...
</insert>

<insert id="insertElectronicSignatureInit">
  INSERT INTO electronic_signature (contract_id, owner_id, buyer_id, created_at)
  VALUES (#{finalContractId}, #{ownerId}, #{buyerId}, NOW())
</insert>

서비스 레이어에서 트랜잭션으로 묶고, 1)의 반환 PK를 2)에 전달하세요.

src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (3)

40-41: 주석 처리된 코드 제거 재권고

이전 코멘트와 동일합니다. 사용하지 않는 주석 코드는 삭제해 주세요.

-          //        ContractMongoDocument document = mongoTemplate.findById(contractChatId,
-          // ContractMongoDocument.class);

88-97: 특약 order 저장 시 +1 오프바이원 문제

최종 특약을 계약서 문서에 매핑할 때 fc.getOrder() + 1로 저장하고 있습니다. 반면 업데이트 로직은 리스트 인덱스를 그대로 사용하고 있어 0/1 기반 불일치가 발생합니다. 내부 저장을 0-based로 통일하세요.

-                                                  .order(fc.getOrder() + 1)
+                                                  .order(fc.getOrder())

122-133: updateSpecialContract 인덱싱 불일치 및 범위 검사 보완

현재 order를 리스트 인덱스로 직접 사용하고 있어 1-based 입력 시 마지막 항목이 갱신되지 않는 등 문제가 발생합니다. 1-based 입력 → 0-based 인덱스로 변환하고, 범위 검사를 보완하세요. 범위를 벗어나면 무시하지 말고 예외/로그를 남기세요.

-              Integer order = newClause.getOrder();
-              if (order != null && order >= 0 && order < existingClauses.size()) {
-                  ContractMongoDocument.SpecialContract target = existingClauses.get(order);
+              Integer order = newClause.getOrder();
+              if (order != null) {
+                  int idx = order > 0 ? order - 1 : order; // 1-based → 0-based
+                  if (idx >= 0 && idx < existingClauses.size()) {
+                      ContractMongoDocument.SpecialContract target = existingClauses.get(idx);
                       if (newClause.getContent() != null) {
                           target.setContent(newClause.getContent());
                       }
                       if (newClause.getTitle() != null) {
                           target.setTitle(newClause.getTitle());
                       }
-              }
+                  } else {
+                      throw new BusinessException(ContractException.CONTRACT_GET, "유효하지 않은 order: " + order);
+                  }
+              }
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (3)

60-67: Thread.sleep 제거 — 비동기/지연 실행으로 대체

서비스 스레드를 블로킹하는 Thread.sleep(2000)은 피하세요. 지연 메세지는 비동기로 전송하세요.

-          try {
-              Thread.sleep(2000);
-          } catch (InterruptedException ie) {
-              Thread.currentThread().interrupt();
-              log.warn("standByContract sleep interrupted", ie);
-          }
+          // 블로킹 회피: 2초 지연 후 비동기 전송
+          java.util.concurrent.CompletableFuture.runAsync(() ->
+                  contractChatService.AiMessageBtn(contractChatId, """
+          기다리는 동안
+          어려운 법률 용어와 법률 팁을 알아볼까요?
+          """), java.util.concurrent.CompletableFuture.delayedExecutor(2, java.util.concurrent.TimeUnit.SECONDS));
+
+          return null; // 아래 AiMessageBtn 호출은 위 비동기 블록에서 수행

참고: 위 변경 시, 아래 중복 AiMessageBtn 호출(라인 70-74)은 제거해야 합니다.


395-417: assert 제거 및 로그 인젝션/민감정보 로그 최소화

assert res != null;는 런타임에서 비활성화될 수 있으며, 응답 전체를 warn 레벨로 로깅하는 것은 민감정보/로그 인젝션 위험을 높입니다.

-              LegalityDTO res = response.getBody();
-              assert res != null;
-              log.warn("AI 응답 값 확인: {}", res.toString());
-
-              log.warn("AI 응답 헤더 확인: {}", response.getStatusCode());
+              LegalityDTO res = response.getBody();
+              if (res == null) {
+                  throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR, "AI 응답이 비어 있습니다");
+              }
+              // 필요 시 디버그 레벨에서 최소 정보만 로깅
+              if (log.isDebugEnabled()) {
+                  log.debug("AI 응답 상태: {}", response.getStatusCode());
+              }

에러 시 응답 바디는 이미 개행 제거 후 로깅하고 있으나, 가능하면 바디 로그를 제거하거나 요약만 남기십시오.


144-171: 인증/권한 검증 순서 문제: 검증 전 상태 변경 및 메시지 전송 발생

getContract에서 사용자 검증 전 상태 변경과 메시지 전송이 수행됩니다. 권한 없는 호출자가 사이드이펙트를 유발할 수 있습니다. 검증을 최우선으로 이동하고 문서 조회도 1회만 수행하세요.

-          // 스텝 변경
-          contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP1);
-
-          // 다음 단계 메세지 보내기
-          contractChatService.AiMessage(contractChatId, "이번 단계는 '정보 확인' 단계입니다");
-
-          ContractMongoDocument doc = repository.getContract(contractChatId);
-          AIMessageDTO aiDto = AIMessageDTO.toDTO(doc);
+          // userId 검증
+          validateUserId(contractChatId, userId);
+          // 문서 1회 조회
+          ContractMongoDocument doc = repository.getContract(contractChatId);
+          if (doc == null) {
+              throw new BusinessException(ContractException.CONTRACT_GET);
+          }
+          // 스텝 변경
+          contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP1);
+          // 다음 단계 메세지
+          contractChatService.AiMessage(contractChatId, "이번 단계는 '정보 확인' 단계입니다");
+          AIMessageDTO aiDto = AIMessageDTO.toDTO(doc);
 
-          // 시작 메세지 보내기
+          // 시작 메세지 보내기
           contractChatService.AiMessage(
                   contractChatId,
                   """
@@
-          // 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);
+          // 찾은 값을 Dto에 넣고 반환하기
+          ContractDTO dto = ContractDTO.toDTO(doc);
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (6)

22-30: CSRF 취약점 해결 필요

POST 요청을 사용하는 상태 변경 엔드포인트에서 CSRF 보호가 필요합니다. 이미 보안 스캐너에서 지적된 문제입니다.


32-40: CSRF 취약점 해결 필요

POST 요청을 사용하는 상태 변경 엔드포인트에서 CSRF 보호가 필요합니다. 이미 보안 스캐너에서 지적된 문제입니다.


51-59: CSRF 취약점 해결 필요

POST 요청을 사용하는 상태 변경 엔드포인트에서 CSRF 보호가 필요합니다. 이미 보안 스캐너에서 지적된 문제입니다.


61-70: CSRF 취약점 해결 필요

POST 요청을 사용하는 상태 변경 엔드포인트에서 CSRF 보호가 필요합니다. 이미 보안 스캐너에서 지적된 문제입니다.


82-91: CSRF 취약점 해결 필요

POST 요청을 사용하는 상태 변경 엔드포인트에서 CSRF 보호가 필요합니다. 이미 보안 스캐너에서 지적된 문제입니다.


122-130: CSRF 취약점 해결 필요

POST 요청을 사용하는 상태 변경 엔드포인트에서 CSRF 보호가 필요합니다. 이미 보안 스캐너에서 지적된 문제입니다.

🧹 Nitpick comments (8)
src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml (2)

7-34: DTO 매핑 확인: snake_case 필드 별칭 또는 글로벌 설정 필요

ContractDTO가 camelCase라면 MyBatis mapUnderscoreToCamelCase 설정이 켜져 있지 않은 이상 deposit_price, monthly_rent 등은 매핑되지 않습니다. 설정이 꺼져 있다면 별칭을 부여하세요.

예시:

-        h.addr1 AS homeAddr1, h.addr2 AS homeAddr2, h.residence_type, h.exclusive_area,
-        h.deposit_price, h.monthly_rent, h.maintenance_fee,
-        hd.home_floor,
+        h.addr1 AS homeAddr1, h.addr2 AS homeAddr2,
+        h.residence_type AS residenceType,
+        h.exclusive_area AS exclusiveArea,
+        h.deposit_price AS depositPrice,
+        h.monthly_rent AS monthlyRent,
+        h.maintenance_fee AS maintenanceFee,
+        hd.home_floor AS homeFloor,
         tpc.expected_move_in_date AS contractStartDate

또한 동일 contract_id에 대해 identity_verification이 다중 존재할 수 있다면(재인증 등) 최신 1건 선택 로직이 필요할 수 있습니다(서브쿼리/윈도우 함수 등).


67-71: UPDATE 태그로의 교정 확인 — LGTM

updateTaxSignature<update>로 교정되었고, 파라미터 바인딩도 적절합니다. 필요 시 updated_at 컬럼 운영 정책에 맞춰 갱신 로직 추가를 고려하세요.

src/main/java/org/scoula/domain/contract/dto/SpecialContractUpdateDTO.java (1)

17-27: DTO 유효성 검증 추가 및 order 기준(0/1-based) 명시 필요

특약 수정 시 유효성 검증 부재로 잘못된 값이 저장될 수 있습니다. 또한 order가 0/1 기반인지 명확하지 않아 리포지토리 갱신 로직과 불일치가 발생 중입니다(아래 리포지토리 코멘트 참조). DTO 차원에서 제약과 문서를 강화해 주세요.

적용 예시:

 import java.util.List;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;

 @ApiModel(description = "적법성 검사 후 수정할 특약사항")
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
 public class SpecialContractUpdateDTO {
-      private List<SpecialClauseDTO> specialClauses;
+      // 부분 업데이트 허용 시 null 허용, 아니라면 @NotNull, @Size(min=1) 고려
+      private List<SpecialClauseDTO> specialClauses;

       @Data
       @Builder
       @NoArgsConstructor
       @AllArgsConstructor
       public static class SpecialClauseDTO {
-          private Integer order;
+          @NotNull
+          @Min(1) // 내부 저장이 0-based라면 DTO는 1-based임을 명시하고 서비스/리포지토리에서 변환하세요
+          private Integer order;
-          private String title;
-          private String content;
+          @Size(max = 200)
+          private String title;
+          @Size(max = 5000)
+          private String content;
       }
 }

그리고 Swagger 문서에 “order는 1부터 시작(1-based)” 등 명확한 기준을 추가해 주세요.

src/main/java/org/scoula/domain/contract/controller/ContractController.java (1)

45-46: 입력 DTO에 @Valid 적용 권장

유효성 검증을 활성화하려면 @RequestBody DTO 파라미터에 @Valid를 추가하세요. 서비스/리포지토리에서의 NPE 및 잘못된 상태 전파를 줄일 수 있습니다.

-              @AuthenticationPrincipal CustomUserDetails userDetails,
-              @RequestBody NextStepDTO dto);
+              @AuthenticationPrincipal CustomUserDetails userDetails,
+              @RequestBody @Valid NextStepDTO dto);

-              @AuthenticationPrincipal CustomUserDetails userDetails,
-              @RequestBody PaymentDTO dto);
+              @AuthenticationPrincipal CustomUserDetails userDetails,
+              @RequestBody @Valid PaymentDTO dto);

-              @AuthenticationPrincipal CustomUserDetails userDetails,
-              @RequestBody SpecialContractUpdateDTO dto);
+              @AuthenticationPrincipal CustomUserDetails userDetails,
+              @RequestBody @Valid SpecialContractUpdateDTO dto);

Also applies to: 56-57, 82-83

src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1)

309-314: Redis 저장에 TTL 추가로 고아 데이터 방지

결제 협상 중단 시 Redis 키가 영구 잔존할 수 있습니다. 만료 시간을 설정해 주세요.

-              stringRedisTemplate.opsForValue().set(redisKey, json);
+              stringRedisTemplate.opsForValue().set(redisKey, json);
+              // 30분 후 만료 (필요에 따라 조정)
+              stringRedisTemplate.expire(redisKey, java.time.Duration.ofMinutes(30));
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (3)

42-49: POST 메서드 대신 GET 메서드 사용 권장

데이터를 조회하는 기능인 getContract는 상태를 변경하지 않으므로 POST보다는 GET 메서드가 더 적절합니다. RESTful API 설계 원칙에 맞게 수정하는 것을 고려해보세요.

-@PostMapping("/getContract")
+@GetMapping("/getContract")

72-80: POST 메서드 대신 GET 메서드 사용 권장

가격 정보를 조회하는 기능인 getDepositPrice는 상태를 변경하지 않으므로 POST보다는 GET 메서드가 더 적절합니다.

-@PostMapping("/getPrice")
+@GetMapping("/getPrice")

113-120: POST 메서드 대신 GET 메서드 사용 권장

적법성 정보를 조회하는 기능인 getLegality는 상태를 변경하지 않으므로 POST보다는 GET 메서드가 더 적절합니다.

-@PostMapping("/legality")
+@GetMapping("/legality")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28a3ad2 and 8e65ad0.

📒 Files selected for processing (13)
  • src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/service/ChatServiceInterface.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (3 hunks)
  • src/main/java/org/scoula/domain/contract/controller/ContractController.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/SpecialContractUpdateDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/exception/ContractException.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractService.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1 hunks)
  • src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/main/java/org/scoula/domain/chat/service/ChatServiceInterface.java
  • src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java
  • src/main/java/org/scoula/domain/contract/service/ContractService.java
  • src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (1)
src/main/java/org/scoula/global/common/exception/BusinessException.java (1)
  • BusinessException (4-21)
src/main/java/org/scoula/domain/contract/controller/ContractController.java (1)
src/main/java/org/scoula/domain/chat/controller/AiClauseImproveTestController.java (1)
  • Api (24-151)
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (2)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (1)
  • Service (31-856)
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)
  • Service (34-1927)
🪛 GitHub Actions: CI
src/main/java/org/scoula/domain/contract/exception/ContractException.java

[error] 19-21: spotlessJavaCheck failed due to formatting violations in ContractException.java. Run './gradlew :spotlessApply' to fix.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CodeQL Security Analysis (java)
🔇 Additional comments (6)
src/main/java/org/scoula/domain/contract/exception/ContractException.java (1)

9-10: Lombok 활용 및 불변 필드 구성 적절

@Getter + @requiredargsconstructor와 final 필드 조합으로 IErrorCode 계약을 깔끔하게 충족하고 불변성을 유지하고 있습니다. 좋습니다.

src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml (1)

73-113: 금액(보증금/월세/관리비) 소스 확인 필요: PR 목표와 일치 여부

insertFinalContracthome 테이블의 금액을 사용합니다. 본 PR 목표에 따르면 금액 조율은 Redis/Mongo를 통해 합의 후 MongoDB에 영속화됩니다. 최종 계약서 생성 시 합의 금액을 반영해야 한다면 파라미터로 전달하거나 별도 조회를 통해 반영해야 합니다.

합의 금액을 반영하려면 아래와 같이 파라미터 사용을 고려하세요(예시):

-            h.deposit_price,
-            h.monthly_rent,
-            h.maintenance_fee,
+            #{agreedDepositPrice},
+            #{agreedMonthlyRent},
+            #{agreedMaintenanceFee},

서비스/리포지토리 단에서 Mongo 합의값을 읽어 파라미터로 전달하는 방식이 안전합니다.

src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)

137-152: AI 보조 메세지 전송(AiMessageNext) 추가는 적절합니다

웹소켓 전송, lastMessage 업데이트 흐름이 기존 AiMessage/AiMessageBtn과 일관됩니다.

src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1)

296-317: 역할 검증 누락: 금액 제안/수락/거절 호출자 검증 필요

비즈니스 정의상

  • 임대인: 제안 저장(saveDepositPrice)
  • 임차인: 거절(deleteDepositPrice)
  • 양측 동의: 수락(updateDepositPrice)

현재 호출자 역할 검증이 없어 오남용 위험이 있습니다. contractChatMapper.findByContractChatId로 역할을 확인해 적절치 않으면 예외를 던지세요.

원하시면 역할 검증 보일러플레이트를 각 메서드에 추가하는 패치를 생성해 드립니다.

src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (2)

14-18: 컨트롤러 구현이 완료되었습니다

13개의 엔드포인트가 모두 구현되어 있고, 각 메서드는 적절하게 서비스 레이어에 위임하며 ApiResponse로 래핑하여 반환합니다. 인증 처리와 경로 변수 바인딩도 올바르게 구현되었습니다.


103-111: 시그니처 일치 확인 완료
ContractController 인터페이스와 구현체(ContractControllerImpl)의 updateDepositPrice 메서드는 모두 Long contractChatIdCustomUserDetails userDetails만을 파라미터로 사용하고 있어 불일치가 없습니다. DTO 파라미터(SpecialContractUpdateDTO) 추가는 필요하지 않습니다.

Comment on lines +284 to 289
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);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

order 파라미터 유효성 검증 누락

현재 order에 대한 null/범위(예: 1..6) 검증이 없어 잘못된 조항 업데이트/AI 요청이 가능해집니다. 유효성 검증을 추가하고 실패 시 명확한 예외를 반환하세요.

적용 예시:

-          SpecialContractFixDocument improveClauseRequest =
-                  updateRecentData(contractChatId, order, result);
+          if (order == null || order < 1 || order > 6) {
+              throw new IllegalArgumentException("유효하지 않은 특약 order: " + order);
+          }
+          SpecialContractFixDocument improveClauseRequest =
+                  updateRecentData(contractChatId, order, result);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
if (order == null || order < 1 || order > 6) {
throw new IllegalArgumentException("유효하지 않은 특약 order: " + order);
}
SpecialContractFixDocument improveClauseRequest =
updateRecentData(contractChatId, order, result);
ClauseImproveResponseDto improveClauseResponse = getAiClauseImprove(improveClauseRequest);
updateSpecialClause(contractChatId, improveClauseResponse);
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 284-289, the method uses the order parameter without validation
which allows null or out-of-range values to proceed and trigger incorrect
updates/AI requests; add a precondition check that order is not null and within
the valid range (e.g., 1..6) before calling updateRecentData/getAiClauseImprove,
and if the check fails throw a clear, specific runtime exception (for example
IllegalArgumentException or a domain-specific exception) with a descriptive
message so callers receive an immediate, understandable error rather than
proceeding with invalid input.

Comment on lines +1581 to +1687
@Override
@Transactional
public FinalSpecialContractDocument saveFinalSpecialContract(Long contractChatId) {
ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
ContractChat.ContractStatus currentStatus = contractChat.getStatus();

boolean isThirdRoundComplete = (currentStatus == ContractChat.ContractStatus.ROUND3);

List<FinalSpecialContractDocument.FinalClause> finalClauses = new ArrayList<>();

if (isThirdRoundComplete) {
log.info("=== 3회차 수정 완료 - 4라운드 데이터에서 최종 특약 생성 ===");

Optional<SpecialContractDocument> 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<SpecialContractFixDocument> incompleteContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(
contractChatId, false);

if (!incompleteContracts.isEmpty()) {
throw new IllegalStateException(
"아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
}

List<SpecialContractFixDocument> 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<FinalSpecialContractDocument.FinalClause> finalClauses = new ArrayList<>();

if (isThirdRoundComplete) {
log.info("=== 3회차 수정 완료 - 4라운드 데이터에서 최종 특약 생성 ===");

Optional<SpecialContractDocument> 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<SpecialContractFixDocument> incompleteContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(contractChatId, false);

if (!incompleteContracts.isEmpty()) {
throw new IllegalStateException(
"아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
}

List<SpecialContractFixDocument> completedContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(contractChatId, true);

if (completedContracts.isEmpty()) {
throw new IllegalStateException("완료된 특약이 없습니다.");
}

for (SpecialContractFixDocument completedContract : completedContracts) {
Long order = completedContract.getOrder();

Optional<SpecialContractDocument> 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<SpecialContractDocument> 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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예외 타입 일관성 및 도메인 예외 사용

saveFinalSpecialContract에서 완료 조건 미충족 시 IllegalStateException을 던지고 있습니다. 서비스 계층 전반에서 BusinessException + 도메인 에러코드 패턴을 사용 중이므로 동일하게 맞추는 것이 좋습니다.

-              if (!incompleteContracts.isEmpty()) {
-                  throw new IllegalStateException(
-                          "아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
-              }
+              if (!incompleteContracts.isEmpty()) {
+                  throw new BusinessException(
+                          ChatErrorCode.CONTRACT_END_REQUEST_INVALID,
+                          "아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
+              }

필요 시 별도의 에러코드 추가를 고려하세요(예: CONTRACT_FINALIZE_INCOMPLETE).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@Transactional
public FinalSpecialContractDocument saveFinalSpecialContract(Long contractChatId) {
ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
ContractChat.ContractStatus currentStatus = contractChat.getStatus();
boolean isThirdRoundComplete = (currentStatus == ContractChat.ContractStatus.ROUND3);
List<FinalSpecialContractDocument.FinalClause> finalClauses = new ArrayList<>();
if (isThirdRoundComplete) {
log.info("=== 3회차 수정 완료 - 4라운드 데이터에서 최종 특약 생성 ===");
Optional<SpecialContractDocument> 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<SpecialContractFixDocument> incompleteContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(
contractChatId, false);
if (!incompleteContracts.isEmpty()) {
throw new IllegalStateException(
"아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
}
List<SpecialContractFixDocument> 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<FinalSpecialContractDocument.FinalClause> finalClauses = new ArrayList<>();
if (isThirdRoundComplete) {
log.info("=== 3회차 수정 완료 - 4라운드 데이터에서 최종 특약 생성 ===");
Optional<SpecialContractDocument> 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<SpecialContractFixDocument> incompleteContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(contractChatId, false);
if (!incompleteContracts.isEmpty()) {
throw new IllegalStateException(
"아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
}
List<SpecialContractFixDocument> completedContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(contractChatId, true);
if (completedContracts.isEmpty()) {
throw new IllegalStateException("완료된 특약이 없습니다.");
}
for (SpecialContractFixDocument completedContract : completedContracts) {
Long order = completedContract.getOrder();
Optional<SpecialContractDocument> 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<SpecialContractDocument> 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;
}
// 기존: 완료되지 않은 특약이 있으면 IllegalStateException
List<SpecialContractFixDocument> incompleteContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(
contractChatId, false);
- if (!incompleteContracts.isEmpty()) {
- throw new IllegalStateException(
- "아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
- }
+ if (!incompleteContracts.isEmpty()) {
+ throw new BusinessException(
+ ChatErrorCode.CONTRACT_END_REQUEST_INVALID,
+ "아직 완료되지 않은 특약이 " + incompleteContracts.size() + "개 있습니다.");
+ }
List<SpecialContractFixDocument> completedContracts =
specialContractMongoRepository.findByContractChatIdAndIsPassed(
contractChatId, true);
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 1581-1687, replace the two IllegalStateException throws used when
unfinished or no completed special contracts are detected with the application's
BusinessException pattern using a domain error code (e.g.,
CONTRACT_FINALIZE_INCOMPLETE for "아직 완료되지 않은 특약..." and
CONTRACT_FINALIZE_NO_COMPLETED for "완료된 특약이 없습니다."). Create or reuse appropriate
ErrorCode enum entries if missing, construct BusinessException instances with
the new error code and a clear message (include the incomplete count for the
first case), and ensure imports and tests are updated accordingly so the service
follows the existing domain exception convention.

Comment on lines +17 to +21
CONTRACT_REDIS("CONTRACT_4005", HttpStatus.BAD_REQUEST, "REDIS에 해당 정보가 없습니다.");
private final String code;
private final HttpStatus httpStatus;
private final String message;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Spotless 포맷팅 실패(파이프라인 깨짐) — 공백 라인 추가 및 자동 포맷 실행 권고

CI 로그에 따르면 spotlessJavaCheck가 본 파일에서 포맷 위반(19-21라인)으로 실패했습니다. 우선 아래처럼 enum 상수와 필드 사이에 공백 한 줄을 추가하면 일반적인 스타일을 충족합니다. 이후 자동 포맷을 실행해 주세요.

-      CONTRACT_REDIS("CONTRACT_4005", HttpStatus.NOT_FOUND, "Redis에 해당 정보가 없습니다.");
-      private final String code;
-      private final HttpStatus httpStatus;
-      private final String message;
+      CONTRACT_REDIS("CONTRACT_4005", HttpStatus.NOT_FOUND, "Redis에 해당 정보가 없습니다.");
+
+      private final String code;
+      private final HttpStatus httpStatus;
+      private final String message;

추가로 아래 명령으로 포맷팅을 적용하면 파이프라인가 복구됩니다:

  • ./gradlew :spotlessApply

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: CI

[error] 19-21: spotlessJavaCheck failed due to formatting violations in ContractException.java. Run './gradlew :spotlessApply' to fix.

🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/exception/ContractException.java
around lines 17 to 21, Spotless reported a formatting failure: add a single
blank line between the enum constant list and the following field declarations
(i.e., insert one empty line after CONTRACT_REDIS(...) and before "private final
String code;"), then run the formatter to apply project style (execute ./gradlew
:spotlessApply) to fix remaining whitespace/format issues.

Comment on lines +207 to +244
@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);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

신뢰할 수 없는 클라이언트 입력 사용 — nextStep에서 권한 위조 가능

클라이언트가 보낸 dto.isOwner()/isBuyer()를 그대로 신뢰하면 역할 위조가 가능합니다. DB에서 userId로 역할을 판별하고 그 결과만 저장하세요.

-            // 2) 이번 요청 값 반영 (이제 step은 DTO에서 받지 않음, DB 상태는 필요 시 별도 조회)
-            if (dto.isOwner()) {
-                state.setOwner(true);
-            }
-            if (dto.isBuyer()) {
-                state.setBuyer(true);
-            }
+            // 2) 이번 요청자 역할 판별 (서버에서 신뢰 가능한 정보로 설정)
+            ContractChat chat = contractChatMapper.findByContractChatId(contractChatId);
+            if (chat == null) {
+                throw new BusinessException(ContractException.CONTRACT_GET, "계약 채팅을 찾을 수 없습니다.");
+            }
+            boolean isOwner = userId.equals(chat.getOwnerId());
+            boolean isBuyer = userId.equals(chat.getBuyerId());
+            if (!isOwner && !isBuyer) {
+                throw new BusinessException(ContractException.CONTRACT_GET, "권한 없는 사용자입니다.");
+            }
+            if (isOwner) state.setOwner(true);
+            if (isBuyer) state.setBuyer(true);

추가로 Redis 키에 TTL을 설정해 장기 고아 상태를 방지하는 것을 권장합니다(예: 24h).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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);
}
}
@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) 이번 요청자 역할 판별 (서버에서 신뢰 가능한 정보로 설정)
ContractChat chat = contractChatMapper.findByContractChatId(contractChatId);
if (chat == null) {
throw new BusinessException(ContractException.CONTRACT_GET, "계약 채팅을 찾을 수 없습니다.");
}
boolean isOwner = userId.equals(chat.getOwnerId());
boolean isBuyer = userId.equals(chat.getBuyerId());
if (!isOwner && !isBuyer) {
throw new BusinessException(ContractException.CONTRACT_GET, "권한 없는 사용자입니다.");
}
if (isOwner) state.setOwner(true);
if (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);
}
}
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
around lines 207 to 244, the method nextStep currently trusts
dto.isOwner()/isBuyer() coming from the client which allows role spoofing;
instead load the user’s actual role for the given contract from the DB (using
userId/contractChatId) and derive owner/buyer booleans server-side, apply those
to the Redis state (ignore role flags in dto), and when writing the Redis key
set an expiration (e.g., 24 hours) to avoid orphaned keys; keep existing logic
for merging state and deleting the key when both parties confirmed, and wrap DB
lookups in the existing try/catch to still throw BusinessException on failure.

Comment on lines +250 to +294
// 스텝 변경
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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

권한 검증 순서 및 메시지 포맷 오류(임대인/임차인 이름 순서 뒤바뀜)

  • 검증 전에 상태 변경/메시지 전송이 수행됩니다. 먼저 검증하세요.
  • “임대인(%s), 임차인(%s)” 포맷의 파라미터 순서가 바뀌어 있습니다.
-          // 스텝 변경
-          contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2);
-
-          // 다음 단계 메세지 보내기
-          contractChatService.AiMessage(contractChatId, "이번 단계는 '금액 조율' 단계입니다");
-
-          ContractMongoDocument doc = repository.getContract(contractChatId);
-          AIMessageDTO aiDto = AIMessageDTO.toDTO(doc);
+          // userId 검증
+          validateUserId(contractChatId, userId);
+          // 스텝 변경
+          contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2);
+          // 다음 단계 메세지
+          contractChatService.AiMessage(contractChatId, "이번 단계는 '금액 조율' 단계입니다");
+          ContractMongoDocument doc = repository.getContract(contractChatId);
+          if (doc == null) {
+              throw new BusinessException(ContractException.CONTRACT_GET);
+          }
+          AIMessageDTO aiDto = AIMessageDTO.toDTO(doc);
@@
-          contractChatService.AiMessage(
-                  contractChatId, """
-          자유롭게 채팅 후 임대인(%s)님께서 금액을 조정해주세요. 임차인(%s)님이 수락 후 해당 조건의 확정이 가능합니다.
-          """.formatted(aiDto.getBuyerName(), aiDto.getOwnerName()));
+          contractChatService.AiMessage(
+                  contractChatId, """
+          자유롭게 채팅 후 임대인(%s)님께서 금액을 조정해주세요. 임차인(%s)님이 수락 후 해당 조건의 확정이 가능합니다.
+          """.formatted(aiDto.getOwnerName(), aiDto.getBuyerName()));
@@
-          // userId 검증
-          validateUserId(contractChatId, userId);
+          // (이미 상단에서 검증 완료)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 스텝 변경
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;
}
// userId 검증
validateUserId(contractChatId, userId);
// 스텝 변경
contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.STEP2);
// 다음 단계 메세지
contractChatService.AiMessage(contractChatId, "이번 단계는 '금액 조율' 단계입니다");
ContractMongoDocument doc = repository.getContract(contractChatId);
if (doc == null) {
throw new BusinessException(ContractException.CONTRACT_GET);
}
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.getOwnerName(), aiDto.getBuyerName()));
// (이미 상단에서 검증 완료)
// MongoDB에서 보증금, 계약금, 잔금, 월세를 조회한다
ContractMongoDocument document = repository.getDepositPrice(contractChatId);
if (document == null) {
throw new BusinessException(ContractException.CONTRACT_GET);
}
// 조회된 금액을 리턴한다.
PaymentDTO dto = PaymentDTO.toDTO(document);
return dto;
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
around lines 250 to 294, the permission check is performed after changing the
contract status and sending AI messages and the formatted message arguments for
landlord/tenant names are reversed; move the validateUserId(contractChatId,
userId) call to the top of this block (before contractChatMapper.updateStatus
and any contractChatService.AiMessage calls) so authorization is verified first,
and swap the name parameters in the formatted message so the owner (임대인) is
passed first and the buyer/tenant (임차인) second (i.e., use aiDto.getOwnerName()
then aiDto.getBuyerName()) when building the string.

Comment on lines +48 to +55
<select id="selectFinalContractId" resultType="Long">
SELECT fc.contract_id
FROM contract_chat cc
INNER JOIN final_contract fc
ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id
WHERE cc.contract_chat_id = #{contractChatId}
</select>

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

최신/정확한 계약 선택 보장 필요: 다중행 위험 및 잘못된 매칭 가능성

selectFinalContractId는 (home_id, owner_id)만으로 조인합니다. 동일 주택/소유자에 대해 여러 차수 계약이 존재하거나, 채팅별/세입자별로 구분이 필요한 경우 오동작/TooManyResultsException이 발생할 수 있습니다.

  • 세입자까지 포함해 유일 매칭을 보장
  • 다수일 경우 최신 1건을 명시적으로 선택

제안 수정:

     <select id="selectFinalContractId" resultType="Long">
         SELECT fc.contract_id
         FROM contract_chat cc
                  INNER JOIN final_contract fc
-                            ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id
+                            ON cc.home_id = fc.home_id
+                           AND cc.owner_id = fc.owner_id
+                           AND cc.buyer_id = fc.buyer_id
         WHERE cc.contract_chat_id = #{contractChatId}
+        ORDER BY fc.created_at DESC
+        LIMIT 1
     </select>

스키마/업무 규칙상 고유키가 다르면 해당 조건을 반영해 주세요.


🏁 Script executed:

#!/bin/bash
set -e

echo "Locating ContractMapper.xml:"
fd ContractMapper.xml

echo -e "\nSearching for selectFinalContractId occurrences:"
rg "selectFinalContractId" -n .

echo -e "\nSearching for buyer_id occurrences:"
rg "buyer_id" -n .

echo -e "\nSearching for created_at occurrences:"
rg "created_at" -n .

echo -e "\nSearching for ContractChat domain class:"
rg "class .*ContractChat" -n src

echo -e "\nSearching for FinalContract domain class:"
rg "class .*FinalContract" -n src

echo -e "\nSearching for contract_chat table references:"
rg "contract_chat" -n .

Length of output: 29016


🏁 Script executed:

#!/bin/bash
set -e
echo "final_contract 테이블 스키마 확인 (라인 130–180):"
sed -n '130,180p' init/database.sql

Length of output: 2811


ContractMapper.xml – selectFinalContractId 조인 조건 및 유일성 보장 필요

현재 <select id="selectFinalContractId">home_id·owner_id(제안대로라면 buyer_id도)로만 조인하여 과거 계약이 여러 건일 경우 잘못된 결과가 리턴될 수 있습니다.
하지만 final_contract 테이블은 contract_idcontract_chat.contract_chat_id의 PK/FK로 참조하므로, 조인 조건을 아래와 같이 수정하면 1:1 매핑이 보장됩니다.

  • 조인 조건을 ON fc.contract_id = cc.contract_chat_id 로 변경
  • 별도의 ORDER BY/LIMIT 는 불필요

제안된 수정 예시:

     <select id="selectFinalContractId" resultType="Long">
-        SELECT fc.contract_id
-        FROM contract_chat cc
-                 INNER JOIN final_contract fc
-                            ON cc.home_id = fc.home_id
-                           AND cc.owner_id = fc.owner_id
+        SELECT fc.contract_id
+        FROM contract_chat cc
+                 INNER JOIN final_contract fc
+                            ON fc.contract_id = cc.contract_chat_id
         WHERE cc.contract_chat_id = #{contractChatId}
     </select>

– 스키마상 PK/FK 매핑만으로도 유일성이 보장되므로 위와 같이 조인만 변경해 주시면 됩니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<select id="selectFinalContractId" resultType="Long">
SELECT fc.contract_id
FROM contract_chat cc
INNER JOIN final_contract fc
ON cc.home_id = fc.home_id AND cc.owner_id = fc.owner_id
WHERE cc.contract_chat_id = #{contractChatId}
</select>
<select id="selectFinalContractId" resultType="Long">
SELECT fc.contract_id
FROM contract_chat cc
INNER JOIN final_contract fc
ON fc.contract_id = cc.contract_chat_id
WHERE cc.contract_chat_id = #{contractChatId}
</select>
🤖 Prompt for AI Agents
In src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
around lines 48 to 55, the current join uses home_id and owner_id which can
return wrong rows when multiple past contracts exist; replace the join condition
so final_contract is joined to contract_chat by the PK/FK relationship (ON
fc.contract_id = cc.contract_chat_id) to guarantee 1:1 mapping and return the
correct contract_id, and remove any ORDER BY/LIMIT additions since the PK/FK
join ensures uniqueness.

Comment on lines +121 to +126
<select id="getDepositAdjustment" resultType="boolean">
SELECT deposit_adjustment
FROM tenant_precontract_check
WHERE contract_chat_id = #{contractChatId}
</select>
</mapper> No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Boolean 매핑 안정화 권장

행이 없거나 NULL일 수 있는 경우를 대비해 resultType="java.lang.Boolean"로 명시하는 편이 안전합니다. 현재 "boolean"은 프리미티브 매핑으로 NULL을 허용하지 않습니다.

-    <select id="getDepositAdjustment" resultType="boolean">
+    <select id="getDepositAdjustment" resultType="java.lang.Boolean">
         SELECT deposit_adjustment
         FROM tenant_precontract_check
         WHERE contract_chat_id = #{contractChatId}
     </select>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<select id="getDepositAdjustment" resultType="boolean">
SELECT deposit_adjustment
FROM tenant_precontract_check
WHERE contract_chat_id = #{contractChatId}
</select>
</mapper>
<select id="getDepositAdjustment" resultType="java.lang.Boolean">
SELECT deposit_adjustment
FROM tenant_precontract_check
WHERE contract_chat_id = #{contractChatId}
</select>
</mapper>
🤖 Prompt for AI Agents
In src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
around lines 121 to 126, the select mapping uses resultType="boolean" which maps
to a primitive and cannot represent NULL or missing rows; change resultType to
"java.lang.Boolean" (the wrapper type) so NULL values or absent rows are handled
safely, and ensure callers handle possible null returns appropriately.

Copy link
Contributor

@MeongW MeongW left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다.

@MeongW MeongW merged commit 004f760 into develop Aug 12, 2025
4 of 7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Aug 17, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능 요청

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 00 공통 메서드 [Feat] 04 적법성 확인 [Feat] 02 금액 조율 [Feat] 01 정보 확인 [Feat] 계약서

2 participants