# 오퍼월 상품 레이아웃 연동 가이드

상품 목록을 다양한 레이아웃으로 렌더링하는 임베드형 위젯입니다.\
스크립트 삽입 후 인증 키와 섹션 구조만 정의하면 적용할 수 있습니다.\
상품 데이터는 위젯이 자동으로 가져옵니다.

**데모:** <a href="https://ad.mallpie.co.kr/ncms/offerwall/index.html" class="button primary">데모 보기</a>

***

## 적용 방법

{% stepper %}
{% step %}

### 컨테이너 준비

위젯이 렌더링될 빈 `div`를 원하는 위치에 추가합니다.

```html
<div id="product-widget"></div>
```

{% endstep %}

{% step %}

### 위젯 마운트

섹션 구조만 정의하면 됩니다.

```html
<script type="module">
  import LayoutWidget from 'https://ad.mallpie.co.kr/ncms/offerwall/layout-widget.js';

  LayoutWidget.mount('#product-widget', {
    userKey: 'YOUR_USER_KEY',
    apiKey: 'YOUR_API_KEY',
    adid: 'YOUR_ADID', // 선택값
    pageTitle: '추천 상품',
    sections: [
      {
        id: 'section-1',
        title: '이번 주 인기',
        layout: { type: 'grid', columns: 2 },
      },
    ],
  });
</script>
```

{% endstep %}
{% endstepper %}

***

## API

### `mount(target, options)`

위젯을 초기 렌더링합니다. 페이지 로드 시 1회 호출합니다.

```js
import LayoutWidget from 'https://ad.mallpie.co.kr/ncms/offerwall/layout-widget.js';

LayoutWidget.mount('#product-widget', options);
```

### `update(target, partialOptions)`

이미 렌더링된 위젯의 데이터를 변경합니다. (예: 탭 전환, 필터링)

```js
LayoutWidget.update('#product-widget', {
  sections: newSections,
});
```

### `unmount(target)`

위젯을 제거합니다.

```js
LayoutWidget.unmount('#product-widget');
```

***

## 옵션 구조

### 최상위 옵션

| 필드                | 타입                  | 필수 | 설명                                          |
| ----------------- | ------------------- | -- | ------------------------------------------- |
| `userKey`         | `string`            | ✅  | <p>사용자 키<br>미설정 시 상품 상세 페이지로 이동이 차단됩니다.</p> |
| `apiKey`          | `string`            | ✅  | API 인증 키                                    |
| `adid`            | `string`            |    | 광고 식별자 (선택값)                                |
| `sections`        | `Section[]`         | ✅  | 섹션 목록                                       |
| `pageTitle`       | `string`            |    | 위젯 상단 타이틀                                   |
| `pageSubtitle`    | `string`            |    | 위젯 상단 서브타이틀                                 |
| `theme`           | `'light' \| 'dark'` |    | 테마 (기본값: `'light'`)                         |
| `showLayoutIndex` | `boolean`           |    | 상단 섹션 앵커 버튼 표시 여부 (기본값: `true`)             |
| `loadingFallback` | `boolean`           |    | API 호출 중 로딩 화면 표시 여부 (기본값: `true`)          |
| `errorFallback`   | `boolean`           |    | API 실패 시 에러 화면 표시 여부 (기본값: `true`)          |

### 섹션(Section)

| 필드           | 타입                | 필수 | 설명                   |
| ------------ | ----------------- | -- | -------------------- |
| `id`         | `string`          | ✅  | 고유 식별자               |
| `title`      | `string`          |    | 섹션 타이틀 (생략 시 헤더 미노출) |
| `layout`     | `Layout`          | ✅  | 레이아웃 설정              |
| `subtitle`   | `string`          |    | 섹션 서브타이틀             |
| `cardStyle`  | `CardStyle`       |    | 카드 표시 옵션             |
| `responsive` | `ResponsiveRules` |    | 반응형 브레이크포인트별 레이아웃 설정 |

### 카드 스타일(CardStyle)

| 필드                  | 타입                                  | 설명                          |
| ------------------- | ----------------------------------- | --------------------------- |
| `showBrand`         | `boolean`                           | 브랜드명 표시                     |
| `showOriginalPrice` | `boolean`                           | 정가 취소선 표시                   |
| `showReviewMeta`    | `boolean`                           | 별점 / 리뷰 수 표시                |
| `showFloatingCart`  | `boolean`                           | 카드 우하단 공유하기 버튼 표시           |
| `showRewardBadge`   | `boolean`                           | 적립 혜택 배지 표시                 |
| `showCouponBadge`   | `boolean`                           | 쿠폰 배지 표시                    |
| `showMissionLabel`  | `boolean`                           | 미션 라벨 표시                    |
| `priceEmphasis`     | `'default' \| 'coupon' \| 'reward'` | 가격 강조 방식 (기본값: `'default'`) |

