Skip to content

Feat/detox-progress - #20

Merged
chaeyylee merged 9 commits into
mainfrom
feat/detox-progress
Jul 10, 2026
Merged

Feat/detox-progress#20
chaeyylee merged 9 commits into
mainfrom
feat/detox-progress

Conversation

@chaeyylee

@chaeyylee chaeyylee commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

📌 개요

  • 미션 인증을 완료한 사용자가 디지털 디톡스 진행 상태와 종료 시간을 조회할 수 있는 API 구현
  • 사용자가 참여한 모든 팀을 기준으로 디톡스 시간이 겹치는 팀원을 조회하며, 자정을 넘기는 디톡스 시간과 시간 구간의 경계 조건 처리

🛠️ 주요 변경 사항

디톡스 진행 상태 조회 API 추가

GET /api/detox/progress

  • X-Device-Id 헤더를 통해 사용자 식별
  • 오늘의 미션 인증 상태 검증
  • SUCCESS 상태의 미션만 진행 화면 조회 허용
  • 미션 로그가 없거나 인증이 완료되지 않은 경우 예외 반환
  • 기존 프로젝트의 Clock을 사용해 서버 현재 시각 계산

디톡스 진행 시간 계산

  • 미션 로그의 targetDate와 사용자 디톡스 시간을 이용해 실제 시작·종료 시각 계산
  • 디톡스 종료 시간이 시작 시간보다 늦으면 같은 날짜에 종료
  • 종료 시간이 시작 시간과 같거나 빠르면 다음 날 종료
  • 다음 조건을 만족할 때 inProgress = true 반환
    • startDateTime <= now < endDateTime
  • 디톡스 진행 중인 경우 종료까지 남은 시간을 초 단위로 반환
  • 시작 전이나 종료 후에는 remainingSeconds = 0 반환
  • 디톡스 종료 시각을 반영한 해제 안내 문구 제공

디톡스 시간이 겹치는 팀원 조회

  • 사용자가 참여 중인 모든 팀의 팀원 조회
  • 본인 제외
  • 여러 팀에 중복으로 참여한 동일 사용자는 한 번만 반환
  • 디톡스 시간이 설정되지 않은 팀원 제외
  • 현재 사용자의 디톡스 시간과 겹치는 팀원만 반환
  • 팀원의 미션 성공 여부와 관계없이 시간 중첩 여부만 사용
  • 닉네임 오름차순으로 정렬
  • 진행 중이 아닌 경우 팀원 목록을 빈 배열로 반환

시간 구간 중첩 처리

다음 조건을 모두 만족하면 두 디톡스 시간이 겹치는 것으로 처리

  • 사용자 시작 시각 < 팀원 종료 시각
  • 팀원 시작 시각 < 사용자 종료 시각

경계 시각만 맞닿는 경우에는 겹치지 않는 것으로 처리

ex.

  • 사용자 22:00 ~ 23:00
  • 팀원 23:00 ~ 24:00
  • 결과: 겹치지 않음

자정을 넘기는 디톡스 시간 지원

  • 23:00 ~ 01:00과 같은 디톡스 구간 지원
  • 종료 시각을 다음 날짜로 계산
  • 모든 팀원의 시작 시각은 현재 미션 로그의 동일한 targetDate에 결합
  • 자정을 넘기는 팀원과의 실제 중첩 구간 계산

팀원 조회 성능 개선

  • 사용자가 참여한 모든 팀의 팀원을 단일 JPQL 쿼리로 조회
  • distinct를 사용해 DB 조회 단계에서 중복 제거
  • 서비스에서도 userId를 기준으로 중복 제거
  • 팀원별 추가 조회가 발생하지 않도록 N+1 문제 방지

📦 응답 정보

진행 상태

  • missionLogId
  • status
  • inProgress
  • startDateTime
  • endDateTime
  • endTime
  • remainingSeconds
  • titleMessage
  • unlockMessage
  • overlappingMemberCount
  • overlappingMembers

겹치는 팀원 정보

  • userId
  • nickname
  • detoxStartTime
  • detoxEndTime
  • overlapStartDateTime
  • overlapEndDateTime

