Skip to content

feat: allow guests one-time sleep jetlag trial without login - #18

Merged
hamtorygoals merged 1 commit into
developfrom
feat/guest-trial-once
Jul 10, 2026
Merged

feat: allow guests one-time sleep jetlag trial without login#18
hamtorygoals merged 1 commit into
developfrom
feat/guest-trial-once

Conversation

@hamtorygoals

@hamtorygoals hamtorygoals commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 비회원도 X-Device-Id 헤더로 수면시차 계산을 1회 체험할 수 있도록 POST /api/sleep/jetlag 인증을 선택적으로 변경
  • 게스트 계산 결과는 서버에 저장하지 않고 반환만 함 (resultId=null), 사용 이력만 guest_usages에 기록
  • 같은 기기로 재호출 시 403(GUEST_TRIAL_EXHAUSTED), 기기 식별자 누락 시 400(GUEST_DEVICE_ID_REQUIRED)
  • 회원은 기존과 동일하게 무제한 계산 가능

Test plan

  • ./gradlew build 전체 통과
  • 비회원 1회 성공 → 같은 기기 재호출 403
  • 기기 식별자 없이 비회원 호출 → 400
  • 회원은 같은 기기여도 횟수 제한 없음

Closes #17

Summary by CodeRabbit

  • New Features

    • Added guest access to sleep jetlag calculations using an optional device identifier.
    • Guests can calculate results once per device; repeat attempts are rejected.
    • Guest results include jetlag details and matched city information without a saved result ID.
    • Authenticated members retain unlimited calculations and saved result history.
  • Bug Fixes

    • Added clear errors for missing device identifiers and exhausted guest trials.
  • Tests

    • Added coverage for guest access, trial limits, validation, and member behavior.

@hamtorygoals hamtorygoals linked an issue Jul 10, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The sleep jetlag API now permits unauthenticated requests. Members retain unlimited persisted calculations, while guests provide X-Device-Id for one calculation tracked in guest_usages.

Changes

Guest jetlag trial

Layer / File(s) Summary
API access and error contracts
src/main/java/com/cotato/cokerthon/domain/sleep/controller/SleepJetlagController.java, src/main/java/com/cotato/cokerthon/global/config/SecurityConfig.java, src/main/java/com/cotato/cokerthon/global/exception/ErrorCode.java
The endpoint accepts optional device IDs, permits unauthenticated POST requests, and documents guest-specific 400 and 403 errors.
Guest usage and response model
src/main/java/com/cotato/cokerthon/domain/sleep/entity/GuestUsage.java, src/main/java/com/cotato/cokerthon/domain/sleep/repository/GuestUsageRepository.java, src/main/java/com/cotato/cokerthon/domain/sleep/dto/response/SleepJetlagResultResponse.java
Guest device usage is persisted separately, and guest responses use a nullable result ID without storing calculation results.
Calculation branching and validation
src/main/java/com/cotato/cokerthon/domain/sleep/service/SleepJetlagService.java, src/test/java/com/cotato/cokerthon/domain/sleep/SleepJetlagIntegrationTest.java
The service separates member and guest flows, limits each device to one guest calculation, and integration tests cover successful, repeated, missing-device, and member requests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SleepJetlagController
  participant SleepJetlagService
  participant GuestUsageRepository
  participant SleepJetlagResultResponse

  Client->>SleepJetlagController: POST /api/sleep/jetlag with X-Device-Id
  SleepJetlagController->>SleepJetlagService: calculate(memberId, deviceId, request)
  SleepJetlagService->>GuestUsageRepository: existsByDeviceId(deviceId)
  GuestUsageRepository-->>SleepJetlagService: usage status
  SleepJetlagService->>GuestUsageRepository: save GuestUsage
  SleepJetlagService->>SleepJetlagResultResponse: guest calculation data
  SleepJetlagResultResponse-->>Client: guest response with null resultId
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: enabling guest sleep jetlag trials without login.
Linked Issues check ✅ Passed The changes implement guest/member branching, device-based guest limits, guest result nullability, error codes, and tests required by #17.
Out of Scope Changes check ✅ Passed The added entity, repository, error codes, security rule, and tests all support the guest trial feature and stay in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/guest-trial-once

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

