
PillCare
* 현재 백엔드 서버가 중단되어 MSW(Mock)로 동작 중입니다.
프로젝트 회고
건강 위험 지수 설계 - 분산된 데이터를 하나의 점수로
복약 기록, 증상 일지, 외부 환경 데이터가 따로 존재하면 사용자는 지금 자신의 건강 상태가 어느 정도 위험한지 직관적으로 알기 어렵습니다. 이 문제를 해결하고자 5개 도메인을 가중합하는 0~100점 건강 위험 지수를 설계했습니다.
# 건강 위험 지수 (Health Index) 산출 - 5개 도메인 가중합
health_index = (
medication_adherence_score * 0.25 # 복약 순응도
+ disease_trend_score * 0.25 # 질환 추이
+ dur_safety_score * 0.20 # 식약처 DUR 안전성
+ symptom_trend_score * 0.15 # 증상 추이
+ environment_index * 0.15 # 환경지수 (날씨·미세먼지)
)각 도메인은 독립적으로 계산되어 DB에 캐싱되고, 스케줄러가 매일 자정 전체 사용자의 지수를 재계산합니다. 가중치는 만성질환 예방 관리 지침의 복약 순응도 중요도를 참고해 설정했습니다.
단일 점수로 내리는 것에 대한 고민이 있었는데, 점수를 내릴 때어떤 도메인이 낮은지를 함께 표시하는 방식으로 정보 손실 없이 요약값과 세부 원인을 동시에 전달하도록 설계했습니다.
환경 데이터 × 건강 알림 - 컨텍스트 기반 트리거
단순히 복약 시간을 알려주는 것에서 나아가, 오늘 환경이 특정 질환에 미치는 영향까지 알림에 반영하고 싶었습니다. 기상청·에어코리아·식약처 DUR 세 가지 외부 API를 스케줄러로 주기 수집해 알림 트리거 조건에 결합했습니다.
# 알림 트리거 조건 예시
# 고혈압 환자 + 기온 급변(일교차 10°C 이상) → 경고 알림
if user.has_disease("hypertension") and weather.temp_diff >= 10:
notify(user, level="WARNING", reason="기온 급변 - 혈압 변동 주의")
# 호흡기 질환 환자 + 미세먼지 나쁨(PM2.5 > 35) → 외출 자제 알림
if user.has_disease("respiratory") and air.pm25 > 35:
notify(user, level="CAUTION", reason="미세먼지 나쁨 - 외출 시 마스크 착용")외부 API 응답은 AsyncPG + FastAPI 비동기 구조로 처리했습니다. 날씨·미세먼지 API는 지역 코드 기반 응답이라 사용자 좌표를 행정구역 코드로 변환하는 매핑 테이블을 별도로 관리했습니다.
AI Hub ResNet - 알약 이미지로 약품명 추론
복약 등록 시 약품명을 직접 검색하는 것은 번거롭고 오기입 위험이 있습니다. AI Hub에서 제공하는 알약 이미지 분류 ResNet 모델을 활용해 사진 한 장으로 약품명을 자동 추론하는 파이프라인을 구성했습니다.
# 알약 인식 파이프라인
# 1. 프론트: 사용자가 알약 사진 촬영 → multipart/form-data로 업로드
# 2. 백엔드: AI Hub ResNet 모델로 추론 → 약품 코드 반환
# 3. 식약처 DUR API에서 약품 코드로 상세 정보 조회 → 복약 등록
# FastAPI 추론 엔드포인트
@router.post("/drugs/recognize")
async def recognize_pill(image: UploadFile):
img_tensor = preprocess(await image.read()) # 전처리
pred = model(img_tensor) # ResNet 추론
drug_code = label_map[pred.argmax().item()] # 코드 매핑
# 식약처 DUR API로 약품 상세 정보 조회
drug_info = await mfds_client.get_drug(drug_code)
return drug_info파인튜닝 없이 AI Hub 제공 가중치를 그대로 사용했기 때문에 인식률은 모델 성능에 의존합니다. 대신 추론 결과에 confidence score를 함께 반환해, 낮은 신뢰도일 때는 UI에서 "직접 검색"을 권유하는 폴백 흐름을 설계했습니다.
식약처 DUR 연동 덕분에 약품명뿐 아니라 병용 금기·주의 성분까지 등록 시점에 바로 표시할 수 있어 안전성 확인 흐름이 자연스럽게 이어졌습니다.
헬스케어 UI 설계 - 정보 계층과 위험도 시각화
헬스케어 앱에서 가장 중요한 것은 사용자가 정보를 읽는 데 인지 부하를 줄이는 것입니다. 이미지에 나타난 홈 대시보드를 설계할 때 3단 정보 계층을 의식적으로 구성했습니다.
홈 대시보드 3단 구조:
- 최상단 - AI 건강 브리핑 카드: 점수 + 오늘 주의사항 2줄 (스캔 단계)
- 중단 - 오늘 복약 현황: 복용/미복용 약 목록, 진행률 (확인 단계)
- 하단 - 건강 습관 트래커: 수면·운동·수분 섭취 (기록 단계)
위험 지수 숫자(38, 72 등)는 크기와 색상을 동시에 변환합니다. 낮은 점수는 파란색·작게, 높은 점수는 빨간색·크게 표시해 숫자를 읽지 않아도 위험도를 직관적으로 인지할 수 있도록 했습니다.
복약 검색 - 등록된 약 + 미등록 약 DB 검색 통합
복약 편집 화면에서 약을 추가할 때, 이미 등록된 약만 검색되면 사용자가 새로 먹기 시작한 약은 매번 이름을 처음부터 다시 입력해야 하는 문제가 있었습니다. 등록된 약 매칭과 서버 DB 검색을 한 검색창에서 함께 보여주도록 개선했습니다.
// 300ms 디바운스로 미등록 약 DB 검색
useEffect(() => {
if (!medSearch.trim()) { setApiSearchResults([]); return; }
const t = setTimeout(async () => {
const res = await fetch(`/api/drugs/search?q=${encodeURIComponent(medSearch)}`);
setApiSearchResults((await res.json()).results ?? []);
}, 300);
return () => clearTimeout(t);
}, [medSearch]);등록된 약 목록에서 먼저 매칭을 시도하고, 없으면 DB 검색 결과를 이어서 보여주는 순서로 배치했습니다. DB에서 찾은 신규 약은 바로 추가하지 않고복용 시간(아침/점심/저녁/자기전)을 먼저 선택하도록 확인 단계를 하나 더 둬서, 복용 시간 없이 등록되는 데이터를 막았습니다.
알림 정합성 - localStorage 이중 소스 제거
알림 벨의 빨간 점(안읽음 표시)이 하드코딩된 값으로 표시되고 있었고, 알림 설정 값도 localStorage와 백엔드에 각각 저장되어 두 값이 어긋나는 경우가 있었습니다. 상태를 백엔드 단일 소스로 좁히는 방향으로 정리했습니다.
// hooks/useUnreadCount.ts - 실제 미읽은 알림 수 기반 벨 표시 GET /events?unread_only=true // 알림 설정: localStorage 캐시 제거, 백엔드 응답만 신뢰
홈·분석·프로필 3개 페이지에서 벨 아이콘을 같은 훅으로 조건부 렌더링하도록 통일해, 페이지마다 안읽음 상태가 다르게 보이는 문제도 함께 없앴습니다.
성장과 배움
이 프로젝트를 통해 얻은 것:
- 건강 도메인 데이터 모델링 - 복약·질환·일지·예약·알림 6개 도메인 간 관계 설계
- FastAPI AsyncPG - Python 비동기 ORM으로 I/O 집약적 외부 API 호출 처리
- AI Hub 추론 파이프라인 - confidence score 기반 폴백으로 낮은 신뢰도 처리, 식약처 DUR 연동으로 병용 금기 등록 시점 표시
- 헬스케어 UI 설계 원칙 - 인지 부하 최소화를 위한 정보 계층 구조와 위험도 시각화
- 상태 단일 소스 원칙 - localStorage·하드코딩 값과 백엔드가 이중 소스가 되면서 생긴 정합성 버그를 겪고, 이후 백엔드를 유일한 소스로 두는 패턴을 습관화