DODODOT/DUVIS ์ ํ๊ตฐ์์ ์ฐ๋ ์ธํฐ๋ํฐ๋ธ ๊ฐ์ด๋(Product Tour / Onboarding Walkthrough) ์ค๋ฒ๋ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ์ ํ ํ๋ฉด์ ํต์ฌ ํ๋ฆ์ ๋จ๊ณ๋ณ๋ก ๊ฐ์กฐํ๊ณ , ๋๋๋ท ๋ธ๋๋์ ํํ ๋ฆฌ์ผ ํ๋ฆ์ ๋ง์ถฐ ์ ์งํฉ๋๋ค.
- ๐ฏ
data-guide-id์์ฑ๋ง์ผ๋ก ํ๊น ์๋ฆฌ๋จผํธ ์ง์ - ๐ช ๋ฐฑ๋๋กญ + ํ์ด๋ผ์ดํธ + ํดํ ์๋ ์์น ๊ณ์ฐ
- โณ ๋น๋๊ธฐ DOM ๋ณํ ๊ฐ์ง (MutationObserver) ๋ฐ ํ์์์ ์ฒ๋ฆฌ
- โจ๏ธ ์คํฌ๋กค/๋ฆฌ์ฌ์ด์ฆ ์ ์์น ์๋ ์ฌ๊ณ์ฐ
- ๐งฉ 4๊ฐ์ง ์ก์
ํ์
์ง์ (
highlight-and-click,highlight-only,auto-click,input) - ๐ ๋ฒํผ/ํ ์คํธ UI ์ปค์คํฐ๋ง์ด์ง (
GuideUIComponents) - ๐จ LLM ์๋ต์์
```guide์ฝ๋๋ธ๋ก ์๋ ํ์ฑ (parseGuideResponse) - ๐ ์ปจํ
์ด๋ ์์ ํน์ ๋ฌธ์ฅ๋ง ํ๊ดํ ์ ๋๋ฉ์ด์
์ผ๋ก ํ์ด๋ผ์ดํธ (
testHighlight/<TestHighlight />)
- ์ค์น
- ๋น ๋ฅธ ์์
- ๋์(Action) ์ ํ ๋ช ์ธ
- ๋ฐ์ดํฐ ์คํ (Schema)
- API
- LLM ์๋ต ํ์ฑ
- ํ๊ดํ ํ์ด๋ผ์ดํธ (
testHighlight) - Chatbot (
<Chatbot />) - HTML ๋ทฐ์ด (
.crg-chatbot-html-view) - UI ์ปค์คํฐ๋ง์ด์ง & ํ ๋ง
- ๊ฐ๋ฐ
pnpm add @connect-dot/dododot-react-guide
# or
npm i @connect-dot/dododot-react-guide
# or
yarn add @connect-dot/dododot-react-guidePeer dependencies:
react >= 16.8,react-dom >= 16.8
๋ฒ๋ค๋ CSS๋ฅผ ์ฑ ์ง์
์ (์: main.tsx, _app.tsx)์์ ํ ๋ฒ๋ง ๊ฐ์ ธ์ต๋๋ค.
import "@connect-dot/dododot-react-guide/styles.css";Tailwind ๋๋ ๋ณ๋ ์ค์ ์ด ์์ด๋ ๋์ํฉ๋๋ค. ๋ชจ๋ ์ ํธ๋ฆฌํฐ ํด๋์ค๋
crg-ํ๋ฆฌํฝ์ค๋ก ์ค์ฝํ๋์ด ํธ์คํธ ์ฑ ์คํ์ผ๊ณผ ์ถฉ๋ํ์ง ์์ต๋๋ค.
import {
GuideProvider,
GuideOverlay,
GuideNotification,
} from "@connect-dot/dododot-react-guide";
export function App() {
return (
<GuideProvider>
<YourApp />
<GuideOverlay />
<GuideNotification />
</GuideProvider>
);
}๊ฐ์ด๋์ ๋์์ด ๋ ๋ชจ๋ ์๋ฆฌ๋จผํธ์ data-guide-id ์์ฑ์ ๋ถ์ฌํฉ๋๋ค.
<button data-guide-id="btn-create">์๋ก ๋ง๋ค๊ธฐ</button>
<input data-guide-id="input-search" />import { useGuide } from "@connect-dot/dododot-react-guide";
function StartTourButton() {
const { startGuide } = useGuide();
return (
<button
onClick={() =>
startGuide({
guide: {
id: "create-flow",
title: "์ ๊ท ์์ฑ ํ๋ก์ฐ",
cancelable: true,
steps: [
{
targetId: "btn-create",
action: "highlight-and-click",
message: "์ฌ๊ธฐ๋ฅผ ํด๋ฆญํด ์ ๊ท ์์ฑ ํ๋ฉด์ผ๋ก ์ด๋ํ์ธ์.",
},
],
},
})
}
>
ํํ ๋ฆฌ์ผ ์์
</button>
);
}๊ฐ ์คํ
(GuideStep)์ 4๊ฐ์ง action ์ค ํ๋๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ก์
๋ณ๋ก ์ฌ์ฉ์์๊ฒ ๋ณด์ด๋ UX์
์ง์ํ๋ ํ๋, ์งํ ํธ๋ฆฌ๊ฑฐ๊ฐ ๋ค๋ฆ
๋๋ค.
| ์ก์ | ์๊ฐ ํจ๊ณผ | ์งํ(๋ค์ ์คํ ) ํธ๋ฆฌ๊ฑฐ | ์ฃผ ์ฌ์ฉ ์ฌ๋ก |
|---|---|---|---|
highlight-and-click |
ํ์ด๋ผ์ดํธ + ํดํ + โํด๋น ์์ญ์ ํด๋ฆญํ์ธ์โ ํํธ | ์ฌ์ฉ์๊ฐ ํ๊น์ ์ง์ ํด๋ฆญ | ๋ฉ๋ด/๋ฒํผ ํด๋ฆญ ์ ๋ |
highlight-only |
ํ์ด๋ผ์ดํธ + ํดํ + โ์ด์ /๋ค์โ ๋ฒํผ | ์ฌ์ฉ์๊ฐ ๋ค์ ๋ฒํผ ํด๋ฆญ | ์ค๋ช /์๋ด๋ง ํ์ํ ๋ |
auto-click |
ํ์ด๋ผ์ดํธ + ํดํ + โ์๋์ผ๋ก ์งํ๋ฉ๋๋คโฆโ ํํธ | delay ๊ฒฝ๊ณผ ํ ์๋ ํด๋ฆญ โ ์๋ ์งํ |
๋ฐ๋ชจ/์๋ ์งํ ์๋๋ฆฌ์ค |
input |
ํ์ด๋ผ์ดํธ + ํดํ + ํ๊น ์๋ focus + โ์ด์ /์ ๋ ฅ ์๋ฃโ | ์ฌ์ฉ์๊ฐ ์ ๋ ฅ ์๋ฃ ๋ฒํผ ํด๋ฆญ | ์ ๋ ฅ ํ๋ ์๋ด |
ํ๊น ์๋ฆฌ๋จผํธ๋ฅผ ๊ฐ์กฐํ๊ณ , ์ฌ์ฉ์๊ฐ ํด๋น ์๋ฆฌ๋จผํธ๋ฅผ ํด๋ฆญํ๋ฉด ์๋์ผ๋ก ๋ค์ ์คํ ์ผ๋ก ๋์ด๊ฐ๋๋ค. ํดํ์๋ โ์ด์ /๋ค์โ ๋ฒํผ์ด ํ์๋์ง ์์ต๋๋ค(ํด๋ฆญ์ด ๊ณง ์งํ ํธ๋ฆฌ๊ฑฐ).
ํ์ ํ๋: targetId, action, message
์ ํ ํ๋: tooltipPosition, waitForTarget, scrollTo, highlightPadding
๋ฌด์๋๋ ํ๋: delay
{
"targetId": "btn-create",
"action": "highlight-and-click",
"message": "์ ๊ท ๊ณ์ฝ์ ๋ง๋ค๋ ค๋ฉด ์ด ๋ฒํผ์ ํด๋ฆญํ์ธ์.",
"tooltipPosition": "bottom",
"highlightPadding": 6
}ํ๊น ์๋ฆฌ๋จผํธ๋ฅผ ๊ฐ์กฐํ๊ณ ์ค๋ช ๋ง ๋ ธ์ถํฉ๋๋ค. ์ฌ์ฉ์๊ฐ ์ง์ โ๋ค์โ ๋ฒํผ์ ๋๋ฌ์ผ ์งํ๋ฉ๋๋ค. ์ฒซ ์คํ ์ด ์๋๋ฉด โ์ด์ โ ๋ฒํผ์ด ํจ๊ป ํ์๋ฉ๋๋ค. ๋ง์ง๋ง ์คํ ์์๋ โ๋ค์โ์ด โ์๋ฃโ๋ก ํ๊ธฐ๋ฉ๋๋ค.
ํ์ ํ๋: targetId, action, message
์ ํ ํ๋: tooltipPosition, waitForTarget, scrollTo, highlightPadding
๋ฌด์๋๋ ํ๋: delay
{
"targetId": "panel-summary",
"action": "highlight-only",
"message": "์ด ํจ๋์์ ์ค๋์ ์ฒ๋ฆฌ ์์ฝ์ ํ์ธํ ์ ์์ต๋๋ค.",
"tooltipPosition": "right",
"highlightPadding": 12
}delay(ms) ๊ฒฝ๊ณผ ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ๊น ์๋ฆฌ๋จผํธ์ .click()์ ํธ์ถํ๊ณ , ๊ณง๋ฐ๋ก ๋ค์ ์คํ
์ผ๋ก
๋์ด๊ฐ๋๋ค. ๋ฐ๋ชจ/๋
นํ/์๋ ์์ฐ ์๋๋ฆฌ์ค์ ์ ํฉํฉ๋๋ค. ํดํ์ ๋ณ๋์ ๋ฒํผ์ ๋
ธ์ถ๋์ง ์์ต๋๋ค.
ํ์ ํ๋: targetId, action, message
์ ํ ํ๋: delay(๊ธฐ๋ณธ 800ms), tooltipPosition, waitForTarget, scrollTo, highlightPadding
{
"targetId": "btn-confirm",
"action": "auto-click",
"message": "ํ์ธ ๋ฒํผ์ ์๋์ผ๋ก ๋๋ฌ๋๋ฆด๊ฒ์.",
"delay": 1200,
"tooltipPosition": "top"
}
โ ๏ธ ์๋ ํด๋ฆญ์ ์ฌ์ฉ์ ์๋ ์์ด ๋์์ด ๋ฐ์ํ๋ฏ๋ก, ๊ฒฐ์ /์ญ์ ๋ฑ ๋น๊ฐ์ญ ์ก์ ์๋ ์ฌ์ฉ์ ๊ถ์ฅํ์ง ์์ต๋๋ค.
ํ๊น ์๋ฆฌ๋จผํธ๋ฅผ ๊ฐ์กฐํ๊ณ ์๋์ผ๋ก focus() ํฉ๋๋ค. ์ฌ์ฉ์๋ ์ง์ ๊ฐ์ ์
๋ ฅํ ๋ค โ์
๋ ฅ ์๋ฃโ
๋ฒํผ์ ๋๋ฌ์ผ ์งํ๋ฉ๋๋ค. ์ฒซ ์คํ
์ด ์๋๋ฉด โ์ด์ โ ๋ฒํผ์ด ํจ๊ป ํ์๋ฉ๋๋ค.
ํ์ ํ๋: targetId, action, message
์ ํ ํ๋: tooltipPosition, delay(ํ๊น ๋ฐ๊ฒฌ ํ ํ์ด๋ผ์ดํธ/ํฌ์ปค์ค ์ง์ฐ), waitForTarget, scrollTo, highlightPadding
๋ฌด์๋๋ ํ๋: ์์
{
"targetId": "input-name",
"action": "input",
"message": "์ฌ๊ธฐ์ ์๊ธ์ ์ด๋ฆ์ ์
๋ ฅํ์ธ์.",
"tooltipPosition": "bottom"
}๐ก ํ๊น์
input,textarea, ๋๋[contenteditable]์ฒ๋ผfocus()๊ฐ ์๋ฏธ ์๋ ์๋ฆฌ๋จผํธ์ฌ์ผ ํฉ๋๋ค.
startGuide(json) ๋ฐ ```guide ์ฝ๋๋ธ๋ก์ ๋ค์ด๊ฐ๋ ์ต์์ ๊ฐ์ฒด์
๋๋ค.
interface GuideJson {
guide: GuideData;
}| ํค | ํ์ | ํ์ | ์ค๋ช |
|---|---|---|---|
guide |
GuideData |
โ | ๊ฐ์ด๋ ๋ณธ๋ฌธ |
| ํค | ํ์ | ํ์ | ๊ธฐ๋ณธ | ์ค๋ช |
|---|---|---|---|---|
id |
string |
โ | โ | ๊ฐ์ด๋ ์๋ณ์. ๋ถ์/๋๋ฒ๊น /์บ์ฑ์ ์ฌ์ฉ |
title |
string |
โ | '' |
๊ฐ์ด๋ ์ ๋ชฉ (ํ์ฌ ์ค๋ฒ๋ ์ด์๋ ๋ ธ์ถ X, ์์ UI์์ ํ์ฉ) |
cancelable |
boolean |
โ | โ | true๋ฉด ๋ฐฑ๋๋กญ ํด๋ฆญ/โ ๋ฒํผ์ผ๋ก ์ทจ์ ๊ฐ๋ฅ |
steps |
GuideStep[] |
โ | โ | 1๊ฐ ์ด์์ ์คํ ๋ฐฐ์ด (๋น ๋ฐฐ์ด ๋ถ๊ฐ) |
| ํค | ํ์ | ํ์ | ๊ธฐ๋ณธ | ์ ์ฉ ์ก์ | ์ค๋ช |
|---|---|---|---|---|---|
targetId |
string |
โ | โ | ์ ์ฒด | DOM์์ [data-guide-id="<targetId>"]๋ก ๋งค์นญ๋๋ ์๋ฆฌ๋จผํธ |
action |
GuideAction |
โ | โ | ์ ์ฒด | 'highlight-and-click' | 'highlight-only' | 'auto-click' | 'input' |
message |
string |
โ | โ | ์ ์ฒด | ํดํ ๋ณธ๋ฌธ |
tooltipPosition |
TooltipPosition |
โ | 'auto' |
์ ์ฒด | 'top' | 'bottom' | 'left' | 'right' | 'auto'. 'auto'๋ ํ๋ฉด ์ฌ๋ฐฑ์ด ๊ฐ์ฅ ํฐ ๋ฐฉํฅ ์๋ ์ ํ |
highlightPadding |
number(px) |
โ | 8 |
์ ์ฒด | ํ์ด๋ผ์ดํธ ๋ฐ์ค์ ํจ๋ฉ |
waitForTarget |
boolean |
โ | true |
์ ์ฒด | ํ๊น์ด ์์ง ์์ผ๋ฉด MutationObserver๋ก ์ต๋ 10์ด ๋๊ธฐ |
scrollTo |
boolean |
โ | true |
์ ์ฒด | ํ๊น์ด ๋ฐ๊ฒฌ๋๋ฉด scrollIntoView({behavior:'smooth', block:'center'}) ์คํ |
delay |
number(ms) |
โ | 800 |
auto-click ๋ง ์ฌ์ฉ |
์๋ ํด๋ฆญ ๋ฐ์๊น์ง ๋๊ธฐ ์๊ฐ |
| ํ๋ | highlight-and-click | highlight-only | auto-click | input |
|---|---|---|---|---|
targetId |
โ | โ | โ | โ |
action |
โ | โ | โ | โ |
message |
โ | โ | โ | โ |
tooltipPosition |
โ | โ | โ | โ |
highlightPadding |
โ | โ | โ | โ |
waitForTarget |
โ | โ | โ | โ |
scrollTo |
โ | โ | โ | โ |
delay |
โ | โ | โ | โ |
| ๋ค๋น๊ฒ์ด์ ๋ฒํผ ๋ ธ์ถ | โ | โ (์ด์ /๋ค์ยท์๋ฃ) | โ | โ (์ด์ /์ ๋ ฅ ์๋ฃ) |
๊ฐ์ด๋ ์งํ ์ํ ๋จธ์ .
idle โโถ active โโฌโโถ waiting-target โโถ active
โโโถ completed
โโโถ cancelled
| ๊ฐ | ์๋ฏธ |
|---|---|
idle |
๊ฐ์ด๋ ๋ฏธ์งํ |
active |
ํ์ฌ ์คํ ํ์ ์ค |
waiting-target |
ํ๊น DOM ๋ฑ์ฅ ๋๊ธฐ ์ค (๋ก๋ฉ ๋ค์ด์ผ๋ก๊ทธ ํ์) |
completed |
๋ชจ๋ ์คํ ์๋ฃ |
cancelled |
์ฌ์ฉ์/ํ๋ก๊ทธ๋จ์ ์ํด ์ค๋จ |
{
"guide": {
"id": "create-recipient-flow",
"title": "์ ๊ท ์๊ธ์ ๋ฑ๋ก ๊ฐ์ด๋",
"cancelable": true,
"steps": [
{
"targetId": "nav-recipient",
"action": "highlight-and-click",
"message": "์๊ธ์ ๋ฉ๋ด๋ฅผ ํด๋ฆญํด์ฃผ์ธ์.",
"tooltipPosition": "right"
},
{
"targetId": "btn-create",
"action": "highlight-and-click",
"message": "โ์ ๊ท ๋ฑ๋กโ ๋ฒํผ์ ๋๋ฌ์ฃผ์ธ์."
},
{
"targetId": "input-name",
"action": "input",
"message": "์๊ธ์ ์ด๋ฆ์ ์
๋ ฅํ์ธ์.",
"tooltipPosition": "bottom"
},
{
"targetId": "panel-summary",
"action": "highlight-only",
"message": "์
๋ ฅํ ์ ๋ณด ์์ฝ์ ํ์ธํ ์ ์์ต๋๋ค.",
"highlightPadding": 12
},
{
"targetId": "btn-confirm",
"action": "auto-click",
"message": "ํ์ธ ๋ฒํผ์ ์๋์ผ๋ก ๋๋ฌ๋๋ฆด๊ฒ์.",
"delay": 1000
}
]
}
}| Component | ์ญํ |
|---|---|
<GuideProvider components={...}> |
์ํ + UI ์ปจํ ์คํธ ์ ๊ณต. ์ฑ ๋ฃจํธ ๊ทผ์ฒ์ 1ํ ๋ง์ดํธ |
<GuideOverlay /> |
๋ฐฑ๋๋กญ ยท ํ์ด๋ผ์ดํธ ยท ํดํ ๋ ๋ (Portal) |
<GuideNotification /> |
์๋ฃ/์ทจ์ ํ ์คํธ (Portal). ui.showToast๊ฐ ์ฃผ์ด์ง๋ฉด ์์ฒด ๋ ๋ ์๋ต |
<TestHighlight /> |
๊ฐ์ด๋์ ๋ ๋ฆฝ์ ์ผ๋ก, ์ปจํ ์ด๋ ์ ํน์ ๋ฌธ์ฅ์ ํ๊ดํ ์ ๋๋ฉ์ด์ ์ผ๋ก ๊ฐ์กฐ |
const {
// ์ํ
status,
guideId,
title,
cancelable,
steps,
currentStepIndex,
currentStep,
error,
// ์ ์ด
startGuide, // (json: GuideJson) => void
advance, // () => void
goBack, // () => void
cancel, // () => void
reset, // () => void
goToStep, // (index: number) => void
setWaiting, // () => void
setFound, // () => void
// ์ปค์คํ
UI
ui, // GuideUIComponents
} = useGuide();| ์ด๋ฆ | ์๊ทธ๋์ฒ | ์ค๋ช |
|---|---|---|
validateGuideJson |
(data: unknown) => data is GuideJson |
๋ฐํ์ ์คํค๋ง ๊ฒ์ฆ |
parseGuideResponse |
(text: string) => { text: string; guide: GuideJson | null } |
LLM ์๋ต์์ ๋ณธ๋ฌธ + ๊ฐ์ด๋ JSON ๋ถ๋ฆฌ |
GUIDE_TARGETS |
Readonly<Record<string, string>> |
๊ถ์ฅ ํ๊น ID ์์ ๋ชจ์ |
testHighlight |
(options: TestHighlightOptions) => () => void |
์ปจํ ์ด๋ ์ ๋ฌธ์ฅ์ ํ๊ดํ ์ ๋๋ฉ์ด์ ์ ์ฉ. ๋ฐํ๊ฐ์ cleanup ํจ์ |
TEST_HIGHLIGHT_CLASS |
string |
testHighlight๊ฐ ์์ฑํ๋ span์ ๋ถ์ฌ๋๋ ํด๋์ค๋ช
('crg-test-highlight') |
์ฑ๋ด/LLM ์๋ต์ ```guide ์ฝ๋๋ธ๋ก์ ํฌํจ์ํค๋ฉด ๋ณธ๋ฌธ๊ณผ ๊ฐ์ด๋ JSON์ ์๋์ผ๋ก ๋ถ๋ฆฌํฉ๋๋ค.
์ํ์๋ ํ๋ฆ์ ๊ฐ์ด๋๋ก ๋ณด์ฌ๋๋ฆด๊ฒ์.
```guide
{
"guide": {
"id": "create-flow",
"cancelable": true,
"steps": [
{ "targetId": "btn-create", "action": "highlight-and-click", "message": "์ฌ๊ธฐ๋ฅผ ํด๋ฆญํ์ธ์." }
]
}
}
```import { parseGuideResponse, useGuide } from "@connect-dot/dododot-react-guide";
function ChatMessage({ raw }: { raw: string }) {
const { startGuide } = useGuide();
const { text, guide } = parseGuideResponse(raw);
return (
<>
<p>{text}</p>
{guide && <button onClick={() => startGuide(guide)}>๊ฐ์ด๋ ์์</button>}
</>
);
}parseGuideResponse๋ 1) ```guide ์ฝ๋๋ธ๋ก, 2) ๋ณธ๋ฌธ ์ ์ฒด๊ฐ JSON์ธ ๊ฒฝ์ฐ ์์ผ๋ก ์๋ํ๊ณ
์คํจ ์ { text: ์๋ณธ, guide: null }์ ๋ฐํํฉ๋๋ค.
ํน์ ์ปจํ ์ด๋ ์์์ ์ฃผ์ด์ง ๋ฌธ์ฅ ๋ชฉ๋ก๊ณผ ์ผ์นํ๋ ํ ์คํธ๋ฅผ ํ๊ดํ์ด ์ง๋๊ฐ๋ฏ์ด ์ขโ์ฐ๋ก ์ฑ์์ง๋ ์ ๋๋ฉ์ด์ ์ผ๋ก ๊ฐ์กฐํ๋ ๊ธฐ๋ฅ์ ๋๋ค. ๊ฐ์ด๋ ์ค๋ฒ๋ ์ด์ ๋ ๋ฆฝ์ ์ผ๋ก ๋์ํ๋ฏ๋ก, ๋ฌธ์/๋ฆฌ๋ทฐ ํ๋ฉด์ "์ด ๋ถ๋ถ ๋ด์ฃผ์ธ์" ์์ ์ปจํ ์คํธ ๊ฐ์กฐ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
containerId๋ก ์ง์ ํ DOM ์๋ฆฌ๋จผํธ(document.getElementById)๋ฅผ ๋ฃจํธ๋ก ์ฌ์ฉํฉ๋๋ค.- ๊ทธ ์์ ํ
์คํธ ๋
ธ๋๋ฅผ ์ํํ๋ฉฐ,
sentences๋ฐฐ์ด์ ๋ฌธ์ฅ๊ณผ ์ผ์นํ๋ ๋ถ๋ถ์<span class="crg-test-highlight">๋ก ๊ฐ์๋๋ค. - ๊ฐ์ธ์ง span์๋ CSS keyframe
crg-test-highlight-sweep์ด ์ ์ฉ๋์ด ๋ฐฐ๊ฒฝ(ํ๊ด์)์ด 0% โ 100%๋ก ์ฑ์์ง๋ sweep ์ ๋๋ฉ์ด์ ์ด ์ฌ์๋ฉ๋๋ค. - ์ฌ๋ฌ ๋งค์น๋
stagger(ms) ๊ฐ๊ฒฉ์ผ๋ก ์์ฐจ ์ฌ์๋์ด ์์ฐ์ค๋ฌ์ด ํ๋ฆ์ ๋ง๋ญ๋๋ค.
๋ช ๋ นํ API. ํธ์ถ ์ ์ฆ์ DOM์ ๋ณํํ๊ณ , ๋ฐํ๋ cleanup ํจ์๋ฅผ ํธ์ถํ๋ฉด ์์ ๋ณต๊ตฌํฉ๋๋ค.
import { testHighlight } from "@connect-dot/dododot-react-guide";
const cleanup = testHighlight({
containerId: "article-body",
sentences: ["์ค์ํ ๋ฌธ์ฅ", "ํต์ฌ ํค์๋"],
caseInsensitive: false,
duration: 800,
stagger: 120,
});
// ๋์ค์ ํ์ด๋ผ์ดํธ ์ ๊ฑฐ
cleanup();| ์ต์ | ํ์ | ํ์ | ๊ธฐ๋ณธ | ์ค๋ช |
|---|---|---|---|---|
containerId |
string |
โ | โ | ํ์ด๋ผ์ดํธ๋ฅผ ๊ฒ์ํ ์ปจํ
์ด๋ ์๋ฆฌ๋จผํธ์ id |
sentences |
string[] |
โ | โ | ๊ฐ์กฐํ ๋ฌธ์ฅ/๊ตฌ๋ฌธ ๋ชฉ๋ก (์ ๊ท์ ๋ฉํ๋ฌธ์๋ ์๋ ์ด์ค์ผ์ดํ) |
caseInsensitive |
boolean |
โ | false |
๋์๋ฌธ์ ๋ฌด์ ๋งค์นญ ์ฌ๋ถ |
duration |
number (ms) |
โ | 800 |
๊ฐ ํ์ด๋ผ์ดํธ sweep ์ ๋๋ฉ์ด์ ๊ธธ์ด |
stagger |
number (ms) |
โ | 120 |
๋งค์น ๊ฐ ์ ๋๋ฉ์ด์ ์์ ์ง์ฐ (์์ฐจ ์ฌ์) |
๋ฐํ๊ฐ์ ํญ์ ํจ์์
๋๋ค. ์ปจํ
์ด๋๊ฐ ์๊ฑฐ๋ ๋งค์น๊ฐ ์์ด๋ ์์ ํ๊ฒ ํธ์ถํ ์ ์๋ no-op ํจ์๊ฐ
๋์์ค๋ฏ๋ก useEffect์ cleanup์ผ๋ก ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
React ์ปดํฌ๋ํธ๋ก๋ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ง์ดํธ ์ ํ์ด๋ผ์ดํธ๋ฅผ ์ ์ฉํ๊ณ , ์ธ๋ง์ดํธ/props ๋ณ๊ฒฝ ์
์๋์ผ๋ก ์ ๋ฆฌํฉ๋๋ค. ๋ ๋ ๊ฒฐ๊ณผ๋ null ์ด๋ฉฐ, ๋์ ์ปจํ
์ด๋๋ ๋ณ๋๋ก ๋งํฌ์
๋์ด ์์ด์ผ ํฉ๋๋ค.
import { TestHighlight } from "@connect-dot/dododot-react-guide";
function ArticleView({ body }: { body: string }) {
return (
<>
<article id="article-body" dangerouslySetInnerHTML={{ __html: body }} />
<TestHighlight
containerId="article-body"
sentences={["์ค์ํ ๋ฌธ์ฅ", "ํต์ฌ ํค์๋"]}
caseInsensitive
duration={900}
stagger={150}
/>
</>
);
}deps์ ์์์ ์์กด์ฑ์ ๋ฃ์ด์ฃผ๋ฉด ํด๋น ๊ฐ์ด ๋ณํ ๋ ํ์ด๋ผ์ดํธ๋ฅผ ๋ค์ ์ ์ฉํฉ๋๋ค.
(์: deps={[body]} โ ๋ณธ๋ฌธ์ด ๋ฐ๋ ๋๋ง๋ค ์ฌ์คํ)
containerId๋ก ์ปจํ ์ด๋๋ฅผ ์ฐพ๊ณ , ๋ชป ์ฐพ์ผ๋ฉด no-op cleanup์ ๋ฐํํฉ๋๋ค.sentences๋ฅผ trim + ์ค๋ณต ์ ๊ฑฐ + ๊ธธ์ด ๋ด๋ฆผ์ฐจ์ ์ ๋ ฌ ํ ๋จ์ผ ์ ๊ท์์ผ๋ก ํฉ์นฉ๋๋ค. (๊ธด ๋ฌธ์ฅ์ด ์ฐ์ ๋งค์นญ๋์ด ๋ถ๋ถ ๋งค์น๋ฅผ ๊ฐ๋ฆฝ๋๋ค.)TreeWalker๋ก ์ปจํ ์ด๋ ์์ ํ ์คํธ ๋ ธ๋๋ง ์ํํฉ๋๋ค.<script>,<style>, ์ด๋ฏธ ํ์ด๋ผ์ดํธ๋ span์ ์์์ ์๋์ผ๋ก ์คํตํฉ๋๋ค.- ๋งค์น๊ฐ ์๋ ํ
์คํธ ๋
ธ๋๋ ๋งค์น ๋ถ๋ถ๋ง
<span class="crg-test-highlight">๋ก ๊ฐ์ผ document fragment๋ก ๊ต์ฒด๋ฉ๋๋ค. - ์์ฑ๋ span์๋ ์ธ๋ฑ์ค์ ๋น๋กํ
animation-delay๊ฐ ๋ถ์ฌ๋์ด sweep ์ ๋๋ฉ์ด์ ์ด ์์ฐจ ์ฌ์๋ฉ๋๋ค. - cleanup์ด ํธ์ถ๋๋ฉด ๋ชจ๋ ํ์ด๋ผ์ดํธ span์ ์๋์ ํ
์คํธ ๋
ธ๋๋ก ๋๋๋ฆฌ๊ณ
๋ถ๋ชจ ๋
ธ๋์
normalize()๋ฅผ ์ ์ฉํฉ๋๋ค.
- ํ
์คํธ ๋
ธ๋ ๋จ์ ๋งค์นญ: ํ ๋ฌธ์ฅ์ด ์ฌ๋ฌ ์๋ฆฌ๋จผํธ์ ๊ฑธ์ณ ๋ถ๋ฆฌ๋์ด ์์ผ๋ฉด(์:
<p>์ค์ํ <strong>๋ฌธ์ฅ</strong></p>) ๋งค์นญ๋์ง ์์ต๋๋ค. ๋งค์นญํ๋ ค๋ ๋ฌธ๊ตฌ๊ฐ ํ๋์ ํ ์คํธ ๋ ธ๋ ์์ ์จ์ ํ ํฌํจ๋์ด ์์ด์ผ ํฉ๋๋ค. - ์ ๊ท์ ๋ฉํ๋ฌธ์๋ ์๋ ์ด์ค์ผ์ดํ๋๋ฏ๋ก ์ผ๋ฐ ๋ฌธ์ฅ์ ๊ทธ๋๋ก ๋ฃ์ผ๋ฉด ๋ฉ๋๋ค.
- SSR ํ๊ฒฝ์์๋
document๊ฐ ์์ผ๋ฏ๋ก ์ฆ์ no-op ํจ์๋ฅผ ๋ฐํํฉ๋๋ค.
ํ๊ด์์ ๋ ๊ฐ์ CSS ๋ณ์๋ก ์กฐ์ ํ ์ ์์ต๋๋ค.
:root {
--crg-highlight: #fef08a; /* ํ๊ดํ ์ */
--crg-highlight-foreground: inherit; /* ๊ฐ์กฐ๋ ํ
์คํธ ์ (๊ธฐ๋ณธ: ์์) */
}๋ํ ์ฌ์ฉ์๊ฐ prefers-reduced-motion: reduce๋ฅผ ์ค์ ํ ๊ฒฝ์ฐ sweep ์ ๋๋ฉ์ด์
์ ์๋์ผ๋ก
๋นํ์ฑํ๋๊ณ , ๋ฐฐ๊ฒฝ์ด ์ฆ์ 100%๋ก ํ์๋ฉ๋๋ค.
๋งค๋ด์ผ/FAQ ์๋ต์ ํ๋จ ์ ๋ ฌ ๋งํ์ ์ฑํ + ์ข์ธก ํ์คํฌ๋ฆฐ ๋งค๋ด์ผ ๋ทฐ์ด๋ก ๋ณด์ฌ์ฃผ๋ ํตํฉ ์ปดํฌ๋ํธ์ ๋๋ค. ํธ์คํธ rem base์ ์ํฅ๋ฐ์ง ์๋๋ก ๋ชจ๋ ์ฌ์ด์ฆ๊ฐ px ๊ธฐ๋ฐ inline style๋ก ๊ณ ์ ๋์ด ์๊ณ , ์ปฌ๋ฌ/ํ์ดํฌ/๊ทธ๋ฆผ์๋ DODODOT ๋์์ธ ํ ํฐ์ CSS variable๋ก ๋ ธ์ถํด ํธ์คํธ์์ override ๊ฐ๋ฅํฉ๋๋ค.
import { Chatbot } from "@connect-dot/dododot-react-guide";
import diff from "html-diff-ts"; // optional, ๋ณ๊ฒฝ์ ๋ณด๊ธฐ ํ์ฑํ
<Chatbot
answers={CHATBOT_ANSWERS}
diffHtml={(previous, latest) => diff(previous, latest)}
title="์ผ์ด๋ง ๋์ฐ๋ฏธ"
subtitle="์
๋ฌด ๋งค๋ด์ผ์ ๋ํด ์ง๋ฌธํด๋ณด์ธ์."
placeholder="์: ์๊ธ์ ๋ฑ๋ก์ ์ด๋ป๊ฒ ํ๋์?"
/>;| prop | ํ์ | ํ์ | ๊ธฐ๋ณธ | ์ค๋ช |
|---|---|---|---|---|
answers |
ChatbotAnswer[] |
โ | โ | ์๋ต ๋ฐ์ดํฐ(knowledge base) |
findAnswer |
(question, answers) => ChatbotAnswer | null |
โ | defaultMatchAnswer |
์ฌ์ฉ์ ์ ์ ๋งค์ฒ. ๊ธฐ๋ณธ์ matchKeywords AND ๋งค์นญ(์ผ์ด์ค ๋ฌด์) |
diffHtml |
(previous, latest) => string |
โ | โ | HTML diff ํจ์. ์ ๊ณต ์ versions๊ฐ 2๊ฐ ์ด์์ด๋ฉด "๋ณ๊ฒฝ์ ๋ณด๊ธฐ" ํญ ๋ ธ์ถ |
title |
string |
โ | '๋์ฐ๋ฏธ' |
์ฑํ ํจ๋ ์ ๋ชฉ |
subtitle |
string |
โ | '๋ฌด์์ด๋ ๋ฌผ์ด๋ณด์ธ์.' |
์ฑํ ํจ๋ ์๋ธํ์ดํ |
placeholder |
string |
โ | '์ง๋ฌธ์ ์
๋ ฅํ์ธ์' |
์ ๋ ฅ placeholder |
emptyMessage |
string |
โ | '์ง๋ฌธ์ ์
๋ ฅํ๋ฉด ๋ต๋ณ๊ณผ ๋งค๋ด์ผ์ ๋ณด์ฌ๋๋ ค์.' |
๋ฉ์์ง๊ฐ ์์ ๋ |
notFoundMessage |
string |
โ | '์ผ์นํ๋ ๋ต๋ณ์ ์ฐพ์ง ๋ชปํ์ด์.' |
๋งค์นญ ์คํจ ์๋ต |
showSuggestions |
boolean |
โ | true |
์ฒซ ๋ฉ์์ง ์ ์ถ์ฒ ์นฉ ๋ ธ์ถ |
defaultOpen |
boolean |
โ | false |
์ด๊ธฐ ์คํ ์ํ |
className |
string |
โ | โ | ํจ๋ ์ปจํ ์ด๋ ์ถ๊ฐ className |
onAsk |
(question, answer | null) => void |
โ | โ | ์ฌ์ฉ์ ์ง๋ฌธ ์ฝ๋ฐฑ |
layout |
'auto' | 'desktop' | 'mobile' |
โ | 'auto' |
๋ ์ด์์ ๋ณํ. 'auto'๋ (max-width: 768px) ๋ฏธ๋์ด ์ฟผ๋ฆฌ๋ก ํ์ |
| ํค | ํ์ | ํ์ | ์ค๋ช |
|---|---|---|---|
id |
string |
โ | ๊ณ ์ ์๋ณ์ |
question |
string |
โ | ๋ํ ์ง๋ฌธ. suggestion chip ๋ผ๋ฒจ๊ณผ bot ์๋ต ํ ์คํธ์ ์ฌ์ฉ |
matchKeywords |
string[] |
โ | defaultMatchAnswer๊ฐ AND ๋งค์นญ์ ์ฌ์ฉ. ๋ฏธ์ง์ ์ question ์ฌ์ฉ |
highlightSentences |
string[] |
โ | ๋ทฐ์ด ๋ ๋ ํ <TestHighlight />๋ก ์๋ ๊ฐ์กฐํ ๋ฌธ์ฅ(diff ๋ชจ๋์์๋ ๋นํ์ฑ) |
versions |
ChatbotVersion[] |
โ | ๋ฒ์ 1๊ฐ ์ด์. ๋ง์ง๋ง ์์๊ฐ "์ต์ "์ผ๋ก ๊ฐ์ฃผ๋จ |
| ํค | ํ์ | ํ์ | ์ค๋ช |
|---|---|---|---|
id |
string |
โ | ๋ฒ์ ์๋ณ์ (ํญ key) |
label |
string |
โ | ํญ์ ๋
ธ์ถ๋๋ ๋ผ๋ฒจ (์: '์ด์ ๋ฒ์ ', '์ต์ ๋ฒ์ ') |
updatedAt |
string |
โ | ๋ฉํ ํ์ ์ต์ข
์์ {updatedAt} ํํ๋ก ๋
ธ์ถ |
html |
string |
โ | ๋ทฐ์ด์ dangerouslySetInnerHTML๋ก ๋ ๋๋๋ HTML |
export interface ChatbotMessage {
id: string;
role: "user" | "bot";
text: string;
historyId?: string; // bot ์๋ต์ด ๊ฐ๋ฆฌํค๋ history entry. ํด๋ฆญ ์ ๋ทฐ์ด๊ฐ ํด๋น ํญ๋ชฉ์ผ๋ก ์ ํ
notFound?: boolean;
}
export interface ChatbotHistoryEntry {
id: string;
question: string;
answer: ChatbotAnswer;
askedAt: number;
}- ์
๋ ฅ์
<textarea>: ์ค๋ฐ๊ฟ ์ง์ + ์ ๋ ฅ ๋ด์ฉ์ ๋ฐ๋ผ ๋์ด ์๋ ํ์ฅ (min-height: 44px~max-height: 160px). ๊ทธ ์ด์์์๋ ๋ด๋ถ ์คํฌ๋กค. - Enter ํค๋ก ์ ์ก, Shift+Enter๋ก ์ค๋ฐ๊ฟ. ํ๊ธ IME ์กฐํฉ ์ค(
isComposing๋๋ legacykeyCode 229)์๋ ๋ฌด์๋์ด ํ๋ณด ํ์ ์ฉ Enter๊ฐ ํผ submit์ผ๋ก ์์ง ์์ต๋๋ค. - ๋ค์ค Q&A ๋์ : ํ ์ธ์
๋ด์์ ์ง๋ฌธํ ๋๋ง๋ค
messages์history์ ํญ๋ชฉ์ด ์ถ๊ฐ๋ฉ๋๋ค. - ์ ๋ํ ๋ฒํผ: ์ฑํ
ํจ๋ ํค๋์
+ ์ ๋ํ๋ฒํผ์ด ๋ฉ์์ง๊ฐ 1๊ฐ ์ด์์ผ ๋ ๋ ธ์ถ๋ฉ๋๋ค. ํด๋ฆญ ์messages/history/ viewer ์ํ๊ฐ ์ด๊ธฐํ๋์ด ์ ์ธ์ ์ด ์์๋ฉ๋๋ค. - ๋ต๋ณ์ด ๋งค์นญ๋๋ฉด bot ๋งํ์ ์๋์ [๋ฌธ์ ๋ณด๊ธฐ] CTA ๋ฒํผ์ด ๋ ธ์ถ๋ฉ๋๋ค. ๋ฒํผ์ ํด๋ฆญํด์ผ ๋ทฐ์ด๊ฐ ์ด๋ฆฝ๋๋ค(๋ฐ์คํฌํฑ: ์ข์ธก side panel, ๋ชจ๋ฐ์ผ: BottomSheet). ๋ต๋ณ์ด ์ถ๊ฐ๋ ๋ ๋ทฐ์ด๊ฐ ์๋์ผ๋ก ์ด๋ฆฌ์ง๋ ์์ต๋๋ค. ํ์ฌ ์ด๋ ค ์๋ ์ด๋ ฅ์ CTA๋ brand ์์์ผ๋ก ๊ฐ์กฐ๋ฉ๋๋ค.
- ๋ทฐ์ด ํค๋์ ์ด๋ ฅ ์ ๋ ํฐ๋ shadcn/ui ์คํ์ผ(Radix Select)๋ก ๊ตฌํ๋๋ฉฐ, ์ต์ ์ ์ต์ ์์ผ๋ก ์ ๋ ฌ๋ฉ๋๋ค. ์ ์ง๋ฌธ์ด ์ถ๊ฐ๋๋ฉด ์๋์ผ๋ก ์ต์ ํญ๋ชฉ์ด ์ ํ๋ฉ๋๋ค(๋ทฐ์ด๋ ๋ซํ ์ํ ์ ์ง).
- ๋ทฐ์ด ๋ชจ๋๋ segmented control ํญ์ผ๋ก ๋ ๊ฐ์ง์
๋๋ค:
๋ฌธ์โ ์ ํ๋ ์ด๋ ฅ์ ์ต์ ๋ฒ์ HTML์ ๊ทธ๋๋ก ํ์ํฉ๋๋ค.์ด๋ ฅ๋ณด๊ธฐโdiffHtmlprop์ด ์ ๊ณต๋๊ณ versions โฅ 2์ผ ๋๋ง ๋ ธ์ถ๋ฉ๋๋ค. ๋ฉํ ํ์ ์ขยท์ฐ ๋ ๊ฐ์ ๋ฒ์ ์ ๋ ํฐ(shadcn Select)๊ฐ ๋ฑ์ฅํ๋ฉฐ ๊ธฐ๋ณธ๊ฐ์ ๋ง์ง๋ง ๋ ๋ฒ์ ์ ๋๋ค. ์ ํ์ ๋ฐ๊พธ๋ฉด ์ฆ์diffHtml(from.html, to.html)์ด ์ฌ๊ณ์ฐ๋์ด ๋ทฐ์ด๊ฐ ๊ฐฑ์ ๋ฉ๋๋ค. ์ ํ๋ ์ด๋ ฅ(selectedHistoryId)์ด ๋ฐ๋๋ฉด ๋ ์ ๋ ํฐ๋ ์ ๋ต๋ณ์ ๋ง์ง๋ง ๋ ๋ฒ์ ์ผ๋ก ๋ฆฌ์ ๋ฉ๋๋ค.
layout prop ('auto' | 'desktop' | 'mobile', ๊ธฐ๋ณธ 'auto')์ผ๋ก ๊ฒฐ์ ๋ฉ๋๋ค. 'auto'๋
viewport ๋๋น (max-width: 768px) ๋ฏธ๋์ด ์ฟผ๋ฆฌ๋ก ๋ฐ์คํฌํฑ/๋ชจ๋ฐ์ผ์ ํ์ ํฉ๋๋ค.
| ์์ญ | ์์น | ํฌ๊ธฐ |
|---|---|---|
| Floating button | position: fixed; right: 24px; bottom: 24px |
56ร56 ์ํ |
| ํจ๋ ์ปจํ ์ด๋ | position: fixed; top: 24px; left: 24px; right: 24px; bottom: 96px; gap: 24px |
viewport ํ-๋ธ๋ฆฌ๋ |
| ์ฑํ ํจ๋ | ์ฐ์ธก (order: 2) |
width 420px, height 100% |
| ๋ฌธ์ ๋ทฐ์ด ํจ๋ | ์ข์ธก (order: 1) |
flex: 1, height 100% โ ์ฑ๋ดยทgap ์ธ ๋๋จธ์ง ๋๋นยท๋์ด ์ ๋ถ ์ฑ์ |
๋ทฐ์ด ํจ๋์ ์ฌ์ฉ์๊ฐ bot ๋ฉ์์ง์ [๋ฌธ์ ๋ณด๊ธฐ] ๋ฒํผ์ ๋๋ฅธ ๋ค์๋ง ๋ํ๋ฉ๋๋ค(๋ซ๊ธฐ ๋ฒํผ์ผ๋ก ๋ซํ).
์ปจํ
์ด๋์ pointer-events: none / ๊ฐ ํจ๋์ pointer-events: auto ๊ฐ ์ ์ฉ๋ผ ํจ๋ ์ฌ์ด
gap ์์ญ์ผ๋ก ํธ์คํธ ํ์ด์ง ์ธํฐ๋์
์ด ํต๊ณผํฉ๋๋ค.
| ์์ญ | ์์น/ํํ | ๋น๊ณ |
|---|---|---|
| Floating button | position: fixed; right: 24px; bottom: 24px (๋์ผ) |
56ร56 ์ํ |
| ์ฑํ ํจ๋ | top: 16px / left: 16px / right: 16px / bottom: 96px |
width: 100%, flex: 1 โ ๊ฑฐ์ ํ์คํฌ๋ฆฐ |
| ๋ฌธ์ ๋ทฐ์ด | BottomSheet (@radix-ui/react-dialog ๊ธฐ๋ฐ) |
height: 85vh, ํ๋จ์์ ์ฌ๋ผ์ด๋ ์
, ๋๋๊ทธ ํธ๋ค + ๋ซ๊ธฐ |
๋ชจ๋ฐ์ผ์์๋ side panel์ด ๋ ๋๋์ง ์์ต๋๋ค. bot ๋ฉ์์ง์ [๋ฌธ์ ๋ณด๊ธฐ] ๋ฒํผ์ ํญํ๋ฉด BottomSheet๊ฐ ์ด๋ฆฌ๊ณ , ์ค๋ฒ๋ ์ด/๋ซ๊ธฐ ๋ฒํผ/Esc๋ก ๋ซํ๋๋ค.
๋ทฐ์ด ๋ณธ๋ฌธ์ ChatbotAnswer.versions[i].html ๋ฌธ์์ด์ dangerouslySetInnerHTML๋ก ๋ ๋ํ๋ฉฐ,
์ปจํ
์ด๋์ ์๋์ผ๋ก .crg-chatbot-html-view ํด๋์ค๊ฐ ๋ถ์ฌ๋ฉ๋๋ค. ์ด ํด๋์ค ์ค์ฝํ์์
DODODOT ๋์์ธ ํ ํฐ ๊ธฐ๋ฐ ํ์ดํฌยท์ปฌ๋ฌ ๊ท์น์ด ์ ์ฉ๋ฉ๋๋ค.
| ํ๊ทธ | Typography ํ ํฐ | ์ปฌ๋ฌ ํ ํฐ | ์๊ฐ (๊ธฐ๋ณธ DODODOT) |
|---|---|---|---|
h1 |
--crg-font-h1 |
--crg-foreground |
48px / 60px / 700 |
h2 |
--crg-font-h2 |
--crg-foreground |
36px / 45px / 700 |
h3 |
--crg-font-h3 |
--crg-foreground |
24px / 36px / 700 |
h4 |
--crg-font-h4 |
--crg-foreground |
21px / 32px / 700 |
p, li |
--crg-font-body |
--crg-foreground |
16px / 28px / 500 |
strong, b |
--crg-font-body-b |
--crg-foreground |
16px / 28px / 700 |
small |
--crg-font-label |
--crg-subtle |
14px / 24px / 500 |
em, i |
(inherited) | (inherited) | italic |
a |
(inherited) | --crg-brand / hover --crg-brand-hover |
underline |
code |
monospace 14px | --crg-muted-foreground on --crg-muted |
inline code chip |
blockquote |
--crg-font-body |
--crg-subtle on --crg-surface-alt |
์ข์ธก 3px border --crg-border |
hr |
โ | --crg-border |
1px top |
table, td |
--crg-font-body |
--crg-foreground |
--crg-border cells |
th |
--crg-font-label-b |
--crg-muted-foreground on --crg-muted |
ํค๋ ์ |
ol, ul |
โ | โ | margin: 14px 0; padding-left: 28px |
diffHtml ๊ฒฐ๊ณผ์ ํฌํจ๋๋ ํด๋์ค์ ์๋ ์คํ์ผ์ด ๋งคํ๋ฉ๋๋ค.
| ์ ๋ ํฐ | ์๋ฏธ | ๋ฐฐ๊ฒฝ | ํ ์คํธ |
|---|---|---|---|
ins.diffins, ins.mod |
์ถ๊ฐ | --crg-diff-ins-bg (#dcfce7) |
--crg-diff-ins-fg (#14532d) |
ins.diffmod |
๋ณ๊ฒฝ(์ถ๊ฐ ์ธก) | --crg-diff-mod-bg (#fef3c7) |
--crg-diff-mod-fg (#78350f) |
del.diffdel, del.diffmod |
์ญ์ (line-through) | --crg-diff-del-bg (#fee2e2) |
--crg-diff-del-fg (#7f1d1d) |
| ํ ํฐ (CSS var) | ๊ฐ | Tailwind class |
|---|---|---|
--crg-font-display |
700 60px/72px Pretendard |
crg-text-display |
--crg-font-h1 |
700 48px/60px Pretendard |
crg-text-h1 |
--crg-font-h2 |
700 36px/45px Pretendard |
crg-text-h2 |
--crg-font-h3 |
700 24px/36px Pretendard |
crg-text-h3 |
--crg-font-h4 |
700 21px/32px Pretendard |
crg-text-h4 |
--crg-font-body-b |
700 16px/28px Pretendard |
crg-text-body-b |
--crg-font-body |
500 16px/28px Pretendard |
crg-text-body |
--crg-font-label-b |
700 14px/24px Pretendard |
crg-text-label-b |
--crg-font-label |
500 14px/24px Pretendard |
crg-text-label |
--crg-font-subtitle-1 |
500 18px/20px Pretendard |
crg-text-subtitle-1 |
--crg-font-subtitle-2 |
500 16px/20px Pretendard |
crg-text-subtitle-2 |
--crg-font-subtitle-3 |
500 14px/20px Pretendard |
crg-text-subtitle-3 |
--crg-font-family ๊ธฐ๋ณธ๊ฐ: 'Pretendard', 'Pretendard Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif
--crg-primary-{00|10|20|30|40|50|60|70|80|90|95|98|99|100} ยท --crg-gray-{00|10|20|30|40|50|60|70|80|90|95|99|100} (DODODOT ํ๋ ํธ ๊ทธ๋๋ก)
| ํ ํฐ | ๊ธฐ๋ณธ๊ฐ | Tailwind class |
|---|---|---|
--crg-brand |
--crg-primary-60 (#2563EB) |
crg-bg-brand / crg-text-brand |
--crg-brand-foreground |
--crg-primary-100 (#FFFFFF) |
crg-text-brand-foreground |
--crg-brand-hover |
--crg-primary-50 (#1D4ED8) |
crg-bg-brand-hover |
--crg-foreground |
--crg-gray-10 (#191C1D) |
crg-text-foreground |
--crg-subtle |
--crg-gray-40 (#5C5F5F) |
crg-text-subtle |
--crg-icon |
--crg-gray-60 (#8E9192) |
crg-text-icon |
--crg-muted |
--crg-gray-95 (#EFF1F1) |
crg-bg-muted |
--crg-muted-foreground |
--crg-gray-20 (#2E3132) |
crg-text-muted-foreground |
--crg-border |
--crg-gray-90 (#E1E3E3) |
crg-border-border |
--crg-surface |
--crg-gray-100 (#FFFFFF) |
crg-bg-surface |
--crg-surface-alt |
--crg-gray-99 (#FAFAFA) |
crg-bg-surface-alt |
| ํ ํฐ | Tailwind class |
|---|---|
--crg-shadow-elevation-01 |
crg-shadow-elevation-01 |
--crg-shadow-elevation-02 |
crg-shadow-elevation-02 (alias: crg-shadow-tooltip) |
--crg-shadow-elevation-03 |
crg-shadow-elevation-03 |
--crg-shadow-elevation-04 |
crg-shadow-elevation-04 |
/* ํธ์คํธ ์ฑ์ :root์์ DODODOT ์ปฌ๋ฌ๋ฅผ ๋ค๋ฅธ ๋ธ๋๋ ์ปฌ๋ฌ๋ก ๋ฎ์ด์ฐ๊ธฐ */
:root {
--crg-brand: #4f46e5;
--crg-brand-hover: #4338ca;
/* body ํ
์คํธ๋ง ํฐํธ ๋ ํฌ๊ฒ */
--crg-font-body: 500 18px/30px var(--crg-font-family);
/* HTML ๋ทฐ์ด ๊ฐ์กฐ ์ ๋ณ๊ฒฝ */
--crg-diff-ins-bg: #e0f2fe;
--crg-diff-ins-fg: #075985;
}<GuideProvider
components={{
renderButton: ({ variant, onClick, children }) => (
<MyButton variant={variant} onClick={onClick}>
{children}
</MyButton>
),
showToast: (message, type) => toast[type](message),
}}
>
...
</GuideProvider>๋ฒ๋ค๋ styles.css๊ฐ ๋ค์ ๋ณ์๋ค์ ๋
ธ์ถํฉ๋๋ค. ํธ์คํธ ์ฑ์ :root์์ ๋ฎ์ด์ฐ๋ฉด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ์ฒด์ ์ฆ์ ๋ฐ์๋ฉ๋๋ค.
:root {
--crg-brand: #4f46e5; /* ํ์ด๋ผ์ดํธ ๋ณด๋ ยท primary ๋ฒํผ ยท ์ก์
ํํธ ์ */
--crg-brand-foreground: #ffffff; /* primary ๋ฒํผ ํ
์คํธ */
--crg-foreground: #1f2937; /* ๋ณธ๋ฌธ ํ
์คํธ */
--crg-subtle: #6b7280; /* ์นด์ดํฐ ยท ๋ณด์กฐ ํ
์คํธ ยท ์ทจ์ ํ ์คํธ ๋ฐฐ๊ฒฝ */
--crg-icon: #9ca3af; /* ๋ซ๊ธฐ(โ) ๋ฒํผ */
--crg-muted: #f3f4f6; /* secondary ๋ฒํผ ๋ฐฐ๊ฒฝ */
--crg-muted-foreground: #374151; /* secondary ๋ฒํผ ํ
์คํธ */
--crg-surface: #ffffff; /* ํดํ ยท ๋ก๋ฉ ์นด๋ ๋ฐฐ๊ฒฝ */
--crg-success: #10b981; /* ์๋ฃ ํ ์คํธ ๋ฐฐ๊ฒฝ */
--crg-backdrop-opacity: 0.55; /* ๋ฐฑ๋๋กญ ์ด๋ก๊ธฐ (0~1) */
--crg-highlight: #fef08a; /* testHighlight ํ๊ดํ ์ */
--crg-highlight-foreground: inherit; /* ํ๊ดํ์ด ์ ์ฉ๋ ํ
์คํธ ์ */
}pnpm install
pnpm build # dist/ ์ ESM + CJS + d.ts ์ฐ์ถ
pnpm dev # watch ๋ชจ๋
pnpm typecheck๋น๋ ์ฐ์ถ๋ฌผ:
dist/
โโ index.js # ESM
โโ index.cjs # CommonJS
โโ index.d.ts # types (ESM)
โโ index.d.cts # types (CJS)
โโ styles.css # Tailwind ๋ฒ๋ค (crg- ํ๋ฆฌํฝ์ค, preflight ์ ๊ฑฐ)
โโ *.map # sourcemap
MIT ยฉ connect-dot