@hamtorygoals
hamtorygoals merged commit d8b289e into develop Jul 10, 2026
4 checks passed

@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: 2

🧹 Nitpick comments (1)
src/main/java/com/cotato/cokerthon/domain/sleep/repository/GuestUsageRepository.java (1)

1-9: 🧹 Nitpick | 🔵 Trivial

LGTM!

One longer-term operational note: guest_usages will grow unbounded with no retention/cleanup policy (e.g., a scheduled purge of records older than N days), since device IDs are kept indefinitely just to enforce the one-time trial. Not a blocker for this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/cotato/cokerthon/domain/sleep/repository/GuestUsageRepository.java`
around lines 1 - 9, Plan a retention policy for GuestUsage records by adding
scheduled cleanup of entries older than a configured number of days, using
GuestUsageRepository with an appropriate date-based delete query and a scheduled
service method; keep the retention period configurable and document the policy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/cotato/cokerthon/domain/sleep/service/SleepJetlagService.java`:
- Around line 88-90: Validate the client-provided deviceId in SleepJetlagService
before creating or persisting GuestUsage: retain the null/blank check and reject
values exceeding the GuestUsage.deviceId column limit of 100 characters with an
appropriate controlled BusinessException/ErrorCode, preventing database
constraint failures.
- Around line 91-102: Prevent concurrent guest requests from surfacing a
database constraint error: in the service method containing existsByDeviceId and
GuestUsage.create, flush the repository immediately after save so the
unique-constraint violation occurs during request handling, then catch and
translate that DataIntegrityViolationException to
BusinessException(ErrorCode.GUEST_TRIAL_EXHAUSTED), preserving the intended
response for the losing request.

---

Nitpick comments:
In
`@src/main/java/com/cotato/cokerthon/domain/sleep/repository/GuestUsageRepository.java`:
- Around line 1-9: Plan a retention policy for GuestUsage records by adding
scheduled cleanup of entries older than a configured number of days, using
GuestUsageRepository with an appropriate date-based delete query and a scheduled
service method; keep the retention period configurable and document the policy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df40b6ee-903f-4085-9536-eb259ff81308

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5225f and 4815e49.

📒 Files selected for processing (8)
  • src/main/java/com/cotato/cokerthon/domain/sleep/controller/SleepJetlagController.java
  • src/main/java/com/cotato/cokerthon/domain/sleep/dto/response/SleepJetlagResultResponse.java
  • src/main/java/com/cotato/cokerthon/domain/sleep/entity/GuestUsage.java
  • src/main/java/com/cotato/cokerthon/domain/sleep/repository/GuestUsageRepository.java
  • src/main/java/com/cotato/cokerthon/domain/sleep/service/SleepJetlagService.java
  • src/main/java/com/cotato/cokerthon/global/config/SecurityConfig.java
  • src/main/java/com/cotato/cokerthon/global/exception/ErrorCode.java
  • src/test/java/com/cotato/cokerthon/domain/sleep/SleepJetlagIntegrationTest.java

