diff --git a/archunit-baseline/05a5820d-39ea-4db6-b8e7-f737ac6a90df b/archunit-baseline/05a5820d-39ea-4db6-b8e7-f737ac6a90df new file mode 100644 index 00000000..2cc1df70 --- /dev/null +++ b/archunit-baseline/05a5820d-39ea-4db6-b8e7-f737ac6a90df @@ -0,0 +1,66 @@ +Constructor (com.carecode.core.client.CareFacilityApiService, com.carecode.domain.careFacility.service.CareFacilityService, com.carecode.domain.careFacility.repository.CareFacilityRepository)> has parameter of type in (CareFacilityApiController.java:0) +Constructor (com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.mapper.CommunityMapper)> has parameter of type in (AdminCommunityController.java:0) +Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (AdminDashboardController.java:0) +Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (AdminDashboardController.java:0) +Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (AdminDashboardController.java:0) +Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.mapper.HealthRecordMapper)> has parameter of type in (AdminHealthController.java:0) +Constructor (com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.health.mapper.HospitalMapper)> has parameter of type in (AdminHospitalController.java:0) +Constructor (com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (AdminNotificationController.java:0) +Constructor (com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (AdminNotificationController.java:0) +Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.admin.service.PolicyAdminService)> has parameter of type in (AdminPolicyController.java:0) +Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (AdminPolicyVerificationController.java:0) +Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.service.UserService)> has parameter of type in (AdminUserController.java:0) +Constructor (com.carecode.domain.notification.repository.NotificationRepository, com.carecode.core.analytics.EventLogger, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (NotificationLinkController.java:0) +Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.analytics.EventLogger, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (BenefitLinkController.java:0) +Field has type in (CareFacilityApiController.java:0) +Field has type in (AdminCommunityController.java:0) +Field has type in (AdminDashboardController.java:0) +Field has type in (AdminDashboardController.java:0) +Field has type in (AdminDashboardController.java:0) +Field has type in (AdminHealthController.java:0) +Field has type in (AdminHospitalController.java:0) +Field has type in (AdminNotificationController.java:0) +Field has type in (AdminNotificationController.java:0) +Field has type in (AdminPolicyController.java:0) +Field has type in (AdminPolicyVerificationController.java:0) +Field has type in (AdminUserController.java:0) +Field has type in (NotificationLinkController.java:0) +Field has type in (BenefitLinkController.java:0) +Method calls method in (CareFacilityApiController.java:86) +Method calls method in (CareFacilityApiController.java:96) +Method calls method in (CareFacilityApiController.java:266) +Method calls method in (AdminCommunityController.java:46) +Method calls method in (AdminCommunityController.java:51) +Method calls method in (AdminCommunityController.java:33) +Method calls method in (AdminDashboardController.java:43) +Method calls method in (AdminDashboardController.java:44) +Method calls method in (AdminDashboardController.java:42) +Method calls method in (AdminDashboardController.java:64) +Method calls method in (AdminDashboardController.java:71) +Method calls method in (AdminDashboardController.java:57) +Method calls method in (AdminDashboardController.java:91) +Method calls method in (AdminHealthController.java:46) +Method calls method in (AdminHealthController.java:51) +Method calls method in (AdminHealthController.java:33) +Method calls method in (AdminHospitalController.java:45) +Method calls method in (AdminHospitalController.java:50) +Method calls method in (AdminHospitalController.java:32) +Method calls method in (AdminNotificationController.java:63) +Method calls method in (AdminNotificationController.java:52) +Method calls method in (AdminNotificationController.java:70) +Method calls method in (AdminNotificationController.java:75) +Method calls method in (AdminNotificationController.java:38) +Method calls method in (AdminPolicyController.java:77) +Method calls method in (AdminPolicyController.java:36) +Method calls method in (AdminPolicyVerificationController.java:73) +Method calls method in (AdminPolicyVerificationController.java:61) +Method calls method in (AdminPolicyVerificationController.java:65) +Method calls method in (AdminPolicyVerificationController.java:41) +Method calls method in (AdminPolicyVerificationController.java:47) +Method calls method in (AdminUserController.java:172) +Method calls method in (AdminUserController.java:177) +Method calls method in (AdminUserController.java:56) +Method calls method in (AdminUserController.java:134) +Method calls method in (NotificationLinkController.java:56) +Method calls method in (NotificationLinkController.java:64) +Method calls method in (BenefitLinkController.java:43) diff --git a/archunit-baseline/05a7a88e-9a95-451a-b96f-4a8eb9be6c1e b/archunit-baseline/05a7a88e-9a95-451a-b96f-4a8eb9be6c1e new file mode 100644 index 00000000..71262174 --- /dev/null +++ b/archunit-baseline/05a7a88e-9a95-451a-b96f-4a8eb9be6c1e @@ -0,0 +1,96 @@ +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.app.CareFacilityFacade) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.controller.CareFacilityController) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.controller.FacilityWaitlistController) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityAdminBookingSearchRequest$CareFacilityAdminBookingSearchRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityAdminBookingSearchRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityAdminStatusUpdateRequest$CareFacilityAdminStatusUpdateRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityAdminStatusUpdateRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityAdvancedSearchRequest$CareFacilityAdvancedSearchRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityAdvancedSearchRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityListBookingsRequest$CareFacilityListBookingsRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilityListBookingsRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilitySearchRequest$CareFacilitySearchRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CareFacilitySearchRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CreateBookingRequest$CreateBookingRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.CreateBookingRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.ReviewRequest$ReviewRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.ReviewRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.UpdateBookingRequest$UpdateBookingRequestBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.UpdateBookingRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.request.WaitlistRequest) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.AdmissionForecastResponse$AdmissionForecastResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.AdmissionForecastResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.BookingListResponse$BookingListResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.BookingListResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.BookingResponse$BookingResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.BookingResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.BookingStats$BookingStatsBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.BookingStats) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityBookingResponse$CareFacilityBookingResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityBookingResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityInfo$CareFacilityInfoBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityInfo) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityListResponse$CareFacilityListResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityListResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityStatsResponse$CareFacilityStatsResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.CareFacilityStatsResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.DailyBookingCount$DailyBookingCountBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.DailyBookingCount) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.FacilityDistribution$FacilityDistributionBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.FacilityDistribution) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.FacilityPopularityResponse$FacilityPopularityResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.FacilityPopularityResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.ForecastAccuracyResponse$Bucket$BucketBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.ForecastAccuracyResponse$Bucket) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.ForecastAccuracyResponse$ForecastAccuracyResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.ForecastAccuracyResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.ReviewResponse$ReviewResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.ReviewResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.StatusDistribution$StatusDistributionBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.StatusDistribution) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.TypeDistribution$TypeDistributionBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.TypeDistribution) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.TypeStats$TypeStatsBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.TypeStats) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.WaitlistStatsResponse$WaitlistStatsResponseBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.dto.response.WaitlistStatsResponse) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.CareFacility$CareFacilityBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.CareFacility) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.CareFacilityBooking$BookingStatus) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.CareFacilityBooking$BookingType) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.CareFacilityBooking) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.FacilityCapacitySnapshot$FacilityCapacitySnapshotBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.FacilityCapacitySnapshot) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.FacilityType) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.FacilityWaitlist$FacilityWaitlistBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.FacilityWaitlist$WaitStatus) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.FacilityWaitlist) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.ForecastAccuracy$ForecastAccuracyBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.ForecastAccuracy) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.Review$ReviewBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.entity.Review) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.mapper.BookingMapper) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.mapper.CareFacilityMapper) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.repository.CareFacilityBookingRepository) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.repository.CareFacilityRepository) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.repository.FacilityCapacitySnapshotRepository) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.repository.FacilityWaitlistRepository) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.repository.ForecastAccuracyRepository) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.repository.ReviewRepository) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.AdmissionForecastCalculator$Forecast$ForecastBuilder) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.AdmissionForecastCalculator$Forecast) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.AdmissionForecastCalculator) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.AdmissionForecastService) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.CareFacilityBookingService$1) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.CareFacilityBookingService) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.CareFacilityService) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.FacilityPopularityService$Rate) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.FacilityPopularityService) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.FacilityVacancyNotifier$VacancyNotifyResult) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.FacilityVacancyNotifier) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.FacilityWaitlistService) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.ForecastAccuracyService$1) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.ForecastAccuracyService) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.ForecastBacktestService$Bucket) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.ForecastBacktestService$Sample) +도메인 패키지 이름이 소문자가 아니다: careFacility (com.carecode.domain.careFacility.service.ForecastBacktestService) diff --git a/archunit-baseline/4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 b/archunit-baseline/4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 new file mode 100644 index 00000000..9be28508 --- /dev/null +++ b/archunit-baseline/4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 @@ -0,0 +1,50 @@ +Constructor (com.carecode.domain.user.repository.UserRepository)> has parameter of type in (CurrentUserFacade.java:0) +Constructor (com.carecode.domain.user.repository.UserRepository)> has parameter of type in (CustomUserDetailsService.java:0) +Constructor (com.carecode.domain.user.service.JwtService)> has parameter of type in (JwtAuthenticationFilter.java:0) +Field has type in (CurrentUserFacade.java:0) +Field has type in (CustomUserDetailsService.java:0) +Field has type in (JwtAuthenticationFilter.java:0) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:114) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:115) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:113) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:109) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:113) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:114) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:115) +Method calls method in (CustomizedResponseEntityExceptionHandler.java:116) +Method has annotation member of type in (CustomizedResponseEntityExceptionHandler.java:0) +Method has parameter of type in (CustomizedResponseEntityExceptionHandler.java:0) +Method calls method in (CurrentUserFacade.java:58) +Method has generic return type > with type argument depending on in (CurrentUserFacade.java:0) +Method calls method in (CurrentUserFacade.java:115) +Method calls method in (CurrentUserFacade.java:115) +Method calls method in (CurrentUserFacade.java:112) +Method has parameter of type in (CurrentUserFacade.java:0) +Method calls method in (CurrentUserFacade.java:63) +Method has return type in (CurrentUserFacade.java:0) +Method calls method in (CurrentUserFacade.java:72) +Method calls method in (CurrentUserFacade.java:68) +Method calls method in (CurrentUserFacade.java:96) +Method has return type in (CurrentUserFacade.java:0) +Method has return type in (CurrentUserFacade.java:0) +Method calls method in (CustomUserDetailsService.java:27) +Method calls method in (CustomUserDetailsService.java:30) +Method calls method in (CustomUserDetailsService.java:22) +Method calls method in (CustomUserDetailsService.java:29) +Method calls method in (CustomUserDetailsService.java:29) +Method calls method in (CustomUserDetailsService.java:20) +Method calls method in (JwtAuthenticationFilter.java:71) +Method calls method in (JwtAuthenticationFilter.java:72) +Method calls method in (JwtAuthenticationFilter.java:70) +Method calls method in (JwtAuthenticationFilter.java:64) +Method calls method in (KakaoUtil.java:119) +Method calls method in (KakaoUtil.java:127) +Method calls method in (KakaoUtil.java:147) +Method calls method in (KakaoUtil.java:151) +Method has parameter of type in (KakaoUtil.java:0) +Method has return type in (KakaoUtil.java:0) +Method references class object in (KakaoUtil.java:145) +Method calls method in (KakaoUtil.java:99) +Method calls method in (KakaoUtil.java:99) +Method has return type in (KakaoUtil.java:0) +Method references class object in (KakaoUtil.java:97) diff --git a/archunit-baseline/74b2056c-4f0e-4cb3-af35-0cfdcc359eeb b/archunit-baseline/74b2056c-4f0e-4cb3-af35-0cfdcc359eeb new file mode 100644 index 00000000..7fd4248d --- /dev/null +++ b/archunit-baseline/74b2056c-4f0e-4cb3-af35-0cfdcc359eeb @@ -0,0 +1 @@ +Method calls method in (CommunityService.java:355) diff --git a/archunit-baseline/87360be1-7d6d-4423-b366-16fa4fe6f6b6 b/archunit-baseline/87360be1-7d6d-4423-b366-16fa4fe6f6b6 new file mode 100644 index 00000000..e69de29b diff --git a/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 b/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 new file mode 100644 index 00000000..2dfb0313 --- /dev/null +++ b/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 @@ -0,0 +1,271 @@ +admin → careFacility 리포지토리: Constructor (com.carecode.domain.careFacility.repository.CareFacilityBookingRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (CareFacilityBookingAdminService.java:0) +admin → careFacility 리포지토리: Field has type in (CareFacilityBookingAdminService.java:0) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:173) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:170) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:54) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:131) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:180) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:181) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:182) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:183) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:184) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:189) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:188) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:187) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:267) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:253) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:252) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:226) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:230) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:231) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:232) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:233) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:234) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:240) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:244) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:245) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:246) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:102) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:142) +admin → careFacility 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:162) +admin → community 리포지토리: Constructor (com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.mapper.CommunityMapper)> has parameter of type in (AdminCommunityController.java:0) +admin → community 리포지토리: Field has type in (AdminCommunityController.java:0) +admin → community 리포지토리: Method calls method in (AdminCommunityController.java:46) +admin → community 리포지토리: Method calls method in (AdminCommunityController.java:51) +admin → community 리포지토리: Method calls method in (AdminCommunityController.java:33) +admin → health 리포지토리: Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (AdminDashboardController.java:0) +admin → health 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.mapper.HealthRecordMapper)> has parameter of type in (AdminHealthController.java:0) +admin → health 리포지토리: Constructor (com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.health.mapper.HospitalMapper)> has parameter of type in (AdminHospitalController.java:0) +admin → health 리포지토리: Field has type in (AdminDashboardController.java:0) +admin → health 리포지토리: Field has type in (AdminHealthController.java:0) +admin → health 리포지토리: Field has type in (AdminHospitalController.java:0) +admin → health 리포지토리: Method calls method in (AdminDashboardController.java:43) +admin → health 리포지토리: Method calls method in (AdminDashboardController.java:64) +admin → health 리포지토리: Method calls method in (AdminHealthController.java:46) +admin → health 리포지토리: Method calls method in (AdminHealthController.java:51) +admin → health 리포지토리: Method calls method in (AdminHealthController.java:33) +admin → health 리포지토리: Method calls method in (AdminHospitalController.java:45) +admin → health 리포지토리: Method calls method in (AdminHospitalController.java:50) +admin → health 리포지토리: Method calls method in (AdminHospitalController.java:32) +admin → notification 리포지토리: Constructor (com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (AdminNotificationController.java:0) +admin → notification 리포지토리: Field has type in (AdminNotificationController.java:0) +admin → notification 리포지토리: Method calls method in (AdminNotificationController.java:63) +admin → notification 리포지토리: Method calls method in (AdminNotificationController.java:70) +admin → notification 리포지토리: Method calls method in (AdminNotificationController.java:75) +admin → notification 리포지토리: Method calls method in (AdminNotificationController.java:38) +admin → policy 리포지토리: Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (AdminDashboardController.java:0) +admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.admin.service.PolicyAdminService)> has parameter of type in (AdminPolicyController.java:0) +admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (AdminPolicyVerificationController.java:0) +admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyCategoryRepository, com.fasterxml.jackson.databind.ObjectMapper)> has parameter of type in (PolicyAdminService.java:0) +admin → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyCategoryRepository, com.fasterxml.jackson.databind.ObjectMapper)> has parameter of type in (PolicyAdminService.java:0) +admin → policy 리포지토리: Field has type in (AdminDashboardController.java:0) +admin → policy 리포지토리: Field has type in (AdminPolicyController.java:0) +admin → policy 리포지토리: Field has type in (AdminPolicyVerificationController.java:0) +admin → policy 리포지토리: Field has type in (PolicyAdminService.java:0) +admin → policy 리포지토리: Field has type in (PolicyAdminService.java:0) +admin → policy 리포지토리: Method calls method in (AdminDashboardController.java:44) +admin → policy 리포지토리: Method calls method in (AdminDashboardController.java:71) +admin → policy 리포지토리: Method calls method in (AdminPolicyController.java:77) +admin → policy 리포지토리: Method calls method in (AdminPolicyController.java:36) +admin → policy 리포지토리: Method calls method in (AdminPolicyVerificationController.java:73) +admin → policy 리포지토리: Method calls method in (AdminPolicyVerificationController.java:61) +admin → policy 리포지토리: Method calls method in (AdminPolicyVerificationController.java:65) +admin → policy 리포지토리: Method calls method in (AdminPolicyVerificationController.java:41) +admin → policy 리포지토리: Method calls method in (AdminPolicyVerificationController.java:47) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:145) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:44) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:170) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:168) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:154) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:71) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:116) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:53) +admin → policy 리포지토리: Method calls method in (PolicyAdminService.java:57) +admin → user 리포지토리: Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.health.repository.HospitalRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (AdminDashboardController.java:0) +admin → user 리포지토리: Constructor (com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (AdminNotificationController.java:0) +admin → user 리포지토리: Constructor (com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.service.UserService)> has parameter of type in (AdminUserController.java:0) +admin → user 리포지토리: Constructor (com.carecode.domain.careFacility.repository.CareFacilityBookingRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (CareFacilityBookingAdminService.java:0) +admin → user 리포지토리: Field has type in (AdminDashboardController.java:0) +admin → user 리포지토리: Field has type in (AdminNotificationController.java:0) +admin → user 리포지토리: Field has type in (AdminUserController.java:0) +admin → user 리포지토리: Field has type in (CareFacilityBookingAdminService.java:0) +admin → user 리포지토리: Method calls method in (AdminDashboardController.java:42) +admin → user 리포지토리: Method calls method in (AdminDashboardController.java:57) +admin → user 리포지토리: Method calls method in (AdminDashboardController.java:91) +admin → user 리포지토리: Method calls method in (AdminNotificationController.java:52) +admin → user 리포지토리: Method calls method in (AdminUserController.java:172) +admin → user 리포지토리: Method calls method in (AdminUserController.java:177) +admin → user 리포지토리: Method calls method in (AdminUserController.java:56) +admin → user 리포지토리: Method calls method in (AdminUserController.java:134) +admin → user 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:317) +admin → user 리포지토리: Method calls method in (CareFacilityBookingAdminService.java:351) +careFacility → notification 리포지토리: Constructor (com.carecode.domain.careFacility.repository.FacilityWaitlistRepository, com.carecode.domain.careFacility.repository.FacilityCapacitySnapshotRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (FacilityVacancyNotifier.java:0) +careFacility → notification 리포지토리: Field has type in (FacilityVacancyNotifier.java:0) +careFacility → notification 리포지토리: Method calls method in (FacilityVacancyNotifier.java:130) +careFacility → user 리포지토리: Constructor (com.carecode.domain.careFacility.repository.CareFacilityBookingRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (CareFacilityBookingService.java:0) +careFacility → user 리포지토리: Constructor (com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.careFacility.repository.CareFacilityBookingRepository, com.carecode.core.ops.sync.SyncFreshnessService, com.carecode.domain.careFacility.repository.ReviewRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.careFacility.mapper.CareFacilityMapper, com.carecode.core.search.FullTextSearchSupport)> has parameter of type in (CareFacilityService.java:0) +careFacility → user 리포지토리: Constructor (com.carecode.domain.careFacility.repository.FacilityWaitlistRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.core.security.CurrentUserFacade, com.carecode.core.analytics.EventLogger)> has parameter of type in (FacilityWaitlistService.java:0) +careFacility → user 리포지토리: Field has type in (CareFacilityBookingService.java:0) +careFacility → user 리포지토리: Field has type in (CareFacilityService.java:0) +careFacility → user 리포지토리: Field has type in (FacilityWaitlistService.java:0) +careFacility → user 리포지토리: Method calls method in (CareFacilityBookingService.java:52) +careFacility → user 리포지토리: Method calls method in (CareFacilityBookingService.java:100) +careFacility → user 리포지토리: Method calls method in (CareFacilityService.java:524) +careFacility → user 리포지토리: Method calls method in (CareFacilityService.java:559) +careFacility → user 리포지토리: Method calls method in (CareFacilityService.java:549) +careFacility → user 리포지토리: Method calls method in (FacilityWaitlistService.java:140) +chatbot → careFacility 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository)> has parameter of type in (CareKnowledgeRetriever.java:0) +chatbot → careFacility 리포지토리: Constructor (com.carecode.domain.chatbot.repository.ChatMessageRepository, com.carecode.domain.chatbot.repository.ChatSessionRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.chatbot.rag.CareKnowledgeRetriever, com.carecode.domain.chatbot.llm.ChatCompletionClient)> has parameter of type in (ChatbotService.java:0) +chatbot → careFacility 리포지토리: Field has type in (CareKnowledgeRetriever.java:0) +chatbot → careFacility 리포지토리: Field has type in (ChatbotService.java:0) +chatbot → careFacility 리포지토리: Method calls method in (CareKnowledgeRetriever.java:82) +chatbot → careFacility 리포지토리: Method calls method in (ChatbotService.java:520) +chatbot → policy 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository)> has parameter of type in (CareKnowledgeRetriever.java:0) +chatbot → policy 리포지토리: Constructor (com.carecode.domain.chatbot.repository.ChatMessageRepository, com.carecode.domain.chatbot.repository.ChatSessionRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.chatbot.rag.CareKnowledgeRetriever, com.carecode.domain.chatbot.llm.ChatCompletionClient)> has parameter of type in (ChatbotService.java:0) +chatbot → policy 리포지토리: Field has type in (CareKnowledgeRetriever.java:0) +chatbot → policy 리포지토리: Field has type in (ChatbotService.java:0) +chatbot → policy 리포지토리: Method calls method in (CareKnowledgeRetriever.java:68) +chatbot → policy 리포지토리: Method calls method in (ChatbotService.java:505) +chatbot → user 리포지토리: Constructor (com.carecode.domain.chatbot.repository.ChatMessageRepository, com.carecode.domain.chatbot.repository.ChatSessionRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.chatbot.rag.CareKnowledgeRetriever, com.carecode.domain.chatbot.llm.ChatCompletionClient)> has parameter of type in (ChatbotService.java:0) +chatbot → user 리포지토리: Field has type in (ChatbotService.java:0) +chatbot → user 리포지토리: Method calls method in (ChatbotService.java:381) +chatbot → user 리포지토리: Method calls method in (ChatbotService.java:380) +community → user 리포지토리: Constructor (com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.TagRepository, com.carecode.domain.user.repository.UserRepository, org.springframework.security.crypto.password.PasswordEncoder)> has parameter of type in (CommunityInitializationService.java:0) +community → user 리포지토리: Constructor (com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.CommentRepository, com.carecode.domain.community.repository.TagRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.community.repository.PostLikeRepository, com.carecode.domain.community.repository.BookmarkRepository, com.carecode.domain.community.mapper.CommunityMapper, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (CommunityService.java:0) +community → user 리포지토리: Constructor (com.carecode.domain.community.repository.ReportRepository, com.carecode.domain.community.repository.UserBlockRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.domain.community.repository.CommentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (ModerationService.java:0) +community → user 리포지토리: Field has type in (CommunityInitializationService.java:0) +community → user 리포지토리: Field has type in (CommunityService.java:0) +community → user 리포지토리: Field has type in (ModerationService.java:0) +community → user 리포지토리: Method calls method in (CommunityInitializationService.java:55) +community → user 리포지토리: Method calls method in (CommunityInitializationService.java:73) +community → user 리포지토리: Method calls method in (CommunityService.java:602) +community → user 리포지토리: Method calls method in (CommunityService.java:372) +community → user 리포지토리: Method calls method in (CommunityService.java:588) +community → user 리포지토리: Method calls method in (CommunityService.java:544) +community → user 리포지토리: Method calls method in (CommunityService.java:514) +community → user 리포지토리: Method calls method in (ModerationService.java:115) +health → careFacility 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.user.service.ConsentGuard, com.carecode.domain.health.repository.HealthRecordAttachmentRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.health.mapper.HealthRecordMapper, com.carecode.domain.health.mapper.ChildMapper)> has parameter of type in (HealthService.java:0) +health → careFacility 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.careFacility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) +health → careFacility 리포지토리: Field has type in (HealthService.java:0) +health → careFacility 리포지토리: Field has type in (SiblingOverviewService.java:0) +health → careFacility 리포지토리: Method calls method in (HealthService.java:524) +health → careFacility 리포지토리: Method calls method in (SiblingOverviewService.java:91) +health → policy 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.user.service.ConsentGuard, com.carecode.domain.health.repository.HealthRecordAttachmentRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.health.mapper.HealthRecordMapper, com.carecode.domain.health.mapper.ChildMapper)> has parameter of type in (HealthService.java:0) +health → policy 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.careFacility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) +health → policy 리포지토리: Constructor (com.carecode.domain.health.service.ChildService, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.policy.repository.PolicyRepository)> has parameter of type in (ChildTimelineService.java:0) +health → policy 리포지토리: Field has type in (HealthService.java:0) +health → policy 리포지토리: Field has type in (SiblingOverviewService.java:0) +health → policy 리포지토리: Field has type in (ChildTimelineService.java:0) +health → policy 리포지토리: Method calls method in (HealthService.java:520) +health → policy 리포지토리: Method calls method in (SiblingOverviewService.java:101) +health → policy 리포지토리: Method calls method in (ChildTimelineService.java:180) +health → user 리포지토리: Constructor (com.carecode.domain.health.service.HealthService, com.carecode.domain.health.repository.HospitalRepository, com.carecode.core.ops.sync.SyncFreshnessService, com.carecode.domain.health.repository.HospitalLikeRepository, com.carecode.domain.health.repository.HospitalReviewRepository, com.carecode.domain.health.mapper.HospitalMapper, com.carecode.domain.health.mapper.HospitalReviewMapper, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (HealthFacade.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.mapper.ChildMapper, com.carecode.core.security.CurrentUserFacade, com.carecode.domain.health.service.VaccinationScheduleService, com.carecode.core.analytics.EventLogger)> has parameter of type in (ChildService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (GrowthChartService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.user.service.ConsentGuard, com.carecode.domain.health.repository.HealthRecordAttachmentRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.health.mapper.HealthRecordMapper, com.carecode.domain.health.mapper.ChildMapper)> has parameter of type in (HealthService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.user.service.ConsentGuard, com.carecode.domain.health.repository.HealthRecordAttachmentRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.careFacility.repository.CareFacilityRepository, com.carecode.domain.health.mapper.HealthRecordMapper, com.carecode.domain.health.mapper.ChildMapper)> has parameter of type in (HealthService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.careFacility.repository.FacilityWaitlistRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (SiblingOverviewService.java:0) +health → user 리포지토리: Constructor (com.carecode.domain.health.repository.VaccinationScheduleRepository, com.carecode.domain.user.repository.ChildRepository)> has parameter of type in (VaccinationScheduleService.java:0) +health → user 리포지토리: Field has type in (HealthFacade.java:0) +health → user 리포지토리: Field has type in (ChildService.java:0) +health → user 리포지토리: Field has type in (GrowthChartService.java:0) +health → user 리포지토리: Field has type in (HealthService.java:0) +health → user 리포지토리: Field has type in (HealthService.java:0) +health → user 리포지토리: Field has type in (SiblingOverviewService.java:0) +health → user 리포지토리: Field has type in (VaccinationScheduleService.java:0) +health → user 리포지토리: Method calls method in (HealthFacade.java:286) +health → user 리포지토리: Method calls method in (HealthFacade.java:183) +health → user 리포지토리: Method calls method in (ChildService.java:48) +health → user 리포지토리: Method calls method in (ChildService.java:84) +health → user 리포지토리: Method calls method in (ChildService.java:60) +health → user 리포지토리: Method calls method in (ChildService.java:98) +health → user 리포지토리: Method calls method in (ChildService.java:79) +health → user 리포지토리: Method calls method in (GrowthChartService.java:81) +health → user 리포지토리: Method calls method in (HealthService.java:303) +health → user 리포지토리: Method calls method in (HealthService.java:622) +health → user 리포지토리: Method calls method in (HealthService.java:95) +health → user 리포지토리: Method calls method in (HealthService.java:845) +health → user 리포지토리: Method calls method in (HealthService.java:837) +health → user 리포지토리: Method calls method in (HealthService.java:326) +health → user 리포지토리: Method calls method in (HealthService.java:258) +health → user 리포지토리: Method calls method in (HealthService.java:269) +health → user 리포지토리: Method calls method in (HealthService.java:280) +health → user 리포지토리: Method calls method in (HealthService.java:513) +health → user 리포지토리: Method calls method in (HealthService.java:291) +health → user 리포지토리: Method calls method in (SiblingOverviewService.java:44) +health → user 리포지토리: Method calls method in (VaccinationScheduleService.java:32) +notification → user 리포지토리: Constructor (com.carecode.domain.notification.service.NotificationService, com.carecode.domain.notification.service.NotificationPreferenceService, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.domain.notification.repository.NotificationPreferenceRepository)> has parameter of type in (NotificationFacade.java:0) +notification → user 리포지토리: Constructor (com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (NotificationInitializationService.java:0) +notification → user 리포지토리: Constructor (com.carecode.domain.notification.repository.NotificationPreferenceRepository, com.carecode.domain.notification.repository.PushDeviceRepository, com.carecode.domain.user.repository.UserRepository)> has parameter of type in (NotificationPreferenceService.java:0) +notification → user 리포지토리: Constructor (com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.notification.factory.NotificationStrategyFactory, com.carecode.domain.notification.sender.NotificationDispatcher)> has parameter of type in (NotificationService.java:0) +notification → user 리포지토리: Field has type in (NotificationFacade.java:0) +notification → user 리포지토리: Field has type in (NotificationInitializationService.java:0) +notification → user 리포지토리: Field has type in (NotificationPreferenceService.java:0) +notification → user 리포지토리: Field has type in (NotificationService.java:0) +notification → user 리포지토리: Method calls method in (NotificationFacade.java:51) +notification → user 리포지토리: Method calls method in (NotificationFacade.java:209) +notification → user 리포지토리: Method calls method in (NotificationFacade.java:195) +notification → user 리포지토리: Method calls method in (NotificationFacade.java:188) +notification → user 리포지토리: Method calls method in (NotificationFacade.java:202) +notification → user 리포지토리: Method calls method in (NotificationInitializationService.java:36) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:138) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:63) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:43) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:283) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:169) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:83) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:108) +notification → user 리포지토리: Method calls method in (NotificationPreferenceService.java:344) +notification → user 리포지토리: Method calls method in (NotificationService.java:97) +notification → user 리포지토리: Method calls method in (NotificationService.java:196) +notification → user 리포지토리: Method calls method in (NotificationService.java:238) +notification → user 리포지토리: Method calls method in (NotificationService.java:382) +notification → user 리포지토리: Method calls method in (NotificationService.java:56) +notification → user 리포지토리: Method calls method in (NotificationService.java:183) +notification → user 리포지토리: Method calls method in (NotificationService.java:172) +notification → user 리포지토리: Method calls method in (NotificationService.java:313) +notification → user 리포지토리: Method calls method in (NotificationService.java:325) +notification → user 리포지토리: Method calls method in (NotificationService.java:217) +policy → notification 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.BenefitAmountReportRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (BenefitReportSolicitor.java:0) +policy → notification 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyChangeRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (PolicyChangeNotifier.java:0) +policy → notification 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyDeadlineNoticeRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (PolicyDeadlineNotifier.java:0) +policy → notification 리포지토리: Field has type in (BenefitReportSolicitor.java:0) +policy → notification 리포지토리: Field has type in (PolicyChangeNotifier.java:0) +policy → notification 리포지토리: Field has type in (PolicyDeadlineNotifier.java:0) +policy → notification 리포지토리: Method calls method in (BenefitReportSolicitor.java:145) +policy → notification 리포지토리: Method calls method in (PolicyChangeNotifier.java:97) +policy → notification 리포지토리: Method calls method in (PolicyDeadlineNotifier.java:140) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.BenefitAmountReportRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (BenefitReportSolicitor.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.BenefitAmountReportRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (BenefitReportSolicitor.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.core.security.CurrentUserFacade, com.carecode.core.analytics.EventLogger)> has parameter of type in (MissedBenefitService.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyChangeRepository, com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (PolicyChangeNotifier.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyDeadlineNoticeRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (PolicyDeadlineNotifier.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyDeadlineNoticeRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.notification.repository.NotificationRepository, com.carecode.domain.notification.sender.NotificationDispatcher, com.carecode.core.analytics.EventLogger)> has parameter of type in (PolicyDeadlineNotifier.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.policy.mapper.PolicyMapper, com.carecode.core.security.CurrentUserFacade, com.carecode.core.analytics.EventLogger)> has parameter of type in (PolicyRecommendationService.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.policy.repository.PolicyBookmarkRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.policy.mapper.PolicyMapper)> has parameter of type in (PolicyService.java:0) +policy → user 리포지토리: Constructor (com.carecode.domain.policy.repository.PolicyRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.core.security.CurrentUserFacade, com.carecode.core.analytics.EventLogger, com.carecode.core.benefit.BenefitProjectionCalculator)> has parameter of type in (RegionalBenefitComparisonService.java:0) +policy → user 리포지토리: Field has type in (BenefitReportSolicitor.java:0) +policy → user 리포지토리: Field has type in (BenefitReportSolicitor.java:0) +policy → user 리포지토리: Field has type in (MissedBenefitService.java:0) +policy → user 리포지토리: Field has type in (PolicyChangeNotifier.java:0) +policy → user 리포지토리: Field has type in (PolicyDeadlineNotifier.java:0) +policy → user 리포지토리: Field has type in (PolicyDeadlineNotifier.java:0) +policy → user 리포지토리: Field has type in (PolicyRecommendationService.java:0) +policy → user 리포지토리: Field has type in (PolicyService.java:0) +policy → user 리포지토리: Field has type in (RegionalBenefitComparisonService.java:0) +policy → user 리포지토리: Method calls method in (BenefitReportSolicitor.java:108) +policy → user 리포지토리: Method calls method in (BenefitReportSolicitor.java:78) +policy → user 리포지토리: Method calls method in (MissedBenefitService.java:41) +policy → user 리포지토리: Method calls method in (PolicyChangeNotifier.java:117) +policy → user 리포지토리: Method calls method in (PolicyDeadlineNotifier.java:164) +policy → user 리포지토리: Method calls method in (PolicyDeadlineNotifier.java:101) +policy → user 리포지토리: Method calls method in (PolicyRecommendationService.java:47) +policy → user 리포지토리: Method calls method in (PolicyService.java:264) +policy → user 리포지토리: Method calls method in (PolicyService.java:263) +policy → user 리포지토리: Method calls method in (RegionalBenefitComparisonService.java:50) +user → community 리포지토리: Constructor (com.carecode.domain.user.repository.UserConsentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (PrivacyService.java:0) +user → community 리포지토리: Field has type in (PrivacyService.java:0) +user → community 리포지토리: Method calls method in (PrivacyService.java:124) +user → health 리포지토리: Constructor (com.carecode.domain.user.repository.UserConsentRepository, com.carecode.domain.user.repository.UserRepository, com.carecode.domain.user.repository.ChildRepository, com.carecode.domain.health.repository.HealthRecordRepository, com.carecode.domain.community.repository.PostRepository, com.carecode.core.security.CurrentUserFacade)> has parameter of type in (PrivacyService.java:0) +user → health 리포지토리: Field has type in (PrivacyService.java:0) +user → health 리포지토리: Method calls method in (PrivacyService.java:123) diff --git a/archunit-baseline/ab7a1eba-4dd5-499e-95ed-0c71f0b6f61f b/archunit-baseline/ab7a1eba-4dd5-499e-95ed-0c71f0b6f61f new file mode 100644 index 00000000..fb2d5789 --- /dev/null +++ b/archunit-baseline/ab7a1eba-4dd5-499e-95ed-0c71f0b6f61f @@ -0,0 +1,2 @@ +Class is annotated with @RestController in (PublicDataController.java:0) +Class is annotated with @RestController in (CareFacilityApiController.java:0) diff --git a/archunit-baseline/d1434e17-b3a6-4ae9-a8f3-86c5e4bd1c05 b/archunit-baseline/d1434e17-b3a6-4ae9-a8f3-86c5e4bd1c05 new file mode 100644 index 00000000..e69de29b diff --git a/archunit-baseline/fb482b80-693f-4971-aa10-71da548d267d b/archunit-baseline/fb482b80-693f-4971-aa10-71da548d267d new file mode 100644 index 00000000..a91a8512 --- /dev/null +++ b/archunit-baseline/fb482b80-693f-4971-aa10-71da548d267d @@ -0,0 +1,3 @@ +Constructor (com.carecode.domain.community.repository.CommentRepository)> has parameter of type in (CommunityMapper.java:0) +Field has type in (CommunityMapper.java:0) +Method calls method in (CommunityMapper.java:58) diff --git a/archunit-baseline/stored.rules b/archunit-baseline/stored.rules new file mode 100644 index 00000000..4e9db0e5 --- /dev/null +++ b/archunit-baseline/stored.rules @@ -0,0 +1,11 @@ +# +#Mon Oct 05 01:50:57 KST 2026 +no\ classes\ that\ reside\ in\ a\ package\ '..entity..'\ should\ depend\ on\ classes\ that\ reside\ in\ any\ package\ ['..dto..',\ '..controller..'],\ because\ API\ \uBAA8\uC591\uC774\ \uBC14\uB014\ \uB54C\ \uD14C\uC774\uBE14\ \uB9E4\uD551\uC774\ \uD568\uAED8\ \uD754\uB4E4\uB9AC\uBA74\ \uC548\ \uB41C\uB2E4=87360be1-7d6d-4423-b366-16fa4fe6f6b6 +no\ classes\ that\ reside\ in\ a\ package\ 'com.carecode.core..'\ should\ be\ annotated\ with\ @RestController\ or\ should\ be\ annotated\ with\ @Controller,\ because\ API\ \uB294\ \uB3C4\uBA54\uC778\ \uD328\uD0A4\uC9C0\uC5D0\ \uB454\uB2E4.\ core\ \uC5D0\ \uC788\uC73C\uBA74\ \uC18C\uC18D\uC744\ \uC54C\ \uC218\ \uC5C6\uB2E4=ab7a1eba-4dd5-499e-95ed-0c71f0b6f61f +classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..'\ should\ \uC18C\uBB38\uC790\ \uB3C4\uBA54\uC778\ \uD328\uD0A4\uC9C0\uC5D0\ \uC788\uB2E4,\ because\ \uD55C\ \uAD70\uB370\uB9CC\ \uB2E4\uB978\ \uADDC\uCE59\uC774\uBA74\ \uC0C8\uB85C\ \uB4E4\uC5B4\uC628\ \uC0AC\uB78C\uC774\ \uB458\ \uB2E4\ \uD749\uB0B4\ \uB0B8\uB2E4=05a7a88e-9a95-451a-b96f-4a8eb9be6c1e +classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..'\ should\ \uB2E4\uB978\ \uB3C4\uBA54\uC778\uC758\ \uB9AC\uD3EC\uC9C0\uD1A0\uB9AC\uB97C\ \uC9C1\uC811\ \uCC38\uC870\uD558\uC9C0\ \uC54A\uB294\uB2E4,\ because\ \uB0A8\uC758\ \uB3C4\uBA54\uC778\ \uD14C\uC774\uBE14\uC744\ \uC9C1\uC811\ \uC77D\uC73C\uBA74\ \uADF8\ \uB3C4\uBA54\uC778\uC774\ \uC9C0\uD0A4\uB294\ \uADDC\uCE59(\uC18C\uC720\uAD8C\ \uAC80\uC99D\ \uB4F1)\uC744\ \uAC74\uB108\uB6F4\uB2E4=a6b21c55-f3df-4514-8faa-c32628f81448 +no\ classes\ that\ reside\ in\ any\ package\ ['com.carecode.core.security..',\ 'com.carecode.core.web..',\ 'com.carecode.core.handler..',\ 'com.carecode.core.config..',\ 'com.carecode.core.storage..',\ 'com.carecode.core.util..',\ 'com.carecode.core.monitoring..',\ 'com.carecode.core.aspect..',\ 'com.carecode.core.annotation..',\ 'com.carecode.core.constants..',\ 'com.carecode.core.components..',\ 'com.carecode.core.ops..']\ should\ depend\ on\ classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..',\ because\ \uAE30\uBC18\ \uCF54\uB4DC\uAC00\ \uD2B9\uC815\ \uB3C4\uBA54\uC778\uC744\ \uC54C\uBA74\ \uADF8\ \uB3C4\uBA54\uC778\uC758\ \uC77C\uBD80\uC9C0,\ \uACF5\uD1B5\uC774\ \uC544\uB2C8\uB2E4=4d025d0f-f5d7-4e6b-b9f8-e044d0950c24 +no\ classes\ that\ have\ simple\ name\ ending\ with\ 'Mapper'\ should\ depend\ on\ classes\ that\ have\ simple\ name\ ending\ with\ 'Repository',\ because\ \uBCC0\uD658\ \uD55C\ \uBC88\uC5D0\ \uCFFC\uB9AC\uAC00\ \uC228\uC5B4\ \uB4E4\uBA74\ N+1\ \uC744\ \uCD94\uC801\uD560\ \uC218\ \uC5C6\uB2E4=fb482b80-693f-4971-aa10-71da548d267d +no\ classes\ that\ reside\ in\ a\ package\ 'com.carecode.domain..'\ should\ depend\ on\ classes\ that\ have\ fully\ qualified\ name\ 'org.springframework.security.core.context.SecurityContextHolder',\ because\ \uD604\uC7AC\ \uC0AC\uC6A9\uC790\uB294\ CurrentUserFacade\ \uB85C\uB9CC\ \uC5BB\uB294\uB2E4=74b2056c-4f0e-4cb3-af35-0cfdcc359eeb +no\ classes\ that\ have\ simple\ name\ ending\ with\ 'Controller'\ should\ depend\ on\ classes\ that\ have\ simple\ name\ ending\ with\ 'Repository',\ because\ \uAC80\uC99D\uACFC\ \uD2B8\uB79C\uC7AD\uC158\ \uACBD\uACC4\uB294\ \uC11C\uBE44\uC2A4\uC5D0\ \uB454\uB2E4=05a5820d-39ea-4db6-b8e7-f737ac6a90df +no\ classes\ that\ reside\ in\ any\ package\ ['..service..',\ '..repository..']\ should\ depend\ on\ classes\ that\ reside\ in\ a\ package\ '..controller..',\ because\ \uD638\uCD9C\ \uBC29\uD5A5\uC740\ \uCEE8\uD2B8\uB864\uB7EC\ \u2192\ \uC11C\uBE44\uC2A4\ \u2192\ \uB9AC\uD3EC\uC9C0\uD1A0\uB9AC\ \uD55C\ \uCABD\uC774\uB2E4=d1434e17-b3a6-4ae9-a8f3-86c5e4bd1c05 diff --git a/build.gradle b/build.gradle index 2eaf9fb7..941cda60 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,10 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + + // 아키텍처 규칙을 테스트로 고정한다. 지금 상태(위반 목록)를 freeze 해 두고 + // 새 위반만 실패시킨다 — 전부 고칠 때까지 기다리지 않고 바로 걸 수 있다. + testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' testImplementation 'org.testcontainers:junit-jupiter:1.21.3' testImplementation 'org.testcontainers:mariadb:1.21.3' diff --git a/docker-compose.yml b/docker-compose.yml index 0ec383e8..e77ecfec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,8 +91,13 @@ services: PUBLIC_DATA_API_KEY: ${PUBLIC_DATA_API_KEY:-local-public-data-key} EMAIL_VERIFICATION_BASE_URL: ${EMAIL_VERIFICATION_BASE_URL:-http://localhost:8082} - # 프런트(CareCode_FE, 3000번)에서 붙을 수 있게. 로컬에서는 쿠키를 http 로도 주고받아야 한다. - CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000} + # 프런트에서 붙을 수 있게. 로컬에서는 쿠키를 http 로도 주고받아야 한다. + # + # 웹(3000번)에 더해 **앱의 WebView 출처**도 넣는다. 앱은 번들된 파일을 여는데 그때 + # 페이지 출처가 안드로이드는 https://localhost, iOS 는 capacitor://localhost 가 된다. + # 이 값을 여기서 명시하면 CorsPolicy 의 기본값을 덮어쓰므로, 빠뜨리면 앱에서 나가는 + # 모든 요청이 CORS 에서 막힌다 — 앱은 켜지는데 데이터만 안 나오는 상태가 된다. + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000,https://localhost,capacitor://localhost} REFRESH_COOKIE_SECURE: ${REFRESH_COOKIE_SECURE:-false} REFRESH_COOKIE_SAME_SITE: ${REFRESH_COOKIE_SAME_SITE:-Lax} diff --git a/docs/quality/architecture-rules.md b/docs/quality/architecture-rules.md new file mode 100644 index 00000000..c19454a1 --- /dev/null +++ b/docs/quality/architecture-rules.md @@ -0,0 +1,65 @@ +# 구조 규칙 + +> 관련 이슈: #132 + +## 왜 + +이 저장소가 지저분해진 방식은 "로직이 엉킴" 이 아니라 **같은 일을 여러 방식으로 하게 된 것** 입니다. + +| 증상 | 수치 | +|------|------| +| 예외 체계 | `CareServiceException` 96건 · `BusinessException` 51건 · 도메인 전용 94건 | +| `catch (Exception e)` 로 원인 덮어쓰기 | ChatbotService 11회 · HealthService 10회 | +| `Map` 응답 | 컨트롤러 24곳 (OpenAPI 에 타입이 `object` 로만 나간다) | +| 소유권 검증 구현 | 3곳 (`ChildService` · `GrowthChartService` · `HealthService`) | +| 다른 도메인 리포지토리 직접 참조 | `UserRepository` 24곳 · `ChildRepository` 12곳 | +| 거대 서비스 | HealthService 977줄 · UserService 626 · CommunityService 616 | + +한 번 정리해도 **다음 기능에서 다시 섞이면 의미가 없습니다.** 그래서 규칙을 먼저 테스트로 고정했습니다. + +## 방식 — 지금 위반은 baseline 으로 묶는다 + +ArchUnit 의 `FreezingArchRule` 을 씁니다. + +- 현재 위반 목록을 `archunit-baseline/` 에 저장하고 **함께 커밋** 합니다. +- **새 위반만** 빌드를 실패시킵니다. 전부 고칠 때까지 규칙 도입을 미루지 않아도 됩니다. +- 기존 위반을 고치면 목록에서 자동으로 빠집니다. **baseline 줄 수가 리팩터링 진척도** 입니다. + +```bash +./gradlew test --tests '*ArchitectureRulesTest' +``` + +## 규칙과 현재 위반 (2026-10-05 기준) + +| 규칙 | 위반 | 뜻 | +|------|------|-----| +| 다른 도메인의 리포지토리를 직접 쓰지 않는다 | 271 | 남의 테이블을 직접 읽으면 그 도메인의 규칙(소유권 검증 등)을 건너뛴다 | +| 도메인 패키지 이름은 소문자다 | 96 | `careFacility` 하나만 낙타등. 한 군데만 다르면 새로 온 사람이 둘 다 흉내 낸다 | +| 컨트롤러는 리포지토리를 직접 쓰지 않는다 | 66 | 검증·트랜잭션 경계가 컨트롤러로 샌다 | +| core 기반 패키지는 domain 을 의존하지 않는다 | 50 | 기반 코드가 도메인을 알면 공통이 아니다 | +| 매퍼는 리포지토리를 쓰지 않는다 | 3 | 변환 한 번에 숨은 쿼리가 생기면 N+1 을 추적할 수 없다 | +| core 에는 컨트롤러가 없다 | 2 | `core/controller/CareFacilityApiController` 373줄 | +| 도메인은 SecurityContextHolder 를 직접 쓰지 않는다 | 1 | principal 모양이 바뀌면 고칠 곳이 흩어진다(시설 API 500 의 원인) | +| 서비스·리포지토리는 컨트롤러를 의존하지 않는다 | **0** | 층 방향은 이미 지켜지고 있다 | +| 엔티티는 DTO·컨트롤러를 의존하지 않는다 | **0** | 매핑 경계는 지켜지고 있다 | + +마지막 두 줄이 0 인 것이 중요합니다 — **층 구조 자체는 맞습니다.** 문제는 같은 층 안에서 경계를 지키지 않는 쪽입니다. + +## 규칙 대상이 아닌 것 + +`core/scheduler` · `core/client` · `core/devtools` · `core/geocoding` · `core/benefit` 은 +공공데이터 동기화와 배치처럼 **여러 도메인을 조합하는 코드** 입니다. 도메인을 아는 게 본질이라 +"기반은 도메인을 모른다" 규칙의 대상이 아닙니다. + +다만 그 코드가 "공통 기반" 자리에 있는 것이 혼란의 원인입니다. 조합 계층으로 따로 옮기는 것은 +다음 단계에서 다룹니다. + +## 다음 단계 + +| 단계 | 내용 | 효과 | +|------|------|------| +| 1. 자리 정리 | `core` 의 도메인 컨트롤러 이동, 조합 계층 분리, `careFacility`→`facility`, 문서의 `facade`↔코드 `app` 통일 | 위반 96+50+2 감소 | +| 2. 예외 한 체계 | `catch(Exception)` 덮어쓰기 제거, ErrorCode 기반 통합 | 상태 코드 정확해짐 | +| 3. `Map` 응답 → DTO | 24곳. 스펙에 타입이 생겨 프런트 계약 대조가 이 경로까지 본다 | 계약 품질 | +| 4. Child 를 제 자리로 | 엔티티는 `user`, 서비스는 `health` 에 쪼개져 있다 | 결합 1위 해소 | +| 5. 거대 서비스 분해 | HealthService 977줄 | 2·3 과 함께 진행 | diff --git a/src/main/java/com/carecode/domain/notification/entity/PushDevice.java b/src/main/java/com/carecode/domain/notification/entity/PushDevice.java new file mode 100644 index 00000000..4470fcb3 --- /dev/null +++ b/src/main/java/com/carecode/domain/notification/entity/PushDevice.java @@ -0,0 +1,52 @@ +package com.carecode.domain.notification.entity; + +import com.carecode.domain.user.entity.User; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 푸시를 받을 기기 하나. + * + *

전에는 토큰을 알림 설정 행의 한 칸에 두었다. 그래서 웹에서 알림을 켜 둔 사람이 앱에서 + * 다시 켜면 앞의 토큰을 덮어썼고, 한쪽은 아무 말 없이 알림을 받지 못했다. 토큰은 사용자의 + * 성질이 아니라 기기의 성질이라 행을 따로 둔다. + */ +@Entity +@Table(name = "TBL_PUSH_DEVICE") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PushDevice { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID", nullable = false) + private User user; + + /** FCM 등록 토큰. 기기를 지우고 다시 깔면 새 토큰이 나온다. */ + @Column(name = "TOKEN", nullable = false, length = 512, unique = true) + private String token; + + /** WEB / ANDROID / IOS. 발송 경로를 가르지는 않지만 "알림이 안 와요" 를 추적하려면 필요하다. */ + @Column(name = "DEVICE_TYPE", length = 20) + private String deviceType; + + @Column(name = "APP_VERSION", length = 40) + private String appVersion; + + @Column(name = "CREATED_AT", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "UPDATED_AT", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/carecode/domain/notification/repository/PushDeviceRepository.java b/src/main/java/com/carecode/domain/notification/repository/PushDeviceRepository.java new file mode 100644 index 00000000..3d142aaa --- /dev/null +++ b/src/main/java/com/carecode/domain/notification/repository/PushDeviceRepository.java @@ -0,0 +1,21 @@ +package com.carecode.domain.notification.repository; + +import com.carecode.domain.notification.entity.PushDevice; +import com.carecode.domain.user.entity.User; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PushDeviceRepository extends JpaRepository { + + /** 토큰은 전역 유일하다. 기기를 바꿔 로그인하면 주인만 옮겨 간다. */ + Optional findByToken(String token); + + List findByUser(User user); + + /** 발송할 때 쓰는 것은 토큰 문자열뿐이다. 엔티티를 통째로 올리지 않는다. */ + @Query("SELECT d.token FROM PushDevice d WHERE d.user = :user") + List findTokensByUser(@Param("user") User user); +} diff --git a/src/main/java/com/carecode/domain/notification/sender/NotificationDispatcher.java b/src/main/java/com/carecode/domain/notification/sender/NotificationDispatcher.java index 114f51a3..eaed239b 100644 --- a/src/main/java/com/carecode/domain/notification/sender/NotificationDispatcher.java +++ b/src/main/java/com/carecode/domain/notification/sender/NotificationDispatcher.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.LinkedHashSet; +import com.carecode.domain.notification.repository.PushDeviceRepository; /** 알림을 사용자 설정에 맞는 채널로 실제 발송한다. */ @Slf4j @@ -21,10 +23,13 @@ public class NotificationDispatcher { private final Map senders = new EnumMap<>(NotificationChannelType.class); private final NotificationPreferenceRepository preferenceRepository; + private final PushDeviceRepository pushDeviceRepository; public NotificationDispatcher(List senderBeans, - NotificationPreferenceRepository preferenceRepository) { + NotificationPreferenceRepository preferenceRepository, + PushDeviceRepository pushDeviceRepository) { this.preferenceRepository = preferenceRepository; + this.pushDeviceRepository = pushDeviceRepository; for (NotificationSender sender : senderBeans) { senders.put(sender.channel(), sender); } @@ -53,7 +58,7 @@ public boolean dispatch(Notification notification) { .title(notification.getTitle()) .message(notification.getMessage()) .emailAddress(preference != null ? preference.getEmailAddress() : null) - .deviceToken(resolveDeviceToken(recipient, preference)) + .deviceTokens(resolveDeviceTokens(recipient, preference)) .phoneNumber(preference != null ? preference.getPhoneNumber() : null) .build(); @@ -83,21 +88,26 @@ public boolean dispatch(Notification notification) { } /** - * 푸시 대상 디바이스 토큰. + * 푸시를 보낼 기기 토큰 전부. * - * 토큰 등록은 SYSTEM 설정 행에만 쓰는데 발송은 알림 유형별 행을 읽는다. 그래서 해당 유형의 - * 행만 보면 SYSTEM 알림 외에는 토큰이 없어 푸시가 조용히 실패한다. 토큰은 기기의 성질이므로 - * 유형과 무관하게 찾는다. + *

한 사람이 휴대폰과 웹을 함께 쓴다. 토큰을 하나만 고르면 나머지 기기는 아무 말 없이 + * 알림을 받지 못한다. + * + *

두 곳에서 모은다. {@code TBL_PUSH_DEVICE} 가 지금의 저장소이고, 알림 설정 행의 + * {@code deviceToken} 은 그 전에 쓰던 자리다. 예전에 등록해 둔 사용자가 앱을 다시 열어 + * 토큰을 새로 올리기 전까지는 옛 자리의 값으로 계속 받아야 하므로 함께 본다. + * 같은 토큰이 양쪽에 있을 수 있어 중복은 걸러낸다. */ - private String resolveDeviceToken(User recipient, NotificationPreference preference) { + private List resolveDeviceTokens(User recipient, NotificationPreference preference) { + LinkedHashSet tokens = new LinkedHashSet<>(pushDeviceRepository.findTokensByUser(recipient)); + if (preference != null && preference.getDeviceToken() != null && !preference.getDeviceToken().isBlank()) { - return preference.getDeviceToken(); + tokens.add(preference.getDeviceToken()); } + tokens.addAll(preferenceRepository.findDeviceTokensByUser(recipient)); - return preferenceRepository.findDeviceTokensByUser(recipient).stream() - .findFirst() - .orElse(null); + return List.copyOf(tokens); } /** 채널 사용 여부. 사용자 설정이 없으면 인앱과 푸시를 기본으로 켠다. */ diff --git a/src/main/java/com/carecode/domain/notification/sender/NotificationPayload.java b/src/main/java/com/carecode/domain/notification/sender/NotificationPayload.java index 86c531c6..472077a5 100644 --- a/src/main/java/com/carecode/domain/notification/sender/NotificationPayload.java +++ b/src/main/java/com/carecode/domain/notification/sender/NotificationPayload.java @@ -1,6 +1,7 @@ package com.carecode.domain.notification.sender; import com.carecode.domain.user.entity.User; +import java.util.List; import lombok.Builder; import lombok.Getter; @@ -16,8 +17,13 @@ public class NotificationPayload { /** 이메일 수신 주소. 없으면 사용자 계정 이메일을 사용한다. */ private final String emailAddress; - /** 푸시 발송 대상 디바이스 토큰. */ - private final String deviceToken; + /** + * 푸시 발송 대상 토큰들. + * + *

한 사람이 휴대폰과 웹을 함께 쓴다. 하나만 보내면 나머지는 아무 말 없이 알림을 + * 받지 못한다 — 사용자는 "알림이 안 와요" 라고만 말할 수 있고 어느 기기인지 모른다. + */ + private final List deviceTokens; /** SMS 수신 번호. */ private final String phoneNumber; @@ -29,6 +35,10 @@ public String resolveEmailAddress() { return recipient != null ? recipient.getEmail() : null; } + public List resolveDeviceTokens() { + return deviceTokens == null ? List.of() : deviceTokens; + } + public String resolvePhoneNumber() { if (phoneNumber != null && !phoneNumber.isBlank()) { return phoneNumber; diff --git a/src/main/java/com/carecode/domain/notification/sender/PushNotificationSender.java b/src/main/java/com/carecode/domain/notification/sender/PushNotificationSender.java index eabc851f..3dd54556 100644 --- a/src/main/java/com/carecode/domain/notification/sender/PushNotificationSender.java +++ b/src/main/java/com/carecode/domain/notification/sender/PushNotificationSender.java @@ -1,7 +1,11 @@ package com.carecode.domain.notification.sender; +import com.carecode.domain.notification.repository.PushDeviceRepository; import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MessagingErrorCode; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.Nullable; @@ -13,9 +17,12 @@ public class PushNotificationSender implements NotificationSender { private final FirebaseMessaging firebaseMessaging; + private final PushDeviceRepository pushDeviceRepository; - public PushNotificationSender(@Autowired(required = false) @Nullable FirebaseMessaging firebaseMessaging) { + public PushNotificationSender(@Autowired(required = false) @Nullable FirebaseMessaging firebaseMessaging, + PushDeviceRepository pushDeviceRepository) { this.firebaseMessaging = firebaseMessaging; + this.pushDeviceRepository = pushDeviceRepository; } @Override @@ -33,6 +40,17 @@ public String getUnavailableReason() { return isAvailable() ? null : "푸시 발송이 아직 설정되지 않았어요."; } + /** + * 사용자의 모든 기기로 보낸다. + * + *

한 사람이 휴대폰과 웹을 함께 쓴다. 한 대만 보내면 나머지는 아무 말 없이 알림을 받지 + * 못하고, 사용자는 "알림이 안 와요" 라고만 말할 수 있다. + * + *

한 대가 실패해도 나머지는 계속 보낸다. 기기 하나가 앱을 지웠다고 다른 기기까지 + * 알림을 못 받을 이유가 없다. + * + * @return 한 대라도 보냈으면 true + */ @Override public boolean send(NotificationPayload payload) { if (!isAvailable()) { @@ -40,12 +58,20 @@ public boolean send(NotificationPayload payload) { return false; } - String token = payload.getDeviceToken(); - if (token == null || token.isBlank()) { - log.debug("푸시 발송 건너뜀 - 디바이스 토큰 없음"); + List tokens = payload.resolveDeviceTokens(); + if (tokens.isEmpty()) { + log.debug("푸시 발송 건너뜀 - 등록된 기기 없음"); return false; } + boolean anySent = false; + for (String token : tokens) { + anySent |= sendToToken(token, payload); + } + return anySent; + } + + private boolean sendToToken(String token, NotificationPayload payload) { try { Message message = Message.builder() .setToken(token) @@ -57,9 +83,48 @@ public boolean send(NotificationPayload payload) { firebaseMessaging.send(message); return true; + } catch (FirebaseMessagingException e) { + if (isDeadToken(e)) { + forgetToken(token); + } else { + log.error("푸시 발송 실패 - token={}...", mask(token), e); + } + return false; } catch (Exception e) { - log.error("푸시 발송 실패 - token={}...", token.length() > 8 ? token.substring(0, 8) : token, e); + log.error("푸시 발송 실패 - token={}...", mask(token), e); return false; } } + + /** + * FCM 이 "이 토큰은 더 이상 없다" 고 답했는지. + * + *

앱을 지웠거나 데이터를 초기화하면 토큰이 죽는다. 그대로 두면 그 사용자에게 알림을 + * 보낼 때마다 헛호출이 쌓이고, 로그는 실패로 가득 차 진짜 문제를 덮는다. + */ + private boolean isDeadToken(FirebaseMessagingException e) { + MessagingErrorCode code = e.getMessagingErrorCode(); + return code == MessagingErrorCode.UNREGISTERED || code == MessagingErrorCode.INVALID_ARGUMENT; + } + + /** + * 죽은 토큰을 지운다. + * + *

{@code delete(entity)} 를 쓰는 이유: 파생 삭제 질의({@code deleteByToken})는 호출하는 + * 쪽에 트랜잭션이 있어야 한다. 발송은 비동기이고 네트워크 호출 중이라 트랜잭션을 열어 두고 + * 싶지 않다. {@code JpaRepository.delete} 는 그 자체로 트랜잭션 안에서 돈다. + */ + private void forgetToken(String token) { + try { + pushDeviceRepository.findByToken(token).ifPresent(pushDeviceRepository::delete); + log.info("더 이상 쓰이지 않는 푸시 토큰 제거 - token={}...", mask(token)); + } catch (Exception e) { + log.warn("푸시 토큰 제거 실패 - token={}...", mask(token), e); + } + } + + /** 토큰은 그 자체가 발송 권한이다. 로그에 전문을 남기지 않는다. */ + private String mask(String token) { + return token != null && token.length() > 8 ? token.substring(0, 8) : token; + } } diff --git a/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java b/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java index 27524cbd..4b99cbea 100644 --- a/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java +++ b/src/main/java/com/carecode/domain/notification/service/NotificationPreferenceService.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import com.carecode.domain.notification.entity.PushDevice; +import com.carecode.domain.notification.repository.PushDeviceRepository; /** 알림 설정 서비스 클래스 사용자별 알림 설정을 관리 */ @Slf4j @@ -29,6 +31,7 @@ public class NotificationPreferenceService { private final NotificationPreferenceRepository preferenceRepository; + private final PushDeviceRepository pushDeviceRepository; private final UserRepository userRepository; // 사용자별 알림 설정 목록 조회 @@ -304,6 +307,35 @@ public void registerPushToken(String userId, NotificationRegisterPushTokenReques .build(); preferenceRepository.save(newPreference); } + + registerDevice(user, request); + } + + /** + * 이 기기를 사용자의 푸시 대상으로 올린다. + * + *

위에서 알림 설정 행에도 토큰을 쓰지만, 그 칸은 사용자당 하나뿐이라 휴대폰에서 켜면 + * 웹에서 켜 둔 것을 덮어썼다. 기기는 여러 대일 수 있으므로 행을 따로 남긴다. + * (설정 행 쪽은 건드리지 않는다 — 알림 켜짐/꺼짐이 그 행에 달려 있고, 예전에 등록해 둔 + * 사용자가 아직 그 값으로 알림을 받고 있다.) + */ + private void registerDevice(User user, NotificationRegisterPushTokenRequest request) { + LocalDateTime now = LocalDateTime.now(); + + PushDevice device = pushDeviceRepository.findByToken(request.getPushToken()) + .orElseGet(() -> PushDevice.builder() + .token(request.getPushToken()) + .createdAt(now) + .build()); + + // 같은 기기를 다른 사람이 쓰기 시작했을 수 있다(기기를 넘겨주거나 계정을 바꿔 로그인). + // 토큰은 전역 유일하므로 주인만 옮긴다 — 그러지 않으면 이전 사용자에게 알림이 계속 간다. + device.setUser(user); + device.setDeviceType(request.getDeviceType()); + device.setAppVersion(request.getAppVersion()); + device.setUpdatedAt(now); + + pushDeviceRepository.save(device); } // 알림 설정 수정 diff --git a/src/main/resources/db/migration/V21__push_devices.sql b/src/main/resources/db/migration/V21__push_devices.sql new file mode 100644 index 00000000..1d537e3c --- /dev/null +++ b/src/main/resources/db/migration/V21__push_devices.sql @@ -0,0 +1,22 @@ +-- 사용자당 푸시 기기 여러 대. +-- +-- 지금까지 토큰은 TBL_NOTIFICATION_PREFERENCE 의 한 칸(DEVICE_TOKEN)에 들어 있었다. +-- 그래서 웹에서 알림을 켜 둔 사람이 앱에서 다시 켜면 앞의 토큰을 덮어썼고, 한쪽은 조용히 +-- 알림을 받지 못했다. 토큰은 "사용자의 성질" 이 아니라 "기기의 성질" 이라 행을 따로 둔다. +CREATE TABLE TBL_PUSH_DEVICE ( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + USER_ID BIGINT NOT NULL, + -- FCM 등록 토큰. 기기를 지우고 다시 깔면 새 토큰이 나온다. + TOKEN VARCHAR(512) NOT NULL, + -- WEB / ANDROID / IOS. 발송 경로를 가르지는 않지만, "알림이 안 와요" 를 추적하려면 필요하다. + DEVICE_TYPE VARCHAR(20) NULL, + APP_VERSION VARCHAR(40) NULL, + CREATED_AT DATETIME NOT NULL, + UPDATED_AT DATETIME NOT NULL, + -- 같은 토큰이 두 사용자에게 달릴 수 없다. 기기를 바꿔 로그인하면 소유자만 옮겨 간다. + CONSTRAINT UK_PUSH_DEVICE_TOKEN UNIQUE (TOKEN), + CONSTRAINT FK_PUSH_DEVICE_USER FOREIGN KEY (USER_ID) REFERENCES TBL_USER (ID) ON DELETE CASCADE +) COMMENT '사용자별 푸시 기기 토큰'; + +-- 발송할 때마다 사용자로 전부 찾는다. +CREATE INDEX IDX_PUSH_DEVICE_USER ON TBL_PUSH_DEVICE (USER_ID); diff --git a/src/test/java/com/carecode/architecture/ArchitectureRulesTest.java b/src/test/java/com/carecode/architecture/ArchitectureRulesTest.java new file mode 100644 index 00000000..b9995b97 --- /dev/null +++ b/src/test/java/com/carecode/architecture/ArchitectureRulesTest.java @@ -0,0 +1,169 @@ +package com.carecode.architecture; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.freeze.FreezingArchRule; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +/** + * 구조 규칙을 테스트로 고정한다. + * + *

이 프로젝트가 지저분해진 방식은 "로직이 엉킴" 이 아니라 **같은 일을 여러 방식으로 하게 된 것**이다. + * 예외 체계가 셋이고, 소유권 검증이 세 군데 있고, 도메인 컨트롤러가 core 에 있고, 다른 도메인의 + * 리포지토리를 그냥 가져다 쓴다. 한 번 정리해도 다음 기능에서 다시 섞이면 의미가 없다. + * + *

그래서 규칙을 코드로 두고, **지금 있는 위반은 baseline 으로 묶는다**({@link FreezingArchRule}). + * 새 위반만 빌드를 실패시키고, 기존 위반을 고치면 목록에서 자동으로 빠진다. 전부 고칠 때까지 + * 규칙 도입을 미루지 않아도 된다. + * + *

baseline 은 {@code archunit-baseline/} 에 텍스트로 저장되며 함께 커밋한다. + * 줄 수가 줄어드는 것이 리팩터링 진척도다. + */ +@DisplayName("구조 규칙") +class ArchitectureRulesTest { + + private static JavaClasses classes; + + @BeforeAll + static void importClasses() { + classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages("com.carecode"); + } + + /** + * core 기반 패키지(보안·웹·설정·저장소·유틸)는 도메인을 몰라야 한다. + * + *

{@code core/scheduler}·{@code core/client}·{@code core/devtools} 는 공공데이터 동기화와 배치처럼 + * **원래 여러 도메인을 조합하는 코드**다. 도메인을 아는 게 본질이라 이 규칙의 대상이 아니다. + * 다만 그 코드가 "공통 기반" 자리에 있는 것이 혼란의 원인이므로, 조합 계층으로 따로 옮기는 것은 + * 리팩터링 1단계에서 다룬다. + */ + @Test + @DisplayName("core 기반 패키지는 domain 을 의존하지 않는다") + void coreFoundationDoesNotDependOnDomain() { + check(noClasses() + .that().resideInAnyPackage( + "com.carecode.core.security..", + "com.carecode.core.web..", + "com.carecode.core.handler..", + "com.carecode.core.config..", + "com.carecode.core.storage..", + "com.carecode.core.util..", + "com.carecode.core.monitoring..", + "com.carecode.core.aspect..", + "com.carecode.core.annotation..", + "com.carecode.core.constants..", + "com.carecode.core.components..", + "com.carecode.core.ops..") + .should().dependOnClassesThat().resideInAPackage("com.carecode.domain..") + .because("기반 코드가 특정 도메인을 알면 그 도메인의 일부지, 공통이 아니다")); + } + + /** + * core 에 API 가 있으면 "이 기능은 어느 도메인인가" 에 답할 수 없다. + * 실제로 {@code core/controller/CareFacilityApiController} 가 373줄짜리 시설 API 다. + */ + @Test + @DisplayName("core 에는 컨트롤러가 없다") + void noControllersInCore() { + check(noClasses() + .that().resideInAPackage("com.carecode.core..") + .should().beAnnotatedWith("org.springframework.web.bind.annotation.RestController") + .orShould().beAnnotatedWith("org.springframework.stereotype.Controller") + .because("API 는 도메인 패키지에 둔다. core 에 있으면 소속을 알 수 없다")); + } + + /** + * 도메인 사이 결합의 1위가 리포지토리 직접 참조다(UserRepository 24곳, ChildRepository 12곳). + * 남의 테이블을 직접 읽으면 그쪽 규칙(소유권·소프트 삭제 등)을 건너뛰게 된다. + */ + @Test + @DisplayName("다른 도메인의 리포지토리를 직접 쓰지 않는다") + void noCrossDomainRepositoryAccess() { + check(classes() + .that().resideInAPackage("com.carecode.domain..") + .should(DomainBoundaryConditions.notAccessRepositoriesOfOtherDomains()) + .because("남의 도메인 테이블을 직접 읽으면 그 도메인이 지키는 규칙(소유권 검증 등)을 건너뛴다")); + } + + /** 컨트롤러가 리포지토리를 직접 쓰면 검증·트랜잭션 경계가 컨트롤러로 흘러 들어온다. */ + @Test + @DisplayName("컨트롤러는 리포지토리를 직접 쓰지 않는다") + void controllersDoNotUseRepositories() { + check(noClasses() + .that().haveSimpleNameEndingWith("Controller") + .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository") + .because("검증과 트랜잭션 경계는 서비스에 둔다")); + } + + /** 엔티티가 요청·응답 모양을 알면 API 를 바꿀 때마다 테이블 매핑이 흔들린다. */ + @Test + @DisplayName("엔티티는 DTO·컨트롤러를 의존하지 않는다") + void entitiesDoNotDependOnWebLayer() { + check(noClasses() + .that().resideInAPackage("..entity..") + .should().dependOnClassesThat().resideInAnyPackage("..dto..", "..controller..") + .because("API 모양이 바뀔 때 테이블 매핑이 함께 흔들리면 안 된다")); + } + + /** 서비스가 컨트롤러를 알면 호출 방향이 뒤집힌다. */ + @Test + @DisplayName("서비스·리포지토리는 컨트롤러를 의존하지 않는다") + void innerLayersDoNotDependOnControllers() { + check(noClasses() + .that().resideInAnyPackage("..service..", "..repository..") + .should().dependOnClassesThat().resideInAPackage("..controller..") + .because("호출 방향은 컨트롤러 → 서비스 → 리포지토리 한 쪽이다")); + } + + /** + * 현재 사용자를 꺼내는 방법이 하나여야 한다. {@code SecurityContextHolder} 를 도메인에서 직접 열면 + * principal 모양이 바뀔 때마다(실제로 이메일 문자열 principal 때문에 시설 API 전체가 500 이었다) + * 고쳐야 할 곳이 흩어진다. + */ + @Test + @DisplayName("도메인은 SecurityContextHolder 를 직접 쓰지 않는다") + void domainsUseCurrentUserFacade() { + check(noClasses() + .that().resideInAPackage("com.carecode.domain..") + .should().dependOnClassesThat() + .haveFullyQualifiedName("org.springframework.security.core.context.SecurityContextHolder") + .because("현재 사용자는 CurrentUserFacade 로만 얻는다")); + } + + /** 매퍼는 변환만 한다. DB 를 읽기 시작하면 변환 한 번에 쿼리가 몇 개 나가는지 알 수 없다. */ + @Test + @DisplayName("매퍼는 리포지토리를 쓰지 않는다") + void mappersDoNotQuery() { + check(noClasses() + .that().haveSimpleNameEndingWith("Mapper") + .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository") + .because("변환 한 번에 쿼리가 숨어 들면 N+1 을 추적할 수 없다")); + } + + /** 패키지 이름은 소문자 한 단어로 둔다. {@code careFacility} 하나만 낙타등이다. */ + @Test + @DisplayName("도메인 패키지 이름은 소문자다") + void domainPackagesAreLowerCase() { + check(classes() + .that().resideInAPackage("com.carecode.domain..") + .should(DomainBoundaryConditions.resideInLowerCaseDomainPackage()) + .because("한 군데만 다른 규칙이면 새로 들어온 사람이 둘 다 흉내 낸다")); + } + + /** + * 지금 있는 위반은 baseline 으로 묶고 새 위반만 실패시킨다. + * baseline 이 줄어드는 것이 진척도다. + */ + private static void check(ArchRule rule) { + FreezingArchRule.freeze(rule).check(classes); + } +} diff --git a/src/test/java/com/carecode/architecture/DomainBoundaryConditions.java b/src/test/java/com/carecode/architecture/DomainBoundaryConditions.java new file mode 100644 index 00000000..208be64c --- /dev/null +++ b/src/test/java/com/carecode/architecture/DomainBoundaryConditions.java @@ -0,0 +1,68 @@ +package com.carecode.architecture; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; + +/** + * 도메인 경계 조건. ArchUnit 기본 문법으로는 "자기 도메인인지" 를 비교할 수 없어 직접 쓴다 + * (패키지 패턴만으로는 "A 도메인이 B 도메인의 리포지토리를 쓴다" 를 표현하지 못한다). + */ +final class DomainBoundaryConditions { + + private static final String DOMAIN_ROOT = "com.carecode.domain."; + + private DomainBoundaryConditions() { + } + + /** 다른 도메인의 리포지토리를 참조하면 위반. 같은 도메인 안에서는 자유롭다. */ + static ArchCondition notAccessRepositoriesOfOtherDomains() { + return new ArchCondition<>("다른 도메인의 리포지토리를 직접 참조하지 않는다") { + @Override + public void check(JavaClass item, ConditionEvents events) { + String ownDomain = domainOf(item.getPackageName()); + if (ownDomain == null) { + return; + } + for (Dependency dependency : item.getDirectDependenciesFromSelf()) { + String targetPackage = dependency.getTargetClass().getPackageName(); + if (!targetPackage.startsWith(DOMAIN_ROOT) || !targetPackage.contains(".repository")) { + continue; + } + String targetDomain = domainOf(targetPackage); + if (targetDomain == null || targetDomain.equals(ownDomain)) { + continue; + } + events.add(SimpleConditionEvent.violated(item, + ownDomain + " → " + targetDomain + " 리포지토리: " + dependency.getDescription())); + } + } + }; + } + + /** 도메인 패키지 이름이 소문자가 아니면 위반 (careFacility 처럼 한 군데만 다른 규칙을 막는다). */ + static ArchCondition resideInLowerCaseDomainPackage() { + return new ArchCondition<>("소문자 도메인 패키지에 있다") { + @Override + public void check(JavaClass item, ConditionEvents events) { + String domain = domainOf(item.getPackageName()); + if (domain != null && !domain.equals(domain.toLowerCase(java.util.Locale.ROOT))) { + events.add(SimpleConditionEvent.violated(item, + "도메인 패키지 이름이 소문자가 아니다: " + domain + " (" + item.getName() + ")")); + } + } + }; + } + + /** com.carecode.domain.<이름>... 에서 <이름>. 도메인 밖이면 null. */ + private static String domainOf(String packageName) { + if (!packageName.startsWith(DOMAIN_ROOT)) { + return null; + } + String rest = packageName.substring(DOMAIN_ROOT.length()); + int dot = rest.indexOf('.'); + return dot < 0 ? rest : rest.substring(0, dot); + } +} diff --git a/src/test/java/com/carecode/domain/notification/sender/NotificationChannelAvailabilityTest.java b/src/test/java/com/carecode/domain/notification/sender/NotificationChannelAvailabilityTest.java index 4c2a5923..038e0b03 100644 --- a/src/test/java/com/carecode/domain/notification/sender/NotificationChannelAvailabilityTest.java +++ b/src/test/java/com/carecode/domain/notification/sender/NotificationChannelAvailabilityTest.java @@ -15,6 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; +import com.carecode.domain.notification.repository.PushDeviceRepository; /** * 채널 가용 여부 검증. @@ -28,6 +29,9 @@ class NotificationChannelAvailabilityTest { @Mock private NotificationPreferenceRepository preferenceRepository; + @Mock + private PushDeviceRepository pushDeviceRepository; + /** 테스트용 발송기. 실제 발송은 하지 않는다. */ private record StubSender(NotificationChannelType channel, boolean available, String reason) implements NotificationSender { @@ -54,7 +58,7 @@ public boolean send(NotificationPayload payload) { } private NotificationDispatcher dispatcherWith(NotificationSender... senders) { - return new NotificationDispatcher(List.of(senders), preferenceRepository); + return new NotificationDispatcher(List.of(senders), preferenceRepository, pushDeviceRepository); } @Test @@ -128,7 +132,8 @@ private NotificationDispatcher dispatcherWith(NotificationSender... senders) { dispatcher.dispatch(notification); assertThat(push.lastPayload).isNotNull(); - assertThat(push.lastPayload.getDeviceToken()).isEqualTo("token-1"); + // 토큰은 이제 기기마다 하나씩 모아 담긴다. 한 대만 보내면 나머지 기기가 조용히 빠진다. + assertThat(push.lastPayload.getDeviceTokens()).contains("token-1"); } /** 전달받은 발송 요청을 기록만 하는 발송기. */ diff --git a/src/test/java/com/carecode/domain/notification/sender/PushNotificationSenderTest.java b/src/test/java/com/carecode/domain/notification/sender/PushNotificationSenderTest.java new file mode 100644 index 00000000..aa7cf8b8 --- /dev/null +++ b/src/test/java/com/carecode/domain/notification/sender/PushNotificationSenderTest.java @@ -0,0 +1,146 @@ +package com.carecode.domain.notification.sender; + +import com.carecode.domain.notification.entity.PushDevice; +import com.carecode.domain.notification.repository.PushDeviceRepository; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MessagingErrorCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * 푸시 발송. + * + *

한 사람이 휴대폰과 웹을 함께 쓴다. 한 대만 보내면 나머지는 아무 말 없이 알림을 + * 받지 못하고, 사용자는 "알림이 안 와요" 라고만 말할 수 있다 — 어느 기기인지도, 왜인지도 + * 모른다. 그래서 여기가 조용히 틀리기 가장 쉬운 자리다. + */ +@DisplayName("푸시 발송") +class PushNotificationSenderTest { + + private FirebaseMessaging firebaseMessaging; + private PushDeviceRepository pushDeviceRepository; + private PushNotificationSender sender; + + @BeforeEach + void setUp() { + firebaseMessaging = mock(FirebaseMessaging.class); + pushDeviceRepository = mock(PushDeviceRepository.class); + sender = new PushNotificationSender(firebaseMessaging, pushDeviceRepository); + } + + private NotificationPayload payload(String... tokens) { + return NotificationPayload.builder() + .title("새 지원금이 열렸어요") + .message("확인해 보세요") + .deviceTokens(List.of(tokens)) + .build(); + } + + /** + * FCM 예외는 생성자가 공개돼 있지 않아 목으로 만든다. + * + *

이 호출을 {@code when(...).thenThrow(...)} 의 인자 안에서 하면 안 된다 — 목을 만드는 + * 도중에 또 스터빙을 시작하는 꼴이라 Mockito 가 UnfinishedStubbingException 을 던진다. + * 반드시 먼저 만들어 변수에 담아 둔다. + */ + private FirebaseMessagingException messagingException(MessagingErrorCode code) { + FirebaseMessagingException e = mock(FirebaseMessagingException.class); + when(e.getMessagingErrorCode()).thenReturn(code); + return e; + } + + @Nested + @DisplayName("여러 기기") + class ManyDevices { + + @Test + @DisplayName("등록된 기기 전부에 보낸다") + void sendsToEveryDevice() throws Exception { + assertThat(sender.send(payload("token-phone", "token-web"))).isTrue(); + + ArgumentCaptor sent = ArgumentCaptor.forClass(Message.class); + verify(firebaseMessaging, times(2)).send(sent.capture()); + assertThat(sent.getAllValues()).hasSize(2); + } + + @Test + @DisplayName("한 대가 실패해도 나머지는 계속 보낸다") + void keepsGoingAfterOneFailure() throws Exception { + FirebaseMessagingException failure = messagingException(MessagingErrorCode.INTERNAL); + when(firebaseMessaging.send(any())).thenThrow(failure).thenReturn("ok"); + + // 기기 하나가 실패했다고 다른 기기까지 알림을 못 받을 이유가 없다. + assertThat(sender.send(payload("token-broken", "token-ok"))).isTrue(); + verify(firebaseMessaging, times(2)).send(any()); + } + + @Test + @DisplayName("전부 실패하면 false") + void reportsFailureWhenNoneSent() throws Exception { + FirebaseMessagingException failure = messagingException(MessagingErrorCode.INTERNAL); + when(firebaseMessaging.send(any())).thenThrow(failure); + + assertThat(sender.send(payload("a", "b"))).isFalse(); + } + + @Test + @DisplayName("등록된 기기가 없으면 보내지 않는다") + void skipsWhenNoDevice() throws Exception { + assertThat(sender.send(payload())).isFalse(); + + verify(firebaseMessaging, never()).send(any()); + } + } + + @Nested + @DisplayName("죽은 토큰 정리") + class StaleTokens { + + @Test + @DisplayName("FCM 이 없는 토큰이라고 하면 지운다") + void removesUnregisteredToken() throws Exception { + PushDevice device = PushDevice.builder().token("token-dead").build(); + when(pushDeviceRepository.findByToken("token-dead")).thenReturn(Optional.of(device)); + FirebaseMessagingException dead = messagingException(MessagingErrorCode.UNREGISTERED); + when(firebaseMessaging.send(any())).thenThrow(dead); + + sender.send(payload("token-dead")); + + // 두면 그 사용자에게 알림을 보낼 때마다 헛호출이 쌓이고 로그가 실패로 가득 찬다. + verify(pushDeviceRepository).delete(device); + } + + @Test + @DisplayName("일시적 실패로는 지우지 않는다") + void keepsTokenOnTemporaryFailure() throws Exception { + FirebaseMessagingException temporary = messagingException(MessagingErrorCode.UNAVAILABLE); + when(firebaseMessaging.send(any())).thenThrow(temporary); + + sender.send(payload("token-ok")); + + // FCM 이 잠깐 죽은 것뿐인데 지우면 그 기기는 영영 알림을 못 받는다. + verify(pushDeviceRepository, never()).delete(any()); + } + } + + @Test + @DisplayName("FCM 설정이 없으면 아무것도 하지 않는다") + void inactiveWithoutFirebase() throws Exception { + PushNotificationSender disabled = new PushNotificationSender(null, pushDeviceRepository); + + assertThat(disabled.isAvailable()).isFalse(); + assertThat(disabled.send(payload("token"))).isFalse(); + } +} diff --git a/src/test/resources/archunit.properties b/src/test/resources/archunit.properties new file mode 100644 index 00000000..90756655 --- /dev/null +++ b/src/test/resources/archunit.properties @@ -0,0 +1,6 @@ +# 위반 목록(baseline)을 저장소에 둔다. 새로 생긴 위반만 테스트가 실패시킨다. +# 처음 한 번은 디렉터리를 만들어야 하므로 생성을 허용한다. +freeze.store.default.allowStoreCreation=true +# 기존 위반이 사라지면 목록에서 함께 지운다. 고친 것이 다시 늘어나지 않게 한다. +freeze.refreeze=false +freeze.store.default.path=archunit-baseline