diff --git a/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 b/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 index 59ef0a7..5e50680 100644 --- a/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 +++ b/archunit-baseline/a6b21c55-f3df-4514-8faa-c32628f81448 @@ -178,11 +178,9 @@ health → user 리포지토리: Method calls method in (ChildService.java:106) health → user 리포지토리: Method calls method in (ChildService.java:81) health → user 리포지토리: Method calls method in (GrowthChartService.java:85) -health → user 리포지토리: Method calls method in (HealthService.java:311) health → user 리포지토리: Method calls method in (HealthService.java:99) health → user 리포지토리: Method calls method in (HealthService.java:879) health → user 리포지토리: Method calls method in (HealthService.java:871) -health → user 리포지토리: Method calls method in (HealthService.java:334) health → user 리포지토리: Method calls method in (HealthService.java:266) health → user 리포지토리: Method calls method in (HealthService.java:277) health → user 리포지토리: Method calls method in (HealthService.java:288) diff --git a/docs/quality/architecture-rules.md b/docs/quality/architecture-rules.md index a0c329f..d82bb81 100644 --- a/docs/quality/architecture-rules.md +++ b/docs/quality/architecture-rules.md @@ -31,7 +31,7 @@ ArchUnit 의 `FreezingArchRule` 을 씁니다. ## 규칙과 현재 위반 -진척: **489건(0단계) → 391건(1b) → 383건(3a) → 364건(4a)**. 남은 것은 도메인 경계(249)와 +진척: **489건(0단계) → 391건(1b) → 383건(3a) → 364건(4a) → 362건(5a)**. 남은 것은 도메인 경계(247)와 컨트롤러의 리포지토리 직접 사용(61), core 기반의 도메인 참조(50)입니다. > 세는 법: `archunit-baseline/` 에는 규칙별 위반 파일 외에 색인 파일 `stored.rules` 도 있습니다. @@ -39,7 +39,7 @@ ArchUnit 의 `FreezingArchRule` 을 씁니다. | 규칙 | 위반 | 뜻 | |------|------|-----| -| 다른 도메인의 리포지토리를 직접 쓰지 않는다 | 249 | 남의 테이블을 직접 읽으면 그 도메인의 규칙(소유권 검증 등)을 건너뛴다 | +| 다른 도메인의 리포지토리를 직접 쓰지 않는다 | 247 | 남의 테이블을 직접 읽으면 그 도메인의 규칙(소유권 검증 등)을 건너뛴다 | | 도메인 패키지 이름은 소문자다 | **0** | `careFacility`→`facility` 로 해소 (1b 단계) | | 컨트롤러는 리포지토리를 직접 쓰지 않는다 | 61 | 검증·트랜잭션 경계가 컨트롤러로 샌다 | | core 기반 패키지는 domain 을 의존하지 않는다 | 50 | 기반 코드가 도메인을 알면 공통이 아니다 | @@ -228,6 +228,47 @@ public interface ChildDirectory { 남아 있습니다(4b) - 전체 테스트 **624개, 실패 0**. OpenAPI 스펙은 변화 없음 — 응답 모양을 바꾸지 않았습니다 +## 분해 전에 지울 것이 먼저였다 (5a 단계) + +거대 서비스를 쪼개려고 `HealthService` 1011줄을 책임별로 나누기 시작했는데, 나누다 보니 +**상당 부분이 아무도 부르지 않는 코드**였습니다. 부르는 곳이 없는 코드를 옮기는 건 옮긴 만큼만 +일이 늘어납니다. + +컨트롤러에서 거꾸로 따라가 도달 가능한지 확인했습니다. + +| 지운 것 | 왜 | +|---------|-----| +| `analyzeHealthStatus`, `generateHealthReport` | 파사드에는 있는데 컨트롤러가 부르지 않는다. `healthScore`·`riskLevel` 같은 값을 만들어 내는데 공개된 적이 없다 | +| `getHealthGoals` | 고정 문자열("모든 예방접종 완료")을 돌려준다. 도달 불가 | +| `getHealthChart` | 도달 불가. 성장 곡선은 `GrowthChartService` 가 따로 한다 | +| `checkSystemHealth` | `{status: UP, version: 1.0.0}` 을 하드코딩. **Actuator 의 `/actuator/health` 와 중복**이고 도달 불가 | +| `getHealthRecords`, `getHealthRecordsByDateRange` | 파사드에도 없다 | +| 위 메서드들만 쓰던 private 헬퍼 11개 | `calculateHealthScore`, `determineRiskLevel`, `generateRecommendations`, `calculateVaccineStatus`, `calculateCheckupStatus`, `calculateProgress`, `generateHealthSummary`, `extractChartValue`, `validateChartType`, `validateMonths`, `validatePaginationParams` | +| 상수 3개 | `HEALTH_SCORE_HIGH_THRESHOLD` 등. 점수 계산이 사라지며 함께 쓸 곳이 없어졌다 | + +`CareFacilityService` 에서도 같은 것이 나왔습니다. `saveCareFacilitiesFromPublicData`, +`countCareFacilities` 와 그 헬퍼 5개는 **3a 에서 지운 `CareFacilityApiController` 가 쓰던 잔재**였습니다. +입구를 지우면 그 뒤에 남는 코드가 생기는데, 그때 같이 지우지 않으면 "쓰이는 코드" 처럼 보입니다. + +### 숫자 + +| 파일 | 전 | 후 | +|------|-----|-----| +| `HealthService` | 1011줄 | **715줄** | +| `CareFacilityService` | 580줄 | **411줄** | +| `HealthFacade` | 338줄 | **309줄** | + +- ArchUnit baseline 364 → **362** +- 전체 테스트 **632개, 실패 0** +- **OpenAPI 스펙 변화 없음** — 공개 API 를 하나도 건드리지 않았습니다. 지운 것은 전부 밖에서 + 부를 수 없던 코드입니다 + +### 남은 분해 (5b) + +`HealthService` 715줄은 아직 다섯 가지를 함께 들고 있습니다 — 건강 기록 CRUD, 아이 조회 +(연령·성별·특별 요구·이름 검색), 접종·검진 일정, 알림, 통계·추천. 이 중 **아이 조회 4개는 +`user` 도메인 일**이라 4a 의 입구로 옮기는 것이 다음 자리입니다. + ## 다음 단계 | 단계 | 내용 | 효과 | @@ -239,4 +280,5 @@ public interface ChildDirectory { | ~~3b. `Map` 응답 → DTO~~ | ~~남은 17곳~~ | **완료** — 스펙 스키마 134 → 158 | | ~~4a. 자녀 조회 입구~~ | ~~policy·facility 가 `ChildRepository` 를 직접 쓰고 소유권 검증이 3중~~ | **완료** — 383 → 364, 404 로 통일 | | 4b. Child 를 제 자리로 | `ChildService` 가 `health` 에 있다. health 는 아직 엔티티가 필요해 `ChildRepository` 를 쓴다 | 결합 추가 해소 | -| 5. 거대 서비스 분해 | HealthService 977줄 | 2·3 과 함께 진행 | +| ~~5a. 도달 불가 코드 제거~~ | ~~HealthService 1011줄, CareFacilityService 580줄~~ | **완료** — 715줄·411줄 | +| 5b. 남은 책임 분리 | HealthService 는 아직 기록·아이 조회·일정·알림·통계를 함께 들고 있다 | 읽기 쉬움 | diff --git a/src/main/java/com/carecode/domain/facility/service/CareFacilityService.java b/src/main/java/com/carecode/domain/facility/service/CareFacilityService.java index 3a94b2e..a56e070 100644 --- a/src/main/java/com/carecode/domain/facility/service/CareFacilityService.java +++ b/src/main/java/com/carecode/domain/facility/service/CareFacilityService.java @@ -52,170 +52,6 @@ public class CareFacilityService { private final CareFacilityMapper careFacilityMapper; private final FullTextSearchSupport fullTextSearchSupport; - // 공공데이터 API에서 받아온 보육시설 데이터를 DB에 저장 - @Transactional - public void saveCareFacilitiesFromPublicData(Map publicData) { - List> facilities = (List>) publicData.get("facilities"); - - if (facilities == null || facilities.isEmpty()) { - log.warn("저장할 보육시설 데이터가 없습니다."); - return; - } - - int savedCount = 0; - int updatedCount = 0; - int errorCount = 0; - - for (Map facilityData : facilities) { - try { - String facilityId = (String) facilityData.get("facilityId"); - - // 기존 시설이 있는지 확인 - Optional existingFacilityOpt = careFacilityRepository.findByFacilityCode(facilityId); - - if (existingFacilityOpt.isPresent()) { - // 기존 시설 업데이트 - CareFacility existingFacility = existingFacilityOpt.get(); - updateCareFacilityFromPublicData(existingFacility, facilityData); - updatedCount++; - log.debug("보육시설 업데이트: {}", facilityId); - } else { - // 새 시설 생성 - CareFacility newFacility = createCareFacilityFromPublicData(facilityData); - careFacilityRepository.save(newFacility); - savedCount++; - log.debug("보육시설 신규 저장: {}", facilityId); - } - - } catch (Exception e) { - errorCount++; - log.error("보육시설 데이터 저장 중 오류 발생: {}", e.getMessage(), e); - } - } - - log.info("공공데이터 보육시설 저장 완료: 신규={}, 업데이트={}, 오류={}", - savedCount, updatedCount, errorCount); - - } - - // 공공데이터로부터 새로운 CareFacility 엔티티 생성 - private CareFacility createCareFacilityFromPublicData(Map facilityData) { - return CareFacility.builder() - .facilityCode((String) facilityData.get("facilityId")) - .name((String) facilityData.get("facilityName")) - .facilityType(mapServiceTypeToFacilityType((String) facilityData.get("serviceType"))) - .city("서울특별시") // 서울시 API이므로 고정 - .district((String) facilityData.get("district")) - .address((String) facilityData.get("address")) - .latitude(CommonUtil.parseDouble(facilityData.get("latitude"))) - .longitude(CommonUtil.parseDouble(facilityData.get("longitude"))) - .website((String) facilityData.get("websiteUrl")) - .ageRange((String) facilityData.get("ageGroup")) - .operatingHours(formatOperatingHours(facilityData)) - .tuitionFee(CommonUtil.parseInteger(facilityData.get("rentFee"))) - .isActive(true) - .isPublic(true) // 공공데이터는 주로 공립 시설 - .subsidyAvailable((Boolean) facilityData.get("isFree")) - .description(generateDescription(facilityData)) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); - - } - - // 기존 CareFacility 엔티티를 공공데이터로 업데이트 - private void updateCareFacilityFromPublicData(CareFacility existingFacility, Map facilityData) { - existingFacility.setName((String) facilityData.get("facilityName")); - existingFacility.setFacilityType(mapServiceTypeToFacilityType((String) facilityData.get("serviceType"))); - existingFacility.setDistrict((String) facilityData.get("district")); - existingFacility.setAddress((String) facilityData.get("address")); - existingFacility.setLatitude(CommonUtil.parseDouble(facilityData.get("latitude"))); - existingFacility.setLongitude(CommonUtil.parseDouble(facilityData.get("longitude"))); - existingFacility.setWebsite((String) facilityData.get("websiteUrl")); - existingFacility.setAgeRange((String) facilityData.get("ageGroup")); - existingFacility.setOperatingHours(formatOperatingHours(facilityData)); - existingFacility.setTuitionFee(CommonUtil.parseInteger(facilityData.get("rentFee"))); - existingFacility.setSubsidyAvailable((Boolean) facilityData.get("isFree")); - existingFacility.setDescription(generateDescription(facilityData)); - existingFacility.setUpdatedAt(LocalDateTime.now()); - - careFacilityRepository.save(existingFacility); - } - - // 서비스 타입을 FacilityType으로 매핑 - private FacilityType mapServiceTypeToFacilityType(String serviceType) { - if (serviceType == null) { - return FacilityType.OTHER; - } - - switch (serviceType) { - case "공동육아나눔터": - return FacilityType.PLAYGROUP; - case "어린이집": - return FacilityType.DAYCARE; - case "유치원": - return FacilityType.KINDERGARTEN; - case "방과후교실": - return FacilityType.NURSERY; - default: - return FacilityType.OTHER; - } - } - - // 운영시간 정보 포맷팅 - private String formatOperatingHours(Map facilityData) { - StringBuilder hours = new StringBuilder(); - - String weekdayStart = (String) facilityData.get("weekdayStartTime"); - String weekdayEnd = (String) facilityData.get("weekdayEndTime"); - String operationType = (String) facilityData.get("operationType"); - Boolean hasSaturday = (Boolean) facilityData.get("hasSaturdayOperation"); - String saturdayStart = (String) facilityData.get("saturdayStartTime"); - String saturdayEnd = (String) facilityData.get("saturdayEndTime"); - - if (weekdayStart != null && weekdayEnd != null) { - hours.append("평일: ").append(weekdayStart).append("~").append(weekdayEnd); - } - - if (operationType != null && !operationType.isEmpty()) { - hours.append(" (").append(operationType).append(")"); - } - - if (hasSaturday != null && hasSaturday && saturdayStart != null && saturdayEnd != null) { - hours.append(", 토요일: ").append(saturdayStart).append("~").append(saturdayEnd); - } - - return hours.toString(); - } - - // 시설 설명 생성 - private String generateDescription(Map facilityData) { - StringBuilder description = new StringBuilder(); - - String serviceType = (String) facilityData.get("serviceType"); - String ageGroup = (String) facilityData.get("ageGroup"); - String district = (String) facilityData.get("district"); - Boolean isFree = (Boolean) facilityData.get("isFree"); - - if (serviceType != null) { - description.append(serviceType); - } - - if (ageGroup != null) { - description.append(" - ").append(ageGroup); - } - - if (district != null) { - description.append(" (").append(district).append(")"); - } - - if (isFree != null && isFree) { - description.append(" - 무료 이용 가능"); - } - - return description.toString(); - } - // 돌봄 시설 목록 조회 /** 시설 목록 조회. 테이블 전체를 메모리로 올리지 않도록 항상 페이지 단위로 읽는다. */ @@ -228,11 +64,6 @@ public List getAllCareFacilities(int page, int size) { .collect(Collectors.toList()); } - // 전체 시설 수 - public long countCareFacilities() { - return careFacilityRepository.count(); - } - // 돌봄 시설 상세 조회 @LogExecutionTime @Cacheable(cacheNames = "careFacility", key = "#facilityId") diff --git a/src/main/java/com/carecode/domain/health/app/HealthFacade.java b/src/main/java/com/carecode/domain/health/app/HealthFacade.java index 81cc53d..db32d52 100644 --- a/src/main/java/com/carecode/domain/health/app/HealthFacade.java +++ b/src/main/java/com/carecode/domain/health/app/HealthFacade.java @@ -122,34 +122,10 @@ public List searchChi return healthService.searchChildrenByName(userId, name); } - // ==================== - // 건강 분석 및 리포트 ==================== - public Map analyzeHealthStatus(HealthCreateHealthRecordRequest request, Long actorUserId) { - return healthService.analyzeHealthStatus(request, actorUserId); - } - - public Map generateHealthReport(HealthCreateHealthRecordRequest request, Long actorUserId) { - return healthService.generateHealthReport(request, actorUserId); - } - - public Map getHealthGoals(String userId, Long actorUserId) { - return healthService.getHealthGoals(userId, actorUserId); - } - public HealthRecommendationResponse getIntegratedRecommendations(String userId, Long actorUserId) { return healthService.getIntegratedRecommendations(userId, actorUserId); } - public List> getHealthChart(String userId, String type, String from, String to, Long actorUserId) { - LocalDate fromDate = from != null ? LocalDate.parse(from) : null; - LocalDate toDate = to != null ? LocalDate.parse(to) : null; - return healthService.getHealthChart(userId, type, fromDate, toDate, actorUserId); - } - - public Map checkSystemHealth() { - return healthService.checkSystemHealth(); - } - // ==================== 병원 관리 ==================== // 병원 관련 작업은 Facade에서 직접 처리하므로 트랜잭션 필요 하지만 public List getAllHospitals(int page, int size) { diff --git a/src/main/java/com/carecode/domain/health/service/HealthService.java b/src/main/java/com/carecode/domain/health/service/HealthService.java index 2c69898..9cbb659 100644 --- a/src/main/java/com/carecode/domain/health/service/HealthService.java +++ b/src/main/java/com/carecode/domain/health/service/HealthService.java @@ -65,10 +65,7 @@ public class HealthService { // 상수 정의 private static final String DEFAULT_ALERT_PRIORITY = "MEDIUM"; - private static final int DEFAULT_MONTHS_FOR_NEXT_CHECKUP = 3; private static final int MAX_UPCOMING_EVENTS = 5; - private static final int HEALTH_SCORE_HIGH_THRESHOLD = 80; - private static final int HEALTH_SCORE_MEDIUM_THRESHOLD = 60; private final HealthRecordRepository healthRecordRepository; private final VaccinationScheduleRepository vaccinationScheduleRepository; @@ -221,41 +218,6 @@ public void deleteHealthRecord(Long recordId, Long actorUserId) { log.info("건강 기록이 삭제되었습니다: 기록ID={}", recordId); } - // 건강 기록 목록 조회 (페이징) - @LogExecutionTime - public List getHealthRecords(Long childId, int page, int size, Long actorUserId) { - validateChildId(childId); - validatePaginationParams(page, size); - assertChildOwnedByUserId(childId, actorUserId); - - log.info("건강 기록 목록 조회 - 아동 ID: {}, 페이지: {}, 크기: {}", childId, page, size); - - Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "recordDate")); - Page records = healthRecordRepository.findByChildIdOrderByRecordDateDesc(childId, pageable); - - return records.getContent().stream() - .map(healthRecordMapper::toResponse) - .collect(Collectors.toList()); - } - - // 기간별 건강 기록 조회 JOIN FETCH를 사용하여 N+1 쿼리 문제 해결 - @LogExecutionTime - public List getHealthRecordsByDateRange(Long childId, LocalDate startDate, LocalDate endDate, Long actorUserId) { - validateChildId(childId); - validateDateRange(startDate, endDate); - assertChildOwnedByUserId(childId, actorUserId); - - log.info("기간별 건강 기록 조회 - 아동 ID: {}, 시작일: {}, 종료일: {}", childId, startDate, endDate); - - // JOIN FETCH를 사용하여 Child와 User를 한 번에 조회 - List records = healthRecordRepository.findByChildIdAndRecordDateBetweenWithChildAndUser( - childId, startDate, endDate); - - return records.stream() - .map(healthRecordMapper::toResponse) - .collect(Collectors.toList()); - } - // ===== 아동 정보 관리 ===== // 연령 범위별 자녀 조회 @@ -302,52 +264,6 @@ public List searchChildrenByName(Long userId, String name) { .collect(Collectors.toList()); } - // 건강 상태 분석 - @LogExecutionTime - public Map analyzeHealthStatus(HealthCreateHealthRecordRequest request, Long actorUserId) { - validateRequest(request); - - Long childId = Long.valueOf(request.getChildId()); - Child child = childRepository.findById(childId) - .orElseThrow(() -> new ResourceNotFoundException("아동을 찾을 수 없습니다: " + request.getChildId())); - assertChildOwnedByUser(child, actorUserId); - - List records = healthRecordRepository.findByChildOrderByRecordDateDesc(child); - - Map analysis = new HashMap<>(); - analysis.put("childId", request.getChildId()); - analysis.put("totalRecords", records.size()); - analysis.put("healthScore", calculateHealthScore(records)); - analysis.put("riskLevel", determineRiskLevel(records)); - analysis.put("recommendations", generateRecommendations(records)); - analysis.put("nextCheckup", LocalDateTime.now().plusMonths(DEFAULT_MONTHS_FOR_NEXT_CHECKUP).toString()); - - return analysis; - } - - // 건강 리포트 생성 - @LogExecutionTime - public Map generateHealthReport(HealthCreateHealthRecordRequest request, Long actorUserId) { - validateRequest(request); - - Long childId = Long.valueOf(request.getChildId()); - Child child = childRepository.findById(childId) - .orElseThrow(() -> new ResourceNotFoundException("아동을 찾을 수 없습니다: " + request.getChildId())); - assertChildOwnedByUser(child, actorUserId); - - List records = healthRecordRepository.findByChildOrderByRecordDateDesc(child); - - Map report = new HashMap<>(); - report.put("childId", request.getChildId()); - report.put("reportDate", LocalDateTime.now().toString()); - report.put("summary", generateHealthSummary(records)); - report.put("vaccineStatus", calculateVaccineStatus(records)); - report.put("checkupStatus", calculateCheckupStatus(records)); - report.put("recommendations", generateRecommendations(records)); - - return report; - } - // 건강 통계 조회 @LogExecutionTime public HealthStatsResponse getHealthStatistics(String userId, Long actorUserId) { @@ -566,69 +482,10 @@ public HealthRecommendationResponse getIntegratedRecommendations(String userId, "아이 연령에 맞는 정책/시설을 확인해보세요."); } - // 건강 목표 조회 - @LogExecutionTime - public Map getHealthGoals(String userId, Long actorUserId) { - validateUserId(userId); - assertUserIdBelongsToActor(userId, actorUserId); - User user = findUserByIdOrUserId(userId); - - List records = healthRecordRepository.findByUserOrderByRecordDateDesc(user); - - Map goals = new HashMap<>(); - goals.put("userId", userId); - goals.put("vaccineGoal", "모든 예방접종 완료"); - goals.put("checkupGoal", "정기 검진 100% 완료"); - // 영양은 목표만 제시하고 달성률은 내지 않는다. 섭취를 기록하는 수단이 없다. - goals.put("nutritionGoal", "균형 잡힌 영양 섭취"); - goals.put("progress", calculateProgress(records)); - - return goals; - } - // ===== 차트 및 시각화 ===== - // 건강 차트 데이터 조회 - @LogExecutionTime - public List> getHealthChart(String userId, String type, LocalDate from, LocalDate to, Long actorUserId) { - validateUserId(userId); - validateChartType(type); - validateDateRange(from, to); - assertUserIdBelongsToActor(userId, actorUserId); - - User user = findUserByIdOrUserId(userId); - List records = healthRecordRepository.findByUserOrderByRecordDateDesc(user); - - return records.stream() - .filter(r -> r.getRecordDate() != null) - .filter(r -> { - LocalDate localDate = r.getRecordDate(); - return (from == null || !localDate.isBefore(from)) && (to == null || !localDate.isAfter(to)); - }) - .map(r -> { - Object value = extractChartValue(r, type); - return Map.of( - "date", r.getRecordDate().toString(), - "value", value != null ? value : "" - ); - }) - .collect(Collectors.toList()); - } - // ===== 시스템 관리 ===== - // 시스템 상태 확인 - public Map checkSystemHealth() { - log.info("시스템 상태 확인"); - - Map healthStatus = new HashMap<>(); - healthStatus.put("status", "UP"); - healthStatus.put("timestamp", System.currentTimeMillis()); - healthStatus.put("version", "1.0.0"); - - return healthStatus; - } - private void assertHealthRecordOwnedByUser(HealthRecord record, Long actorUserId) { if (actorUserId == null) { throw new BusinessException(ErrorCode.UNAUTHORIZED, "인증이 필요합니다."); @@ -713,51 +570,6 @@ private HealthAlertResponse convertToHealthAlertResponse(HealthRecord record) { .build(); } - // ===== 계산 및 분석 Helper Methods ===== - private int calculateHealthScore(List records) { - if (records.isEmpty()) return 0; - - int completedVaccines = countCompletedVaccines(records); - int totalVaccines = (int) records.stream() - .filter(r -> r.getRecordType() == HealthRecord.RecordType.VACCINATION) - .count(); - - return totalVaccines > 0 ? (completedVaccines * 100) / totalVaccines : 0; - } - - private String determineRiskLevel(List records) { - if (records.isEmpty()) return "UNKNOWN"; - - int healthScore = calculateHealthScore(records); - if (healthScore >= HEALTH_SCORE_HIGH_THRESHOLD) return "LOW"; - else if (healthScore >= HEALTH_SCORE_MEDIUM_THRESHOLD) return "MEDIUM"; - else return "HIGH"; - } - - private List generateRecommendations(List records) { - List recommendations = new java.util.ArrayList<>(); - - if (records.isEmpty()) { - recommendations.add("첫 건강 기록을 등록해주세요"); - return recommendations; - } - - int completedVaccines = countCompletedVaccines(records); - int totalVaccines = (int) records.stream() - .filter(r -> r.getRecordType() == HealthRecord.RecordType.VACCINATION) - .count(); - - if (completedVaccines < totalVaccines) { - recommendations.add("예방접종 완료를 권장합니다"); - } - - if (records.stream().noneMatch(r -> r.getRecordType() == HealthRecord.RecordType.CHECKUP)) { - recommendations.add("정기 건강검진을 권장합니다"); - } - - return recommendations; - } - private int countCompletedVaccines(List records) { return (int) records.stream() .filter(r -> r.getRecordType() == HealthRecord.RecordType.VACCINATION && Boolean.TRUE.equals(r.getIsCompleted())) @@ -798,65 +610,6 @@ private List generateUpcomingEvents(List records) { .collect(Collectors.toList()); } - private String generateHealthSummary(List records) { - if (records.isEmpty()) return "건강 기록이 없습니다."; - - int totalRecords = records.size(); - int completedRecords = (int) records.stream() - .filter(r -> Boolean.TRUE.equals(r.getIsCompleted())) - .count(); - - return String.format("총 %d개의 건강 기록 중 %d개 완료 (완료율: %d%%)", - totalRecords, completedRecords, totalRecords > 0 ? (completedRecords * 100) / totalRecords : 0); - } - - private String calculateVaccineStatus(List records) { - int completedVaccines = countCompletedVaccines(records); - int totalVaccines = (int) records.stream() - .filter(r -> r.getRecordType() == HealthRecord.RecordType.VACCINATION) - .count(); - - return totalVaccines > 0 ? - String.format("완료율 %d%%", (completedVaccines * 100) / totalVaccines) : - "예방접종 기록 없음"; - } - - private String calculateCheckupStatus(List records) { - int completedCheckups = countCompletedCheckups(records); - int totalCheckups = (int) records.stream() - .filter(r -> r.getRecordType() == HealthRecord.RecordType.CHECKUP) - .count(); - - return totalCheckups > 0 ? - String.format("완료율 %d%%", (completedCheckups * 100) / totalCheckups) : - "검진 기록 없음"; - } - - /** - * 목표별 달성률. 계산할 근거가 없는 항목은 넣지 않는다. - * - *

