Skip to content

[Feat] 전화 알림 기능 - #72

Merged
marshmallowing merged 6 commits into
developfrom
feature/53
Jul 17, 2026
Merged

[Feat] 전화 알림 기능#72
marshmallowing merged 6 commits into
developfrom
feature/53

Conversation

@marshmallowing

@marshmallowing marshmallowing commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🔍️ 작업 내용

  • Closes [Feat] 전화 알림 기능 #53
    • 복용 시간 기반 전화 알림 기능 구현
    • SOLAPI 음성 전화 발신 연동
    • 전화 수신 결과에 따른 홈 복약 카드 연동
    • 미수신/실패 시 보호자 및 부모님 문자 알림 처리
    • 로컬 테스트용 전화 발신 트리거 API 추가

✨ 상세 설명

1. 복용 시간 기반 전화 알림

  • 부모님의 식사 시간을 기준으로 식후 30분 시점에 복약 알림 전화가 자동으로 발신되도록 스케줄러를 구현
  • VoiceCallSender 인터페이스를 통해 SOLAPI 음성 전화 발신 로직을 추상화하여 설계

2. SOLAPI 기본 재시도 정책 반영

  • SOLAPI 음성 메시지는 부재중 시 자체적으로 자동 재시도를 수행하므로, 서버에서 별도 10분 재발신 로직은 수행하지 않도록 정리
  • 서버는 최초 발신 1회만 수행하고, SOLAPI가 전달하는 최종 전화 결과 웹훅을 기준으로 상태를 갱신

3. 문자(SMS) Fallback 처리

  • 1차 + 재시도 전화가 모두 실패(미수신)한 경우, 보호자에게 SMS 알림이 발송
  • 기존에 구축된 SMS 인프라(CoolSmsSender 등)를 재사용하여 별도의 문자 발송 로직 중복 없이 처리

4. 홈 화면 복약 카드 연동

  • 전화 수신 결과(성공/미수신/실패)에 따라 홈 화면의 복약 카드 상태가 실시간으로 반영
  • 전화 결과 → 복약 상태 업데이트 → 홈 카드 조회 API 응답에 반영되는 흐름으로 구성

5. 로컬 테스트용 트리거 API

  • 실제 식사 시간까지 기다리지 않고도 전화 발신 로직을 검증할 수 있도록, 특정 사용자에 대해 즉시 전화를 트리거하는 테스트 전용 API를 추가
  • 운영 환경에서는 비활성화되도록 프로파일 분리

6. CallLog 기반 중복 발신 방지 및 상태 관리

  • 전화 발신 전 PENDING 상태의 CallLog를 먼저 저장해 발신 작업을 선점하도록 구성
  • seniorId + mealTime + callDate 복합 유니크 제약조건을 통해 동일 날짜, 동일 시간대 중복 발신을 DB 레벨에서 방지

🛠️ 추후 리팩토링 및 고도화 계획

  • 전화 발신 이력에 대한 모니터링/알림 대시보드 연동
  • SOLAPI 웹훅 실패 시 재처리(dead-letter / 재시도 큐) 로직 보강

📸 스크린샷 (선택)

image

💬 리뷰 요구사항

Summary by CodeRabbit

Summary by CodeRabbit

  • 새로운 기능
    • 복약 시간에 맞춰 자동 음성 전화를 발신하고, 통화 결과에 따라 상태를 기록합니다.
    • 통화 결과 수신용 웹훅 엔드포인트와 로컬 테스트용 호출 기능을 추가했습니다.
    • 실제 발신/모의 발신 전환을 지원합니다.
  • 버그 수정
    • 오늘의 홈 화면에서 복약이 완료된 식사 시간이 표시되지 않도록 개선했습니다.
  • 개선
    • 통화 상태에 결과 대기/응답/미응답/실패 흐름을 반영했습니다.
    • 미수신 재시도 관련 시간 계산 로직을 제거했습니다.

@marshmallowing marshmallowing self-assigned this Jul 16, 2026
@marshmallowing marshmallowing added the ✨ Feature 기능 개발 label Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f150e5a-e587-4b31-94c7-4de588cdd2f2

📥 Commits

Reviewing files that changed from the base of the PR and between bc15ec9 and c05965a.

