-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor/diary 코드 확인 #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "refactor/diary-\uCF54\uB4DC-\uD655\uC778"
Refactor/diary 코드 확인 #114
Changes from all commits
6d4a7ec
eda0784
aa39268
d68eee8
ff5f892
891deb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,12 +25,11 @@ | |||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||
| public class AvatarService { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private static final Long AI_AVATAR_MASTER_ID = 9999L; | ||||||||||||||||||||||||||||||
| private final AvatarRepository avatarRepository; | ||||||||||||||||||||||||||||||
| private final UserRepository userRepository; | ||||||||||||||||||||||||||||||
| private final AvatarMasterRepository avatarMasterRepository; // [추가] AvatarMaster 조회 위해 주입 | ||||||||||||||||||||||||||||||
| private final NotificationService notificationService; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private static final Long AI_AVATAR_MASTER_ID = 9999L; | ||||||||||||||||||||||||||||||
| private final WishTreeRepository wishTreeRepository; | ||||||||||||||||||||||||||||||
| private final GardenRepository gardenRepository; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -39,21 +38,27 @@ public void createAvatar(Long userId, String nickname, String imageUrl, Long mas | |||||||||||||||||||||||||||||
| User user = | ||||||||||||||||||||||||||||||
| userRepository | ||||||||||||||||||||||||||||||
| .findById(userId) | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND)); | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.USER_NOT_FOUND)); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| AvatarMaster master; | ||||||||||||||||||||||||||||||
| if (masterId != null) { | ||||||||||||||||||||||||||||||
| // 1. 기존 목록에서 선택한 경우: 전달받은 masterId로 AvatarMaster를 찾습니다. | ||||||||||||||||||||||||||||||
| // TODO: 'masterId + 2'와 같은 매직 넘버 로직은 위험합니다. | ||||||||||||||||||||||||||||||
| // 프론트엔드에서 전달하는 ID와 DB의 ID가 일치하도록 데이터 정합성을 맞추거나, | ||||||||||||||||||||||||||||||
| // 이 로직에 대한 명확한 주석과 문서화가 필요합니다. | ||||||||||||||||||||||||||||||
| master = | ||||||||||||||||||||||||||||||
| avatarMasterRepository | ||||||||||||||||||||||||||||||
| .findById(masterId + 2) | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND)); // 예외 유형 구체화 | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.AVATAR_MASTER_NOT_FOUND)); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| // 2. AI로 생성한 경우: 약속된 AI_AVATAR_MASTER_ID로 AvatarMaster를 찾습니다. | ||||||||||||||||||||||||||||||
| master = | ||||||||||||||||||||||||||||||
| avatarMasterRepository | ||||||||||||||||||||||||||||||
| .findById(AI_AVATAR_MASTER_ID) | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND)); | ||||||||||||||||||||||||||||||
| .orElseThrow( | ||||||||||||||||||||||||||||||
| () -> | ||||||||||||||||||||||||||||||
| new CustomApiException( | ||||||||||||||||||||||||||||||
| ErrorCode.AVATAR_MASTER_NOT_FOUND, "AI 아바타 원본을 찾을 수 없습니다.")); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Avatar newAvatar = | ||||||||||||||||||||||||||||||
|
|
@@ -69,7 +74,8 @@ public void createAvatar(Long userId, String nickname, String imageUrl, Long mas | |||||||||||||||||||||||||||||
| WishTree wishTree = | ||||||||||||||||||||||||||||||
| wishTreeRepository | ||||||||||||||||||||||||||||||
| .findByUserId(userId) | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND)); | ||||||||||||||||||||||||||||||
| .orElseThrow( | ||||||||||||||||||||||||||||||
| () -> new CustomApiException(ErrorCode.NOT_FOUND, "사용자의 소원나무를 찾을 수 없습니다.")); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainWishTree 미존재 시 전용 에러 코드 사용 검토 현재 NOT_FOUND + 메시지를 사용합니다. 에러 핸들링 일관성과 클라이언트 분기 처리를 위해 WISH_TREE_NOT_FOUND 같은 전용 코드 사용을 권장합니다(존재 시). - .orElseThrow(
- () -> new CustomApiException(ErrorCode.NOT_FOUND, "사용자의 소원나무를 찾을 수 없습니다."));
+ .orElseThrow(
+ () -> new CustomApiException(ErrorCode.WISH_TREE_NOT_FOUND, "사용자의 소원나무를 찾을 수 없습니다."));ErrorCode에 WISH_TREE_NOT_FOUND가 없으면 추가 도입이 필요합니다. 다음 스크립트로 확인하세요. 🏁 Script executed: #!/bin/bash
rg -n "enum\s+ErrorCode" -A200 | rg -n "WISH_TREE_NOT_FOUND|AVATAR_NOT_FOUND|GARDEN_SLOT_MAXED_OUT"Length of output: 256 WishTree 전용 에러 코드 추가 필요 ErrorCode enum에 • src/main/java/com/example/cp_main_be/global/common/ErrorCode.java public enum ErrorCode {
// ...
GARDEN_SLOT_MAXED_OUT(HttpStatus.BAD_REQUEST, "E40006", "더 이상 텃밭을 추가할 수 없습니다."),
+ WISH_TREE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40404", "사용자의 소원나무를 찾을 수 없습니다."),
// ...
}• src/main/java/com/example/cp_main_be/domain/avatar/avatar/service/AvatarService.java - .orElseThrow(
- () -> new CustomApiException(ErrorCode.NOT_FOUND, "사용자의 소원나무를 찾을 수 없습니다."));
+ .orElseThrow(
+ () -> new CustomApiException(ErrorCode.WISH_TREE_NOT_FOUND, "사용자의 소원나무를 찾을 수 없습니다."));📝 Committable suggestion
Suggested change
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| WishTreeStage stage = wishTree.getStage(); | ||||||||||||||||||||||||||||||
| int maxGardens = stage.getMaxGardens(); | ||||||||||||||||||||||||||||||
|
|
@@ -80,22 +86,23 @@ public void createAvatar(Long userId, String nickname, String imageUrl, Long mas | |||||||||||||||||||||||||||||
| Garden.builder().user(user).slotNumber(userGardens.size() + 1).avatar(newAvatar).build(); | ||||||||||||||||||||||||||||||
| gardenRepository.save(newGarden); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| throw new CustomApiException(ErrorCode.MAX_GARDENS_REACHED); | ||||||||||||||||||||||||||||||
| // 이미 존재하는 'GARDEN_SLOT_MAXED_OUT' 에러 코드를 재사용하여 일관성을 유지합니다. | ||||||||||||||||||||||||||||||
| throw new CustomApiException(ErrorCode.GARDEN_SLOT_MAXED_OUT); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Transactional(readOnly = true) | ||||||||||||||||||||||||||||||
| public Avatar findAvatarById(Long avatarId) { | ||||||||||||||||||||||||||||||
| return avatarRepository | ||||||||||||||||||||||||||||||
| .findById(avatarId) | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND)); | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND, "해당 아바타를 찾을 수 없습니다.")); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public void givePollen(Long senderId, Long avatarId) { | ||||||||||||||||||||||||||||||
| User sender = | ||||||||||||||||||||||||||||||
| userRepository | ||||||||||||||||||||||||||||||
| .findById(senderId) | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND)); | ||||||||||||||||||||||||||||||
| .orElseThrow(() -> new CustomApiException(ErrorCode.USER_NOT_FOUND)); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Avatar avatar = findAvatarById(avatarId); | ||||||||||||||||||||||||||||||
| User receiver = avatar.getUser(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,16 +18,19 @@ | |
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.context.ApplicationEventPublisher; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| @Slf4j | ||
| public class GardenService { | ||
|
|
||
| private static final int MAX_GARDEN_COUNT = 4; | ||
| private static final int MAX_GARDEN_COUNT = 3; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainMAX_GARDEN_COUNT 4→3 변경은 브레이킹 체인지: 도메인/클라이언트 영향 검증 필요 정원 최대 개수 축소는 사용자 경험과 기존 데이터/클라이언트 로직에 영향을 줍니다. 정책 변경 근거와 마이그레이션/알림 계획, 관련 화면(예: 슬롯 해제 버튼, 홈 요약)의 표기 변경을 점검해 주세요. 영향 분포 탐색 스크립트: 🏁 Script executed: #!/bin/bash
# MAX_GARDEN_COUNT 사용처 및 슬롯 관련 문구/상수 검색
rg -n "MAX_GARDEN_COUNT|maxGardens|GARDEN_SLOT_MAXED_OUT|slotNumber|정원|텃밭" -C2Length of output: 39874 🏁 Script executed: #!/bin/bash
cat src/main/java/com/example/cp_main_be/domain/mission/wishTree/WishTreeStage.java | sed -n '1,200p'Length of output: 1063 GardenService 최대 개수 논리 불일치 – 즉각 수정 필요 GardenService의
등 심각한 동작 불일치가 발생합니다. 아래 항목을 긴급 정비해주세요: • 위 수정이 완료되면 종합 테스트 후 클라이언트 영향 범위를 최종 확인해 주세요. 🤖 Prompt for AI Agents |
||
| private static final int WATERING_POINTS = 2; | ||
| private static final int SUNLIGHT_POINTS = 3; | ||
| private static final int MAX_FRIEND_WATERING_PER_DAY = 3; | ||
|
|
@@ -50,57 +53,81 @@ public GardenResponse findGardenById(Long gardenId) { | |
|
|
||
| @Transactional | ||
| public void waterGarden(Long actorId, Long gardenId) { | ||
| // N+1 문제를 방지하기 위해 Garden과 User를 함께 조회하는 것을 권장합니다. | ||
| // 예: gardenRepository.findByIdWithUser(gardenId) | ||
| Garden garden = | ||
| gardenRepository | ||
| .findById(gardenId) | ||
| .orElseThrow(() -> new IllegalArgumentException("해당 텃밭을 찾을 수 없습니다.")); | ||
| .orElseThrow(() -> new CustomApiException(ErrorCode.GARDEN_NOT_FOUND)); | ||
|
|
||
| User owner = garden.getUser(); // 정원 주인 | ||
| User owner = garden.getUser(); | ||
|
|
||
| // Case 1: 자신의 정원에 물을 주는 경우 | ||
| if (owner.getId().equals(actorId)) { | ||
| // 8시간 쿨타임 체크 | ||
| if (garden.getLastWateredByOwnerAt() != null | ||
| && garden.getLastWateredByOwnerAt().plusHours(8).isAfter(LocalDateTime.now())) { | ||
| throw new IllegalStateException("아직 물을 줄 수 없습니다. 8시간이 지나야 가능합니다."); | ||
| } | ||
|
|
||
| garden.increaseWaterCount(); | ||
| userService.addExperience(actorId, WATERING_POINTS); | ||
| garden.recordOwnerWateringTime(); // 주인이 물 준 시간 기록 | ||
| waterOwnGarden(actorId, garden); | ||
| } else { | ||
| waterFriendGarden(actorId, garden); | ||
| } | ||
| // Case 2: 남의 정원에 물을 주는 경우 | ||
| else { | ||
| User actor = | ||
| userRepository | ||
| .findById(actorId) | ||
| .orElseThrow(() -> new IllegalArgumentException("물을 주는 사용자를 찾을 수 없습니다.")); | ||
|
|
||
| LocalDateTime startOfWateringDay = getStartOfCurrentWateringDay(); | ||
|
|
||
| // 1. 하루에 3회 제한 체크 | ||
| int todayWateringCount = | ||
| friendWateringLogRepository.countByWaterGiverAndWateredAtAfter(actor, startOfWateringDay); | ||
| if (todayWateringCount >= MAX_FRIEND_WATERING_PER_DAY) { | ||
| throw new IllegalStateException("오늘은 다른 사람의 정원에 더 이상 물을 줄 수 없습니다. (일일 3회 제한)"); | ||
| } | ||
|
|
||
| // 2. 같은 정원에 하루 한 번 제한 체크 | ||
| boolean alreadyWatered = | ||
| friendWateringLogRepository.existsByWaterGiverAndWateredGardenAndWateredAtAfter( | ||
| actor, garden, startOfWateringDay); | ||
| if (alreadyWatered) { | ||
| throw new IllegalStateException("이 정원에는 오늘 이미 물을 주었습니다."); | ||
| } | ||
|
|
||
| // 남한테 주는 경우에는 준 사람이 물 경험치를 받고 정원의 waterCount가 증가한다. | ||
| userService.addExperience(actorId, WATERING_POINTS); | ||
| garden.increaseWaterCount(); | ||
|
|
||
| // 물주기 활동 기록 | ||
| FriendWateringLog log = | ||
| FriendWateringLog.builder().waterGiver(actor).wateredGarden(garden).build(); | ||
| friendWateringLogRepository.save(log); | ||
| } | ||
|
|
||
| /** 자신의 정원에 물을 주는 로직을 처리합니다. */ | ||
| private void waterOwnGarden(Long ownerId, Garden garden) { | ||
| // 8시간 쿨타임 체크 | ||
| if (garden.getLastWateredByOwnerAt() != null | ||
| && garden.getLastWateredByOwnerAt().plusHours(8).isAfter(LocalDateTime.now())) { | ||
| throw new CustomApiException(ErrorCode.WATERING_COOL_DOWN); | ||
| } | ||
|
|
||
| garden.increaseWaterCount(); | ||
| userService.addExperience(ownerId, WATERING_POINTS); | ||
| garden.recordOwnerWateringTime(); // 주인이 물 준 시간 기록 | ||
| } | ||
|
|
||
| /** 친구의 정원에 물을 주는 로직을 처리합니다. */ | ||
| private void waterFriendGarden(Long actorId, Garden garden) { | ||
| User actor = | ||
| userRepository | ||
| .findById(actorId) | ||
| .orElseThrow(() -> new CustomApiException(ErrorCode.USER_NOT_FOUND)); | ||
|
|
||
| // 낮 12시 이후 -> 그날의 12시 날짜 반환, 이전 -> 전날의 12시 날짜 반환 | ||
| LocalDateTime startOfWateringDay = getStartOfCurrentWateringDay(); | ||
|
|
||
| // 1. 하루에 3회 제한 체크 | ||
| checkFriendWateringLimit(actor, startOfWateringDay); | ||
|
|
||
| // 2. 같은 정원에 하루 한 번 제한 체크 | ||
| checkAlreadyWateredToday(actor, garden, startOfWateringDay); | ||
|
|
||
| // 남한테 주는 경우에는 준 사람이 물 경험치를 받고 정원의 waterCount가 증가한다. | ||
| userService.addExperience(actorId, WATERING_POINTS); | ||
| garden.increaseWaterCount(); | ||
|
|
||
| // 물주기 활동 기록 | ||
| FriendWateringLog log = | ||
| FriendWateringLog.builder() | ||
| .waterGiver(actor) | ||
| .wateredGarden(garden) // wateredAt은 @CreatedDate가 자동으로 설정합니다. | ||
| .build(); | ||
| friendWateringLogRepository.save(log); | ||
| } | ||
|
Comment on lines
+86
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 친구 물주기 동시성·중복 처리 보강 동시에 두 요청이 들어올 경우 존재 여부 체크 후 삽입 사이 레이스가 발생할 수 있습니다. DB 레벨 제약 또는 고유 인덱스를 두어 하루 1회/정원 제한을 강제하세요. 또한 count/exists 쿼리에 필요한 인덱스(water_giver_id, watered_garden_id, watered_at)를 확인해 주세요.
|
||
|
|
||
| // 물주기 남은 횟수 확인 | ||
| private void checkFriendWateringLimit(User actor, LocalDateTime startOfWateringDay) { | ||
| int todayWateringCount = | ||
| friendWateringLogRepository.countByWaterGiverAndWateredAtAfter(actor, startOfWateringDay); | ||
| if (todayWateringCount >= MAX_FRIEND_WATERING_PER_DAY) { | ||
| throw new CustomApiException(ErrorCode.FRIEND_WATERING_LIMIT_EXCEEDED); | ||
| } | ||
| } | ||
|
|
||
| // 당일에 해당 정원에 이미 물을 주었는지 확인 | ||
| private void checkAlreadyWateredToday( | ||
| User actor, Garden garden, LocalDateTime startOfWateringDay) { | ||
| boolean alreadyWatered = | ||
| friendWateringLogRepository.existsByWaterGiverAndWateredGardenAndWateredAtAfter( | ||
| actor, garden, startOfWateringDay); | ||
| if (alreadyWatered) { | ||
| throw new CustomApiException(ErrorCode.ALREADY_WATERED_GARDEN); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -141,7 +168,7 @@ public void unlockNewGardenSlot(Long userId) { | |
| if (currentGardens >= MAX_GARDEN_COUNT) { | ||
| // 이미 최대치이므로 조용히 종료하거나 예외를 던질 수 있습니다. | ||
| // 여기서는 추가 생성을 막고 그냥 리턴합니다. | ||
| return; | ||
| throw new CustomApiException(ErrorCode.GARDEN_SLOT_MAXED_OUT); | ||
| } | ||
|
|
||
| // [기존 레벨 체크 로직 삭제!] | ||
|
|
@@ -211,4 +238,16 @@ private LocalDateTime getStartOfCurrentSunlightDay() { | |
| return todaySixAM; | ||
| } | ||
| } | ||
|
|
||
| /** 오래된 친구 물주기 로그를 주기적으로 삭제하는 스케줄링 작업입니다. cron = "0 0 4 * * *" : 매일 새벽 4시에 실행됩니다. */ | ||
| @Scheduled(cron = "0 0 4 * * *") | ||
| @Transactional // 쓰기 작업이므로 클래스 레벨의 readOnly 설정을 오버라이드합니다. | ||
| public void cleanupOldWateringLogs() { | ||
| // 7일 이상된 기록을 삭제하도록 설정. 이 값은 application.yml에서 관리하는 것이 더 좋습니다. | ||
| final int RETENTION_DAYS = 7; | ||
| LocalDateTime cutoffDate = LocalDateTime.now().minusDays(RETENTION_DAYS); | ||
| log.info("Starting cleanup of friend watering logs older than {} days...", RETENTION_DAYS); | ||
| int deletedCount = friendWateringLogRepository.deleteByWateredAtBefore(cutoffDate); | ||
| log.info("Finished cleanup. Deleted {} old friend watering logs.", deletedCount); | ||
| } | ||
|
Comment on lines
+242
to
+252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain정기 로그 정리: 리포지토리 메서드 시그니처/인덱스/설정 외부화 확인
리포지토리/인덱스/프로퍼티 점검 스크립트: 설정 외부화 예시: @Value("${watering-log.retention-days:7}")
private int retentionDays;🏁 Script executed: #!/bin/bash
# 메서드 선언 확인
rg -nP "interface\s+FriendWateringLogRepository" -A200 | rg -n "deleteByWateredAtBefore"
# @Modifying 사용 여부
rg -n "@Modifying" -C2
# watered_at 컬럼 인덱스 힌트 (DDL/엔티티)
rg -n "watered_at|wateredAt|@Index|@Table" -C2
# 설정 파일에서 보존일 외부화 여부
fd -a "application*.yml" | xargs -I{} sh -c 'echo "--- {} ---"; rg -n "watering-log-retention|retention|cleanup" {}'Length of output: 10651 GardenService.cleanupOldWateringLogs() 개선 필요 사항 확인 아래 사항들을 반영해 주세요:
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,15 +4,14 @@ | |
| import com.example.cp_main_be.domain.member.user.domain.User; | ||
| import jakarta.persistence.*; | ||
| import java.time.LocalDateTime; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.*; | ||
| import org.springframework.data.annotation.CreatedDate; | ||
| import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Builder | ||
| @AllArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
|
Comment on lines
+13
to
15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 중복 @builder로 인한 Lombok 생성 코드 충돌 클래스 레벨과 생성자 레벨에 동시에 @Getter
-@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
...
- @Builder
public FriendWateringLog(User waterGiver, Garden wateredGarden) {
this.waterGiver = waterGiver;
this.wateredGarden = wateredGarden;
}필요 시 클래스 레벨에만 Also applies to: 36-40 🤖 Prompt for AI Agents |
||
| @EntityListeners(AuditingEntityListener.class) | ||
| @Table(name = "friend_watering_log") | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,11 +5,23 @@ | |||||||||||||
| import com.example.cp_main_be.domain.member.user.domain.User; | ||||||||||||||
| import java.time.LocalDateTime; | ||||||||||||||
| import org.springframework.data.jpa.repository.JpaRepository; | ||||||||||||||
| import org.springframework.data.jpa.repository.Modifying; | ||||||||||||||
| import org.springframework.data.jpa.repository.Query; | ||||||||||||||
| import org.springframework.data.repository.query.Param; | ||||||||||||||
|
|
||||||||||||||
| public interface FriendWateringLogRepository extends JpaRepository<FriendWateringLog, Long> { | ||||||||||||||
|
|
||||||||||||||
| int countByWaterGiverAndWateredAtAfter(User waterGiver, LocalDateTime startOfDay); | ||||||||||||||
|
|
||||||||||||||
| boolean existsByWaterGiverAndWateredGardenAndWateredAtAfter( | ||||||||||||||
| User waterGiver, Garden wateredGarden, LocalDateTime startOfDay); | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 지정된 날짜 이전의 모든 물주기 로그를 삭제합니다. (벌크 삭제) | ||||||||||||||
| * | ||||||||||||||
| * @return 삭제된 레코드의 수 | ||||||||||||||
| */ | ||||||||||||||
| @Modifying | ||||||||||||||
| @Query("DELETE FROM FriendWateringLog fwl WHERE fwl.wateredAt < :cutoffDate") | ||||||||||||||
| int deleteByWateredAtBefore(@Param("cutoffDate") LocalDateTime cutoffDate); | ||||||||||||||
|
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 벌크 삭제 후 영속성 컨텍스트 정합성 보장 JPQL 벌크 연산은 1차 캐시를 무시합니다. 이후 동일 트랜잭션 내에서 엔티티 상태 불일치가 생길 수 있으므로 - @Modifying
+ @Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM FriendWateringLog fwl WHERE fwl.wateredAt < :cutoffDate")
int deleteByWateredAtBefore(@Param("cutoffDate") LocalDateTime cutoffDate);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -97,4 +97,8 @@ public void addComment(Comment comment) { | |||||||||||||||||||||||||||||||||
| this.comments.add(comment); | ||||||||||||||||||||||||||||||||||
| comment.setDiary(this); // Comment 엔티티에 setDiary 메서드가 있다고 가정 | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public void setDiaryImage(DiaryImage diaryImage) { | ||||||||||||||||||||||||||||||||||
| this.diaryImage = diaryImage; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainsetDiaryImage는 양방향 일관성을 보장하지 않음 → 가시성 축소 또는 내부 전용화 권장
두 가지 중 하나 선택: 옵션 A) 내부 전용화(패키지 프라이빗) - public void setDiaryImage(DiaryImage diaryImage) {
+ void setDiaryImage(DiaryImage diaryImage) {
this.diaryImage = diaryImage;
}옵션 B) 양방향 일관성 유지 public void setDiaryImage(DiaryImage diaryImage) {
- this.diaryImage = diaryImage;
+ DiaryImage prev = this.diaryImage;
+ this.diaryImage = diaryImage;
+ if (prev != null && prev != diaryImage) {
+ prev.setDiary(null);
+ }
+ if (diaryImage != null) {
+ diaryImage.setDiary(this);
+ }
}해당 setter 호출처를 점검해 외부 사용 여부를 확인해 주세요. 🏁 Script executed: #!/bin/bash
rg -nP '\bsetDiaryImage\s*\(' --type=java -C2Length of output: 1098 setDiaryImage 메서드에 양방향 연관관계 일관성 추가 필요 현 아래와 같이 setter 내부에서
로직을 추가해주세요. public void setDiaryImage(DiaryImage diaryImage) {
- this.diaryImage = diaryImage;
+ // 1) 이전 연결 해제
+ DiaryImage prev = this.diaryImage;
+ if (prev != null && prev != diaryImage) {
+ prev.setDiary(null);
+ }
+ // 2) 필드 교체
+ this.diaryImage = diaryImage;
+ // 3) 새 연결 설정
+ if (diaryImage != null) {
+ diaryImage.setDiary(this);
+ }
}— 또는 — 서비스 레이어에서 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,4 +25,6 @@ public class CreateDiaryRequest { | |
| // 공개 여부는 선택사항으로, 값을 보내지 않으면 엔티티의 기본값(true)을 따릅니다. | ||
| @NotNull(message = "공개 여부는 필수값입니다. (true/false)") | ||
| private Boolean isPublic; | ||
|
|
||
| private Long imageId; // 있으면 받고 없으면 안받음 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion imageId 유효성 보강 + imageUrl과의 상호배타(XOR) 검증이 필요합니다
적용 예시: - private Long imageId; // 있으면 받고 없으면 안받음
+ @Positive(message = "imageId는 양수여야 합니다.")
+ private Long imageId; // 있으면 받고 없으면 안받음추가로, 클래스 수준의 단순 Bean Validation도 권장합니다(선택): // 추가 import
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Positive;
// 클래스 내부에 추가
@AssertTrue(message = "imageUrl과 imageId는 동시에 보낼 수 없습니다.")
public boolean isImageReferenceValid() {
return imageUrl == null || imageId == null; // 둘 다 null 허용, 동시 지정 금지
}필요 import:
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,4 +23,6 @@ public class UpdateDiaryRequest { | |
|
|
||
| @NotNull(message = "공개 여부는 필수값입니다. (true/false)") | ||
| private Boolean isPublic; | ||
|
|
||
| private Long imageId; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion imageId에 양수 제약 추가 및 imageUrl과의 동시 입력 방지 권장 Update에서도 동일하게 유효성 보강이 필요합니다. 적용 예시: - private Long imageId;
+ @Positive(message = "imageId는 양수여야 합니다.")
+ private Long imageId;추가(선택): Create와 동일한 XOR 검증을 넣어 일관성 확보 import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Positive;
@AssertTrue(message = "imageUrl과 imageId는 동시에 보낼 수 없습니다.")
public boolean isImageReferenceValid() {
return imageUrl == null || imageId == null;
}🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
매직 넘버(masterId + 2) 사용은 즉시 제거해야 합니다
masterId에 2를 가산하는 로직은 데이터 정합성을 심각하게 훼손할 수 있는 크리티컬 이슈입니다. FE/DB 간 ID 매핑 문제는 계약을 맞춰 해결해야 하며, 임시 변환은 금지해야 합니다.
📝 Committable suggestion
🤖 Prompt for AI Agents