Summary by CodeRabbit

  • 새로운 기능
    • 디톡스 진행 상태를 조회하는 API를 추가했습니다(진행 여부, 남은 시간, 시작·종료 정보 및 상태별 안내 메시지 포함).
    • 함께 진행 중인 팀원의 목록과 겹치는 시간 구간을 함께 제공하도록 했습니다.
  • 오류 처리
    • 미션 인증 완료 전 조회를 차단하며, 관련 오류 코드/응답을 명확히 했습니다.
  • 개선
    • 이미지 업로드 시 생성되는 S3 URL의 리전 처리 및 예외 상세 정보 구성을 개선했습니다.
  • 테스트
    • 디톡스 진행 상태 및 관련 시나리오에 대한 테스트 커버리지를 확장했습니다.

@chaeyylee chaeyylee self-assigned this Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cc02280a-0985-4d52-bd91-f4be0077609f

📥 Commits

Reviewing files that changed from the base of the PR and between bbcb512 and 2cc57a4.

📒 Files selected for processing (2)
  • src/main/java/com/example/hackathon/domain/detox/service/DetoxProgressService.java
  • src/test/java/com/example/hackathon/domain/detox/service/DetoxProgressServiceTest.java

📝 Walkthrough

Walkthrough

디톡스 진행 조회 API와 응답 DTO를 추가하고, 미션 상태·디톡스 기간·남은 시간·겹치는 팀원 정보를 계산하는 서비스를 구현했다. 미션 인증 트랜잭션과 목표 날짜 계산을 분리·통합했으며, S3 리전 기본값과 이미지 콘텐츠 타입 검증을 보강했다.

Changes

디톡스 진행 조회

