Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

복약 알림 시스템 — 프로젝트 설명서

가족용 복약(약 챙겨먹기) 알림 봇. "이게 뭐고 어떻게 쓰는지"를 빠르게 파악하기 위한 문서다. 설계 결정의 이유(일반화된 형태)는 DESIGN.md를 볼 것 — 이 문서는 "무엇을 할 수 있고 어떻게 동작하는가"에 집중하고, "왜 이렇게 짰는가"는 대부분 DESIGN.md로 넘긴다.

참고: 이 저장소는 실제 운영 중인 개인 프로젝트를 이름/개인정보/실제 URL·ID를 제거하고 공개용으로 정리한 사본이다. 자세한 배경은 DESIGN.md 맨 위 참고.


1. 한눈에 보기

  • 매일 정해진 시각에 약 먹을 시간을 알려주고, 안 먹으면 재알림을 보내고, 먹으면 버튼 한 번으로 기록되는 개인/가족용 봇.
  • 주 채널은 텔레그램(버튼 UI). 텔레그램을 안 쓰는 가족을 위해 웹푸시 채널도 있음(가입부터 복약관리까지 텔레그램 없이 브라우저만으로 가능).
  • 기록은 Google Sheets에 쌓이고, **Google Apps Script(GAS)**가 백엔드 로직 전부를 담당한다.
  • 비용 0원을 목표로 설계됨 — 전부 무료 티어(GAS, Google Sheets, Telegram Bot API, Cloudflare Workers 무료 티어) 안에서 동작.

2. 전체 구성과 그 이유

                          ┌─────────────────────┐
SharpTools(물리 스위치) ──┼──────────────────────┼──▶ GAS doPost ──▶ Google Sheets(기록)
                          │  Cloudflare Worker   │         │
텔레그램(메시지/버튼) ────┼──────────────────────┼─────────┤         ├──▶ Google Calendar(나만)
                          │                      │         │
웹 브라우저(가입/개인관리) ┼──────────────────────┼─────────┘         └──▶ Telegram Bot API(알림 발송)
                          └──────────┬───────────┘
                                     │ VAPID Web Push
                                     ▼
                              가족 브라우저(웹푸시 알림)
구성 요소 역할 왜 이걸 썼나
Code.js (Google Apps Script) 전체 비즈니스 로직 — 스케줄 판단, 명령어 처리, 기록/취소, 알림 발송 무료, Google Sheets/Calendar와 네이티브로 연동, 트리거(cron)를 코드 안에 그대로 짤 수 있음
Google Sheets 복용 기록(메인 시트) + 누락 기록 + 실행 진단 로그, 3개 탭 사람이 직접 열어서 눈으로 훑어볼 수 있는 DB — 별도 DB 서버가 필요 없음
Script Properties 사람별 설정(임시약, 시각 오버라이드, 가입 대기열 등) GAS 자체 키-값 저장소, Sheets에 안 넣어도 되는 "설정성" 데이터용
Telegram Bot 주 채널 — 알림 수신, 버튼 탭, 명령어 입력 무료, 인라인 버튼 UI가 강력해서 타이핑 없이 대부분 조작 가능
worker.js (Cloudflare Worker) 텔레그램 웹훅 중계, 대시보드 인증, 웹푸시(VAPID/RFC8291 암호화) 발송 GAS doPost가 못 하는 것들을 대신함(바로 아래 "왜 Cloudflare Worker가 따로 있나" 참고) — 이 저장소엔 참고용 사본만 있고 실제 배포는 사용자가 Cloudflare 대시보드에 직접 코드를 붙여넣는 수동 작업
Web Push 텔레그램 없는 가족을 위한 보조 채널 카카오톡/SMS는 비용이 들어서 배제, Web Push는 브라우저 표준이라 무료
DESIGN.md 설계 결정 요약 코드 주석만으론 안 남는 "왜"를 일반화해서 기록
tests/harness*.js Node.js vm 샌드박스로 GAS 전역(PropertiesService/CacheService/SpreadsheetApp 등)을 흉내내서 Code.js를 그대로 실행·검증 GAS엔 로컬 실행 환경이 없어서, 실제 배포 전에 로직을 검증할 유일한 방법

