Conversation
Walkthrough계약 도메인 신규 모듈을 추가하고 컨트롤러·서비스·리포지토리·매퍼·DTO·예외 등을 도입했습니다. MongoDB에 계약서를 저장·조회·갱신하고 Redis로 금액 조율 상태를 관리하며 외부 AI 서버에 적법성 검증을 요청합니다. Changes
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<...>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
Fixed
Show fixed
Hide fixed
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
Fixed
Show fixed
Hide fixed
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
Fixed
Show fixed
Hide fixed
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
Fixed
Show fixed
Hide fixed
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
Fixed
Show fixed
Hide fixed
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
Fixed
Show fixed
Hide fixed
There was a problem hiding this comment.
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 체인 개선
ContractDurationenum에 메서드를 추가하여 코드를 간소화할 수 있습니다.- 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);
ContractDurationenum에 다음 메서드 추가: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
📒 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년 계약문자열은 현재ContractDurationenum 정의부에서만 사용되고 있습니다.
–displayName기반의equals/contains비교도 검색되지 않았습니다.추가로 아래 영역을 반드시 검토해 주세요:
- API 응답 직렬화 시 해당 문자열을 함께 내려주는 부분 여부
- DB 저장 값으로서의 활용(매핑 컬럼 등)
- 클라이언트/서버 하드코딩 비교, 스냅샷 테스트, 문서 예시 등에 의존성 존재 여부
위 확인 후 문제가 없으면 머지 진행 부탁드립니다.
src/main/java/org/scoula/global/mongodb/config/MongoConfig.java (1)
31-33: 스캔 범위에 contract.repository 추가: OKMongoTemplate를 사용하는 수동 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 | |||
There was a problem hiding this comment.
💡 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."
fiLength 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 디렉토리 없음."
fiLength 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 --recursive및git submodule update --remote명령을 실행하지만,actions/checkout에submodules: true설정은 적용되어 있지 않습니다.
조치 항목:
.gitmodules의 URL과 SHA 설정을 올바른 값으로 수정하고, 누락된config서브모듈 커밋을 HEAD에 반영해주세요.- 원격 저장소 존재 여부 및 접근 권한(토큰·SSH) 문제를 해결하세요.
- CI 설정에
actions/checkout단계에서submodules: true옵션을 추가하거나, 인증된 방식으로 서브모듈을 초기화하도록 구성해주세요. - 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.
src/main/java/org/scoula/domain/contract/controller/ContractController.java
Outdated
Show resolved
Hide resolved
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
Outdated
Show resolved
Hide resolved
| 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(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
toDocument 메서드 개선 필요
메서드에 몇 가지 개선사항이 있습니다:
- DTO null 체크 누락
- 날짜 변환 시 null 체크 누락
- 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.
| 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.
There was a problem hiding this comment.
@minnieming 개선사항을 적용해주셔서 감사합니다! null 안전성과 제네릭 타입 처리가 이제 더 안전하고 깔끔해졌네요. 👍
\(^▽^)/ 깔끔하게 수정 완료!
If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!
| @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; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| @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.
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
Outdated
Show resolved
Hide resolved
| <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> |
There was a problem hiding this comment.
하드코딩된 값 제거 필요
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.
| <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.
src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
Outdated
Show resolved
Hide resolved
| 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> |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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만 추가되어 있습니다.ContractMongoRepository는org.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 이름
SpecialContractDTO가ContractDTO.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))필요 시
DateTimeFormatterimport 추가.src/main/java/org/scoula/domain/contract/document/ContractMongoDocument.java (1)
52-56: 문서 내 날짜를 String으로 저장하는 설계 재검토(선택)
contractStartDate/contractEndDate를 String으로 저장하고 DTO 변환 시 파싱합니다. 스키마/인덱스/타입 안전성을 고려하면 Mongo에서도LocalDate(또는Date) 타입 저장이 더 안전합니다. 기존 데이터 호환이 필요하다면 마이그레이션 전략을 별도 이슈로 관리하는 것을 권장합니다.필요 시
@Field와Converters기반으로LocalDate<->Date변환 설정까지 제안 드릴 수 있습니다.src/main/java/org/scoula/domain/contract/dto/ContractDTO.java (1)
100-113: JDK 의존성/일관성: toList vs Collectors.toList 통일위
toDTO는stream().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 재사용
getDepositPrice가getContract와 동일 쿼리를 수행합니다. 주석 처리된 코드도 남아 있습니다. 중복 제거와 주석 정리를 권장합니다.- 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)은 현재 스레드를 블로킹하여 성능 문제를 일으킬 수 있습니다. 비동기 처리나 스케줄링을 고려해 보세요.더 나은 방법:
CompletableFuture.delayedExecutor를 사용한 비동기 처리- Spring의
@Async어노테이션 활용- 메시지 큐를 통한 지연 처리
- 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 제거 필요
HttpServletResponse와MultipartFile은 사용되지 않으므로 제거해야 합니다.-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
📒 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/javaconfig-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: 스캔 범위 확장 LGTMMongo 레이어에
precontract와contract저장소 스캔 추가가 의도와 일치합니다.src/main/java/org/scoula/domain/contract/service/ContractService.java (1)
1-120: Spotless 포맷팅 위반 해결 필요CI가 Spotless 위반을 보고했습니다. 로컬에서
./gradlew :spotlessApply실행 후 커밋해 주세요.
| // 전월세 | ||
| private String rentType; | ||
| // 보증금 | ||
| // 월세 | ||
| // 관리비 | ||
| private int depositPrice; | ||
| private int monthlyRent; | ||
| private int maintenanceFee; |
There was a problem hiding this comment.
💡 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/javaLength of output: 18574
AIMessageDTO rentType 필드 미매핑: 제거 혹은 매핑 구현 필요
- 위치
- src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java
- 29행:
private String rentType; toDTO(ContractMongoDocument)내 Builder 호출부에.rentType(...)미포함
- 29행:
- src/main/java/org/scoula/domain/contract/dto/AIMessageDTO.java
- 원인
- ContractMongoDocument에
rentType필드·getter 정의가 없어 DTO가 항상null반환
- ContractMongoDocument에
수정 방안(택1):
- ContractMongoDocument에
private String rentType+getRentType()추가 후 Builder에 매핑 - 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.
| document.getSpecialContracts().stream() | ||
| .map( | ||
| documentSpecialContract -> | ||
| SpecialContractDTO.builder() | ||
| .content( | ||
| documentSpecialContract | ||
| .getContent()) | ||
| .title(documentSpecialContract.getTitle()) | ||
| .order(documentSpecialContract.getOrder()) | ||
| .build()) | ||
| .toList()) | ||
| .build(); |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| import io.swagger.annotations.ApiModel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Data; | ||
| import lombok.NoArgsConstructor; |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| private MultipartFile ownerTaxSignature; | ||
| private MultipartFile ownerPrioritySignature; | ||
| private MultipartFile ownerContractSignature; | ||
| private MultipartFile buyerContractSignature; |
There was a problem hiding this comment.
🛠️ 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.
| 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/javaLength 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; // 조정 동의 여부 |
There was a problem hiding this comment.
🛠️ 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.
| 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.
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
Outdated
Show resolved
Hide resolved
| <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> |
There was a problem hiding this comment.
하드코딩된 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.
| <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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml
Outdated
Show resolved
Hide resolved
| 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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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); | ||
|
|
| } | ||
| // Remove newlines and carriage returns | ||
| responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " "); | ||
| log.error(responseBodyStr); |
Check failure
Code scanning / CodeQL
Log Injection High
Show autofix suggestion
Hide autofix suggestion
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
responseBodyStrbefore logging it on line 415.
| @@ -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("%", "%%"); | ||
| } | ||
|
|
There was a problem hiding this comment.
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):
- final_contract INSERT 시 생성된 PK 회수
- MyBatis에서 useGeneratedKeys를 활성화
- 전자서명 초기 레코드 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라면 MyBatismapUnderscoreToCamelCase설정이 켜져 있지 않은 이상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
📒 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 목표와 일치 여부
insertFinalContract는home테이블의 금액을 사용합니다. 본 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 contractChatId와CustomUserDetails userDetails만을 파라미터로 사용하고 있어 불일치가 없습니다. DTO 파라미터(SpecialContractUpdateDTO) 추가는 필요하지 않습니다.
| 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); | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| @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; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| @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.
| CONTRACT_REDIS("CONTRACT_4005", HttpStatus.BAD_REQUEST, "REDIS에 해당 정보가 없습니다."); | ||
| private final String code; | ||
| private final HttpStatus httpStatus; | ||
| private final String message; | ||
| } |
There was a problem hiding this comment.
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.
| @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); | ||
| } | ||
| } |
There was a problem hiding this comment.
신뢰할 수 없는 클라이언트 입력 사용 — 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.
| @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.
| // 스텝 변경 | ||
| 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; | ||
| } |
There was a problem hiding this comment.
권한 검증 순서 및 메시지 포맷 오류(임대인/임차인 이름 순서 뒤바뀜)
- 검증 전에 상태 변경/메시지 전송이 수행됩니다. 먼저 검증하세요.
- “임대인(%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.
| // 스텝 변경 | |
| 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.
| <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> | ||
|
|
There was a problem hiding this comment.
💡 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.sqlLength of output: 2811
ContractMapper.xml – selectFinalContractId 조인 조건 및 유일성 보장 필요
현재 <select id="selectFinalContractId">는 home_id·owner_id(제안대로라면 buyer_id도)로만 조인하여 과거 계약이 여러 건일 경우 잘못된 결과가 리턴될 수 있습니다.
하지만 final_contract 테이블은 contract_id를 contract_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.
| <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.
| <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 |
There was a problem hiding this comment.
🛠️ 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.
| <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.
🚀 관련 이슈
🔑 주요 변경사항
계약서 작성
0. 대기
✔️ 체크 리스트
mainbranch에 실수로 PR 생성 금지)📢 To Reviewers
📸 스크린샷 or 실행영상
Summary by CodeRabbit
New Features
Chores