@nowsoft-lab/chear-widget-react 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -2
- package/dist/index.d.ts +27 -0
- package/dist/index.js +66 -0
- package/dist/loader.d.ts +13 -0
- package/dist/loader.js +32 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.js +4 -0
- package/package.json +52 -4
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nowsoft-lab/chear-widget-react
|
|
2
|
+
|
|
3
|
+
치어(CHEAR) 위젯 — 방송 **플레이어**와 **채팅**을 리액트 앱에 컴포넌트로 넣습니다.
|
|
4
|
+
스튜디오 › 위젯의 iframe 위젯과 같은 화면 · 같은 동작이고, 위젯이 바뀌면 이 패키지를 다시 설치하지 않아도 같이 바뀝니다.
|
|
5
|
+
|
|
6
|
+
## 설치
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm i @nowsoft-lab/chear-widget-react
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
React 18 이상. Next.js(App Router)에서도 그대로 씁니다(`"use client"` 포함 — 서버에서는 빈 div 만 그리고 브라우저에서 위젯이 뜹니다).
|
|
13
|
+
npm 을 쓸 수 없으면 위젯 출처의 설치 파일로도 받을 수 있습니다: `npm i https://player.chear.live/sdk/chear-widget-react.tgz`
|
|
14
|
+
|
|
15
|
+
## 사용
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { ChearPlayer, ChearChat } from "@nowsoft-lab/chear-widget-react";
|
|
19
|
+
|
|
20
|
+
<ChearPlayer channel="채널아이디" />
|
|
21
|
+
<ChearChat channel="채널아이디" />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`channel` 은 스튜디오에 보이는 @아이디입니다. 이것만 넣으면 동작합니다.
|
|
25
|
+
|
|
26
|
+
### 플레이어 `<ChearPlayer>`
|
|
27
|
+
|
|
28
|
+
| prop | 기본 | 뜻 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `channel` | (필수) | 채널 아이디 |
|
|
31
|
+
| `autoplay` | `true` | 자동 재생(브라우저 정책상 소리 끔과 함께만 확실) |
|
|
32
|
+
| `muted` | `true` | 소리 끔으로 시작 |
|
|
33
|
+
| `controls` | `true` | 플레이어 컨트롤 |
|
|
34
|
+
| `chat` | `false` | 오른쪽(좁은 세로 화면은 아래)에 채팅 같이 |
|
|
35
|
+
| `stats` | `true` | 왼쪽 위 LIVE · 시청 중 |
|
|
36
|
+
|
|
37
|
+
크기: 폭 100% · 16:9 가 기본. `style={{ width: 1280 }}` · `className` 으로 바꿉니다.
|
|
38
|
+
|
|
39
|
+
### 채팅 `<ChearChat>`
|
|
40
|
+
|
|
41
|
+
| prop | 기본 | 뜻 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `channel` | (필수) | 채널 아이디 |
|
|
44
|
+
| `theme` | `"dark"` | `"dark"` · `"light"` |
|
|
45
|
+
|
|
46
|
+
크기: 폭 100% · 부모 높이(없으면 최소 480px). 로그인은 위젯 안에서 합니다(이메일 · 가입).
|
|
47
|
+
|
|
48
|
+
### 공통
|
|
49
|
+
|
|
50
|
+
| prop | 뜻 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `ssoToken` | 간편 로그인(SSO) 토큰 — 파트너 회원으로 바로 채팅(스튜디오 › 위젯 › 채팅 › 간편 로그인 안내). 바뀌면 다시 로그인 |
|
|
53
|
+
| `origin` | 위젯 출처(기본 `https://player.chear.live`). 여러 개면 `<ChearProvider origin="…">` 로 감쌉니다 |
|
|
54
|
+
| `onError` | 위젯을 불러오지 못했을 때(네트워크 · 사이트 보안 정책) |
|
|
55
|
+
|
|
56
|
+
## 동작 방식 · 주의
|
|
57
|
+
|
|
58
|
+
- 화면은 위젯 출처의 런타임(`https://player.chear.live/sdk/v1/runtime.js`)이 그립니다. 처음 한 번 받고 캐시합니다.
|
|
59
|
+
- 컴포넌트의 div 안 **Shadow DOM** 에 그려서 페이지 스타일과 섞이지 않습니다. 모달 · 로그인 창도 위젯 틀 안에만 뜹니다.
|
|
60
|
+
- 사이트에 CSP 를 쓰면 `script-src` · `connect-src` · `media-src` · `img-src` 에 위젯 출처와 영상 · Firebase 주소를 넣어야 합니다.
|
|
61
|
+
- 로그인 토큰은 이 페이지의 localStorage(`chear.widget:` 로 시작하는 키)에 둡니다. 채팅 · 내 정보 · 신고 · 팔로우만 되는 짧은 토큰입니다. 더 강한 격리가 필요하면 iframe 위젯을 쓰세요.
|
|
62
|
+
- 채널 설정의 "퍼가기 허용"을 그대로 따릅니다(꺼져 있으면 안내만). 채널에 허용 도메인이 있으면 그 사이트에서만 로그인할 수 있습니다(아니면 EMB-005).
|
|
63
|
+
|
|
64
|
+
## 판
|
|
65
|
+
|
|
66
|
+
- 이 패키지는 껍데기라 판이 거의 바뀌지 않습니다. 위젯 화면 · 기능 개선은 런타임(`/sdk/v1/`)으로 바로 반영됩니다.
|
|
67
|
+
- props 가 바뀌는 큰 변경이면 런타임 `/sdk/v2/` 와 이 패키지 2.x 를 같이 냅니다(1.x 는 한동안 유지).
|
|
2
68
|
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { ChatOptions, PlayerOptions } from "./types";
|
|
3
|
+
export { DEFAULT_ORIGIN, loadRuntime, runtimeUrl } from "./loader";
|
|
4
|
+
export type { ChatOptions, PlayerOptions } from "./types";
|
|
5
|
+
/** 위젯 출처를 바꿀 때만(개발서버 · 다른 플랫폼). 운영 치어는 필요 없음 */
|
|
6
|
+
export declare function ChearProvider({ origin, children }: {
|
|
7
|
+
origin: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
interface CommonProps {
|
|
11
|
+
/** 채널 아이디 — 스튜디오에 보이는 @아이디(위젯 주소의 /embed/{아이디}/…) */
|
|
12
|
+
channel: string;
|
|
13
|
+
/** 간편 로그인(SSO) 토큰 — 파트너 회원으로 바로 채팅. 없으면 위젯 안 로그인 */
|
|
14
|
+
ssoToken?: string;
|
|
15
|
+
/** 위젯 출처 — ChearProvider 보다 우선 */
|
|
16
|
+
origin?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
style?: CSSProperties;
|
|
19
|
+
/** 런타임을 못 불러왔을 때(네트워크 · 사이트 CSP 등) */
|
|
20
|
+
onError?: (error: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
export type ChearPlayerProps = CommonProps & PlayerOptions;
|
|
23
|
+
export type ChearChatProps = CommonProps & ChatOptions;
|
|
24
|
+
/** 플레이어 — 기본 폭 100% · 16:9. `chat` 이면 오른쪽에 채팅 같이 */
|
|
25
|
+
export declare function ChearPlayer({ autoplay, muted, controls, chat, stats, ...common }: ChearPlayerProps): import("react").JSX.Element;
|
|
26
|
+
/** 채팅 — 실제로 채팅(위젯 안 로그인 · 이모티콘 · 신고). 기본 폭 100% · 부모 높이(없으면 최소 480px) */
|
|
27
|
+
export declare function ChearChat({ theme, ...common }: ChearChatProps): import("react").JSX.Element;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { DEFAULT_ORIGIN, loadRuntime } from "./loader";
|
|
5
|
+
export { DEFAULT_ORIGIN, loadRuntime, runtimeUrl } from "./loader";
|
|
6
|
+
/**
|
|
7
|
+
* 치어 위젯 — 플레이어 · 채팅을 리액트 컴포넌트로 (docs/design/embed-sdk.md).
|
|
8
|
+
* 화면은 위젯 출처의 런타임(`/sdk/v1/runtime.js`)이 그린다 — 스튜디오 › 위젯의 iframe 위젯과 같은 화면 · 같은 동작.
|
|
9
|
+
* 파트너 페이지 스타일과 섞이지 않게 컴포넌트의 div 안 Shadow DOM 에 그린다.
|
|
10
|
+
*/
|
|
11
|
+
const OriginContext = createContext(DEFAULT_ORIGIN);
|
|
12
|
+
/** 위젯 출처를 바꿀 때만(개발서버 · 다른 플랫폼). 운영 치어는 필요 없음 */
|
|
13
|
+
export function ChearProvider({ origin, children }) {
|
|
14
|
+
return _jsx(OriginContext.Provider, { value: origin, children: children });
|
|
15
|
+
}
|
|
16
|
+
/** 정의된 값만 — 없는 옵션은 위젯 기본값 */
|
|
17
|
+
function defined(value) {
|
|
18
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
19
|
+
}
|
|
20
|
+
function Widget({ kind, channel, ssoToken, origin, className, style, onError, options, defaultStyle }) {
|
|
21
|
+
const fromContext = useContext(OriginContext);
|
|
22
|
+
const target = origin ?? fromContext;
|
|
23
|
+
const host = useRef(null);
|
|
24
|
+
const instance = useRef(null);
|
|
25
|
+
const [failed, setFailed] = useState(false);
|
|
26
|
+
const config = { kind, channel, options: defined(options), ssoToken };
|
|
27
|
+
const latest = useRef(config);
|
|
28
|
+
latest.current = config;
|
|
29
|
+
const onErrorRef = useRef(onError);
|
|
30
|
+
onErrorRef.current = onError;
|
|
31
|
+
// 출처마다 한 번 그린다 — 옵션 · 채널이 바뀌면 아래 update 로
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const el = host.current;
|
|
34
|
+
if (!el)
|
|
35
|
+
return;
|
|
36
|
+
let cancelled = false;
|
|
37
|
+
setFailed(false);
|
|
38
|
+
loadRuntime(target).then((runtime) => {
|
|
39
|
+
if (!cancelled)
|
|
40
|
+
instance.current = runtime.mount(el, latest.current);
|
|
41
|
+
}, (error) => {
|
|
42
|
+
if (cancelled)
|
|
43
|
+
return;
|
|
44
|
+
setFailed(true);
|
|
45
|
+
onErrorRef.current?.(error);
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
instance.current?.unmount();
|
|
50
|
+
instance.current = null;
|
|
51
|
+
};
|
|
52
|
+
}, [target]);
|
|
53
|
+
const key = JSON.stringify(config);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
instance.current?.update(latest.current);
|
|
56
|
+
}, [key]);
|
|
57
|
+
return _jsx("div", { ref: host, className: className, style: { ...defaultStyle, ...style }, "data-chear-widget": kind, "data-failed": failed || undefined });
|
|
58
|
+
}
|
|
59
|
+
/** 플레이어 — 기본 폭 100% · 16:9. `chat` 이면 오른쪽에 채팅 같이 */
|
|
60
|
+
export function ChearPlayer({ autoplay, muted, controls, chat, stats, ...common }) {
|
|
61
|
+
return _jsx(Widget, { kind: "player", options: { autoplay, muted, controls, chat, stats }, defaultStyle: { display: "block", width: "100%", aspectRatio: "16 / 9" }, ...common });
|
|
62
|
+
}
|
|
63
|
+
/** 채팅 — 실제로 채팅(위젯 안 로그인 · 이모티콘 · 신고). 기본 폭 100% · 부모 높이(없으면 최소 480px) */
|
|
64
|
+
export function ChearChat({ theme, ...common }) {
|
|
65
|
+
return _jsx(Widget, { kind: "chat", options: { theme }, defaultStyle: { display: "block", width: "100%", height: "100%", minHeight: 480 }, ...common });
|
|
66
|
+
}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WidgetRuntime } from "./types";
|
|
2
|
+
/** 운영 위젯 출처 — 개발 · 다른 플랫폼은 <ChearProvider origin="…"> */
|
|
3
|
+
export declare const DEFAULT_ORIGIN = "https://player.chear.live";
|
|
4
|
+
/** 런타임 판 — 위젯 출처의 /sdk/v{N}/runtime.js */
|
|
5
|
+
export declare const RUNTIME_VERSION = 1;
|
|
6
|
+
/** `https://player.chear.live/` · 경로가 붙은 주소 → 출처만 */
|
|
7
|
+
export declare function normalizeOrigin(origin: string): string;
|
|
8
|
+
export declare const runtimeUrl: (origin: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* 런타임을 위젯 출처에서 한 번만 받는다(같은 출처면 컴포넌트 여러 개가 같이 씀).
|
|
11
|
+
* 화면은 런타임에 있어서 위젯이 바뀌면 이 패키지를 다시 받지 않아도 같이 바뀐다. 실패하면 다음 시도 때 다시 받는다
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadRuntime(origin?: string): Promise<WidgetRuntime>;
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** 운영 위젯 출처 — 개발 · 다른 플랫폼은 <ChearProvider origin="…"> */
|
|
2
|
+
export const DEFAULT_ORIGIN = "https://player.chear.live";
|
|
3
|
+
/** 런타임 판 — 위젯 출처의 /sdk/v{N}/runtime.js */
|
|
4
|
+
export const RUNTIME_VERSION = 1;
|
|
5
|
+
/** `https://player.chear.live/` · 경로가 붙은 주소 → 출처만 */
|
|
6
|
+
export function normalizeOrigin(origin) {
|
|
7
|
+
return new URL(origin).origin;
|
|
8
|
+
}
|
|
9
|
+
export const runtimeUrl = (origin) => `${normalizeOrigin(origin)}/sdk/v${RUNTIME_VERSION}/runtime.js`;
|
|
10
|
+
const loading = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* 런타임을 위젯 출처에서 한 번만 받는다(같은 출처면 컴포넌트 여러 개가 같이 씀).
|
|
13
|
+
* 화면은 런타임에 있어서 위젯이 바뀌면 이 패키지를 다시 받지 않아도 같이 바뀐다. 실패하면 다음 시도 때 다시 받는다
|
|
14
|
+
*/
|
|
15
|
+
export function loadRuntime(origin = DEFAULT_ORIGIN) {
|
|
16
|
+
const url = runtimeUrl(origin);
|
|
17
|
+
let pending = loading.get(url);
|
|
18
|
+
if (!pending) {
|
|
19
|
+
// 런타임 조각들이 실행되기 전에 "위젯 SDK 로 돈다 · 위젯 출처"를 알린다(런타임이 파트너 문서의 <html> 을 건드리지 않게)
|
|
20
|
+
const g = globalThis;
|
|
21
|
+
g.__CHEAR_SDK__ ?? (g.__CHEAR_SDK__ = { origin: normalizeOrigin(origin) });
|
|
22
|
+
pending = import(/* @vite-ignore */ /* webpackIgnore: true */ url).then((mod) => {
|
|
23
|
+
const runtime = mod.default;
|
|
24
|
+
if (!runtime || runtime.version !== RUNTIME_VERSION)
|
|
25
|
+
throw new Error(`치어 위젯 런타임을 불러오지 못했어요(${url})`);
|
|
26
|
+
return runtime;
|
|
27
|
+
});
|
|
28
|
+
pending.catch(() => loading.delete(url));
|
|
29
|
+
loading.set(url, pending);
|
|
30
|
+
}
|
|
31
|
+
return pending;
|
|
32
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 위젯 런타임과의 약속(v1) — 런타임 쪽 같은 모양: user-web `src/sdk/types.ts`. 바꾸면 둘 다 같이(docs/design/embed-sdk.md 2장)
|
|
3
|
+
*/
|
|
4
|
+
/** 플레이어 옵션 — 위젯 주소 쿼리와 같은 이름 · 같은 기본값 */
|
|
5
|
+
export interface PlayerOptions {
|
|
6
|
+
/** 자동 재생(기본 켬 — 브라우저 정책상 소리 끔과 함께만 확실) */
|
|
7
|
+
autoplay?: boolean;
|
|
8
|
+
/** 소리 끔(기본 켬) */
|
|
9
|
+
muted?: boolean;
|
|
10
|
+
/** 플레이어 컨트롤(기본 켬) */
|
|
11
|
+
controls?: boolean;
|
|
12
|
+
/** 오른쪽(좁은 세로 화면은 아래)에 채팅 같이(기본 끔) */
|
|
13
|
+
chat?: boolean;
|
|
14
|
+
/** 왼쪽 위 LIVE · 시청 중(기본 켬) */
|
|
15
|
+
stats?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** 채팅 옵션 */
|
|
18
|
+
export interface ChatOptions {
|
|
19
|
+
/** 다크(기본) · 라이트 */
|
|
20
|
+
theme?: "dark" | "light";
|
|
21
|
+
}
|
|
22
|
+
export type WidgetKind = "player" | "chat";
|
|
23
|
+
export interface WidgetConfig {
|
|
24
|
+
kind: WidgetKind;
|
|
25
|
+
channel: string;
|
|
26
|
+
options?: PlayerOptions | ChatOptions;
|
|
27
|
+
ssoToken?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface WidgetInstance {
|
|
30
|
+
update(config: WidgetConfig): void;
|
|
31
|
+
unmount(): void;
|
|
32
|
+
}
|
|
33
|
+
export interface WidgetRuntime {
|
|
34
|
+
version: 1;
|
|
35
|
+
origin: string;
|
|
36
|
+
mount(host: HTMLElement, config: WidgetConfig): WidgetInstance;
|
|
37
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowsoft-lab/chear-widget-react",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "치어(CHEAR) 위젯 — 플레이어 · 채팅을 리액트 컴포넌트로. <ChearPlayer channel=\"…\" />",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chear",
|
|
7
|
+
"react",
|
|
8
|
+
"live",
|
|
9
|
+
"streaming",
|
|
10
|
+
"player",
|
|
11
|
+
"chat",
|
|
12
|
+
"widget"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://www.chear.live/developers/api#widget-sdk",
|
|
15
|
+
"license": "UNLICENSED",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"pack:sdk": "pnpm build && node scripts/pack-sdk.mjs",
|
|
39
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
40
|
+
"release": "bash scripts/publish.sh"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/react": "^19.3.0",
|
|
47
|
+
"react": "^19.3.0",
|
|
48
|
+
"react-dom": "^19.3.0",
|
|
49
|
+
"vitest": "^5.0.3"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|