빠른 시작
정적 프론트엔드는 브라우저용 스토어프론트 키로 사이트를 식별하고, 로그인 이후에는 사용자 토큰을 추가해서 개인화 API를 호출합니다.
1. 기본 값 준비
섹션 제목: “1. 기본 값 준비”const API_BASE = 'https://your-site.runmoa.com/api/storefront/v1';const STOREFRONT_KEY = 'moa_pub_xxxxxxxxx';브라우저 앱에서는 서버용 비공개 API 키를 사용하지 않습니다.
2. 사이트 정보 조회
섹션 제목: “2. 사이트 정보 조회”const site = await fetch(`${API_BASE}/site`, { credentials: 'include', headers: { 'X-Runmoa-Site-Key': STOREFRONT_KEY, Accept: 'application/json', },}).then((response) => response.json());3. 상품 또는 콘텐츠 목록 조회
섹션 제목: “3. 상품 또는 콘텐츠 목록 조회”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 ?? [];4. Schoolmoa 로그인 시작
섹션 제목: “4. Schoolmoa 로그인 시작”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를 호출해서 최신 사용자 정보를 가져옵니다.
6. 사용자 API 호출
섹션 제목: “6. 사용자 API 호출”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());7. 마이페이지 조회
섹션 제목: “7. 마이페이지 조회”마이페이지는 로그인 토큰으로 프로필, 주문, 주문 항목, 보유 콘텐츠를 조회합니다.
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를 사용합니다.
8. 결제 흐름
섹션 제목: “8. 결제 흐름”- 장바구니에 상품 또는 콘텐츠를 담습니다.
- 로그인 사용자 토큰으로
POST /orders를 호출해 주문을 생성합니다. POST /payments/initialize를 호출해 결제를 시작합니다.- 외부 구현 기준 결제는 표준 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가 유지되도록 합니다.