> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 예약 확인 및 방치된 예약 복구

> 실시간 예약 상태를 기반으로 예약 확인 또는 복구 이메일을 전송하기 위해 커스텀 이벤트, Journeys 및 Data Feeds를 사용합니다.

## 개요

이 튜토리얼에서는 일반적인 예약 워크플로우를 설정합니다:

* 사용자가 예약을 완료한 후 예약 확인 이메일을 전송합니다.
* 사용자가 예약을 시작했지만 시간 내에 완료하지 않으면 복구 이메일을 전송합니다.

완료되면 다음을 갖게 됩니다:

* 두 개의 커스텀 이벤트 (`booking_started`, `booking_complete`)
* 완료 대 방치에 따라 분기하는 하나의 Journey
* 확인 세부 정보를 위한 예약 Data Feed
* 복구 인센티브를 위한 선택적 쿠폰 Data Feed

이 가이드는 OneSignal 구성에 중점을 둡니다. 예약 시스템과 백엔드는 모든 언어나 프레임워크로 구현할 수 있습니다.

### 설정 플로우

1. 앱이 `booking_started` [커스텀 이벤트](./custom-events)를 추적합니다.
2. 이것이 사용자를 Journey에 진입시킵니다.
3. Journey는 `booking_complete` 이벤트를 기다리고, 시간 내에 수신되지 않으면 후속 리마인더를 전송합니다.
4. 예약이 완료되면, OneSignal은 전송 시 예약 Data Feed를 호출하고 최신 예약 세부 정보가 포함된 확인 이메일을 전송합니다.
5. 예약이 대기 시간 내에 완료되지 않으면, Journey는 만료 경로를 따라 복구 이메일을 전송합니다.

***

## 설정

### 전제 조건

시작하기 전에 다음이 있는지 확인하세요:

* [**이메일**](./email-setup) 채널이 활성화된 OneSignal 앱
* 예약 및/또는 쿠폰 데이터를 JSON으로 반환할 수 있는 백엔드 엔드포인트
* 앱, 백엔드 및 OneSignal [외부 ID](./users#external-id) 간에 공유되는 안정적인 사용자 식별자
* [커스텀 이벤트](./custom-events)에 대한 액세스

### 1. 예약 이벤트 추적

다음 커스텀 이벤트를 추적하세요. 이들은 앱에서 (SDK 사용) 또는 백엔드에서 (REST API 사용) 올 수 있습니다.

**이벤트 이름:**

* `booking_started` — 사용자가 예약 플로우를 시작할 때
* `booking_complete` — 예약이 성공적으로 완료될 때

<Tabs>
  <Tab title="앱 - 모바일 및 웹">
    [모바일 SDK](./mobile-sdk-reference) 및/또는 [웹 SDK](./web-sdk-reference)의 `trackEvent()` 메서드를 사용하여 앱/웹사이트에서 직접 커스텀 이벤트를 전송하세요.

    ```js 예시 theme={null}
    OneSignal.User.trackEvent("booking_started");
    OneSignal.User.trackEvent("booking_complete");
    ```
  </Tab>

  <Tab title="서버 사이드">
    백엔드에서 이벤트를 추적하는 경우, [커스텀 이벤트 생성 API](/reference/create-custom-events)를 사용하여 OneSignal로 이벤트를 전송하세요.

    ```bash theme={null}
    curl --request POST \
      --url https://api.onesignal.com/apps/{app_id}/custom_events \
      --header 'Authorization: Key YOUR_APP_API_KEY' \
      --header 'Content-Type: application/json' \
      --data '{
        "events": [
          {
            "name": "booking_started",
            "external_id": "user123",
            "properties": {}
          }
        ]
      }'
    ```
  </Tab>
</Tabs>

<Note>
  이벤트 추적 시와 백엔드에서 데이터를 반환할 때 동일한 사용자 ID를 사용하세요. 일치하지 않는 ID는 개인화가 누락되는 가장 일반적인 원인입니다.
</Note>

### 2. Data Feed 별칭 생성

OneSignal에서 **설정 > Data Feeds**로 이동하여 다음 별칭을 생성하세요.

**예약 Data Feed:**
이 피드를 사용하여 전송 시 최신 예약 세부 정보를 가져옵니다.

* **별칭:** `booking_data`
* **메서드:** GET
* **URL:**

```liquid 엔드포인트 예시 theme={null}
https://your-domain.com/datafeed/booking?user_id={{subscription.external_id}}
```

응답 예시:

```json JSON theme={null}
{
  "first_name": "Sam",
  "last_booking": {
    "service_type": "상담",
    "booking_date": "2026년 1월 22일",
    "booking_time": "오후 2:00",
    "price": 45
  }
}
```

**쿠폰 Data Feed (선택사항):**

복구 이메일에 쿠폰 코드를 포함하려면 이 선택적 피드를 사용하세요.

* **별칭:** `coupon`
* **메서드:** GET
* **URL:**

```liquid 엔드포인트 예시 theme={null}
https://your-domain.com/datafeed/coupon?user_id={{subscription.external_id}}
```

응답 예시:

```json JSON theme={null}
{
  "first_name": "Sam",
  "code": "PROMO8F3K2",
  "discount_text": "10%",
  "expires_in_hours": 2,
  "deep_link": "https://your-domain.com/checkout?coupon=PROMO8F3K2"
}
```

<Warning>
  Data Feed 엔드포인트를 보호하세요. 프로덕션에서는 요청 헤더에 API 키(예: `x-api-key`)를 전송하고 URL에 비밀을 포함하는 대신 **설정 > Data Feeds**에서 해당 헤더를 구성하세요.
</Warning>

### 3. 이메일 템플릿 생성

#### 예약 확인 이메일:

**제목:**

```text theme={null}
예약 세부 정보
```

**본문:**

```liquid theme={null}
{{ data_feed.booking_data.first_name | default: "고객님" }}, 안녕하세요

예약해 주셔서 감사합니다! 예약 세부 정보는 다음과 같습니다:

서비스: {{ data_feed.booking_data.last_booking.service_type }}
날짜: {{ data_feed.booking_data.last_booking.booking_date }}
시간: {{ data_feed.booking_data.last_booking.booking_time }}
가격: {{ data_feed.booking_data.last_booking.price }}원

만나 뵙기를 기대하겠습니다!
```

#### 예약 복구 이메일

**제목:**

```text theme={null}
예약을 완료하고 할인 받으세요
```

**본문:**

<Tabs>
  <Tab title="쿠폰 Data Feed 사용">
    ```liquid theme={null}
    {{ data_feed.coupon.first_name | default: "고객님" }}, 안녕하세요

    앞으로 {{ data_feed.coupon.expires_in_hours }}시간 내에 예약을 완료하시면 
    이 코드로 {{ data_feed.coupon.discount_text }} 할인을 받으실 수 있습니다:

    {{ data_feed.coupon.code }}

    여기서 사용하세요:
    {{ data_feed.coupon.deep_link }}
    ```
  </Tab>

  <Tab title="쿠폰 Data Feed 미사용">
    ```text theme={null}
    안녕하세요, 

    아직 예약을 완료하지 않으셨습니다! 

    지금 완료하여 다음 예약에서 할인을 받으세요.

    이 링크를 사용하여 예약을 완료하세요:
    [여기에 딥링크 삽입]
    ```
  </Tab>
</Tabs>

<Note>
  Data Feed 필드가 누락된 경우 빈 콘텐츠를 방지하기 위해 Liquid에 항상 `default` 필터를 포함하세요.
</Note>

### 4. Journey 구축

1. OneSignal에서 **메시지 > Journeys > Journey 생성**으로 이동

2. **입장 트리거**를 다음으로 설정:
   * **커스텀 이벤트:** `booking_started`

3. **다음까지 대기** 단계 추가:
   * **조건:** 커스텀 이벤트 발생
   * **이벤트 이름:** `booking_complete`
   * **최대 대기 시간:** 10분
   * 만료 경로 활성화

4. 분기 구성:
   * **완료됨:** 예약 확인 이메일 전송
     * **Data Feed:** `booking_data`
   * **만료됨:** 복구 이메일 전송
     * **Data Feed:** `coupon`

<Info>
  만료 분기를 통해 앱에서 추가 로직 없이 방치를 처리할 수 있습니다. 참조:

  * [Journey 설정](./journeys-settings) - 커스텀 이벤트 입장 및 퇴장 규칙의 세부 정보
  * [Journeys 작업](./journeys-actions) - 다음까지 대기 단계 및 만료 분기의 세부 정보
</Info>

### 5. 테스트 및 확인

#### 이벤트 확인

앱이나 백엔드에서 커스텀 이벤트를 트리거하고 확인하세요.

OneSignal에서 **분석 > 커스텀 이벤트**로 이동하여 다음이 표시되는지 확인:

* 외부 ID에 대해 `booking_started` 이벤트가 나타남
* 외부 ID에 대해 `booking_complete` 이벤트가 나타남

#### Data Feeds 확인

알려진 사용자 ID를 사용하여 Data Feed 엔드포인트를 수동으로 호출하고 확인:

* 200 응답이 반환됨
* 모든 예상 필드가 존재함

#### 이메일 확인

Journey 편집기에서 테스트 메시지를 전송하고 확인:

* 예약 이메일에 실제 예약 세부 정보가 포함됨
* 복구 이메일에 유효한 쿠폰이 포함됨
* Liquid 변수가 비어있게 렌더링되지 않음

<Check>
  개인화가 누락된 경우, Data Feed 요청의 사용자 ID가 Journey를 트리거한 사용자와 일치하는지 확인하세요.
</Check>

## 예시: Data Feed 구현

<Accordion title="Node.js Data Feed 엔드포인트 예시">
  이 예시는 예약 확인 및 복구 Data Feed를 위한 최소 Express 구현을 보여줍니다. JSON 응답 형태가 이메일 템플릿과 일치하는 한 백엔드 언어, 프레임워크 및 데이터 소스는 다를 수 있습니다.

  ### 예약 Data Feed 예시

  ```js theme={null}
  import express from "express";

  const app = express();

  function dataFeedAuth(req, res, next) {
    if (req.headers["x-api-key"] !== process.env.DATAFEED_API_KEY) {
      return res.status(401).json({ error: "Unauthorized" });
    }
    next();
  }

  app.get("/datafeed/booking", dataFeedAuth, async (req, res) => {
    const { user_id } = req.query;

    if (!user_id) {
      return res.status(400).json({ error: "Missing user_id" });
    }

    const booking = await getLatestBookingForUser(user_id);

    if (!booking) {
      return res.status(404).json({ error: "No booking found" });
    }

    res.json({
      first_name: booking.first_name,
      last_booking: {
        service_type: booking.service_type,
        booking_date: booking.booking_date,
        booking_time: booking.booking_time,
        price: booking.price
      }
    });
  });
  ```

  ### 쿠폰 Data Feed 예시

  ```js theme={null}
  app.get("/datafeed/coupon", dataFeedAuth, async (req, res) => {
    const { user_id } = req.query;

    if (!user_id) {
      return res.status(400).json({ error: "Missing user_id" });
    }

    const coupon = await generateCouponForUser(user_id);

    res.json({
      first_name: coupon.first_name,
      code: coupon.code,
      discount_text: coupon.discount_text,
      expires_in_hours: coupon.expires_in_hours,
      deep_link: coupon.deep_link
    });
  });
  ```

  ### 구현 가이드라인

  * 응답을 빠르게 유지 (Data Feeds는 전송 시 호출됨)
  * 항상 예측 가능한 JSON 구조 반환
  * 데이터가 존재하지 않을 때 404 사용
  * 요청 헤더를 통해 전송된 API 키로 엔드포인트 보안
</Accordion>

## 일반적인 문제

### 이메일에 빈 값 표시

* Data Feed가 404 반환
* JSON 응답에서 필드 이름 변경
* 사용자 식별 불일치

### Journey가 분기하지 않음

* `booking_complete` 이벤트가 추적되지 않음
* 이벤트 이름 불일치 (대소문자 구분)
* 대기 시간 외부에서 이벤트 발생

### Data Feed가 401 또는 403 반환

* API 키 누락 또는 무효
* Data Feed 설정에서 헤더가 구성되지 않음

## 다음 단계

* 더 고급 Journey 조건을 위한 이벤트 속성(서비스 유형, 가격) 추가
* 푸시 또는 SMS 리마인더와 같은 추가 복구 단계 추가
* 반복되는 복구 메시지를 방지하기 위해 Journey 종료 규칙 사용

***
