Skip to content

Feat/water sunlight#66

Merged
c5ln merged 4 commits into
developfrom
feat/water-sunlight
Aug 24, 2025
Merged

Feat/water sunlight#66
c5ln merged 4 commits into
developfrom
feat/water-sunlight

Conversation

@xoruddl

@xoruddl xoruddl commented Aug 24, 2025

Copy link
Copy Markdown
Member

📝 개요
이번 PR의 핵심 내용을 한 줄로 요약해 주세요.

💻 작업 내용
이번 PR에서 작업한 내용을 상세히 설명해 주세요.

작업 내용 1
작업 내용 2
...

✅ PR 체크리스트
PR을 보내기 전에 아래 체크리스트를 확인해 주세요.

커밋 메시지는 포맷에 맞게 작성했나요?
스스로 코드를 다시 한번 검토했나요?
관련 이슈를 연결했나요?
빌드 및 테스트가 로컬에서 성공했나요?

🔗 관련 이슈
이번 PR과 관련된 이슈 번호를 기재해 주세요.

스크린샷 (선택)
UI 변경 사항이 있다면 스크린샷을 첨부해 주세요.

Summary by CodeRabbit

  • 새 기능
    • 내 정원과 친구 정원 물주기를 별도 기능으로 제공합니다(각각 별도 동작으로 기록됨).
    • 친구 물주기 제한: 하루 총 3회, 동일 정원은 하루 1회만 가능합니다.
    • 내 정원 물주기에는 8시간 쿨타임이 적용됩니다.
    • 물주기 시점을 영구 기록하여 진행 상황과 보상이 정확히 반영됩니다(친구 물주기 시 포인트 지급).
    • 일일 초기화 기준을 한국(Asia/Seoul) 시간대의 정오 기준으로 조정했습니다.

@coderabbitai

coderabbitai Bot commented Aug 24, 2025

Copy link
Copy Markdown

Walkthrough

소유자/친구 물주기 흐름을 분리하고 시간·일자 제약과 로그를 도입했습니다. Garden에 소유자/친구 물주기 타임스탬프를 추가하고, 친구 물주기 전용 로그 엔티티 및 레포지토리를 추가하여 일별·정원별 중복을 검사합니다. 컨트롤러는 단일 물주기 엔드포인트를 두 개로 분리했습니다.

Changes

