# 레거시 스킨 API

> 예전 채팅방이 쓰는 레거시 스킨(basic)의 room.skin API — jQuery DOM, 메뉴바, 팝업, 닉네임 메뉴 — 목록과 기본 스킨으로 옮기는 대응표예요.

레거시 스킨(`basic`, 관리 페이지에는 "레거시 스킨 (구버전)"으로 보임)은 예전 채팅방이 쓰는 스킨이에요. 새 채팅방은 기본 스킨(`basic-v2`)으로 만들어지며, 새 기능은 기본 스킨에만 더해져요. 이 문서는 아직 레거시 스킨을 쓰는 채팅방을 위한 `room.skin` 목록이에요. 기본 스킨의 API는 [스킨 UI API](/doc/3.4)를 보세요.

페이지 스크립트의 `U.chat()`, 이벤트, `room.action`, 채널(`room.plugin`), 서버 API는 두 스킨에서 같아요. 다른 것은 `room.skin`뿐이에요.

## 어느 스킨인지 확인 {#detect}

`room.skin`의 멤버는 스킨이 화면을 만든 뒤에 생기므로 `after.create` 안에서 확인해요.

```js
U.chat('my-room').on('after.create', function (room) {
	if (room.skin.menubar) {
		// 레거시 스킨
	} else if (room.skin.menu) {
		// 기본 스킨
	}
});
```

`userMenu`는 두 스킨에 모두 있으므로 구별에 쓰지 마세요. 스킨은 관리 페이지 **스킨** 메뉴나 `<u-chat skin="basic-v2">` 속성으로 바꿀 수 있어요. 회원 연동처럼 스킨과 상관없는 기능은 바꿔도 그대로 동작해요.

## 원칙 {#principles}

- 레거시 스킨은 채팅방 iframe 안의 DOM을 jQuery(1.12)로 직접 다뤄요. 스킨 HTML 구조와 클래스 이름은 정해진 계약이 아니며, 고친 화면은 스킨이 바뀌면 깨질 수 있어요.
- 등록은 모두 `after.create` 안에서 해요. 그 전에는 `room.skin.arguments`와 `room.skin.messages`만 있어요.
- 유챗 로고와 정보 표시를 가리거나 지우지 마세요.

## room.skin 목록 {#members}

