← 여행 eSIM 기획서
🛠️ 봉이 여행 eSIM — 개발 명세 (프론트·어드민 샘플)
개발자가 이 문서만으로 착수 가능 · 유심사 API 구조 벤치마크 · Mock→플래그 교체 · 2026-07
유심사 3단 API(products→plans→options) 구조를 표준으로 채택. 공급사(CMLink/eSIM Access)는 services/esimProvider.js 어댑터로 추상화 → env ESIM_PROVIDER=mock|cmlink|esimaccess로 교체.
1 · 아키텍처
// 데이터 흐름
[고객 프론트 client/src/pages/esim/]
│ fetch /api/esim/*
▼
[Express routes/esim.js] ──▶ [services/esimProvider.js // 어댑터(Mock→실API)]
│ │ ESIM_PROVIDER 플래그로 mock|cmlink|esimaccess 선택
▼ ▼
[Supabase cs.esim_*] [공급사 API (CMLink/eSIM Access)]
product·option·order products/plans/options/order
│
▼
[어드민 docs/esim-admin.html] // 상품 동기화·마진·주문/발급 현황
2 · DB 스키마 (Supabase cs 스키마)
create table cs.esim_products (
id bigserial primary key,
provider text, -- 'cmlink'|'esimaccess'|'mock'
provider_pid text, -- 공급사 상품ID
destination text, -- '일본','유럽 42개국'
dest_type text, -- 'single'|'multiple'
supplier text, carrier text, -- 'CMI-DW', 'Softbank/KDDI'
keyword text, -- 검색용(도쿄;오사카…)
is_active boolean default true, synced_at timestamptz
);
create table cs.esim_options (
id bigserial primary key,
product_id bigint references cs.esim_products,
provider_oid text, -- 공급사 옵션ID(주문 시 사용)
name text, -- '5일 / 완전 무제한'
days int, data_type text, -- 'unlimited'|'fixed'
quota_text text, is_unlimited boolean,
wholesale_usd numeric, -- 도매(계약 후 채움, 없으면 null)
fx_rate numeric, margin_rate numeric, -- 정책 fetch
retail_krw int, -- 소매가(계산 or 수동)
is_hotspot boolean, free_googlemap boolean, care_amount int -- 유심사 벤치
);
create table cs.esim_orders (
id bigserial primary key,
customer_id bigint, option_id bigint references cs.esim_options,
retail_krw int, status text, -- 'paid'|'provisioning'|'active'|'failed'
provider_oid text, provider_order_id text,
qr_url text, install_code text, -- LPA:1$... eSIM 프로파일
ordered_at timestamptz, activated_at timestamptz
);
3 · API 명세 (routes/esim.js) — 요청/응답 샘플
GET /api/esim/destinations 목적지 목록
// res 200
{ "data": [
{ "id":1, "destination":"일본", "dest_type":"single", "from_krw":1500, "keyword":"도쿄;오사카" },
{ "id":2, "destination":"유럽 42개국", "dest_type":"multiple", "from_krw":1700 }
] }
GET /api/esim/products/:destId/options 옵션(일수·데이터·가격)
// res 200
{ "data": [
{ "id":11, "name":"5일 / 완전 무제한", "days":5, "data_type":"unlimited",
"retail_krw":16100, "is_hotspot":true, "free_googlemap":true, "care_amount":300000 },
{ "id":12, "name":"1일 / 2GB", "days":1, "data_type":"fixed", "retail_krw":2600 }
] }
POST /api/esim/orders 주문·결제 후 발급
// req { "option_id":11, "email":"user@x.com", "payment_token":"..." }
// res 200 { "order_id":1001, "status":"active",
"qr_url":"https://.../qr/1001.png", "install_code":"LPA:1$smdp..." }
플로우: 결제 성공 → provider.order() → QR/프로파일 저장 → 고객 마이페이지. 실패 시 status=failed + 재발급 큐.
4 · 공급사 어댑터 (services/esimProvider.js) — Mock 샘플
// env ESIM_PROVIDER=mock|cmlink|esimaccess
const providers = { mock, cmlink, esimaccess };
export const esim = providers[process.env.ESIM_PROVIDER || 'mock'];
// --- mock 어댑터 (계약 전 전 기능 개발용) ---
const mock = {
async listDestinations() { return [{id:1,destination:'일본',from_krw:1500}]; },
async listOptions(destId) { return [{id:11,name:'5일/무제한',retail_krw:16100}]; },
async order(optionId, customer) {
return { order_id: Date.now(), status:'active',
qr_url:'https://mock/qr.png', install_code:'LPA:1$mock' };
},
async status(orderId) { return { status:'active' }; }
};
// --- cmlink 어댑터 (계약 후: 실 API 호출로 동일 인터페이스 구현) ---
const cmlink = {
async listOptions(destId){ const r=await fetch(CMLINK_API+..., {headers:{Authorization:key}}); ... }
// order/status 동일 시그니처
};
✅ 인터페이스 고정(listDestinations·listOptions·order·status) → Mock으로 프론트·어드민 완성 후, 계약되면 cmlink/esimaccess 구현만 채우고 플래그 전환. cti.js와 동일 패턴.
5 · 프론트 (client/src/pages/esim/) — 컴포넌트·샘플
화면 구성 (라우트)
| 경로 | 컴포넌트 | API | 역할 |
| /esim | DestinationGrid | GET /destinations | 목적지 선택(검색·인기) |
| /esim/:destId | OptionList | GET /products/:id/options | 일수·데이터 선택 |
| /esim/checkout | Checkout | POST /orders | PASS·결제 |
| /esim/complete/:orderId | QrInstall | GET /orders/:id | QR·설치가이드(iOS/Android) |
| /mypage/esim | MyEsimList | GET /orders?mine | 사용현황 |
샘플 — OptionList.jsx
function OptionList({ destId }) {
const [opts, setOpts] = useState([]);
useEffect(() => { api.get(`/esim/products/${destId}/options`).then(r=>setOpts(r.data)); }, [destId]);
return opts.map(o => (
<OptionCard key={o.id}
title={o.name} price={o.retail_krw}
badges={[o.free_googlemap&&'구글맵무료', o.care_amount&&'안심케어'].filter(Boolean)}
onSelect={() => nav(`/esim/checkout?option=${o.id}`)} />
));
}
유심사 벤치 반영: 옵션 카드에 구글맵무료·안심케어(care_amount)·핫스팟 배지, "로밍 대비 최대 70%↓" 라벨, cs 로밍상담 딥링크(?from=cs).
6 · 어드민 (docs/esim-admin.html) — 구성·샘플
① 상품 관리
// 공급사 상품 동기화
[동기화] → provider.listProducts()
→ upsert cs.esim_products/options
[마진 설정] retail = round(
wholesale_usd × fx × (1+margin_rate))
// margin_rate=정책 fetch(하드코딩X)
② 주문·발급 현황
order_id│고객│상품│소매│발급│상태
1001│홍*동│일본5일│16,100│✅QR│active
1002│김*수│유럽5일│19,900│—│paid
// 실패 시 [재발급] → provider.order 재시도
어드민 기능 체크리스트
| 기능 | 설명 |
| 상품 동기화 | 공급사 API pull → esim_products/options upsert (수동/cron) |
| 마진율 정책 | 목적지·데이터타입별 margin_rate 설정(하드코딩 금지) |
| 주문/발급 모니터 | status별 필터·재발급·환불(refund) |
| 정산 연동 | 기존 페이백/포인트 트리거 재사용, 티켓 prefix E |
7 · 유심사 벤치마크 (반영 포인트)
| 유심사 요소 | 봉이 반영 |
| API 3단(products→plans→options) | 동일 채택(routes/esim.js) |
| 목적지 68(multiple 19 지역묶음·single 49) | dest_type 컬럼으로 구분·검색 |
| usimsaCare(30만원 보상) | care_amount 컬럼 → 안심케어 배지·부가상품화 |
| 구글맵 데이터 무료(isFreeGoogleMap) | free_googlemap 배지 노출 |
| 무제한(데일리)+정량 210옵션 | data_type(unlimited/fixed)·일수×데이터 매트릭스 |
| 핫스팟·음성·QoS 필드 | is_hotspot 등 옵션 속성 노출 |
봉이 차별화(유심사 대비): 통신 상담 고객 크로스셀(cs 무인센터 로밍상담 → eSIM 딥링크) + 기존 페이백/포인트 결합.