diff --git a/src/main/java/com/example/medicare_call/controller/CareCallController.java b/src/main/java/com/example/medicare_call/controller/CareCallController.java index fa0886b..1de4112 100644 --- a/src/main/java/com/example/medicare_call/controller/CareCallController.java +++ b/src/main/java/com/example/medicare_call/controller/CareCallController.java @@ -51,6 +51,11 @@ public ResponseEntity getCareCallSetting(@Parameter(hid return ResponseEntity.ok(response); } + /** + * 전화 서버로부터 통화 완료 raw 데이터를 수신하여 저장 + * /care-call/test는 CareCallTestService.TEST_SETTING_ID(-1)를 사용하므로 저장을 건너뛴다. + * /care-call/immediate는 실제 DB의 settingId를 사용하므로 정상적으로 저장된다. + */ @Override @PostMapping("/call-data") public ResponseEntity receiveCallData(@Valid @RequestBody CareCallDataProcessRequest request) { @@ -59,6 +64,10 @@ public ResponseEntity receiveCallData(@Valid @RequestBody CareCa return ResponseEntity.status(HttpStatus.CREATED).build(); } + /** + * 특정 어르신에게 cron을 통하지 않고, 즉시 케어콜을 발송 (베타테스트용) + * 실제 DB에 등록된 Elder와 CareCallSetting을 사용하며, 통화 결과가 정상적으로 DB에 저장 + */ @Override @PostMapping("/care-call/immediate") public ResponseEntity sendImmediateCareCall(@Valid @RequestBody ImmediateCareCallRequest request) { @@ -66,7 +75,11 @@ public ResponseEntity sendImmediateCareCall(@Valid @RequestBody Immediat return ResponseEntity.ok(result); } - //TODO: 커스텀 프롬프트 케어콜 테스트용, 개발 완료 후 삭제 + /** + * 개발자용 케어콜 발신 테스트 API (프로덕션 미사용) + * staging 환경에서 프롬프트와 전화번호로 발신 로직만 확인하기 위한 용도 + * 더미 Elder, CareCallSetting을 사용하며, 통화 결과(settingId = -1)는 DB에 저장되지 않는다 + */ @Override @PostMapping("/care-call/test") public ResponseEntity testCareCall(@Valid @RequestBody CareCallTestRequest req) { diff --git a/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallService.java b/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallService.java index d6b1b42..6eb4228 100644 --- a/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallService.java +++ b/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallService.java @@ -26,15 +26,23 @@ public class CareCallService { private final CareCallSettingRepository careCallSettingRepository; /** - * 케어콜 통화 데이터를 저장하고 CareCallCompletedEvent 이벤트를 발행 - * - * @param request 케어콜 데이터 처리 요청 정보를 담은 DTO - * @return 저장된 케어콜 기록 엔티티 + * 전화 서버로부터 수신된 통화 raw 데이터를 저장하고 CareCallCompletedEvent를 발행 + * + * settingId가 음수(< 0)인 경우 개발자용 테스트 발송(/care-call/test)의 결과이므로 + * DB 저장 및 이벤트 발행을 건너뛴다 + * + * @param request 전화 서버가 전송한 통화 완료 데이터 + * @return 저장된 케어콜 기록 엔티티. 테스트 발송인 경우 null 반환 */ @Transactional public CareCallRecord saveCallData(CareCallDataProcessRequest request) { log.info("통화 데이터 저장 시작: elderId={}, settingId={}", request.getElderId(), request.getSettingId()); + if (request.getSettingId() != null && request.getSettingId() < 0) { + log.info("테스트 발송 결과 수신 (settingId={}), 저장을 건너뜁니다.", request.getSettingId()); + return null; + } + Elder elder = elderRepository.findById(request.getElderId()) .orElseThrow(() -> new CustomException(ErrorCode.ELDER_NOT_FOUND)); diff --git a/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadService.java b/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadService.java index de6838f..3872e11 100644 --- a/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadService.java +++ b/src/main/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadService.java @@ -66,6 +66,10 @@ public CareCallRecord processUploadedCallData(CallDataUploadRequest request) { .build(); CareCallRecord saved = careCallService.saveCallData(processRequest); + if (saved == null) { + log.info("테스트 발송 - 저장 건너뜀 (settingId={})", processRequest.getSettingId()); + return null; + } log.info("STT 데이터 처리 완료: recordId={}", saved.getId()); return saved; } diff --git a/src/main/java/com/example/medicare_call/service/carecall/outbound/CareCallTestService.java b/src/main/java/com/example/medicare_call/service/carecall/outbound/CareCallTestService.java index 28ac2d4..0a60d92 100644 --- a/src/main/java/com/example/medicare_call/service/carecall/outbound/CareCallTestService.java +++ b/src/main/java/com/example/medicare_call/service/carecall/outbound/CareCallTestService.java @@ -2,7 +2,6 @@ import com.example.medicare_call.domain.CareCallSetting; import com.example.medicare_call.domain.Elder; -import com.example.medicare_call.domain.Member; import com.example.medicare_call.dto.carecall.CareCallTestRequest; import com.example.medicare_call.dto.carecall.ImmediateCareCallRequest.CareCallOption; import com.example.medicare_call.global.enums.CallType; @@ -10,6 +9,7 @@ import com.example.medicare_call.global.enums.ResidenceType; import com.example.medicare_call.global.exception.CustomException; import com.example.medicare_call.global.exception.ErrorCode; +import com.example.medicare_call.repository.CareCallSettingRepository; import com.example.medicare_call.repository.ElderRepository; import com.example.medicare_call.service.carecall.outbound.client.CareCallClient; import lombok.RequiredArgsConstructor; @@ -22,47 +22,49 @@ @RequiredArgsConstructor public class CareCallTestService { + /** 테스트 발송 시 사용하는 settingId. 실제 CareCallSetting과 무관한 식별용 음수 값 */ + static final int TEST_SETTING_ID = -1; + private final ElderRepository elderRepository; - private final com.example.medicare_call.service.carecall.setting.CareCallSettingService careCallSettingService; + private final CareCallSettingRepository careCallSettingRepository; private final CareCallRequestSenderService careCallRequestSenderService; private final CareCallClient careCallClient; - // TODO: KUIT 데모데이 시연용 일시적 기능으로 불완전합니다. 제거 혹은 개선이 필요합니다. /** - * 특정 어르신에게 즉시 케어콜을 발송 + * 특정 어르신에게 즉시 케어콜을 발송 (베타테스트용) + * 실제 DB에 등록된 Elder와 CareCallSetting을 기반으로 발송하며, + * 통화 결과는 실제 케어콜과 동일하게 웹훅을 통해 DB에 저장 * * @param elderId 대상 어르신 ID * @param careCallOption 케어콜 옵션 (회차 정보 등) * @return 발송 완료 메시지 */ - @Transactional + @Transactional(readOnly = true) public String sendImmediateCall(Long elderId, CareCallOption careCallOption) { Elder elder = elderRepository.findById(elderId.intValue()) .orElseThrow(() -> new CustomException(ErrorCode.ELDER_NOT_FOUND, "케어콜 발송 대상 어르신을 찾을 수 없습니다. ID: " + elderId)); - try { - CareCallSetting setting = careCallSettingService.getOrCreateImmediateSetting(elder); - CallType callType = convertOptionToCallType(careCallOption); + CareCallSetting setting = careCallSettingRepository.findByElder(elder) + .orElseThrow(() -> new CustomException(ErrorCode.CARE_CALL_SETTING_NOT_FOUND, + "즉시 케어콜 발송 대상 어르신의 케어콜 설정을 찾을 수 없습니다. ID: " + elderId)); + CallType callType = convertOptionToCallType(careCallOption); - careCallRequestSenderService.sendCall(setting.getId(), elderId.intValue(), callType); - return String.format("%s 어르신께 즉시 케어콜 발송이 완료되었습니다.", elder.getName()); - } catch (Exception e) { - log.error("즉시 케어콜 발송 실패 - elderId: {}, error: {}", elder.getId(), e.getMessage()); - throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR, "케어콜 발송 중 오류가 발생했습니다: " + e.getMessage()); - } + careCallRequestSenderService.sendCall(setting.getId(), elderId.intValue(), callType); + return String.format("%s 어르신께 즉시 케어콜 발송이 완료되었습니다.", elder.getName()); } + // TODO: 테스트 서버를 운용하게 될 경우, @Profile로 분리를 권장 /** - * 테스트용 케어콜을 발신 - * 가상의 어르신 데이터(더미)를 사용하여 실제 발신 로직만 테스트하고 DB 저장은 수행하지 않는다 - * + * 개발자용 케어콜 발신 테스트 (production에서는 미사용) + * 더미 Elder 데이터를 사용하여 발신 로직만 확인하는 용도입니다. + * TEST_SETTING_ID(-1)를 사용하므로, 통화 완료 후 웹훅이 수신되어도 DB에 저장되지 않습니다. + * * @param req 테스트할 프롬프트와 전화번호 정보 */ public void sendTestCall(CareCallTestRequest req) { - // 테스트용이라 하드코딩된 더미 데이터 사용, DB 저장 안함 Elder testElder = Elder.builder() .id(100) - .name("김옥자") // 테스트 이름 + .name("김옥자") .phone("01011111111") .gender((byte)0) .relationship(ElderRelation.CHILD) @@ -70,8 +72,7 @@ public void sendTestCall(CareCallTestRequest req) { .build(); String testPrompt = req.prompt(); - // 테스트 호출은 settingId 100으로 가정 - careCallClient.requestCall(100, testElder.getId(), req.phoneNumber(), testPrompt); + careCallClient.requestCall(TEST_SETTING_ID, testElder.getId(), req.phoneNumber(), testPrompt); } /** diff --git a/src/main/java/com/example/medicare_call/service/carecall/setting/CareCallSettingService.java b/src/main/java/com/example/medicare_call/service/carecall/setting/CareCallSettingService.java index 8260091..4d789b8 100644 --- a/src/main/java/com/example/medicare_call/service/carecall/setting/CareCallSettingService.java +++ b/src/main/java/com/example/medicare_call/service/carecall/setting/CareCallSettingService.java @@ -16,7 +16,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalTime; @Service @RequiredArgsConstructor @@ -98,30 +97,4 @@ private void validateManageAuthority(Integer memberId, Integer elderId) { throw new CustomException(ErrorCode.HANDLE_ACCESS_DENIED); } } - - /** - * 즉시 케어콜을 위한 케어콜 설정을 생성하거나 조회 - * 1차 케어콜 시간을 현재 시간으로 설정하여 반환 - * - * @param elder 대상 어르신 엔티티 - * @return 케어콜 설정 엔티티 - */ - @Transactional - public CareCallSetting getOrCreateImmediateSetting(Elder elder) { - LocalTime currentTime = LocalTime.now().withSecond(0).withNano(0); - - return careCallSettingRepository.findByElder(elder) - .map(setting -> { - setting.update(currentTime, setting.getSecondCallTime(), setting.getThirdCallTime()); - return setting; - }) - .orElseGet(() -> { - CareCallSetting newSetting = CareCallSetting.builder() - .elder(elder) - .firstCallTime(currentTime) - .recurrence(CallRecurrenceType.DAILY) - .build(); - return careCallSettingRepository.save(newSetting); - }); - } } diff --git a/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallServiceTest.java b/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallServiceTest.java index 91e528c..91c04d9 100644 --- a/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallServiceTest.java +++ b/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallServiceTest.java @@ -279,4 +279,46 @@ void saveCallData_fail_settingNotFound() { verify(careCallRecordRepository, never()).save(any()); eventsMockedStatic.verify(() -> Events.raise(any(CareCallCompletedEvent.class)), never()); } -} + + @Test + @DisplayName("통화 데이터 저장 건너뜀 - 테스트 발송 (TEST_SETTING_ID = -1)") + void saveCallData_skip_whenTestSettingId() { + // given + CareCallDataProcessRequest request = CareCallDataProcessRequest.builder() + .elderId(1) + .settingId(-1) // CareCallTestService.TEST_SETTING_ID + .status(CareCallStatus.COMPLETED) + .responded((byte) 1) + .build(); + + // when + CareCallRecord result = careCallService.saveCallData(request); + + // then: 저장 없이 null 반환 + assertThat(result).isNull(); + verify(elderRepository, never()).findById(any()); + verify(careCallSettingRepository, never()).findById(any()); + verify(careCallRecordRepository, never()).save(any()); + eventsMockedStatic.verify(() -> Events.raise(any(CareCallCompletedEvent.class)), never()); + } + + @Test + @DisplayName("통화 데이터 저장 건너뜀 - 음수 settingId는 모두 스킵") + void saveCallData_skip_whenAnyNegativeSettingId() { + // given + CareCallDataProcessRequest request = CareCallDataProcessRequest.builder() + .elderId(1) + .settingId(-99) + .status(CareCallStatus.COMPLETED) + .responded((byte) 1) + .build(); + + // when + CareCallRecord result = careCallService.saveCallData(request); + + // then + assertThat(result).isNull(); + verify(careCallRecordRepository, never()).save(any()); + eventsMockedStatic.verify(() -> Events.raise(any(CareCallCompletedEvent.class)), never()); + } +} diff --git a/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadServiceTest.java b/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadServiceTest.java index 9012319..0d34d5c 100644 --- a/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadServiceTest.java +++ b/src/test/java/com/example/medicare_call/service/carecall/inbound/CareCallUploadServiceTest.java @@ -328,4 +328,22 @@ void processUploadedCallData_success_longText() { request.getTranscription().getFullText().size() == 20 )); } + + @Test + @DisplayName("파일 업로드 - 테스트 발송(settingId < 0)이면 저장 없이 null 반환") + void processUploadedCallData_skip_whenTestSettingId() { + // given + uploadRequest.setSettingId(-1); // TEST_SETTING_ID + + when(openAiSttService.transcribe(mockAudioFile)).thenReturn(sttResponse); + when(careCallService.saveCallData(any())).thenReturn(null); // 테스트 발송은 null 반환 + + // when + CareCallRecord result = careCallUploadService.processUploadedCallData(uploadRequest); + + // then: NPE 없이 null 반환 + assertThat(result).isNull(); + verify(openAiSttService).transcribe(mockAudioFile); + verify(careCallService).saveCallData(any()); + } } diff --git a/src/test/java/com/example/medicare_call/service/carecall/outbound/CareCallTestServiceTest.java b/src/test/java/com/example/medicare_call/service/carecall/outbound/CareCallTestServiceTest.java index 833fabc..eec063f 100644 --- a/src/test/java/com/example/medicare_call/service/carecall/outbound/CareCallTestServiceTest.java +++ b/src/test/java/com/example/medicare_call/service/carecall/outbound/CareCallTestServiceTest.java @@ -7,9 +7,9 @@ import com.example.medicare_call.global.enums.CallType; import com.example.medicare_call.global.exception.CustomException; import com.example.medicare_call.global.exception.ErrorCode; +import com.example.medicare_call.repository.CareCallSettingRepository; import com.example.medicare_call.repository.ElderRepository; import com.example.medicare_call.service.carecall.outbound.client.CareCallClient; -import com.example.medicare_call.service.carecall.setting.CareCallSettingService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,7 +32,7 @@ class CareCallTestServiceTest { @Mock private ElderRepository elderRepository; @Mock - private CareCallSettingService careCallSettingService; + private CareCallSettingRepository careCallSettingRepository; @Mock private CareCallRequestSenderService careCallRequestSenderService; @Mock @@ -51,7 +51,7 @@ void sendImmediateCall_success() { CareCallSetting setting = CareCallSetting.builder().id(10).build(); when(elderRepository.findById(1)).thenReturn(Optional.of(elder)); - when(careCallSettingService.getOrCreateImmediateSetting(elder)).thenReturn(setting); + when(careCallSettingRepository.findByElder(elder)).thenReturn(Optional.of(setting)); // when String result = careCallTestService.sendImmediateCall(elderId, option); @@ -84,6 +84,6 @@ void sendTestCall_success() { careCallTestService.sendTestCall(request); // then - verify(careCallClient).requestCall(eq(100), eq(100), eq(request.phoneNumber()), eq("테스트")); + verify(careCallClient).requestCall(eq(CareCallTestService.TEST_SETTING_ID), eq(100), eq(request.phoneNumber()), eq("테스트")); } }