주문과 마이페이지
이 페이지의 API는 모두 사용자 토큰이 필요합니다.
외부 정적 스토어프론트의 마이페이지에서는 프로필, 주문 목록, 주문 상세, 보유 콘텐츠를 브라우저용 스토어프론트 키와 사용자 토큰으로 조회합니다.
기본 fetch wrapper
섹션 제목: “기본 fetch wrapper”마이페이지 API는 항상 X-Runmoa-Site-Key와 Authorization을 함께 보냅니다.
const API_BASE = 'https://your-site.runmoa.com/api/storefront/v1';const STOREFRONT_KEY = 'moa_pub_xxxxxxxxx';const token = localStorage.getItem('runmoa_user_token');
async function storefrontMe(path) { const response = await fetch(`${API_BASE}${path}`, { credentials: 'include', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, Accept: 'application/json', }, });
if (!response.ok) { throw new Error(`Runmoa API ${response.status}`); }
return response.json();}내 정보
섹션 제목: “내 정보”| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /me | 내 정보 조회 |
| GET | /me/access-context | 사용자 접근 권한 컨텍스트 조회 |
const me = await storefrontMe('/me');
// callback 응답의 user는 최소 정보일 수 있으므로,// 로그인 직후에도 /me를 다시 호출해서 이름, 이메일, 전화번호를 표시합니다.const user = me.user;주문 조회
섹션 제목: “주문 조회”| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /me/orders | 내 주문 목록 |
| GET | /me/orders/{orderId} | 내 주문 상세 |
| GET | /me/orders/{orderId}/details | 내 주문 상세 항목 |
| GET | /me/order-lines | 상품/콘텐츠 주문 항목 통합 목록 |
| GET | /me/order-lines/{lineItemId} | 상품/콘텐츠 주문 항목 통합 상세 |
마이페이지 첫 화면은 GET /me/orders로 주문 단위 목록을 보여줍니다.
const orders = await storefrontMe('/me/orders?page=1&limit=20');대표 주문 목록 응답:
{ "orders": [ { "ID": 12837, "id": 12837, "order_number": "RM-20260622-12837", "status": "paid", "price": 49000, "price_total": 49000, "created_at": "2026-06-22T09:30:00+09:00", "paid_date": "2026-06-22T09:31:00+09:00" } ], "total": 1, "current_page": 1}const orderId = 12837;
const [summary, details] = await Promise.all([ storefrontMe(`/me/orders/${orderId}`), storefrontMe(`/me/orders/${orderId}/details`),]);대표 주문 상세 응답:
{ "order": { "ID": 12837, "order_number": "RM-20260622-12837", "status": "paid", "price_total": 49000, "receiver_name": "홍길동", "receiver_phone": "01012345678", "address": "서울시 강남구 테헤란로 123", "address_detailed": "101호", "postal_code": "06234" }}주문 항목 통합 API
섹션 제목: “주문 항목 통합 API”상품과 콘텐츠를 같은 UI 구조로 보여주려면 GET /me/order-lines를 사용합니다.
const orderLines = await storefrontMe('/me/order-lines?page=1&limit=20');line_item_id는 주문 항목을 가리키는 문자열입니다. 프론트엔드는 값을 분해하지 말고 그대로 다음 API에 전달합니다.
대표 응답:
{ "line_items": [ { "line_item_id": "product:77", "kind": "product", "content_type": null, "order_id": 12837, "lookup_id": 77, "item_id": 12001, "type": "product", "product_id": 12001, "variant_id": 88421, "title": "러닝 재킷", "subtitle": "Black / M", "quantity": 1, "price": 49000, "shipping_price": 3000, "image_url": "https://cdn.example.com/products/running-jacket.jpg", "status": "delivered", "serve": { "mode": "none" }, "actions": [ { "type": "exchange_request", "enabled": true, "label": "교환요청", "method": "POST" }, { "type": "return_request", "enabled": true, "label": "반품요청", "method": "POST" }, { "type": "confirm_purchase", "enabled": true, "label": "구매확정", "method": "POST" } ], "review": { "exists": false, "review_id": null } } ], "total": 1, "current_page": 1, "per_page": 20}콘텐츠 주문 항목은 같은 배열 안에 아래처럼 들어올 수 있습니다.
{ "line_item_id": "content:99031", "kind": "content", "content_type": "vod", "order_id": 12838, "lookup_id": 99031, "item_id": 6401, "content_id": 6401, "option_id": 55102, "curriculum_id": 91201, "title": "브랜드 포지셔닝 워크숍", "subtitle": "1강. 브랜드 핵심 메시지 정리", "quantity": 1, "price": 99000, "shipping_price": 0, "image_url": "https://cdn.example.com/contents/brand-positioning-thumb.jpg", "status": "paid", "access": { "state": "available", "watchable": true, "reason": null }, "serve": { "mode": "internal_player" }, "actions": [ { "type": "open", "enabled": true, "label": "콘텐츠 열기", "method": "POST" }, { "type": "confirm_purchase", "enabled": true, "label": "구매확정", "method": "POST" }, { "type": "refund_request", "enabled": true, "label": "환불 요청", "method": "POST" } ], "review": { "exists": false, "review_id": null, "targets": [ { "curriculum_id": 91201, "title": "1강. 브랜드 핵심 메시지 정리" } ] }}상품과 콘텐츠 모두 버튼은 actions[].enabled === true인 값만 표시합니다. line_item_id는 product:77, content:99031 같은 문자열 그대로 다음 API path에 넣습니다. open 버튼은 POST /me/content-entitlements/{lineItemId}/open을 호출하고, 구매확정/취소/환불/반품/교환 버튼은 POST /me/order-lines/{lineItemId}/actions를 호출합니다.
특정 주문 항목 상세는 같은 line_item_id로 조회합니다.
const line = await storefrontMe('/me/order-lines/product:77');내 콘텐츠와 커리큘럼
섹션 제목: “내 콘텐츠와 커리큘럼”| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /me/content-counts/{type} | 내 콘텐츠 수량 |
| GET | /me/contents/{type}/{status} | 내 콘텐츠 목록 |
| GET | /me/content-items/{contentId} | 구매한 콘텐츠 상세/수강 데이터 |
| GET | /me/content-items/{contentId}/progress | 구매한 콘텐츠 진행률/시청 만료 정보 |
보유 콘텐츠 화면은 콘텐츠 유형과 상태를 path 값으로 조회합니다. 처음 붙일 때는 all/all로 조회하고, 사이트에서 사용하는 유형과 상태가 정해지면 탭을 나누면 됩니다.
const ownedContents = await storefrontMe('/me/contents/all/all?page=1&limit=20');대표 보유 콘텐츠 응답:
{ "contents": [ { "id": 501, "content_id": 4321, "title": "러닝 자세 클래스", "name": "러닝 자세 클래스", "type": "vod", "status": "active", "progress_rate": 42, "expire_date": "2026-12-31" } ], "total": 1}구매 후 콘텐츠 상세 표시
섹션 제목: “구매 후 콘텐츠 상세 표시”콘텐츠 판매 스토어를 만들 때는 목록만으로 끝나지 않습니다. 사용자가 구매한 뒤 실제로 들어가는 화면은 GET /me/content-items/{contentId}를 기준으로 구현합니다.
const lesson = await storefrontMe('/me/content-items/6401');이 응답은 공개 상세와 달리 “내 구매 이력 기준” 데이터를 내려줍니다. 즉 아래 같은 필드가 붙을 수 있습니다.
class: 내가 접근 가능한 실제 커리큘럼/회차 목록chapters: VOD 챕터/영상 목록watch_able: 지금 재생 가능한지hold_blocked: 일시정지 상태 때문에 재생이 막혀 있는지parent_class: 원본 콘텐츠 기본 정보
대표 응답 예시:
{ "class": [ { "ID": 91201, "title": "1강. 브랜드 핵심 메시지 정리", "option_id": 55102, "class_id": 6401, "type": "vod", "lookup_id": 99031, "status": "processing", "progress_rate": 42, "last_viewed_date": "2026-06-23 18:30:00", "resume_position": 318, "download_duration": 90 } ], "chapters": [ { "ID": 71, "title": "브랜드 전략", "curriculums_details": [ { "ID": 91201, "title": "1강. 브랜드 핵심 메시지 정리", "paid": "1" } ] } ], "watch_able": true, "hold_blocked": false, "parent_class": { "ID": 6401, "title": "브랜드 포지셔닝 워크숍" }}공개 상세 GET /contents/{contentId}와 구매 후 상세 GET /me/content-items/{contentId}는 같은 화면 모델로 쓰지 않습니다. 공개 상세는 “판매용”, 구매 후 상세는 “학습/다운로드용”입니다.
구매 후 콘텐츠 열기
섹션 제목: “구매 후 콘텐츠 열기”구매한 콘텐츠를 실제로 보여줄 때는 line_item_id를 사용합니다.
| 메서드 | 경로 | 설명 |
|---|---|---|
| POST | /me/content-entitlements/{lineItemId}/open | 구매한 콘텐츠 열기 |
VOD 콘텐츠는 런모아 플레이어 URL을 반환합니다.
const result = await fetch(`${API_BASE}/me/content-entitlements/content:99031/open`, { method: 'POST', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', },}).then((response) => response.json());
window.location.href = result.serve.url;대표 VOD 응답:
{ "line_item": { "line_item_id": "content:99031", "kind": "content", "content_type": "vod", "title": "브랜드 포지셔닝 워크숍" }, "serve": { "mode": "internal_player", "url": "https://your-site.runmoa.com/my-page/media-player?class_id=6401&curriculum_id=91201" }}디지털 콘텐츠는 먼저 파일 정보를 조회합니다.
const info = await fetch(`${API_BASE}/me/content-entitlements/content:99032/open`, { method: 'POST', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', },}).then((response) => response.json());다운로드 URL이 필요할 때는 issue_urls: true를 보냅니다. URL은 짧은 시간만 유효하므로 버튼을 누를 때마다 새로 발급합니다.
const download = await fetch(`${API_BASE}/me/content-entitlements/content:99032/open`, { method: 'POST', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ issue_urls: true }),}).then((response) => response.json());대표 디지털 파일 정보 응답:
{ "line_item": { "line_item_id": "content:99032", "kind": "content", "content_type": "digital_content", "title": "인스타그램 콘텐츠 캘린더 기획 서비스" }, "serve": { "mode": "download" }, "files": [ { "file_id": 8801, "name": "instagram-calendar-template.pdf", "size": 2480000, "expires_at": null } ]}대표 다운로드 URL 발급 응답:
{ "serve": { "mode": "download", "expires_in": 300 }, "urls": [ { "file_id": 8801, "url": "https://download.example.com/runmoa/signed-token", "expires_at": "2026-06-25T18:30:00+09:00" } ]}주문 항목 액션
섹션 제목: “주문 항목 액션”상품 주문 취소, 교환/반품 요청, 구매확정, 콘텐츠 환불 요청, 콘텐츠 구매확정은 같은 액션 API를 사용합니다.
| 메서드 | 경로 | 설명 |
|---|---|---|
| POST | /me/order-lines/{lineItemId}/actions | 주문 항목 액션 실행 |
프론트엔드는 먼저 line_item.actions에서 enabled: true인 액션만 버튼으로 보여줍니다.
await fetch(`${API_BASE}/me/order-lines/product:77/actions`, { method: 'POST', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', },body: JSON.stringify({ type: 'confirm_purchase' }),});대표 액션 응답은 다시 line_item을 반환합니다. 화면은 응답의 status, actions, review를 다시 반영합니다.
{ "line_item": { "line_item_id": "product:77", "kind": "product", "status": "purchase_confirmed", "title": "러닝 재킷", "actions": [ { "type": "review_create", "enabled": true, "label": "후기 작성", "method": "POST" } ], "review": { "exists": false, "review_id": null } }}사용 가능한 type:
| type | 대상 | 설명 |
|---|---|---|
cancel_request | 상품 | 배송 전 주문 취소 요청 |
exchange_request | 상품 | 교환 요청 |
return_request | 상품 | 반품 요청 |
confirm_purchase | 상품/콘텐츠 | 구매확정 |
refund_request | 콘텐츠 | 환불 또는 취소 요청 |
취소/환불/반품/교환 요청에는 사유를 함께 보낼 수 있습니다.
{ "type": "refund_request", "title": "환불 요청", "description": "수강 일정이 맞지 않습니다."}후기 작성
섹션 제목: “후기 작성”상품 후기와 콘텐츠 후기는 같은 경로를 사용합니다.
| 메서드 | 경로 | 설명 |
|---|---|---|
| POST | /me/order-lines/{lineItemId}/review | 주문 항목 후기 작성 또는 수정 |
후기는 line_item.actions에 review_create 또는 review_update가 enabled: true일 때만 표시합니다.
await fetch(`${API_BASE}/me/order-lines/content:99031/review`, { method: 'POST', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ rating: 5, content: '실무에 바로 적용하기 좋았습니다.' }),});VOD처럼 후기 대상 커리큘럼을 사용자가 선택해야 하는 화면에서는 curriculum_id를 함께 보냅니다.
진행률과 다운로드
섹션 제목: “진행률과 다운로드”VOD나 디지털 콘텐츠는 보유 목록만 보여주는 것으로 끝나지 않습니다. 진행률 표시와 다운로드도 별도 API가 있습니다.
const progress = await storefrontMe('/me/content-items/6401/progress');대표 진행률 응답:
{ "lookup": { "lookup_id": 99031, "progress_rate": 42, "view_expire_at": "2026-09-21 23:59:59", "hold_limit": 3, "hold_used": 1, "hold_active": 0 }}디지털 파일이 있는 콘텐츠도 주문 항목의 line_item_id로 엽니다. 파일 목록은 POST /me/content-entitlements/{lineItemId}/open으로 조회하고, 실제 다운로드 URL이 필요하면 같은 경로에 issue_urls: true를 보냅니다.
const info = await fetch(`${API_BASE}/me/content-entitlements/content:99032/open`, { method: 'POST', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', }}).then((response) => response.json());const download = await fetch(`${API_BASE}/me/content-entitlements/content:99032/open`, { method: 'POST', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ issue_urls: true, }),}).then((response) => response.json());