Comment on lines +88 to +90
if (deviceId == null || deviceId.isBlank()) {
throw new BusinessException(ErrorCode.GUEST_DEVICE_ID_REQUIRED);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

No length/format validation on deviceId before it hits the DB column constraint.

deviceId comes straight from a client-controlled header (X-Device-Id) with only a null/blank check. GuestUsage.deviceId is length = 100; an oversized value will fail at insert time with an unhandled exception (500) instead of a controlled validation error.

 		if (deviceId == null || deviceId.isBlank()) {
 			throw new BusinessException(ErrorCode.GUEST_DEVICE_ID_REQUIRED);
 		}
+		if (deviceId.length() > 100) {
+			throw new BusinessException(ErrorCode.GUEST_DEVICE_ID_REQUIRED);
+		}
📝 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 (deviceId == null || deviceId.isBlank()) {
throw new BusinessException(ErrorCode.GUEST_DEVICE_ID_REQUIRED);
}
if (deviceId == null || deviceId.isBlank()) {
throw new BusinessException(ErrorCode.GUEST_DEVICE_ID_REQUIRED);
}
if (deviceId.length() > 100) {
throw new BusinessException(ErrorCode.GUEST_DEVICE_ID_REQUIRED);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/cotato/cokerthon/domain/sleep/service/SleepJetlagService.java`
around lines 88 - 90, Validate the client-provided deviceId in
SleepJetlagService before creating or persisting GuestUsage: retain the
null/blank check and reject values exceeding the GuestUsage.deviceId column
limit of 100 characters with an appropriate controlled
BusinessException/ErrorCode, preventing database constraint failures.

Comment on lines +91 to +102
if (guestUsageRepository.existsByDeviceId(deviceId)) {
throw new BusinessException(ErrorCode.GUEST_TRIAL_EXHAUSTED);
}

MidSleep currentSleep = midSleepOf(request.currentBedtime(), request.currentWaketime());
MidSleep targetSleep = midSleepOf(request.targetBedtime(), request.targetWaketime());

Jetlag jetlag = calculateJetlag(currentSleep.midTime(), targetSleep.midTime());
City matchedCity = findMatchedCity(jetlag.direction(), jetlag.minutes());
City seoul = getSeoul();

guestUsageRepository.save(GuestUsage.create(deviceId));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Race condition: concurrent requests with the same deviceId can both pass the exhaustion check.

existsByDeviceId (line 91) and save() (line 102) run in the same transaction, and save() only queues the insert — it doesn't flush/hit the unique constraint until commit. Two concurrent guest calls for the same device can both pass the exists check and both proceed to compute a result; at commit, the second insert throws DataIntegrityViolationException instead of the intended 403 GUEST_TRIAL_EXHAUSTED, surfacing as an unhandled 500.

🔒 Proposed fix: flush immediately and translate the constraint violation
-		guestUsageRepository.save(GuestUsage.create(deviceId));
+		try {
+			guestUsageRepository.saveAndFlush(GuestUsage.create(deviceId));
+		} catch (DataIntegrityViolationException e) {
+			throw new BusinessException(ErrorCode.GUEST_TRIAL_EXHAUSTED);
+		}
📝 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 (guestUsageRepository.existsByDeviceId(deviceId)) {
throw new BusinessException(ErrorCode.GUEST_TRIAL_EXHAUSTED);
}
MidSleep currentSleep = midSleepOf(request.currentBedtime(), request.currentWaketime());
MidSleep targetSleep = midSleepOf(request.targetBedtime(), request.targetWaketime());
Jetlag jetlag = calculateJetlag(currentSleep.midTime(), targetSleep.midTime());
City matchedCity = findMatchedCity(jetlag.direction(), jetlag.minutes());
City seoul = getSeoul();
guestUsageRepository.save(GuestUsage.create(deviceId));
if (guestUsageRepository.existsByDeviceId(deviceId)) {
throw new BusinessException(ErrorCode.GUEST_TRIAL_EXHAUSTED);
}
MidSleep currentSleep = midSleepOf(request.currentBedtime(), request.currentWaketime());
MidSleep targetSleep = midSleepOf(request.targetBedtime(), request.targetWaketime());
Jetlag jetlag = calculateJetlag(currentSleep.midTime(), targetSleep.midTime());
City matchedCity = findMatchedCity(jetlag.direction(), jetlag.minutes());
City seoul = getSeoul();
try {
guestUsageRepository.saveAndFlush(GuestUsage.create(deviceId));
} catch (DataIntegrityViolationException e) {
throw new BusinessException(ErrorCode.GUEST_TRIAL_EXHAUSTED);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/cotato/cokerthon/domain/sleep/service/SleepJetlagService.java`
around lines 91 - 102, Prevent concurrent guest requests from surfacing a
database constraint error: in the service method containing existsByDeviceId and
GuestUsage.create, flush the repository immediately after save so the
unique-constraint violation occurs during request handling, then catch and
translate that DataIntegrityViolationException to
BusinessException(ErrorCode.GUEST_TRIAL_EXHAUSTED), preserving the intended
response for the losing request.

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.

비회원 수면시차 계산 1회 체험 기능

1 participant