왜 Cloudflare Worker가 따로 있나

  1. 텔레그램 웹훅 중계: 텔레그램은 GAS 웹앱이 돌려주는 302 리다이렉트를 안 따라간다. Worker가 대신 따라가서 최종 응답을 텔레그램에 전달.
  2. doPost가 HTTP 헤더를 못 읽음: 텔레그램의 웹훅 위조 방지용 시크릿 헤더 검증은 Worker에서만 가능 → Worker가 검증하고 _workerAuth라는 내부 공유 시크릿을 body에 심어 GAS에 전달 (자세한 배경은 DESIGN.md의 "보안" 항목).
  3. 대시보드 인증: 브라우저 표준 Basic Auth 팝업으로 로그인받은 뒤 GAS로 리다이렉트.
  4. 웹푸시 암호화(VAPID/RFC8291): GAS엔 타원곡선 서명 기능이 없어서, Worker의 crypto.subtle로 직접 구현(외부 라이브러리 없음). GAS는 "누구에게 뭘 보내라"만 Worker에 요청 — 이 방향(GAS→Worker)은 텔레그램 웹훅 방향(Worker→GAS)과 반대라 양방향 통신이 된다.

3. 할 수 있는 일 (기능 목록)

사용자(가족 구성원)가 할 수 있는 것

  • 복용 완료 기록(버튼 1번 또는 /take), 90초 안에 취소(/undo)
  • 오늘 복약 현황 조회(/status), 최근 30일 누락 조회(/miss)
  • 스누즈(10분/30분 후 다시 알림) — 리마인더 버튼에서 바로
  • 임시약(감기약 등 단기 처방) 추가/시각수정/기간연장/중단 — 전부 버튼으로("🔧 복약 조정")
  • 고정약(매일 복용) 추가/시각수정/제거 — 텔레그램으로 등록된 사람만(코드에 박힌 기본 두 사람은 코드를 고쳐야 해서 불가)
  • 주기형 약(예: 특정 요일 패턴으로 복용/휴약이 반복되는 약) 일시중지/재개(/pause_*, /resume_*), 리필 알림
  • 처방 갱신(/refill)
  • 텔레그램 없이: 웹 링크로 가입 신청(관리자 승인 필요) → 이후 개인 관리 페이지(/push/me)에서 위 기능 대부분을 직접 수행 가능(단, 능동 알림 몇 종은 아직 웹푸시 미지원)

관리자(me)만 할 수 있는 것

  • 신규 가입 승인/거절 (텔레그램 버튼)
  • 다른 사람 대신 관리 — 약 추가/수정/취소를 _for 명령어로 대리 수행
  • 사람 이름 변경/제거(/rename_for, /removeperson, /people의 "⚙️ 관리" 버튼)
  • 전체 가족 현황을 한 번에 조회(/status가 전원 표시)
  • 대시보드(웹, Basic Auth)로 가족 전체 현황을 브라우저에서 확인

자동으로 일어나는 것 (트리거)

  • 매분: 모든 사람 × 모든 약의 체크/재알림 시각 폴링, 스누즈 큐 처리
  • 매일: 처방 갱신 알림, 주기형 약 리필 알림, 트리거 상태 점검, 스누즈 트리거 정리, 공휴일 데이터 점검, 어제자 누락 기록 저장
  • 매주/매월(선택): 주간 리포트, 월간 백업
  • 상시: 실행이 느려지거나(🐢) 에러가 나면(🚨) 텔레그램 + 실행진단로그 시트로 자동 보고 (자세한 배경은 DESIGN.md의 "실행 진단" 항목)

4. 명령 체계 (요청이 어떻게 처리되나)

진입점은 4가지

진입점 실행 방식 코드 반영 시점
checkAllTimedEvents 1분 타이머 트리거 GAS 편집기 저장 즉시(clasp push만 하면 됨, "Head" 실행)
dailyChecks/weeklyReport/monthlyBackup 시간 기반 트리거 위와 동일
doPost 웹앱 POST — 텔레그램 웹훅, SharpTools, 웹푸시(가입/버튼/개인페이지) 새 배포(버전)를 만들어야 반영됨
doGet 웹앱 GET — 대시보드 위와 동일