***

## 레이아웃 타입

### `grid` — 기본 그리드

```js
layout: { type: 'grid', columns: 2 } // columns: 2 또는 3
```

가장 익숙한 모바일 커머스 진열 방식입니다. 2단(`columns: 2`)은 카드 정보를 충분히 보여주면서도 탐색 효율이 높고, 3단(`columns: 3`)은 이미지 위주 카테고리에 적합합니다.

***

### `square-grid` — 공유 버튼 그리드

```js
layout: { type: 'square-grid', columns: 3 } // columns: 2 또는 3
```

카드 우하단에 공유하기 버튼(`showFloatingCart`)을 노출하는 대량 진열형 구조입니다. 모바일에서는 OS 네이티브 공유 시트가 열리고, 미지원 환경에서는 클립보드 복사로 fallback 처리됩니다.

***

### `list` — 리스트

```js
layout: { type: 'list' }
```

상품명, 가격, 리뷰 수 같은 텍스트 정보 중심으로 비교하기 쉬운 형태입니다. 검색 결과, 가격 비교 영역에 적합합니다.

***

### `simple-list` — 단순 리스트

```js
layout: { type: 'simple-list' }
```

썸네일 + 상품명 + 가격만 노출하는 경량 행(row) 형태입니다. 위시리스트, 최근 본 상품처럼 정보 밀도를 낮추고 빠르게 훑어보는 영역에 적합합니다.

***

### `horizontal` — 가로 스크롤

```js
layout: { type: 'horizontal', itemWidth: 260 } // itemWidth 기본값: 260(px)
```

가로 스크롤 방식입니다. 추천·연관 상품 영역에 적합합니다.

***

### `hero` — 히어로

```js
layout: { type: 'hero' }
```

지금 가장 밀고 싶은 상품을 대형 카드로 강조하고, 보조 상품으로 자연스럽게 이어주는 구조입니다.

***

### `magazine` — 매거진

```js
layout: { type: 'magazine' }
```

상단 오버레이 카드 2개 + 하단 가로 스크롤 구조입니다. 브랜드 무드와 탐색 경험을 강조하는 에디토리얼 스타일 큐레이션 영역에 적합합니다.

***

### `feature-side` — 피처 사이드

```js
layout: { type: 'feature-side' }
```

첫 번째 상품을 좌측 대형 카드로 강조하고, 우측에 2개 카드를 세로로 쌓은 뒤 나머지를 2열 그리드로 이어붙인 구성입니다. MD 추천이나 기획전처럼 대표 상품 하나를 부각하면서 관련 상품을 함께 노출할 때 적합합니다.

***

### `editorial` — 에디토리얼

```js
layout: { type: 'editorial' }
```

상품별로 대형 이미지와 텍스트 정보가 번갈아 나오는 풀 너비 스크롤 구조입니다. 상품 5개 이하의 기획전·에디터 추천 콘텐츠에 적합합니다.

***

### `split` — 스플릿

```js
layout: { type: 'split' }
```

고관여 상품에서 이미지와 상세 정보, 관련 상품을 동시에 확인하는 데스크톱 친화형 구조입니다.

***

### `masonry` — Masonry 그리드

```js
layout: { type: 'masonry', columns: 2 } // columns: 2 또는 3
```

상품 카드를 다단으로 배치해 자연스러운 시선 흐름과 구경하는 재미를 살리는 방식입니다.

***

### `overlay` — 오버레이

```js
layout: { type: 'overlay' }
```

화이트 스페이스를 줄이고 비주얼 임팩트를 극대화하는 스트릿 무드형 카드입니다. 이미지 위에 상품명·가격이 오버레이되며, 다크 배경 섹션에 적합합니다.

***

### `mixed-grid` — 혼합 그리드

```js
layout: { type: 'mixed-grid' }
```

첫 번째 상품을 대형으로 강조하고, 나머지를 2열 그리드로 나열합니다.

***

### `swipe` — 스와이프 캐러셀

```js
layout: { type: 'swipe' }
```

한 번에 한 상품씩 전체 너비로 노출하는 스와이프 캐러셀입니다. 손가락으로 밀거나 하단 dot을 탭해 상품을 전환할 수 있습니다. 기획전·메인 배너형 상품 노출에 적합합니다.

***

### `interactive` — 인터랙티브

```js
layout: { type: 'interactive', columns: 2, mobileInteraction: 'tap' } // columns: 2 또는 3
```

데스크톱에서는 마우스 오버 시 대체 이미지로 전환됩니다. 모바일 동작은 `mobileInteraction` 옵션으로 설정합니다.

| `mobileInteraction` | 설명                                           |
| ------------------- | -------------------------------------------- |
| `'tap'`             | 이미지 영역 탭 → 대체 이미지 전환 / 카드 바디 탭 → 상품 이동 (기본값) |
| `'auto'`            | CSS 애니메이션으로 대체 이미지 자동 교대 노출                  |

***

## 반응형(Responsive)

섹션별로 태블릿·모바일 브레이크포인트에서의 레이아웃을 조정할 수 있습니다.

```js
{
  id: 'section-1',
  title: '추천 상품',
  layout: { type: 'grid', columns: 3 },
  responsive: {
    tablet: { columns: 2 },
    mobile: { columns: 2 },
  },
}
```

### ResponsiveRules

| 필드       | 타입                         | 설명                 |
| -------- | -------------------------- | ------------------ |
| `tablet` | `ResponsiveBreakpointRule` | 태블릿 브레이크포인트 설정     |
| `mobile` | `ResponsiveBreakpointRule` | 모바일 브레이크포인트 설정     |
| `notes`  | `string[]`                 | 의도 메모 (렌더링에 영향 없음) |

### ResponsiveBreakpointRule

| 필드               | 타입                                                       | 설명                   |
| ---------------- | -------------------------------------------------------- | -------------------- |
| `columns`        | `1 \| 2 \| 3`                                            | 그리드 열 수              |
| `masonryColumns` | `1 \| 2 \| 3`                                            | masonry 열 수          |
| `itemWidth`      | `number`                                                 | horizontal 카드 너비(px) |
| `layoutMode`     | `'stack' \| 'preserve' \| 'single-column' \| 'carousel'` | 레이아웃 변환 방식           |

***

## 다크 모드

최상위 옵션의 `theme` 값을 `'dark'`로 설정하면 다크 모드가 적용됩니다.

```js
ProductListLayoutWidget.mount('#product-widget', {
  pageTitle: '추천 상품',
  theme: 'dark',
  sections: [ /* ... */ ],
});
```

`'light'`(기본값)과 `'dark'` 두 가지 값을 지원합니다.

### 다크 모드 적용 범위

| 요소                | 변경 내용                               |
| ----------------- | ----------------------------------- |
| 루트 배경             | 딥 네이비 그라디언트 (`#071120` → `#0d1726`) |
| 기본 텍스트            | `#eff6ff` (밝은 블루 화이트)               |
| 서브타이틀·설명          | `rgba(148, 163, 184, 0.75)`         |
| 섹션 배경             | 반투명 다크 + 미세 보더·그림자                  |
| 카드 배경             | `rgba(255, 255, 255, 0.06)` 반투명     |
| CTA 버튼            | 반투명 화이트 배경 + `#e2e8f0` 텍스트          |
| 매거진 섹션            | 딥 퍼플 그라디언트                          |
| 스플릿·스펙 섹션         | 섹션별 전용 다크 그라디언트                     |
| `simple-list` 구분선 | `rgba(255, 255, 255, 0.1)`          |

### `update`로 테마 전환

런타임에 테마를 전환하려면 `update`를 사용합니다.

```js
// 다크 모드로 전환
LayoutWidget.update('#product-widget', { theme: 'dark' });

// 라이트 모드로 복귀
LayoutWidget.update('#product-widget', { theme: 'light' });
```

***

## 로딩 처리

페이지 최초 진입 시 API 호출이 완료될 때까지 위젯은 기본적으로 스피너와 안내 문구를 표시합니다.

```js
LayoutWidget.mount('#product-widget', {
  userKey: 'YOUR_USER_KEY',
  apiKey: 'YOUR_API_KEY',
  sections: [ /* ... */ ],
  // loadingFallback: true, // 기본값 — API 호출 중 로딩 화면 표시
});
```

로딩 화면을 표시하지 않으려면 `loadingFallback: false`로 설정합니다.\
로딩 중에는 아무것도 렌더링되지 않습니다.

```js
LayoutWidget.mount('#product-widget', {
  userKey: 'YOUR_USER_KEY',
  apiKey: 'YOUR_API_KEY',
  sections: [ /* ... */ ],
  loadingFallback: false, // 로딩 중 빈 화면 유지
});
```

***

## 에러 처리

상품 API 호출이 실패하면 위젯은 기본적으로 에러 화면을 렌더링합니다.

#### API 에러 코드

상품 API 요청이 실패하거나 조건에 맞는 상품이 없는 경우, 아래 HTTP 상태 코드로 응답될 수 있습니다.

| 에러 코드                       | 설명                              | 원인                                                        |
| --------------------------- | ------------------------------- | --------------------------------------------------------- |
| 400 (Bad Request)           | 요청 파라미터 또는 입력값 처리 중 오류가 발생했습니다. | <p>- 필수 파라미터 누락<br>- 잘못된 형식의 값 전달<br>- 유효하지 않은 요청 데이터</p> |
| 401 (Unauthorized)          | 유효하지 않은 UserKey입니다.             | <p>- uKey 누락<br>- 유효하지 않거나 만료된 UserKey</p>                |
| 403 (Forbidden)             | 유효하지 않은 ServiceKey입니다.          | <p>- API-KEY 오류<br>- 접근 권한 없음</p>                         |
| 204 (No Content)            | 조회 가능한 상품 리스트가 없습니다.            | <p>- ServiceKey, UserKey는 정상<br>- 조건에 맞는 상품 없음</p>        |
| 500 (Internal Server Error) | 서버 내부 오류가 발생했습니다.               | <p>- 서버 처리 중 예외 발생<br>- 시스템 장애</p>                        |

```js
LayoutWidget.mount('#product-widget', {
  userKey: 'YOUR_USER_KEY',
  apiKey: 'YOUR_API_KEY',
  sections: [ /* ... */ ],
  // errorFallback: true, // 기본값 — 에러 시 안내 화면 표시
});
```

에러 화면을 표시하지 않으려면 `errorFallback: false`로 설정합니다.

```js
LayoutWidget.mount('#product-widget', {
  userKey: 'YOUR_USER_KEY',
  apiKey: 'YOUR_API_KEY',
  sections: [ /* ... */ ],
  errorFallback: false, // 에러 시 빈 화면 유지
});
```

{% hint style="warning" %}
위젯 스크립트 자체(`layout-widget.js`)가 로드되지 않는 경우(CDN 장애 등)는 위젯 내부에서 처리할 수 없습니다. 이 경우 `import()` dynamic import로 직접 핸들링해야 합니다.
{% endhint %}

```js
import('https://ad.mallpie.co.kr/ncms/offerwall/layout-widget.js')
  .then(({ default: LayoutWidget }) => {
    LayoutWidget.mount('#product-widget', { /* ... */ });
  })
  .catch(() => {
    document.getElementById('product-widget').textContent = '위젯을 불러오지 못했습니다.';
  });
```

***

## 상품 링크

상품 카드 클릭 시 `https://{shopId}.mallpie.kr/product/{상품ID}` 로 이동합니다.

`shopId`는 `apiKey`를 통해 위젯이 자동으로 해석하며, 별도로 설정할 필요가 없습니다. API 호출이 실패해 `shopId`를 확인할 수 없는 경우 상품 링크는 비활성화됩니다.

***

## 레이아웃 선택 기준

| 목적      | 권장 레이아웃                            |
| ------- | ---------------------------------- |
| 전체 탐색   | `grid`, `list`                     |
| 공유하기    | `square-grid`                      |
| 빠른 훑기   | `simple-list`, `horizontal`        |
| 큐레이션    | `editorial`, `magazine`, `masonry` |
| 상품 강조   | `hero`, `feature-side`, `split`    |
| 분위기 연출  | `overlay`                          |
| 배너형 캐러셀 | `swipe`                            |

***

## 전체 적용 예시

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>상품 목록</title>
</head>
<body>

  <div id="product-widget"></div>

  <script type="module">
    import LayoutWidget from 'https://ad.mallpie.co.kr/ncms/offerwall/layout-widget.js';

    LayoutWidget.mount('#product-widget', {
      userKey: 'YOUR_USER_KEY',
      apiKey: 'YOUR_API_KEY',
      adid: 'YOUR_ADID', // 선택값
      pageTitle: '이번 주 추천',
      theme: 'light',
      sections: [
        {
          id: 'best',
          title: '베스트 상품',
          layout: { type: 'hero' },
          cardStyle: {
            showBrand: true,
            showOriginalPrice: true,
            showReviewMeta: true,
            showRewardBadge: true,
          },
          responsive: {
            tablet: { layoutMode: 'preserve' },
            mobile: { layoutMode: 'stack' },
          },
        },
        {
          id: 'all',
          title: '전체 상품',
          layout: { type: 'grid', columns: 2 },
          cardStyle: { showBrand: true, showCouponBadge: true },
          responsive: {
            tablet: { columns: 2 },
            mobile: { columns: 2 },
          },
        },
      ],
    });
  </script>

</body>
</html>
```
