# 검색 연동 프론트엔드 설치 가이드

<figure><img src="https://889768860-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F86EGDbqiEQQc91OjPHgn%2Fuploads%2FMNCLWATbLzVyzBMw55cT%2F%E1%84%8C%E1%85%A6%E1%86%AB%E1%84%89%E1%85%A5%E1%84%8B%E1%85%A7%E1%86%AB%E1%84%80%E1%85%A7%E1%86%AF%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5.png?alt=media&#x26;token=0ddf6a2e-399e-4f42-94c7-53119dc00ce0" alt=""><figcaption></figcaption></figure>

프론트엔드에서 genser 검색 엔진을 연동하는 전체 과정을 안내합니다. 복잡한 개발 없이, **스크립트 설치**와 **함수 호출**만으로 AI 검색을 구현할 수 있습니다.

{% stepper %}
{% step %}

## 연동 키 준비하기 (Setup)

#### 서비스 키 및 인스턴스 키 확인

스크립트 연동에 필요한 두 가지 필수 키(Key) 값을 genser 어드민에서 미리 확보해야 합니다.

<table data-full-width="false"><thead><tr><th width="209.5078125">키 이름</th><th width="250.69921875">설명</th><th>용도</th></tr></thead><tbody><tr><td>서비스 키(Service Key)</td><td>서비스 연동을 위한 고유 인증 키</td><td>초기화 스크립트(<code>head</code> 영역) 설정용</td></tr><tr><td>인스턴스 키(Instance Key)</td><td>생성된 검색 인스턴스의 고유 식별자</td><td>검색 API(<code>searchProducts</code>) 및 로그 수집 호출용</td></tr></tbody></table>

{% hint style="info" %}
**서비스 키 확인 경로**

genser 어드민의 설정 메뉴에서 "서비스 키" 확인이 가능합니다.
{% endhint %}

<figure><img src="https://889768860-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F86EGDbqiEQQc91OjPHgn%2Fuploads%2FGhA3PN9r2C74IJf7X3mO%2F%E1%84%89%E1%85%A5%E1%84%87%E1%85%B5%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B5.png?alt=media&#x26;token=ca5f9a87-619a-491f-95ac-fc06bde27fba" alt=""><figcaption><p>어드민 설정 화면에서 서비스 키 위치</p></figcaption></figure>

{% hint style="info" %}
**인스턴스 키 확인 경로**

genser 어드민의 인스턴스 설정 메뉴에서 테이블의 인스턴스 관리 도구 "코드 복사"로 인스턴스 키 복사 가능합니다.
{% endhint %}

<figure><img src="https://889768860-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F86EGDbqiEQQc91OjPHgn%2Fuploads%2FGLAG0UVGUNk7uH7eDBIk%2F%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B5.png?alt=media&#x26;token=9ebd9dd6-4749-4b0b-88b3-d8751bedb58d" alt=""><figcaption><p>어드민 인스턴스 설정에서 인스턴스 키 복사 위치</p></figcaption></figure>

#### 기능 활성화 확인

API 응답에 특정 데이터를 포함받으려면 어드민의 인스턴스 설정에서 해당 기능을 미리 '활성화(ON)' 상태로 변경해야 합니다.
{% endstep %}

{% step %}

## 스크립트 설치하기 (Install)

genser 서비스를 이용하기 위해서는 모든 페이지의 공통 레이아웃에 기본 스크립트를 삽입해야 합니다.&#x20;

{% hint style="warning" %}
공통 레이아웃이 적용되지 않는 페이지가 있다면 해당 페이지에도 개별적으로 스크립트를 삽입해야 합니다.&#x20;
{% endhint %}

* **위치:** HTML 문서의 `<head>` 태그 종료 직전.
* **주의사항:**
  * 공통 레이아웃이 적용되지 않는 페이지가 있다면 해당 페이지에도 개별적으로 삽입해야 합니다.
  * `발급 받은 서비스 키` 부분에 실제 할당받은 키 값을 입력해야 합니다.

