# 이벤트

> 채팅방 이벤트의 이름, 받는 값, before에서 false를 돌려줄 때 실제로 막히는 것을 정리한 목록이에요.

채팅방에서 일어나는 일을 `U.chat(채팅방).on(이름, function (room, data) { ... })`으로 받아요. 이 문서는 이벤트마다 언제 오는지, `data`에 무엇이 있는지, `before.`에서 `false`를 돌려주면 실제로 무엇이 막히는지를 코드 기준으로 정리해요. 등록·해제 방법은 [U.chat()과 room 객체](/doc/3.2#on-off)를 보세요.

## 규칙 {#rules}

- 대부분의 이벤트는 `before.이름`과 `after.이름` 짝으로 와요. `before.`는 기본 처리 전에, `after.`는 처리 뒤에 와요.
- 콜백은 `(room, data)`를 받아요. `room`은 [room 객체](/doc/3.2)이에요.
- `before.` 콜백이 `false`를 정확히 돌려주면 그 뒤의 콜백은 불리지 않고, 아래 표의 "false를 돌려주면" 칸에 적힌 처리만 멈춰요. **이벤트마다 막히는 범위가 달라요.**
- `before.`에서 `data`의 값을 고치면 이어지는 처리(화면 표시, 전송)가 고친 값을 써요. 고칠 수 있는 이벤트는 표에 적었어요.
- 막혀서 기본 처리가 멈추면 `after.`도 오지 않아요.

## 이벤트 목록 {#events}

| 이벤트 | 언제 | `data` | `before`에서 false를 돌려주면 |
| --- | --- | --- | --- |
| `before.create` | 채팅방 설치를 시작함(설정·스킨을 받기 전). 다시 설치할 때마다 다시 옴 | `{}` | 막을 수 없어요(설치는 그대로 진행) |
| `after.create` | 채팅방 화면 준비 완료(접속 전). 다시 설치(`room.reload()`)할 때마다 다시 옴 | `{ id }` | — |
| `before.join` | 서버에 입장을 요청하기 직전. 다시 접속할 때마다 다시 옴 | `{ user }` | 회원 연동이 아닌 방: 입장 요청을 보내지 않아요. 회원 연동 방: 아래 주의 참고 |
| `after.join` | 입장 완료, 내 정보를 받음. 다시 접속할 때마다 다시 옴 | `{ my }` | (`before` 없음) |
| `before.message`, `after.message` | 채팅 메시지 한 줄(지난 대화, 내 메시지 포함) | [메시지](#message) | 화면에 표시하지 않아요 |
| `before.whisper`, `after.whisper` | 귓속말(내가 보낸 귓속말의 확인 포함) | [귓속말](#whisper) | 화면에 표시하지 않아요 |
| `before.send`, `after.send` | 내가 메시지를 보낼 때(`/명령`, `!명령` 입력은 제외) | `{ message, bold, italic, underline, color }` | 보내지 않아요. `room.action.send()`가 `false`를 돌려줘요 |
| `before.command`, `after.command` | 명령을 실행할 때 | `{ type, data }` | 실행하지 않아요. `room.action.command()`가 `false`를 돌려줘요 |
| `before.plugin`, `after.plugin` | 채널 데이터를 받거나, `!명령`을 입력할 때 | [채널](#plugin) | 받은 데이터: `onReceived` 콜백을 부르지 않아요. `!명령`: `addParser` 콜백을 부르지 않고 `room.action.plugin()`이 `false`를 돌려줘요 |
| `before.user.join`, `after.user.join` | 다른 사람이 들어옴. 오지 않는 경우는 [사용자 변화](#users) 참고 | `{ user }` | 화면에 입장 표시를 하지 않아요. `room.user.list`에는 이미 들어가 있어요 |
| `before.user.quit`, `after.user.quit` | 다른 사람이 나감 | `{ user: { nick } }` | 화면에 퇴장 표시를 하지 않고 `room.user.list`에서도 지우지 않아요 |
| `before.user.modi`, `after.user.modi` | 누군가(나 포함)의 닉네임·권한·채팅금지가 바뀜 | `{ target, changed }` | `room.my`, `room.user.list`, 화면에 반영하지 않아요. `room.user.get()`은 이미 새 값을 돌려줄 수 있어요. 쓰지 않는 것을 권해요 |
| `before.user.count`, `after.user.count` | 접속자 수만 바뀜(입장·퇴장 대신 오는 경우는 [사용자 변화](#users) 참고) | `{ count }` | 화면의 접속자 수를 바꾸지 않아요 |
| `before.update`, `after.update` | 채팅방 설정이 바뀜 | 바뀐 값 `{ 키: 값 }` | `room.info`와 화면에 반영하지 않아요 |
| `before.system`, `after.system` | 시스템 안내 줄(공지·차단·채팅금지·호출 등, `room.print` 포함) | `{ type, data }` | 화면에 표시하지 않아요 |
| `before.control`, `after.control` | 서버 제어 신호(대화 청소, 투표 상태 등) | `{ type, data }` | 스킨에 전달하지 않아요 |
| `before.error`, `after.error` | 오류(서버 오류와, 명령 인자 검사 같은 내 쪽 오류) | `{ code }` | 화면에 오류를 표시하지 않아요 |
| `before.disconnect`, `after.disconnect` | 연결이 끊김(페이지를 떠나거나 태그를 지울 때는 오지 않음) | `{}` | 스킨에 알리지 않고, 네트워크 오류일 때의 자동 재접속도 하지 않아요 |

`before.`에서 막아도 서버에서 일어난 일은 그대로예요. 예를 들어 `before.system`으로 차단 안내를 숨겨도 차단은 적용되고, `before.user.quit`을 막아도 그 사람은 이미 나갔어요. 대부분 **내 화면에 보이는 것만** 바뀌어요.

## 일어나는 순서 {#order}

**채팅방이 열릴 때**

1. `before.create` — 설치 시작. `room.info`, `room.my`는 아직 비어 있어요.
2. 채팅방 설정·언어·플러그인·스킨을 불러와요(플러그인 실행 파일이 여기서 실행됨).
3. `after.create` — 스킨 화면 준비 완료. 스킨 UI와 채널은 여기서 등록해요.
4. `before.join` → 서버에 입장 요청 → `after.join`.
5. 지난 대화와 공지가 `message`, `whisper`, `system` 이벤트로 와요.

입장 확인(봇 검사) 화면이 뜨는 방은 확인을 통과한 뒤 1번부터 시작해요. 네트워크 문제로 다시 접속하면 4번부터 다시 와요.

**내가 메시지를 보낼 때**

1. `before.send` → 서버로 전송 → `after.send`
2. 서버가 채팅방 모두에게(나 포함) 메시지를 보내요.
3. `before.message` → 화면에 표시 → `after.message`

`after.send`는 "서버로 보냈다"는 뜻일 뿐, 받아들여졌다는 뜻이 아니에요. 서버가 거절하면 메시지가 돌아오지 않고 `error`(권한 없음 410, 같은 내용 반복 430 등), `system`(`muted`, `bad_word`), `control`(`slowWait`) 이벤트가 대신 와요. 실제로 올라갔는지는 `after.message`로 확인하세요.

채팅창 입력이 `/`로 시작하면 `send` 대신 `command`, `!`로 시작하면 `plugin` 이벤트로 가요. 기본 스킨은 `!명령`을 처리할 채널이 없거나 막히면 그 입력을 일반 메시지로 보내요.

**연결이 끊길 때**

1. `before.disconnect` → 스킨에 알림 → 자동 재접속 판단 → `after.disconnect`
2. 네트워크 오류로 끊겼으면 다시 접속하고 `before.join`부터 다시 와요.

차단되었거나 서버가 오류를 보낸 직후 끊겼으면 다시 접속하지 않아요. 브라우저를 닫거나, 다른 페이지로 가거나, `<u-chat>` 태그를 지우거나, `room.reload()`할 때는 `disconnect` 이벤트가 **오지 않아요**. 떠나기 전에 할 일은 브라우저의 `pagehide` 이벤트에서 하세요.

## 입장 {#join}

```js
U.chat('my-room').on('after.join', function (room, data) {
	// data.my 는 room.my 와 같은 모양의 사용자 객체
	room.print(data.my.nick + '님, 환영합니다.');
});
```

`before.create`는 화면도 정보도 없는 시점이라 쓸 일이 드물어요. 막을 수 없고, 입장 정보를 바꾸려면 `before.join`을 써요.

`before.join`의 `data.user`:

| 방 | `data.user` | 고칠 수 있는 것 |
| --- | --- | --- |
| 회원 연동이 아닌 방 | `{ nick }` — 이 브라우저에 저장된 닉네임(없으면 빈 값) | `data.user.nick`을 바꾸면 그 닉네임으로 입장을 요청해요 |
| 회원 연동 방 | `user_data`(또는 `user_data_url`)를 푼 값. `user_data`에 넣은 키와 `time`, `hash` | 고치지 마세요. 서명이 맞지 않아 오류 202가 나요 |

> **주의:** 회원 연동 방에서 `before.join`이 `false`를 돌려주면 입장이 막히는 것이 아니라 빈 입장 정보로 요청이 가서 오류로 끝나요. 회원 연동 방에서는 `before.join`으로 입장을 막지 마세요. 입장시키지 않으려면 `user_data`를 넣지 않거나 태그를 넣지 않아요.

## 메시지 {#message}

```js
U.chat('*').on('after.message', function (room, data) {
	console.log(data.user.nick, data.content);
});
```

| 키 | 설명 |
| --- | --- |
| `user` | 보낸 사람 [사용자 객체](/doc/3.2#user-object) |
| `content` | 내용 |
| `style` | `{ bold, italic, underline, size, color }`. `color`가 없으면 채팅방 기본 글자색 |
| `created` | 보낸 시각 `'YYYY-MM-DD HH:MM:SS'`(보는 사람의 시간대) |
| `time` | 보낸 시각(Unix 초) |
| `uid` | 메시지 고유 번호(16진수 문자열). 없으면 빈 값 |
| `past` | 입장·재접속 때 다시 받은 지난 대화면 `true`. 새로 올라온 메시지에는 없어요 |

- `before.message`에서 `data.content`를 고치면 고친 내용이 **내 화면에만** 보여요.
- 내가 무시한 사람(부관리자 미만)의 메시지는 `before.message`까지만 오고 화면 표시와 `after.message`는 없어요.

## 귓속말 {#whisper}

| 키 | 설명 |
| --- | --- |
| `from`, `to` | 보낸 사람, 받는 사람 [사용자 객체](/doc/3.2#user-object) |
| `content` | 내용 |
| `style` | 지금은 늘 빈 문자열이에요. 메시지의 `style` 객체와 모양이 달라요 |
| `created`, `time` | 보낸 시각(문자열, Unix 초) |

- 서버는 귓속말을 보낸 사람과 받는 사람 모두에게 보내요. 그래서 내가 보낸 귓속말도 이 이벤트로 돌아와요. `data.from.nick`이 내 닉네임이면 내가 보낸 거예요.
- 메시지와 달리 `uid`가 없어요.
- 입장할 때 지난 대화와 함께 나와 관련된 귓속말이 다시 와요.
- 내가 무시한 사람(부관리자 미만)의 귓속말이거나 내 설정이 귓속말 거부이면 `before.whisper`까지만 오고 화면 표시와 `after.whisper`는 없어요.

## 보내기와 명령 {#send-command}

```js
// 보내는 메시지 끝에 서명 붙이기
U.chat('*').on('before.send#sign', function (room, data) {
	data.message += ' ~';
});

// 귓속말 명령 막기
U.chat('*').on('before.command#no-whisper', function (room, data) {
	if (data.type === 'whisper') return false;
});
```

- `before.send`에서 `message`, `bold`, `italic`, `underline`, `color`를 고치면 고친 값으로 보내요.
- `before.command`의 `type`은 소문자에 `_`를 뺀 이름이에요(`un_ignore` → `unignore`). `data.data`(인자 배열)를 고치면 고친 인자로 실행해요.

## 채널 {#plugin}

`before.plugin`/`after.plugin`은 두 경우에 오며 `data` 모양이 달라요.

| 경우 | `data` |
| --- | --- |
| 다른 사람이나 서버가 보낸 채널 데이터를 받음 | `{ user, type, data }` — `user`는 보낸 사람, `type`은 `0`(모두에게) 또는 `1`(나에게만), `data`는 값 배열 |
| 채팅창에 `!이름 인자` 입력(또는 `room.action.plugin()`) | `{ type: '이름', data: ['인자', ...] }` — `type`이 채널 이름 |

서버(REST API)가 보낸 데이터와, 입장할 때 다시 받는 채널 저장값은 `user.auth`가 `4`이고 닉네임이 비어 있어요. 서버 발신인지는 `data.user.auth === 4`로만 확인하세요.

받은 데이터에는 채널 이름이 들어 있지 않아요. 채널별로 나눠 받으려면 `room.plugin.add(채널).onReceived`를 쓰세요. [채널 API와 한도](/doc/3.5)를 보세요.

## 사용자 변화 {#users}

```js
U.chat('*').on('after.user.modi', function (room, data) {
	// data.target  바뀌기 전 닉네임
	// data.changed { nick?, auth?, mute? } 중 바뀐 것
	if (data.changed.nick) room.print(data.target + ' → ' + data.changed.nick);
});
```

- 입장한 뒤의 변화에만 와요. 내 닉네임·권한·채팅금지가 바뀌어도 오며, 이때 `room.my`도 바뀌어요. 입장할 때의 권한은 `user.join`의 `data.user`나 `after.join`의 `data.my`로 봐요.
- `before.user.modi` 안에서는 `room.my`와 `room.user.list`가 아직 바뀌기 전이에요. `room.user.get()`은 이미 새 값일 수 있어요.
- `user.join`, `user.quit`이 오지 않고 `user.count`(3초마다)만 오는 경우가 있어요.
  - **기본 스킨.** 사용자 목록이 열려 있든 닫혀 있든 늘 이렇게 와요.
  - 레거시 스킨에서 사용자 목록이 닫혀 있을 때
  - 입장할 때 접속자가 100명 이상인 방
- 이때는 입장·퇴장이 사용자 목록 화면에만 반영되고, 페이지 스크립트에는 누가 들어오고 나갔는지 알리는 이벤트가 없어요. `room.user.list`도 갱신되지 않아요. 페이지 스크립트가 믿을 수 있는 것은 접속자 수(`user.count`)뿐이에요.
- 입장·퇴장 알림 같은 기능은 레거시 스킨에서 사용자 목록이 열린 작은 방에서만 동작한다고 보고 만드세요. 서버 쪽에서 알아야 하면 [서버 REST API](/doc/3.6#stats)의 접속자 수를 써요.
- `user.list` 이벤트(`{ list }`)는 레거시 스킨에서만 와요. 기본 스킨은 사용자 목록을 나눠 받으므로 이 이벤트가 오지 않아요.

## 시스템 안내 {#system}

`data.type`은 안내 종류, `data.data`는 값 배열이에요. 값 배열의 모양은 종류마다 달라요.

| `type` | 뜻 |
| --- | --- |
| `notice` | 공지, `room.print()` |
| `ban` | 차단 |
| `mute`, `permanent_mute` | 채팅금지(기간, 영구) |
| `un_mute` | 채팅금지 해제 |
| `to_call`, `from_call` | 호출을 보냄, 받음 |
| `invite`, `invite_notice` | 개인채널 초대를 받음, 보냄 |
| `ignore`, `un_ignore` | 무시하기, 해제 |
| `frozen`, `unfrozen` | 채팅방 얼림, 풀림 |
| `op`, `deop` | 부관리자 지정, 해제 |
| `user_info` | `info` 명령 결과 |
| `ip` | `ip` 명령 결과 |
| `admin_login`, `admin_logout` | 누군가 `/login`으로 관리자가 됨, 내려놓음 |
| `clear_log` | 대화 청소 |
| `muted` | 채팅금지 중이라 내 메시지가 거절됨 |
| `bad_word` | 단어 필터에 걸려 내 메시지가 거절됨 |
| `retry_connect` | 다시 접속을 시도함(시도 횟수) |

```js
U.chat('*').on('after.system', function (room, data) {
	if (data.type === 'from_call') window.focus();
});
```

호출 받기·초대 받기를 꺼 둔 사용자에게는 `from_call`, `invite`의 `before.system`도 오지 않아요.

## 제어 신호 {#control}

`data.type`은 신호 이름, `data.data`는 값 배열이에요. 대부분 스킨이 쓰는 내부 신호이므로 이름과 모양이 바뀔 수 있어요. 페이지 스크립트에서 쓸 만한 것은 아래 정도예요.

| `type` | 뜻 | `data` |
| --- | --- | --- |
| `clearLog` | 대화 청소 | `[]` |
| `removeChat` | 관리자가 메시지를 지움 | 지운 메시지의 `uid` 목록. `message`의 `uid`와 맞춰 봐요 |
| `slowWait` | 슬로우 모드라 내 메시지가 거절됨 | `[남은 밀리초, 보내려던 내용]` |
| `pluginDataReplaced` | 무료 방에서 채널 저장값이 바뀜 | `[]`. [채널 API와 한도](/doc/3.5#save) |

## 오류 {#error}

`data.code`는 오류 번호이고, 현재 언어의 안내 문구는 `room.language.error[code]`에 있어요.

```js
U.chat('*').on('after.error', function (room, data) {
	console.warn(data.code, room.language.error[data.code]);
});
```

| 코드 | 뜻 |
| --- | --- |
| `101` | 프로토콜 오류(잘못된 값, 너무 큰 채널 저장값 등) |
| `201` | 존재하지 않는 채팅방 |
| `301` | 이미 쓰는 닉네임 |
| `202` | 회원 연동 서명이 맞지 않음 |
| `203` | 회원 연동 방에 접속 정보 없이 접속 |
| `205` | 시간 연동 기준보다 오래된 접속 정보, 또는 처음 쓴 지 5분이 지난 같은 `user_data` |
| `207` | 채팅방 인원이 가득 참 |
| `208`, `701` | 비밀번호가 틀림, 필요함 |
| `402` | 권한 없음 |
| `404` | 대상이 없음 |
| `407` | 자기 자신에게 쓸 수 없는 명령 |
| `410` | 채팅 권한 없음(기능 조건) |
| `412` | 명령 인자가 잘못됨 |
| `415` | 차단된 상태 |
| `416` | 이 채팅방에서는 쓸 수 없는 명령 |
| `421` | 이미 그 권한임 |
| `422` | 비밀번호가 틀림 |
| `413` | 채팅금지 중 |
| `418` | 얼린 채팅방 |
| `424` | 내가 귓속말 거부 중이라 보낼 수 없음 |
| `425` | 너무 자주 보냄 |
| `428` | 채널 전송 한도 초과 |
| `429` | 로그인 시도가 너무 많음 |
| `430` | 같은 내용을 반복해 보냄 |
| `501`, `502` | 개인채널이 없음, 개인채널 안에서는 개인채널을 만들 수 없음 |

회원 연동 오류의 원인과 해결은 [회원 연동](/doc/2.1#troubleshooting)을 보세요.

## data 예시 {#data-examples}

이벤트마다 받는 `data`의 실제 모양이에요. 값은 예시이고, 서버가 보내지 않은 키는 `undefined`일 수 있어요. 직접 확인하려면 `U.chat('*').on('after.message', function (room, data) { console.log(data); })`처럼 콘솔에 찍어 보세요.

```js
// after.join
{ my: { nick: '홍길동', id: 'user1', level: 5, auth: 1, icons: '', nickcon: '',
        connected: '2026-10-17 12:00:00', time: 1792209600, mute: 0, session: 'a1b2c3d4e5f6', profileimg: '' } }

// before.join (회원 연동이 아닌 방)
{ user: { nick: '홍길동' } }

// after.message
{ user: { nick: '홍길동', auth: 1, ... },
  content: '안녕하세요',
  style: { bold: false, italic: false, underline: false, size: '9pt', color: '#000000' },
  created: '2026-10-17 12:00:01', time: 1792209601, uid: '0a1b2c3d4e' }

// after.whisper
{ from: { nick: '홍길동', ... }, to: { nick: '김철수', ... }, style: '', content: '잠깐요',
  created: '2026-10-17 12:00:02', time: 1792209602 }

// before.send
{ message: '안녕하세요', bold: undefined, italic: undefined, underline: undefined, color: undefined }

// before.command  (/w 김철수 잠깐요)
{ type: 'whisper', data: ['김철수', '잠깐요'] }

// after.user.join / after.user.quit
{ user: { nick: '김철수', auth: 0, ... } }
{ user: { nick: '김철수' } }

// after.user.modi
{ target: '김철수', changed: { nick: '철수' } }
{ target: '김철수', changed: { auth: 2 } }
{ target: '김철수', changed: { mute: 1 } }

// after.user.count
{ count: 152 }

// after.update
{ slowSeconds: 5 }

// after.system
{ type: 'mute', data: ['관리자', '김철수', 10, '도배'] } // 관리자, 대상, 분, 사유
{ type: 'from_call', data: ['김철수', '홍길동'] }       // 보낸 사람, 받는 사람

// after.control
{ type: 'removeChat', data: ['0a1b2c3d4e'] }

// after.plugin  (채널 데이터를 받음 / !dice 1 6 입력)
{ user: { nick: '김철수', auth: 1, ... }, type: 0, data: [4] }
{ type: 'dice', data: ['1', '6'] }

// after.error
{ code: 402 }
```
