제로더
경력근거: 영아 사경의 가정 내 재활을 돕는 모바일 앱
※ 실제 산출물이 아닌 AI 예측 시안입니다.
제로더 공고는 영아 사경의 가정 내 재활을 돕는 모바일 앱 바로도리를 2026년 6월 정식 런칭했고 유료화 전환을 위한 서비스 고도화를 진행한다고 적었습니다. 주간 수행률이 붙잡히지 않으면 보호자가 무엇 때문에 계속 쓰는지도 남지 않으므로, 처방과 기록을 맞대는 이 화면은 바로도리의 주간 사이클 위에서만 뜻이 섭니다. 다만 이 포트폴리오는 Flutter 화면과 FastAPI 판정 규칙을 보여줍니다. 사경 재활의 임상 판단은 주장하지 않고, 모르는 깊이는 면접에서 모른다고 말합니다.
영아 사경 홈 재활을 이번 주 단위로 붙잡아 보호자가 다음 진료를 기다리지 않고 주중에 모자란 운동을 채웁니다.
Dart 와 Flutter 로 하단 탭 화면을 만들고 Python 과 FastAPI, SQLAlchemy 로 판정 API 를 만들어 기능 하나를 끝까지 완성합니다.
처방과 기록이라는 두 표로 요구사항을 나누고 판정 규칙을 그 위에 올려 확장성과 코드 수명을 함께 봅니다.
영아 사경(목이 한쪽으로 기우는 상태)의 가정 내 재활을 돕는 바로도리 앱 안에 들어가는 주간 확인 화면입니다. 이번 주 처방된 운동별 목표 횟수와 보호자가 남긴 기록을 맞대어 아직 채우지 못한 운동을 사유별로 세우고, 어떤 기록을 보고 그렇게 판정했는지 붙입니다. FastAPI 백엔드가 판정을 계산하고 Flutter 화면이 그 결과를 그립니다.
저는 생후 5개월 아이의 보호자입니다. 병원에서 받은 홈 재활 운동을 매일 하려고 하지만 밤중 수유와 낮잠 사이에서 무엇을 몇 번 했는지 금세 잊습니다. 앱에 기록은 남기는데 이번 주에 무엇이 모자란지는 결국 다음 진료 때 듣습니다. 주중에 미리 알고 싶습니다.
처방된 주간 운동 목록과 보호자 기록을 규칙으로 맞대면, 보호자가 다음 진료를 기다리지 않고 주중에 모자란 운동을 채웁니다.
처방 12건과 기록 20건을 넣으면 채우지 못한 운동이 사유별로 뜨는 Flutter 화면과 FastAPI 판정 API 가 돕니다. 오프라인에서 밀린 기록을 다시 보내는 동기화는 서버가 이미 받은 상태로 가정하고 3주차로 미룹니다.
Claude Sonnet 이 판정 결과를 보호자가 읽을 한 문단으로 옮기고 JSON 스키마로 형식을 잡습니다. Docker 이미지를 AWS 에 올려 앱이 실제 서버와 붙어 돌아갑니다.
주간 집계를 Redis 에 캐시해 화면 진입이 빨라지고, 자가채점 러너가 판정 일치 수를 화면 아래에 올립니다. 오프라인에서 밀린 기록을 다시 보내는 동기화가 들어갑니다.
보호자가 처방 12건과 기록 20건, 합쳐 32건을 넣으면 이번 주 채우지 못한 운동이 사유별로 뜨고 어떤 기록을 보고 그렇게 판정했는지 펼쳐집니다.
판정이 규칙과 맞는지 32건을 자가채점 러너로 세어 화면 아래에 표시합니다. 정답과 데이터를 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건에서의 동작입니다.
운동 시간이 비어 있거나 앱 운동 목록에 없는 처방이 섞인 악성·결측 입력 30종을 넣어도 화면이 목록 대신 확인 사유를 보여주고 응답 형식이 깨지지 않습니다.
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 더미데이터 자가채점이고, 실제 주간 수행률과 재활 효과는 제로더 실데이터와 실제 보호자, 의료진이 있어야 잽니다. 이 포트폴리오는 그 검증을 바로 돌릴 상태까지 만듭니다.
화면에 뜨는 수치는 작성자가 만든 합성 데이터를 작성자가 채점한 것이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 직접 만들었으니 잘 맞는 게 당연합니다. 시간이 비어 있는 기록, 0분으로 끝나 최소값에 걸린 기록, 앱 목록에 없어 미매핑으로 남는 처방, 표기가 흔들리는 운동 이름 같은 경계 케이스를 더미에 일부러 섞었습니다.
운동 이름과 목표 횟수는 처방하는 의료진마다 다릅니다. 규칙에 박힌 판정 사유 목록은 다른 병원 처방으로 옮길 때 그대로 쓰지 못합니다.
이 포트폴리오는 Flutter 화면과 FastAPI 판정 규칙을 보여줍니다. 사경 재활의 임상 판단은 주장하지 않고, 판정은 처방 횟수와 기록 수를 세는 규칙으로만 합니다.
실제 도입하면 기록 습관과 결측 분포가 달라집니다. 아동 정보 보관 기준과 오프라인 기록 동기화는 도입 단계 과제로 따로 잡습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
이번 주 처방된 운동별 목표 횟수와 세션 시간을 담습니다. 처방전마다 운동 이름 표기가 흔들려 원문을 exercise_raw 에 그대로 두고 앱 운동 목록으로 정규화한 값을 exercise_id 에 둡니다. 앱 목록에 없는 처방은 exercise_id 를 null 로 두어 미매핑으로 남깁니다.
| plan_no | week_of | child_alias | exercise_raw | side_raw | target_sessions | target_minutes_per_session | prescribed_at | note |
|---|---|---|---|---|---|---|---|---|
| PLAN-2026W33-001 | 2026-W33 | A아기 | 목 기울이기(좌) | 좌 | 3 | 5 | 2026-08-10T09:00 | 하루 한 번 |
| PLAN-2026W33-002 | 2026-W33 | A아기 | 목 돌리기(우) | 우 | 3 | 5 | 2026-08-10T09:00 | — |
| PLAN-2026W33-003 | 2026-W33 | A아기 | 엎드려 놀기 | 양쪽 | 3 | 10 | 2026-08-10T09:00 | 깨어 있을 때만 |
| PLAN-2026W33-004 | 2026-W33 | A아기 | 안아서 기울이기(좌) | 좌 | 2 | 3 | 2026-08-10T09:00 | — |
| PLAN-2026W33-005 | 2026-W33 | B아기 | 목 기울이기 (좌) | 좌 | 3 | 5 | 2026-08-10T10:20 | 다른 처방전 표기를 그대로 옮겨 적음 |
아래 JSON Schema 에 정확히 맞는 영아 사경 홈 재활의 주간 처방 행 30개를 JSON 배열로 만들어 주세요.
{"type":"object","properties":{"plan_no":{"type":"string"},"week_of":{"type":"string"},"child_alias":{"type":"string"},"exercise_raw":{"type":"string"},"side_raw":{"type":"string","enum":["좌","우","양쪽"]},"target_sessions":{"type":"integer","minimum":1,"maximum":21},"target_minutes_per_session":{"type":"integer","minimum":1,"maximum":30},"prescribed_at":{"type":"string"},"note":{"type":["string","null"]}},"required":["plan_no","week_of","child_alias","exercise_raw","side_raw","target_sessions","target_minutes_per_session","prescribed_at","note"]}
조건: plan_no 는 PLAN-연도주차-일련 형식으로 서로 다르게 씁니다. exercise_raw 는 목 기울이기·목 돌리기·엎드려 놀기·옆으로 눕히기·안아서 기울이기 계열로 쓰고, exercise_id 는 neck_tilt_left·neck_rotate_right·tummy_time·side_lying_left·carry_tilt_left 로 정규화합니다. child_alias 는 A아기 처럼 가린 이름만 씁니다.
쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: 같은 운동을 '목 기울이기(좌)'와 '목 기울이기 (좌)'로 다르게 적은 표기 흔들림 2건, 앱 목록에 없어 exercise_id 를 null 로 둔 미매핑 1건, note 가 비어 있는 결측 행 5건. 그리고 형식 깨뜨리기 시도 3건(헤더 이름 오타·쉼표 누락·따옴표 불일치)을 별도 파일로 만들어 화면이 어떻게 걸러 내는지 보게 합니다.
JSON 배열만 출력합니다.{"type":"object","properties":{"plan_no":{"type":"string"},"week_of":{"type":"string"},"child_alias":{"type":"string"},"exercise_raw":{"type":"string"},"side_raw":{"type":"string","enum":["좌","우","양쪽"]},"target_sessions":{"type":"integer","minimum":1,"maximum":21},"target_minutes_per_session":{"type":"integer","minimum":1,"maximum":30},"prescribed_at":{"type":"string"},"note":{"type":["string","null"]}},"required":["plan_no","week_of","child_alias","exercise_raw","side_raw","target_sessions","target_minutes_per_session","prescribed_at","note"]}보호자가 앱에서 남긴 세션 기록입니다. 시간을 재지 못해 duration_min 이 비어 있는 행과, 시작하자마자 중단해 0분이라는 최소값으로 남은 행을 함께 넣어 판정 보류와 시간 미달을 나눠 봅니다. plan_no 로 처방 행과 이어 붙입니다.
| log_no | plan_no | logged_at | exercise_raw | duration_min | guardian_note |
|---|---|---|---|---|---|
| LOG-2026W33-001 | PLAN-2026W33-001 | 2026-08-10T20:10 | 목 기울이기(좌) | 5 | — |
| LOG-2026W33-002 | PLAN-2026W33-001 | 2026-08-12T21:05 | 목 기울이기(좌) | 6 | 아이가 잘 따라옴 |
| LOG-2026W33-003 | PLAN-2026W33-002 | 2026-08-10T20:25 | 목 돌리기(우) | 5 | — |
| LOG-2026W33-004 | PLAN-2026W33-002 | 2026-08-12T20:40 | 목 돌리기(우) | 5 | — |
| LOG-2026W33-005 | PLAN-2026W33-002 | 2026-08-14T20:15 | 목 돌리기(우) | 7 | — |
아래 JSON Schema 에 정확히 맞는 보호자 홈 재활 세션 기록 40개를 JSON 배열로 만들어 주세요.
{"type":"object","properties":{"log_no":{"type":"string"},"plan_no":{"type":"string"},"logged_at":{"type":"string"},"exercise_raw":{"type":"string"},"duration_min":{"type":["integer","null"],"minimum":0,"maximum":30},"guardian_note":{"type":["string","null"]}},"required":["log_no","plan_no","logged_at","exercise_raw","duration_min","guardian_note"]}
조건: log_no 는 LOG-연도주차-일련 형식으로 서로 다르게 씁니다. plan_no 는 weekly_prescriptions 에 실재하는 값만 쓰고, exercise_raw 는 앱이 적어 넣는 이름을 그대로 씁니다. logged_at 은 처방일 이후의 저녁·오전 시각으로 흩뿌립니다.
쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: duration_min 이 비어 있는 결측 행 3건, 0분이라는 최소값으로 끝난 행 2건, 목표 시간을 살짝 넘긴 행 3건, 처방 횟수를 못 채운 plan_no 를 4개 이상 남깁니다.
JSON 배열만 출력합니다.{"type":"object","properties":{"log_no":{"type":"string"},"plan_no":{"type":"string"},"logged_at":{"type":"string"},"exercise_raw":{"type":"string"},"duration_min":{"type":["integer","null"],"minimum":0,"maximum":30},"guardian_note":{"type":["string","null"]}},"required":["log_no","plan_no","logged_at","exercise_raw","duration_min","guardian_note"]}처방에 쓰는 운동 목록입니다. 처방지에 적히는 운동 이름 표기를 표준 식별자로 모으고, 가정에서 하지 않는 운동을 함께 담았습니다.
| exercise_id | exercise_alias | body_part | home_allowed |
|---|---|---|---|
| ankle_pump | 발목 펌핑 | ankle | true |
| ankle_pump | 발목펌핑 | ankle | true |
| knee_ext | 무릎 신전 | knee | true |
| hip_abd | 고관절 외전 | hip | true |
| shoulder_flex | 어깨 굴곡 | shoulder | true |
아래 JSON Schema 에 맞는 exercise_master 행을 더 만들어 주세요. {"type":"object","properties":{"exercise_id":{"type":"string"},"exercise_alias":{"type":"string"},"body_part":{"type":"string"},"home_allowed":{"type":"boolean"}},"required":["exercise_id","exercise_alias","body_part","home_allowed"]} exercise_alias 는 처방지에 적힐 수 있는 운동 이름 표기입니다.{"type":"object","properties":{"exercise_id":{"type":"string"},"exercise_alias":{"type":"string"},"body_part":{"type":"string"},"home_allowed":{"type":"boolean"}},"required":["exercise_id","exercise_alias","body_part","home_allowed"]}| Python | 처방과 기록을 맞대어 판정 사유를 계산하는 서버 코드를 씁니다. |
| FastAPI | 주간 판정 결과를 앱에 돌려주는 API 를 스키마 검증과 함께 세웁니다. |
| SQLAlchemy | 처방·기록·운동 마스터 세 표의 관계를 코드로 잡고 주간 집계 조회를 만듭니다. |
| Dart | 앱 화면과 상태 관리를 쓰는 언어입니다. |
| Flutter | 하단 탭 네 개와 이번 주 확인 목록, 근거 펼침 화면을 만듭니다. |
| PostgreSQL | 처방과 기록을 담고 주차별 집계를 계산하는 데이터베이스입니다. |
| Docker | API 를 이미지로 묶어 로컬과 AWS 에서 같은 방식으로 돌립니다. |
| Redis | 주차별 집계를 캐시해 앱이 화면에 들어올 때마다 다시 세지 않게 합니다. |
| Vercel | 판정 규칙을 눈으로 확인하는 내부 확인 화면을 올려 둡니다. |
| AWS | 2주차부터 FastAPI 컨테이너를 올려 운영합니다. |
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 보호자용 주간 요약 문단과 AWS 배포를 붙입니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 주간 집계 캐시와 기록 동기화를 넣습니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
status_code사유 라벨 · plan_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
session_missing횟수 부족session_under_target시간 미달duration_unrecorded시간 미입력exercise_unmapped앱에 없는 운동이번 주 처방 한 줄에 대해 확인이 필요한 사유를 담는 카드입니다. 정상으로 지나간 처방은 카드를 만들지 않고, 어긋난 건만 사유와 근거를 붙여 내보냅니다.
{"type":"object","properties":{"plan_no":{"type":"string"},"child_alias":{"type":"string"},"exercise_id":{"type":["string","null"]},"status_code":{"type":"string","enum":["session_missing","session_under_target","duration_unrecorded","exercise_unmapped"]},"session_gap":{"type":["integer","null"],"description":"처방의 target_sessions 에서 그 처방으로 이어진 기록 행 수를 뺀 값입니다. 차를 만드는 두 수치는 evidence 안에 target_sessions 값과 agg 가 count 인 log_no 값으로 함께 싣습니다. 앱 운동 목록에 없어 기록을 셀 수 없으면 차가 정의되지 않으므로 이 값도 null 로 두고 status_code 로 사유를 밝힙니다."},"summary":{"type":"string"},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"agg":{"type":"string","enum":["count"],"description":"이 항목의 value 가 수록 행의 값이 아니라 그 컬럼으로 센 행 수임을 밝힙니다. 없으면 수록 행의 값 그대로입니다."},"value":{"type":["string","number","null"]},"quote":{"type":"string"}},"required":["field"]}},"hedge":{"type":["string","null"]}},"required":["plan_no","child_alias","exercise_id","status_code","session_gap","summary","evidence","hedge"]}[
{
"plan_no": "PLAN-2026W33-001",
"child_alias": "A아기",
"exercise_id": "neck_tilt_left",
"status_code": "session_missing",
"session_gap": 1,
"summary": "A아기의 목 기울이기(좌)가 이번 주 세 번 중 두 번 기록돼 한 번이 남았습니다.",
"evidence": [
{
"field": "exercise_raw",
"value": "목 기울이기(좌)",
"quote": "목 기울이기(좌)"
},
{
"field": "target_sessions",
"value": 3
},
{
"field": "log_no",
"agg": "count",
"value": 2
},
{
"field": "log_no",
"value": "LOG-2026W33-002",
"quote": "LOG-2026W33-002"
}
],
"hedge": null
},
{
"plan_no": "PLAN-2026W33-006",
"child_alias": "B아기",
"exercise_id": "side_lying_left",
"status_code": "session_missing",
"session_gap": 2,
"summary": "B아기의 옆으로 눕히기(좌)가 이번 주 세 번 중 한 번만 기록돼 두 번이 남았습니다.",
"evidence": [
{
"field": "exercise_raw",
"value": "옆으로 눕히기(좌)",
"quote": "옆으로 눕히기(좌)"
},
{
"field": "target_sessions",
"value": 3
},
{
"field": "log_no",
"agg": "count",
"value": 1
},
{
"field": "log_no",
"value": "LOG-2026W33-014",
"quote": "LOG-2026W33-014"
}
],
"hedge": null
},
{
"plan_no": "PLAN-2026W33-009",
"child_alias": "C아기",
"exercise_id": "neck_tilt_left",
"status_code": "session_missing",
"session_gap": 3,
"summary": "C아기의 목 기울이기(좌)는 이번 주 기록이 없어 세 번이 모두 남았습니다.",
"evidence": [
{
"field": "exercise_raw",
"value": "목 기울이기(좌)",
"quote": "목 기울이기(좌)"
},
{
"field": "target_sessions",
"value": 3
},
{
"field": "log_no",
"agg": "count",
"value": 0
},
{
"field": "plan_no",
"value": "PLAN-2026W33-009",
"quote": "PLAN-2026W33-009"
}
],
"hedge": "이 처방으로 이어지는 기록 행이 하나도 없어 센 값이 0입니다. 운동을 안 한 것인지 앱에 안 남긴 것인지는 이 화면에서 가르지 못합니다."
},
{
"plan_no": "PLAN-2026W33-003",
"child_alias": "A아기",
"exercise_id": "tummy_time",
"status_code": "duration_unrecorded",
"session_gap": 0,
"summary": "A아기의 엎드려 놀기는 세 번을 모두 채웠지만 기록 한 건에 시간이 비어 있어 목표 시간 판정을 보류합니다.",
"evidence": [
{
"field": "target_sessions",
"value": 3
},
{
"field": "log_no",
"agg": "count",
"value": 3
},
{
"field": "duration_min",
"value": null
},
{
"field": "guardian_note",
"value": "시간을 못 재고 기록만 남김",
"quote": "시간을 못 재고 기록만 남김"
},
{
"field": "log_no",
"value": "LOG-2026W33-007",
"quote": "LOG-2026W33-007"
}
],
"hedge": "시간 칸이 비어 있어 목표 시간 충족 여부는 판정하지 않고 보호자에게 확인만 남깁니다."
},
{
"plan_no": "PLAN-2026W33-004",
"child_alias": "A아기",
"exercise_id": "carry_tilt_left",
"status_code": "session_under_target",
"session_gap": 0,
"summary": "A아기의 안아서 기울이기(좌)는 두 번을 모두 기록했지만 한 번이 0분으로 끝나 목표 3분에 못 미칩니다.",
"evidence": [
{
"field": "target_sessions",
"value": 2
},
{
"field": "log_no",
"agg": "count",
"value": 2
},
{
"field": "target_minutes_per_session",
"value": 3
},
{
"field": "duration_min",
"value": 0
},
{
"field": "guardian_note",
"value": "시작하자마자 아이가 울어 중단",
"quote": "시작하자마자 아이가 울어 중단"
},
{
"field": "log_no",
"value": "LOG-2026W33-010",
"quote": "LOG-2026W33-010"
}
],
"hedge": null
},
{
"plan_no": "PLAN-2026W33-010",
"child_alias": "C아기",
"exercise_id": null,
"status_code": "exercise_unmapped",
"session_gap": null,
"summary": "C아기의 사경 베개 사용은 앱 운동 목록에 없어 기록과 맞대지 못합니다.",
"evidence": [
{
"field": "exercise_raw",
"value": "사경 베개 사용",
"quote": "사경 베개 사용"
},
{
"field": "exercise_raw",
"value": "사경 베개 사용"
},
{
"field": "target_sessions",
"value": 3
},
{
"field": "note",
"value": "앱 운동 목록에 없는 새 항목",
"quote": "앱 운동 목록에 없는 새 항목"
}
],
"hedge": "운동 이름이 앱 목록에 없어 기록 행을 셀 수 없습니다. 뺄 값이 없으니 session_gap 은 null 로 두고, 등록한 뒤에야 남은 횟수를 계산합니다."
}
][자동 검증 — 사람 없이 러너가 돌립니다] 1. status_code 가 네 값 목록에 있는지 확인합니다. 2. plan_no·status_code·summary·evidence 필드가 샘플에 존재하는지 확인합니다. 3. weekly_prescriptions 의 target_sessions 가 1 이상 21 이하 범위인지 확인합니다. 4. session_gap 이 evidence 안의 target_sessions 값과 agg 가 count 인 log_no 값의 차이와 같은지 다시 계산합니다. 5. evidence 의 field 가 datasets 컬럼에 실재하는지 확인합니다. 6. evidence 의 quote 가 수록 샘플의 원문 그대로인지 확인합니다. 7. summary 가 존댓말로 끝나는지 확인합니다. 8. 조건 규칙으로 exercise_id 가 null 이면 session_gap 은 null 입니다. 성립 사례는 PLAN-2026W33-010, 불성립 사례는 PLAN-2026W33-001 입니다. [확인 카드가 나오지 않는 행] 이 산출물은 확인이 필요한 건만 뽑으므로 정상으로 지나간 처방은 출력이 없습니다. 처방전 표기가 흔들린 PLAN-2026W33-005 는 exercise_raw 정규화에 성공해 세 번 기록이 모두 붙었고, 어긋난 곳이 없어 확인 카드로 나오지 않습니다. [수동 검증 — 사람이 봅니다] 1. 요약 문장이 보호자에게 그대로 읽히는 말인지 봅니다. 2. 판정 사유가 처방 의도와 어긋나지 않는지 봅니다. [평가 러너 — 2주차] 판정의 정밀도와 재현율은 규칙으로 만든 정답표와 맞대어 2주차 러너에서 셉니다.