📒 Files selected for processing (6)
  • src/main/java/com/piuda/callcare/domain/calllog/entity/CallLog.java
  • src/main/java/com/piuda/callcare/domain/calllog/enums/CallStatus.java
  • src/main/java/com/piuda/callcare/domain/calllog/repository/CallLogRepository.java
  • src/main/java/com/piuda/callcare/domain/calllog/service/CallReminderCommandService.java
  • src/main/java/com/piuda/callcare/domain/notification/util/NotificationTimeCalculator.java
  • src/test/java/com/piuda/callcare/domain/notification/util/NotificationTimeCalculatorTest.java

📝 Walkthrough

Walkthrough

Changes

전화 알림 기능

Layer / File(s) Summary
통화 계약과 상태 모델
build.gradle, src/main/java/com/piuda/callcare/domain/calllog/client/*, src/main/java/com/piuda/callcare/domain/calllog/entity/CallLog.java, src/main/java/com/piuda/callcare/domain/calllog/enums/CallStatus.java
SOLAPI 의존성과 음성 발신 인터페이스, 실제·모의 발신기, PENDING 상태 및 콜 로그 상태 전이가 추가되었습니다.
콜 로그와 복약 일정 조회
src/main/java/com/piuda/callcare/domain/calllog/repository/CallLogRepository.java, src/main/java/com/piuda/callcare/domain/medication/repository/MedicationScheduleRepository.java
메시지 ID, 보호자 알림 대상과 활성 복약 일정 조회 메서드가 추가되었습니다.
통화 발신과 결과 처리 서비스
src/main/java/com/piuda/callcare/domain/calllog/service/CallReminderCommandService.java, src/main/java/com/piuda/callcare/domain/calllog/scheduler/CallReminderScheduler.java, src/main/java/com/piuda/callcare/CallcareApplication.java
스케줄 기반 통화 발신, 콜백 상태 처리, 복약 로그 기록 및 미응답 알림 흐름이 구현되고 스케줄링이 활성화되었습니다.
콜백·테스트 진입점과 접근 설정
src/main/java/com/piuda/callcare/domain/calllog/dto/request/CallResultWebhookRequest.java, src/main/java/com/piuda/callcare/domain/calllog/controller/*, src/main/java/com/piuda/callcare/global/config/SecurityConfig.java
웹훅 payload 정규화, 로컬 테스트 발신 API, 웹훅·테스트 경로의 인증 예외가 추가되었습니다.
완료 식사 시간 홈 카드 반영
src/main/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryService.java, src/test/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryServiceTest.java
오늘 모드 응답에서 완료된 식사 시간 그룹을 제외하도록 로직과 테스트가 변경되었습니다.
재시도 시간 계산 제거
src/main/java/com/piuda/callcare/domain/notification/util/NotificationTimeCalculator.java, src/test/java/com/piuda/callcare/domain/notification/util/NotificationTimeCalculatorTest.java
재시도 발신 시각 계산 메서드와 관련 테스트가 제거되었습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CallReminderScheduler
  participant CallReminderCommandService
  participant VoiceCallSender
  participant CallLogRepository
  participant CallResultWebhookController
  CallReminderScheduler->>CallReminderCommandService: sendDueFirstCalls(now)
  CallReminderCommandService->>VoiceCallSender: call(from, to, headerMessage, bodyMessage)
  VoiceCallSender-->>CallReminderCommandService: messageId
  CallReminderCommandService->>CallLogRepository: save pending CallLog
  CallResultWebhookController->>CallReminderCommandService: applyCallResult(messageId, rawStatus)
  CallReminderCommandService->>CallLogRepository: findByMessageId(messageId)
  CallReminderCommandService->>CallLogRepository: update ANSWERED, NO_ANSWER, or FAILED
Loading

Possibly related PRs

  • PIUDAProject/Backend#31: NotificationTimeCalculator의 재시도 시간 계산 메서드와 직접 연결됩니다.
  • PIUDAProject/Backend#36: HomeCardQueryService의 홈 카드 생성 및 완료 식사 시간 필터링 경로가 관련됩니다.
  • PIUDAProject/Backend#47: SecurityConfig의 인증 필터 체인 및 요청 허용 규칙이 관련됩니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed 직접 링크된 #53의 기능 요구를 호출 알림, 재시도, 보호자 SMS, 테스트 API까지 포함해 충족합니다.
Out of Scope Changes check ✅ Passed 추가된 변경은 모두 전화 알림 기능 구현과 테스트·연동 범위 안에 있어 명백한 범위 이탈이 보이지 않습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 전화 기반 알림 기능이라는 핵심 변경을 잘 요약하며, SOLAPI 음성콜·재시도·보호자 알림까지 포함하는 변경과도 관련됩니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/53

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/main/java/com/piuda/callcare/domain/calllog/entity/CallLog.java (1)

82-96: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

방어적 코딩: 언박싱 시 NPE 방지

retryCount 필드가 Integer 래퍼 타입이므로, 데이터베이스에 0 기본값이 세팅되지 않은 상태에서 객체가 로드되거나 테스트 데이터가 null인 경우 ++ 또는 + 1 연산 시 NullPointerException이 발생할 수 있습니다. 방어적으로 Null 처리를 추가하는 것을 권장합니다.

🛠️ 널 안전성(Null-Safety) 적용 예시
     public void incrementRetryCount() {
-        this.retryCount++;
+        this.retryCount = (this.retryCount == null ? 0 : this.retryCount) + 1;
     }

     public void markAsNotified() {
         this.isNotified = true;
     }

     // 재시도 발신 시 호출: 새 messageId/발신 시각으로 갱신하고 결과 대기 상태로 되돌린다
     public void markRetried(String messageId, LocalDateTime calledAt) {
         this.messageId = messageId;
         this.calledAt = calledAt;
         this.status = CallStatus.PENDING;
-        this.retryCount = this.retryCount + 1;
+        this.retryCount = (this.retryCount == null ? 0 : this.retryCount) + 1;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/piuda/callcare/domain/calllog/entity/CallLog.java` around
lines 82 - 96, Update incrementRetryCount and markRetried in CallLog to handle a
null retryCount defensively before incrementing, treating null as zero so both
retry paths increment safely without unboxing a null Integer.
src/main/java/com/piuda/callcare/domain/calllog/service/CallReminderCommandService.java (1)

51-61: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

전체 Senior 조회로 인한 메모리 및 성능 문제

seniorRepository.findAll()을 통해 시스템의 모든 사용자를 한 번에 메모리로 로드한 뒤 조건을 검사하고 있습니다. 사용자가 늘어날 경우 심각한 응답 지연과 OOM(Out of Memory)을 유발할 수 있습니다.

해당 날짜 및 식사 시간대에 활성화된 복약 일정(Active Schedule)이 존재하는 Senior 식별자만 가져오도록 Repository에 별도 쿼리를 추가하거나, 페이징/배치 단위 조회를 적용하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/piuda/callcare/domain/calllog/service/CallReminderCommandService.java`
around lines 51 - 61, Update sendDueFirstCalls to avoid loading every Senior via
seniorRepository.findAll(); add and use a repository query that retrieves only
Senior identifiers with active medication schedules for the target date and each
CALL_MEAL_TIMES value, or process those results in bounded pages/batches.
Preserve the existing phone-number validation and sendFirstCallIfDue behavior
for the filtered seniors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/piuda/callcare/domain/calllog/client/CoolVoiceCallSender.java`:
- Around line 47-52: Update the send flow around CoolVoiceCallSender’s
messageService.send(message) so a single recipient failure is isolated and does
not stop processing subsequent recipients. Handle the failure at the per-call
boundary used by CallReminderCommandService or the scheduler loop, skip only the
failed call, and preserve continuation for remaining targets instead of
propagating an uncaught IllegalStateException.

In
`@src/main/java/com/piuda/callcare/domain/calllog/service/CallReminderCommandService.java`:
- Around line 101-109: Update the callback result handling for NO_ANSWER and
FAILED so it only records the outcome via markNoAnswer or markFailed and allows
the retry scheduler flow to proceed. Remove the immediate notifyGuardian calls
and ensure isNotified remains eligible for retry processing; notify the guardian
only after all configured retries fail.
- Around line 30-34: CallReminderCommandService applies a class-level
transaction around external API calls, risking prolonged DB connection usage and
batch-wide rollbacks. Remove class-level `@Transactional`, keep sendDueFirstCalls
and notifyGuardiansForUnansweredCalls outside transactions, and add narrowly
scoped `@Transactional` annotations only to state-update methods such as
applyCallResult or save.

In `@src/main/java/com/piuda/callcare/global/config/SecurityConfig.java`:
- Line 46: Update the webhook handling for POST requests matched by
"/api/calllogs/webhook/**" so requests are authenticated with SOLAPI’s HMAC
signature, such as validating the Authorization header, before processing any
messageId or call-status changes. Add the verification in the webhook controller
or an appropriate security filter, and reject missing or invalid signatures
while preserving access for valid SOLAPI requests.

---

Nitpick comments:
In `@src/main/java/com/piuda/callcare/domain/calllog/entity/CallLog.java`:
- Around line 82-96: Update incrementRetryCount and markRetried in CallLog to
handle a null retryCount defensively before incrementing, treating null as zero
so both retry paths increment safely without unboxing a null Integer.

In
`@src/main/java/com/piuda/callcare/domain/calllog/service/CallReminderCommandService.java`:
- Around line 51-61: Update sendDueFirstCalls to avoid loading every Senior via
seniorRepository.findAll(); add and use a repository query that retrieves only
Senior identifiers with active medication schedules for the target date and each
CALL_MEAL_TIMES value, or process those results in bounded pages/batches.
Preserve the existing phone-number validation and sendFirstCallIfDue behavior
for the filtered seniors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cfd15d8b-4d86-41dd-ac98-12358a92b1ab

📥 Commits

Reviewing files that changed from the base of the PR and between 3cbff01 and bc15ec9.

📒 Files selected for processing (17)
  • build.gradle
  • src/main/java/com/piuda/callcare/CallcareApplication.java
  • src/main/java/com/piuda/callcare/domain/calllog/client/CoolVoiceCallSender.java
  • src/main/java/com/piuda/callcare/domain/calllog/client/MockVoiceCallSender.java
  • src/main/java/com/piuda/callcare/domain/calllog/client/VoiceCallSender.java
  • src/main/java/com/piuda/callcare/domain/calllog/controller/CallResultWebhookController.java
  • src/main/java/com/piuda/callcare/domain/calllog/controller/CallTestController.java
  • src/main/java/com/piuda/callcare/domain/calllog/dto/request/CallResultWebhookRequest.java
  • src/main/java/com/piuda/callcare/domain/calllog/entity/CallLog.java
  • src/main/java/com/piuda/callcare/domain/calllog/enums/CallStatus.java
  • src/main/java/com/piuda/callcare/domain/calllog/repository/CallLogRepository.java
  • src/main/java/com/piuda/callcare/domain/calllog/scheduler/CallReminderScheduler.java
  • src/main/java/com/piuda/callcare/domain/calllog/service/CallReminderCommandService.java
  • src/main/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryService.java
  • src/main/java/com/piuda/callcare/domain/medication/repository/MedicationScheduleRepository.java
  • src/main/java/com/piuda/callcare/global/config/SecurityConfig.java
  • src/test/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryServiceTest.java

.requestMatchers(HttpMethod.GET, "/api/conflicts/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/conflicts/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/ocr/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/calllogs/webhook/**").permitAll()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

웹훅 엔드포인트의 무결성 검증이 누락되어 있습니다.

해당 웹훅 API를 permitAll()로 완전 개방할 경우, 악의적인 사용자가 임의의 messageId를 포함한 페이로드를 전송하여 통화 상태(수신/미수신 등)를 조작할 수 있는 보안 취약점이 발생합니다.

SOLAPI에서 제공하는 서명 검증(HMAC) 방식(예: Authorization 헤더 검증)을 컨트롤러나 필터에 추가하여, 실제 SOLAPI 서버에서 보낸 요청인지 검증하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/piuda/callcare/global/config/SecurityConfig.java` at line
46, Update the webhook handling for POST requests matched by
"/api/calllogs/webhook/**" so requests are authenticated with SOLAPI’s HMAC
signature, such as validating the Authorization header, before processing any
messageId or call-status changes. Add the verification in the webhook controller
or an appropriate security filter, and reject missing or invalid signatures
while preserving access for valid SOLAPI requests.

@marshmallowing
marshmallowing merged commit 0ae96ae into develop Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 전화 알림 기능

1 participant