```js
<head>
    <!-- ... 기존 헤더 내용 ... -->
    
    <!-- genser 스크립트 설치 코드 시작 -->
    <script type="text/javascript">
        (function (a, i, u, e, o) {
            a[u] = a[u] || function () { (a[u].q = a[u].q || []).push(arguments) };
        })(window, document, "genser");
        
        // 발급 받은 서비스 키를 입력하세요
        genser("serviceKey", "발급 받은 서비스 키"); 
        genser("siteType", "custom");
    </script>
    <script charset="utf-8" src="//static.groobee.io/dist/g2/genser.init.min.js"></script>
    <!-- genser 스크립트 설치 코드 끝 -->
</head>
```

{% endstep %}

{% step %}

## 검색 결과 호출하기 (Search)

기본 스크립트 모든 페이지에 삽입되어 있다는 가정 하에 해당 페이지 내에서 아래와 같은 자바스크립트 함수를 사용할 수 있습니다.&#x20;

아래의 함수에 타입에 맞는 데이터를 넣어 호출하면 상품 검색 결과 데이터를 받을 수 있습니다.&#x20;

### 검색 호출(searchProducts)

```js
genser.call('searchProducts', {
    instanceKey: 'instanceKey', // 필수: 어드민에서 생성한 인스턴스 키
    queryText: '상품 검색어',     // 필수: 사용자 입력 검색어
  })
  .success((res) => {
    console.log(res);           // 검색 결과 데이터 반환 (아래 응답 구조 참고)
  })
  .error((err) => {
    console.error(err);         // 오류 발생 시
  });
```

<table data-full-width="false"><thead><tr><th width="131.3828125">항목</th><th width="79.3046875">타입</th><th width="79.578125">필수</th><th>설명</th></tr></thead><tbody><tr><td>instanceKey</td><td>string</td><td>필수</td><td>genser 어드민에서 생성한 인스턴스 키(고유 식별자)</td></tr><tr><td>queryText</td><td>string</td><td>필수</td><td>사용자가 입력한 상품 검색어</td></tr><tr><td>...</td><td></td><td>선택</td><td>기타 확장 필드 (사용 목적에 따라 추가 가능)</td></tr></tbody></table>

### 응답 데이터 구조(Response)

`success` 콜백을 통해 반환되는 데이터는 `type`에 따라 4가지 형태로 구분됩니다.

#### 1. 상품 정보(SEARCH)

검색된 상품 목록입니다.&#x20;

```js
{
    "requestId": "검색어에 대한 ID",
    "type": "SEARCH",
    "products": [
        {
            "code": "상품 코드",
            "name": "상품 이름",
            ...
        },
        {
            "code": "상품 코드",
            "name": "상품 이름",
            ...
        },
    ],
    "facet": {},
    "nextPageToken": ''
}
```

<table data-full-width="false"><thead><tr><th width="151.2421875">항목</th><th width="79.3046875">타입</th><th width="79.578125">필수</th><th>설명</th></tr></thead><tbody><tr><td>requestId</td><td>string</td><td>필수</td><td>검색어에 대한 고유 요청 ID</td></tr><tr><td>type</td><td>string</td><td>필수</td><td>응답 타입(예: " SEARCH")</td></tr><tr><td>products</td><td>array</td><td>필수</td><td>상품 목록 배열</td></tr><tr><td>products[].code</td><td>string</td><td>필수</td><td>상품 코드</td></tr><tr><td>products[].name</td><td>string</td><td>필수</td><td>상품 이름</td></tr><tr><td>facet</td><td>object</td><td>선택</td><td>-</td></tr><tr><td>nextPageToken</td><td>string</td><td>선택</td><td>-</td></tr></tbody></table>

#### 2. 검색 결과 설명(SUMMARY)

