피트크루
경력근거: 만성질환자들의 복약관리를 돕는 Pillo앱을 운영하고 있습니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고는 피트크루를 '만성질환자들의 복약관리를 돕는 Pillo앱을 운영하고 있습니다' 라고 소개하고, Pillo 가 '북미에서 가장 사랑받는 복약 관리앱' 이 되었다고 밝힙니다. 이 자리의 일은 React·TypeScript 로 모바일 웹을 만드는 것이고, 자격요건은 '웹뷰 기반 하이브리드 앱 환경' 에서 일해 본 경험과 앱과 웹 사이 브릿지 통신('JavaScript Interface, postMessage') 설계 경험을 따로 묻습니다. 진료 전 리포트는 앱이 가진 복약 기록을 웹뷰로 받아 판정해 보여주고, 다시 앱에 PDF 를 만들어 공유 시트를 열어 달라고 요청하는 화면이라 그 두 요구가 한 기능 안에서 오갑니다. 약 여러 개를 서로 다른 일정으로 먹는 만성질환자가 주 사용자인 앱이어야 약마다 일정을 펼쳐 세는 계산이 쓸모 있고, 우대사항의 '모바일 웹 성능 최적화(초기 로딩 속도, Core Web Vitals 등)' 는 진료 전날 이 리포트를 처음 여는 그 화면에서 잽니다. 이 포트폴리오는 복약 기록을 세는 판정 계산과 웹뷰·앱 브릿지, PDF 내보내기를 보여줍니다. 복약이나 임상 전문성은 주장하지 않습니다.
공고는 Pillo 를 만성질환자의 복약관리를 돕는 앱으로 소개하고, 북미에서 가장 사랑받는 복약 관리앱이 되었다고 밝힙니다. 여러 약을 서로 다른 일정으로 먹는 사용자가 주인 앱이라, 이 편은 약마다 일정을 펼쳐 지난 진료 뒤 처방대로 먹었는지 세는 리포트를 그 사용자의 화면으로 둡니다.
자격요건은 React·TypeScript 프론트엔드 경험에 더해 웹뷰 기반 하이브리드 앱 환경, JavaScript Interface·postMessage 같은 앱·웹 브릿지 설계 경험을 요구합니다. 리포트는 셸 앱에서 네 표를 받아 오고 다시 셸 앱에 PDF 를 만들어 달라고 요청하므로, 메시지 계약과 시간 제한, 실패 처리가 이 편의 뼈대입니다.
우대사항은 모바일 웹 초기 로딩 속도와 Core Web Vitals 최적화, React Native, AI 활용과 멀티 에이전트 오케스트레이션을 꼽습니다. 이 편은 웹뷰 안에서 LCP·INP·CLS 를 재 빌드마다 남기고, 셸 앱을 React Native 로 만들며, 걸린 약의 패턴 문장과 경계 테스트 분담에 Claude 를 씁니다.
피트크루는 북미 만성질환자가 쓰는 복약 관리 앱 Pillo 를 운영하고, 이 화면은 그 앱 안 웹뷰로 여는 진료 전 리포트입니다. 사용자가 등록한 약별 일정(매일·요일·간격·필요시)을 지난 진료부터 리포트를 연 시각까지 회차로 펼치고, 알림에 누른 복용·건너뛰기·미루기 기록과 남은 약 개수·리필 기록을 견줘 약마다 다섯 판정 가운데 하나를 냅니다. 사용자는 진료 전날 판정과 근거 숫자를 읽은 뒤, 같은 화면을 PDF 로 만들어 의사에게 보내거나 진료실에서 큰 글씨로 띄워 보여줍니다.
Pillo 로 혈압약·당뇨약처럼 여러 만성질환 약을 서로 다른 일정으로 챙기는 북미 사용자. 진료 예약 전날 앱에서 지난 진료 이후 약별 판정을 보고, 그 화면을 진료실에서 보여 주거나 PDF 로 보내 '거의 다 먹었다' 는 기억 대신 기록으로 말한다
만성질환자가 진료실에서 '거의 다 먹었다' 고 말하는 것은 기록이 없어서가 아닙니다. Pillo 는 알림마다 복용·건너뛰기·미루기 응답을 남기지만, 그 기록은 약별·회차별로 흩어져 있어 석 달치를 진료 전에 다시 세어 보는 사람이 없습니다. 일정이 중간에 바뀐 약, 약이 떨어져 못 먹은 닷새, 미루기를 누른 뒤 늦게 먹은 아침을 규칙으로 가려 약마다 판정 하나로 정리하면, 환자는 기억 대신 그 숫자로 말합니다.
Expo 셸 앱이 더미 네 표를 담고 웹뷰로 리포트 페이지를 엽니다. 사용자는 10월 16일 진료 앞에서 약 여섯 개의 판정과 근거 숫자, 걸린 약의 패턴 문장을 읽고, PDF로 보내기를 누르면 앱이 같은 내용을 A4 한 장 PDF 로 만들어 공유 시트를 엽니다. Pillo 의 실제 서버·알림과의 연결은 이 범위 밖이고, 네 표는 Pillo 가 가진 데이터 모양을 가정한 더미입니다.
기준값 바로 안팎을 짝으로 둔 경계 사례 60건이 매번 돌고, 약별 달력 탭에서 사용자가 92일 각 날의 복용·늦음·건너뜀·응답 없음·약 없음을 칸으로 보며 판정이 기억과 맞는지 확인합니다. 웹뷰 안에서 잰 LCP·INP·CLS 를 브릿지로 앱에 보내 빌드마다 남깁니다.
2주차에 잰 숫자를 기준으로 리포트 첫 화면을 정적 셸로 먼저 그리고 판정 계산을 Web Worker 로 옮겨, 바꾸기 전후 LCP 를 표로 냅니다. PDF 를 보낼 때 그 리포트를 남겨 두고, 다음 진료 리포트의 약 카드에 지난번 판정을 나란히 적어 사용자가 무엇이 달라졌는지 읽습니다.
사용자가 진료 전날 리포트를 열면 7월 15일 진료 뒤 약 여섯 개가 다섯 판정으로 나뉘고, Metformin 카드에는 건너뛴 44회 가운데 41회가 저녁 회차라는 문장과 근거 숫자가 뜹니다. PDF로 보내기를 누르면 같은 내용이 A4 한 장 PDF 로 공유 시트에 올라옵니다.
정확히 2시간 늦게 먹은 Magnesium 회차는 늦은 날로 세지 않아 23.3% 로 처방대로 먹음에 남고, 8월 20일에 알림 시각이 바뀐 Levothyroxine 은 바뀌기 전 회차를 07:00 으로 펼쳐 응답 없음이 3회에 그칩니다. 미루기 뒤 복용한 회차는 마지막 응답인 복용 하나로 셉니다.
visit_at 이 null 인 진료 예약, 예정 시각이 없는 필요시 약 기록, 셸 앱 응답이 3초 안에 오지 않는 경우를 섞은 입력 30종에서도 화면이 깨지지 않고, 판정에서 뺀 기록은 이유를 달고 리포트 아래에 남습니다.
기준값 바로 안팎을 짝으로 둔 경계 사례 60건 가운데 판정이 기대와 맞은 건수를 화면의 자가채점 칸에 적습니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 결과는 더미데이터 자가채점이며, 이 리포트를 들고 간 환자가 기억 대신 기록으로 말하게 되는지, 의사가 그 판정을 진료에 쓸 만하다고 보는지는 피트크루의 실제 사용자와 진료 현장이 있어야 측정됩니다. 이 포트폴리오는 그 측정을 바로 돌릴 수 있는 상태로 만듭니다.
약 여섯 개의 일정과 92일치 응답 기록, 기대 판정은 모두 제가 만든 합성 데이터라 자가채점 결과는 성능 증명이 아닙니다. 알림에 누른 응답이 실제로 먹은 것과 얼마나 맞는지가 이 리포트의 바탕인데, 그 차이는 Pillo 의 실제 사용자 기록에만 있습니다.
판정이 경계 하나로 뒤집히는 약을 일부러 넣었습니다. Levothyroxine 을 8월 20일에 바뀐 06:30 일정 하나로 92일 전부 펼치면 07:00 에 먹은 35일이 응답 없음으로 잡혀 37회(40.2%)가 되고 판단 못 함으로 바뀝니다. Magnesium 의 9월 26일 회차는 정확히 2시간 늦어 '넘게' 를 '이상' 으로 읽으면 늦은 날이 8일(26.7%)로 늘어 늦게 먹은 날이 많음으로 바뀝니다.
리필을 앱에 적지 않은 사용자는 남은 개수가 0 아래로 굴러가고, 그 뒤로 건너뛴 날이 모두 약이 떨어진 날로 잡힙니다. 남은 개수가 0 아래인데 복용 기록이 이어지면 리필을 빠뜨린 것으로 보고 그 약의 약 떨어짐 판정을 보류할지는, 2주차 경계 사례에 이 경우를 넣어 본 뒤 정합니다.
진료 예약을 의사 구분 없이 한 줄로 보니, 두 정기 진료 사이에 다른 과 진료가 끼면 판정 기간이 그 진료부터로 짧아집니다. 예약에 의사나 진료과를 붙여 같은 의사의 지난 진료부터 세는 일은 실제 도입에서 먼저 할 과제입니다.
Pillo 사용자와 의사는 영어를 쓰므로 실제 화면과 PDF 문구는 영어여야 합니다. 이 포트폴리오의 예시는 한국어로 적었고, 화면 문구는 문구 사전 한 곳에 모아 언어를 바꿀 수 있게 둡니다.
이 편은 복약 응답 기록을 세어 판정하는 계산과 웹뷰·셸 앱 사이 브릿지, PDF 내보내기를 다룹니다. 복약이나 임상 전문성은 주장하지 않습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
사용자가 Pillo 에 등록한 약 일정입니다. 한 행이 일정 한 판이라, 지난 진료에서 Metformin 이 하루 2회로 늘거나 Levothyroxine 알림이 07:00 에서 06:30 으로 당겨지면 앞 행에 end_date 가 찍히고 새 행이 생깁니다. 지난 진료 전에 끝난 항생제 일정과 예정 회차가 없는 필요시 흡입기가 섞입니다.
| med_id | med_label | rule | times | dose_qty | start_date | end_date |
|---|---|---|---|---|---|---|
| med_01 | Lisinopril 10mg | 매일 | ["08:00"] | 1 | 2026-01-12 | — |
| med_02 | Metformin 500mg | 매일 | ["08:00"] | 1 | 2026-01-12 | 2026-07-14 |
| med_02 | Metformin 500mg | 매일 | ["08:00","20:00"] | 1 | 2026-07-15 | — |
| med_03 | Atorvastatin 20mg | 매일 | ["21:00"] | 1 | 2026-01-12 | — |
| med_04 | Levothyroxine 50mcg | 매일 | ["07:00"] | 1 | 2026-04-20 | 2026-08-19 |
아래 JSON Schema 에 맞는 약 일정 행을 더 만들어 주세요. {"type":"object","properties":{"med_id":{"type":"string"},"med_label":{"type":"string","description":"사용자가 등록한 약 이름과 함량입니다. 화면 표시용이고 판정에는 쓰지 않습니다."},"rule":{"type":"string","pattern":"^(매일|요일:[월화수목금토일](,[월화수목금토일])*|간격:[0-9]+일|주기:[0-9]+일 복용 [0-9]+일 쉼|필요시)$","description":"반복 규칙입니다. 간격:N일 은 start_date 부터 N일마다, 주기는 복용 일수와 쉬는 일수를 번갈아 갑니다. 필요시 약은 예정 회차가 없습니다."},"times":{"type":"array","items":{"type":"string","pattern":"^[0-2][0-9]:[0-5][0-9]$"},"description":"복용일마다 알림이 울리는 현지 시각입니다. 필요시 약은 빈 배열입니다."},"dose_qty":{"type":"integer","minimum":1,"description":"한 회차에 먹는 알 수입니다."},"start_date":{"type":"string","format":"date"},"end_date":{"type":["string","null"],"format":"date","description":"이 일정의 마지막 날(포함)입니다. 계속 먹는 약은 null 입니다."}},"required":["med_id","med_label","rule","times","dose_qty","start_date","end_date"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 같은 med_id 에서 앞 행 end_date 다음 날이 뒷 행 start_date 인 일정 변경(복용 시각이나 하루 횟수가 바뀜), (2) 판정 기간 한가운데서 end_date 로 끝나는 약, (3) rule 이 요일:일 처럼 일주일에 한 번인 약과 간격:2일·주기:21일 복용 7일 쉼 인 약, (4) rule 이 필요시이고 times 가 빈 배열인 약, (5) 판정 기간 전에 끝나 회차가 하나도 펼쳐지지 않는 약. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"med_id":{"type":"string"},"med_label":{"type":"string","description":"사용자가 등록한 약 이름과 함량입니다. 화면 표시용이고 판정에는 쓰지 않습니다."},"rule":{"type":"string","pattern":"^(매일|요일:[월화수목금토일](,[월화수목금토일])*|간격:[0-9]+일|주기:[0-9]+일 복용 [0-9]+일 쉼|필요시)$","description":"반복 규칙입니다. 간격:N일 은 start_date 부터 N일마다, 주기는 복용 일수와 쉬는 일수를 번갈아 갑니다. 필요시 약은 예정 회차가 없습니다."},"times":{"type":"array","items":{"type":"string","pattern":"^[0-2][0-9]:[0-5][0-9]$"},"description":"복용일마다 알림이 울리는 현지 시각입니다. 필요시 약은 빈 배열입니다."},"dose_qty":{"type":"integer","minimum":1,"description":"한 회차에 먹는 알 수입니다."},"start_date":{"type":"string","format":"date"},"end_date":{"type":["string","null"],"format":"date","description":"이 일정의 마지막 날(포함)입니다. 계속 먹는 약은 null 입니다."}},"required":["med_id","med_label","rule","times","dose_qty","start_date","end_date"]}알림에 사용자가 누른 응답 기록입니다. 아무 것도 누르지 않은 회차는 행이 없습니다. 미루기를 누른 뒤 나중에 복용을 누르면 같은 회차에 기록이 둘 남고, 필요시 약을 직접 기록하면 scheduled_at 이 null 입니다. 92일치 전체는 수백 행이라 여기에는 판정을 가르는 기록만 실었고, 나머지는 validation_notes 의 펼침 규칙으로 만듭니다.
| med_id | scheduled_at | action | logged_at |
|---|---|---|---|
| med_02 | 2026-07-15T20:00:00-04:00 | 복용 | 2026-07-15T20:05:00-04:00 |
| med_02 | 2026-09-04T20:00:00-04:00 | 건너뛰기 | 2026-09-04T20:02:00-04:00 |
| med_02 | 2026-10-14T20:00:00-04:00 | 건너뛰기 | 2026-10-14T20:02:00-04:00 |
| med_03 | 2026-09-01T21:00:00-04:00 | 복용 | 2026-09-01T21:05:00-04:00 |
| med_03 | 2026-09-02T21:00:00-04:00 | 건너뛰기 | 2026-09-02T21:02:00-04:00 |
아래 JSON Schema 에 맞는 복약 응답 기록을 더 만들어 주세요. scheduled_at 은 med_schedules 의 rule·times 로 펼친 회차 시각과 같아야 합니다. {"type":"object","properties":{"med_id":{"type":"string"},"scheduled_at":{"type":["string","null"],"format":"date-time","description":"알림이 울린 예정 회차의 시각입니다. 필요시 약을 사용자가 직접 기록하면 null 입니다."},"action":{"type":"string","enum":["복용","건너뛰기","미루기"],"description":"사용자가 알림에 누른 응답입니다."},"logged_at":{"type":"string","format":"date-time","description":"응답을 누른 시각입니다."}},"required":["med_id","scheduled_at","action","logged_at"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 같은 med_id·scheduled_at 에 미루기 기록이 있고 몇 시간 뒤 복용 기록이 이어지는 회차, (2) 미루기만 있고 끝내 복용도 건너뛰기도 없는 회차, (3) logged_at 이 scheduled_at 보다 정확히 2시간 뒤인 복용과 2시간 1분 뒤인 복용, (4) 22:00 회차를 자정을 넘겨 다음 날 복용한 기록, (5) 응답 행이 아예 없는 회차를 일주일 넘게 이어서 비워 둔 약, (6) scheduled_at 이 null 인 필요시 약 기록. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"med_id":{"type":"string"},"scheduled_at":{"type":["string","null"],"format":"date-time","description":"알림이 울린 예정 회차의 시각입니다. 필요시 약을 사용자가 직접 기록하면 null 입니다."},"action":{"type":"string","enum":["복용","건너뛰기","미루기"],"description":"사용자가 알림에 누른 응답입니다."},"logged_at":{"type":"string","format":"date-time","description":"응답을 누른 시각입니다."}},"required":["med_id","scheduled_at","action","logged_at"]}남은 약 개수를 직접 센 기록과 리필 기록입니다. 지난 진료 뒤 약국에서 받아 온 날 개수를 적은 행에서 굴리기를 시작하고, 9월 20일 Metformin 처럼 다시 센 값이 굴린 값(67정)과 다르면 센 값(65정)으로 맞춥니다. 남은 개수를 적지 않는 약(Vitamin D3)은 행이 없습니다.
| med_id | event_at | pill_count | refill_qty |
|---|---|---|---|
| med_01 | 2026-07-15T13:05:00-04:00 | 30 | — |
| med_01 | 2026-08-10T17:40:00-04:00 | — | 90 |
| med_02 | 2026-07-15T12:30:00-04:00 | 180 | — |
| med_02 | 2026-09-20T09:00:00-04:00 | 65 | — |
| med_03 | 2026-07-15T22:05:00-04:00 | 48 | — |
아래 JSON Schema 에 맞는 남은 약·리필 기록을 더 만들어 주세요. {"type":"object","properties":{"med_id":{"type":"string"},"event_at":{"type":"string","format":"date-time"},"pill_count":{"type":["integer","null"],"minimum":0,"description":"사용자가 직접 센 남은 개수입니다. 이 값이 있으면 그 시각의 남은 개수를 이 값으로 다시 맞춥니다. 리필 행은 null 입니다."},"refill_qty":{"type":["integer","null"],"minimum":1,"description":"리필로 더한 개수입니다. 개수 입력 행은 null 입니다. 한 행에는 둘 중 하나만 채웁니다."}},"required":["med_id","event_at","pill_count","refill_qty"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 남은 개수가 0 이 된 지 며칠 뒤에야 들어온 리필, (2) 0 이 된 날 같은 날 오전에 들어온 리필, (3) 굴린 값과 몇 정 다른 값으로 다시 센 pill_count, (4) 리필을 적지 않아 남은 개수가 0 아래로 내려가는데 복용 기록은 이어지는 약, (5) 남은 개수를 한 번도 적지 않은 약. 한 행에는 pill_count 와 refill_qty 가운데 하나만 채웁니다. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"med_id":{"type":"string"},"event_at":{"type":"string","format":"date-time"},"pill_count":{"type":["integer","null"],"minimum":0,"description":"사용자가 직접 센 남은 개수입니다. 이 값이 있으면 그 시각의 남은 개수를 이 값으로 다시 맞춥니다. 리필 행은 null 입니다."},"refill_qty":{"type":["integer","null"],"minimum":1,"description":"리필로 더한 개수입니다. 개수 입력 행은 null 입니다. 한 행에는 둘 중 하나만 채웁니다."}},"required":["med_id","event_at","pill_count","refill_qty"]}사용자가 앱에 적어 둔 진료 예약입니다. 리포트를 연 시각보다 앞선 예약 가운데 가장 늦은 것이 지난 진료, 뒤에 오는 첫 예약이 이번 진료입니다. 날짜를 정하지 않고 이름만 적어 둔 안과 검진은 visit_at 이 null 이라 고르는 대상에서 뺍니다. 1월 진료는 동부 표준시(-05:00)로 적혀 있습니다.
| appt_id | visit_at | title |
|---|---|---|
| appt_101 | 2024-10-10T09:40:00-04:00 | Dr. Emily Chen 정기 진료 |
| appt_102 | 2025-01-16T09:40:00-05:00 | Dr. Emily Chen 정기 진료 |
| appt_103 | 2025-04-17T10:00:00-04:00 | Dr. Emily Chen 정기 진료 |
| appt_104 | 2025-07-17T09:20:00-04:00 | Dr. Emily Chen 정기 진료 |
| appt_105 | 2025-10-16T09:40:00-04:00 | Dr. Emily Chen 정기 진료 |
아래 JSON Schema 에 맞는 진료 예약 행을 더 만들어 주세요. {"type":"object","properties":{"appt_id":{"type":"string"},"visit_at":{"type":["string","null"],"format":"date-time","description":"진료 시각입니다. 날짜를 정하지 않고 적어 둔 예약은 null 입니다."},"title":{"type":"string","description":"사용자가 붙인 예약 이름입니다. 화면 표시용입니다."}},"required":["appt_id","visit_at","title"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) visit_at 이 null 인 날짜 미정 예약, (2) 서머타임 앞뒤로 UTC 오프셋이 -04:00 과 -05:00 으로 갈리는 예약, (3) 리포트를 연 시각 바로 다음 날 아침의 예약, (4) 두 정기 진료 사이에 끼어든 다른 과 진료. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"appt_id":{"type":"string"},"visit_at":{"type":["string","null"],"format":"date-time","description":"진료 시각입니다. 날짜를 정하지 않고 적어 둔 예약은 null 입니다."},"title":{"type":"string","description":"사용자가 붙인 예약 이름입니다. 화면 표시용입니다."}},"required":["appt_id","visit_at","title"]}| TypeScript | 판정 엔진(일정 펼치기, 회차별 마지막 기록 고르기, 남은 개수 굴리기, 판정 차례)을 순수 함수로 짜고, 브릿지 메시지 타입을 웹과 셸 앱이 같은 정의로 씁니다. |
| React | 리포트 화면을 만듭니다. 판정 그룹, 약 카드, 펼치면 나오는 근거 표, 하단 보내기 바, 진료실용 큰 글씨 모드입니다. |
| Next.js | App Router 의 /report/[apptId] 페이지를 정적 내보내기(static export)로 빌드해 웹뷰가 엽니다. 같은 컴포넌트로 PDF 용 인쇄 HTML 도 그립니다. |
| 웹뷰 기반 하이브리드 앱 | Expo(React Native) 셸 앱의 react-native-webview 가 리포트 페이지를 띄우고, 하단 탭은 셸 앱이 그립니다. 더미 네 표는 셸 앱의 SQLite 에 둡니다. |
| 앱·웹 브릿지 통신(postMessage) | postMessage 메시지 계약 {v, id, type, payload} 을 정합니다. 웹이 report.ready 를 보내면 셸 앱이 report.data 로 네 표를 넘기고, 웹이 report.pdf 로 인쇄용 HTML 을 보내면 셸 앱이 PDF 를 만들어 report.pdf.done 으로 답합니다. 3초 안에 답이 없으면 bridge.error 로 끝냅니다. Android JavascriptInterface·iOS WKScriptMessageHandler 자리는 onMessage·injectJavaScript 가 같은 계약으로 받습니다. |
| HTML/CSS | @media print 로 A4 한 장 레이아웃을 짜고, 큰 글씨 모드는 루트 글자 크기만 바꿔 같은 마크업을 씁니다. |
| React Native | 셸 앱입니다. expo-print 로 받은 HTML 을 PDF 로 만들고 expo-sharing 으로 공유 시트를 엽니다. |
| 모바일 웹 성능 최적화(Core Web Vitals) | 2주차에 web-vitals 로 웹뷰 안 LCP·INP·CLS 를 재 브릿지로 셸 앱에 보내고, 3주차에 정적 셸과 Web Worker 로 첫 화면을 줄인 뒤 같은 방식으로 다시 잽니다. |
| AI 활용 | Claude Haiku 4.5 가 걸린 약마다 패턴 문장 하나를 씁니다. 개발은 Claude Code 로 합니다. |
| 멀티 에이전트 오케스트레이션 | 2주차 경계 사례 60건을 Claude Code 서브에이전트 셋(일정 펼치기, 남은 개수 굴리기, 판정 차례)이 나눠 쓰고, 메인 에이전트가 모아 전체 테스트를 돌립니다. |
✓ pass → 리포트를 보고 PDF 로 보내는 흐름이 돌면 그 자체로 사용자가 진료 전날 쓰는 화면입니다. 더 키우려면 경계 사례 60건을 매번 돌리고 약별 달력으로 날짜마다 다시 보는 단계로 갑니다.
✓ pass → 2주차까지로도 완결입니다. 더 키우려면 잰 숫자를 기준으로 첫 화면을 줄이고, 보낸 리포트를 남겨 다음 진료 리포트에 지난번 판정을 나란히 적는 단계로 갑니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · med_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
처방대로 먹음처방대로자주 건너뜀자주 건너뜀늦게 먹은 날이 많음늦은 날 많음약이 떨어져 못 먹은 날이 있음약 떨어짐기록이 비어 판단 못 함기록 부족이번 진료 앞에서 약 하나의 진료 전 약별 복약 판정과 그 근거를 남기는 형식입니다. 판정과 개수는 판정식이 정하고, pattern_note 만 Claude Haiku 4.5 가 pattern 값으로 씁니다.
{"type":"object","properties":{"appt_id":{"type":"string","description":"이번 진료입니다."},"prev_appt_id":{"type":"string","description":"지난 진료입니다."},"window_from":{"type":"string","format":"date-time","description":"지난 진료의 visit_at 입니다. 이 시각 이후 회차부터 셉니다."},"window_to":{"type":"string","format":"date-time","description":"리포트를 연 시각입니다. 이 시각 전 회차까지 셉니다."},"med_id":{"type":"string","description":"판정한 약입니다. 화면의 줄 키입니다."},"med_label":{"type":"string"},"verdict":{"type":"string","enum":["처방대로 먹음","자주 건너뜀","늦게 먹은 날이 많음","약이 떨어져 못 먹은 날이 있음","기록이 비어 판단 못 함"]},"scheduled_doses":{"type":"integer","minimum":0,"description":"window 안에 med_schedules 의 rule·times 로 펼친 회차 수입니다. 날짜마다 그날 유효한 일정 행(start_date 이상, end_date 이하)으로 펼칩니다."},"scheduled_days":{"type":"integer","minimum":0,"description":"펼친 회차가 하나라도 있는 날짜 수입니다."},"missing_doses":{"type":"integer","minimum":0,"description":"같은 med_id·scheduled_at 의 dose_logs 가 하나도 없는 회차 수입니다."},"missing_pct":{"type":"number","minimum":0,"maximum":100},"skipped_doses":{"type":"integer","minimum":0,"description":"회차마다 logged_at 이 가장 늦은 기록을 그 회차의 마지막 기록으로 보고, 그것이 건너뛰기인 회차 수입니다."},"skipped_pct":{"type":"number","minimum":0,"maximum":100},"late_days":{"type":"integer","minimum":0,"description":"마지막 기록이 복용이고 logged_at 이 scheduled_at 보다 7,200초 넘게 뒤인 회차가 있는 날짜 수입니다. 날짜는 scheduled_at 의 날짜입니다."},"late_pct":{"type":"number","minimum":0,"maximum":100},"stock_from":{"type":["string","null"],"format":"date-time","description":"window 안에서 pill_count 가 적힌 첫 stock_events 행의 event_at 입니다. 남은 개수를 적지 않는 약은 null 입니다."},"out_days":{"type":["integer","null"],"minimum":0,"description":"남은 개수가 0 이하가 된 날부터 다음 리필 event_at 전까지, 예정 회차가 있는데 복용 기록이 없는 날짜 수입니다. stock_from 이 null 이면 null 입니다."},"out_from":{"type":["string","null"],"format":"date"},"out_until":{"type":["string","null"],"format":"date-time"},"pills_left":{"type":["integer","null"],"description":"stock_from 의 pill_count 에서 복용마다 dose_qty 를 빼고, 리필은 더하고, 다시 센 pill_count 는 그 값으로 맞춰 window_to 까지 굴린 값입니다."},"pattern":{"type":["object","null"],"description":"verdict 를 만든 사건(건너뛰기·늦은 복용·복용 없는 날·기록 없음)의 쏠림입니다. 처방대로 먹음이면 null 입니다.","properties":{"event":{"type":"string","enum":["건너뛰기","2시간 넘게 늦은 복용","복용 없는 날","기록 없음"]},"by_time":{"type":"object","additionalProperties":{"type":"integer"},"description":"회차 시각별 사건 수입니다."},"longest_run":{"type":"object","properties":{"time":{"type":"string"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"count":{"type":"integer"}},"required":["time","from","to","count"],"description":"같은 시각 회차를 날짜순으로 늘어놓았을 때 사건이 끊기지 않고 이어진 가장 긴 구간입니다."}},"required":["event","by_time","longest_run"]},"pattern_note":{"type":["string","null"],"description":"Claude Haiku 4.5 가 pattern 을 보고 쓴 한 문장입니다. 처방대로 먹음이면 null 입니다."},"evidence":{"type":"array","items":{"type":"object","properties":{"dataset":{"type":"string","enum":["med_schedules","dose_logs","stock_events","appointments"]},"field":{"type":"string"},"agg":{"type":"string","enum":["count","sum","avg","min","max"]},"where":{"type":"string"},"value":{"description":"agg 가 없으면 수록된 행의 그 컬럼 값 그대로이고, agg 가 count 면 펼침 규칙으로 만든 전체 기록에서 where 조건으로 센 수입니다."}},"required":["dataset","field","value"]}}},"required":["appt_id","prev_appt_id","window_from","window_to","med_id","med_label","verdict","scheduled_doses","scheduled_days","missing_doses","missing_pct","skipped_doses","skipped_pct","late_days","late_pct","stock_from","out_days","out_from","out_until","pills_left","pattern","pattern_note","evidence"]}[
{
"appt_id": "appt_109",
"prev_appt_id": "appt_108",
"window_from": "2026-07-15T10:20:00-04:00",
"window_to": "2026-10-15T19:30:00-04:00",
"med_id": "med_03",
"med_label": "Atorvastatin 20mg",
"verdict": "약이 떨어져 못 먹은 날이 있음",
"scheduled_doses": 92,
"scheduled_days": 92,
"missing_doses": 2,
"missing_pct": 2.2,
"skipped_doses": 3,
"skipped_pct": 3.3,
"late_days": 0,
"late_pct": 0,
"stock_from": "2026-07-15T22:05:00-04:00",
"out_days": 5,
"out_from": "2026-09-01",
"out_until": "2026-09-07T11:15:00-04:00",
"pills_left": 22,
"pattern": {
"event": "복용 없는 날",
"by_time": {
"21:00": 5
},
"longest_run": {
"time": "21:00",
"from": "2026-09-02",
"to": "2026-09-06",
"count": 5
}
},
"pattern_note": "9월 1일 밤 마지막 한 알을 먹은 뒤 9월 7일 리필 전까지, 9월 2일부터 9월 6일까지 5일 동안 복용 기록이 없습니다.",
"evidence": [
{
"dataset": "stock_events",
"field": "pill_count",
"value": 48
},
{
"dataset": "med_schedules",
"field": "dose_qty",
"value": 1
},
{
"dataset": "dose_logs",
"field": "action",
"agg": "count",
"where": "stock_from 뒤 2026-09-01T21:05 까지 복용",
"value": 48
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-09-01T21:00:00-04:00"
},
{
"dataset": "stock_events",
"field": "event_at",
"value": "2026-09-07T11:15:00-04:00"
},
{
"dataset": "stock_events",
"field": "refill_qty",
"value": 30
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-09-02T21:00:00-04:00"
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-09-06T21:00:00-04:00"
},
{
"dataset": "med_schedules",
"field": "times",
"agg": "count",
"where": "window 안 rule·times 로 펼친 회차",
"value": 92
}
]
},
{
"appt_id": "appt_109",
"prev_appt_id": "appt_108",
"window_from": "2026-07-15T10:20:00-04:00",
"window_to": "2026-10-15T19:30:00-04:00",
"med_id": "med_02",
"med_label": "Metformin 500mg",
"verdict": "자주 건너뜀",
"scheduled_doses": 184,
"scheduled_days": 93,
"missing_doses": 2,
"missing_pct": 1.1,
"skipped_doses": 44,
"skipped_pct": 23.9,
"late_days": 0,
"late_pct": 0,
"stock_from": "2026-07-15T12:30:00-04:00",
"out_days": 0,
"out_from": null,
"out_until": null,
"pills_left": 40,
"pattern": {
"event": "건너뛰기",
"by_time": {
"08:00": 3,
"20:00": 41
},
"longest_run": {
"time": "20:00",
"from": "2026-09-04",
"to": "2026-10-14",
"count": 41
}
},
"pattern_note": "건너뛴 44회 가운데 41회가 저녁 20:00 회차이고, 9월 4일부터 10월 14일까지 저녁 회차를 41번 이어서 건너뛰었습니다.",
"evidence": [
{
"dataset": "med_schedules",
"field": "start_date",
"value": "2026-07-15"
},
{
"dataset": "med_schedules",
"field": "end_date",
"value": "2026-07-14"
},
{
"dataset": "med_schedules",
"field": "times",
"agg": "count",
"where": "window 안 rule·times 로 펼친 회차",
"value": 184
},
{
"dataset": "dose_logs",
"field": "action",
"agg": "count",
"where": "회차별 마지막 기록이 건너뛰기",
"value": 44
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-07-15T20:00:00-04:00"
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-09-04T20:00:00-04:00"
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-10-14T20:00:00-04:00"
},
{
"dataset": "stock_events",
"field": "pill_count",
"value": 65
}
]
},
{
"appt_id": "appt_109",
"prev_appt_id": "appt_108",
"window_from": "2026-07-15T10:20:00-04:00",
"window_to": "2026-10-15T19:30:00-04:00",
"med_id": "med_04",
"med_label": "Levothyroxine 50mcg",
"verdict": "늦게 먹은 날이 많음",
"scheduled_doses": 92,
"scheduled_days": 92,
"missing_doses": 3,
"missing_pct": 3.3,
"skipped_doses": 1,
"skipped_pct": 1.1,
"late_days": 31,
"late_pct": 33.7,
"stock_from": "2026-07-15T12:40:00-04:00",
"out_days": 0,
"out_from": null,
"out_until": null,
"pills_left": 62,
"pattern": {
"event": "2시간 넘게 늦은 복용",
"by_time": {
"06:30": 31
},
"longest_run": {
"time": "06:30",
"from": "2026-09-14",
"to": "2026-10-14",
"count": 31
}
},
"pattern_note": "9월 14일부터 10월 14일까지 31일 이어서 06:30 회차를 2시간 넘게 지나 먹었습니다.",
"evidence": [
{
"dataset": "med_schedules",
"field": "end_date",
"value": "2026-08-19"
},
{
"dataset": "med_schedules",
"field": "start_date",
"value": "2026-08-20"
},
{
"dataset": "dose_logs",
"field": "logged_at",
"value": "2026-08-19T07:05:00-04:00"
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-09-14T06:30:00-04:00"
},
{
"dataset": "dose_logs",
"field": "logged_at",
"value": "2026-09-14T09:10:00-04:00"
},
{
"dataset": "dose_logs",
"field": "logged_at",
"value": "2026-09-03T08:30:00-04:00"
},
{
"dataset": "dose_logs",
"field": "logged_at",
"agg": "count",
"where": "마지막 기록이 복용이고 scheduled_at 뒤 7,200초 초과인 날짜",
"value": 31
},
{
"dataset": "med_schedules",
"field": "times",
"agg": "count",
"where": "window 안 rule·times 로 펼친 회차",
"value": 92
}
]
},
{
"appt_id": "appt_109",
"prev_appt_id": "appt_108",
"window_from": "2026-07-15T10:20:00-04:00",
"window_to": "2026-10-15T19:30:00-04:00",
"med_id": "med_05",
"med_label": "Vitamin D3 50,000IU",
"verdict": "기록이 비어 판단 못 함",
"scheduled_doses": 13,
"scheduled_days": 13,
"missing_doses": 7,
"missing_pct": 53.8,
"skipped_doses": 1,
"skipped_pct": 7.7,
"late_days": 0,
"late_pct": 0,
"stock_from": null,
"out_days": null,
"out_from": null,
"out_until": null,
"pills_left": null,
"pattern": {
"event": "기록 없음",
"by_time": {
"09:00": 7
},
"longest_run": {
"time": "09:00",
"from": "2026-09-13",
"to": "2026-10-11",
"count": 5
}
},
"pattern_note": "일요일 09:00 회차 13번 가운데 7번이 아무 응답 없이 비었고, 9월 13일부터 10월 11일까지는 5번 이어서 비었습니다.",
"evidence": [
{
"dataset": "med_schedules",
"field": "rule",
"value": "요일:일"
},
{
"dataset": "med_schedules",
"field": "times",
"agg": "count",
"where": "window 안 rule·times 로 펼친 회차",
"value": 13
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"agg": "count",
"where": "펼친 회차 가운데 응답 기록이 하나라도 있는 회차",
"value": 6
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-09-06T09:00:00-04:00"
},
{
"dataset": "appointments",
"field": "visit_at",
"value": "2026-07-15T10:20:00-04:00"
},
{
"dataset": "appointments",
"field": "visit_at",
"value": "2026-10-16T09:40:00-04:00"
}
]
},
{
"appt_id": "appt_109",
"prev_appt_id": "appt_108",
"window_from": "2026-07-15T10:20:00-04:00",
"window_to": "2026-10-15T19:30:00-04:00",
"med_id": "med_01",
"med_label": "Lisinopril 10mg",
"verdict": "처방대로 먹음",
"scheduled_doses": 92,
"scheduled_days": 92,
"missing_doses": 2,
"missing_pct": 2.2,
"skipped_doses": 3,
"skipped_pct": 3.3,
"late_days": 4,
"late_pct": 4.3,
"stock_from": "2026-07-15T13:05:00-04:00",
"out_days": 0,
"out_from": null,
"out_until": null,
"pills_left": 33,
"pattern": null,
"pattern_note": null,
"evidence": [
{
"dataset": "med_schedules",
"field": "start_date",
"value": "2026-01-12"
},
{
"dataset": "med_schedules",
"field": "times",
"agg": "count",
"where": "window 안 rule·times 로 펼친 회차",
"value": 92
},
{
"dataset": "dose_logs",
"field": "action",
"agg": "count",
"where": "회차별 마지막 기록이 건너뛰기",
"value": 3
},
{
"dataset": "dose_logs",
"field": "scheduled_at",
"value": "2026-08-27T08:00:00-04:00"
},
{
"dataset": "stock_events",
"field": "pill_count",
"value": 30
},
{
"dataset": "stock_events",
"field": "refill_qty",
"value": 90
}
]
},
{
"appt_id": "appt_109",
"prev_appt_id": "appt_108",
"window_from": "2026-07-15T10:20:00-04:00",
"window_to": "2026-10-15T19:30:00-04:00",
"med_id": "med_08",
"med_label": "Magnesium 250mg",
"verdict": "처방대로 먹음",
"scheduled_doses": 30,
"scheduled_days": 30,
"missing_doses": 0,
"missing_pct": 0,
"skipped_doses": 1,
"skipped_pct": 3.3,
"late_days": 7,
"late_pct": 23.3,
"stock_from": "2026-08-03T21:00:00-04:00",
"out_days": 0,
"out_from": null,
"out_until": null,
"pills_left": 1,
"pattern": null,
"pattern_note": null,
"evidence": [
{
"dataset": "med_schedules",
"field": "rule",
"value": "간격:2일"
},
{
"dataset": "med_schedules",
"field": "end_date",
"value": "2026-09-30"
},
{
"dataset": "med_schedules",
"field": "times",
"agg": "count",
"where": "window 안 rule·times 로 펼친 회차",
"value": 30
},
{
"dataset": "dose_logs",
"field": "logged_at",
"value": "2026-09-23T00:30:00-04:00"
},
{
"dataset": "dose_logs",
"field": "logged_at",
"value": "2026-09-27T00:00:00-04:00"
},
{
"dataset": "dose_logs",
"field": "logged_at",
"agg": "count",
"where": "마지막 기록이 복용이고 scheduled_at 뒤 7,200초 초과인 날짜",
"value": 7
},
{
"dataset": "stock_events",
"field": "pill_count",
"value": 30
}
]
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 처방대로 먹음·자주 건너뜀·늦게 먹은 날이 많음·약이 떨어져 못 먹은 날이 있음·기록이 비어 판단 못 함 다섯 값 중 하나인지, pattern.event 가 네 값 중 하나인지, evidence 의 agg 가 count·sum·avg·min·max 중 하나인지 확인합니다. 2. appt_id·prev_appt_id·window_from·window_to·med_id·verdict·scheduled_doses·scheduled_days·missing_doses·skipped_doses·late_days·stock_from·out_days·pills_left·pattern·pattern_note·evidence 필수 필드가 모두 존재하는지 확인합니다. 3. window 를 다시 계산합니다. appointments 에서 visit_at 이 null 인 행을 빼고, window_to 보다 앞선 visit_at 가운데 가장 늦은 것이 window_from(prev_appt_id), 뒤에 오는 첫 visit_at 이 appt_id 입니다. 4. scheduled_doses 와 scheduled_days 를 다시 계산합니다. window_from 이상 window_to 미만의 날짜마다 start_date 이상 end_date 이하인 med_schedules 행 하나로 rule·times 를 펼쳐 셉니다. rule 이 필요시인 약은 펼칠 회차가 없어 출력 행을 만들지 않습니다. 5. missing_doses 는 scheduled_doses 에서 응답 기록이 하나라도 있는 회차 수를 뺀 차이와 일치해야 합니다. missing_doses * 100 이 scheduled_doses * 40 이상이면 verdict 는 기록이 비어 판단 못 함이고, 그보다 적으면 이 판정은 나오지 않습니다. 6. out_days·out_from·pills_left 를 다시 계산합니다. stock_from 의 pill_count 에서 시작해 복용 기록(logged_at 순)마다 dose_qty 를 빼고, refill_qty 는 더하고, 뒤에 다시 적힌 pill_count 는 그 값으로 맞춥니다. 남은 개수가 0 이하가 된 복용의 날짜가 out_from 이고, 그날부터 다음 refill_qty 행의 event_at(out_until) 전까지 예정 회차가 있는데 복용 기록이 없는 날짜 수가 out_days 입니다. out_days 가 1 이상이면 verdict 는 약이 떨어져 못 먹은 날이 있음입니다(기록이 비어 판단 못 함이 먼저 걸린 약은 빼고). 7. skipped_doses 를 다시 계산합니다. skipped_doses * 100 이 scheduled_doses * 20 이상이면 verdict 는 자주 건너뜀입니다(앞 두 판정이 걸린 약은 빼고). 8. late_days 를 다시 계산합니다. logged_at - scheduled_at 이 정확히 7,200초인 복용은 늦은 날로 세지 않습니다. late_days * 100 이 scheduled_days * 25 이상이면 verdict 는 늦게 먹은 날이 많음입니다(앞 세 판정이 걸린 약은 빼고). 9. missing_pct·skipped_pct·late_pct 가 0 이상 100 이하 범위이고, 각 개수를 분모로 나눈 값에 100 을 곱해 소수 둘째 자리에서 반올림한 값과 같은지 봅니다. 판정은 반올림 전 정수 비교(5~8번)로 합니다. 10. verdict 가 처방대로 먹음이면 pattern 과 pattern_note 는 null 이고, 나머지 네 값이면 둘 다 null 이 아닙니다. 11. evidence 의 field 이름이 네 데이터셋에 실재하는 컬럼인지 대조합니다. 12. agg 가 없는 evidence value 가 수록된 행의 원문 값과 같은지 확인합니다. 13. pattern_note 가 존댓말 문장으로 끝나는지, 그 안의 숫자가 그 행의 개수·pattern 값(날짜의 월·일, 회차 시각)과 기준값(2시간·20%·25%·40%)에서만 나오는지 확인합니다. [수동 검증 · 사람이 봅니다] 1. pattern_note 가 용량을 바꾸라거나 약을 끊으라는 식의 조언으로 읽히지 않는지 사람이 읽고 표시합니다. 금지 낱말 목록으로 거른 뒤에도 남는 문장을 봅니다. 2. PDF 를 실제로 내보내 A4 한 장에 약 여섯 개가 다 들어가는지, 큰 글씨 모드가 진료실에서 화면을 돌려 보여 줄 때 읽히는지 직접 봅니다. [판정 차례 · 둘 이상 걸릴 때] 기록이 비어 판단 못 함 → 약이 떨어져 못 먹은 날이 있음 → 자주 건너뜀 → 늦게 먹은 날이 많음 → 처방대로 먹음. 응답 기록이 40% 넘게 빈 약은 나머지 비율이 무엇을 세었는지부터 흔들려 맨 앞에 둡니다. 약이 떨어진 날은 대개 건너뛰기로도 남아 건너뜀 비율을 같이 올리므로, 원인인 약 떨어짐을 먼저 말합니다. 먹지 않은 회차가 늦게 먹은 회차보다 진료에서 먼저 물을 일이라 건너뜀을 늦음 앞에 둡니다. [하드 케이스 · 판정이 갈리는 이유] Levothyroxine(med_04)은 8월 20일에 알림이 07:00 에서 06:30 으로 바뀌었습니다. 날짜마다 그날 유효한 일정으로 펼치면 기록 없음은 3회(3.3%)이고, 지금 일정 하나로 92일을 다 펼치면 07:00 에 먹은 7/16~8/19 의 35일이 06:30 회차와 맞지 않아 기록 없음이 37회(40.2%)로 늘어 기록이 비어 판단 못 함으로 바뀝니다. 9월 3일 회차는 정확히 2시간 뒤인 08:30 에 먹어 늦은 날로 세지 않습니다. 9월 14일처럼 06:31 에 미루기를 누르고 09:10 에 복용을 누른 회차는 마지막 기록인 복용 하나로 셉니다. Magnesium(med_08)의 9월 26일 22:00 회차는 자정을 넘긴 9/27 00:00 에 정확히 2시간 뒤 먹어 늦은 날이 7일(23.3%)에 머물고 처방대로 먹음입니다. '넘게' 를 '이상' 으로 읽으면 8일(26.7%)로 늘어 늦게 먹은 날이 많음으로 뒤집힙니다. 늦은 날의 날짜는 logged_at 이 아니라 scheduled_at 의 날짜입니다. Atorvastatin(med_03)은 9월 1일 21:05 복용으로 남은 개수가 0 이 되었고, 9월 7일 11:15 리필 전까지 9/02~9/06 의 5일이 복용 기록이 없습니다. 9월 1일은 그날 복용이 있어 세지 않고, 9월 7일 21:00 회차는 리필 뒤라 세지 않습니다. Metformin(med_02)은 7월 15일 진료 날 10:20 뒤의 20:00 회차부터 셉니다. 같은 날 08:00 회차는 진료 전이라 빠지고, 그래서 회차 184회가 93일에 걸칩니다. 9월 20일에 다시 센 65정은 굴린 값 67정과 달라 65정부터 다시 굴립니다. Vitamin D3(med_05)는 일요일 회차 13번 가운데 응답 기록이 있는 회차가 6번뿐이라 기록이 없는 7번(53.8%)으로 판단 못 함이고, 남은 개수를 적지 않는 약이라 out_days 와 pills_left 는 null 입니다. Albuterol(med_06)은 필요시 약이라 펼칠 회차가 없어 판정 행을 만들지 않고, 리포트 맨 아래에 기간 중 복용 2회만 적습니다. 안과 검진(appt_110)은 visit_at 이 null 이라 지난 진료·이번 진료를 고를 때 빠집니다. Amoxicillin(med_07)은 6월 11일에 끝나 window 안에 펼칠 회차가 없습니다. [판정식이 정하지 않아 이 편이 정한 것] 네 판정식은 카드 그대로 씁니다. 이번 진료 visit_at 까지가 아니라 리포트를 연 시각(window_to)까지만 셉니다. 그 사이 회차는 아직 오지 않아 셀 수 없기 때문이고, 10월 15일 20:00·21:00 회차와 10월 16일 아침 회차가 여기서 빠집니다. 일정 행의 end_date 는 그날을 포함합니다. 한 회차에 기록이 여럿이면 logged_at 이 가장 늦은 기록이 그 회차의 응답이고, 미루기만 남은 회차는 기록은 있으되 복용도 건너뛰기도 아닙니다. 남은 개수는 window 안에서 pill_count 가 적힌 첫 행부터 굴리고, 그 앞 회차는 약 떨어짐 판단에서 뺍니다. 남은 개수가 0 이하인 동안 pill_count 가 0 보다 큰 값으로 다시 적혀도 구간이 끝납니다. 비율 판정은 소수 반올림 없이 정수 곱으로 비교합니다. 처방대로 먹음은 네 조건 어디에도 걸리지 않은 약입니다. [전체 복약 기록 · 펼침 규칙] dose_logs 에는 판정을 가르는 기록만 실었고, 나머지는 아래 규칙으로 펼칩니다. 펼친 회차마다 기본은 action 복용, logged_at 은 scheduled_at 5분 뒤이며, 건너뛰기는 2분 뒤에 남깁니다. 아래에 적은 날만 다르고, dose_logs 에 실린 행은 이 규칙이 만드는 행과 같습니다. med_01 Lisinopril: 건너뛰기 7/29·8/27·9/24, 응답 없음 8/08·9/05, 2시간 40분 뒤 복용 7/26·8/23·9/20·10/11 med_02 Metformin: 20:00 회차 건너뛰기 9/04~10/14 매일(41회), 08:00 회차 건너뛰기 7/30·8/20·9/10, 20:00 회차 응답 없음 8/14·8/15 med_03 Atorvastatin: 건너뛰기 9/02·9/03·9/06, 응답 없음 9/04·9/05 med_04 Levothyroxine: 07:00 회차 건너뛰기 8/09, 응답 없음 8/02·8/28·9/08, 9/14~10/14 매일 06:31 미루기 뒤 09:10 복용(31일), 9/03 은 정확히 2시간 뒤인 08:30 복용 med_05 Vitamin D3: 건너뛰기 8/09, 응답 없음 8/23·8/30·9/13·9/20·9/27·10/04·10/11 med_06 Albuterol: 필요시 복용 8/22 15:40·9/30 07:15 두 기록뿐이고 scheduled_at 은 null 입니다 med_08 Magnesium: 건너뛰기 8/25, 2시간 30분 뒤 복용 8/07·8/13·8/19·8/31·9/08·9/16·9/22, 9/26 은 정확히 2시간 뒤인 9/27 00:00 복용 이 규칙으로 펼친 기록은 모두 520행이고, 여섯 출력 샘플의 회차 수·건너뜀·늦은 날·응답 없음·남은 개수가 그대로 다시 나옵니다. [출처 · 공고에 없는 낱말] 공고 원문이 밝히는 것은 만성질환자 복약관리 앱 Pillo 와 북미 시장까지입니다. 알림 응답(복용·건너뛰기·미루기), 남은 약 개수와 리필, 진료 예약은 Pillo 앱 소개에 나온 기능을 바탕으로 이 편이 가정한 데이터 모양입니다. 약 이름은 흔히 쓰는 일반명이고, 기준값(40%·20%·25%·2시간)은 이 편의 판정식이 정한 값입니다. [평가 러너 · 2주차] 기준값 바로 안쪽과 바깥쪽을 짝으로 둔 경계 사례 60건을 매번 돌립니다. 기록 없음 39.9%·40.0%, 건너뜀 19.9%·20.0%, 늦은 날 24.9%·25.0%, 7,200초·7,201초 늦은 복용, 0 이 된 날과 같은 날 들어온 리필, 일정이 바뀐 날, end_date 당일, window_to 1분 앞뒤 회차, 미루기 뒤 복용과 미루기만 남은 회차, 리필을 적지 않아 0 아래로 내려간 약이 들어갑니다. 판정이 기대와 다른 사례는 어느 줄의 계산이 갈렸는지 회차 단위로 출력합니다.
근거: 만성질환자들의 복약관리를 돕는 Pillo앱을 운영하고 있습니다.