doPost/doGet은 특정 배포 버전에 고정되기 때문에, 트리거 함수와 달리 코드를 고쳐도 웹앱을 재배포해야 실제로 반영된다.

doPost 내부 라우팅 순서 (위에서부터 먼저 매칭되는 걸로 처리)

  1. message/callback_query(텔레그램) — _workerAuth 필수(Worker를 거쳐야만 통과)
  2. _pushResolveLabel/_pushJoin/_pushAction/_pushMe(웹푸시, Worker가 전달) — 전부 _workerAuth 필수
  3. 그 외(SharpTools) — 자체 SECRET_KEY로 인증

텔레그램 텍스트 명령어 라우팅

COMMAND_ROUTES(Code.js)라는 {접두사: 핸들러} 맵을 접두사 길이 내림차순으로 순회해서 매칭한다(/addtemp_gf/addtemp보다 항상 먼저 검사되도록). 새 명령어는 이 맵에 한 줄만 추가하면 됨.

버튼(인라인 키보드) 라우팅

버튼을 누르면 callback_data 문자열(예: log_me_morning, snooze_10_gf_pill)이 오고, handleTelegramCallback이 첫 토큰(action)으로 분기한다. 대부분의 기능(약 추가/수정/제거, 사람 관리, 가입 승인 등)이 결국 타이핑 없이 이 버튼 흐름만으로 끝까지 가능하도록 되어 있다.

대화형 마법사(wizard)

"며칠 드시나요?" 같은 여러 단계 질문-답변이 필요한 흐름은 wizard_<personKey> Script Property에 {mode, step, target, ...그때까지 모은 값, startedAt} 형태로 진행 상태를 저장한다. 사용자가 텍스트를 보낼 때마다 handleTelegramMessage가 이 상태를 읽어서 (getWizardState) mode별 핸들러로 분기하고, 그 핸들러가 다음 질문을 보내며 step을 한 단계 전진시킨다(setWizardState). 아무 단계에서나 "취소"라고 보내면 즉시 종료된다.

TTL(슬라이딩 윈도우): 마지막 응답으로부터 1분(WIZARD_TTL_MS)이 지나면 자동 폐기된다. 매 단계 응답마다 startedAt이 갱신되므로 활발히 답하는 동안은 6단계짜리 주기약 등록도 안전하게 끝까지 진행되고, 반대로 답하다 방치하면 며칠 뒤 보낸 아무 텍스트가 옛 마법사의 답변으로 잘못 소비되는 사고를 막는다.

마법사 종류와 단계

mode 진입 경로 단계 순서
add ➕ 약 추가 → 대상 선택 → 🤒 단기 임시약 며칠 드시나요 → 하루 몇 번 → 약 이름
adddaily/addcycle ➕ 약 추가 → 대상 선택 → ☀️ 매일 고정약/🔄 주기형 약 약 이름 → 체크시각 → (주기형만: 복용일수 → 휴약일수 → 시작일 → 재알림시각)
edit 🔧 복약 조정 → 시각 수정 → 대상 → 임시약 선택 새 시각(들, 콤마 구분)
edittime 🔧 복약 조정 → 시각 수정 → 대상 → 고정약 선택 새 시각
extendtemp 🔧 복약 조정 → 기간 연장 → 대상 → 약 선택 연장 일수
renameperson /people → ⚙️ 관리 → 이름 변경 새 이름

질문 순서 원칙: "➕ 약 추가"와 "🔧 복약 조정" 둘 다 가장 많은 후속 선택지를 좌우하는 질문을 먼저 물어보도록 설계돼 있다(복약 조정: 누구 → 뭘 할지 → 어떤 약, 약 추가: 누구 → 어떤 종류). 다음 단계에서 실제로 가능한 선택지만 필터링해서 보여줄 수 있어야 "골랐는데 안 되는" 경험을 막을 수 있기 때문 — 두 메뉴 다 처음엔 반대 순서였다가 이 원칙에 맞춰 재정렬된 히스토리가 있다(자세한 배경은 DESIGN.md의 "대화형 상태 머신(마법사)과 질문 순서 설계" 항목).