`검색 결과 설명 설정`이 어드민에서 활성화된 경우,  검색 결과에 대한 요약 텍스트를 반환합니다.

```js
{
    "requestId": "검색어에 대한 ID",
    "type": "SUMMARY",
    "summary": "요약 내용 텍스트"
}
```

<table data-full-width="false"><thead><tr><th width="151.2421875">항목</th><th width="79.3046875">타입</th><th width="79.578125">필수</th><th>설명</th></tr></thead><tbody><tr><td>requestId</td><td>string</td><td>필수</td><td>검색어에 대한 고유 요청 ID</td></tr><tr><td>type</td><td>string</td><td>필수</td><td>응답 타입(예: " SUMMARY")</td></tr><tr><td>summary</td><td>string</td><td>필수</td><td>검색어 또는 결과에 대한 요약 내용 텍스트</td></tr></tbody></table>

#### 3. 연관 키워드(KEYWORDS)

`연관 키워드 설정`이 어드민에서 활성화된 경우, 연관된 추천 키워드 리스트를 반환됩니다.

```js
{
    "requestId": "검색어에 대한 ID",
    "type": "KEYWORDS",
    "keywords": ["키워드1", "키워드2"]
}
```

<table data-full-width="false"><thead><tr><th width="151.2421875">항목</th><th width="79.3046875">타입</th><th width="79.578125">필수</th><th>설명</th></tr></thead><tbody><tr><td>requestId</td><td>string</td><td>필수</td><td>검색어에 대한 고유 요청 ID</td></tr><tr><td>type</td><td>string</td><td>필수</td><td>응답 타입(예: " KEYWORDS")</td></tr><tr><td>keywords</td><td>string[]</td><td>필수</td><td>추천 또는 연관 키워드 배열 (문자열 리스트)</td></tr></tbody></table>

#### 4. 제안 질문(QUESTIONS)

`제안 질문 설정`이 어드민에서 활성화된 경우, 사용자가 추가로 궁금해할 만한 질문 리스트를 반환합니다.

```js
{
    "requestId": "검색어에 대한 ID",
    "type": "QUESTIONS",
    "questions": [
        "질문 내용 1",
        "질문 내용 2"
    ]
}
```

<table data-full-width="false"><thead><tr><th width="151.2421875">항목</th><th width="79.3046875">타입</th><th width="79.578125">필수</th><th>설명</th></tr></thead><tbody><tr><td>requestId</td><td>string</td><td>필수</td><td>검색어에 대한 고유 요청 ID</td></tr><tr><td>type</td><td>string</td><td>필수</td><td>응답 타입(예: " QUESTIONS")</td></tr><tr><td>questions</td><td>string[]</td><td>필수</td><td>제안 질문 배열 (문자열 리스트)</td></tr></tbody></table>
{% endstep %}

{% step %}

## 로그 데이터 수집하기 (Analytics)

검색 품질 향상과 통계를 위해 사용자 행동 데이터를 수집합니다.

* **검색(SE):** `searchProducts` 함수 호출 시 자동으로 수집됩니다.
* **노출(DI) & 클릭(CL):** 아래 함수를 사용하여 직접 호출해야 합니다.

### 노출 (DI)

검색 결과 상품이 화면에 노출되었을 때 호출합니다.

```js
genser.call('DI', {
    instance: { key: 'instanceKey' }, // 인스턴스 키
    goods: [
      {
        code: '상품 코드', // searchProducts 결과의 code
        name: '상품 이름', // searchProducts 결과의 name
      },
      ...
    ], // 상품 정보
    requestId: '검색어에 대한 ID', // searchProducts 결과에서 응답 받은 requestId
  })
  .success((res) => {
    console.log(res);   // 정상 callback
  })
  .error((err) => {
    console.error(err); // 오류 callback
  });

```

<table data-full-width="false"><thead><tr><th width="151.2421875">항목</th><th width="79.3046875">타입</th><th width="79.578125">필수</th><th>설명</th></tr></thead><tbody><tr><td>instance</td><td>object</td><td>필수</td><td>genser 어드민에서 생성한 인스턴스 정보</td></tr><tr><td>instance.key</td><td>string</td><td>필수</td><td>genser 어드민에서 생성한 인스턴스 키 (고유 식별자)</td></tr><tr><td>goods</td><td>array</td><td>필수</td><td>사용자에게 추천/선택된 상품 목록</td></tr><tr><td>goods[].code</td><td>string</td><td>필수</td><td>상품 코드 (<code>searchProducts</code> 응답의 상품 코드 사용)</td></tr><tr><td>goods[].name</td><td>string</td><td>필수</td><td>상품 이름 (<code>searchProducts</code> 응답의 상품 이름 사용)</td></tr><tr><td>requestId</td><td>string</td><td>필수</td><td>상품 검색 결과에서 받은 요청 ID (<code>searchProducts</code> 응답의 requestId 사용)</td></tr></tbody></table>

### 클릭 (CL)

사용자가 상품을 클릭했을 때 호출합니다.

```js
genser.call('CL', {
    instance: { key: 'instanceKey' }, // 인스턴스 키
    goods: [
      {
        code: '상품 코드', // searchProducts 결과의 code
        name: '상품 이름', // searchProducts 결과의 name
      },
      ...
    ], // 상품 정보
    requestId: '검색어에 대한 ID', // searchProducts 결과에서 응답 받은 requestId
  })
  .success((res) => {
    console.log(res);   // 정상 callback
  })
  .error((err) => {
    console.error(err); // 오류 callback
  });

```

<table data-full-width="false"><thead><tr><th width="151.2421875">항목</th><th width="79.3046875">타입</th><th width="79.578125">필수</th><th>설명</th></tr></thead><tbody><tr><td>instance</td><td>object</td><td>필수</td><td>genser 어드민에서 생성한 인스턴스 정보</td></tr><tr><td>instance.key</td><td>string</td><td>필수</td><td>genser 어드민에서 생성한 인스턴스 키 (고유 식별자)</td></tr><tr><td>goods</td><td>array</td><td>필수</td><td>사용자에게 추천/선택된 상품 목록</td></tr><tr><td>goods[].code</td><td>string</td><td>필수</td><td>상품 코드 (<code>searchProducts</code> 응답의 상품 코드 사용)</td></tr><tr><td>goods[].name</td><td>string</td><td>필수</td><td>상품 이름 (<code>searchProducts</code> 응답의 상품 이름 사용)</td></tr><tr><td>requestId</td><td>string</td><td>필수</td><td>상품 검색 결과에서 받은 요청 ID (<code>searchProducts</code> 응답의 requestId 사용)</td></tr></tbody></table>
{% endstep %}
{% endstepper %}

<details>

<summary>React 코드 예시</summary>