영양은 목표 문구만 있고 섭취를 기록하는 수단이 없다. 예전에는 여기서 85 를 돌려주어 - * 모든 사용자가 자기 아이의 영양 상태를 85% 로 봤다. 근거 없는 숫자는 없는 것보다 나쁘다. - */ - private Map calculateProgress(List records) { - Map progress = new HashMap<>(); - - int completedVaccines = countCompletedVaccines(records); - int totalVaccines = (int) records.stream() - .filter(r -> r.getRecordType() == HealthRecord.RecordType.VACCINATION) - .count(); - - int completedCheckups = countCompletedCheckups(records); - int totalCheckups = (int) records.stream() - .filter(r -> r.getRecordType() == HealthRecord.RecordType.CHECKUP) - .count(); - - progress.put("vaccine", totalVaccines > 0 ? (completedVaccines * 100) / totalVaccines : 0); - progress.put("checkup", totalCheckups > 0 ? (completedCheckups * 100) / totalCheckups : 0); - - return progress; - } - // HealthRecord 엔티티를 HealthRecordResponse DTO로 변환 // HealthRecord 매핑은 HealthRecordMapper 사용 @@ -923,18 +676,6 @@ private void validateUserId(String userId) { } } - // 페이징 파라미터 검증 - private void validatePaginationParams(int page, int size) { - if (page < 0) { - throw new BusinessException(ErrorCode.INVALID_INPUT, - "페이지 번호는 0 이상이어야 합니다: " + page); - } - if (size <= 0 || size > 100) { - throw new BusinessException(ErrorCode.INVALID_INPUT, - "페이지 크기는 1 이상 100 이하여야 합니다: " + size); - } - } - // 날짜 범위 검증 private void validateDateRange(LocalDate startDate, LocalDate endDate) { if (startDate != null && endDate != null && startDate.isAfter(endDate)) { @@ -942,43 +683,6 @@ private void validateDateRange(LocalDate startDate, LocalDate endDate) { } } - // 개월 수 검증 - private void validateMonths(int months) { - if (months <= 0 || months > 120) { - throw new BusinessException(ErrorCode.INVALID_MONTHS, - ErrorCode.INVALID_MONTHS.getMessage() + ": " + months); - } - } - - // 차트 타입 검증 - private void validateChartType(String type) { - if (!StringUtils.hasText(type)) { - throw new BusinessException(ErrorCode.INVALID_INPUT, "차트 타입이 필요합니다."); - } - // 지원하는 타입: weight, height, temperature, pulseRate, bloodPressure - List validTypes = List.of("weight", "height", "temperature", "pulseRate", "bloodPressure"); - if (!validTypes.contains(type.toLowerCase())) { - throw new BusinessException(ErrorCode.INVALID_CHART_TYPE, - ErrorCode.INVALID_CHART_TYPE.getMessage() + ": " + type); - } - } - - // 차트 값 추출 - private Object extractChartValue(HealthRecord record, String type) { - if (record == null || type == null) { - return null; - } - - return switch (type.toLowerCase()) { - case "weight" -> record.getWeight(); - case "height" -> record.getHeight(); - case "temperature" -> record.getTemperature(); - case "pulserate" -> record.getPulseRate(); - case "bloodpressure" -> record.getBloodPressure(); - default -> null; - }; - } - private HealthRecordAttachmentResponse toAttachmentResponse(HealthRecordAttachment attachment) { return HealthRecordAttachmentResponse.builder() .attachmentId(attachment.getId())