Conversation
|
Caution Review failedThe pull request is closed. Walkthrough최종 계약(수정·삭제·확정) 및 특약(스페셜) 라운드 관련 다수의 API·서비스·레포지토리·DTO 추가·변경, ContractStatus에 ROUND4 추가, 프리컨트랙트 저장 시 채팅으로 URLLINK 전송 로직과 서브모듈 포인터 갱신이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Owner
participant Controller as ContractChatController
participant Service as ContractChatService
participant Redis as RedisStore
participant DB as Mongo/DB
Owner->>Controller: POST /final-contract/{id}/modification-request
Controller->>Service: requestFinalContractModification(id, ownerId, dto)
Service->>Redis: 저장(수정 요청 데이터)
Service-->>Controller: ModificationRequestData
Controller-->>Owner: ApiResponse(ModificationRequestData)
Buyer->>Controller: POST /final-contract/{id}/modification-response
Controller->>Service: respondToModificationRequest(id, buyerId, responseDto)
Service->>Redis: 조회·삭제(대기중 요청)
alt accepted
Service->>DB: 최종문서 적용/갱신
end
Service-->>Controller: FinalSpecialContractDocument
Controller-->>Buyer: ApiResponse(FinalSpecialContractDocument)
sequenceDiagram
actor Owner
actor Buyer
participant Controller as ContractChatController
participant Service as ContractChatService
participant Redis as RedisStore
participant DB as Mongo/DB
Owner->>Controller: POST /{id}/final-contract/request-confirmation
Controller->>Service: requestFinalContractConfirmation(id, ownerId)
Service->>Redis: set(confirm:pending)
Service-->>Controller: ok
Buyer->>Controller: POST /{id}/final-contract/accept-confirmation
Controller->>Service: acceptFinalContractConfirmation(id, buyerId, responseDto)
Controller->>Service: hasPendingModificationRequest/ownership 체크
Service->>Redis: 검증(confirm:pending)
Service->>DB: 상태 업데이트(예: ROUND4 등) 및 알림
Service-->>Controller: Map(result)
Controller-->>Buyer: ApiResponse(Map)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
✨ 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 (
|
There was a problem hiding this comment.
Actionable comments posted: 16
🔭 Outside diff range comments (2)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (1)
636-651: isUserInContractChatRoom 접근 체크 로직 불일치 가능성현재 구현은 userCurrentChatRoom(일반 채팅방 ID를 담는 맵)에서 contractChatId와 비교합니다. 일반/계약 채팅방 ID 네임스페이스가 다르다면 항상 false가 나올 수 있습니다. 계약 채팅방 입장/퇴장 시점을 별도로 기록하거나, 공용 맵을 사용하더라도 동일 ID 체계를 보장해야 합니다.
제안:
- 별도의 맵 도입: Map<Long, Long> userCurrentContractChatRoom
- 입장/퇴장 트래킹 메서드 추가: setUserCurrentContractChatRoom/removeUserFromCurrentContractChatRoom
- isUserInContractChatRoom은 해당 맵을 참조
- 혹은 기존 맵을 재사용하려면 저장하는 ID가 항상 "계약 채팅방 ID"가 되도록 호출부 전반을 정렬
원하시면 적용 패치를 제안드리겠습니다.
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)
1596-1724: 복잡한 메서드를 더 작은 단위로 분리하세요
saveFinalSpecialContract메서드가 130줄이 넘어 너무 깁니다. 로직을 더 작은 메서드로 분리하면 가독성과 테스트 용이성이 향상됩니다.+ private FinalSpecialContractDocument.FinalClause findValidClauseFromRounds( + Long contractChatId, Integer clauseOrder, Long startRound) { + for (Long searchRound = startRound; searchRound >= 1L; searchRound--) { + Optional<SpecialContractDocument> docOpt = + specialContractMongoRepository.findSpecialContractDocumentByContractChatIdAndRound( + contractChatId, searchRound); + + if (docOpt.isPresent()) { + Optional<SpecialContractDocument.Clause> clauseOpt = + findValidClauseInDocument(docOpt.get(), clauseOrder); + + if (clauseOpt.isPresent()) { + SpecialContractDocument.Clause clause = clauseOpt.get(); + log.info("특약 {}번 저장 완료 (라운드 {}): {}", clauseOrder, searchRound, clause.getTitle()); + return FinalSpecialContractDocument.FinalClause.builder() + .order(clauseOrder) + .title(clause.getTitle().trim()) + .content(clause.getContent().trim()) + .build(); + } + } + } + return null; + } + + private Optional<SpecialContractDocument.Clause> findValidClauseInDocument( + SpecialContractDocument doc, Integer clauseOrder) { + return doc.getClauses().stream() + .filter(clause -> clause.getOrder() != null && clause.getOrder().equals(clauseOrder)) + .filter(clause -> isValidClause(clause)) + .findFirst(); + } + + private boolean isValidClause(SpecialContractDocument.Clause clause) { + return clause.getTitle() != null && !clause.getTitle().trim().isEmpty() + && clause.getContent() != null && !clause.getContent().trim().isEmpty(); + }
♻️ Duplicate comments (3)
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)
10-10: 와일드카드 임포트 사용을 지양하세요Controller와 동일하게 와일드카드 임포트를 사용하고 있습니다. 명시적 임포트를 사용하는 것이 좋습니다.
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (2)
12-12: 와일드카드 임포트 사용을 지양하세요
903-903: 일관성 없는 파라미터 타입Controller 인터페이스의 주석과 동일하게,
FinalContractDeletionResponseDto가 확정 수락 메서드에 사용되고 있습니다.
🧹 Nitpick comments (7)
config-submodule (1)
1-1: 메시징/알림 트랜잭션 경계·재시도·아이도템포턴시 고려프리컨트랙트 저장 직후 URL 링크 메시지 전송 로직이 포함된 것으로 보입니다. 다음을 반영하면 신뢰성이 올라갑니다.
- 트랜잭션 커밋 이후 비동기 발송(트랜잭션 동기화 이벤트/아웃박스 패턴).
- 메시지 아이디 기반 중복 방지(Idempotency-Key).
- SSE 재연결/오프라인 백필 전략, WebSocket 구독 권한 재검증.
- 장애 시 재시도(지수 백오프)와 데드레터 채널 운영.
src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java (1)
5-13: Lombok 애노테이션 중복 제거 및 스타일 일관성@DaTa와 @Getter/@Setter를 동시에 사용하고 있습니다. 중복이며, 다른 DTO(FinalContractModificationResponseDto)와 스타일도 다릅니다. 한 쪽만 유지하는 게 좋습니다. 아래처럼 @DaTa를 제거해 ModificationResponseDto와 맞추길 권장합니다.
-@Data @Builder @AllArgsConstructor @NoArgsConstructor @Getter @Setter public class FinalContractDeletionResponseDto { private boolean accepted; }src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java (4)
247-249: 메시지 비존재 미완료 특약 조회 메서드의 스키마/인덱스 가정 명시 필요
getIncompleteSpecialContractsWithoutMessage의 "message 없음" 정의(필드 미존재 vs 공백 문자열 vs 빈 배열)를 명확히 해주세요. 또한 repository 레벨에서 해당 조건을 효율적으로 처리하기 위한 인덱스 전략을 문서/주석로 남기는 것을 권장합니다.
265-266: AI 메시지 메서드 네이밍/역할 정의 통일
AiMessage,AiMessageBtn,AiMessageNext의 역할/차이를 인터페이스 주석으로 명확히 해주세요. 호출 순서 보장, 버튼 payload 스펙 등도 함께 정의되면 구현체/호출부 정합성 유지에 도움이 됩니다.
267-276: 최종 특약 수정 요청/응답 API 계약 명확화 및 예외 규격화역할(임대인만 요청 가능, 임차인만 응답 가능), 중복 요청 방지, 동시성 처리(동일 조항 동시 요청)에 대한 계약을 자바독으로 명시하고, 권한/상태 오류 시 던지는 예외 타입을 통일해 주세요.
예시 자바독(패턴 제안):
/** * 임대인이 특정 조항에 대해 최종 특약 수정 요청을 등록합니다. * * 권한: owner만 호출 가능 * 전제: 해당 contractChatId가 존재하고, 최종 특약 확정 전 상태여야 함 * 제약: 동일 clauseOrder에 대해 pending 요청이 이미 존재하면 BusinessException * * @throws IllegalArgumentException null 파라미터 * @throws BusinessException 권한 불일치, 상태 불일치, 중복 요청 */
277-304: Map 반환 타입을 전용 DTO로 치환 고려
acceptFinalContractConfirmation,acceptFinalContractDeletion,respondToFinalContractDeletionRequest등이Map<String, Object>를 반환하고 있습니다. 스키마가 불명확해 프론트/호출부에서 타입 안정성이 떨어집니다. 전용 DTO를 도입해 계약을 명확히 해주세요.예: FinalContractActionResultDto { status, message, updatedDocumentId, ... }
src/main/java/org/scoula/domain/chat/controller/ContractChatController.java (1)
10-10: 와일드카드 임포트 사용을 지양하세요
org.scoula.domain.chat.dto.*와 같은 와일드카드 임포트 대신 필요한 클래스를 명시적으로 임포트하는 것이 좋습니다. 이는 코드의 가독성을 높이고 사용되는 클래스를 명확히 파악할 수 있게 합니다.-import org.scoula.domain.chat.dto.*; +import org.scoula.domain.chat.dto.ContractChatMessageRequestDto; +import org.scoula.domain.chat.dto.SpecialContractUserViewDto; +import org.scoula.domain.chat.dto.FinalContractModificationRequestDto; +import org.scoula.domain.chat.dto.FinalContractModificationResponseDto; +import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto; +import org.scoula.domain.chat.dto.ModificationRequestData;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
config-submodule(1 hunks)src/main/java/org/scoula/domain/chat/controller/ContractChatController.java(3 hunks)src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java(6 hunks)src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java(1 hunks)src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java(1 hunks)src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java(1 hunks)src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java(1 hunks)src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java(1 hunks)src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java(1 hunks)src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java(12 hunks)src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java(3 hunks)src/main/java/org/scoula/domain/chat/vo/ContractChat.java(1 hunks)src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java(3 hunks)src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java(3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/main/java/org/scoula/**/service/*Service{Interface,Impl}.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use interface-implementation pattern for services:
*ServiceInterface.java(interface) and*ServiceImpl.java(implementation)
Files:
src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.javasrc/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.javasrc/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.javasrc/main/java/org/scoula/domain/chat/service/ChatServiceImpl.javasrc/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
src/main/java/org/scoula/domain/*/repository/*Repository.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
Place MongoDB repositories under
src/main/java/org/scoula/domain/*/repository/*Repository.java
Files:
src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java
src/main/java/org/scoula/**/controller/*Controller{,Impl}.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use interface-implementation pattern for controllers:
*Controller.java(interface) and*ControllerImpl.java(implementation)
Files:
src/main/java/org/scoula/domain/chat/controller/ContractChatController.javasrc/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
src/main/java/org/scoula/**/controller/**/*.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
src/main/java/org/scoula/**/controller/**/*.java: All controller endpoints should return a consistentApiResponse<T>wrapper
Add Swagger annotations to controller endpoints for API documentation
Files:
src/main/java/org/scoula/domain/chat/controller/ContractChatController.javasrc/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
🧠 Learnings (1)
📚 Learning: 2025-08-12T03:59:30.512Z
Learnt from: CR
PR: ITZEEP/backend#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-12T03:59:30.512Z
Learning: Applies to src/main/java/org/scoula/domain/*/repository/*Repository.java : Place MongoDB repositories under `src/main/java/org/scoula/domain/*/repository/*Repository.java`
Applied to files:
src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java
🧬 Code Graph Analysis (4)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (2)
src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java (1)
Getter(5-15)src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java (1)
Getter(5-14)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java (2)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)
Getter(5-12)src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java (1)
Getter(5-15)
src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java (1)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)
Getter(5-12)
src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java (2)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)
Getter(5-12)src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java (1)
Getter(5-14)
🔇 Additional comments (8)
config-submodule (4)
1-1: Submodule 포인터 업데이트 자체는 적절합니다상위 리포에서 서브모듈 커밋 포인터만 변경된 메타데이터 변경이며, 이 파일 단위로는 문제 없어 보입니다.
1-1: 영향 범위 확인 완료: 제거된 MongoRepository 퍼블릭 API 호출 없음config-submodule을 제외한 전체 코드베이스에서 아래 제거된 메서드에 대한 호출이 전혀 발견되지 않았습니다.
빌드 타임 컴파일 오류 가능성이 없으므로 별도 조치가 필요 없습니다.
1-1: 모듈 경로 수정 및 컨트롤러 보안 애너테이션 검증 안내
config-submodule에는 Java 컨트롤러가 없으므로, 실제 컨트롤러 엔드포인트 검증 대상은 메인 리포지토리의src/main/java/org/scoula/domain/chat/controller입니다. 아래 스크립트로 @PreAuthorize/@Secured/@RolesAllowed 누락 매핑을 확인하세요.• 점검 대상 파일
- src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
- src/main/java/org/scoula/domain/chat/controller/ChatControllerImpl.java
#!/bin/bash set -euo pipefail echo "클래스 레벨 보안 애너테이션 검사" rg -n '@(PreAuthorize|Secured|RolesAllowed)' src/main/java/org/scoula/domain/chat/controller || echo " → 보안 애너테이션 미사용 클래스 없음" echo echo "매핑 라인 및 직전 컨텍스트 출력" rg -n -B 3 '@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\(' src/main/java/org/scoula/domain/chat/controller | sed 's/^/ /' || true이 결과에서 보안 애너테이션이 없는 매핑이 발견되면 즉시 추가 적용을 권장합니다.
또한 SSE/WebSocket 엔드포인트가 있다면 토큰 검증, 구독 권한, CORS/캐시 제어도 별도 점검해 주세요.Likely an incorrect or invalid review comment.
1-1: ContractStatus에 ROUND4 추가로 인한 분기 누락 위험 점검 필요
현재 config-submodule 내에서ContractStatusenum 정의나 처리 분기 로직(switch/when/if)을 찾지 못했습니다.
- 해당 enum 및 분기 처리 코드가 위치한 파일 경로를 알려주시거나 직접 위치를 확인해 주세요.
ROUND4가 모든 상태 전이 분기 경로에 포함되어 있는지 재검토 바랍니다.src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)
1-12: 단순 수락/거절 응답 DTO: OK필드/롬복 조합이 간결하고 용도에 부합합니다. 일관된 네이밍(accepted)도 좋습니다.
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (1)
830-831: 계약 링크 발신자를 소유자로 설정: 타당계약을 수락한 주체(소유자)가 링크를 발송하도록 변경된 점 합리적입니다.
src/main/java/org/scoula/domain/chat/controller/ContractChatController.java (1)
145-147: order 파라미터 필수화에 따른 클라이언트 영향 검증 필요
sendAiMessage메서드가 이제@RequestParam Long order를 필수 로 요구합니다.
백엔드 코드베이스 내 검색 결과,order없이 호출되는 곳은 다음 두 군데 선언부뿐이어서 내부 컴파일 오류는 없지만, 외부(프론트엔드·모바일·API 소비자 등)에서 호출 시에는 breaking change가 될 수 있습니다.검증할 사항:
- 프론트엔드/모바일/외부 API 스펙에
order파라미터 추가 여부 확인- 레거시 호출을 그대로 지원해야 한다면
required=false또는defaultValue지정 고려src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)
1013-1014: ROUND4 상태 처리 확인 완료
- src/main/java/org/scoula/domain/chat/vo/ContractChat.java
–ContractStatusenum에ROUND4항목이 정의되어 있습니다.- src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
–contractChatMapper.updateStatus(..., ContractChat.ContractStatus.ROUND4)호출(3곳)
–switch문 내case ROUND4:분기 처리("?step=3&round=4"반환)위 검증 결과,
ROUND4상태는 enum 정의부터 서비스 로직, 매퍼 호출, 분기 처리까지 시스템 전반에서 정상적으로 지원되고 있어 추가 조치가 필요하지 않습니다.
config-submodule
Outdated
| @@ -1 +1 @@ | |||
| Subproject commit 821c2c2ab5a06ba5e9f3e7ed6f23041d5c36bbec | |||
| Subproject commit 2d91d5b967d71c3c6a3a658d296a1453141a3c24 | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion
DTO 입력값 검증 강화(@Valid, Bean Validation) 제안
신규 DTO(예: FinalContractModificationRequestDto, ModificationRequestData 등)에 @NotNull/@SiZe 등 제약을 부여하고, 컨트롤러 메서드에서 @Valid를 적용하세요. 유효성 검증 실패 시 표준 에러 응답 포맷을 일관 제공하는 것이 좋습니다.
예시:
// DTO
@Getter @Setter
public class FinalContractModificationRequestDto {
@NotNull
private Integer clauseOrder;
@NotBlank @Size(max = 100)
private String newTitle;
@NotBlank @Size(max = 5000)
private String newContent;
}
// Controller
@PostMapping("/final-contract/{contractChatId}/modification")
public ResponseEntity<?> requestModification(
@PathVariable Long contractChatId,
@Valid @RequestBody FinalContractModificationRequestDto dto) { ... }🤖 Prompt for AI Agents
In config-submodule around lines 1-1, DTOs like
FinalContractModificationRequestDto and ModificationRequestData currently lack
Bean Validation annotations and controllers are not using @Valid; add
appropriate javax.validation constraints (@NotNull, @NotBlank, @Size, etc.) to
DTO fields and annotate controller @RequestBody parameters with @Valid, and
implement a global @ControllerAdvice (or extend existing exception handler) to
catch MethodArgumentNotValidException and ConstraintViolationException to return
a consistent error response payload and HTTP 400 status. Ensure DTO fields have
clear constraint messages where useful and update any unit/integration tests to
assert validation behavior.
💡 Verification agent
🧩 Analysis chain
서브모듈 커밋 유효성(.gitmodules 매핑, 원격 커밋 존재, 체크아웃 상태) 검증 권장
원격에 해당 커밋이 실제로 존재하는지, .gitmodules 설정과 워킹트리 체크아웃이 일치하는지 확인해 주세요.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
commit="2d91d5b967d71c3c6a3a658d296a1453141a3c24"
name="config-submodule"
echo "[1/5] .gitmodules에서 $name URL 확인"
test -f .gitmodules || { echo "ERROR: .gitmodules 없음"; exit 1; }
url="$(git config -f .gitmodules --get submodule.${name}.url || true)"
echo "submodule URL: ${url:-<empty>}"
test -n "${url:-}" || { echo "ERROR: ${name} URL 누락"; exit 1; }
echo "[2/5] 원격에 커밋 존재 여부 확인"
git ls-remote "$url" | grep -q "$commit" && echo "OK: 원격에 커밋 존재" || { echo "ERROR: 원격에 커밋 미존재"; exit 1; }
echo "[3/5] 서브모듈 초기화/업데이트"
git submodule update --init "$name"
echo "[4/5] 서브모듈 HEAD 확인"
( cd "$name" && git rev-parse HEAD )
echo "[5/5] 상위 포인터와 일치 여부 확인"
actual="$(cd "$name" && git rev-parse HEAD)"
if [[ "$actual" == "$commit" ]]; then
echo "OK: 체크아웃 커밋이 상위 포인터와 일치"
else
echo "WARN: 체크아웃 커밋($actual)이 상위 포인터($commit)와 불일치"
exit 2
fiLength of output: 838
서브모듈 config-submodule의 원격 리포지토리 또는 커밋 미존재 확인 필요
git ls-remote https://github.com/ITZEEP/backend-config.git 실행 시 “Repository not found” 오류가 발생하여 커밋 2d91d5b967d71c3c6a3a658d296a1453141a3c24이 원격에 존재하지 않는 것으로 확인되었습니다. 아래 항목을 확인/수정해주세요:
.gitmodules의submodule.config-submodule.url이 올바른 저장소를 가리키는지 검증- 해당 저장소 접근 권한(프라이빗 여부) 및 URL 정확성 확인
- 커밋 SHA-1 해시가 실제로 원격에 존재하는지 확인
- 필요 시 올바른 커밋 해시로 업데이트한 뒤
git submodule update --init재실행 및 HEAD 일치 확인
🤖 Prompt for AI Agents
In config-submodule around lines 1-1, the submodule remote or commit is missing
on the remote (git ls-remote shows “Repository not found” for
https://github.com/ITZEEP/backend-config.git and commit 2d91d5b9… is absent);
verify and correct .gitmodules submodule.config-submodule.url to the proper
repository URL, confirm repository accessibility (private vs public) and
credentials, check that the referenced commit SHA exists on the remote (or
update to a valid SHA/branch), then update the submodule reference and run git
submodule sync && git submodule update --init --recursive and ensure the
submodule HEAD matches the expected commit.
| @RequestBody FinalContractDeletionResponseDto responseDto, | ||
| Authentication authentication); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
일관성 없는 메서드 파라미터 타입
FinalContractDeletionResponseDto가 acceptFinalContractConfirmation 메서드의 파라미터로 사용되고 있는데, 메서드명과 DTO 이름이 일치하지 않습니다. 확정 수락용 별도 DTO를 만들거나 더 범용적인 DTO 이름을 사용하는 것이 좋습니다.
- ResponseEntity<ApiResponse<Map<String, Object>>> acceptFinalContractConfirmation(
- @PathVariable Long contractChatId,
- @RequestBody FinalContractDeletionResponseDto responseDto,
- Authentication authentication);
+ ResponseEntity<ApiResponse<Map<String, Object>>> acceptFinalContractConfirmation(
+ @PathVariable Long contractChatId,
+ @RequestBody FinalContractConfirmationResponseDto responseDto,
+ Authentication authentication);📝 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.
| @RequestBody FinalContractDeletionResponseDto responseDto, | |
| Authentication authentication); | |
| ResponseEntity<ApiResponse<Map<String, Object>>> acceptFinalContractConfirmation( | |
| @PathVariable Long contractChatId, | |
| @RequestBody FinalContractConfirmationResponseDto responseDto, | |
| Authentication authentication); |
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/controller/ContractChatController.java
around lines 191-192, the method acceptFinalContractConfirmation(...) currently
uses FinalContractDeletionResponseDto which does not match the method intent;
change this by either creating a dedicated DTO named
AcceptFinalContractRequestDto (or similar) for accepting the final contract and
replace the parameter type, or rename the existing DTO to a more
generic/appropriate name and update all usages; ensure the new DTO contains only
the required fields, update any validation, mappers, controller tests and API
docs/signatures accordingly so types are consistent with the method purpose.
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
Show resolved
Hide resolved
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
Show resolved
Hide resolved
| package org.scoula.domain.chat.dto; | ||
|
|
||
| import lombok.*; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class FinalContractModificationRequestDto { | ||
| private Integer clauseOrder; | ||
| private String newTitle; | ||
| private String newContent; | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
입력값 유효성 검증 애노테이션 추가 제안
외부 입력을 직접 받는 DTO입니다. 빈 문자열/음수 등 방어를 위해 Bean Validation을 추가하는 것이 안전합니다(컨트롤러 파라미터에 @Valid 병행 권장).
아래와 같이 보완을 제안드립니다:
package org.scoula.domain.chat.dto;
import lombok.*;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FinalContractModificationRequestDto {
- private Integer clauseOrder;
- private String newTitle;
- private String newContent;
+ @NotNull @Min(1)
+ private Integer clauseOrder;
+ @NotBlank
+ private String newTitle;
+ @NotBlank
+ private String newContent;
}컨트롤러 메서드 시그니처에 @Valid 적용 여부도 확인 부탁드립니다.
입력값 유효성 검증 설정 현황 확인을 위해 아래 스크립트를 실행해주세요:
🏁 Script executed:
#!/bin/bash
set -e
echo "1) @Valid 애노테이션 사용 여부 확인"
rg "@Valid" -n src/main/java || echo " → @Valid 애노테이션 사용 내역이 없습니다."
echo
echo "2) Bean Validation 제약 애노테이션(import) 확인"
rg "jakarta.validation.constraints" -n src || rg "javax.validation.constraints" -n src || echo " → 제약 애노테이션 import 내역이 없습니다."
echo
echo "3) 빌드 파일(validation 의존성) 확인"
if [ -f pom.xml ]; then
echo " • pom.xml:"
rg "validation" -n pom.xml || echo " → 의존성 항목 없음"
fi
if [ -f build.gradle ]; then
echo " • build.gradle:"
rg "validation" -n build.gradle || echo " → 의존성 항목 없음"
fiLength of output: 8167
입력값 검증 애노테이션 추가 제안
FinalContractModificationRequestDto는 외부 입력을 받으므로 Bean Validation 제약 애노테이션을 추가하고, 이를 처리하는 컨트롤러에 @Valid가 적용되었는지 반드시 확인해주세요.
• 수정 대상
- 파일:
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java
• Diff 예시
package org.scoula.domain/chat/dto;
import lombok.*;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FinalContractModificationRequestDto {
- private Integer clauseOrder;
- private String newTitle;
- private String newContent;
+ @NotNull
+ @Min(1)
+ private Integer clauseOrder;
+
+ @NotBlank
+ private String newTitle;
+
+ @NotBlank
+ private String newContent;
}• 확인 사항
- 이 DTO를 받는 컨트롤러 메서드 파라미터에
@Valid @RequestBody FinalContractModificationRequestDto dto가 적용되어 있는지 검증 - 이미
build.gradle에javax.validation:validation-api:2.0.1.Final의존성이 포함되어 있음을 확인했습니다.
📝 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.
| package org.scoula.domain.chat.dto; | |
| import lombok.*; | |
| @Getter | |
| @Setter | |
| @NoArgsConstructor | |
| @AllArgsConstructor | |
| @Builder | |
| public class FinalContractModificationRequestDto { | |
| private Integer clauseOrder; | |
| private String newTitle; | |
| private String newContent; | |
| } | |
| package org.scoula.domain.chat.dto; | |
| import lombok.*; | |
| import javax.validation.constraints.Min; | |
| import javax.validation.constraints.NotBlank; | |
| import javax.validation.constraints.NotNull; | |
| @Getter | |
| @Setter | |
| @NoArgsConstructor | |
| @AllArgsConstructor | |
| @Builder | |
| public class FinalContractModificationRequestDto { | |
| @NotNull | |
| @Min(1) | |
| private Integer clauseOrder; | |
| @NotBlank | |
| private String newTitle; | |
| @NotBlank | |
| private String newContent; | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java
around lines 1-14, this DTO lacks Bean Validation annotations for external
input; add javax.validation constraints: annotate clauseOrder with @NotNull (and
@Min(1) if clause index must be positive) and annotate newTitle and newContent
with @NotBlank to prevent empty values, and ensure required imports are added.
Also verify every controller endpoint that accepts this DTO uses @Valid on the
@RequestBody parameter (e.g., @Valid @RequestBody
FinalContractModificationRequestDto dto) so validation is triggered, and update
any unit/integration tests or exception handlers if necessary to handle
ConstraintViolation/MethodArgumentNotValid exceptions.
| .build(); | ||
|
|
||
| try { | ||
| String jsonData = objectMapper.writeValueAsString(requestData); | ||
| String valueData = | ||
| String.format( | ||
| "{\"clauseOrder\":%d,\"requestData\":%s}", | ||
| requestDto.getClauseOrder(), jsonData); | ||
| stringRedisTemplate.opsForValue().set(redisKey, valueData, Duration.ofHours(24)); | ||
|
|
||
| String notificationMessage = | ||
| String.format( | ||
| "임대인이 특약 %d번 수정을 요청했습니다.\n\n" + "📝 수정 제목: %s\n" + "✏️ 수정 내용: %s\n\n", | ||
| requestDto.getClauseOrder(), | ||
| requestDto.getNewTitle(), | ||
| requestDto.getNewContent()); | ||
|
|
||
| AiMessageBtn(contractChatId, notificationMessage); | ||
| log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey); | ||
| return requestData; | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("수정 요청 저장 실패", e); | ||
| throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다."); | ||
| } |
There was a problem hiding this comment.
JSON 파싱 에러 처리 개선 필요
JSON 파싱 시 구체적인 예외 타입을 catch하고 더 명확한 에러 메시지를 제공해야 합니다.
- } catch (Exception e) {
- log.error("수정 요청 저장 실패", e);
- throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다.");
- }
+ } catch (JsonProcessingException e) {
+ log.error("JSON 직렬화 실패: {}", e.getMessage(), e);
+ throw new IllegalStateException("수정 요청 데이터 직렬화에 실패했습니다.", e);
+ } catch (Exception e) {
+ log.error("Redis 저장 실패: {}", e.getMessage(), e);
+ throw new RuntimeException("수정 요청 저장 중 예기치 않은 오류가 발생했습니다.", e);
+ }📝 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.
| .build(); | |
| try { | |
| String jsonData = objectMapper.writeValueAsString(requestData); | |
| String valueData = | |
| String.format( | |
| "{\"clauseOrder\":%d,\"requestData\":%s}", | |
| requestDto.getClauseOrder(), jsonData); | |
| stringRedisTemplate.opsForValue().set(redisKey, valueData, Duration.ofHours(24)); | |
| String notificationMessage = | |
| String.format( | |
| "임대인이 특약 %d번 수정을 요청했습니다.\n\n" + "📝 수정 제목: %s\n" + "✏️ 수정 내용: %s\n\n", | |
| requestDto.getClauseOrder(), | |
| requestDto.getNewTitle(), | |
| requestDto.getNewContent()); | |
| AiMessageBtn(contractChatId, notificationMessage); | |
| log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey); | |
| return requestData; | |
| } catch (Exception e) { | |
| log.error("수정 요청 저장 실패", e); | |
| throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다."); | |
| } | |
| .build(); | |
| try { | |
| String jsonData = objectMapper.writeValueAsString(requestData); | |
| String valueData = | |
| String.format( | |
| "{\"clauseOrder\":%d,\"requestData\":%s}", | |
| requestDto.getClauseOrder(), jsonData); | |
| stringRedisTemplate.opsForValue().set(redisKey, valueData, Duration.ofHours(24)); | |
| String notificationMessage = | |
| String.format( | |
| "임대인이 특약 %d번 수정을 요청했습니다.\n\n" + "📝 수정 제목: %s\n" + "✏️ 수정 내용: %s\n\n", | |
| requestDto.getClauseOrder(), | |
| requestDto.getNewTitle(), | |
| requestDto.getNewContent()); | |
| AiMessageBtn(contractChatId, notificationMessage); | |
| log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey); | |
| return requestData; | |
| } catch (JsonProcessingException e) { | |
| log.error("JSON 직렬화 실패: {}", e.getMessage(), e); | |
| throw new IllegalStateException("수정 요청 데이터 직렬화에 실패했습니다.", e); | |
| } catch (Exception e) { | |
| log.error("Redis 저장 실패: {}", e.getMessage(), e); | |
| throw new RuntimeException("수정 요청 저장 중 예기치 않은 오류가 발생했습니다.", e); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 2120 to 2144, replace the broad catch(Exception) with targeted
exception handling for JSON parsing and Redis errors: catch
com.fasterxml.jackson.core.JsonProcessingException to handle
objectMapper.writeValueAsString failures and log the exception message and
stacktrace, then throw a RuntimeException with a clear message and the original
exception as the cause; catch org.springframework.dao.DataAccessException (or
the specific Redis exception your template throws) around the redis set call to
log detailed redis error info and rethrow with the cause; keep a final
catch(Exception) only as a fallback that logs full details and rethrows the
exception as the cause so callers can inspect the original error.
| String param = getContractChatStatus(contractChatId.getStatus()); | ||
| return "http://localhost:5173/contract/" + contractChatRoomId + param; |
There was a problem hiding this comment.
하드코딩된 URL을 설정으로 분리하세요
프론트엔드 URL이 하드코딩되어 있습니다. 환경별로 다른 URL을 사용할 수 있도록 설정 파일로 분리해야 합니다.
+ @Value("${frontend.base-url}")
+ private String frontendBaseUrl;
- return "http://localhost:5173/contract/" + contractChatRoomId + param;
+ return frontendBaseUrl + "/contract/" + contractChatRoomId + param;application.yml에 추가:
frontend:
base-url: ${FRONTEND_URL:http://localhost:5173}🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 2549 to 2550, the frontend base URL is hardcoded; change to read a
configurable property (e.g., frontend.base-url) injected into the service (via
@Value or a @ConfigurationProperties bean) and use that value to build the
returned URL, falling back to the existing default; add the suggested
application.yml entry (frontend.base-url: ${FRONTEND_URL:http://localhost:5173})
and update any tests to mock or provide the property as needed.
| ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); | ||
| contractChatService.getContractChatStatus(contractChat.getStatus()); | ||
| String contractChatUrls = "http://localhost:5173/contract/" + contractChatId.toString(); | ||
| ChatMessageRequestDto linkMessages = | ||
| ChatMessageRequestDto.builder() | ||
| .chatRoomId(contractChatId) | ||
| .senderId(contractChat.getBuyerId()) | ||
| .receiverId(contractChat.getOwnerId()) | ||
| .content(contractChatUrls) | ||
| .type("URLLINK") | ||
| .build(); | ||
| chatService.handleChatMessage(linkMessages); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
하드코딩 URL 제거, 불필요 호출 제거, 송수신자 반전 오류 수정, NPE 방지
- 하드코딩된 프론트엔드 URL을 설정 기반으로 변경하세요.
getContractChatStatus(...)호출 결과를 사용하지 않으므로 제거하세요.- 현재 senderId=buyer, receiverId=owner로 설정되어 있어, 임대인 플로우에서 송신/수신이 반대로 표기됩니다. owner가 발신자로 설정되어야 합니다.
contractChat조회 실패 시 NPE를 방지하세요.
수정 제안 diff:
- ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
- contractChatService.getContractChatStatus(contractChat.getStatus());
- String contractChatUrls = "http://localhost:5173/contract/" + contractChatId.toString();
+ ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+ if (contractChat == null) {
+ throw new BusinessException(OwnerPreContractErrorCode.OWNER_SELECT, "계약 채팅 정보를 찾을 수 없습니다.");
+ }
+ String contractChatUrls = String.format("%s/contract/%d", frontendBaseUrl, contractChatId);
ChatMessageRequestDto linkMessages =
ChatMessageRequestDto.builder()
.chatRoomId(contractChatId)
- .senderId(contractChat.getBuyerId())
- .receiverId(contractChat.getOwnerId())
+ .senderId(contractChat.getOwnerId())
+ .receiverId(contractChat.getBuyerId())
.content(contractChatUrls)
.type("URLLINK")
.build();
chatService.handleChatMessage(linkMessages);클래스 상단에 환경설정 주입 필드를 추가해 주세요.
import org.springframework.beans.factory.annotation.Value;
// ...
@Value("${app.frontend.base-url}")
private String frontendBaseUrl;🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java
around lines 422 to 434, remove the unused call to getContractChatStatus(...),
replace the hardcoded "http://localhost:5173" with a configurable frontend base
URL injected via @Value("${app.frontend.base-url}") (add the import and private
field at class top), guard against contractChat being null (check after
findByContractChatId and handle appropriately — throw a meaningful exception or
return early to avoid NPE), and fix the sender/receiver reversal by setting
senderId to contractChat.getOwnerId() and receiverId to
contractChat.getBuyerId() before building and sending the ChatMessageRequestDto.
| ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); | ||
|
|
||
| String contractChatUrls = | ||
| "http://localhost:5173/pre-contract/" + contractChatId.toString() + "/owner?step=1"; | ||
| ChatMessageRequestDto linkMessages = | ||
| ChatMessageRequestDto.builder() | ||
| .chatRoomId(contractChatId) | ||
| .senderId(contractChat.getBuyerId()) | ||
| .receiverId(contractChat.getOwnerId()) | ||
| .content(contractChatUrls) | ||
| .type("URLLINK") | ||
| .build(); | ||
| contractChatService.AiMessage(contractChatId, "안녕하세요!\n" + "임대인이 입장하면 바로 계약서 작성을 시작할게요."); | ||
| contractChatService.AiMessageBtn(contractChatId, "기다리는 동안 \n" + "어려운 법률 용어와 법률 팁을 알아볼까요?"); | ||
| chatService.handleChatMessage(linkMessages); | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
하드코딩된 프론트엔드 URL 제거 및 NPE 방지 처리
- URL을 코드에 하드코딩하면 환경별(로컬/스테이징/운영) 배포시 오동작합니다. 환경변수/설정값 기반으로 구성하세요.
contractChatMapper.findByContractChatId의 반환값에 대한 NPE 방지가 필요합니다. 조회 실패 시 비즈니스 예외로 전환하는 편이 안전합니다.
아래와 같이 수정 제안드립니다.
- ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
-
- String contractChatUrls =
- "http://localhost:5173/pre-contract/" + contractChatId.toString() + "/owner?step=1";
+ ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+ if (contractChat == null) {
+ throw new BusinessException(PreContractErrorCode.TENANT_SELECT, "계약 채팅 정보를 찾을 수 없습니다.");
+ }
+
+ String contractChatUrls = String.format("%s/pre-contract/%d/owner?step=1", frontendBaseUrl, contractChatId);
ChatMessageRequestDto linkMessages =
ChatMessageRequestDto.builder()
.chatRoomId(contractChatId)
.senderId(contractChat.getBuyerId())
.receiverId(contractChat.getOwnerId())
.content(contractChatUrls)
.type("URLLINK")
.build();
contractChatService.AiMessage(contractChatId, "안녕하세요!\n" + "임대인이 입장하면 바로 계약서 작성을 시작할게요.");
contractChatService.AiMessageBtn(contractChatId, "기다리는 동안 \n" + "어려운 법률 용어와 법률 팁을 알아볼까요?");
chatService.handleChatMessage(linkMessages);클래스 상단에 환경설정 주입 필드를 추가해 주세요.
import org.springframework.beans.factory.annotation.Value;
// ...
@Value("${app.frontend.base-url}")
private String frontendBaseUrl;추가 확인:
- 메시지 타입 "URLLINK"는 시스템 전반에서 합의된 상수인가요? Enum 전환 또는 상수화가 가능하면 더 안전합니다.
추가 검증을 위해 코드베이스를 살펴봅니다.
🏁 Script executed:
#!/bin/bash
set -e
echo "1) findByContractChatId 사용 위치:"
rg "findByContractChatId" -n src
echo -e "\n2) ContractChatMapper 인터페이스 정의:"
rg "interface ContractChatMapper" -n src -A10
echo -e "\n3) \"URLLINK\" 문자열 사용 위치:"
rg "\"URLLINK\"" -n src
echo -e "\n4) ChatMessageRequestDto 클래스 정의:"
rg "class ChatMessageRequestDto" -n src -A30
echo -e "\n5) 프로젝트 내 Enum 정의 목록:"
rg "enum" -n src -nLength of output: 24494
하드코딩된 프론트엔드 URL 제거 및 null 처리 추가
환경별(front/스테이징/운영) URL을 application.yml 등 설정 파일로 분리하고, findByContractChatId 호출 결과에 대한 NPE 방지를 위해 null 체크를 반드시 추가해주세요. 또한 메시지 타입 "URLLINK"는 상수나 enum으로 관리하는 것이 안전합니다.
수정 대상:
- src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java (354–369행)
- src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (유사 로직)
- application.yml 또는 각 환경별 프로퍼티 파일
예시 diff:
--- a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
@@ -10,6 +10,8 @@
import org.scoula.domain.precontract.enums.RentType;
import org.springframework.beans.factory.annotation.Value;
+@Value("${app.frontend.base-url}")
+private String frontendBaseUrl;
public class PreContractServiceImpl implements PreContractService {
@@ -352,11 +354,17 @@
// 1) ContractChat 조회 및 null 체크
- ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+ ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+ if (contractChat == null) {
+ throw new BusinessException(
+ PreContractErrorCode.CONTRACT_CHAT_NOT_FOUND,
+ "해당 계약 채팅 정보를 찾을 수 없습니다. contractChatId=" + contractChatId
+ );
+ }
- String contractChatUrls =
- "http://localhost:5173/pre-contract/" + contractChatId + "/owner?step=1";
+ // 2) URL 환경변수화
+ String contractChatUrls = String.format(
+ "%s/pre-contract/%d/owner?step=1", frontendBaseUrl, contractChatId
+ );
ChatMessageRequestDto linkMessages = ChatMessageRequestDto.builder()
.chatRoomId(contractChatId)
@@ -364,7 +372,7 @@
.content(contractChatUrls)
- .type("URLLINK")
+ .type(ChatMessageType.URL_LINK) // enum 또는 상수로 변경
.build();
contractChatService.AiMessage(…application.yml 예시:
app:
frontend:
base-url: https://your-frontend-domain.com추가 제안:
PreContractErrorCode에CONTRACT_CHAT_NOT_FOUND같은 전용 에러 코드를 추가해주시고,"URLLINK"대신ChatMessageTypeenum 혹은ChatConstants.URL_LINK상수를 만들어 사용해주세요.
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
around lines 354 to 369, the code hardcodes the frontend URL, does not
null-check the result of contractChatMapper.findByContractChatId(...), and uses
a raw string for the message type; update to read the frontend base URL from
application.yml (e.g. app.frontend.base-url), add a null check after
findByContractChatId and throw/use a
PreContractErrorCode.CONTRACT_CHAT_NOT_FOUND (add that enum value if missing)
when null, construct the link using the injected config value, and replace the
literal "URLLINK" with a ChatMessageType enum or ChatConstants.URL_LINK; apply
the same changes to OwnerPreContractServiceImpl and add the
app.frontend.base-url entry to application.yml for each environment.
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (7)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (2)
110-123: URLLINK 프리뷰 처리 추가, 잘 반영됨"TEXT/URLLINK/FILE"에 대한 미리보기 문구가 명확해져 UX 측면에서 개선되었습니다. 기존 리뷰 지적사항을 정확히 반영했습니다.
204-230: URLLINK 알림 케이스 누락 → switch에 케이스 추가 필요계약 수락 후 발송되는 링크 메시지(type="URLLINK", Line 850)와 정합성을 맞추려면 알림 switch에도 "URLLINK" 케이스를 추가해야 합니다. 현재는 default로 처리되어 제목/본문이 의도와 다를 수 있습니다.
아래 diff처럼 케이스를 보강해 주세요.
switch (dto.getType()) { case "TEXT": notificationBody = dto.getContent(); notificationTitle = senderName + "님의 새 메시지"; break; + case "URLLINK": + notificationBody = "[링크] " + (dto.getContent() != null ? dto.getContent() : ""); + notificationTitle = senderName + "님이 링크를 보냈습니다"; + break; case "FILE": notificationBody = "[파일] " + (dto.getContent() != null ? dto.getContent() : "파일을 보냈습니다"); notificationTitle = senderName + "님이 파일을 보냈습니다"; break; case "CONTRACT_REQUEST": notificationBody = "계약을 요청했습니다"; notificationTitle = senderName + "님의 계약 요청"; break; case "CONTRACT_REJECT": notificationBody = "계약 요청을 거절했습니다"; notificationTitle = senderName + "님의 계약 거절"; break; default: notificationBody = dto.getContent() != null ? dto.getContent() : "새 메시지"; notificationTitle = senderName + "님의 새 메시지"; break; }Also applies to: 844-851
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (3)
50-50: ObjectMapper를 Spring Bean으로 주입받아 사용하세요ObjectMapper를 직접 생성하지 말고 Spring이 관리하는 Bean으로 주입받아 사용하는 것이 좋습니다. 이렇게 하면 전역 설정을 일관되게 적용할 수 있습니다.
- private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper;생성자에 ObjectMapper를 추가하면 Spring이 자동으로 주입합니다 (@requiredargsconstructor 사용 중).
1063-1088: 중복 코드를 별도 메서드로 추출하세요최종 특약서 저장 및 상태 업데이트 로직이 중복됩니다. 별도 메서드로 추출하면 유지보수성이 향상됩니다.
+ private Map<String, Object> processFinalContractCompletion(Long contractChatId, String message) { + try { + FinalSpecialContractDocument finalContract = saveFinalSpecialContract(contractChatId); + AiMessageNext(contractChatId, message); + contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.ROUND4); + + return Map.of( + "message", "모든 특약이 완료되었습니다!", + "completed", true, + "finalContractId", finalContract.getId(), + "totalFinalClauses", finalContract.getTotalFinalClauses() + ); + } catch (Exception e) { + log.error("최종 특약 저장 실패", e); + return Map.of("message", "특약은 완료되었지만 최종 저장 중 오류가 발생했습니다.", "completed", true); + } + } if (remainingIncompleteContracts.isEmpty()) { - try { - FinalSpecialContractDocument finalContract = - saveFinalSpecialContract(contractChatId); - - AiMessageNext(contractChatId, "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다."); - contractChatMapper.updateStatus( - contractChatId, ContractChat.ContractStatus.ROUND4); - - return Map.of( - "message", - "모든 특약이 완료되었습니다!", - "completed", - true, - "finalContractId", - finalContract.getId(), - "totalFinalClauses", - finalContract.getTotalFinalClauses()); - } catch (Exception e) { - log.error("최종 특약 저장 실패", e); - return Map.of( - "message", "특약은 완료되었지만 최종 저장 중 오류가 발생했습니다.", "completed", true); - } + return processFinalContractCompletion(contractChatId, + "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다."); }
2126-2150: JSON 파싱 에러 처리를 개선하세요JSON 파싱 시 구체적인 예외 타입을 catch하고 더 명확한 에러 메시지를 제공해야 합니다.
- } catch (Exception e) { - log.error("수정 요청 저장 실패", e); - throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다."); - } + } catch (JsonProcessingException e) { + log.error("JSON 직렬화 실패: {}", e.getMessage(), e); + throw new IllegalStateException("수정 요청 데이터 직렬화에 실패했습니다.", e); + } catch (Exception e) { + log.error("Redis 저장 실패: {}", e.getMessage(), e); + throw new RuntimeException("수정 요청 저장 중 예기치 않은 오류가 발생했습니다.", e); + }src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java (1)
361-376: 프론트엔드 URL 하드코딩 제거 및 null 체크 추가환경별(로컬/스테이징/운영) URL 설정이 필요하고,
contractChat이 null일 경우 NPE가 발생할 수 있습니다.ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new BusinessException(PreContractErrorCode.TENANT_SELECT, + "계약 채팅 정보를 찾을 수 없습니다: contractChatId=" + contractChatId); + } String contractChatUrls = URL + precontractUrl + (contractChatId.toString()) + ownerUrl;또한 메시지 타입 "URLLINK"를 상수나 enum으로 관리하는 것을 권장합니다:
- .type("URLLINK") + .type(ChatMessageType.URL_LINK) // 또는 ChatConstants.URL_LINKsrc/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (1)
427-444: 불필요한 메서드 호출 제거 및 null 체크 추가
getContractChatStatus()호출 결과를 사용하지 않으므로 제거해야 합니다contractChat과chatRoom에 대한 null 체크가 필요합니다- 송수신자가 반대로 설정되어 있습니다 (임대인 플로우에서는 owner가 발신자여야 함)
ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId); + if (contractChat == null) { + throw new BusinessException(OwnerPreContractErrorCode.OWNER_SELECT, + "계약 채팅 정보를 찾을 수 없습니다: contractChatId=" + contractChatId); + } + ChatRoom chatRoom = chatRoomMapper.findByUserAndHome( contractChat.getOwnerId(), contractChat.getBuyerId(), contractChat.getHomeId()); - contractChatService.getContractChatStatus(contractChat.getStatus()); + if (chatRoom == null) { + throw new BusinessException(OwnerPreContractErrorCode.OWNER_SELECT, + "채팅방을 찾을 수 없습니다"); + } + ChatMessageRequestDto linkMessages = ChatMessageRequestDto.builder() .chatRoomId(chatRoom.getChatRoomId()) - .senderId(contractChat.getBuyerId()) - .receiverId(contractChat.getOwnerId()) + .senderId(contractChat.getOwnerId()) + .receiverId(contractChat.getBuyerId()) .content("계약 채팅방 URL") .type("URLLINK") .build(); chatService.handleChatMessage(linkMessages); - contractChatService.AiMessage(contractChatId, "임대인꼐서 입장하셨습니다! 🤝 이제 계약을 시작합니다."); + contractChatService.AiMessage(contractChatId, "임대인께서 입장하셨습니다! 🤝 이제 계약을 시작합니다.");
🧹 Nitpick comments (7)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (2)
56-61: 환경 URL 필드명과 경로 상수 선언 방식 개선
- 인스턴스 필드명을 대문자(URL)로 두면 타입(java.net.URL)과 혼동될 수 있고 관례에도 어긋납니다. 의미가 드러나도록 frontBaseUrl 등으로 변경 권장.
- 경로 상수는 불변(static final)로 선언하고 상수 네이밍 컨벤션(UPPER_SNAKE_CASE)을 따르는 것을 권장합니다.
아래처럼 정리하면 가독성과 안전성이 올라갑니다.
- @Value("${front.base.url}") - private String URL; - - private String PRECONTRACTURL = "/pre-contract/"; - private String BUYERURL = "/buyer?step=1"; + @Value("${front.base.url}") + private String frontBaseUrl; + + private static final String PRECONTRACT_PATH = "/pre-contract/"; + private static final String BUYER_PATH = "/buyer?step=1";
842-842: 계약 채팅 URL 조립 시 이중 슬래시/인코딩 이슈 예방베이스 URL이 슬래시로 끝나는 경우 등 문자열 단순 연결은 이중 슬래시가 생길 수 있습니다. UriComponentsBuilder로 조립하면 안전합니다. (Line 56–61에서 제안한 네이밍을 적용한 예시입니다.)
- String contractChatUrl = URL + PRECONTRACTURL + (contractChatRoomId.toString()) + BUYERURL; + String contractChatUrl = UriComponentsBuilder.fromHttpUrl(frontBaseUrl) + .pathSegment("pre-contract", contractChatRoomId.toString(), "buyer") + .queryParam("step", 1) + .build() + .toUriString();아래 import 추가 필요:
import org.springframework.web.util.UriComponentsBuilder;참고: 만약 필드명을 그대로 유지한다면 frontBaseUrl → URL로 변경해 사용하셔도 됩니다.
src/main/java/org/scoula/domain/chat/vo/ContractChat.java (2)
40-53: ROUND4에 대한 라운드 매핑 추가 LGTMROUND4 -> 5L 매핑 보완으로 이전 리뷰에서 지적된 누락이 해결되었습니다. 동작 측면 문제 없습니다.
중복 switch/조건을 줄이기 위해 enum에 라운드 인덱스를 속성으로 부여하면 가독성이 좋아집니다.
예시:
public enum ContractStatus { STEP0(null), STEP1(null), STEP2(null), ROUND0(1L), ROUND1(2L), ROUND2(3L), ROUND3(4L), ROUND4(5L), STEP4(null); private final Long roundIndex; ContractStatus(Long roundIndex) { this.roundIndex = roundIndex; } public boolean isRound() { return roundIndex != null; } public Long getRoundIndex() { return roundIndex; } } // 사용부 public Long getCurrentRound() { return (status != null && status.getRoundIndex() != null) ? status.getRoundIndex() : 1L; }
56-64: ROUND4에 대한 라운드 여부 판정 추가 LGTM라운드 포함 여부에 ROUND4가 반영되었습니다. 추후 유지보수를 위해 enum에 isRound() 유틸을 두는 방식을 권장합니다(위 코멘트 참조).
src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml (1)
14-19: 쿼리 추가 OK. 포맷팅/스타일 컨벤션 일치 권장기능은 문제없습니다. 다른 쿼리들과 동일한 들여쓰기/공백 컨벤션을 맞추면 가독성이 좋아집니다.
적용 예시(diff):
- <select id="selectContractOwnerId" resultType="Long"> - SELECT owner_id - FROM contract_chat - WHERE contract_chat_id=#{contractChatId} -</select> + <select id="selectContractOwnerId" resultType="Long"> + SELECT owner_id + FROM contract_chat + WHERE contract_chat_id = #{contractChatId} + </select>src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1)
476-482: 검증 메서드 일관성/의미 명확화 제안
- 예외 코드: 소유자 검증 실패에 PreContractErrorCode.TENANT_USER를 재사용하면 의미가 모호합니다. OWNER_USER 또는 UNAUTHORIZED 성격의 코드 신설/사용을 권장합니다.
- 중복 제거: validateUserId(=buyer), validateIsOwner(=owner)를 내부 공통 메서드로 일반화하면 유지보수가 수월합니다.
예시:
private void validateParticipant(Long contractChatId, Long userId, Function<Long, Optional<Long>> idResolver, BusinessException whenNotMatch) { Long expected = idResolver.apply(contractChatId).orElseThrow(() -> whenNotMatch); if (!userId.equals(expected)) throw whenNotMatch; }src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (1)
76-83: 중복 제거 OK. 헬퍼 사용으로 인증 로직 간결화됨getUserIdFromAuthentication 사용으로 기존 중복 코드가 정리되었습니다. 추가로 전 엔드포인트에 Swagger 문서화(@operation 등) 적용을 권장합니다.
예시:
@Operation(summary = "계약 채팅 URL 이동", description = "계약 채팅방 URL을 반환합니다.") @GetMapping("/{chatRoomId}/moveContractChat") public ResponseEntity<ApiResponse<String>> moveContractChat(...)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
config-submodule(1 hunks)src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java(6 hunks)src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java(4 hunks)src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java(12 hunks)src/main/java/org/scoula/domain/chat/vo/ContractChat.java(3 hunks)src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java(2 hunks)src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java(1 hunks)src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java(4 hunks)src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java(4 hunks)src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- config-submodule
🧰 Additional context used
📓 Path-based instructions (5)
src/main/java/org/scoula/**/service/*Service{Interface,Impl}.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use interface-implementation pattern for services:
*ServiceInterface.java(interface) and*ServiceImpl.java(implementation)
Files:
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.javasrc/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.javasrc/main/java/org/scoula/domain/chat/service/ChatServiceImpl.javasrc/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.javasrc/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
src/main/java/org/scoula/domain/*/mapper/*Mapper.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
src/main/java/org/scoula/domain/*/mapper/*Mapper.java: Define MyBatis mapper interfaces undersrc/main/java/org/scoula/domain/*/mapper/*Mapper.java
Annotate MyBatis mapper interfaces with@Mapper
Files:
src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java
src/main/resources/org/scoula/domain/*/mapper/*Mapper.xml
📄 CodeRabbit Inference Engine (CLAUDE.md)
src/main/resources/org/scoula/domain/*/mapper/*Mapper.xml: Place MyBatis XML mappers atsrc/main/resources/org/scoula/domain/*/mapper/*Mapper.xml
Name MyBatis XML mappers*Mapper.xmlto match their corresponding*Mapper.javainterfaces
Files:
src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
src/main/java/org/scoula/**/controller/*Controller{,Impl}.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
Use interface-implementation pattern for controllers:
*Controller.java(interface) and*ControllerImpl.java(implementation)
Files:
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
src/main/java/org/scoula/**/controller/**/*.java
📄 CodeRabbit Inference Engine (CLAUDE.md)
src/main/java/org/scoula/**/controller/**/*.java: All controller endpoints should return a consistentApiResponse<T>wrapper
Add Swagger annotations to controller endpoints for API documentation
Files:
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
🧠 Learnings (3)
📚 Learning: 2025-08-04T00:51:44.587Z
Learnt from: Whatdoyumin
PR: ITZEEP/backend#40
File: src/main/resources/org/scoula/domain/precontract/mapper/OwnerPreContractMapper.xml:188-192
Timestamp: 2025-08-04T00:51:44.587Z
Learning: In the ITZEEP backend codebase, the owner_precontract_check table uses contract_chat_id as both primary key (PK) and foreign key (FK), so selectOwnerPrecheckId correctly returns contract_chat_id for use in upsertRestoreScope operations. This is confirmed by the join pattern "LEFT JOIN owner_restore_scope ors ON opc.contract_chat_id = ors.owner_precheck_id" in the mapper files.
Applied to files:
src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.javasrc/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
📚 Learning: 2025-08-04T00:51:44.587Z
Learnt from: Whatdoyumin
PR: ITZEEP/backend#40
File: src/main/resources/org/scoula/domain/precontract/mapper/OwnerPreContractMapper.xml:188-192
Timestamp: 2025-08-04T00:51:44.587Z
Learning: In the ITZEEP backend codebase, the owner_precontract_check table uses contract_chat_id as both primary key (PK) and foreign key (FK), so selectOwnerPrecheckId correctly returns contract_chat_id for use in upsertRestoreScope operations.
Applied to files:
src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.javasrc/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
📚 Learning: 2025-07-30T11:26:48.397Z
Learnt from: minnieming
PR: ITZEEP/backend#29
File: src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml:0-0
Timestamp: 2025-07-30T11:26:48.397Z
Learning: In the ITZEEP backend codebase's precontract domain, the developer prefers to separate MyBatis queries when they have different return types (e.g., Long vs String) rather than combining them into a single query with a resultMap, for clarity and maintainability purposes.
Applied to files:
src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: CodeQL Security Analysis (java)
- GitHub Check: Test Coverage
🔇 Additional comments (10)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (2)
844-851: 계약 수락 후 링크 메시지: senderId=소유자, type=URLLINK 전환 적절수락 후 링크 전달 주체를 소유자로, 타입을 URLLINK로 보내도록 변경한 판단이 명확합니다. 프론트 미리보기/알림 정책과도 일관성이 높아집니다(알림 switch의 URLLINK 케이스만 보강하면 완전).
655-671: 검토 결과:ChatServiceImpl.isUserInContractChatRoom로직은 문제 없습니다
ChatControllerImpl의 enterChatRoom 핸들러가 전달받은chatRoomId파라미터에 계약 채팅방 ID를 그대로 사용하여setUserCurrentChatRoom(userId, chatRoomId)를 호출하므로,userCurrentChatRoom맵을 통해 일반/계약 채팅방을 모두 올바르게 추적합니다.
또한 계약 채팅 전용으로는 ContractChatServiceImpl 에서 별도의 맵(contractChatOnlineUsers)과 메서드(isUserInContractChatRoom)를 두고 있어, 전용 맵 추가나 분리된 훅 구현은 필요치 않습니다.Likely an incorrect or invalid review comment.
src/main/java/org/scoula/domain/chat/vo/ContractChat.java (1)
32-34: ROUND4/STEP4 추가: ordinal 의존성 확인 결과
- JPA(@Enumerated) 매핑 어노테이션이나
EnumType.ORDINAL사용 흔적이 없습니다.- MyBatis XML 매퍼(src/main/resources/org/scoula/domain/chat/mapper/ContractChatMapper.xml)에서
resultType="…ContractChat$ContractStatus"으로만 매핑되어 있어, 기본 EnumTypeHandler(문자열 기반)로 동작합니다.하지만 DB 스키마의
contract_chat.status컬럼이 VARCHAR(…)로 정의되어 있는지, 별도 커스텀 TypeHandler가 등록되어 있지 않은지 직접 한번 더 확인해 주세요.src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1)
85-86: Mongo 계약서 최초 생성 권한을 ‘소유자’로 제한 — 요구사항 확인 권장saveContractMongo에서 buyer 검증 → owner 검증으로 변경되었습니다. 제품 요구사항에 부합하는지(초기 생성 권한이 임대인 한정인지) 프론트/기획과 합의된 내용인지 확인해 주세요.
변경 영향:
- 기존에 임차인이 계약서를 생성하던 플로우가 있었다면 403/에러로 바뀝니다.
- 관련 E2E/통합 테스트 업데이트 필요.
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (4)
497-497: 사소: @OverRide 추가 변경별도 이슈 없습니다.
549-579: 접근 제어 포함 및 흐름 적절사용자 인증 → 접근권한 체크 → 업데이트 → 시작점 세팅의 플로우가 명확합니다.
893-939: 최종 특약 확정 응답: 역할 검증 및 Redis 키 체크 적절임차인 역할 검증 및 Redis 키 검증 플로우가 명확합니다. 응답 메시지도 일관적입니다.
941-964: 계약 상태 조회: 접근권한 체크 및 상태 유효성 검사 적절ApiResponse 래핑과 에러 메시지 일관성도 좋습니다.
src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (2)
605-609: 빌더 패턴 반환 개선AI 특약 요청을 위한 빌더가 이제 null 대신 적절한 객체를 반환하도록 개선되었습니다.
614-616: null 체크 로직 개선됨contractDocument가 null일 때 빈 OcrData 대신 null을 반환하도록 개선되어 더 명확한 의도를 전달합니다.
| @Override | ||
| @GetMapping("/special-contract/{contractChatId}/incomplete/now") | ||
| public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>> | ||
| getIncompleteSpecialContractsWithoutMessage( | ||
| @PathVariable Long contractChatId, Authentication authentication) { | ||
|
|
||
| try { | ||
| Long userId = getUserIdFromAuthentication(authentication); | ||
| List<SpecialContractFixDocument> result = | ||
| contractChatService.getIncompleteSpecialContractsWithoutMessage( | ||
| contractChatId, userId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(result, "메시지가 없는 미완료 특약 목록 조회 성공")); | ||
| } catch (Exception e) { | ||
| return ResponseEntity.internalServerError() | ||
| .body(ApiResponse.error("INTERNAL_ERROR", "미완료 특약 목록 조회 중 오류가 발생했습니다.")); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
접근권한 체크 누락 — 임의 접근 가능성
getIncompleteSpecialContractsWithoutMessage는 isUserInContractChat 체크가 없습니다. 다른 엔드포인트와 일관되게 접근 제어를 추가하세요.
수정안(diff):
public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>>
getIncompleteSpecialContractsWithoutMessage(
@PathVariable Long contractChatId, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
+ if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+ return ResponseEntity.badRequest()
+ .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+ }
List<SpecialContractFixDocument> result =
contractChatService.getIncompleteSpecialContractsWithoutMessage(
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.
| @Override | |
| @GetMapping("/special-contract/{contractChatId}/incomplete/now") | |
| public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>> | |
| getIncompleteSpecialContractsWithoutMessage( | |
| @PathVariable Long contractChatId, Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| List<SpecialContractFixDocument> result = | |
| contractChatService.getIncompleteSpecialContractsWithoutMessage( | |
| contractChatId, userId); | |
| return ResponseEntity.ok(ApiResponse.success(result, "메시지가 없는 미완료 특약 목록 조회 성공")); | |
| } catch (Exception e) { | |
| return ResponseEntity.internalServerError() | |
| .body(ApiResponse.error("INTERNAL_ERROR", "미완료 특약 목록 조회 중 오류가 발생했습니다.")); | |
| } | |
| } | |
| @Override | |
| @GetMapping("/special-contract/{contractChatId}/incomplete/now") | |
| public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>> | |
| getIncompleteSpecialContractsWithoutMessage( | |
| @PathVariable Long contractChatId, Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| if (!contractChatService.isUserInContractChat(contractChatId, userId)) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); | |
| } | |
| List<SpecialContractFixDocument> result = | |
| contractChatService.getIncompleteSpecialContractsWithoutMessage( | |
| contractChatId, userId); | |
| return ResponseEntity.ok(ApiResponse.success(result, "메시지가 없는 미완료 특약 목록 조회 성공")); | |
| } catch (Exception e) { | |
| return ResponseEntity.internalServerError() | |
| .body(ApiResponse.error("INTERNAL_ERROR", "미완료 특약 목록 조회 중 오류가 발생했습니다.")); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 666 to 684, the method getIncompleteSpecialContractsWithoutMessage
is missing the access control check present in other endpoints; before calling
contractChatService.getIncompleteSpecialContractsWithoutMessage, retrieve the
userId from Authentication, call
contractChatService.isUserInContractChat(contractChatId, userId) and if it
returns false immediately return a 403 ResponseEntity with an ApiResponse.error
code (e.g., "FORBIDDEN", message "접근 권한이 없습니다."), otherwise continue to call the
service and return the success response as before; keep the existing try/catch
handling.
| @Override | ||
| @PostMapping("/final-contract/{contractChatId}/modification-request") | ||
| public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification( | ||
| @PathVariable Long contractChatId, | ||
| @RequestBody FinalContractModificationRequestDto requestDto, | ||
| Authentication authentication) { | ||
| try { | ||
| Long userId = getUserIdFromAuthentication(authentication); | ||
|
|
||
| ModificationRequestData result = | ||
| contractChatService.requestFinalContractModification( | ||
| contractChatId, userId, requestDto); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(result, "수정 요청이 성공적으로 전송되었습니다.")); | ||
|
|
||
| } catch (IllegalArgumentException e) { | ||
| return ResponseEntity.badRequest() | ||
| .body(ApiResponse.error("REQUEST_FAILED", e.getMessage())); | ||
| } catch (Exception e) { | ||
| log.error("최종 특약서 수정 요청 실패", e); | ||
| return ResponseEntity.internalServerError() | ||
| .body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다.")); | ||
| } | ||
| } |
There was a problem hiding this comment.
최종 특약 수정 요청: 접근권한 체크 누락
해당 계약 채팅 참여자 검증이 없습니다. 일관된 보안을 위해 추가 필요합니다.
수정안(diff):
public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationRequestDto requestDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
+ if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+ return ResponseEntity.badRequest()
+ .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+ }
ModificationRequestData result =
contractChatService.requestFinalContractModification(
contractChatId, userId, requestDto);📝 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 | |
| @PostMapping("/final-contract/{contractChatId}/modification-request") | |
| public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification( | |
| @PathVariable Long contractChatId, | |
| @RequestBody FinalContractModificationRequestDto requestDto, | |
| Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| ModificationRequestData result = | |
| contractChatService.requestFinalContractModification( | |
| contractChatId, userId, requestDto); | |
| return ResponseEntity.ok(ApiResponse.success(result, "수정 요청이 성공적으로 전송되었습니다.")); | |
| } catch (IllegalArgumentException e) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("REQUEST_FAILED", e.getMessage())); | |
| } catch (Exception e) { | |
| log.error("최종 특약서 수정 요청 실패", e); | |
| return ResponseEntity.internalServerError() | |
| .body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다.")); | |
| } | |
| } | |
| @Override | |
| @PostMapping("/final-contract/{contractChatId}/modification-request") | |
| public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification( | |
| @PathVariable Long contractChatId, | |
| @RequestBody FinalContractModificationRequestDto requestDto, | |
| Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| + if (!contractChatService.isUserInContractChat(contractChatId, userId)) { | |
| + return ResponseEntity.badRequest() | |
| + .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); | |
| + } | |
| ModificationRequestData result = | |
| contractChatService.requestFinalContractModification( | |
| contractChatId, userId, requestDto); | |
| return ResponseEntity.ok(ApiResponse.success(result, "수정 요청이 성공적으로 전송되었습니다.")); | |
| } catch (IllegalArgumentException e) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("REQUEST_FAILED", e.getMessage())); | |
| } catch (Exception e) { | |
| log.error("최종 특약서 수정 요청 실패", e); | |
| return ResponseEntity.internalServerError() | |
| .body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다.")); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 724 to 747, the handler for POST
/final-contract/{contractChatId}/modification-request is missing a check that
the authenticated user is a participant of the contract chat; add a validation
after retrieving userId that calls a service method (e.g.
contractChatService.isParticipant(contractChatId, userId) or
contractChatService.validateParticipant(contractChatId, userId)) and if the
check fails return
ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error("FORBIDDEN",
"권한이 없습니다.")) before calling requestFinalContractModification so only
participants can send modification requests.
| @Override | ||
| @PostMapping("/final-contract/{contractChatId}/modification-response") | ||
| public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest( | ||
| @PathVariable Long contractChatId, | ||
| @RequestBody FinalContractModificationResponseDto responseDto, | ||
| Authentication authentication) { | ||
| try { | ||
| Long userId = getUserIdFromAuthentication(authentication); | ||
|
|
||
| FinalSpecialContractDocument result = | ||
| contractChatService.respondToModificationRequest( | ||
| contractChatId, userId, responseDto); | ||
|
|
||
| String message = responseDto.isAccepted() ? "수정 요청을 수락했습니다." : "수정 요청을 거절했습니다."; | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(result, message)); | ||
|
|
||
| } catch (IllegalArgumentException e) { | ||
| return ResponseEntity.badRequest() | ||
| .body(ApiResponse.error("RESPONSE_FAILED", e.getMessage())); | ||
| } catch (Exception e) { | ||
| log.error("수정 요청 응답 실패", e); | ||
| return ResponseEntity.internalServerError() | ||
| .body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다.")); | ||
| } | ||
| } |
There was a problem hiding this comment.
최종 특약 수정 응답: 접근권한 체크 누락
요청과 동일하게 접근 제어 추가가 필요합니다.
수정안(diff):
public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationResponseDto responseDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
+ if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+ return ResponseEntity.badRequest()
+ .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+ }
FinalSpecialContractDocument result =
contractChatService.respondToModificationRequest(📝 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 | |
| @PostMapping("/final-contract/{contractChatId}/modification-response") | |
| public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest( | |
| @PathVariable Long contractChatId, | |
| @RequestBody FinalContractModificationResponseDto responseDto, | |
| Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| FinalSpecialContractDocument result = | |
| contractChatService.respondToModificationRequest( | |
| contractChatId, userId, responseDto); | |
| String message = responseDto.isAccepted() ? "수정 요청을 수락했습니다." : "수정 요청을 거절했습니다."; | |
| return ResponseEntity.ok(ApiResponse.success(result, message)); | |
| } catch (IllegalArgumentException e) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("RESPONSE_FAILED", e.getMessage())); | |
| } catch (Exception e) { | |
| log.error("수정 요청 응답 실패", e); | |
| return ResponseEntity.internalServerError() | |
| .body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다.")); | |
| } | |
| } | |
| @Override | |
| @PostMapping("/final-contract/{contractChatId}/modification-response") | |
| public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest( | |
| @PathVariable Long contractChatId, | |
| @RequestBody FinalContractModificationResponseDto responseDto, | |
| Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| if (!contractChatService.isUserInContractChat(contractChatId, userId)) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); | |
| } | |
| FinalSpecialContractDocument result = | |
| contractChatService.respondToModificationRequest( | |
| contractChatId, userId, responseDto); | |
| String message = responseDto.isAccepted() ? "수정 요청을 수락했습니다." : "수정 요청을 거절했습니다."; | |
| return ResponseEntity.ok(ApiResponse.success(result, message)); | |
| } catch (IllegalArgumentException e) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("RESPONSE_FAILED", e.getMessage())); | |
| } catch (Exception e) { | |
| log.error("수정 요청 응답 실패", e); | |
| return ResponseEntity.internalServerError() | |
| .body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다.")); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 749 to 774, the respondToModificationRequest endpoint is missing an
access control check; before calling
contractChatService.respondToModificationRequest retrieve/verify the
authenticated user's permission for the given contractChatId (e.g., call an
existing service method to validate participation/ownership or fetch the chat
and compare userId to allowed parties) and if the user is unauthorized return a
403 (or throw an AccessDeniedException translated to 403); ensure the controller
does not proceed to process the response unless the access check passes so only
authorized participants can accept/reject modification requests.
| @Override | ||
| @PostMapping("/{contractChatId}/final-contract/request-confirmation") | ||
| public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation( | ||
| @PathVariable Long contractChatId, Authentication authentication) { | ||
| try { | ||
| Long userId = getUserIdFromAuthentication(authentication); | ||
| contractChatService.requestFinalContractConfirmation(contractChatId, userId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); | ||
| } catch (Exception e) { | ||
| log.error("최종 특약 확정 요청 실패", e); | ||
| return ResponseEntity.badRequest() | ||
| .body(ApiResponse.error("최종 특약 확정 요청에 실패했습니다: " + e.getMessage())); | ||
| } | ||
| } |
There was a problem hiding this comment.
최종 특약 확정 요청: 접근권한 체크 누락 및 경로 컨벤션 일관성 제안
- 접근 제어 추가 필요.
- 경로 패턴이 일부는 "/final-contract/{contractChatId}/..." 일부는 "/{contractChatId}/final-contract/..."로 혼재. 일관된 prefix 유지 권장.
수정안(diff):
public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation(
@PathVariable Long contractChatId, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
+ if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+ return ResponseEntity.badRequest()
+ .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+ }
contractChatService.requestFinalContractConfirmation(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.
| @Override | |
| @PostMapping("/{contractChatId}/final-contract/request-confirmation") | |
| public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation( | |
| @PathVariable Long contractChatId, Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| contractChatService.requestFinalContractConfirmation(contractChatId, userId); | |
| return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); | |
| } catch (Exception e) { | |
| log.error("최종 특약 확정 요청 실패", e); | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("최종 특약 확정 요청에 실패했습니다: " + e.getMessage())); | |
| } | |
| } | |
| @Override | |
| @PostMapping("/{contractChatId}/final-contract/request-confirmation") | |
| public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation( | |
| @PathVariable Long contractChatId, Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| if (!contractChatService.isUserInContractChat(contractChatId, userId)) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); | |
| } | |
| contractChatService.requestFinalContractConfirmation(contractChatId, userId); | |
| return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다.")); | |
| } catch (Exception e) { | |
| log.error("최종 특약 확정 요청 실패", e); | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("최종 특약 확정 요청에 실패했습니다: " + e.getMessage())); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 832-846, the endpoint lacks an access control check and its path
pattern conflicts with other routes; add an authorization check (e.g., validate
the caller is the contract participant/owner by calling a service method or add
@PreAuthorize to ensure proper role/ownership) before invoking
contractChatService.requestFinalContractConfirmation, and change the
@PostMapping to follow the chosen project convention consistently (for example,
use @PostMapping("/final-contract/{contractChatId}/request-confirmation") or
update all similar endpoints to the "/{contractChatId}/final-contract/..."
pattern across the controller) so routes are uniform.
| @Override | ||
| @PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}") | ||
| public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion( | ||
| Long contractChatId, Integer clauseOrder, Authentication authentication) { | ||
| try { | ||
| Long userId = getUserIdFromAuthentication(authentication); | ||
| contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder); | ||
|
|
||
| return ResponseEntity.ok( | ||
| ApiResponse.success( | ||
| String.format("특약 %d번 삭제 요청이 임차인에게 전송되었습니다.", clauseOrder))); | ||
| } catch (Exception e) { | ||
| log.error("최종 특약 삭제 요청 실패", e); | ||
| return ResponseEntity.badRequest() | ||
| .body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage())); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
PathVariable 바인딩 누락으로 런타임 오류 위험 + 접근권한 체크 누락
메서드 시그니처에 @PathVariable이 없어 경로 변수 바인딩 실패(400/500)가 발생합니다. 또한 접근 제어 추가가 필요합니다.
수정안(diff):
- public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion(
- Long contractChatId, Integer clauseOrder, Authentication authentication) {
+ public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion(
+ @PathVariable Long contractChatId,
+ @PathVariable Integer clauseOrder,
+ Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
+ if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+ return ResponseEntity.badRequest()
+ .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+ }
contractChatService.requestFinalContractDeletion(contractChatId, userId, 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.
| @Override | |
| @PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}") | |
| public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion( | |
| Long contractChatId, Integer clauseOrder, Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder); | |
| return ResponseEntity.ok( | |
| ApiResponse.success( | |
| String.format("특약 %d번 삭제 요청이 임차인에게 전송되었습니다.", clauseOrder))); | |
| } catch (Exception e) { | |
| log.error("최종 특약 삭제 요청 실패", e); | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage())); | |
| } | |
| } | |
| @Override | |
| @PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}") | |
| public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion( | |
| @PathVariable Long contractChatId, | |
| @PathVariable Integer clauseOrder, | |
| Authentication authentication) { | |
| try { | |
| Long userId = getUserIdFromAuthentication(authentication); | |
| if (!contractChatService.isUserInContractChat(contractChatId, userId)) { | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다.")); | |
| } | |
| contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder); | |
| return ResponseEntity.ok( | |
| ApiResponse.success( | |
| String.format("특약 %d번 삭제 요청이 임차인에게 전송되었습니다.", clauseOrder))); | |
| } catch (Exception e) { | |
| log.error("최종 특약 삭제 요청 실패", e); | |
| return ResponseEntity.badRequest() | |
| .body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage())); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 848-865, the controller method is missing @PathVariable annotations
(so path variables won't bind) and lacks an access control check; add
@PathVariable("contractChatId") to the contractChatId parameter and
@PathVariable("clauseOrder") to the clauseOrder parameter, then enforce
authorization either with a method-level security annotation (e.g.
@PreAuthorize) or by calling a service check (e.g.
contractChatService.verifyUserAccess(contractChatId, userId)) after resolving
userId and before performing the deletion request, throwing or returning a 403
response on unauthorized access.
| @Value("${front.base.url}") | ||
| private String baseUrl; | ||
|
|
||
| private String contractChatUrl = "/contract/"; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
하드코딩된 URL 패턴을 설정 파일로 관리하세요
프론트엔드 URL과 계약 채팅 경로가 하드코딩되어 있습니다. 이를 application.yml에서 관리하면 환경별 배포가 용이합니다.
- @Value("${front.base.url}")
- private String baseUrl;
-
- private String contractChatUrl = "/contract/";
+ @Value("${front.base.url}")
+ private String baseUrl;
+
+ @Value("${front.contract-chat.path:/contract/}")
+ private String contractChatUrl;application.yml에 추가:
front:
contract-chat:
path: /contract/🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 53 to 57, the contractChatUrl string is hardcoded; move this value
into configuration by adding front.contract-chat.path to application.yml as
suggested and inject it into the class (e.g. via
@Value("${front.contract-chat.path}") or a @ConfigurationProperties bean),
replace the hardcoded field with the injected property, and update any usages to
reference the injected variable so the path can be managed per environment.
| public String getContractChatRoomUrl(Long chatRoomId) { | ||
| ChatRoom chatRoom = chatRoomMapper.findById(chatRoomId); | ||
| if (chatRoom == null) { | ||
| log.error("채팅방을 찾을 수 없음: {}", chatRoomId); | ||
| throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND); | ||
| } | ||
| ContractChat contractChatId = | ||
| contractChatMapper.findByUserAndHome( | ||
| chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId()); | ||
| if (contractChatId == null) { | ||
| log.error("채팅방을 찾을 수 없음: {}", chatRoomId); | ||
| throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND); | ||
| } | ||
| Long contractChatRoomId = contractChatId.getContractChatId(); | ||
| String param = getContractChatStatus(contractChatId.getStatus()); | ||
|
|
||
| return baseUrl + contractChatUrl + contractChatRoomId.toString() + param; | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
프론트엔드 URL 생성 로직을 개선하세요
URL이 하드코딩되어 있고, null 체크가 불충분합니다.
public String getContractChatRoomUrl(Long chatRoomId) {
ChatRoom chatRoom = chatRoomMapper.findById(chatRoomId);
if (chatRoom == null) {
log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
}
+
ContractChat contractChatId =
contractChatMapper.findByUserAndHome(
chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId());
if (contractChatId == null) {
- log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
- throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
+ log.error("계약 채팅을 찾을 수 없음 - chatRoomId: {}, ownerId: {}, buyerId: {}, homeId: {}",
+ chatRoomId, chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId());
+ throw new BusinessException(ChatErrorCode.CONTRACT_CHAT_NOT_FOUND);
}
+
Long contractChatRoomId = contractChatId.getContractChatId();
String param = getContractChatStatus(contractChatId.getStatus());
return baseUrl + contractChatUrl + contractChatRoomId.toString() + param;
}다음 스크립트를 실행하여 CONTRACT_CHAT_NOT_FOUND 에러 코드가 정의되어 있는지 확인하세요:
🏁 Script executed:
#!/bin/bash
# ChatErrorCode enum에서 CONTRACT_CHAT_NOT_FOUND 확인
rg -A 5 "enum ChatErrorCode" --type java
rg "CONTRACT_CHAT_NOT_FOUND" --type javaLength of output: 872
프론트엔드 URL 생성 로직 개선 및 에러 처리 강화
baseUrl과 contractChatUrl을 애플리케이션 설정으로 외부화하고, UriComponentsBuilder 등을 사용해 URL을 안전하게 조립하세요. 또한, contractChat 미조회 시 로깅 메시지에 상세 정보를 추가하고, 정의된 CONTRACT_CHAT_NOT_FOUND 에러 코드를 사용하도록 수정합니다.
적용 대상:
- 파일:
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java(2541–2558)
제안된 변경(diff):
if (contractChatId == null) {
- log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
- throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
+ log.error(
+ "계약 채팅을 찾을 수 없음 - chatRoomId: {}, ownerId: {}, buyerId: {}, homeId: {}",
+ chatRoomId, chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId()
+ );
+ throw new BusinessException(ChatErrorCode.CONTRACT_CHAT_NOT_FOUND);
}🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 2541-2558, the method builds the frontend URL by concatenating
baseUrl and contractChatUrl directly and logs the wrong error/insufficient
details when contractChat lookup fails; change baseUrl and contractChatUrl to be
injected from application settings (e.g., @Value or configuration properties),
use UriComponentsBuilder (or equivalent) to safely build the URL with path
segments and query params instead of simple string concatenation, enhance the
null-case logging to include chatRoomId, ownerId, buyerId and homeId, and throw
the defined CONTRACT_CHAT_NOT_FOUND BusinessException constant when contractChat
is null; ensure contractChatId is used safely (null-check) before calling
getContractChatId and return the constructed URI.toString().
| Optional<Long> selectContractOwnerId(@Param("contractChatId") Long contractChatId); | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
계약 소유자 조회 메서드 추가 적절. 네이밍 컨벤션만 정리 제안
Optional selectContractOwnerId(Long contractChatId) 도입은 ContractService의 소유자 검증에 적합합니다.
- 네이밍: 동일 블록에 selectContractBuyerId, selectBuyerId가 공존해 의미가 혼재됩니다. 일관성을 위해
- selectContractBuyerId ↔ selectContractOwnerId (contract_chat 기준)
- selectBuyerId → selectBuyerIdFromPrecontract 혹은 selectPrecontractBuyerId (precontract 기준)
와 같이 구분하면 이해가 쉬워집니다.
다음 스크립트로 매퍼 XML/사용처 일치 여부를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
rg -n "selectContractOwnerId\\(" -A 2 -B 2
rg -n "id=\"selectContractOwnerId\"" src/main/resourcesLength of output: 5096
네이밍 컨벤션 통일 제안
매퍼 XML과 인터페이스 메서드 매핑에는 문제가 없으므로, 메서드 이름만 일관되게 정리하면 가독성이 크게 향상됩니다.
- src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java (18–22라인)
• selectContractBuyerId, selectContractOwnerId: 둘 다contractChatId기준이므로 “selectContractBuyerId ↔ selectContractOwnerId” 형태로 유지
• selectBuyerId: precontract 기준임을 명확히 하기 위해 “selectBuyerIdFromPrecontract” 또는 “selectPrecontractBuyerId”로 변경 제안
위와 같이 네이밍을 통일해 주시면 좋겠습니다.
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java
around lines 18–22, the method names should be made consistent: keep the pair
selectContractBuyerId and selectContractOwnerId as they both use contractChatId,
and rename selectBuyerId to a clearer name such as selectPrecontractBuyerId (or
selectBuyerIdFromPrecontract). Update the method signature in this interface to
the new name and then update the corresponding mapper XML / any call sites to
use the new method name so the interface and XML mapping remain in sync.
🚀 관련 이슈
특약 로직 수정 및 AI 메시지 추가
#37
🔑 주요 변경사항
✔️ 체크 리스트
mainbranch에 실수로 PR 생성 금지)📢 To Reviewers
📸 스크린샷 or 실행영상
Summary by CodeRabbit
New Features
Refactor
Chores