```js
import React, { useState, useEffect } from 'react';

/**
 * Genser Search React Component
 * 전제조건: public/index.html의 <head>에 Genser SDK 스크립트가 로드되어 있어야 합니다.
 */
const GenserSearch = () => {
  const [query, setQuery] = useState('');
  const [products, setProducts] = useState([]);
  const [requestId, setRequestId] = useState(null);
  
  // 인스턴스 키 (환경변수나 상수로 관리 권장)
  const INSTANCE_KEY = 'YOUR_INSTANCE_KEY';

  // 1. 검색 핸들러
  const handleSearch = (e) => {
    e.preventDefault();
    if (!query.trim()) return;

    if (!window.genser) {
      console.error("Genser SDK가 로드되지 않았습니다.");
      return;
    }

    // searchProducts 호출
    window.genser.call('searchProducts', {
      instanceKey: INSTANCE_KEY,
      queryText: query
    })
    .success((res) => {
      if (res.type === 'SEARCH') {
        // 상태 업데이트 -> 리렌더링 유발
        setProducts(res.products);
        setRequestId(res.requestId);
      }
    })
    .error((err) => {
      console.error('검색 에러:', err);
    });
  };

  // 2. 노출(DI) 로그 전송
  // products나 requestId가 변경되면(즉, 검색 결과가 렌더링되면) 실행
  useEffect(() => {
    if (products.length > 0 && requestId) {
      window.genser.call('DI', {
        instance: { key: INSTANCE_KEY },
        goods: products.map(p => ({ code: p.code, name: p.name })),
        requestId: requestId
      });
      // console.log('DI(노출) 로그 전송 완료');
    }
  }, [products, requestId]);

  // 3. 클릭(CL) 로그 전송 핸들러
  const handleProductClick = (product) => {
    if (!requestId) return;

    window.genser.call('CL', {
      instance: { key: INSTANCE_KEY },
      goods: [{ code: product.code, name: product.name }],
      requestId: requestId
    });
    // console.log('CL(클릭) 로그 전송 완료:', product.name);

    // 실제 상품 페이지로 이동 로직
    // window.location.href = `/product/${product.code}`;
  };

  return (
    <div className="genser-search-container">
      {/* 검색 폼 */}
      <form onSubmit={handleSearch}>
        <input 
          type="text" 
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="검색어를 입력하세요"
        />
        <button type="submit">검색</button>
      </form>

      {/* 검색 결과 리스트 */}
      <div className="product-list">
        {products.map((product) => (
          <div 
            key={product.code} 
            className="product-item"
            onClick={() => handleProductClick(product)}
            style={{ cursor: 'pointer', margin: '10px 0', border: '1px solid #ddd', padding: '10px' }}
          >
            {/* 이미지 처리 (API 응답에 따라 필드명 조정 필요) */}
            {product.imageUrl && <img src={product.imageUrl} alt={product.name} width="100" />}
            <h3>{product.name}</h3>
            <p>{product.code}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

export default GenserSearch;
```

</details>

<details>

<summary>Vue 코드 예시</summary>

```js
<template>
  <div class="genser-search">
    <div class="search-bar">
      <input 
        v-model="query" 
        @keyup.enter="handleSearch" 
        placeholder="상품을 검색하세요" 
      />
      <button @click="handleSearch">검색</button>
    </div>

    <ul v-if="products.length > 0">
      <li 
        v-for="product in products" 
        :key="product.code" 
        @click="handleProductClick(product)"
      >
        {{ product.name }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';

// 전제조건: public/index.html의 <head>에 Genser SDK 스크립트가 로드되어 있어야 합니다. [cite: 64, 239]

// 어드민에서 발급받은 인스턴스 키 [cite: 11]
const INSTANCE_KEY = 'YOUR_INSTANCE_KEY';

const query = ref('');
const products = ref([]);
const requestId = ref(null);

// 1. 검색 핸들러 [cite: 95]
const handleSearch = () => {
  if (!query.value.trim()) return;

  if (!window.genser) {
    console.error("Genser SDK가 로드되지 않았습니다.");
    return;
  }

  // genser 검색 API 호출
  window.genser.call('searchProducts', {
    instanceKey: INSTANCE_KEY, // [cite: 98]
    queryText: query.value     // [cite: 99]
  })
  .success((res) => {
    // 응답 타입이 SEARCH인 경우 데이터 업데이트 [cite: 118]
    if (res.type === 'SEARCH') {
      products.value = res.products;   // [cite: 119]
      requestId.value = res.requestId; // [cite: 117]
    }
  })
  .error((err) => {
    console.error('검색 에러:', err);
  });
};

// 2. 노출(DI) 로그 전송 [cite: 177]
// products 상태가 변경되어 화면에 렌더링된 후 호출
watch([products, requestId], ([newProducts, newRequestId]) => {
  if (newProducts.length > 0 && newRequestId) {
    window.genser.call('DI', {
      instance: { key: INSTANCE_KEY }, // [cite: 180]
      goods: newProducts.map(p => ({ 
        code: p.code, 
        name: p.name 
      })), // [cite: 181]
      requestId: newRequestId // [cite: 186]
    })
    .success((res) => {
       // console.log('DI 로그 전송 성공');
    });
  }
});

// 3. 클릭(CL) 로그 전송 핸들러 [cite: 200]
const handleProductClick = (product) => {
  if (!requestId.value) return;

  window.genser.call('CL', {
    instance: { key: INSTANCE_KEY }, // [cite: 206]
    goods: [{ 
      code: product.code, 
      name: product.name 
    }], // [cite: 207]
    requestId: requestId.value // [cite: 212]
  })
  .success((res) => {
    console.log('CL 로그 전송 성공, 상품 페이지로 이동 로직 수행');
  });
};
</script>
```

