의약품 B2B 주문 플랫폼
운영 중약국·의료기관과 제약 영업자를 잇는 주문 접수 플랫폼 — 판매자가 아니라 중개 도구로 설계
- 기간
- 2026-07 ~ 현재
- 커밋
- 150
- 콘솔
- 3종
- 배치 잡
- 7개
- DB 마이그레이션
- 55건
기술 스택
- Cloudflare Workers
- Next.js
- OpenNext
- Prisma
- React
- Supabase
- Tailwind CSS
- TypeScript
실제 화면 운영 중인 서비스를 그대로 캡처했습니다
2026-10-06 기준

개요
무엇이고 누구를 위한 것인가의료기관·약국(구매회원)과 제약사 영업 담당자·CSO(판매회원)를 잇는 의약품 주문 접수 플랫폼이다. 영업 담당자가 제품을 등록하고 거래처를 초대하면, 의사·약사가 로그인해 담당자별 제품을 보고 수량을 주문하며, 담당자는 알림을 받아 주문을 처리한다.
구매자 콘솔(주문하기)·영업자 콘솔·관리자 콘솔 세 앱으로 나뉘고, 공공 의약품 데이터를 매주·매월 동기화해 검색만으로 제품을 등록하게 한다.
문제
무엇이 문제였나의약품 영업의 주문은 지금도 전화·문자·메신저로 오간다. 담당자는 거래처마다 받은 주문을 손으로 옮기고, 약국은 어느 담당자에게 무엇을 시켰는지 기록이 흩어진다.
이걸 플랫폼으로 옮기는 데는 기술보다 규제가 먼저 걸린다.
약사법. 플랫폼이 판매 주체가 되면 의약품 도매상 허가가 필요하다.
일반 소비자 노출 금지. 전문의약품의 제품·가격 정보가 일반인에게 보이면 안 된다.
리베이트 규제. 가격·프로모션 정보는 리베이트 규제 검토 대상이라 변경 이력이 남아야 한다.
개인정보. 영업자가 입력하는 병원 직원 이름·연락처·주문 메모는 플랫폼의 정보가 아니라 남의 정보다.
그리고 제품 등록 자체가 무겁다. 의약품마다 허가 정보·표준코드·보험 코드·상한금액이 있는데, 영업자가 이걸 손으로 입력하면 틀린다.
접근
어떻게 풀었나- 판매자가 아니라 중개 도구로 포지셔닝했다. 플랫폼은 주문을 "접수·전달"만 한다. 판매 당사자가 아니므로 도매상 허가 대상이 아니고, 통신판매업 신고도 필요 없다(전자상거래법 제12조). 약관에 "통신판매중개자"라고도 쓰지 않는다.
- 검증된 의료인만 들인다. 구매자는 요양기관기호·면허를 확인한 의료인·약사만 가입하고, 로그인 전에는 제품·가격을 보여주지 않는다. 영업자는 소속(제약사·CSO)을 확인하고, CSO는 판촉영업자 신고 여부를 필수로 받는다.
- 가격 변경은 전부 감사 로그로. 가격·프로모션 이력을 모두 보존한다.
- 제품 등록을 "검색해서 고르기"로 바꿨다. 식약처·심평원 공공데이터를 내부 의약품 마스터 DB로 배치 동기화하고, 영업자는 검색·선택만 한다. 등록 시점마다 외부 API를 부르지 않는다.
주요 기능
사용자가 실제로 쓰는 것- 세 콘솔 — 구매자 주문하기(모바일 웹 우선), 영업자 콘솔(데스크톱·모바일), 관리자 콘솔
- 담당 관계 — 영업자 ↔ 거래처 연결, 거래처 초대와 대량 가져오기
- 제품 등록 — 의약품 마스터 검색 기반 단건 등록과 대량 등록. 의료기기 마스터도 별도 동기화
- 주문 — 접수 → 확인 → 준비 → 완료의 상태 머신. 구매자는 확인 전까지만 취소, 영업자는 거절 가능
- 주문 템플릿 — 반복 주문을 템플릿으로
- 약가 인하 알림 — 최근 산 품목의 상한금액이 내리면 "얼마나 내렸고 그동안 몇 개 샀는지"를 먼저 알려준다
- 알림 — 주문 이벤트별 채널 매트릭스(앱 내 알림·메일)
- 약관·개인정보 처리방침 — 코드가 실제로 하는 일과 일치하도록 한 파일에서 관리
아키텍처
어떻게 구성돼 있나 buyer-web 구매자 — 주문하기
rep-web 영업자 콘솔
admin-web 관리자 콘솔
│ Next.js 16 · Cloudflare Workers · main push 시 Workers Builds 자동 배포
▼
packages/ui 공용 — API·인증·메뉴·기능 정의서·약관
│
api-proxy (Worker)
│
NestJS 11 + Prisma 7 ── GCP Cloud Run (서울)
├─ API 서비스
├─ 배치 서비스 (크론 수신)
└─ Cloud Run Jobs × 7
drug-master-sync · drug-easy-info-sync · permit-detail-sync
equipment-master-sync · drug-price-sync · drug-price-alert · order-templates
│
Supabase PostgreSQL (트랜잭션 풀러 6543 / 마이그레이션은 세션 풀러 5432)
공공데이터 → 의약품 마스터
식약처 허가정보(주 1회) · 심평원 약가마스터·표준코드(월 1회)
· 급여목록(월 1회) · 낱알식별(월 1회)
→ 정규화·코드 매핑(item_seq) → upsert → 변경분 로그 → 매핑 실패는 관리자 리뷰 큐기술적 의사결정
무엇을 고르고 무엇을 버렸나- 외부 API를 실시간으로 부르지 않는다. 제품 등록마다 식약처 API를 부르면 느리고, 쿼터에 걸리고, 상대 장애가 우리 장애가 된다. 배치로 내부 마스터에 적재하고 내부에서 검색한다. 매핑에 실패한 건은 버리지 않고 관리자 리뷰 큐로 보낸다.
- 약가 인상은 알리지 않는다. 약가가 내리면 약국은 보유 재고의 차액을 도매에 청구하는데, 낱알까지 세어 근거를 만드는 게 일이다. 그래서 인하만 알리고 구매 수량을 함께 보여준다. 인상은 청구할 일이 없으니 알림 피로만 늘린다.
- 마스터 변경 알림은 메일로 보내지 않는다. 메일 발송 한도가 하루 200통이라, 대량 동기화 결과를 메일로 보내면 그날 한도가 바로 찬다. 마스터 데이터 변경은 일부러 앱 안에서만 보여주고, 메일은 주문 같은 개별 이벤트에 아낀다.
- 처리방침은 코드와 일치해야 한다. 약관·처리방침 원문을 한 파일에 두고, 수집 항목·암호화 필드·위탁처를 바꾸면 이 파일도 같이 고치게 했다. 화면에 나가는 문장에 치환 자리표시자가 남으면 빌드가 실패하게 해뒀다. 서울 리전이어도 클라우드 사업자가 해외에서 지원 목적으로 접근하므로 국외이전으로 명시했다.
- 남의 정보는 맡아 둘 뿐이다. 영업자가 입력한 병원 직원 연락처·메모·주문 원문에 대해 플랫폼은 수탁자다. 교차 결합·통계·학습에 쓰지 않고 탈퇴 시 지운다. 이런 정보를 새로 저장하는 기능을 만들면 탈퇴 처리와 처리방침을 같이 고치는 것을 규약으로 정했다.
결과
무엇이 달라졌나세 콘솔이 각자 도메인으로 운영 중이다 — 주문하기(pharma), 영업자 콘솔(pharma-rep), 관리자 콘솔(pharma-admin).
- 공공 의약품 데이터 4종과 의료기기 마스터를 배치 잡 7개로 동기화해, 영업자는 코드를 입력하지 않고 검색으로 제품을 등록한다.
- 루트
npm run check하나로 백엔드 타입·테스트·빌드와 세 앱 빌드를 전부 검사한다. 백엔드 테스트 29개, DB 마이그레이션 55건. - 커밋 150개.
배운 점
다시 한다면백엔드 코드가 API 서비스·배치 서비스·Cloud Run Jobs 7개, 세 군데에서 돈다는 걸 배포 절차가 따라가지 못했다. 하나만 올리고 끝낸 일이 두 번(2026-09-16, 09-20) 있었고, 그때마다 구버전 잡이 매일 오탐 알림 메일을 보냈다. 그래서 세 곳을 순서대로 올리는 단일 배포 스크립트를 만들고 --check로 뒤처진 곳만 표시하게 했다. 스키마 변경은 반드시 배포보다 먼저 — 새 코드가 먼저 뜨면 그 자리에서 터진다. "배포는 한 번에"라는 원칙은 프런트엔드에서는 지켜졌지만 백엔드에서는 사고를 겪고 나서야 구조로 박혔다.