Cohort / File(s) Change summary
도메인: 정원 타임스탬프 추가
src/main/java/.../garden/garden/domain/Garden.java
마지막 물주기 시각 필드 2개 추가(last_watered_by_owner_at, last_watered_by_friend_at) 및 기록 메서드 2개 추가(recordOwnerWateringTime(), recordFriendWateringTime()).
프레젠테이션: 엔드포인트 분리
src/main/java/.../garden/garden/presentation/GardenController.java
기존 POST /{gardenId}/water 제거. POST /{gardenId}/mywater(내 정원 물주기), POST /{gardenId}/friendwater(친구 정원 물주기) 추가. 둘 다 서비스 호출 후 200 응답.
서비스: 친구 물주기 로직 및 제약
src/main/java/.../garden/garden/service/GardenService.java
소유자 물주기: 8시간 쿨다운 검사 및 recordOwnerWateringTime() 호출. 친구 물주기: 사용자 검증, 일별 최대 3회 제한, 정원당 1회/일 제한(아시아/서울 정오 기준 경계), 포인트/카운트 증가 및 FriendWateringLog 저장. 이벤트 방식 제거, 로그 기반 검사 도입.
도메인: 친구 물주기 로그 엔티티
src/main/java/.../garden/wateringlog/domain/FriendWateringLog.java
friend_watering_log 엔티티 추가(waterGiver: User, wateredGarden: Garden, wateredAt: @CreatedDate).
레포지토리: 친구 물주기 로그 조회
src/main/java/.../garden/wateringlog/domain/repository/FriendWateringLogRepository.java
JPA 레포지토리 추가. countByWaterGiverAndWateredAtAfter(...), existsByWaterGiverAndWateredGardenAndWateredAtAfter(...) 메서드 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as 클라이언트
  participant Ctrl as GardenController
  participant Svc as GardenService
  participant UR as UserRepository
  participant FLR as FriendWateringLogRepository
  participant G as Garden

  rect rgba(230,245,255,0.6)
  note over C,Ctrl: 내 정원 물주기 (POST /{gardenId}/mywater)
  C->>Ctrl: POST /{gardenId}/mywater (userId, gardenId)
  Ctrl->>Svc: waterGarden(userId, gardenId)
  Svc->>G: 조회 및 lastWateredByOwnerAt 확인
  alt 8시간 이내
    Svc-->>Ctrl: 오류 반환
  else 허용
    Svc->>G: recordOwnerWateringTime()
    Svc-->>Ctrl: 200 OK
  end
  end

  rect rgba(240,255,240,0.6)
  note over C,Ctrl: 친구 정원 물주기 (POST /{gardenId}/friendwater)
  C->>Ctrl: POST /{gardenId}/friendwater (userId, gardenId)
  Ctrl->>Svc: waterGarden(userId, gardenId)
  Svc->>UR: 사용자 검증/조회
  Svc->>FLR: countByWaterGiverAndWateredAtAfter(user, startOfDay)
  alt 일일 3회 초과
    Svc-->>Ctrl: 오류 반환
  else 허용
    Svc->>FLR: existsByWaterGiverAndWateredGardenAndWateredAtAfter(user, garden, startOfDay)
    alt 이미 해당 정원에 물줌
      Svc-->>Ctrl: 오류 반환
    else 최초
      Svc->>G: recordFriendWateringTime()
      Svc->>FLR: save(FriendWateringLog)
      Svc-->>Ctrl: 200 OK
    end
  end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • lejuho
  • c5ln

Poem

물 한 방울 톡, 로그에 찍힌 시간표⏱️
내 물줄기 따로, 친구 물줄기 따로💧
토끼는 폴짝—카운트 세며 기뻐하고🐰
정오 지나면 새 하루, 다시 촉촉하길.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 08aaa82 and bf248b3.

📒 Files selected for processing (1)
  • src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/water-sunlight

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

@github-actions

Copy link
Copy Markdown
Contributor

🚨 PR 본문이 비어있습니다!

아래 템플릿을 복사하여 PR 내용을 작성해주세요.


📝 개요

이번 PR의 핵심 내용을 한 줄로 요약해 주세요.


💻 작업 내용

이번 PR에서 작업한 내용을 상세히 설명해 주세요.

  • 작업 내용 1
  • 작업 내용 2
  • ...

✅ PR 체크리스트

PR을 보내기 전에 아래 체크리스트를 확인해 주세요.

  • 커밋 메시지는 포맷에 맞게 작성했나요?
  • 스스로 코드를 다시 한번 검토했나요?
  • 관련 이슈를 연결했나요?
  • 빌드 및 테스트가 로컬에서 성공했나요?

🔗 관련 이슈

이번 PR과 관련된 이슈 번호를 기재해 주세요.
예: Closes #123


스크린샷 (선택)

UI 변경 사항이 있다면 스크린샷을 첨부해 주세요.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/example/cp_main_be/domain/garden/garden/domain/Garden.java (1)

17-17: Lombok @builder 중복 사용으로 인한 컴파일 충돌 위험

클래스 레벨(@builder)과 특정 생성자(@builder)가 동시에 선언되어 있어 동일한 builder() 팩토리 메서드가 중복 생성될 수 있습니다. 이는 Lombok 처리 단계에서 컴파일 에러로 이어집니다.

한쪽만 유지하거나 메서드명을 분리하세요.