</details>

<details>

<summary>HTML/JS 코드 예시</summary>

```js
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Genser Search Example</title>
    </head>
<body>

    <div id="search-container">
        <input type="text" id="searchInput" placeholder="검색어를 입력하세요">
        <button id="searchBtn">검색</button>
    </div>

    <ul id="resultList"></ul>

    <script type="text/javascript">
        // 설정 값
        const INSTANCE_KEY = 'YOUR_INSTANCE_KEY'; // [cite: 11]
        
        // 상태 변수
        let currentRequestId = null;

        // DOM 요소 참조
        const searchInput = document.getElementById('searchInput');
        const searchBtn = document.getElementById('searchBtn');
        const resultList = document.getElementById('resultList');

        // 1. 검색 버튼 클릭 이벤트
        searchBtn.addEventListener('click', function() {
            const query = searchInput.value;
            if (!query.trim()) return;
            
            doSearch(query);
        });

        // 검색 실행 함수 [cite: 95]
        function doSearch(queryText) {
            if (!window.genser) return console.error("SDK 미로드");

            window.genser.call('searchProducts', {
                instanceKey: INSTANCE_KEY, // [cite: 98]
                queryText: queryText       // [cite: 99]
            })
            .success((res) => {
                if (res.type === 'SEARCH') {
                    currentRequestId = res.requestId; // [cite: 117]
                    renderProducts(res.products);
                    
                    // 렌더링 직후 노출(DI) 로그 전송
                    sendImpressionLog(res.products, res.requestId);
                }
            })
            .error((err) => {
                console.error("검색 실패:", err);
            });
        }

        // 화면 렌더링 함수
        function renderProducts(products) {
            resultList.innerHTML = ''; // 기존 목록 초기화

            products.forEach(product => {
                const li = document.createElement('li');
                li.textContent = product.name;
                li.style.cursor = 'pointer';
                
                // 클릭 이벤트 바인딩
                li.addEventListener('click', () => {
                    handleProductClick(product);
                });

                resultList.appendChild(li);
            });
        }

        // 2. 노출(DI) 로그 전송 함수 [cite: 177]
        function sendImpressionLog(products, reqId) {
            window.genser.call('DI', {
                instance: { key: INSTANCE_KEY }, // [cite: 180]
                goods: products.map(p => ({
                    code: p.code,
                    name: p.name
                })), // [cite: 181]
                requestId: reqId // [cite: 186]
            });
        }

        // 3. 클릭(CL) 로그 전송 함수 [cite: 200]
        function handleProductClick(product) {
            if (!currentRequestId) return;

            window.genser.call('CL', {
                instance: { key: INSTANCE_KEY }, // [cite: 206]
                goods: [{
                    code: product.code,
                    name: product.name
                }], // [cite: 207]
                requestId: currentRequestId // [cite: 212]
            })
            .success(() => {
                console.log('클릭 로그 전송 완료: ' + product.name);
                // 이후 상품 상세 페이지 이동 등의 로직 수행
            });
        }
    </script>
</body>
</html>
```

</details>
