콘텐츠로 이동

빠른 시작

정적 프론트엔드는 브라우저용 스토어프론트 키로 사이트를 식별하고, 로그인 이후에는 사용자 토큰을 추가해서 개인화 API를 호출합니다.

const API_BASE = 'https://your-site.runmoa.com/api/storefront/v1';
const STOREFRONT_KEY = 'moa_pub_xxxxxxxxx';

브라우저 앱에서는 서버용 비공개 API 키를 사용하지 않습니다.

const site = await fetch(`${API_BASE}/site`, {
credentials: 'include',
headers: {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
Accept: 'application/json',
},
}).then((response) => response.json());
const products = await fetch(`${API_BASE}/products?page=1`, {
credentials: 'include',
headers: {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
Accept: 'application/json',
},
}).then((response) => response.json());

콘텐츠 storefront를 만들 때는 아래처럼 type query를 사용합니다. 콘텐츠 목록 응답은 products가 아니라 classes.data에서 읽습니다.

const contents = await fetch(`${API_BASE}/contents?type=offline&page=1&limit=12`, {
credentials: 'include',
headers: {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
Accept: 'application/json',
},
}).then((response) => response.json());
const contentCards = contents.classes?.data ?? [];
const authConfig = await fetch(`${API_BASE}/auth/schoolmoa-client`, {
credentials: 'include',
headers: {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
Accept: 'application/json',
},
}).then((response) => response.json());
const redirectUri = `${window.location.origin}/auth/callback`;
window.location.href =
`https://www.runmoa.com/login/?redirect_uri=${encodeURIComponent(redirectUri)}` +
`&client_id=${encodeURIComponent(authConfig.scid)}&lg=kr`;

5. Callback 처리 후 사용자 토큰 저장

섹션 제목: “5. Callback 처리 후 사용자 토큰 저장”
const callback = await fetch(`${API_BASE}/auth/schoolmoa/callback`, {
method: 'POST',
credentials: 'include',
headers: {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ code }),
});
const { token } = await callback.json();
localStorage.setItem('runmoa_user_token', token);
const me = await fetch(`${API_BASE}/me`, {
credentials: 'include',
headers: {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
}).then((response) => response.json());

Callback 응답의 user 값은 최소 정보만 포함될 수 있습니다. 화면에 이름, 이메일, 전화번호 같은 프로필을 바로 표시해야 하면 토큰 저장 후 GET /me를 호출해서 최신 사용자 정보를 가져옵니다.

const token = localStorage.getItem('runmoa_user_token');
const me = await fetch(`${API_BASE}/me`, {
credentials: 'include',
headers: {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
}).then((response) => response.json());

마이페이지는 로그인 토큰으로 프로필, 주문, 주문 항목, 보유 콘텐츠를 조회합니다.

const headers = {
'X-Runmoa-Site-Key': STOREFRONT_KEY,
Authorization: `Bearer ${token}`,
Accept: 'application/json',
};
const [orders, orderLines, contents] = await Promise.all([
fetch(`${API_BASE}/me/orders?page=1&limit=20`, {
credentials: 'include',
headers,
}).then((response) => response.json()),
fetch(`${API_BASE}/me/order-lines?page=1&limit=20`, {
credentials: 'include',
headers,
}).then((response) => response.json()),
fetch(`${API_BASE}/me/contents/all/all?page=1&limit=20`, {
credentials: 'include',
headers,
}).then((response) => response.json()),
]);

상품/콘텐츠 주문 항목은 GET /me/order-lines 응답의 line_item_id를 기준으로 처리합니다. 구매한 콘텐츠 열기는 POST /me/content-entitlements/{lineItemId}/open, 구매확정/취소/환불/반품/교환은 POST /me/order-lines/{lineItemId}/actions, 후기는 POST /me/order-lines/{lineItemId}/review를 사용합니다.

  1. 장바구니에 상품 또는 콘텐츠를 담습니다.
  2. 로그인 사용자 토큰으로 POST /orders를 호출해 주문을 생성합니다.
  3. POST /payments/initialize를 호출해 결제를 시작합니다.
  4. 외부 구현 기준 결제는 표준 NicePay 카드결제만 사용합니다. /payments/initialize 응답의 form payload를 submit합니다.

콘텐츠를 결제할 때는 상품처럼 variant.id를 쓰지 않습니다. GET /contents/{contentId}에서 class.default.all_options[].ID를 먼저 확인하고, 이 값이 없으면 class.curriculums[].option_id를 선택값으로 저장합니다. 주문 생성 시에는 old_data: [{ ID: optionId }] 형태로 보냅니다.

  • 브라우저에는 moa_pub_... 형식의 스토어프론트 키만 넣습니다.
  • 상품/콘텐츠 생성, 수정, 삭제는 정적 프론트엔드에서 직접 호출하지 않습니다.
  • 사용자별 장바구니, 주문, 마이페이지 API는 사용자 토큰이 필요합니다.
  • 게스트 장바구니를 쓰는 브라우저 앱은 fetch 요청에 credentials: 'include'를 넣어 device cookie가 유지되도록 합니다.