웹/푸시 경로 (Worker, worker.js)

경로 용도 인증
/ 가족 전체 대시보드 Basic Auth → GAS DASHBOARD_TOKEN
/push/join 텔레그램 없이 새로 가입 없음(관리자 텔레그램 승인이 실제 보안 경계)
/push/subscribe 기존 사람이 이 기기에서 처음 구독 Basic Auth
/push/me 본인 개인 관리 페이지(취소/추가/수정) 없음, 구독 endpoint 자체가 신원증명
/push/action, /push/send, /push/promote 버튼 탭 처리, GAS↔Worker 내부 통신 endpoint 일치 확인 / _workerAuth

5. 명령어 전체 목록

실제 최신 목록은 텔레그램에서 /help(코드의 handleHelpCommand())를 치면 본인 권한에 맞게 보여준다. 아래는 요약.

누구나

명령어 설명
/menu 하단 버튼 메뉴 고정
/take 알림 기다리지 않고 즉시 복용 완료 처리
/status 오늘 복약 현황(완료 항목은 취소 버튼도 같이)
/undo 방금 기록 취소(90초 이내)
/miss 최근 30일 누락 조회
/edittime, /edittemp (인자 없이) "🔧 복약 조정" — 고정약+임시약 통합 목록에서 시각수정/연장/중단
/edittime <약키> <시각> 고정약 체크 시각 직접 지정
/edittime <약키> retry <시각> 재알림 시각 직접 지정
/addtemp <하루횟수> <일수> <이름> 또는 /addtemp <시각들> <일수> <이름> 임시약 추가
/extendtemp, /removetemp 임시약 연장/중단(타이핑 버전, "🔧 복약 조정"과 동일 기능)
/refill[_YYYY-MM-DD] 처방 갱신 (고정 매일약이 있는 사람만 노출)
/pause_<약키>, /resume_<약키>[_YYYY-MM-DD] 주기형 약 휴약/재개

관리자(me)만

명령어 설명
/people 등록된 사람/약 목록 (사람마다 "⚙️ 관리"로 이름변경/추방)
/addmed_for <대상> <이름> <시각> 대신 고정약(매일) 추가
/addcyclemed_for <대상> <이름> <시각> <복용일수> <휴약일수> <시작일> 대신 주기형 약 추가
/rename_for <대상> <새이름> 등록된 사람 이름 변경
/removeperson <대상> 등록된 사람 제거(과거 기록은 유지)
/addtemp_for, /edittemp_for, /extendtemp_for, /removetemp_for <대상> ... 임시약 대리 관리
/test_morning, /test_evening, /test_gfpill, /test_gfpillretry, /test_daily, /test_timed, /test_holiday 트리거를 기다리지 않고 즉시 테스트 실행

새 사람 등록

  • 텔레그램으로: 등록 안 된 사람이 봇에게 아무 메시지나 보내면 "이용하기" 버튼 → 이름 입력 → 관리자 승인.
  • 텔레그램 없이: Worker의 /push/join 링크로 이름 입력 → 관리자 승인(텔레그램 버튼은 동일).

6. 더 깊이 알고 싶다면

  • 왜 이렇게 짰는지(일반화된 설계 교훈): DESIGN.md.
  • 실제 구현: Code.js(GAS 백엔드), worker.js(Cloudflare Worker, 참고용 사본).
  • 동작이 실제로 맞는지 검증: tests/harness*.js — Node.js로 바로 실행 가능 (node tests/harnessN.js), GAS 서비스(Properties/Cache/Spreadsheet 등)를 흉내낸 샌드박스 안에서 Code.js를 그대로 돌려서 확인한다.
  • 직접 배포하는 데 필요한 설정값 목록: DESIGN.md 맨 아래 섹션.

About

A serverless medication reminder bot for families — Google Apps Script + Telegram Bot + Cloudflare Workers + Web Push, running entirely on free tiers.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages