> For the complete documentation index, see [llms.txt](https://help.genser.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.genser.ai/developers/script-guide/genser-discovery-mini.md).

# 젠서 디스커버리 미니

## **1. 개요**

<figure><img src="/files/AYEntFRpb0tepgl1RPG9" alt=""><figcaption></figcaption></figure>

* 젠서 디스커버리 미니는 고객사 사이트에 코드 한 줄만 넣으면 자동으로 나타나는 연동 방식입니다.
* 젠서 디스커버리 미니 안에서 사용자가 상품을 클릭해 상세 페이지로 이동하려 할 때, 그 동작을 고객사가 원하는 방식(자체 페이지 이동 로직 등)으로 직접 연결할 수 있도록 "이벤트가 발생하면 자동으로 실행되는 함수(콜백)"를 제공합니다.

***

## **2. 연동 방식**

### 2-1. Quick Start

* 아래 코드 조각(스니펫)을 페이지에 붙여 넣으면, 이 코드가 실제 위젯 프로그램(메인 스크립트)을 자동으로 불러와(로더) 초기화까지 진행합니다.

```html
<script>
  (function (w, d, s, l) {
    w[l] = w[l] || { q: [] };
    if (!w[l].init) {
      w[l].init = function (c) {
        w[l].q.push({ c: c });
      };
    }
    var f = d.getElementsByTagName(s)[0];
    var j = d.createElement(s);
    j.async = true;
    j.setAttribute('data-vendor-id', 'YOUR_VENDOR_ID');
    j.src = 'https://cdn.genser.ai/dist/genser-discovery-loader-kr.min.js';
    f.parentNode.insertBefore(j, f);
  })(window, document, 'script', 'genserDiscovery');

  window.genserDiscovery.init({
    vendorId: 'YOUR_VENDOR_ID',
    environment: 'web',
    onUrlOpen: function (payload, context) {
      var url = payload.pcUrl || payload.mobileUrl || payload.fallbackUrl;
      if (!url) return { ok: false, reason: 'url_not_found' };
      window.location.href = url;
      return { ok: true, url: url };
    },
  });
</script>
```

### 2-2. init(config)

* 역할: 이 함수를 호출해야 위젯이 실제로 화면에 나타나고 동작을 시작합니다.
* 함수명: `window.genserDiscovery.init`
* 반환: 실행 결과를 비동기로 알려주는 값(`Promise`)
* 파라미터

| 옵션          | 타입                 | 필수 | 설명                            |
| ----------- | ------------------ | -- | ----------------------------- |
| vendorId    | string             | 필수 | 고객사 식별 ID                     |
| onUrlOpen   | function           | 권장 | 상품 상세/외부 이동 이벤트 처리 콜백 (3장 참고) |
| environment | 'web' \| 'webview' | -  | 실행 환경. 미지정 시 자동 감지            |
| onReady     | function           | -  | UI 렌더링 완료 후 1회 호출             |
| onError     | function           | -  | 초기화/로드 실패 시 에러 객체와 함께 호출      |

### 2-3. 콜백 이름 (alias)

이동 처리 콜백은 아래 이름 중 하나로 지정할 수 있으며, 여러 개를 넣으면 위에서부터 우선 적용됩니다.

```
onOpen > openCallback > onUrlOpen > onProductOpenDetail > onProductDetail > productDetailCallback
```

`onUrlOpen`을 표준으로 사용하는 것을 권장합니다.

### 2-4. environment 자동 감지

`environment`를 지정하지 않으면 SDK가 실행 환경을 자동 감지합니다. 네이티브 WebView(브릿지 존재) 환경이면 `webview`, 그 외에는 `web`으로 동작합니다. 일반 웹 연동 시에는 `environment: 'web'` 명시를 권장합니다.

### 2-5. 위젯 유형 (기본 / 커스텀)

위젯 유형은 어드민에서 설정하며, 두 가지 방식이 있습니다.

* **기본**: 별도 스크립트 작업 없이, SDK가 제공하는 오버레이 트리거를 그대로 사용합니다.
* **커스텀**: SDK가 제공하는 기본 위젯 요소 대신 고객사가 직접 만든 요소(버튼 등)를 트리거로 사용합니다. 이 경우 해당 요소의 클릭 이벤트에 `open()`을, 닫는 동작이 필요한 지점에는 `close()`를 직접 연결해줘야 합니다.

```js
document.querySelector('#my-custom-trigger').addEventListener('click', function () {
  window.genserDiscovery.open();
});

document.querySelector('#my-custom-close-button').addEventListener('click', function () {
  window.genserDiscovery.close();
});
```

{% hint style="info" %}
위젯 유형을 커스텀으로 설정한 경우, `open()`/`close()`를 연결하지 않으면 오버레이가 열리거나 닫히지 않습니다. `open()`/`close()`의 상세 스펙은 [**\[5. API 레퍼런스\]**](#id-5.-api)를 참고해 주세요.
{% endhint %}

***

## **3. 이벤트 처리 방식**

사용자가 위젯에서 상품을 클릭했을 때, 그 클릭을 우리 사이트가 알아채고 반응하도록 만드는 방법은 두 가지입니다: **콜백 방식**(이벤트가 발생하면 자동 실행되는 함수를 `init()`에 미리 등록하는 `onUrlOpen`)과 **구독 방식**(원하는 이벤트만 따로 등록해서 듣는 `on`/`off`).

{% hint style="info" %}
`product.openDetail` 이벤트가 팝업(모달)을 거쳐 발생할지, 클릭 즉시 발생할지는 어드민의 [**\[부가 옵션 > 상품 카드 > PDP 오픈 방식\]**](https://help.genser.ai/~/changes/356/design/additional-options#id-2-2.-pdp) 설정에 따라 달라집니다.
{% endhint %}

| 이벤트명               | 설명              |
| ------------------ | --------------- |
| product.openDetail | 상품 상세 페이지 이동 요청 |

### 3-1. onUrlOpen (콜백 방식)

```
onUrlOpen(payload, context)
```

{% hint style="warning" %}
**콜백 인자는 payload, context 2개입니다.**\
이벤트 타입이 필요하면 `context.message.type`으로 확인합니다.
{% endhint %}

| 인자      | 설명                                                                                   |
| ------- | ------------------------------------------------------------------------------------ |
| payload | 클릭된 상품의 정보와 이동할 URL이 담긴 데이터                                                          |
| context | 이 이벤트에 대해 응답(회신)할 수 있는 통로가 되는 객체 (원본 메시지는 `context.message`, 응답 함수는 `context.reply`) |

**payload 필드**

| 필드          | 타입             | 설명                          |
| ----------- | -------------- | --------------------------- |
| productSku  | string         | 상품 SKU                      |
| pcUrl       | string         | PC 이동 URL                   |
| mobileUrl   | string \| null | Mobile 이동 URL               |
| fallbackUrl | string         | 기본 fallback URL             |
| source      | string         | 이벤트 발생 위치 (예: product-card) |
| utmParams   | object         | (선택) URL 쿼리로 병합할 UTM 파라미터   |

**예시**

```json
{
  "productSku": "3370616",
  "pcUrl": "https://shop.example.com/product/3370616",
  "mobileUrl": null,
  "fallbackUrl": "https://shop.example.com/product/3370616",
  "source": "product-card"
}
```

**URL 우선순위**

이동 URL은 아래 순서로 선택하는 것을 권장합니다. (SDK 기본 처리도 동일한 순서를 사용합니다.)

| 우선순위 | 필드                  |
| ---- | ------------------- |
| 1    | payload.pcUrl       |
| 2    | payload.mobileUrl   |
| 3    | payload.fallbackUrl |

**권장 예시**

```js
function handleUrlOpen(payload, context) {
  var url = payload.pcUrl || payload.mobileUrl || payload.fallbackUrl;
  if (!url) {
    return { ok: false, reason: 'url_not_found' };
  }
  window.location.href = url;
  return { ok: true, url: url };
}

window.genserDiscovery.init({
  vendorId: 'YOUR_VENDOR_ID',
  environment: 'web',
  onUrlOpen: handleUrlOpen,
});
```

### 3-2. on / off (구독 방식)

`init()`에 콜백을 등록하는 대신, 원하는 이벤트가 발생할 때마다 실행할 동작을 별도로 미리 등록(구독)해둘 수도 있습니다.

```js
window.genserDiscovery.on('product.openDetail', function (payload, context) {
  var url = payload.pcUrl || payload.mobileUrl || payload.fallbackUrl;
  if (!url) {
    context.reply('product.openDetail.result', { ok: false, reason: 'url_not_found' });
    return;
  }
  window.location.href = url;
  context.reply('product.openDetail.result', { ok: true, url: url });
});
```

{% hint style="warning" %}
**on()은 반환값이 자동 회신되지 않습니다.**

`on()`으로 구독한 핸들러는 반환값이 자동으로 회신되지 않으므로, 반드시 `context.reply('<이벤트명>.result', { … })`로 직접 결과를 회신해야 합니다. (반면 `init`의 `onUrlOpen`은 반환값이 자동 회신됩니다.([**3-1 참고**](#id-3-1.-onurlopen))
{% endhint %}

**구독 해제**

```js
window.genserDiscovery.off('product.openDetail', handler); // 특정 핸들러만 해제
window.genserDiscovery.off('product.openDetail');          // 해당 타입 전체 해제
```

***

## **4. 환경별 주의 사항**

### 4-1. SPA

SPA(페이지를 이동해도 새로고침 없이 화면 일부만 바뀌는 방식)에서는 앱 최초 진입 시 1회만 스니펫을 로드하고 `init()`을 호출합니다. 이후 라우팅(페이지 이동) 시에는 `init()`을 반복 호출하지 마세요.

### 4-2. JSP

JSP(페이지를 이동할 때마다 화면이 통째로 새로고침되는 방식)는 페이지 이동 시마다 전체 페이지가 새로고침되므로, 페이지 템플릿에 스니펫과 `init()`을 함께 넣습니다.

```html
<script>
  window.genserDiscovery.init({
    vendorId: '<%= vendorId %>',
    environment: 'web',
    onUrlOpen: handleUrlOpen,
  });
</script>
```

***

## **5. API 레퍼런스**

`window.genserDiscovery`에서 제공하는 주요 메서드입니다.

<table data-search="false"><thead><tr><th>메서드</th><th>반환</th><th>설명</th></tr></thead><tbody><tr><td>init(config)</td><td>Promise</td><td>초기화. 옵션은 2장 참고</td></tr><tr><td>on(type, handler)</td><td>-</td><td>이벤트 구독 (3-2 참고)</td></tr><tr><td>off(type, handler?)</td><td>-</td><td>이벤트 구독 해제</td></tr><tr><td>open()</td><td>-</td><td>미니 오버레이 열기. active 상태에서만 동작하며, 열려있는 상태에서 재호출 시 동작 없음</td></tr><tr><td>close()</td><td>-</td><td>미니 오버레이 닫기. 닫혀있는 상태에서 재호출 시 동작 없음</td></tr><tr><td>toggle()</td><td>-</td><td>Discovery 오버레이 열기/닫기 토글</td></tr><tr><td>changeLanguage(lang)</td><td>boolean</td><td>iframe 재로드 없이 앱 언어 변경. 지원 lang 값(ko, en, ja)</td></tr></tbody></table>

`toggle`, `changeLanguage` 등은 메인 스크립트 로드 및 렌더링 완료 이후에 동작합니다.\
`open()`/`close()`는 파라미터와 반환값이 없으며, 위젯 유형(기본/커스텀)과 무관하게 모두 사용할 수 있습니다.

***

## **6. 작성 예시**

코드 작성 예시를 보여줍니다.

```html
<script>
  (function (w, d, s, l) {
    w[l] = w[l] || { q: [] };
    if (!w[l].init) {
      w[l].init = function (c) {
        w[l].q.push({ c: c });
      };
    }
    var f = d.getElementsByTagName(s)[0];
    var j = d.createElement(s);
    j.async = true;
    j.setAttribute('data-vendor-id', 'YOUR_VENDOR_ID');
    j.src = 'https://cdn.genser.ai/dist/genser-discovery-loader-kr.min.js';
    f.parentNode.insertBefore(j, f);
  })(window, document, 'script', 'genserDiscovery');

  function handleUrlOpen(payload, context) {
    var url = payload.pcUrl || payload.mobileUrl || payload.fallbackUrl;
    if (!url) {
      return { ok: false, reason: 'url_not_found' };
    }
    window.location.href = url;
    return { ok: true, url: url };
  }

  window.genserDiscovery.init({
    vendorId: 'YOUR_VENDOR_ID',
    environment: 'web',
    onUrlOpen: handleUrlOpen,
    onReady: function () {
      console.log('Genser Discovery Mini ready');
    },
    onError: function (err) {
      console.error('Genser Discovery Mini init failed', err);
    },
  });
</script>
```

***

## **7. 유의 사항**

1. 로더 파일명은 `genser-discovery-loader-kr.min.js`를 사용해 주세요.
2. `vendorId`는 고객사 식별 값으로, 임의 변경 시 데이터 집계에 영향을 줄 수 있습니다.
3. SPA 환경에서는 `init()` 중복 호출을 피해 주세요.
4. `on()` 구독 방식을 사용할 경우 반드시 `context.reply(...)`로 결과를 회신해 주세요.

   회신하지 않으면 응답 대기 상태로 남을 수 있습니다.
