작성일: 2026년 4월 10일 진행 단계: Phase 8 (테스트 및 버그 수정) — 계속 이전 일지: Day 11 (FCM 연동, 알림 액션 버튼, Undo 기능, 비활성 스케줄 정리)
| 작업 | 내용 | 상태 |
|---|---|---|
| QStash 자동 스케줄링 | 약 등록/수정/삭제/토글 시 QStash cron 자동 등록/해제 | ✅ 완료 |
| KST → UTC 변환 | scheduled_time(KST)을 QStash cron(UTC)으로 변환 | ✅ 완료 |
| QStash JWT 서명 검증 | HMAC → JWT 방식으로 웹훅 서명 검증 수정 | ✅ 완료 |
기존에는 알림 발송을 수동으로 트리거해야 했다. 약 복용 시간에 맞춰 자동으로 푸시 알림이 발송되도록 QStash(Upstash) 기반의 cron 스케줄링을 도입했다.
새 파일: app/services/qstash_service.py
sync_schedules() — 약의 모든 활성 스케줄에 대해 QStash cron 등록delete_all_for_medication() — 약 삭제/비활성화 시 QStash 스케줄 제거_build_cron() — scheduled_time + cycle_type을 cron 표현식으로 변환_create_schedule() / _delete_schedule() — QStash REST API 호출medication_service.py 수정
기존 동기 메서드를 async로 변경하고, 각 CRUD 동작에 QStash 동기화 로직 추가:
async def create(self, user_id: str, req: MedicationCreate) -> dict:
# ... 약 등록 + 스케줄 DB 저장 ...
await self._qstash.sync_schedules(medication_id, user_id)
async def update(self, medication_id: str, user_id: str, req: MedicationUpdate) -> dict:
# ... 스케줄 소프트 삭제 + 새 스케줄 삽입 ...
await self._qstash.sync_schedules(medication_id, user_id)
async def delete(self, medication_id: str, user_id: str) -> None:
await self._qstash.delete_all_for_medication(medication_id)
# ... 소프트 삭제 ...
async def toggle(self, medication_id: str, user_id: str) -> dict:
if new_state:
await self._qstash.sync_schedules(medication_id, user_id)
else:
await self._qstash.delete_all_for_medication(medication_id)
DB 마이그레이션
ALTER TABLE schedules ADD COLUMN IF NOT EXISTS qstash_schedule_id TEXT DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_schedules_qstash_id ON schedules (qstash_schedule_id)
WHERE qstash_schedule_id IS NOT NULL;