| 이름 | 설명 |
| --- | --- |
| `room.skin.arguments` | 스킨 설정값(관리 페이지 스킨 설정 + `skin_arguments` 속성 + 전역 `Uchat_argument`). 스킨이 색 기본값을 채워 넣어요 |
| `room.skin.messages` | 스킨 화면 문구(현재 언어) |
| `room.skin.window` | 채팅방 iframe의 `window`. jQuery는 `room.skin.window.$` |
| `room.skin.document` | 채팅방 iframe의 `document` |
| `room.skin.dom` | 주요 영역의 jQuery 객체. 아래 [dom](#dom) |
| `room.skin.menubar` | 입력창 아래 메뉴바. 아래 [menubar](#menubar) |
| `room.skin.popup` | 팝업(알림·확인·입력·선택). 아래 [popup](#popup) |
| `room.skin.userMenu` | 닉네임을 누르면 나오는 메뉴. 아래 [userMenu](#user-menu) |
| `room.skin.scrollDown(force)` | 대화를 맨 아래로 내려요. `force`가 참이 아니면 사용자가 위로 올려 둔 동안에는 내리지 않아요 |

## dom {#dom}

모두 채팅방 iframe 안 요소의 jQuery 객체예요.

| 키 | 영역 |
| --- | --- |
| `wrap` | 채팅방 전체(`.wrap`) |
| `top` | 위쪽 제목 영역(`.top`) |
| `middle` | 대화와 사용자 목록 영역(`.middle`) |
| `content` | 대화 스크롤 영역(`.contentWrap`) |
| `content_target` | 대화 줄이 붙는 요소(`.content`) |
| `userList` | 사용자 목록 스크롤 영역(`.userListWrap`) |
| `contour` | 사용자 목록과 대화 사이 구분선(`.contour`) |
| `bottom` | 아래 입력 영역(`.bottom`) |
| `inputContent` | 입력창(`.inputContent`) |
| `menubar` | 메뉴바(`.menuBar`) |
| `latest_chat` | 위로 스크롤했을 때 보이는 최근 대화 알림(`.latest_chat`) |
| `setting`, `settingBG` | 설정 창과 그 배경(`.settingContent`, `.settingBackground`) |

```js
U.chat('my-room').on('after.create', function (room) {
	var $ = room.skin.window.$;
	var banner = $('<div/>').text('오늘 8시에 이벤트가 있습니다!').css({ padding: '6px', background: '#fff4cc' });
	room.skin.dom.middle.before(banner);
	$(room.skin.window).resize(); // 높이가 바뀌었으면 스킨이 다시 배치하게
});
```

요소를 더해 높이가 바뀌면 `$(room.skin.window).resize()`를 불러 스킨이 대화 영역 크기를 다시 계산하게 해요. 사용자 입력은 `.text()`로 넣고 `.html()`에 넣지 마세요.

## menubar {#menubar}

입력창 아래 메뉴바에 버튼을 더해요.

```js
U.chat('my-room').on('after.create', function (room) {
	room.skin.menubar.add({
		id: 'hello',
		title: '인사 보내기',
		html: '<div>인사</div>',
		condition: function (room, item) {
			return room.my.auth >= 1; // false면 보이지 않음
		},
		onClick: function (room, item) {
			room.action.send('안녕하세요!');
		},
	});
});
```

| 키 | 설명 |
| --- | --- |
| `id` | 필수. 겹치면 등록되지 않아요. 버튼에 `menubar-아이디` 클래스가 붙어요 |
| `title` | 마우스를 올렸을 때 설명 |
| `html` | 버튼 내용(HTML 문자열). 없으면 빈 `<div>` |
| `icon` | 배경 이미지 주소 |
| `condition(room, item)` | 정확히 `false`를 돌려주면 그리지 않아요 |
| `onClick(room, item)` | 눌렀을 때. `item.event`가 클릭 이벤트예요 |
| `beforeLoad(room, item)`, `afterLoad(room, item)` | 그리기 전후. `beforeLoad`가 `false`면 그리지 않아요 |
| `beforeUnload()`, `afterUnload()` | 지우기 전후 |

`item`은 `add`에 넘긴 객체 자신이며, 그려진 버튼의 jQuery 객체가 `item.layout`에 들어가요.

| 함수 | 설명 |
| --- | --- |
| `add(item)` | 등록해요. 성공하면 `true`, 아이디가 없거나 겹치면 `false` |
| `refresh()` | 등록된 버튼을 `condition`에 따라 그려요 |
| `remove(id)` | 등록을 지워요 |
| `hide()`, `show()`, `toggle()` | 메뉴바 전체를 숨기기·보이기·바꾸기 |
| `list` | 등록된 항목 배열 |

- **`add`만으로는 화면에 나오지 않아요.** 스킨이 입장할 때마다 `refresh()`를 부르므로 `after.create`에서 등록하면 입장할 때 보여요. 입장한 뒤에 등록했다면 `room.skin.menubar.refresh()`를 직접 부르세요.
- `condition`은 `refresh()` 때만 다시 계산해요. 이미 그려진 버튼의 `condition`이 `false`가 되면 등록이 지워지고, 나중에 다시 참이 되어도 돌아오지 않아요. 권한에 따라 버튼을 숨겼다 보였다 해야 하면 `condition` 대신 `afterLoad`에서 받은 `item.layout`을 `toggle()`하세요.
- `remove(id)`는 아이디가 숫자가 아니면 그려진 버튼을 화면에서 지우지 못해요. 버튼을 지울 때는 `item.layout.remove()`도 함께 부르세요.
- 기본 버튼의 아이디는 `scroll`, `bold`, `picture`, `call_admin` 등이에요.

## popup {#popup}

채팅방 안에 팝업을 띄워요. 팝업은 한 번에 하나만 보이고, 나머지는 순서대로 기다려요. 모든 함수가 팝업 번호를 돌려주며, `close(번호)`로 닫을 수 있어요. 결과는 Promise가 아니라 콜백으로 받아요.

```js
var popup = room.skin.popup;

popup.confirm('가위바위보', '도전을 받을까요?', function (ok) {
	// 확인 true, 취소 false, 바깥을 눌러 닫으면 null
});

popup.prompt('닉네임', '새 닉네임을 적어 주세요.', '기본값', function (value) {
	// 입력한 글자, 취소하면 null
});

popup.select('선택', '하나를 고르세요.', { r: '바위', p: '보', s: '가위' }, function (key) {
	// 고른 키(문자열), 취소하면 null
});

var id = popup.alert('알림', '10초 뒤에 닫힙니다.');
setTimeout(function () { popup.close(id); }, 10000);
```

| 함수 | 콜백이 받는 값 |
| --- | --- |
| `alert(title, html, onOpen, onClose)` | `onOpen(layer, pb, pu)`은 **열릴 때** 불리고, 닫힐 때는 `onClose()`가 불려요. `html`은 HTML로 들어가므로 사용자 값은 이스케이프하세요 |
| `confirm(title, subtitle, fn)` | `true`, `false`, 바깥을 누르면 `null` |
| `prompt(title, subtitle, value, fn)` | 입력한 글자, 취소하면 `null` |
| `password(title, subtitle, value, fn)` | `prompt`와 같고 입력이 가려져요 |
| `select(title, subtitle, list, fn)` | `list`(객체 또는 배열)의 키, 취소하면 `null` |
| `add(html, type, fn(layer, pb, pu), after)` | 직접 만든 팝업. `layer`는 `{ close(), centering() }`, `pb`는 배경, `pu`는 팝업 본체(jQuery). `after()`는 닫힐 때 |
| `close([번호])` | 그 팝업을 닫거나 대기열에서 빼요. 번호를 빼면 지금 보이는 팝업을 닫아요 |
| `closeByType(type)` | `add`에 준 `type`이 같은 팝업을 모두 닫아요 |

- 입력 팝업에서 Enter를 누르면 확인과 같아요.
- 브라우저 창이 포커스를 잃으면 지금 보이는 팝업이 바깥을 누른 것처럼 닫혀요(`confirm`·`prompt`·`select`는 `null`). 다른 창을 열고 돌아오는 흐름에서는 결과가 `null`일 수 있다고 보고 짜세요.

## userMenu {#user-menu}

닉네임을 누르면 나오는 메뉴에 항목을 더해요. 메뉴는 누를 때마다 새로 만들어지므로 `condition`이 매번 다시 계산돼요.

```js
room.skin.userMenu.add({
	id: 'profile',
	text: '프로필 보기',
	condition: function (room, data) {
		return data.target !== room.my.nick; // 내 닉네임에는 숨김
	},
	onClick: function (room, data) {
		var user = room.user.get(data.target);
		if (user && user.id) window.open('/profile/' + encodeURIComponent(user.id));
	},
});
```

| 키 | 설명 |
| --- | --- |
| `id` | 필수. 겹치면 등록되지 않아요(`false`) |
| `text` | 항목 글자 |
| `condition(room, data)` | 정확히 `false`를 돌려주면 숨겨요. `data.target`은 누른 **닉네임 문자열**이에요 |
| `onClick(room, data)` | 눌렀을 때. `data`는 `{ event, id, target, messageUid }` |

| 함수 | 설명 |
| --- | --- |
| `add(item)` | 항목 하나를 더해요 |
| `group(left, right)` | 두 항목을 한 줄에 나란히 둬요 |
| `hr([{ condition }])` | 구분선. 아이디는 늘 `hr`이라 `remove('hr')`는 구분선을 모두 지워요 |
| `remove(id)` | 그 아이디의 항목을 모두 지워요(기본 항목 포함) |
| `list` | 등록된 항목 배열 |

기본 항목의 아이디는 `info`, `whisper`, `call`, `ignore`, `un_ignore`, `report`, `invite`, `individual`, `mute`, `unmute`, `ban`, `ip`, `op`, `deop`, `freeze`, `unfreeze`, `clearlog`, `removeChat`, `logout` 등이에요. 기본 항목을 숨기려면 `remove(id)`를 부르세요. 메뉴를 숨겨도 권한 검사는 서버가 해요.

## 레거시 전용 이벤트 {#legacy-events}

`before.user.list`, `after.user.list`(`data`는 `{ list }`)는 레거시 스킨이 사용자 목록을 열어 전체 목록을 받을 때 와요. 기본 스킨은 사용자 목록을 나눠 받으므로 이 이벤트가 오지 않아요. 나머지 이벤트는 [이벤트](/doc/3.3)와 같아요.

## 기본 스킨으로 옮기기 {#migrate}

| 하는 일 | 레거시 스킨 | 기본 스킨 |
| --- | --- | --- |
| 버튼 더하기 | `menubar.add({ id, title, html, condition(room, item), onClick(room, item) })` | `menu.add({ id, label, title, condition(ctx), onSelect(ctx) })` |
| 버튼 다시 계산 | `menubar.refresh()` | `menu.resolver.refresh()` |
| 닉네임 메뉴 | `userMenu.add({ id, text, onClick(room, data) })`, `data.target`은 닉네임 | `userMenu.add({ id, label, onSelect(ctx) })`, `ctx.target`은 사용자 객체 |
| 기본 항목 숨기기 | `userMenu.remove('ignore')` | `userMenu.resolver.add(...)`로 `skin.ignore` 걸러 내기 |
| 팝업 | `popup.confirm(t, s, fn)` 등 콜백 | `dialog.confirm(t, s)` 등 Promise |
| 화면에 영역 넣기 | `dom.middle.before(...)` 등 DOM 직접 | `mount.messageList({ position: 'before', mount(host) })` 등 |
| 대화 위에 겹치기 | `dom.content.append(...)` + CSS | `mount.overlay({ layer, scope, mount(host) })` |
| 닉네임·메시지 꾸미기 | `window.$`로 `<style>` 넣기 | `nickname.resolver`, `message.resolver` |
| 맨 아래로 | `scrollDown(force)` | `scrollToBottom(force)` |

기본 스킨에서는 스킨 DOM, jQuery, `<style>` 넣기를 지원하지 않아요. 옮긴 뒤의 사용법은 [스킨 UI 확장하기](/doc/2.3)부터 보세요.