Layer / File(s) Summary
응답 계약과 API 진입점
src/main/java/com/example/hackathon/domain/detox/dto/*, src/main/java/com/example/hackathon/domain/detox/controller/DetoxProgressController.java
진행 응답 DTO와 GET /api/detox/progress 엔드포인트를 추가했다.
진행 상태와 겹침 계산
src/main/java/com/example/hackathon/domain/detox/service/DetoxProgressService.java, src/main/java/com/example/hackathon/domain/team/repository/TeamMemberRepository.java, src/main/java/com/example/hackathon/global/exception/ErrorCode.java
미션 로그와 디톡스 기간을 검증하고 진행 상태, 남은 시간, 메시지 및 겹치는 팀원 목록을 계산한다.
경계 조건 검증
src/test/java/com/example/hackathon/domain/detox/service/DetoxProgressServiceTest.java
미션 상태, 로그 누락, 시간 경계, 자정 통과 기간, 팀원 중복 및 정렬을 검증한다.

미션 인증 처리

Layer / File(s) Summary
목표 날짜 계산 통합
src/main/java/com/example/hackathon/domain/mission/service/MissionTargetDateResolver.java, src/main/java/com/example/hackathon/domain/mission/service/MissionService.java, src/main/java/com/example/hackathon/domain/mission/service/MissionCertificationService.java
목표 날짜와 데드라인 계산을 전용 리졸버로 통합했다.
인증 트랜잭션 분리
src/main/java/com/example/hackathon/domain/mission/service/MissionCertificationTransactionService.java, src/main/java/com/example/hackathon/domain/mission/service/MissionCertificationService.java, src/main/java/com/example/hackathon/global/exception/BusinessException.java
이미지 인증의 로그 검증, 상태 변경 및 저장을 별도 트랜잭션 서비스로 위임하고 원인 예외 전달을 지원한다.
인증 흐름 테스트 갱신
src/test/java/com/example/hackathon/domain/mission/service/MissionCertificationServiceTest.java
서비스 위임, 저장 시각, 이미지 정리, 실패 처리 및 야간 목표 날짜 동작을 검증한다.

S3 설정 및 이미지 검증

Layer / File(s) Summary
S3 리전 해석
src/main/java/com/example/hackathon/domain/image/config/*, src/main/java/com/example/hackathon/domain/image/service/S3StorageService.java
공백 리전에 기본값을 적용하고 S3 클라이언트와 업로드 URL 생성에 사용한다.
이미지 업로드 검증
src/test/java/com/example/hackathon/domain/image/service/S3StorageServiceTest.java
null 콘텐츠 타입, 리전별 URL, 기본 리전 및 삭제 요청의 버킷과 키를 검증한다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DetoxProgressController
  participant DetoxProgressService
  participant UserRepository
  participant UserMissionLogRepository
  participant TeamMemberRepository

  Client->>DetoxProgressController: GET /api/detox/progress with X-Device-Id
  DetoxProgressController->>DetoxProgressService: getProgress(deviceId)
  DetoxProgressService->>UserRepository: find user by deviceId
  DetoxProgressService->>UserMissionLogRepository: find current or previous mission log
  DetoxProgressService->>TeamMemberRepository: findDistinctTeammatesByUserId(userId)
  DetoxProgressService-->>DetoxProgressController: DetoxProgressResponse
  DetoxProgressController-->>Client: ApiResponse.ok(response)
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive 제목은 detox progress와 관련은 있지만, 변경의 핵심인 새 API와 검증 로직을 구체적으로 드러내지 못합니다. Feat/detox-progress 대신 Add detox progress API처럼 주요 변경을 직접 드러내는 제목으로 바꾸세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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/detox-progress

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

@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.

🧹 Nitpick comments (2)
src/main/java/com/example/hackathon/domain/detox/service/DetoxProgressService.java (1)

81-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

자정교차 판단 로직 중복.

!user.getDetoxEndTime().isAfter(user.getDetoxStartTime())(Line 81)와 DetoxPeriod.ofendTime.isAfter(startTime)(Line 146) 조건이 동일한 "자정을 넘기는지" 여부를 각각 별도로 계산합니다. 두 곳 중 한 곳만 수정되면 로그 선택과 기간 계산이 어긋날 수 있으니 공용 헬퍼로 추출하는 것을 권장합니다.

♻️ 제안 리팩터
+    private static boolean isOvernight(LocalTime startTime, LocalTime endTime) {
+        return !endTime.isAfter(startTime);
+    }
+
     private UserMissionLog findCurrentMissionLog(User user, LocalDate today) {
         return userMissionLogRepository.findByUserIdAndTargetDate(user.getId(), today)
                 .orElseGet(() -> {
-                    if (!user.getDetoxEndTime().isAfter(user.getDetoxStartTime())) {
+                    if (isOvernight(user.getDetoxStartTime(), user.getDetoxEndTime())) {
                         return userMissionLogRepository
                                 .findByUserIdAndTargetDate(user.getId(), today.minusDays(1))
                                 .orElseThrow(() -> new BusinessException(ErrorCode.MISSION_ERROR_404_NOT_FOUND));
                     }
                     throw new BusinessException(ErrorCode.MISSION_ERROR_404_NOT_FOUND);
                 });
     }
         private static DetoxPeriod of(LocalDate targetDate, LocalTime startTime, LocalTime endTime) {
-            LocalDate endDate = endTime.isAfter(startTime) ? targetDate : targetDate.plusDays(1);
+            LocalDate endDate = isOvernight(startTime, endTime) ? targetDate.plusDays(1) : targetDate;
             return new DetoxPeriod(

Also applies to: 146-146

🤖 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/example/hackathon/domain/detox/service/DetoxProgressService.java`
at line 81, 중복된 자정 교차 판단 로직을 공용 헬퍼로 통합하세요. DetoxProgressService의 조건문과
DetoxPeriod.of 내부의 endTime.isAfter(startTime) 판단이 동일한 헬퍼를 사용하도록 수정해 로그 선택과 기간
계산이 항상 일치하게 하세요.
src/test/java/com/example/hackathon/domain/detox/service/DetoxProgressServiceTest.java (1)

35-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

detoxStartTime/detoxEndTime 미설정 케이스 테스트 부재.

DetoxProgressService.findUser(Line 72-74)의 MISSION_ERROR_400_DETOX_TIME_NOT_SET 분기에 대한 테스트가 없습니다. 다른 예외 분기들과 동일한 패턴으로 쉽게 추가할 수 있는 케이스입니다.

🤖 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/test/java/com/example/hackathon/domain/detox/service/DetoxProgressServiceTest.java`
around lines 35 - 46, DetoxProgressService.findUser의 detoxStartTime/detoxEndTime
미설정 분기를 검증하는 테스트가 없습니다. 기존 예외 분기 테스트 패턴을 따라 해당 시간 값이 설정되지 않은 사용자를 구성하고,
MISSION_ERROR_400_DETOX_TIME_NOT_SET 예외와 관련 메시지가 반환되는지 검증하는 테스트를
DetoxProgressServiceTest에 추가하세요.
🤖 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.

Nitpick comments:
In
`@src/main/java/com/example/hackathon/domain/detox/service/DetoxProgressService.java`:
- Line 81: 중복된 자정 교차 판단 로직을 공용 헬퍼로 통합하세요. DetoxProgressService의 조건문과
DetoxPeriod.of 내부의 endTime.isAfter(startTime) 판단이 동일한 헬퍼를 사용하도록 수정해 로그 선택과 기간
계산이 항상 일치하게 하세요.

In
`@src/test/java/com/example/hackathon/domain/detox/service/DetoxProgressServiceTest.java`:
- Around line 35-46: DetoxProgressService.findUser의 detoxStartTime/detoxEndTime
미설정 분기를 검증하는 테스트가 없습니다. 기존 예외 분기 테스트 패턴을 따라 해당 시간 값이 설정되지 않은 사용자를 구성하고,
MISSION_ERROR_400_DETOX_TIME_NOT_SET 예외와 관련 메시지가 반환되는지 검증하는 테스트를
DetoxProgressServiceTest에 추가하세요.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 74cb60f6-4e57-464c-a23c-863fb7755953

📥 Commits

Reviewing files that changed from the base of the PR and between 19dbc67 and 9a8138c.

📒 Files selected for processing (7)
  • src/main/java/com/example/hackathon/domain/detox/controller/DetoxProgressController.java
  • src/main/java/com/example/hackathon/domain/detox/dto/DetoxProgressResponse.java
  • src/main/java/com/example/hackathon/domain/detox/dto/OverlappingMemberResponse.java
  • src/main/java/com/example/hackathon/domain/detox/service/DetoxProgressService.java
  • src/main/java/com/example/hackathon/domain/team/repository/TeamMemberRepository.java
  • src/main/java/com/example/hackathon/global/exception/ErrorCode.java
  • src/test/java/com/example/hackathon/domain/detox/service/DetoxProgressServiceTest.java

@chaeyylee
chaeyylee merged commit b9dcff4 into main Jul 10, 2026
1 check 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

🤖 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/example/hackathon/domain/mission/service/MissionTargetDateResolver.java`:
- Line 20: MissionTargetDateResolver의 날짜 판정이 마감 시각과 같은 순간에 오늘 날짜로 전환됩니다. 해당
resolver 메서드의 조건을 isAfter 기준으로 변경해 now가 yesterdayDeadline을 초과한 경우에만 오늘 날짜를 반환하고,
동일한 시각에는 전날 날짜를 반환하도록 수정하세요. 마감 시각과 정확히 일치하는 경계 조건 테스트도 추가하세요.

In
`@src/test/java/com/example/hackathon/domain/image/service/S3StorageServiceTest.java`:
- Around line 70-78: Update uploadReturnsUrlWithConfiguredRegion to configure a
region different from the default, such as us-west-2, and assert the generated
URL contains that configured region. Ensure the test setup exercises
resolvedRegion() using the non-default configuration so it fails if the
configured value is ignored.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e6720590-e000-4b8f-a386-183729a8246e

📥 Commits

Reviewing files that changed from the base of the PR and between 9a8138c and bbcb512.

📒 Files selected for processing (10)
  • src/main/java/com/example/hackathon/domain/image/config/S3Config.java
  • src/main/java/com/example/hackathon/domain/image/config/S3Properties.java
  • src/main/java/com/example/hackathon/domain/image/service/S3StorageService.java
  • src/main/java/com/example/hackathon/domain/mission/service/MissionCertificationService.java
  • src/main/java/com/example/hackathon/domain/mission/service/MissionCertificationTransactionService.java
  • src/main/java/com/example/hackathon/domain/mission/service/MissionService.java
  • src/main/java/com/example/hackathon/domain/mission/service/MissionTargetDateResolver.java
  • src/main/java/com/example/hackathon/global/exception/BusinessException.java
  • src/test/java/com/example/hackathon/domain/image/service/S3StorageServiceTest.java
  • src/test/java/com/example/hackathon/domain/mission/service/MissionCertificationServiceTest.java

LocalDateTime yesterdayDeadline = LocalDateTime
.of(today.minusDays(1), user.getDetoxStartTime())
.plusMinutes(DEADLINE_MINUTES);
return now.isBefore(yesterdayDeadline) ? today.minusDays(1) : today;

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

마감 시각을 전날 미션에 포함하세요.

다른 검증은 deadlineAt 이후에만 실패하지만, 여기서는 정확히 마감 시각에 오늘 날짜를 반환합니다. 예를 들어 23:55 시작이면 다음 날 00:05에 오늘 로그로 전환되어 BEFORE_DETOX_START가 발생합니다. isAfter 기준으로 맞추고 해당 경계 테스트를 추가하세요.

수정 예시
-        return now.isBefore(yesterdayDeadline) ? today.minusDays(1) : today;
+        return now.isAfter(yesterdayDeadline) ? today : today.minusDays(1);
📝 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
return now.isBefore(yesterdayDeadline) ? today.minusDays(1) : today;
return now.isAfter(yesterdayDeadline) ? today : today.minusDays(1);
🤖 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/example/hackathon/domain/mission/service/MissionTargetDateResolver.java`
at line 20, MissionTargetDateResolver의 날짜 판정이 마감 시각과 같은 순간에 오늘 날짜로 전환됩니다. 해당
resolver 메서드의 조건을 isAfter 기준으로 변경해 now가 yesterdayDeadline을 초과한 경우에만 오늘 날짜를 반환하고,
동일한 시각에는 전날 날짜를 반환하도록 수정하세요. 마감 시각과 정확히 일치하는 경계 조건 테스트도 추가하세요.

Comment on lines +70 to +78
void uploadReturnsUrlWithConfiguredRegion() {
when(s3Client.putObject(any(PutObjectRequest.class), any(software.amazon.awssdk.core.sync.RequestBody.class)))
.thenReturn(PutObjectResponse.builder().build());
MockMultipartFile image = new MockMultipartFile("image", "proof.jpg", "image/jpeg", new byte[]{1});

String imageUrl = storageService.uploadMissionImage(15L, image);

assertThat(imageUrl).startsWith("https://test-bucket.s3.ap-northeast-2.amazonaws.com/mission/15/")
.endsWith(".jpg");

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

구성 리전을 기본값과 다른 값으로 검증하세요.

현재 ap-northeast-2는 기본값과 같아서 resolvedRegion()이 구성값을 무시해도 테스트가 통과합니다.

수정 예시
-        S3Properties properties = new S3Properties(
-                "ap-northeast-2",
+        S3Properties properties = new S3Properties(
+                "us-west-2",
                 new S3Properties.S3("test-bucket", "access", "secret", 300)
         );
...
-        assertThat(imageUrl).startsWith("https://test-bucket.s3.ap-northeast-2.amazonaws.com/mission/15/")
+        assertThat(imageUrl).startsWith("https://test-bucket.s3.us-west-2.amazonaws.com/mission/15/")
🤖 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/test/java/com/example/hackathon/domain/image/service/S3StorageServiceTest.java`
around lines 70 - 78, Update uploadReturnsUrlWithConfiguredRegion to configure a
region different from the default, such as us-west-2, and assert the generated
URL contains that configured region. Ensure the test setup exercises
resolvedRegion() using the non-default configuration so it fails if the
configured value is ignored.

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 participant