약국 건강기능식품 플랫폼
소스가 절반쯤 빠진 채 운영 중이던 건강기능식품 앱을 넘겨받아, 빌드되게 만들고 그 위에서 다시 만들었어요
- 기간
- 2026년 9월 – 지금
- 대상
- 약국 건강기능식품 추천 플랫폼
- 역할
- 1인: 인수, 빌드, 배포, 운영
- 기술
- flutter, next.js, react, spring kotlin, python, postgresql, github actions, docker
- 규모
- 커밋 139개, 코드베이스 8개, 인수받은 1,445개 파일 코드베이스 위에 약 3만 3천 줄 작성
- 외주 작업, neoworks
다른 언어: english
어떤 서비스인가요
이 서비스는 국가건강검진과 처방 데이터를 보고 영양제 성분을 추천해 주고, 근처 약국에서 주문할 수 있게 해 주는 한국 앱이에요. 양대 스토어에 올라간 Flutter 앱, 사용자용 웹, 약국용 파트너 사이트, 관리자 사이트, Spring 백엔드, Python AI 서버로 이루어져 있어요. 다른 에이전시가 만든 걸 9월에 제가 넘겨받았고, 그때 이미 운영 중이었어요.
어려웠던 점: 다시 빌드할 수 없는 걸 넘겨받기
처음 받은 인수인계 자료는 Flutter 앱 zip 하나와 pdf 하나였어요. 웹 프론트엔드, 백엔드, AI 서버는 "따로 공유하겠다"고 했는데, 나중에는 공유 폴더가 로그인을 요구하기 시작했어요.
운영 서버에는 빌드 결과물만 있었어요. 프론트엔드에 source map이 51개 있었는데 sourcesContent가 들어 있는 건 하나도 없었고, 파트너 사이트와 관리자 사이트는 nginx 뒤에 놓인 정적 파일이었어요. 노트북에서 파트너 사이트 소스 zip을 하나 찾긴 했는데, 8월 7일 버전이었고 운영 중인 건 8월 26일에 빌드된 거였어요. 운영 버전에는 이 소스에 없는 일괄 등록 기능과 엑셀 템플릿이 있었어요. 그대로 배포하면 약국들이 쓰고 있던 기능이 조용히 사라지는 거라, 이 소스는 배포 금지 참고용으로만 남겨 뒀어요.
전체 소스는 9월 29일에 도착했어요. 파일 1,445개였어요. 그 전까지는 실제로 손에 있던 부분만 작업했어요.
앱을 재현 가능하게 만들기
목표는 v* 태그를 푸시하면 CI가 두 앱을 빌드하고, 서명하고, 업로드해서 스토어 심사까지 제출하는 거였어요. 거기까지 가는 길은 자잘한 함정의 연속이었어요.
main.dart는webview_screen.dart를 import하는데 실제 파일 이름은webView_screen.dart였어요. mac이나 windows에서는 문제가 없지만 linux runner에서는 파일이 없는 걸로 나와서 빌드가 죽어요.- gradle이 keystore가 없어도 서명 설정을 평가해서,
key.properties없이 빌드하면 매번 null cast로 터졌어요. 지금은 키가 있을 때만 릴리스 서명을 하고, debug 서명된 번들이 잡히면 릴리스 workflow가 실패해요. - 넘겨받은
.jkskeystore는 열어 보니 실제로는 pkcs12였어요. - 계정의 artifact 저장 공간이 꽉 차서
upload-artifact가 실패했고, 그 뒤의 단계가 Play 업로드까지 전부 건너뛰어졌어요. 사본을 보관하는 일 때문에 릴리스가 막히면 안 되니까, 지금은 그 단계가 실패해도 넘어가요. - 빌드 번호는
100 + run number라서 아무도 손으로 올릴 필요가 없고, 스토어에서 번호가 거꾸로 가는 일도 없어요.
Android 릴리스는 CI에서 서명돼요. iOS 서명과 마지막 스토어 제출은 계정 소유자만 줄 수 있는 인증 정보를 아직 기다리고 있어요.
에뮬레이터로 스크린샷을 찍는 job이 GitHub runner에서 자꾸 멈춰서, 앱 화면을 폰 크기 png로 바로 렌더링하는 widget test로 바꿨어요.
에뮬레이터 대신 widget test로 렌더링한 오프라인 화면
두 번째 업데이트
소스가 들어온 뒤로는 쓸 만하게 만드는 게 일이었어요. 여러 개였던 동의 화면을 하나로 합치고, 로그인이나 결제 후에 원래 있던 페이지로 돌아오게 하고, 홈에는 시작 버튼을 하나만 두고, 문진에는 진행 상황 표시와 이어하기를 넣고, 결과 페이지는 결론부터 보여 주면서 성분마다 이유를 하나씩 붙였어요.
클라이언트는 약국 상품 등록을 가장 중요한 부분으로 꼽았어요. 그래서 검색하고, 고르고, 한 알당 가격을 입력하면 끝나게 바꿨어요. 월 가격은 자동으로 계산돼요. 나중에는 카탈로그에 없는 상품을 요청할 수 있게 했고, 승인되면 다른 약국들도 그 상품을 등록할 수 있어요.
카탈로그 밑바닥은 엉망이었어요. 같은 성분이 비타민C, 비타민c, (10)비타민C로 따로 저장돼 있었어요. 저장할 때 이름을 정규화하도록 하고, 기존 중복은 검토를 거친 SQL 한 번으로 합쳤어요. 406개 그룹, 성분 651개 비활성화(삭제는 하나도 안 했어요), 상품 연결 44,246건을 새로 가리키게 했어요. AI 추천 후보는 일부러 건드리지 않았어요. 처음부터 원칙이 추천 엔진은 바꾸지 않고 그 주변만 고친다는 거였거든요.
AI 채팅에도 따로 버그가 있었어요. 모든 메시지가 설문 응답으로 처리되다 보니 사용자의 실제 질문은 모델에 빈 문자열로 전달됐고, 그래서 무슨 말을 해도 같은 답만 돌아왔어요.
약사분들을 위해서는 서버에서 인쇄 작업을 받아와 상담 요약을 한 장으로 출력하는 Windows 트레이 앱을 만들었어요. 영수증을 한국어 폰트를 넣은 이미지로 렌더링하기 때문에 프린터 코드 페이지와 상관없이 출력돼요.
프린터 앱 CI 빌드에서 샘플 데이터로 뽑은 80mm 영수증
운영하기
인수 후에는 배포 파이프라인이 하나도 남아 있지 않았어요. 그래서 직접 만들었어요. 로컬에서 빌드하고, 이미지를 ssh로 보내고, 컨테이너를 교체하고, 헬스 체크를 하고, 이전 이미지는 롤백용 태그로 남겨 둬요. 9월 30일에는 서버에 인증서 갱신이 아예 없어서 TLS 인증서 세 개가 한꺼번에 만료됐고, https가 약 2시간 20분 동안 내려가 있었어요. 지금은 systemd timer가 그 일을 해요. 그 뒤 백엔드 배포 때는 새 컨테이너가 env 변수 하나가 빠져서 뜨지 못했고, 3~4분 다운타임 뒤에 스스로 롤백됐어요. 그 이후로 스크립트는 새 이미지를 먼저 여분 포트에 띄워 보고, 정상일 때만 교체해요.
지금은
운영 중이고 계속 바뀌고 있어요. 웹, 파트너, 관리자, 백엔드는 공유 UI 패키지가 있는 하나의 저장소에서 배포돼요. 약국 주문 알림은 배포는 돼 있지만 메시지 템플릿 승인이 날 때까지 꺼 둔 상태예요.