---
title: 알림
description: 조치가 필요한 대화에 대한 이메일 요약과 웹훅, 서명 검증 방법을 설명합니다.
---

Hearl은 대화에 조치가 필요해지는 즉시 팀에 알려 줄 수 있습니다. **알림** 페이지에서 설정하세요. 이 설정은 소유자와 관리자만 변경할 수 있습니다.

## 알림이 전송되는 시점 [#when]

Hearl은 15분마다 확인합니다. 다음 조건을 모두 만족하는 대화가 포함됩니다.

- 플랫폼 상태가 **답변 전** 또는 **사용자 후속 답글**입니다.
- 진행 상태가 아직 **시작 전**입니다.
- Hearl AI가 제품에 관한 글이라고 판단했으며, 불확실한 답이 없습니다.
- 우선순위가 설정한 최소 기준 이상입니다. 전체, 보통 이상, 높음만 중에서 선택합니다.

대화는 상태별로 한 번씩만 알립니다. **답변 전** 상태로 알림을 받은 뒤 사용자가 다시 글을 남기면 **사용자 후속 답글** 상태로 다시 알립니다.

## 이메일 [#email]

**이메일 보내기**를 켜고 수신자를 추가하세요. 목록을 비워 두면 워크스페이스의 모든 멤버에게 이메일을 보냅니다. 공용 지원 메일함 같은 주소도 사용할 수 있으며, 수신자는 최대 20명입니다.

확인할 때마다 요약 메일 한 통을 보내며, 최대 10개의 대화를 나열하고 나머지는 받은함으로 연결하는 링크를 넣습니다. **언어**에서 영어 또는 중국어를 선택하세요.

## 웹훅 [#webhook]

**엔드포인트**를 입력하세요. 공개된 `https` URL이어야 합니다. Hearl은 주소를 보고 일반적인 채팅 봇을 인식해 해당 봇이 기대하는 메시지 형식으로 보냅니다.

| 주소 | 전송 형식 |
| --- | --- |
| `hooks.slack.com` | Slack 메시지 |
| `feishu.cn`, `larksuite.com` | Feishu 텍스트 메시지 |
| `qyapi.weixin.qq.com` | WeCom Markdown 메시지 |
| `oapi.dingtalk.com` | DingTalk Markdown 메시지 |
| `discord.com`, `discordapp.com` | Discord 메시지 |
| 그 외 모든 URL | 일반 JSON (아래 참고) |

페이지에 감지된 종류가 표시됩니다. **테스트 보내기**는 샘플을 전송하며, 페이지에서 마지막 전송 결과를 확인할 수 있습니다. 요청이 8초보다 오래 걸리면 타임아웃으로 처리합니다.

### 일반 JSON [#json]

```json
{
  "event": "conversations.needs_attention",
  "workspace": { "id": "org_123", "name": "Acme" },
  "text": "*2 conversations need attention* · Acme\n• [reddit] Sync stopped working after 2.3 — No reply yet <https://…>",
  "conversations": [
    {
      "id": "c_8f2a",
      "platform": "reddit",
      "product": "Acme",
      "title": "Sync stopped working after 2.3",
      "author": "some_user",
      "url": "https://www.reddit.com/r/…",
      "status": "待回复",
      "priority": "高",
      "category": "故障",
      "inbox_url": "https://hearl.org/app?c=c_8f2a"
    }
  ],
  "sent_at": "2026-11-08T09:15:00.000Z"
}
```

| 필드 | 설명 |
| --- | --- |
| `event` | `conversations.needs_attention`, 또는 **테스트 보내기**의 경우 `test`. |
| `workspace` | 워크스페이스의 `id`와 `name`. |
| `text` | 바로 게시할 수 있는 요약. Slack은 이 필드만 읽습니다. |
| `conversations` | 이번 전송에 포함된 모든 대화. |
| `sent_at` | 전송 시각(ISO 8601). |

`platform`은 `x`, `reddit`, `hn` 중 하나입니다. `status`, `priority`, `category`는 중국어로 된 고정 코드이므로, 워크스페이스 언어와 관계없이 코드에서 이 값으로 일치 여부를 확인할 수 있습니다.

| 필드 | 코드 | 의미 |
| --- | --- | --- |
| `status` | `待回复` | 답변 전 |
| `status` | `用户有新回复` | 사용자 후속 답글 |
| `priority` | `高` / `中` / `低` | 높음 / 보통 / 낮음 |
| `category` | `故障` | 버그 |
| `category` | `求助` | 질문 |
| `category` | `功能建议` | 기능 요청 |
| `category` | `价格` | 가격 |
| `category` | `负面反馈` | 비판 |
| `category` | `正面反馈` | 칭찬 |
| `category` | `购买咨询` | 구매 문의 |
| `category` | `其他` | 기타 |

### 서명 검증 [#signature]

웹훅 엔드포인트를 저장하면 Hearl이 **서명 시크릿**을 만듭니다. 알림 페이지에서 시크릿을 표시하거나 교체할 수 있습니다. 요청에는 다음 헤더가 포함됩니다.

```txt
X-Hearloop-Signature: sha256=<hex>
```

`<hex>`는 서명 시크릿을 키로 사용해 요청 본문 원본에 HMAC-SHA256을 적용한 값이며, 소문자 16진수로 표기합니다. JSON을 파싱하기 전에 받은 바이트 그대로 계산하고, 상수 시간 비교로 확인하세요.

```js Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.HEARL_WEBHOOK_SECRET;

app.post("/hearl", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + createHmac("sha256", secret).update(req.body).digest("hex");
  const received = req.get("x-hearloop-signature") ?? "";
  const ok =
    received.length === expected.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const payload = JSON.parse(req.body.toString("utf8"));
  console.log(payload.event, payload.conversations.length);
  res.status(200).end();
});

app.listen(3000);
```

```python Python
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["HEARL_WEBHOOK_SECRET"].encode()


@app.post("/hearl")
def hearl():
    body = request.get_data()
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    received = request.headers.get("X-Hearloop-Signature", "")
    if not hmac.compare_digest(received, expected):
        abort(401)

    payload = json.loads(body)
    print(payload["event"], len(payload["conversations"]))
    return "", 200
```

푸시를 수신했음을 알리려면 아무 `2xx` 상태나 반환하세요.
