# 결제 완료 알림

> 내 서버에서 결제가 끝나면 서버 REST API(v2)로 채팅방에 알리고, 페이지 스크립트가 받아 채팅창에 표시하는 예제예요.

내 서버에서 결제가 끝나는 순간 채팅방에 있는 모두에게 "구매 완료" 알림을 띄워요. 서버는 [서버 REST API](/doc/3.6)로 `payment` 채널에 데이터를 보내고, 페이지 스크립트는 서버가 보낸 것인지 확인한 뒤 채팅창에 표시해요.

## 흐름 {#flow}

1. 결제 대행사가 내 서버에 결제 완료를 알려요(내 서버의 기존 처리).
2. 내 서버가 `POST https://api.uchat.io/v2/rooms/{id}/events`로 `payment` 채널에 보내요.
3. 지금 채팅방에 접속한 모든 사람의 페이지에서 `payment` 채널로 데이터가 도착해요.
4. 페이지 스크립트가 보낸 사람의 권한이 `4`(서버)인지 확인하고 `room.print`로 표시해요.

## 서버: Node.js {#server-node}

Node.js 18 이상의 내장 `fetch`를 써요. API 키와 API 채팅방 아이디(`room_…`)는 관리 페이지 **API** 메뉴에서 발급·확인하고, 키는 서버 환경 변수에만 둬요. 키 권한에는 **이벤트**가 있어야 해요.

```js
// notify-payment.mjs
const ROOM = 'room_xxxxxxxxxxxxxxxx'; // API 채팅방 아이디

export async function notifyPayment(productName) {
	const res = await fetch(`https://api.uchat.io/v2/rooms/${ROOM}/events`, {
		method: 'POST',
		headers: {
			Authorization: `Bearer ${process.env.UCHAT_API_KEY}`,
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({ plugin: 'payment', data: ['paid', productName] }),
	});
	const result = await res.json();
	if (!res.ok) {
		// 429 chat_rate_limited: 무료 채팅방 전송 한도 초과 등
		console.warn('uchat notify failed', res.status, result.error);
	}
	return result;
}

// 결제 완료 처리 안에서
// await notifyPayment('프리미엄 이용권');
```

## 서버: PHP {#server-php}

```php
<?php
function notify_payment($product_name) {
	$ch = curl_init('https://api.uchat.io/v2/rooms/room_xxxxxxxxxxxxxxxx/events');
	curl_setopt_array($ch, [
		CURLOPT_POST => true,
		CURLOPT_RETURNTRANSFER => true,
		CURLOPT_HTTPHEADER => [
			'Authorization: Bearer ' . getenv('UCHAT_API_KEY'),
			'Content-Type: application/json',
		],
		CURLOPT_POSTFIELDS => json_encode(['plugin' => 'payment', 'data' => ['paid', $product_name]], JSON_UNESCAPED_UNICODE),
		CURLOPT_TIMEOUT => 5,
	]);
	$body = curl_exec($ch);
	$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
	curl_close($ch);
	$result = json_decode($body, true);
	if ($status >= 400) {
		error_log('uchat notify failed: ' . $status . ' ' . ($result['error']['code'] ?? 'no response'));
	}
	return $result;
}

// 결제 완료 처리 안에서
// notify_payment('프리미엄 이용권');
```

성공하면 HTTP 202와 `{"data":{"sent":true}}`를 돌려줘요. 채팅방에 아무도 없어도 성공이에요(받을 사람이 없을 뿐이에요). 실패하면 HTTP 상태와 `{"error":{"code":…,"message":…}}`가 와요. 전체 오류 코드는 [서버 REST API](/doc/3.6#errors)를 보세요.

## 페이지: 받아서 표시하기 {#page-script}

```html
<script>U=window.U=window.U||{},U.events=U.events||[],U.chat=function(n){return{on:function(e,t){U.events.push([n,e,t])},off:function(e){for(var t=U.events.length;t>0;)U.events[--t][0]==n&&U.events[t][1]==e&&U.events.splice(t,1)}}};</script>
<script async src="https://client.uchat.io/uchat.js"></script>
<u-chat room="my-room" style="display:inline-block;width:100%;max-width:400px;height:500px"></u-chat>

<script>
U.chat('my-room').on('after.create#payment', function (room) {
	function esc(value) {
		return String(value).replace(/[&<>"']/g, function (c) {
			return '&#' + c.charCodeAt(0) + ';';
		});
	}

	room.plugin.add('payment').onReceived(function (room, data) {
		// 서버 API가 보낸 것만 믿는다. 사용자도 같은 채널로 보낼 수 있다
		if (!data.user || data.user.auth !== 4) return;
		if (data.type !== 0) return; // 입장 때 오는 저장값(type 1)은 건너뛴다
		var msg = data.data;
		if (msg[0] !== 'paid' || typeof msg[1] !== 'string') return;
		room.print('🎉 누군가 <b>' + esc(msg[1].slice(0, 50)) + '</b>을(를) 구매했습니다!');
	});
});
</script>
```

## 짚어 볼 점 {#notes}

- `room.print`는 HTML로 표시하므로 받은 값은 위 코드처럼 꼭 이스케이프해요.
- 권한 확인 없이 표시하면 아무 사용자나 같은 채널로 가짜 결제 알림을 띄울 수 있어요. `data.user.auth === 4`는 서버 API로 보낸 데이터 또는 부관리자 이상이 저장한 값에만 붙어요. 저장값은 입장할 때 `type` 1로 오므로 위 코드는 `type` 0(서버 API로 모두에게 보낸 것)만 받아요. 닉네임을 지정해 보낸 서버 API 데이터도 `type` 1이라 저장값과 구분할 수 없어요.
- REST 요청 본문 앞부분과 요청 IP는 관리 페이지 **API** 메뉴의 호출 기록에 30일 동안 남아요. 구매자 이름·연락처·카드 정보 같은 개인정보는 보내지 마세요.
- 그냥 보낸 데이터는 지금 접속한 사람에게만 가요. 채팅방에 아무도 없어도 성공으로 답하고, 실패하더라도 결제 처리 자체를 실패로 돌리지 말고 기록만 남겨요. 결제 알림은 지나간 일이라 `save`로 저장하지 않는 게 맞아요.
- 한 사람에게만 보내려면 본문에 `"to":"닉네임"`을 넣어요(한글 닉네임도 돼요). 이렇게 보낸 데이터는 `type` 1로 오므로 위 코드의 `type` 확인을 빼야 해요.
- 무료 채팅방은 최근 60초 동안 받는 사람 수 합계가 한도를 넘으면 HTTP 429 `chat_rate_limited`가 나요([채널 API와 한도](/doc/3.5)).
- 코드 없이 채팅방에 띄우기만 하려면 **이벤트 알림창** 플러그인을 켜고 `plugin`을 `alertBox`, `data`를 `["custom", "구매 완료", 상품 이름]`으로 보내도 돼요.
- 서버 알림을 보내는 일반 흐름은 [서버에서 채팅방으로 알림 보내기](/doc/2.5)에 있어요.