-@Builder
 public class Garden {
@@
-  @Builder
   public Garden(User user, Integer slotNumber, GardenBackground gardenBackground, Avatar avatar) {

또는

-  @Builder
+  @Builder(builderMethodName = "createGarden")
   public Garden(User user, Integer slotNumber, GardenBackground gardenBackground, Avatar avatar) {

Also applies to: 63-64

🧹 Nitpick comments (9)
src/main/java/com/example/cp_main_be/domain/garden/garden/domain/Garden.java (1)

81-87: recordFriendWateringTime()가 서비스 로직에서 사용되지 않음

친구 물주기 시각을 기록하는 public 메서드를 추가하셨지만, GardenService의 친구 물주기 경로에서 호출되지 않습니다. 도메인 모델에만 시각이 남지 않아 API 응답/관리 화면에서 “최근 친구 물주기 시각”을 노출하려 할 때 일관성이 깨질 수 있습니다.

아래처럼 서비스에서 호출을 추가해 주세요(서비스 파일 제안 diff도 함께 남깁니다).

// GardenService.java - 친구 물주기 성공 분기 직후
-      userService.addExperience(actorId, WATERING_POINTS);
-      garden.increaseWaterCount();
+      userService.addExperience(actorId, WATERING_POINTS);
+      garden.increaseWaterCount();
+      garden.recordFriendWateringTime();
src/main/java/com/example/cp_main_be/domain/garden/garden/presentation/GardenController.java (3)

33-39: 엔드포인트 의미 구분은 되었으나 서비스 호출이 동일 — 의도 명시성/검증 보강 제안

/mywater가 본인 정원만 대상으로 동작해야 한다면, 컨트롤러 레벨에서 소유자 일치 검증을 선제적으로 수행하거나, 서비스에 waterMyGarden(...) 같은 명시적 메서드를 두어 의도를 분리하는 편이 가독성과 오용 방지에 유리합니다. 현재는 서비스 내부 분기로 동작은 맞지만, API 의미와 구현이 강하게 결합되지 않아 유지보수성이 떨어집니다.

예시:

- gardenService.waterGarden(userId, gardenId);
+ gardenService.waterMyGarden(userId, gardenId); // 내부에서 소유자 검증/쿨타임 처리

41-47: 친구 물주기 엔드포인트도 동일 서비스 호출 — 오용 방지 검증 보강

/friendwater 호출자가 자신의 정원을 대상으로 호출하는 경우에도 현재는 “주인 경로”로 분기되어 진행됩니다(의도상 문제는 없으나 API 의미와 달라질 수 있음). 컨트롤러 레벨에서 “대상 정원이 본인 소유가 아닐 것”을 단언(400/409)하면 API 사용성이 명확해집니다.

원치 않으시면 최소한 스웨거 설명에 “본인 정원을 대상으로 호출 시 내부적으로 ‘내 물주기’로 처리됨”을 명시해 주세요.


33-39: 204 No Content 고려

성공 시 ApiResponse.success(null)(200)을 반환하고 있는데, 바디가 항상 null이면 204 응답이 의미상 더 적절합니다. 물론 기존 클라이언트가 ApiResponse 래퍼를 기대한다면 그대로 유지하세요.

Also applies to: 41-47

src/main/java/com/example/cp_main_be/domain/garden/wateringlog/domain/repository/FriendWateringLogRepository.java (1)

11-15: 일자 경계 상·하한 모두를 파라미터화하는 쿼리 시그니처 고려

현재는 “이후(after)” 조건만 있으나, 장기적으로 리포트/검증에 “구간” 기반 조회가 필요해질 수 있습니다. 또한 상한을 명시하면 옵티마이저가 인덱스를 더 잘 활용할 여지가 있습니다.

예시(추가 메서드):

int countByWaterGiverAndWateredAtBetween(User waterGiver, LocalDateTime start, LocalDateTime end);
boolean existsByWaterGiverAndWateredGardenAndWateredAtBetween(User waterGiver, Garden garden, LocalDateTime start, LocalDateTime end);
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (4)

97-105: 친구 물주기 시 Garden 도메인의 최근 시각 동기화 누락

도메인에 recordFriendWateringTime()가 있으므로 상태 일관성을 위해 호출해 두는 편이 좋습니다(별도 UI/응답 노출 대비).

       FriendWateringLog log =
           FriendWateringLog.builder().waterGiver(actor).wateredGarden(garden).build();
       friendWateringLogRepository.save(log);
+      garden.recordFriendWateringTime();

34-35: 매직 넘버/하드코딩된 정책 값의 외부화

MAX_FRIEND_WATERING_PER_DAY = 3는 운영 중 조정 가능성이 높은 값입니다. @Value 또는 설정 프로퍼티로 외부화하는 것을 권장합니다.

Also applies to: 41-41


14-16: 예외 타입 일관성

서비스 전반에 IllegalArgumentException/IllegalStateException과 커스텀 예외(CustomApiException)가 혼재되어 있습니다. 글로벌 핸들러 정책에 맞춰 일관된 예외/에러코드로 교체하면 클라이언트 처리와 모니터링이 쉬워집니다.

Also applies to: 52-58, 108-122


36-40: 미사용 필드 정리

ApplicationEventPublisher eventPublisher가 더 이상 사용되지 않으면 제거를 고려하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 11cf52a and 08aaa82.

📒 Files selected for processing (5)
  • src/main/java/com/example/cp_main_be/domain/garden/garden/domain/Garden.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/garden/presentation/GardenController.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (4 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/wateringlog/domain/FriendWateringLog.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/wateringlog/domain/repository/FriendWateringLogRepository.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (1)
src/main/java/com/example/cp_main_be/domain/member/user/service/UserService.java (1)
  • Service (20-113)
🔇 Additional comments (1)
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (1)

172-188: “정오(12:00) 기준 일자 경계”가 요구사항인지 확인 필요

일반적으로 일별 제한은 자정(00:00) 기준이 많습니다. 정오 기준이 의도라면 OK이며, 그렇지 않다면 오해 소지가 큽니다. 스웨거/에러 메시지에도 기준 시각을 명시해 주세요.

요구사항이 “자정 기준”이라면 아래처럼 바꿉니다.

-  private LocalDateTime getStartOfCurrentWateringDay() {
-    LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
-    LocalDateTime todayNoon = now.toLocalDate().atTime(12, 0);
-    if (now.isBefore(todayNoon)) {
-      return todayNoon.minusDays(1);
-    } else {
-      return todayNoon;
-    }
-  }
+  private LocalDateTime getStartOfCurrentWateringDay() {
+    LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
+    return now.toLocalDate().atStartOfDay(); // 00:00 KST
+  }

Comment on lines +51 to +56
@Column(name = "last_watered_by_owner_at")
private LocalDateTime lastWateredByOwnerAt;

@Column(name = "last_watered_by_friend_at")
private LocalDateTime lastWateredByFriendAt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

시간대 불일치로 인한 쿨타임/일자 계산 오류 가능성(LocalDateTime 사용)

lastWateredByOwnerAt, lastWateredByFriendAt를 LocalDateTime(서버 기본 시간대)로 저장하고, 서비스 레이어에서는 Asia/Seoul 기준으로 일자 경계를 계산합니다. 서버 TZ가 KST가 아닐 경우, 8시간 쿨타임/일일 제한 로직과 저장 시점의 시간대가 어긋나 오판정이 발생할 수 있습니다.

아래처럼 KST로 고정 저장(최소 조치) 또는 애그리게이트에서 Instant/OffsetDateTime로 일관 저장(권장)을 고려해주세요.

@@
-  public void recordOwnerWateringTime() {
-    this.lastWateredByOwnerAt = LocalDateTime.now();
-  }
+  public void recordOwnerWateringTime() {
+    this.lastWateredByOwnerAt = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
+  }
@@
-  public void recordFriendWateringTime() {
-    this.lastWateredByFriendAt = LocalDateTime.now();
-  }
+  public void recordFriendWateringTime() {
+    this.lastWateredByFriendAt = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
+  }

또는(권장) 필드를 Instant로 변경하고 서비스에서 필요 시점에 atZone(ZoneId.of("Asia/Seoul"))로 변환하는 방식으로 전역 일관성을 확보해 주세요.

📝 Committable suggestion

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

Suggested change
@Column(name = "last_watered_by_owner_at")
private LocalDateTime lastWateredByOwnerAt;
@Column(name = "last_watered_by_friend_at")
private LocalDateTime lastWateredByFriendAt;
import java.time.LocalDateTime;
import java.time.ZoneId;
public class Garden {
// ...
public void recordOwnerWateringTime() {
this.lastWateredByOwnerAt = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
}
public void recordFriendWateringTime() {
this.lastWateredByFriendAt = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
}
// ...
}
🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/garden/garden/domain/Garden.java
around lines 51–56, the two fields use LocalDateTime which can cause timezone
mismatches; change the fields to Instant (or OffsetDateTime) and update JPA
mappings to persist a timezone-aware column (e.g., TIMESTAMP WITH TIME ZONE) so
timestamps are stored in a global instant; update constructors/getters/setters,
any JSON serialization config, and service-layer code to convert to Asia/Seoul
via instant.atZone(ZoneId.of("Asia/Seoul")) when computing cooldowns/daily
boundaries; ensure DB schema and migration reflect the new column type and
adjust any tests that assert on stored timestamps.

Comment on lines +35 to +37
public ResponseEntity<ApiResponse<Void>> waterMyGarden(
@AuthenticationPrincipal Long userId, @PathVariable Long gardenId) {
gardenService.waterGarden(userId, gardenId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

@AuthenticationPrincipal Long 바인딩 확인 필요

프로젝트 전반에 Long 타입 Principal을 주입하는 설정이 일관적으로 적용돼 있어야 합니다. DTO/도메인 User 주입 방식과 혼용되면 SecurityContext 설정에 따라 NPE/캐스팅 오류가 날 수 있습니다.

다음 스크립트로 컨트롤러 전반의 사용 현황을 확인해 주세요. 일관성이 깨진 곳이 있으면 통일하는 것을 권장합니다.

Also applies to: 43-46


🏁 Script executed:

#!/bin/bash
# 컨트롤러에서 @AuthenticationPrincipal 사용 패턴 확인
rg -n --type=java '@AuthenticationPrincipal' -C2

Length of output: 29284


@AuthenticationPrincipal 바인딩 타입 일관화 필요

프로젝트 전반에서 @AuthenticationPrincipal로 주입하는 Principal 타입이 혼용되어 있어 SecurityContext 설정에 따라 NPE나 캐스팅 오류가 발생할 수 있습니다. 다음 위치를 확인하고 하나의 타입으로 통일해주세요.

• 주입 타입별 사용 현황

  • @AuthenticationPrincipal User user (대다수 컨트롤러)
  • @AuthenticationPrincipal Long userId
    • src/main/java/com/example/cp_main_be/domain/reports/presentation/ReportController.java:27
    • src/main/java/com/example/cp_main_be/domain/garden/garden/presentation/GardenController.java:36, 44, 52
  • @AuthenticationPrincipal UserDetails userDetails
    • src/main/java/com/example/cp_main_be/domain/delivery/presentation/DeliveryController.java:26

권장 조치

  • Principal DTO/도메인(User 또는 UserDetails) 중 하나로 통일
  • ID가 필요한 경우 user.getId() 호출로 추출

Comment on lines +64 to +67
if (garden.getLastWateredByOwnerAt() != null
&& garden.getLastWateredByOwnerAt().plusHours(8).isAfter(LocalDateTime.now())) {
throw new IllegalStateException("아직 물을 줄 수 없습니다. 8시간이 지나야 가능합니다.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

8시간 쿨타임 판단의 시간대 불일치

여기서는 LocalDateTime.now()(서버 TZ)를 사용하지만, 친구 물주기 일자 경계는 Asia/Seoul을 사용합니다. 동일 이벤트(물주기)에 서로 다른 시간대 규칙을 적용하면 경계 시각에서 불일치가 발생할 수 있습니다.

KST 기준으로 통일하거나, 전역적으로 Instant를 사용해 비교하세요.

-      if (garden.getLastWateredByOwnerAt() != null
-          && garden.getLastWateredByOwnerAt().plusHours(8).isAfter(LocalDateTime.now())) {
+      LocalDateTime nowKst = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
+      if (garden.getLastWateredByOwnerAt() != null
+          && garden.getLastWateredByOwnerAt().plusHours(8).isAfter(nowKst)) {

또는 garden.recordOwnerWateringTime()에서 KST로 저장하도록 변경(별도 코멘트 참고)해 두 경로를 일치시키세요.

📝 Committable suggestion

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

Suggested change
if (garden.getLastWateredByOwnerAt() != null
&& garden.getLastWateredByOwnerAt().plusHours(8).isAfter(LocalDateTime.now())) {
throw new IllegalStateException("아직 물을 줄 수 없습니다. 8시간이 지나야 가능합니다.");
}
LocalDateTime nowKst = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
if (garden.getLastWateredByOwnerAt() != null
&& garden.getLastWateredByOwnerAt().plusHours(8).isAfter(nowKst)) {
throw new IllegalStateException("아직 물을 줄 수 없습니다. 8시간이 지나야 가능합니다.");
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java
around lines 64-67, the 8-hour cooldown comparison mixes LocalDateTime.now()
(server default TZ) with timestamps that are intended to be KST, causing
boundary mismatches; update the comparison to use the same time basis as the
rest of the app by either (A) converting both sides to Instant and comparing
Instants (preferred global approach), or (B) use
ZonedDateTime.now(ZoneId.of("Asia/Seoul")) (or
LocalDateTime.now(ZoneId.of("Asia/Seoul")) if stored values are KST) so both the
stored lastWatered time and the current time are in Asia/Seoul; alternatively,
if you choose to change storage, ensure garden.recordOwnerWateringTime() stores
KST and adjust this check to use KST consistently.

Comment on lines +80 to +96
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("이 정원에는 오늘 이미 물을 주었습니다.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

일일 제한 판정의 경합 상태 및 시간대 일관성

  • 동일 사용자가 거의 동시에 두 요청을 보내면, exists/count 체크 사이에 로그가 삽입되어 중복 허용 가능성이 있습니다(포인트 중복 부여 위험).
  • startOfWateringDay는 KST 기준이지만 wateredAt 저장이 서버 TZ이면 경계에서 틀어질 수 있습니다(엔티티 코멘트 참고).

대응 방안:

  • DB 제약(유니크) 또는 “체크+삽입”을 하나의 원자적 연산으로 만들기 위한 잠금. 예) FriendWateringLog 저장 전, 대상 (actor, garden) 구간에 대해 짧은 PESSIMISTIC_WRITE를 잡거나, “KST 일자 버킷” 컬럼에 유니크 인덱스를 둠.
  • 최소한 서비스 레벨에서 실패 시(유니크 위반 등) 예외를 캐치하고 포인트 중복 부여를 롤백하는 보완이 필요합니다.

예시(간단 보완 — 성공 후 기록/포인트 순서 조정과 기록 실패 시 롤백):

-      userService.addExperience(actorId, WATERING_POINTS);
-      garden.increaseWaterCount();
-      FriendWateringLog log = FriendWateringLog.builder().waterGiver(actor).wateredGarden(garden).build();
-      friendWateringLogRepository.save(log);
+      FriendWateringLog log = FriendWateringLog.builder().waterGiver(actor).wateredGarden(garden).build();
+      friendWateringLogRepository.save(log); // 기록 성공 보장 후
+      garden.increaseWaterCount();
+      userService.addExperience(actorId, WATERING_POINTS);

가능하다면 DB 유니크 제약을 병행하는 것이 가장 확실합니다.

Comment on lines +14 to +19
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Table(name = "friend_watering_log")
public class FriendWateringLog {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

조회 패턴에 맞춘 인덱스/제약 추가로 성능·정합성 보강

  • 레포지토리는 countByWaterGiverAndWateredAtAfterexistsByWaterGiverAndWateredGardenAndWateredAtAfter를 자주 수행합니다. 이에 맞춘 복합 인덱스가 없으면 테이블 성장 시 성능 저하가 큽니다.
  • “같은 정원 하루 1회”는 경쟁 상황에서 이중 기록이 날 수 있어(포인트 중복 부여) DB 레벨 제약이 있으면 안전합니다.

인덱스 추가 예시:

-@Table(name = "friend_watering_log")
+@Table(
+  name = "friend_watering_log",
+  indexes = {
+    @Index(name = "idx_fwl_giver_at", columnList = "water_giver_id, watered_at"),
+    @Index(name = "idx_fwl_giver_garden_at", columnList = "water_giver_id, watered_garden_id, watered_at")
+  }
+)

DB 제약(옵션):

  • 가능하면 “KST 기준 일자 버킷” 컬럼(예: watered_day_kst DATE)을 추가해 (water_giver_id, watered_garden_id, watered_day_kst) 유니크 제약을 두면 애플리케이션 레벨 경쟁 상태를 원천 차단할 수 있습니다. 표준 JPA만으로는 표현이 어렵지만, 마이그레이션 스크립트로 처리 가능합니다.

Also applies to: 25-35

🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/garden/wateringlog/domain/FriendWateringLog.java
around lines 14-19 (and also apply same changes for lines 25-35), the entity
lacks DB indexes and a uniqueness constraint required by hotspots: add composite
indexes to support the frequent repository queries (e.g., an index on
(water_giver_id, watered_at) and another on (water_giver_id, watered_garden_id,
watered_at)) via your migration script or JPA @Table(indexes=...) and add a
DB-level uniqueness constraint to prevent duplicate same-garden-per-day records
by introducing a new DATE column (e.g., watered_day_kst) populated by the
application or migration (KST bucket) and create a unique constraint on
(water_giver_id, watered_garden_id, watered_day_kst) in the migration; implement
the schema changes in a migration (Flyway/Liquibase) to add the column, populate
existing rows, create the indexes and the unique constraint, and update the
entity annotations if you mirror indexes/constraints in JPA.

Comment on lines +33 to +35
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime wateredAt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

감사 타임스탬프의 시간대 일관성 확보 필요

@CreatedDate가 서버 기본 시간대를 따르는 경우, 서비스는 Asia/Seoul 기준으로 일자 경계를 계산하므로 일일 제한 판단이 경계 시간대에서 틀어질 수 있습니다.

Auditing의 DateTimeProvider를 KST로 고정하거나, 엔티티에 @PrePersist로 KST 기준 값을 강제 설정하세요(권장은 Instant 저장).

예시(전역 설정):

// @Configuration
@EnableJpaAuditing(dateTimeProviderRef = "seoulDateTimeProvider")
class JpaConfig {
  @Bean
  DateTimeProvider seoulDateTimeProvider() {
    return () -> Optional.of(LocalDateTime.now(ZoneId.of("Asia/Seoul")));
  }
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/garden/wateringlog/domain/FriendWateringLog.java
around lines 33-35, the @CreatedDate LocalDateTime field may use the server
default timezone causing inconsistent daily-boundary calculations; fix by
ensuring KST-consistent timestamps either by configuring a JPA DateTimeProvider
bean that returns LocalDateTime.now(ZoneId.of("Asia/Seoul")) (and register it
via @EnableJpaAuditing(dateTimeProviderRef = "...")) or by replacing the field
with an Instant (recommended) and/or setting the KST value explicitly in an
@PrePersist method that assigns Instant.now() or
LocalDateTime.now(ZoneId.of("Asia/Seoul")) before persist.

@c5ln c5ln merged commit 0e07cf2 into develop Aug 24, 2025
6 checks passed
@c5ln c5ln deleted the feat/water-sunlight branch August 24, 2025 09:29
@coderabbitai coderabbitai Bot mentioned this pull request Sep 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants