Conversation
📝 WalkthroughWalkthrough관리자 대시보드 기능이 새로 추가되었습니다. 컨트롤러, 요청/응답 DTO, 도메인 타입·포트, QueryDSL 기반 쿼리 저장소, Redis 캐시 구현, 그리고 유스케이스 서비스가 포함되며 흐름은 캐시 조회 → DB 집계 쿼리 → 결과 조립 → 캐시 저장입니다. Changes
Sequence DiagramsequenceDiagram
participant Client
participant Controller as AdminDashboardController
participant UseCase as AdminGetDashboardUseCase
participant Cache as AdminDashboardCacheRepository
participant Query as AdminDashboardQueryRepository
participant DB as Database
Client->>Controller: GET /admin/dashboard (period, year)
activate Controller
Controller->>UseCase: execute(period, year)
activate UseCase
UseCase->>Cache: find(period, year)
activate Cache
Cache-->>UseCase: Optional<DashboardStatistics>
deactivate Cache
alt Cache Hit
UseCase-->>Controller: DashboardStatistics (from cache)
else Cache Miss
UseCase->>Query: countWorkspacesByPeriod(period, year)
activate Query
Query->>DB: SELECT EXTRACT(...) AS label, COUNT(*) AS count
DB-->>Query: PeriodCount[]
deactivate Query
UseCase->>Query: countUsersByPeriod(period, year)
activate Query
Query->>DB: SELECT EXTRACT(...) AS label, COUNT(*) AS count
DB-->>Query: PeriodCount[]
deactivate Query
UseCase->>Query: countReportsBetween(from, to)
activate Query
Query->>DB: SELECT COUNT(*) WHERE createdAt BETWEEN
DB-->>Query: long
deactivate Query
UseCase->>Query: countNewWorkersBetween(from, to)
activate Query
Query->>DB: SELECT COUNT(DISTINCT ...) WHERE status=ACTIVATED AND createdAt BETWEEN
DB-->>Query: long
deactivate Query
UseCase->>UseCase: calcGrowthRate & build DashboardStatistics
UseCase->>Cache: save(period, year, DashboardStatistics)
activate Cache
Cache-->>UseCase: void
deactivate Cache
UseCase-->>Controller: DashboardStatistics (computed)
end
deactivate UseCase
Controller-->>Client: 200 OK (CommonApiResponse<AdminDashboardResponseDto>)
deactivate Controller
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java`:
- Around line 97-105: The method countActiveUsersBetween currently counts
workspaceWorker rows created in the date range by using
workspaceWorker.createdAt and status ACTIVATED; change the implementation to
reflect the intended metric: if you mean "users active in the week" modify the
query in countActiveUsersBetween to filter by an activity timestamp (e.g.,
workspaceWorker.lastActiveAt or join to the activity/events table and use
activity.timestamp) and count distinct user ids, or if the intent is to count
"new workers created in the week" rename the method to countNewWorkersBetween
(and update callers) and keep the createdAt/status logic; update references to
workspaceWorker.createdAt, workspaceWorker.status, and the method name
accordingly.
- Around line 43-49: The query in AdminDashboardQueryRepositoryImpl filters by
calendar year (workspace.createdAt.year().eq(year)) but groups by EXTRACT(WEEK
...) which causes ISO-week/year boundary mismatches; change the filtering to use
ISO year extraction or align grouping to a week-start range: replace the year
filter with an ISO year expression (e.g., EXTRACT(ISOYEAR FROM
workspace.createdAt) == year) and include the same ISO-year expression in
groupBy/orderBy (or build a composite key of ISOYEAR+ISOWEEK) so grouping and
filtering use the same ISO-week calendar; update the usages around extractExpr,
groupBy and orderBy to use the new ISOYEAR/ISOWEEK expressions (or compute
week-start date) so weeks that span calendar years are correctly included.
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`:
- Around line 3-4: The AdminGetDashboard use case currently imports adapter DTOs
(AdminDashboardRequestDto, AdminDashboardResponseDto) which violates hexagonal
layering; remove those imports and instead define and use application-layer DTOs
or domain DTOs (e.g., application.admin.dto.GetDashboardCommand and
GetDashboardResult or
domain.admin.dto.AdminDashboardRequestDto/AdminDashboardResponseDto) in the
AdminGetDashboard class, update its method signatures to accept/return the new
types, and adjust the controller/adapter to map between adapter.inbound DTOs and
these application/domain DTOs before calling AdminGetDashboard.
- Around line 77-78: The method in AdminGetDashboard is annotated
`@Transactional`(readOnly = true) but performs a cache write via
adminDashboardCacheRepository.save(request.getPeriod(), year, result); to fix,
move the cache write into a separate non-readOnly method (e.g., create a private
saveDashboardCache(...) or a new service method) or change the cache-write call
to a method annotated `@Transactional`(propagation = Propagation.REQUIRES_NEW,
readOnly = false); update AdminGetDashboard to call that non-readOnly helper so
the intent of the original readOnly transaction remains clear and cache writes
run in their own writable transaction context.
- Around line 21-24: The class AdminGetDashboard is missing the `@Slf4j`
annotation required by our coding guideline for Use Case logging; add the Lombok
`@Slf4j` annotation to the AdminGetDashboard class (above the class declaration)
so you can use the generated log field for cache hit/miss and timing logs within
methods such as those implementing AdminGetDashboardUseCase; ensure Lombok is
enabled and imports/annotation processing are available in the project build.
- Around line 52-55: The current week range calculation using LocalDate.now()
and atTime(23,59,59) can miss sub-second data and ignores time zone; update
AdminGetDashboard to compute weekStart and a weekEndExclusive instead of
inclusive atTime: derive weekStart from LocalDate.now(ZoneId/Clock) to make
timezone testable (replace LocalDate.now() with LocalDate.now(clockOrZone)) and
set weekEndExclusive = weekStart.plusWeeks(1) (or compute ZonedDateTime/Instant
boundaries) and use a < weekEndExclusive query condition; adjust any uses of
weekEnd and weekStart accordingly (look for variables weekStart, weekEnd and
LocalDate today).
In
`@src/main/java/com/dreamteam/alter/domain/admin/port/inbound/AdminGetDashboardUseCase.java`:
- Around line 3-8: The inbound port AdminGetDashboardUseCase currently depends
on controller DTOs AdminDashboardRequestDto/AdminDashboardResponseDto; create
domain-level request/result models (e.g., AdminGetDashboardCommand and
AdminGetDashboardResult) and change the interface signature to
AdminGetDashboardResult execute(AdminGetDashboardCommand command); update
implementing use case classes to use these domain models and move any mapping
between AdminDashboardRequestDto <-> AdminGetDashboardCommand and
AdminGetDashboardResult <-> AdminDashboardResponseDto into the controller layer
so the port has no infrastructure/controller DTO dependencies.
In
`@src/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardCacheRepository.java`:
- Around line 3-4: AdminDashboardCacheRepository currently exposes the adapter
DTO AdminDashboardResponseDto causing a domain→adapter dependency; change the
port method signatures in AdminDashboardCacheRepository to use a pure
domain/application model (e.g., a new AdminDashboardData or
AdminDashboardCacheModel) and reference only domain types like DashboardPeriod,
then implement conversion between AdminDashboardData and
AdminDashboardResponseDto inside the Redis adapter implementation (not in the
port). Update all callers to use the domain model and let the adapter map
to/from AdminDashboardResponseDto.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 6d80eeea-87a6-4358-8a74-96f83a12626f
📒 Files selected for processing (11)
src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/controller/AdminDashboardController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/controller/AdminDashboardControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardRequestDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/admin/redis/AdminDashboardCacheRepositoryImpl.javasrc/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.javasrc/main/java/com/dreamteam/alter/domain/admin/port/inbound/AdminGetDashboardUseCase.javasrc/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardCacheRepository.javasrc/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/admin/type/DashboardPeriod.java
| .where( | ||
| workspace.status.eq(WorkspaceStatus.ACTIVATED), | ||
| workspace.createdAt.year().eq(year) | ||
| ) | ||
| .groupBy(extractExpr) | ||
| .orderBy(extractExpr.asc()) | ||
| .fetch(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
wc -l src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.javaRepository: alter-app/alter-backend
Length of output: 178
🏁 Script executed:
cat -n src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java | sed -n '35,160p'Repository: alter-app/alter-backend
Length of output: 5095
주간 집계 시 ISO 주-연도 경계에서 데이터 손실 및 불일치 발생
Line 45와 Line 68에서 캘린더 연도로 필터링하면서 Line 140과 Line 148의 EXTRACT(WEEK ...)로 그룹핑하면 ISO 주의 연도 경계에서 문제가 발생합니다.
ISO 8601 표준에서는 주가 연도를 넘어가므로:
- 1월 초의 일부 레코드는 전년도 52/53주에 속할 수 있습니다. 캘린더 연도 필터로 인해 그룹핑 결과에서 누락됩니다.
- 12월 말의 일부 레코드는 다음해 1주에 속할 수 있습니다. 캘린더 연도 필터와 맞지 않으면서 데이터 불일치가 발생합니다.
EXTRACT(ISOYEAR FROM ...)와 함께 필터링하거나, 주 시작일 기준으로 그룹핑하는 방식으로 수정하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java`
around lines 43 - 49, The query in AdminDashboardQueryRepositoryImpl filters by
calendar year (workspace.createdAt.year().eq(year)) but groups by EXTRACT(WEEK
...) which causes ISO-week/year boundary mismatches; change the filtering to use
ISO year extraction or align grouping to a week-start range: replace the year
filter with an ISO year expression (e.g., EXTRACT(ISOYEAR FROM
workspace.createdAt) == year) and include the same ISO-year expression in
groupBy/orderBy (or build a composite key of ISOYEAR+ISOWEEK) so grouping and
filtering use the same ISO-week calendar; update the usages around extractExpr,
groupBy and orderBy to use the new ISOYEAR/ISOWEEK expressions (or compute
week-start date) so weeks that span calendar years are correctly included.
| public long countActiveUsersBetween(LocalDateTime from, LocalDateTime to) { | ||
| Long count = queryFactory | ||
| .select(workspaceWorker.user.id.countDistinct()) | ||
| .from(workspaceWorker) | ||
| .where( | ||
| workspaceWorker.status.eq(WorkspaceWorkerStatus.ACTIVATED), | ||
| workspaceWorker.createdAt.goe(from), | ||
| workspaceWorker.createdAt.loe(to) | ||
| ) |
There was a problem hiding this comment.
주간 활성 사용자 수가 아니라 주간 신규 근로자 수를 집계하고 있습니다.
countActiveUsersBetween()는 workspaceWorker.createdAt 범위 안에서 현재 ACTIVATED 상태인 행만 세고 있어서, 실제로 그 주에 활동한 사용자 수가 아니라 그 주에 생성된 재직 레코드 수에 더 가깝습니다. 현재 WorkspaceWorker에는 활동 시각 필드가 보이지 않으므로, 실제 활동 이벤트/마지막 활동 시각 기준으로 다시 정의하거나 의도가 현재 로직이라면 지표명을 바꿔야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java`
around lines 97 - 105, The method countActiveUsersBetween currently counts
workspaceWorker rows created in the date range by using
workspaceWorker.createdAt and status ACTIVATED; change the implementation to
reflect the intended metric: if you mean "users active in the week" modify the
query in countActiveUsersBetween to filter by an activity timestamp (e.g.,
workspaceWorker.lastActiveAt or join to the activity/events table and use
activity.timestamp) and count distinct user ids, or if the intent is to count
"new workers created in the week" rename the method to countNewWorkersBetween
(and update callers) and keep the createdAt/status logic; update references to
workspaceWorker.createdAt, workspaceWorker.status, and the method name
accordingly.
src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java
Outdated
Show resolved
Hide resolved
| @Service("adminGetDashboard") | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class AdminGetDashboard implements AdminGetDashboardUseCase { |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
코딩 가이드라인: @Slf4j 어노테이션 누락
가이드라인에 따르면 Use Case에서 로깅은 @Slf4j를 사용해야 합니다. 캐시 히트/미스 여부나 실행 시간 등의 로깅이 필요할 수 있으므로 추가를 권장합니다.
♻️ 수정 제안
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
`@Service`("adminGetDashboard")
`@RequiredArgsConstructor`
`@Transactional`(readOnly = true)
public class AdminGetDashboard implements AdminGetDashboardUseCase {As per coding guidelines: "Logging uses @Slf4j"
📝 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.
| @Service("adminGetDashboard") | |
| @RequiredArgsConstructor | |
| @Transactional(readOnly = true) | |
| public class AdminGetDashboard implements AdminGetDashboardUseCase { | |
| import lombok.extern.slf4j.Slf4j; | |
| `@Slf4j` | |
| `@Service`("adminGetDashboard") | |
| `@RequiredArgsConstructor` | |
| `@Transactional`(readOnly = true) | |
| public class AdminGetDashboard implements AdminGetDashboardUseCase { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`
around lines 21 - 24, The class AdminGetDashboard is missing the `@Slf4j`
annotation required by our coding guideline for Use Case logging; add the Lombok
`@Slf4j` annotation to the AdminGetDashboard class (above the class declaration)
so you can use the generated log field for cache hit/miss and timing logs within
methods such as those implementing AdminGetDashboardUseCase; ensure Lombok is
enabled and imports/annotation processing are available in the project build.
| // 주간 범위 (이번 주 월요일 00:00 ~ 일요일 23:59:59) | ||
| LocalDate today = LocalDate.now(); | ||
| LocalDateTime weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay(); | ||
| LocalDateTime weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
주간 범위 계산 시 경계값 및 타임존 고려
-
경계값 누락:
atTime(23, 59, 59)는23:59:59.000000001~23:59:59.999999999구간의 데이터를 놓칠 수 있습니다. -
타임존:
LocalDate.now()는 시스템 기본 타임존을 사용합니다. 서버 배포 환경에 따라 예상과 다른 결과가 나올 수 있습니다.
♻️ 경계값 처리 개선안
LocalDate today = LocalDate.now();
LocalDateTime weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay();
-LocalDateTime weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59);
+LocalDateTime weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(LocalTime.MAX);또는 exclusive upper bound 패턴 사용:
LocalDateTime weekEndExclusive = weekStart.plusWeeks(1); // 다음 주 월요일 00:00:00
// 쿼리에서 < weekEndExclusive 조건 사용📝 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.
| // 주간 범위 (이번 주 월요일 00:00 ~ 일요일 23:59:59) | |
| LocalDate today = LocalDate.now(); | |
| LocalDateTime weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay(); | |
| LocalDateTime weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59); | |
| // 주간 범위 (이번 주 월요일 00:00 ~ 일요일 23:59:59) | |
| LocalDate today = LocalDate.now(); | |
| LocalDateTime weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay(); | |
| LocalDateTime weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(LocalTime.MAX); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`
around lines 52 - 55, The current week range calculation using LocalDate.now()
and atTime(23,59,59) can miss sub-second data and ignores time zone; update
AdminGetDashboard to compute weekStart and a weekEndExclusive instead of
inclusive atTime: derive weekStart from LocalDate.now(ZoneId/Clock) to make
timezone testable (replace LocalDate.now() with LocalDate.now(clockOrZone)) and
set weekEndExclusive = weekStart.plusWeeks(1) (or compute ZonedDateTime/Instant
boundaries) and use a < weekEndExclusive query condition; adjust any uses of
weekEnd and weekStart accordingly (look for variables weekStart, weekEnd and
LocalDate today).
| // 캐시 저장 | ||
| adminDashboardCacheRepository.save(request.getPeriod(), year, result); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
readOnly = true 트랜잭션 내에서 캐시 저장 수행
@Transactional(readOnly = true)로 선언되어 있지만 캐시에 쓰기 작업을 수행합니다. Redis 작업은 JDBC 트랜잭션에 영향받지 않지만, 메서드의 의도가 "읽기 전용"이라고 표현되어 있어 의미상 혼란을 줄 수 있습니다.
캐시 저장이 실패해도 비즈니스 로직에 영향이 없다면 현재 구조도 허용 가능하나, 명확성을 위해 별도 메서드로 분리하거나 트랜잭션 전파 설정을 검토해 보세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`
around lines 77 - 78, The method in AdminGetDashboard is annotated
`@Transactional`(readOnly = true) but performs a cache write via
adminDashboardCacheRepository.save(request.getPeriod(), year, result); to fix,
move the cache write into a separate non-readOnly method (e.g., create a private
saveDashboardCache(...) or a new service method) or change the cache-write call
to a method annotated `@Transactional`(propagation = Propagation.REQUIRES_NEW,
readOnly = false); update AdminGetDashboard to call that non-readOnly helper so
the intent of the original readOnly transaction remains clear and cache writes
run in their own writable transaction context.
src/main/java/com/dreamteam/alter/domain/admin/port/inbound/AdminGetDashboardUseCase.java
Outdated
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardCacheRepository.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java (3)
24-25: 🧹 Nitpick | 🔵 Trivial
readOnly = true트랜잭션에서 캐시 쓰기를 수행하고 있습니다.Line 24의 읽기 전용 트랜잭션 안에서 Line 73의
save(...)가 실행되어 의도/정책이 혼재됩니다. 캐시 저장을 별도 writable 경로로 분리하거나 트랜잭션 경계를 재설계해 주세요.As per coding guidelines: "Read-only operations use
@Transactional(readOnly = true)." and "Write operations use@Transactional."Also applies to: 72-74
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java` around lines 24 - 25, The class AdminGetDashboard is annotated with `@Transactional`(readOnly = true) but calls save(...) (cache write) inside its flow; remove the write from the read-only transaction by moving the cache persistence into a writable transaction or a separate component/method annotated with `@Transactional` (no readOnly) and invoke that from AdminGetDashboard (e.g., extract the save logic into a new method saveDashboardCache(...) or a CacheUpdater service and call it from the use case), or change the transaction boundary on AdminGetDashboard to a non-readOnly transaction if the class must perform writes; ensure the method doing cache.save(...) runs under a writable `@Transactional` while read-only query methods remain readOnly.
53-55:⚠️ Potential issue | 🟠 Major주간 집계 범위 계산이 경계값/타임존 이슈를 유발할 수 있습니다.
Line 55의
atTime(23, 59, 59)는 서브초 데이터를 누락할 수 있고, Line 53의LocalDate.now()는 서버 기본 타임존에 종속되어 집계 오차를 만들 수 있습니다.♻️ 제안 패치
+import java.time.Clock; +import java.time.LocalTime; ... public class AdminGetDashboard implements AdminGetDashboardUseCase { ... + private final Clock clock; ... - LocalDate today = LocalDate.now(); + LocalDate today = LocalDate.now(clock); LocalDateTime weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay(); - LocalDateTime weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59); + LocalDateTime weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(LocalTime.MAX);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java` around lines 53 - 55, The weekly range calculation in AdminGetDashboard uses LocalDate.now() and atTime(23,59,59), which is vulnerable to timezone-dependent variance and drops subsecond precision; update the logic in AdminGetDashboard to obtain the current date/time with an explicit ZoneId (e.g., ZoneId.systemDefault() or a configured zone) instead of LocalDate.now(), compute weekStart/weekEnd with ZonedDateTime or Instant, and make the end bound exclusive (e.g., use start of next day/week or LocalTime.MAX / Instant.ofEpochMilli with full precision) so no subsecond data is lost and timezones are handled deterministically (adjust usages of today, weekStart, and weekEnd accordingly).
22-25:⚠️ Potential issue | 🟡 Minor
@Slf4j누락으로 UseCase 로깅 가이드라인을 위반합니다.Line 22~25 구간에
@Slf4j를 추가해 캐시 히트/미스 및 집계 지연 시간 로깅 포인트를 확보해주세요.♻️ 제안 패치
+import lombok.extern.slf4j.Slf4j; `@Service`("adminGetDashboard") +@Slf4j `@RequiredArgsConstructor` `@Transactional`(readOnly = true) public class AdminGetDashboard implements AdminGetDashboardUseCase {As per coding guidelines: "Logging uses
@Slf4j."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java` around lines 22 - 25, The class AdminGetDashboard (implementing AdminGetDashboardUseCase) is missing the `@Slf4j` annotation required by our logging guideline; add the Lombok `@Slf4j` annotation to the class declaration so you can call log.debug/info/error from within AdminGetDashboard, then instrument existing cache and aggregation sections to emit log statements for cache hits/misses and aggregation latency (use the class' methods where cache is checked and aggregation performed to record timing and call log.info/debug with contextual messages).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.java`:
- Around line 34-41: AdminDashboardResponseDto.from currently calls
ChartData.from(statistics.getWorkspaceChart()) and
ChartData.from(statistics.getMemberChart()) without null checks which can throw
NPE; update the method to guard those calls (in AdminDashboardResponseDto.from)
by checking statistics.getWorkspaceChart() and statistics.getMemberChart() and
only call ChartData.from(...) when non-null (otherwise set
workspaceChart/memberChart to null or an empty ChartData as appropriate), e.g.
use ternary/Optional to map values safely while keeping the rest of the builder
usage intact.
- Around line 62-71: ChartData.from currently calls
chartData.getDataPoints().stream() which will NPE if getDataPoints() is null;
update the ChartData.from method to null-safe map the points by checking
DashboardChartData.getDataPoints() (or using
Optional/Objects.requireNonNullElse) and defaulting to an empty list before
streaming, then map each entry to DataPoint.of(label, count) and set that result
into the builder so ChartData.from and DataPoint mapping remain unchanged.
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/redis/AdminDashboardCacheRepositoryImpl.java`:
- Around line 26-27: 현재 클래스 AdminDashboardCacheRepositoryImpl에 하드코딩된 TTL 상수(TTL,
KEY_PREFIX) 대신 외부 설정을 사용하도록 변경하세요: TTL 필드를 제거하고 생성자 또는 필드 주입으로 구성값(ttlMinutes 또는
ttlSeconds)을 읽어 들여 멤버로 보관한 뒤 save와 find에서 Duration.ofMinutes(ttlMinutes) (또는
Duration.ofSeconds(ttlSeconds) 사용 시 해당 단위)로 TTL을 생성해 적용하도록 수정합니다; 설정 이름은 예를 들어
admin.dashboard.cache.ttlMinutes로 하고 KEY_PREFIX는 그대로 두되 TTL은 환경설정에서 주입받도록 변경하세요.
In `@src/main/java/com/dreamteam/alter/domain/admin/type/DashboardChartData.java`:
- Around line 22-34: The of factory method in DashboardChartData currently
assigns the externally provided mutable List dataPoints directly into the built
object; change DashboardChartData.of to defensively copy and/or wrap the
incoming dataPoints (e.g., create a new ArrayList<>(dataPoints) or use
List.copyOf(dataPoints) / Collections.unmodifiableList(...)) before passing it
to the builder so the internal state of DashboardChartData is not mutated by
callers; ensure you also handle null input consistently (either allow null or
replace with empty unmodifiable list) when updating the of method and the
builder usage.
In
`@src/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.java`:
- Around line 9-13: Jackson cannot instantiate DashboardStatistics during
objectMapper.readValue in AdminDashboardCacheRepositoryImpl because the class
has a private no-arg constructor and a private builder; change the Lombok
annotation on DashboardStatistics from `@NoArgsConstructor`(access =
AccessLevel.PRIVATE) to `@NoArgsConstructor`(access = AccessLevel.PROTECTED) so
Jackson can deserialize while preserving domain immutability, or alternatively
add `@JsonDeserialize`(builder =
DashboardStatistics.DashboardStatisticsBuilder.class) if you prefer
builder-based deserialization; update the annotation on the DashboardStatistics
class accordingly (referencing DashboardStatistics, `@NoArgsConstructor`,
`@Builder`, and AdminDashboardCacheRepositoryImpl).
---
Duplicate comments:
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`:
- Around line 24-25: The class AdminGetDashboard is annotated with
`@Transactional`(readOnly = true) but calls save(...) (cache write) inside its
flow; remove the write from the read-only transaction by moving the cache
persistence into a writable transaction or a separate component/method annotated
with `@Transactional` (no readOnly) and invoke that from AdminGetDashboard (e.g.,
extract the save logic into a new method saveDashboardCache(...) or a
CacheUpdater service and call it from the use case), or change the transaction
boundary on AdminGetDashboard to a non-readOnly transaction if the class must
perform writes; ensure the method doing cache.save(...) runs under a writable
`@Transactional` while read-only query methods remain readOnly.
- Around line 53-55: The weekly range calculation in AdminGetDashboard uses
LocalDate.now() and atTime(23,59,59), which is vulnerable to timezone-dependent
variance and drops subsecond precision; update the logic in AdminGetDashboard to
obtain the current date/time with an explicit ZoneId (e.g.,
ZoneId.systemDefault() or a configured zone) instead of LocalDate.now(), compute
weekStart/weekEnd with ZonedDateTime or Instant, and make the end bound
exclusive (e.g., use start of next day/week or LocalTime.MAX /
Instant.ofEpochMilli with full precision) so no subsecond data is lost and
timezones are handled deterministically (adjust usages of today, weekStart, and
weekEnd accordingly).
- Around line 22-25: The class AdminGetDashboard (implementing
AdminGetDashboardUseCase) is missing the `@Slf4j` annotation required by our
logging guideline; add the Lombok `@Slf4j` annotation to the class declaration so
you can call log.debug/info/error from within AdminGetDashboard, then instrument
existing cache and aggregation sections to emit log statements for cache
hits/misses and aggregation latency (use the class' methods where cache is
checked and aggregation performed to record timing and call log.info/debug with
contextual messages).
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 9b74505a-de5a-4272-a72f-9395e875478d
📒 Files selected for processing (9)
src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/controller/AdminDashboardController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/outbound/admin/redis/AdminDashboardCacheRepositoryImpl.javasrc/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.javasrc/main/java/com/dreamteam/alter/domain/admin/port/inbound/AdminGetDashboardUseCase.javasrc/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardCacheRepository.javasrc/main/java/com/dreamteam/alter/domain/admin/type/DashboardChartData.javasrc/main/java/com/dreamteam/alter/domain/admin/type/DashboardDataPoint.javasrc/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.java
| public static AdminDashboardResponseDto from(DashboardStatistics statistics) { | ||
| return AdminDashboardResponseDto.builder() | ||
| .workspaceChart(ChartData.from(statistics.getWorkspaceChart())) | ||
| .memberChart(ChartData.from(statistics.getMemberChart())) | ||
| .weeklyReportCount(statistics.getWeeklyReportCount()) | ||
| .weeklyActiveUserCount(statistics.getWeeklyActiveUserCount()) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
workspaceChart/memberChart null 체크를 고려하세요.
statistics.getWorkspaceChart() 또는 statistics.getMemberChart()가 null인 경우 ChartData.from() 호출 시 NullPointerException이 발생할 수 있습니다.
🛡️ null 안전 처리 제안
public static AdminDashboardResponseDto from(DashboardStatistics statistics) {
return AdminDashboardResponseDto.builder()
- .workspaceChart(ChartData.from(statistics.getWorkspaceChart()))
- .memberChart(ChartData.from(statistics.getMemberChart()))
+ .workspaceChart(statistics.getWorkspaceChart() != null
+ ? ChartData.from(statistics.getWorkspaceChart()) : null)
+ .memberChart(statistics.getMemberChart() != null
+ ? ChartData.from(statistics.getMemberChart()) : null)
.weeklyReportCount(statistics.getWeeklyReportCount())
.weeklyActiveUserCount(statistics.getWeeklyActiveUserCount())
.build();
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.java`
around lines 34 - 41, AdminDashboardResponseDto.from currently calls
ChartData.from(statistics.getWorkspaceChart()) and
ChartData.from(statistics.getMemberChart()) without null checks which can throw
NPE; update the method to guard those calls (in AdminDashboardResponseDto.from)
by checking statistics.getWorkspaceChart() and statistics.getMemberChart() and
only call ChartData.from(...) when non-null (otherwise set
workspaceChart/memberChart to null or an empty ChartData as appropriate), e.g.
use ternary/Optional to map values safely while keeping the rest of the builder
usage intact.
| public static ChartData from(DashboardChartData chartData) { | ||
| return ChartData.builder() | ||
| .period(chartData.getPeriod()) | ||
| .year(chartData.getYear()) | ||
| .yearOverYearGrowthRate(chartData.getYearOverYearGrowthRate()) | ||
| .dataPoints(chartData.getDataPoints().stream() | ||
| .map(dp -> DataPoint.of(dp.getLabel(), dp.getCount())) | ||
| .toList()) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
getDataPoints() null 체크가 필요합니다.
chartData.getDataPoints()가 null을 반환하면 .stream() 호출 시 NullPointerException이 발생합니다.
🛡️ null 안전 처리 제안
public static ChartData from(DashboardChartData chartData) {
+ List<DataPoint> dataPoints = chartData.getDataPoints() != null
+ ? chartData.getDataPoints().stream()
+ .map(dp -> DataPoint.of(dp.getLabel(), dp.getCount()))
+ .toList()
+ : List.of();
+
return ChartData.builder()
.period(chartData.getPeriod())
.year(chartData.getYear())
.yearOverYearGrowthRate(chartData.getYearOverYearGrowthRate())
- .dataPoints(chartData.getDataPoints().stream()
- .map(dp -> DataPoint.of(dp.getLabel(), dp.getCount()))
- .toList())
+ .dataPoints(dataPoints)
.build();
}📝 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.
| public static ChartData from(DashboardChartData chartData) { | |
| return ChartData.builder() | |
| .period(chartData.getPeriod()) | |
| .year(chartData.getYear()) | |
| .yearOverYearGrowthRate(chartData.getYearOverYearGrowthRate()) | |
| .dataPoints(chartData.getDataPoints().stream() | |
| .map(dp -> DataPoint.of(dp.getLabel(), dp.getCount())) | |
| .toList()) | |
| .build(); | |
| } | |
| public static ChartData from(DashboardChartData chartData) { | |
| List<DataPoint> dataPoints = chartData.getDataPoints() != null | |
| ? chartData.getDataPoints().stream() | |
| .map(dp -> DataPoint.of(dp.getLabel(), dp.getCount())) | |
| .toList() | |
| : List.of(); | |
| return ChartData.builder() | |
| .period(chartData.getPeriod()) | |
| .year(chartData.getYear()) | |
| .yearOverYearGrowthRate(chartData.getYearOverYearGrowthRate()) | |
| .dataPoints(dataPoints) | |
| .build(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.java`
around lines 62 - 71, ChartData.from currently calls
chartData.getDataPoints().stream() which will NPE if getDataPoints() is null;
update the ChartData.from method to null-safe map the points by checking
DashboardChartData.getDataPoints() (or using
Optional/Objects.requireNonNullElse) and defaulting to an empty list before
streaming, then map each entry to DataPoint.of(label, count) and set that result
into the builder so ChartData.from and DataPoint mapping remain unchanged.
| private static final String KEY_PREFIX = "admin:dashboard:"; | ||
| private static final Duration TTL = Duration.ofMinutes(5); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
TTL 값을 외부 설정으로 분리하는 것을 고려하세요.
현재 5분으로 하드코딩된 TTL을 환경별로 다르게 설정해야 할 수 있습니다 (개발/스테이징/프로덕션).
♻️ 외부 설정 적용 제안
+import org.springframework.beans.factory.annotation.Value;
+
`@Slf4j`
`@Component`
`@RequiredArgsConstructor`
public class AdminDashboardCacheRepositoryImpl implements AdminDashboardCacheRepository {
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
private static final String KEY_PREFIX = "admin:dashboard:";
- private static final Duration TTL = Duration.ofMinutes(5);
+
+ `@Value`("${admin.dashboard.cache.ttl-minutes:5}")
+ private long ttlMinutes;find/save 메서드에서 Duration.ofMinutes(ttlMinutes) 사용.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/redis/AdminDashboardCacheRepositoryImpl.java`
around lines 26 - 27, 현재 클래스 AdminDashboardCacheRepositoryImpl에 하드코딩된 TTL
상수(TTL, KEY_PREFIX) 대신 외부 설정을 사용하도록 변경하세요: TTL 필드를 제거하고 생성자 또는 필드 주입으로
구성값(ttlMinutes 또는 ttlSeconds)을 읽어 들여 멤버로 보관한 뒤 save와 find에서
Duration.ofMinutes(ttlMinutes) (또는 Duration.ofSeconds(ttlSeconds) 사용 시 해당 단위)로
TTL을 생성해 적용하도록 수정합니다; 설정 이름은 예를 들어 admin.dashboard.cache.ttlMinutes로 하고
KEY_PREFIX는 그대로 두되 TTL은 환경설정에서 주입받도록 변경하세요.
| public static DashboardChartData of( | ||
| DashboardPeriod period, | ||
| int year, | ||
| double yearOverYearGrowthRate, | ||
| List<DashboardDataPoint> dataPoints | ||
| ) { | ||
| return DashboardChartData.builder() | ||
| .period(period) | ||
| .year(year) | ||
| .yearOverYearGrowthRate(yearOverYearGrowthRate) | ||
| .dataPoints(dataPoints) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
dataPoints 리스트에 대한 방어적 복사를 고려하세요.
외부에서 전달된 가변 리스트가 내부 상태에 직접 할당되면 불변성이 깨질 수 있습니다.
♻️ 방어적 복사 적용 제안
public static DashboardChartData of(
DashboardPeriod period,
int year,
double yearOverYearGrowthRate,
List<DashboardDataPoint> dataPoints
) {
return DashboardChartData.builder()
.period(period)
.year(year)
.yearOverYearGrowthRate(yearOverYearGrowthRate)
- .dataPoints(dataPoints)
+ .dataPoints(dataPoints != null ? List.copyOf(dataPoints) : List.of())
.build();
}📝 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.
| public static DashboardChartData of( | |
| DashboardPeriod period, | |
| int year, | |
| double yearOverYearGrowthRate, | |
| List<DashboardDataPoint> dataPoints | |
| ) { | |
| return DashboardChartData.builder() | |
| .period(period) | |
| .year(year) | |
| .yearOverYearGrowthRate(yearOverYearGrowthRate) | |
| .dataPoints(dataPoints) | |
| .build(); | |
| } | |
| public static DashboardChartData of( | |
| DashboardPeriod period, | |
| int year, | |
| double yearOverYearGrowthRate, | |
| List<DashboardDataPoint> dataPoints | |
| ) { | |
| return DashboardChartData.builder() | |
| .period(period) | |
| .year(year) | |
| .yearOverYearGrowthRate(yearOverYearGrowthRate) | |
| .dataPoints(dataPoints != null ? List.copyOf(dataPoints) : List.of()) | |
| .build(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/dreamteam/alter/domain/admin/type/DashboardChartData.java`
around lines 22 - 34, The of factory method in DashboardChartData currently
assigns the externally provided mutable List dataPoints directly into the built
object; change DashboardChartData.of to defensively copy and/or wrap the
incoming dataPoints (e.g., create a new ArrayList<>(dataPoints) or use
List.copyOf(dataPoints) / Collections.unmodifiableList(...)) before passing it
to the builder so the internal state of DashboardChartData is not mutated by
callers; ensure you also handle null input consistently (either allow null or
replace with empty unmodifiable list) when updating the of method and the
builder usage.
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Builder(access = AccessLevel.PRIVATE) | ||
| public class DashboardStatistics { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# ObjectMapper 설정 및 Jackson 모듈 등록 확인
rg -n "ObjectMapper" --type=java -A 5 | head -100
# Jackson 관련 설정 확인
rg -n "@JsonDeserialize|jackson-module|ParameterNamesModule" --type=javaRepository: alter-app/alter-backend
Length of output: 11800
🏁 Script executed:
# Find and examine DashboardStatistics class
find . -name "DashboardStatistics.java" -type fRepository: alter-app/alter-backend
Length of output: 145
🏁 Script executed:
# Find AdminDashboardCacheRepositoryImpl
find . -name "AdminDashboardCacheRepositoryImpl.java" -type fRepository: alter-app/alter-backend
Length of output: 170
🏁 Script executed:
# Check for Jackson configuration in the project
rg -n "Jackson|ObjectMapper|@Configuration" --type=java -A 3 | grep -E "(class|interface|@Configuration|ObjectMapper|Jackson)" | head -50Repository: alter-app/alter-backend
Length of output: 7470
🏁 Script executed:
# Check application.yml or application.properties for Jackson configuration
find . -name "application*.yml" -o -name "application*.properties" | xargs cat 2>/dev/null | grep -i jacksonRepository: alter-app/alter-backend
Length of output: 49
🏁 Script executed:
# Read DashboardStatistics.java
cat -n src/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.javaRepository: alter-app/alter-backend
Length of output: 1300
🏁 Script executed:
# Read AdminDashboardCacheRepositoryImpl.java
cat -n src/main/java/com/dreamteam/alter/adapter/outbound/admin/redis/AdminDashboardCacheRepositoryImpl.javaRepository: alter-app/alter-backend
Length of output: 2975
🏁 Script executed:
# Search for Jackson configuration classes
rg -n "class.*Configuration|@Configuration" --type=java -A 10 | grep -B 5 -A 10 -i "jackson\|objectmapper"Repository: alter-app/alter-backend
Length of output: 49
🏁 Script executed:
# Check for pom.xml or build.gradle for Jackson and Lombok versions
find . -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" | xargs cat 2>/dev/null | grep -A 3 -B 1 -i "jackson\|lombok"Repository: alter-app/alter-backend
Length of output: 520
🏁 Script executed:
# Search for any Jackson configuration classes or beans
rg -n "@Bean.*ObjectMapper|@Configuration" --type=java -A 15 | head -100Repository: alter-app/alter-backend
Length of output: 9409
🏁 Script executed:
# Check if there's any Jackson annotation configuration in application files
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.properties" \) | xargs cat 2>/dev/null | grep -i -A 5 -B 5 "jackson"Repository: alter-app/alter-backend
Length of output: 49
🏁 Script executed:
# Search for jackson module registrations
rg -n "registerModule|Jackson2ObjectMapperBuilderCustomizer|ObjectMapperFactory" --type=javaRepository: alter-app/alter-backend
Length of output: 49
Jackson 역직렬화 실패 위험 - 접근 수준 수정 필요
@NoArgsConstructor(access = AccessLevel.PRIVATE)와 @Builder(access = AccessLevel.PRIVATE) 조합에서 Jackson ObjectMapper가 JSON을 역직렬화할 수 없습니다. AdminDashboardCacheRepositoryImpl 라인 34의 objectMapper.readValue(value, DashboardStatistics.class) 호출이 JsonProcessingException으로 실패합니다.
Jackson은 다음 중 하나가 필요합니다:
- No-args 생성자를
AccessLevel.PROTECTED로 변경 (권장) - Builder를
AccessLevel.PUBLIC으로 변경 - 클래스에
@JsonDeserialize(builder = DashboardStatistics.DashboardStatisticsBuilder.class)추가 (권장) - ObjectMapper 커스텀 설정 (복잡함)
생성자는 인프라 계층 코드가 접근할 수 있어야 하고, 도메인 계층의 불변성 보장을 위해서는 PROTECTED가 가장 적절합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.java`
around lines 9 - 13, Jackson cannot instantiate DashboardStatistics during
objectMapper.readValue in AdminDashboardCacheRepositoryImpl because the class
has a private no-arg constructor and a private builder; change the Lombok
annotation on DashboardStatistics from `@NoArgsConstructor`(access =
AccessLevel.PRIVATE) to `@NoArgsConstructor`(access = AccessLevel.PROTECTED) so
Jackson can deserialize while preserving domain immutability, or alternatively
add `@JsonDeserialize`(builder =
DashboardStatistics.DashboardStatisticsBuilder.class) if you prefer
builder-based deserialization; update the annotation on the DashboardStatistics
class accordingly (referencing DashboardStatistics, `@NoArgsConstructor`,
`@Builder`, and AdminDashboardCacheRepositoryImpl).
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java (1)
37-49:⚠️ Potential issue | 🟠 MajorWEEKLY 집계는 ISO week-year 기준으로 맞춰 주세요.
EXTRACT(WEEK FROM ...)로 주차를 만들면서createdAt.year().eq(year)로 자르면 연말/연초 데이터가 다른 week-year에 걸릴 때 빠지거나 잘못 묶입니다.WEEKLY일 때는 calendar year 대신 같은 week calendar 기준(예:ISOYEAR + WEEK, 또는 주 시작일)으로 필터와 그룹핑을 맞춰야 합니다. 같은 문제가countWorkspacesByPeriod()와countUsersByPeriod()둘 다에 있습니다.Also applies to: 61-72, 138-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java` around lines 37 - 49, The weekly aggregation currently uses EXTRACT(WEEK FROM ...) but still filters by createdAt.year().eq(year), which breaks ISO week-year boundaries; update AdminDashboardQueryRepositoryImpl so that for Period.WEEKLY you compute both the group and the filter using the same ISO week-year logic (e.g., use ISOYEAR + WEEK or a single expression like to_char(createdAt, 'IYYY-IW') / extract('isoyear' from createdAt) together with extract(week from createdAt)), and replace the createdAt.year().eq(year) check with a predicate derived from that same expression; apply this change in buildExtractExpr/buildLabelExpr (so label/grouping uses ISO week-year) and ensure countWorkspacesByPeriod() and countUsersByPeriod() use the new weekly filter and group expression consistently.src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java (2)
32-32:⚠️ Potential issue | 🟠 Major주간 집계 upper bound를 exclusive로 바꾸세요.
Line 55의
atTime(23, 59, 59)는 일요일 마지막 sub-second 데이터를 놓칩니다. 또 Line 32와 Line 53의LocalDate.now()는 시스템 기본 타임존에 묶여 테스트와 운영 결과를 흔듭니다.Clock을 주입해서LocalDate.now(clock)를 쓰고,weekEndExclusive = weekStart.plusWeeks(1)로 계산한 뒤 저장소에서도< weekEndExclusive로 비교하는 편이 안전합니다.Also applies to: 52-58
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java` at line 32, Change AdminGetDashboard to accept and use a java.time.Clock (use LocalDate.now(clock) instead of LocalDate.now()) and compute week boundaries as an exclusive upper bound: replace the current end-of-day atTime(23,59,59) logic with weekStart.plusWeeks(1) assigned to weekEndExclusive, use weekEndExclusive in repository queries with a '< weekEndExclusive' comparison, and ensure resolvedYear calculation uses the injected Clock (resolvedYear = year != null ? year : LocalDate.now(clock).getYear()) so tests aren’t tied to system default timezone.
22-25: 🧹 Nitpick | 🔵 Trivial가이드라인대로
@Slf4j를 추가해 주세요.캐시 hit/miss와 DB fallback이 모두 이 클래스 안에 있어서 운영 중 가장 먼저 확인할 로그 포인트가 모여 있습니다. 이번 유스케이스에는
@Slf4j를 같이 두는 편이 좋습니다.♻️ 최소 수정 예시
+import lombok.extern.slf4j.Slf4j; ... +@Slf4j `@Service`("adminGetDashboard") `@RequiredArgsConstructor` `@Transactional`(readOnly = true)As per coding guidelines,
src/main/java/com/dreamteam/alter/application/**: "Logging uses@Slf4j."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java` around lines 22 - 25, Add the Lombok SLF4J logger to the AdminGetDashboard class by annotating the class with `@Slf4j` (import lombok.extern.slf4j.Slf4j) so the existing cache hit/miss and DB fallback points can use the generated log instance; update class declaration (AdminGetDashboard implements AdminGetDashboardUseCase) to include `@Slf4j` and replace/ensure any manual logger declarations with the Lombok-provided log variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.java`:
- Around line 3-5: AdminDashboardResponseDto currently exposes the domain enum
DashboardPeriod directly via the ChartData.period field; change ChartData.period
to a String and map DashboardPeriod -> String when constructing the DTO (e.g.,
in the AdminDashboardResponseDto builder/constructor or mapper used to populate
ChartData) so the API surface uses a stable String representation instead of the
domain enum DashboardPeriod; update any usages that set or read
AdminDashboardResponseDto.ChartData.period to convert to/from
DashboardPeriod.toString()/name() as appropriate.
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java`:
- Around line 97-105: The current countNewWorkersBetween method incorrectly
filters by workspaceWorker.createdAt and workspaceWorker.status; change it to
count distinct workspaceWorker.user.id by workspaceWorker.employedAt between the
provided from/to and remove the status.eq(WorkspaceWorkerStatus.ACTIVATED)
filter so the metric reflects actual join (employedAt) events; update the method
signature/contract comment if present
(AdminDashboardQueryRepository.countNewWorkersBetween) to indicate it is
date-based on employedAt rather than current status or createdAt.
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`:
- Around line 39-40: The repository returns only non-empty buckets from
countWorkspacesByPeriod and countUsersByPeriod and then passes them through
toDataPoints, causing missing WEEKLY/MONTHLY intervals; update AdminGetDashboard
(around where workspaceCounts/userCounts are fetched and where toDataPoints is
called) to first generate the full list of expected period buckets for the given
period and resolvedYear (e.g., all days/weeks/months), then merge the query
results into that list filling any missing buckets with count=0 before
constructing DashboardChartData; apply the same normalization logic where
similar calls occur (the other occurrences you noted at the later blocks) so all
returned time series have fixed length and include zero-filled intervals.
In
`@src/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardQueryRepository.java`:
- Around line 10-22: The port exposes formatted labels in PeriodCount which
leaks presentation logic; change the record PeriodCount(String label, long
count) to carry the raw bucket identifier (e.g., int bucket or Integer period)
and update the signatures of countWorkspacesByPeriod and countUsersByPeriod to
return List<PeriodCount> with that raw bucket field; then update the SQL
adapter/mapper to populate the raw bucket number (week/month index) instead of a
formatted string, leaving label/formatting to the presentation or adapter layer.
In
`@src/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.java`:
- Around line 9-31: Add explicit validation in DashboardStatistics.of to ensure
workspaceChart and memberChart are non-null and that weeklyReportCount and
weeklyNewWorkerCount are non-negative, throwing clear exceptions (e.g.,
NullPointerException or IllegalArgumentException) when invariants fail;
similarly, update DashboardChartData.of to validate its input parameters are not
null and throw on invalid values so the domain invariants are enforced at
construction time (refer to DashboardStatistics.of and DashboardChartData.of to
locate the factory methods).
---
Duplicate comments:
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java`:
- Around line 37-49: The weekly aggregation currently uses EXTRACT(WEEK FROM
...) but still filters by createdAt.year().eq(year), which breaks ISO week-year
boundaries; update AdminDashboardQueryRepositoryImpl so that for Period.WEEKLY
you compute both the group and the filter using the same ISO week-year logic
(e.g., use ISOYEAR + WEEK or a single expression like to_char(createdAt,
'IYYY-IW') / extract('isoyear' from createdAt) together with extract(week from
createdAt)), and replace the createdAt.year().eq(year) check with a predicate
derived from that same expression; apply this change in
buildExtractExpr/buildLabelExpr (so label/grouping uses ISO week-year) and
ensure countWorkspacesByPeriod() and countUsersByPeriod() use the new weekly
filter and group expression consistently.
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`:
- Line 32: Change AdminGetDashboard to accept and use a java.time.Clock (use
LocalDate.now(clock) instead of LocalDate.now()) and compute week boundaries as
an exclusive upper bound: replace the current end-of-day atTime(23,59,59) logic
with weekStart.plusWeeks(1) assigned to weekEndExclusive, use weekEndExclusive
in repository queries with a '< weekEndExclusive' comparison, and ensure
resolvedYear calculation uses the injected Clock (resolvedYear = year != null ?
year : LocalDate.now(clock).getYear()) so tests aren’t tied to system default
timezone.
- Around line 22-25: Add the Lombok SLF4J logger to the AdminGetDashboard class
by annotating the class with `@Slf4j` (import lombok.extern.slf4j.Slf4j) so the
existing cache hit/miss and DB fallback points can use the generated log
instance; update class declaration (AdminGetDashboard implements
AdminGetDashboardUseCase) to include `@Slf4j` and replace/ensure any manual logger
declarations with the Lombok-provided log variable.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: eff7c2b5-f2e9-4d81-a4bc-dc2fcac92f90
📒 Files selected for processing (5)
src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.javasrc/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.java
| import com.dreamteam.alter.domain.admin.type.DashboardChartData; | ||
| import com.dreamteam.alter.domain.admin.type.DashboardPeriod; | ||
| import com.dreamteam.alter.domain.admin.type.DashboardStatistics; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.javaRepository: alter-app/alter-backend
Length of output: 3988
🏁 Script executed:
find . -type f -name "DashboardPeriod.java" -o -name "DashboardChartData.java" -o -name "DashboardStatistics.java" | head -5Repository: alter-app/alter-backend
Length of output: 298
🏁 Script executed:
cat -n src/main/java/com/dreamteam/alter/domain/admin/type/DashboardPeriod.java
cat -n src/main/java/com/dreamteam/alter/domain/admin/type/DashboardChartData.javaRepository: alter-app/alter-backend
Length of output: 1402
응답 DTO가 도메인 enum을 직접 노출하지 않도록 수정해 주세요.
AdminDashboardResponseDto.ChartData의 period 필드가 도메인 enum DashboardPeriod를 그대로 사용하면 API 계약이 도메인 레이어의 enum 정의에 직접 결합됩니다. 도메인 enum이 변경될 때마다 API 클라이언트에 영향을 미치게 되므로, 도메인 타입과 API 경계를 분리하기 위해 String으로 변환하시기 바랍니다.
수정 예시
-import com.dreamteam.alter.domain.admin.type.DashboardPeriod;
...
- private DashboardPeriod period;
+ private String period;
...
- .period(chartData.getPeriod())
+ .period(chartData.getPeriod().name())🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/admin/dashboard/dto/AdminDashboardResponseDto.java`
around lines 3 - 5, AdminDashboardResponseDto currently exposes the domain enum
DashboardPeriod directly via the ChartData.period field; change ChartData.period
to a String and map DashboardPeriod -> String when constructing the DTO (e.g.,
in the AdminDashboardResponseDto builder/constructor or mapper used to populate
ChartData) so the API surface uses a stable String representation instead of the
domain enum DashboardPeriod; update any usages that set or read
AdminDashboardResponseDto.ChartData.period to convert to/from
DashboardPeriod.toString()/name() as appropriate.
| public long countNewWorkersBetween(LocalDateTime from, LocalDateTime to) { | ||
| Long count = queryFactory | ||
| .select(workspaceWorker.user.id.countDistinct()) | ||
| .from(workspaceWorker) | ||
| .where( | ||
| workspaceWorker.status.eq(WorkspaceWorkerStatus.ACTIVATED), | ||
| workspaceWorker.createdAt.goe(from), | ||
| workspaceWorker.createdAt.loe(to) | ||
| ) |
There was a problem hiding this comment.
weeklyNewWorkerCount가 실제 합류 이벤트를 세지 못합니다.
AdminDashboardResponseDto Line 31-32는 "주간 신규 합류자 수"인데, 여기서는 workspaceWorker.createdAt과 현재 ACTIVATED 상태를 기준으로 세고 있습니다. 그러면 이번 주에 합류했다가 이후 퇴사한 사람은 과거 주간 집계에서도 사라지고, 레코드 생성이 늦게 되면 실제 합류 주차와 다른 주에 잡힙니다. WorkspaceWorker의 employedAt 기준으로 집계하고 현재 상태 필터는 제거하는 쪽이 지표 의미와 맞습니다. 이 변경이면 AdminDashboardQueryRepository.countNewWorkersBetween(...) 계약도 날짜 기반으로 같이 정리하는 편이 낫습니다.
🛠️ 의도에 맞춘 최소 수정 예시
- .where(
- workspaceWorker.status.eq(WorkspaceWorkerStatus.ACTIVATED),
- workspaceWorker.createdAt.goe(from),
- workspaceWorker.createdAt.loe(to)
- )
+ .where(
+ workspaceWorker.employedAt.goe(from.toLocalDate()),
+ workspaceWorker.employedAt.loe(to.toLocalDate())
+ )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java`
around lines 97 - 105, The current countNewWorkersBetween method incorrectly
filters by workspaceWorker.createdAt and workspaceWorker.status; change it to
count distinct workspaceWorker.user.id by workspaceWorker.employedAt between the
provided from/to and remove the status.eq(WorkspaceWorkerStatus.ACTIVATED)
filter so the metric reflects actual join (employedAt) events; update the method
signature/contract comment if present
(AdminDashboardQueryRepository.countNewWorkersBetween) to indicate it is
date-based on employedAt rather than current status or createdAt.
| List<PeriodCount> workspaceCounts = adminDashboardQueryRepository.countWorkspacesByPeriod(period, resolvedYear); | ||
| List<PeriodCount> userCounts = adminDashboardQueryRepository.countUsersByPeriod(period, resolvedYear); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
빈 기간을 0으로 채우는 정규화가 필요합니다.
countWorkspacesByPeriod()/countUsersByPeriod()는 데이터가 있는 버킷만 반환하고, toDataPoints()는 그 결과를 그대로 옮깁니다. 지금 형태면 특히 WEEKLY/MONTHLY에서 0건인 주/월이 응답에서 빠져 시계열 길이가 가변이 됩니다. period와 resolvedYear에 맞는 전체 버킷을 먼저 만든 뒤 누락 구간을 0으로 채워 DashboardChartData를 구성해 주세요.
Also applies to: 61-66, 86-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/application/admin/usecase/AdminGetDashboard.java`
around lines 39 - 40, The repository returns only non-empty buckets from
countWorkspacesByPeriod and countUsersByPeriod and then passes them through
toDataPoints, causing missing WEEKLY/MONTHLY intervals; update AdminGetDashboard
(around where workspaceCounts/userCounts are fetched and where toDataPoints is
called) to first generate the full list of expected period buckets for the given
period and resolvedYear (e.g., all days/weeks/months), then merge the query
results into that list filling any missing buckets with count=0 before
constructing DashboardChartData; apply the same normalization logic where
similar calls occur (the other occurrences you noted at the later blocks) so all
returned time series have fixed length and include zero-filled intervals.
| List<PeriodCount> countWorkspacesByPeriod(DashboardPeriod period, int year); | ||
|
|
||
| List<PeriodCount> countUsersByPeriod(DashboardPeriod period, int year); | ||
|
|
||
| long countReportsBetween(LocalDateTime from, LocalDateTime to); | ||
|
|
||
| long countNewWorkersBetween(LocalDateTime from, LocalDateTime to); | ||
|
|
||
| long countWorkspacesInYear(int year); | ||
|
|
||
| long countUsersInYear(int year); | ||
|
|
||
| record PeriodCount(String label, long count) {} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardQueryRepository.javaRepository: alter-app/alter-backend
Length of output: 888
🏁 Script executed:
# Check if there are any usages of AdminDashboardQueryRepository to understand how label is populated
rg "AdminDashboardQueryRepository" --type java -A 3 -B 3Repository: alter-app/alter-backend
Length of output: 4901
🏁 Script executed:
# Search for implementations of this port to see how label is actually populated
fd -e java -path "*admin*" -name "*AdminDashboardQueryRepository*"Repository: alter-app/alter-backend
Length of output: 235
🏁 Script executed:
# Search for PeriodCount usages across the codebase
rg "PeriodCount" --type java -B 2 -A 2Repository: alter-app/alter-backend
Length of output: 6749
🏁 Script executed:
cat -n src/main/java/com/dreamteam/alter/adapter/outbound/admin/persistence/AdminDashboardQueryRepositoryImpl.java | head -100Repository: alter-app/alter-backend
Length of output: 4470
🏁 Script executed:
# Search for buildLabelExpr implementation to see what kind of labels are generated
rg "buildLabelExpr" -A 10 -B 2Repository: alter-app/alter-backend
Length of output: 5588
🏁 Script executed:
# Check DashboardDataPoint to see how label is used in presentation layer
rg "DashboardDataPoint" --type-add 'java:*.java' -t java -A 5 -B 2 | head -50Repository: alter-app/alter-backend
Length of output: 5076
조회 포트에서 포맷된 라벨을 내보내지 마세요.
PeriodCount의 String label 필드가 SQL 어댑터에서 생성한 포맷된 문자열("1주", "1월" 등)을 노출합니다. 이는 포트 계약을 프레젠테이션 레이어의 SQL 포맷팅 로직과 결합하여 도메인 경계를 침범합니다. 포트는 원본 버킷 값(주 번호, 월 번호)만 반환하고, 라벨 포맷팅은 애플리케이션 또는 어댑터 레이어에서 처리해야 합니다.
♻️ 계약을 더 얇게 만드는 예시
- record PeriodCount(String label, long count) {}
+ record PeriodCount(int bucket, long count) {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/dreamteam/alter/domain/admin/port/outbound/AdminDashboardQueryRepository.java`
around lines 10 - 22, The port exposes formatted labels in PeriodCount which
leaks presentation logic; change the record PeriodCount(String label, long
count) to carry the raw bucket identifier (e.g., int bucket or Integer period)
and update the signatures of countWorkspacesByPeriod and countUsersByPeriod to
return List<PeriodCount> with that raw bucket field; then update the SQL
adapter/mapper to populate the raw bucket number (week/month index) instead of a
formatted string, leaving label/formatting to the presentation or adapter layer.
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @Builder(access = AccessLevel.PRIVATE) | ||
| public class DashboardStatistics { | ||
|
|
||
| private DashboardChartData workspaceChart; | ||
| private DashboardChartData memberChart; | ||
| private long weeklyReportCount; | ||
| private long weeklyNewWorkerCount; | ||
|
|
||
| public static DashboardStatistics of( | ||
| DashboardChartData workspaceChart, | ||
| DashboardChartData memberChart, | ||
| long weeklyReportCount, | ||
| long weeklyNewWorkerCount | ||
| ) { | ||
| return DashboardStatistics.builder() | ||
| .workspaceChart(workspaceChart) | ||
| .memberChart(memberChart) | ||
| .weeklyReportCount(weeklyReportCount) | ||
| .weeklyNewWorkerCount(weeklyNewWorkerCount) | ||
| .build(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.javaRepository: alter-app/alter-backend
Length of output: 1296
🏁 Script executed:
# Check the complete structure of the domain layer
fd -t f "DashboardStatistics" --exec cat -n {} \;Repository: alter-app/alter-backend
Length of output: 1296
🏁 Script executed:
# Search for DashboardChartData definition to understand the type
fd "DashboardChartData.java" -t fRepository: alter-app/alter-backend
Length of output: 142
🏁 Script executed:
# Check if Lombok is used elsewhere in the domain layer and review guidelines interpretation
rg -t java "@Getter|@NoArgsConstructor|@AllArgsConstructor|@Builder" src/main/java/com/dreamteam/alter/domain/ | head -20Repository: alter-app/alter-backend
Length of output: 2239
🏁 Script executed:
# Check for any validation methods or invariant checks in the domain directory
rg -t java "validate|invariant|null|isEmpty|require|Assert" src/main/java/com/dreamteam/alter/domain/ --max-count=10Repository: alter-app/alter-backend
Length of output: 31088
🏁 Script executed:
# Check if DashboardChartData is used elsewhere and if it has validation
cat -n src/main/java/com/dreamteam/alter/domain/admin/type/DashboardChartData.javaRepository: alter-app/alter-backend
Length of output: 1259
🏁 Script executed:
# Look for similar value objects in the domain layer to see if they validate inputs
rg -t java "public static.*of\(" src/main/java/com/dreamteam/alter/domain/ -A 5 | head -40Repository: alter-app/alter-backend
Length of output: 2668
도메인 타입의 생성 시 입력값을 검증하세요.
DashboardStatistics.of()와 DashboardChartData.of() 팩토리 메서드가 null 검증 없이 객체를 생성합니다. workspaceChart와 memberChart의 null 여부, weeklyReportCount와 weeklyNewWorkerCount의 음수 여부를 팩토리 메서드 내에서 명시적으로 검증하여 도메인 불변식을 보호하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/dreamteam/alter/domain/admin/type/DashboardStatistics.java`
around lines 9 - 31, Add explicit validation in DashboardStatistics.of to ensure
workspaceChart and memberChart are non-null and that weeklyReportCount and
weeklyNewWorkerCount are non-negative, throwing clear exceptions (e.g.,
NullPointerException or IllegalArgumentException) when invariants fail;
similarly, update DashboardChartData.of to validate its input parameters are not
null and throw on invalid values so the domain invariants are enforced at
construction time (refer to DashboardStatistics.of and DashboardChartData.of to
locate the factory methods).
관련 문서
https://www.notion.so/BE-API-2b186553162880afa2a8cb224b1147ed?source=copy_link
Summary by CodeRabbit