From f2ff998283120da4b3c1913cdbea492551367ba7 Mon Sep 17 00:00:00 2001 From: melonhong Date: Wed, 7 Jan 2026 23:43:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20FitnessService=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=B0=8F=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FitnessController에 혼재되어 있던 체력 측정 등록, 수정, 조회 로직을 Service 계층으로 이관 - detached entity passed to persist 에러 해결을 위해 findUser 메서드 추가 - 반복되는 saveOrUpdate 로직을 private 메서드로 추출하여 중복 코드 제거 - 주요 메서드에 Javadoc 주석 추가하여 코드 가독성 및 유지보수성 향상 --- .../service/fitness/FitnessService.java | 69 +++++ .../service/fitness/FitnessServiceImpl.java | 248 ++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 src/main/java/com/fitlink/service/fitness/FitnessService.java create mode 100644 src/main/java/com/fitlink/service/fitness/FitnessServiceImpl.java diff --git a/src/main/java/com/fitlink/service/fitness/FitnessService.java b/src/main/java/com/fitlink/service/fitness/FitnessService.java new file mode 100644 index 0000000..4bd2a52 --- /dev/null +++ b/src/main/java/com/fitlink/service/fitness/FitnessService.java @@ -0,0 +1,69 @@ +package com.fitlink.service.fitness; + +import com.fitlink.domain.Users; +import com.fitlink.web.dto.FitnessGeneralRequestDTO; +import com.fitlink.web.dto.FitnessKookminRequestDTO; +import com.fitlink.web.dto.FitnessResponseDTO; + +/** + * 체력 측정(국민체력100, 일반 측정) 결과의 등록, 수정, 조회를 담당하는 서비스 인터페이스 + */ +public interface FitnessService { + + /** + * 국민체력 100 측정 결과를 등록 + * + * 사용자 정보(키, 몸무게 등)를 갱신하고, 측정 값을 저장한 뒤 점수를 계산 + * + * + * @param user 인증된 사용자 엔티티 + * @param request 국민체력 100 측정 데이터 (악력, 윗몸일으키기 등) + * @return 계산된 점수와 저장된 결과 정보가 담긴 DTO + */ + FitnessResponseDTO registerKookminTest(Users user, FitnessKookminRequestDTO request); + + /** + * 일반 체력 측정(간편 측정) 결과를 등록 + * + * 슬라이더 점수 및 YMCA 스텝 테스트 등의 결과를 저장하고 점수를 계산 + * + * + * @param user 인증된 사용자 엔티티 + * @param request 일반 체력 측정 데이터 + * @return 계산된 점수와 저장된 결과 정보가 담긴 DTO + */ + FitnessResponseDTO registerGeneralTest(Users user, FitnessGeneralRequestDTO request); + + /** + * 기존 국민체력 100 측정 결과를 수정 + * + * 가장 최신의 측정 기록을 찾아 값을 덮어쓰고, 점수를 재계산 + * + * + * @param user 인증된 사용자 엔티티 + * @param request 수정할 국민체력 100 측정 데이터 + * @return 업데이트된 결과 및 재계산된 점수 DTO + */ + FitnessResponseDTO updateKookminTest(Users user, FitnessKookminRequestDTO request); + + /** + * 기존 일반 체력 측정 결과를 수정 + * + * @param user 인증된 사용자 엔티티 + * @param request 수정할 일반 체력 측정 데이터 + * @return 업데이트된 결과 및 재계산된 점수 DTO + */ + FitnessResponseDTO updateGeneralTest(Users user, FitnessGeneralRequestDTO request); + + /** + * 사용자의 가장 최신 체력 측정 결과를 조회합니다. + * + * 국민체력 100 결과 혹은 일반 측정 결과 중 최신 항목을 반환하며, + * 연령별 평균 데이터(Standard)도 함께 포함됩니다. + * + * + * @param user 인증된 사용자 엔티티 + * @return 최신 체력 측정 결과 DTO + */ + FitnessResponseDTO getLatestFitnessResult(Users user); +} \ No newline at end of file diff --git a/src/main/java/com/fitlink/service/fitness/FitnessServiceImpl.java b/src/main/java/com/fitlink/service/fitness/FitnessServiceImpl.java new file mode 100644 index 0000000..3b244eb --- /dev/null +++ b/src/main/java/com/fitlink/service/fitness/FitnessServiceImpl.java @@ -0,0 +1,248 @@ +package com.fitlink.service.fitness; + +import com.fitlink.domain.*; +import com.fitlink.domain.enums.Sex; +import com.fitlink.repository.*; +import com.fitlink.service.fitness.standards.FitnessStandardSet; +import com.fitlink.service.fitness.standards.FitnessStandards; +import com.fitlink.web.dto.FitnessGeneralRequestDTO; +import com.fitlink.web.dto.FitnessKookminRequestDTO; +import com.fitlink.web.dto.FitnessResponseDTO; +import com.fitlink.web.mapper.FitnessMapper; +import com.fitlink.web.mapper.FitnessResultMapper; +import com.fitlink.web.mapper.UserInfoMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.List; + +/** + * 체력 측정(국민체력100, 일반 측정) 결과의 등록, 수정, 조회를 담당하는 서비스 구현체입니다. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class FitnessServiceImpl implements FitnessService { + + private final FitnessResultRepository fitnessResultRepository; + private final UsersInfoRepository usersInfoRepository; + private final TestKookminRepository testKookminRepository; + private final TestGeneralRepository testGeneralRepository; + private final UserRepository userRepository; + + private final FitnessScoreService fitnessScoreService; + private final FitnessStandards fitnessStandards; + + private final FitnessMapper fitnessMapper; + private final FitnessResultMapper fitnessResultMapper; + private final UserInfoMapper userInfoMapper; + + /** + * Detached 상태의 User 객체를 영속(Persistent) 상태로 전환하기 위해 DB에서 재조회합니다. + * JPA 저장 시 'detached entity passed to persist' 에러 방지용입니다. + */ + private Users findUser(Users user) { + return userRepository.findById(user.getId()) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + } + + /** + * 국민체력 100 측정 결과를 등록합니다. + */ + @Override + public FitnessResponseDTO registerKookminTest(Users user, FitnessKookminRequestDTO request) { + Users managedUser = findUser(user); + + // 1. 유저 기본 정보(키, 몸무게 등) 업데이트 + FitnessResponseDTO.UserInfo userInfo = saveOrUpdateUsersInfo(managedUser, request.getSex(), request.getBirthDate(), request.getHeight(), request.getWeight()); + + // 2. 측정 데이터 저장 + TestKookmin savedTest = saveOrUpdateTestKookmin(managedUser, request); + + // 3. 점수 계산 + FitnessResponseDTO response = fitnessScoreService.calculateKookmin(request); + + // 4. 결과 엔티티 생성 및 저장 + FitnessResult entity = fitnessResultMapper.toEntity(response, managedUser); + entity.setKookminResultId(savedTest); + fitnessResultRepository.save(entity); + + // 5. 응답 조합 + response.setUserInfo(userInfo); + response.setTestKookmin(fitnessMapper.toKookminDTO(savedTest)); + + return response; + } + + /** + * 일반 체력 측정 결과를 등록합니다. + */ + @Override + public FitnessResponseDTO registerGeneralTest(Users user, FitnessGeneralRequestDTO request) { + Users managedUser = findUser(user); + + FitnessResponseDTO.UserInfo userInfo = saveOrUpdateUsersInfo(managedUser, request.getSex(), request.getBirthDate(), request.getHeight(), request.getWeight()); + + TestGeneral savedTest = saveOrUpdateTestGeneral(managedUser, request); + FitnessResponseDTO response = fitnessScoreService.calculateGeneral(request); + + FitnessResult entity = fitnessResultMapper.toEntity(response, managedUser); + entity.setGeneralResultId(savedTest); + fitnessResultRepository.save(entity); + + response.setUserInfo(userInfo); + response.setTestGeneral(fitnessMapper.toGeneralDTO(savedTest)); + + return response; + } + + /** + * 기존 국민체력 100 측정 결과를 수정합니다. + */ + @Override + public FitnessResponseDTO updateKookminTest(Users user, FitnessKookminRequestDTO request) { + Users managedUser = findUser(user); + + FitnessResult existingResult = findLatestFitnessResult(managedUser); + + TestKookmin savedTest = saveOrUpdateTestKookmin(managedUser, request); + FitnessResponseDTO.UserInfo userInfo = saveOrUpdateUsersInfo(managedUser, request.getSex(), request.getBirthDate(), request.getHeight(), request.getWeight()); + + FitnessResponseDTO response = fitnessScoreService.calculateKookmin(request); + FitnessResponseDTO.FitnessStandardResponse standard = response.getStandard(); + + fitnessResultMapper.updateEntityFromResponse(response, existingResult); + existingResult.setKookminResultId(savedTest); + fitnessResultRepository.save(existingResult); + + response = fitnessResultMapper.toResponseDTO(existingResult); + response.setUserInfo(userInfo); + response.setStandard(standard); + + return response; + } + + /** + * 기존 일반 체력 측정 결과를 수정합니다. + */ + @Override + public FitnessResponseDTO updateGeneralTest(Users user, FitnessGeneralRequestDTO request) { + Users managedUser = findUser(user); + + FitnessResult existingResult = findLatestFitnessResult(managedUser); + + TestGeneral savedTest = saveOrUpdateTestGeneral(managedUser, request); + FitnessResponseDTO.UserInfo userInfo = saveOrUpdateUsersInfo(managedUser, request.getSex(), request.getBirthDate(), request.getHeight(), request.getWeight()); + + FitnessResponseDTO response = fitnessScoreService.calculateGeneral(request); + FitnessResponseDTO.FitnessStandardResponse standard = response.getStandard(); + + fitnessResultMapper.updateEntityFromResponse(response, existingResult); + existingResult.setGeneralResultId(savedTest); + fitnessResultRepository.save(existingResult); + + response = fitnessResultMapper.toResponseDTO(existingResult); + response.setUserInfo(userInfo); + response.setStandard(standard); + + return response; + } + + /** + * 사용자의 가장 최신 체력 측정 결과를 조회합니다. + */ + @Override + @Transactional(readOnly = true) + public FitnessResponseDTO getLatestFitnessResult(Users user) { + Users managedUser = findUser(user); + + FitnessResult entity = findLatestFitnessResult(managedUser); + FitnessResponseDTO response = fitnessResultMapper.toResponseDTO(entity); + + UsersInfo userInfo = usersInfoRepository.findById(managedUser.getId()).orElse(null); + if (userInfo != null) { + response.setUserInfo(userInfoMapper.toDTO(userInfo)); + + int age = FitnessScoreService.calculateAge(userInfo.getBirthDate()); + FitnessStandardSet st = fitnessStandards.getStandard(userInfo.getSex(), age); + response.setStandard(fitnessScoreService.getStandards(st)); + } + + if (entity.getKookminResultId() != null) { + TestKookmin testKookmin = testKookminRepository.findById(entity.getKookminResultId().getId()).orElse(null); + response.setTestKookmin(fitnessMapper.toKookminDTO(testKookmin)); + } else if (entity.getGeneralResultId() != null) { + TestGeneral testGeneral = testGeneralRepository.findById(entity.getGeneralResultId().getId()).orElse(null); + response.setTestGeneral(fitnessMapper.toGeneralDTO(testGeneral)); + } + + return response; + } + + // ================= Private Helper Methods ================= + + /** + * 사용자의 가장 최신 FitnessResult 엔티티를 조회합니다. + */ + private FitnessResult findLatestFitnessResult(Users user) { + List list = fitnessResultRepository.findByUser(user); + if (list.isEmpty()) { + throw new IllegalArgumentException("저장된 측정 결과가 없습니다."); + } + return list.stream() + .max(Comparator.comparing(FitnessResult::getCreatedAt)) + .orElseThrow(() -> new IllegalArgumentException("결과를 찾을 수 없습니다.")); + } + + /** + * 사용자의 신체 정보(성별, 생년월일, 키, 몸무게)를 저장하거나 업데이트합니다. + */ + private FitnessResponseDTO.UserInfo saveOrUpdateUsersInfo(Users user, String sexStr, String birthDate, Float height, Float weight) { + UsersInfo userInfo = usersInfoRepository.findById(user.getId()) + .orElse(UsersInfo.builder().users(user).build()); + + userInfo.setSex(Sex.valueOf(sexStr)); + userInfo.setBirthDate(birthDate); + userInfo.setHeight(height); + userInfo.setWeight(weight); + + return userInfoMapper.toDTO(usersInfoRepository.save(userInfo)); + } + + /** + * 국민체력 100 측정 데이터를 저장하거나, 이미 최신 기록이 있다면 업데이트합니다. + */ + private TestKookmin saveOrUpdateTestKookmin(Users user, FitnessKookminRequestDTO request) { + TestKookmin testKookmin = testKookminRepository.findTopByUserOrderByCreatedAtDesc(user) + .orElse(TestKookmin.builder().user(user).build()); + + testKookmin.setGripStrength(BigDecimal.valueOf(request.getGripStrength())); + testKookmin.setSitUp(request.getSitUp()); + testKookmin.setSitAndReach(BigDecimal.valueOf(request.getSitAndReach())); + testKookmin.setShuttleRun(request.getShuttleRun()); + testKookmin.setSprint(BigDecimal.valueOf(request.getSprint())); + testKookmin.setStandingLongJump(BigDecimal.valueOf(request.getStandingLongJump())); + + return testKookminRepository.save(testKookmin); + } + + /** + * 일반 체력 측정 데이터를 저장하거나, 이미 최신 기록이 있다면 업데이트합니다. + */ + private TestGeneral saveOrUpdateTestGeneral(Users user, FitnessGeneralRequestDTO request) { + TestGeneral testGeneral = testGeneralRepository.findTopByUserOrderByCreatedAtDesc(user) + .orElse(TestGeneral.builder().user(user).build()); + + testGeneral.setSliderStrength(request.getSliderStrength()); + testGeneral.setSitUp(request.getSitUp()); + testGeneral.setSitAndReach(BigDecimal.valueOf(request.getSitAndReach())); + testGeneral.setYmcaStepTest(BigDecimal.valueOf(request.getYmcaStepTest())); + testGeneral.setSliderAgility(request.getSliderAgility()); + testGeneral.setSliderPower(request.getSliderPower()); + + return testGeneralRepository.save(testGeneral); + } +} \ No newline at end of file