Skip to content

connect-dot/dododot-react-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

4 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

@connect-dot/dododot-react-guide

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 />)

๋ชฉ์ฐจ

  1. ์„ค์น˜
  2. ๋น ๋ฅธ ์‹œ์ž‘
  3. ๋™์ž‘(Action) ์œ ํ˜• ๋ช…์„ธ
  4. ๋ฐ์ดํ„ฐ ์ŠคํŽ™ (Schema)
  5. API
  6. LLM ์‘๋‹ต ํŒŒ์‹ฑ
  7. ํ˜•๊ด‘ํŽœ ํ•˜์ด๋ผ์ดํŠธ (testHighlight)
  8. Chatbot (<Chatbot />)
  9. HTML ๋ทฐ์–ด (.crg-chatbot-html-view)
  10. UI ์ปค์Šคํ„ฐ๋งˆ์ด์ง• & ํ…Œ๋งˆ
  11. ๊ฐœ๋ฐœ

์„ค์น˜

pnpm add @connect-dot/dododot-react-guide
# or
npm i @connect-dot/dododot-react-guide
# or
yarn add @connect-dot/dododot-react-guide

Peer dependencies: react >= 16.8, react-dom >= 16.8


๋น ๋ฅธ ์‹œ์ž‘

1. ์Šคํƒ€์ผ ์‹œํŠธ ๋กœ๋“œ

๋ฒˆ๋“ค๋œ CSS๋ฅผ ์•ฑ ์ง„์ž…์ (์˜ˆ: main.tsx, _app.tsx)์—์„œ ํ•œ ๋ฒˆ๋งŒ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

import "@connect-dot/dododot-react-guide/styles.css";

Tailwind ๋˜๋Š” ๋ณ„๋„ ์„ค์ •์ด ์—†์–ด๋„ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋Š” crg- ํ”„๋ฆฌํ”ฝ์Šค๋กœ ์Šค์ฝ”ํ”„๋˜์–ด ํ˜ธ์ŠคํŠธ ์•ฑ ์Šคํƒ€์ผ๊ณผ ์ถฉ๋Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

2. Provider ๋“ฑ๋ก

import {
  GuideProvider,
  GuideOverlay,
  GuideNotification,
} from "@connect-dot/dododot-react-guide";

export function App() {
  return (
    <GuideProvider>
      <YourApp />
      <GuideOverlay />
      <GuideNotification />
    </GuideProvider>
  );
}

2. ๊ฐ€์ด๋“œ ํƒ€๊นƒ ๋งˆํ‚น

๊ฐ€์ด๋“œ์˜ ๋Œ€์ƒ์ด ๋  ๋ชจ๋“  ์—˜๋ฆฌ๋จผํŠธ์— data-guide-id ์†์„ฑ์„ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.

<button data-guide-id="btn-create">์ƒˆ๋กœ ๋งŒ๋“ค๊ธฐ</button>
<input data-guide-id="input-search" />

3. ๊ฐ€์ด๋“œ ์‹คํ–‰

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>
  );
}

๋™์ž‘(Action) ์œ ํ˜• ๋ช…์„ธ

๊ฐ ์Šคํ…(GuideStep)์€ 4๊ฐ€์ง€ action ์ค‘ ํ•˜๋‚˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์•ก์…˜๋ณ„๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์ด๋Š” UX์™€ ์ง€์›ํ•˜๋Š” ํ•„๋“œ, ์ง„ํ–‰ ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

์•ก์…˜ ์‹œ๊ฐ ํšจ๊ณผ ์ง„ํ–‰(๋‹ค์Œ ์Šคํ…) ํŠธ๋ฆฌ๊ฑฐ ์ฃผ ์‚ฌ์šฉ ์‚ฌ๋ก€
highlight-and-click ํ•˜์ด๋ผ์ดํŠธ + ํˆดํŒ + โ€œํ•ด๋‹น ์˜์—ญ์„ ํด๋ฆญํ•˜์„ธ์š”โ€ ํžŒํŠธ ์‚ฌ์šฉ์ž๊ฐ€ ํƒ€๊นƒ์„ ์ง์ ‘ ํด๋ฆญ ๋ฉ”๋‰ด/๋ฒ„ํŠผ ํด๋ฆญ ์œ ๋„
highlight-only ํ•˜์ด๋ผ์ดํŠธ + ํˆดํŒ + โ€œ์ด์ „/๋‹ค์Œโ€ ๋ฒ„ํŠผ ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์Œ ๋ฒ„ํŠผ ํด๋ฆญ ์„ค๋ช…/์•ˆ๋‚ด๋งŒ ํ•„์š”ํ•  ๋•Œ
auto-click ํ•˜์ด๋ผ์ดํŠธ + ํˆดํŒ + โ€œ์ž๋™์œผ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹คโ€ฆโ€ ํžŒํŠธ delay ๊ฒฝ๊ณผ ํ›„ ์ž๋™ ํด๋ฆญ โ†’ ์ž๋™ ์ง„ํ–‰ ๋ฐ๋ชจ/์ž๋™ ์ง„ํ–‰ ์‹œ๋‚˜๋ฆฌ์˜ค
input ํ•˜์ด๋ผ์ดํŠธ + ํˆดํŒ + ํƒ€๊นƒ ์ž๋™ focus + โ€œ์ด์ „/์ž…๋ ฅ ์™„๋ฃŒโ€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ ์™„๋ฃŒ ๋ฒ„ํŠผ ํด๋ฆญ ์ž…๋ ฅ ํ•„๋“œ ์•ˆ๋‚ด

1. highlight-and-click โ€” ์‚ฌ์šฉ์ž ํด๋ฆญ ์œ ๋„

ํƒ€๊นƒ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๊ฐ•์กฐํ•˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋‹ค์Œ ์Šคํ…์œผ๋กœ ๋„˜์–ด๊ฐ‘๋‹ˆ๋‹ค. ํˆดํŒ์—๋Š” โ€œ์ด์ „/๋‹ค์Œโ€ ๋ฒ„ํŠผ์ด ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค(ํด๋ฆญ์ด ๊ณง ์ง„ํ–‰ ํŠธ๋ฆฌ๊ฑฐ).

ํ•„์ˆ˜ ํ•„๋“œ: targetId, action, message ์„ ํƒ ํ•„๋“œ: tooltipPosition, waitForTarget, scrollTo, highlightPadding ๋ฌด์‹œ๋˜๋Š” ํ•„๋“œ: delay

{
  "targetId": "btn-create",
  "action": "highlight-and-click",
  "message": "์‹ ๊ทœ ๊ณ„์•ฝ์„ ๋งŒ๋“ค๋ ค๋ฉด ์ด ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”.",
  "tooltipPosition": "bottom",
  "highlightPadding": 6
}

2. highlight-only โ€” ์„ค๋ช…๋งŒ ๋…ธ์ถœ

ํƒ€๊นƒ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๊ฐ•์กฐํ•˜๊ณ  ์„ค๋ช…๋งŒ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ โ€œ๋‹ค์Œโ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์•ผ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค. ์ฒซ ์Šคํ…์ด ์•„๋‹ˆ๋ฉด โ€œ์ด์ „โ€ ๋ฒ„ํŠผ์ด ํ•จ๊ป˜ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰ ์Šคํ…์—์„œ๋Š” โ€œ๋‹ค์Œโ€์ด โ€œ์™„๋ฃŒโ€๋กœ ํ‘œ๊ธฐ๋ฉ๋‹ˆ๋‹ค.

ํ•„์ˆ˜ ํ•„๋“œ: targetId, action, message ์„ ํƒ ํ•„๋“œ: tooltipPosition, waitForTarget, scrollTo, highlightPadding ๋ฌด์‹œ๋˜๋Š” ํ•„๋“œ: delay

{
  "targetId": "panel-summary",
  "action": "highlight-only",
  "message": "์ด ํŒจ๋„์—์„œ ์˜ค๋Š˜์˜ ์ฒ˜๋ฆฌ ์š”์•ฝ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
  "tooltipPosition": "right",
  "highlightPadding": 12
}

3. auto-click โ€” ์ž๋™ ํด๋ฆญ

delay(ms) ๊ฒฝ๊ณผ ํ›„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํƒ€๊นƒ ์—˜๋ฆฌ๋จผํŠธ์˜ .click()์„ ํ˜ธ์ถœํ•˜๊ณ , ๊ณง๋ฐ”๋กœ ๋‹ค์Œ ์Šคํ…์œผ๋กœ ๋„˜์–ด๊ฐ‘๋‹ˆ๋‹ค. ๋ฐ๋ชจ/๋…นํ™”/์ž๋™ ์‹œ์—ฐ ์‹œ๋‚˜๋ฆฌ์˜ค์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ํˆดํŒ์— ๋ณ„๋„์˜ ๋ฒ„ํŠผ์€ ๋…ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ•„์ˆ˜ ํ•„๋“œ: targetId, action, message ์„ ํƒ ํ•„๋“œ: delay(๊ธฐ๋ณธ 800ms), tooltipPosition, waitForTarget, scrollTo, highlightPadding

{
  "targetId": "btn-confirm",
  "action": "auto-click",
  "message": "ํ™•์ธ ๋ฒ„ํŠผ์„ ์ž๋™์œผ๋กœ ๋ˆŒ๋Ÿฌ๋“œ๋ฆด๊ฒŒ์š”.",
  "delay": 1200,
  "tooltipPosition": "top"
}

โš ๏ธ ์ž๋™ ํด๋ฆญ์€ ์‚ฌ์šฉ์ž ์˜๋„ ์—†์ด ๋™์ž‘์ด ๋ฐœ์ƒํ•˜๋ฏ€๋กœ, ๊ฒฐ์ œ/์‚ญ์ œ ๋“ฑ ๋น„๊ฐ€์—ญ ์•ก์…˜์—๋Š” ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

4. input โ€” ์ž…๋ ฅ ์œ ๋„

ํƒ€๊นƒ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๊ฐ•์กฐํ•˜๊ณ  ์ž๋™์œผ๋กœ focus() ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์ง์ ‘ ๊ฐ’์„ ์ž…๋ ฅํ•œ ๋’ค โ€œ์ž…๋ ฅ ์™„๋ฃŒโ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์•ผ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค. ์ฒซ ์Šคํ…์ด ์•„๋‹ˆ๋ฉด โ€œ์ด์ „โ€ ๋ฒ„ํŠผ์ด ํ•จ๊ป˜ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

ํ•„์ˆ˜ ํ•„๋“œ: targetId, action, message ์„ ํƒ ํ•„๋“œ: tooltipPosition, delay(ํƒ€๊นƒ ๋ฐœ๊ฒฌ ํ›„ ํ•˜์ด๋ผ์ดํŠธ/ํฌ์ปค์Šค ์ง€์—ฐ), waitForTarget, scrollTo, highlightPadding ๋ฌด์‹œ๋˜๋Š” ํ•„๋“œ: ์—†์Œ

{
  "targetId": "input-name",
  "action": "input",
  "message": "์—ฌ๊ธฐ์— ์ˆ˜๊ธ‰์ž ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”.",
  "tooltipPosition": "bottom"
}

๐Ÿ’ก ํƒ€๊นƒ์€ input, textarea, ๋˜๋Š” [contenteditable] ์ฒ˜๋Ÿผ focus()๊ฐ€ ์˜๋ฏธ ์žˆ๋Š” ์—˜๋ฆฌ๋จผํŠธ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๋ฐ์ดํ„ฐ ์ŠคํŽ™ (Schema)

GuideJson (์ตœ์ƒ์œ„ ํŽ˜์ด๋กœ๋“œ)

startGuide(json) ๋ฐ ```guide ์ฝ”๋“œ๋ธ”๋ก์— ๋“ค์–ด๊ฐ€๋Š” ์ตœ์ƒ์œ„ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.

interface GuideJson {
  guide: GuideData;
}
ํ‚ค ํƒ€์ž… ํ•„์ˆ˜ ์„ค๋ช…
guide GuideData โœ… ๊ฐ€์ด๋“œ ๋ณธ๋ฌธ

GuideData

ํ‚ค ํƒ€์ž… ํ•„์ˆ˜ ๊ธฐ๋ณธ ์„ค๋ช…
id string โœ… โ€” ๊ฐ€์ด๋“œ ์‹๋ณ„์ž. ๋ถ„์„/๋””๋ฒ„๊น…/์บ์‹ฑ์— ์‚ฌ์šฉ
title string โŒ '' ๊ฐ€์ด๋“œ ์ œ๋ชฉ (ํ˜„์žฌ ์˜ค๋ฒ„๋ ˆ์ด์—๋Š” ๋…ธ์ถœ X, ์ƒ์œ„ UI์—์„œ ํ™œ์šฉ)
cancelable boolean โœ… โ€” true๋ฉด ๋ฐฑ๋“œ๋กญ ํด๋ฆญ/โœ• ๋ฒ„ํŠผ์œผ๋กœ ์ทจ์†Œ ๊ฐ€๋Šฅ
steps GuideStep[] โœ… โ€” 1๊ฐœ ์ด์ƒ์˜ ์Šคํ… ๋ฐฐ์—ด (๋นˆ ๋ฐฐ์—ด ๋ถˆ๊ฐ€)

GuideStep

ํ‚ค ํƒ€์ž… ํ•„์ˆ˜ ๊ธฐ๋ณธ ์ ์šฉ ์•ก์…˜ ์„ค๋ช…
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 โ€” โ€” โœ… โ€”
๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฒ„ํŠผ ๋…ธ์ถœ โŒ โœ… (์ด์ „/๋‹ค์Œยท์™„๋ฃŒ) โŒ โœ… (์ด์ „/์ž…๋ ฅ ์™„๋ฃŒ)

GuideStatus

๊ฐ€์ด๋“œ ์ง„ํ–‰ ์ƒํƒœ ๋จธ์‹ .

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
      }
    ]
  }
}

API

Components

Component ์—ญํ• 
<GuideProvider components={...}> ์ƒํƒœ + UI ์ปจํ…์ŠคํŠธ ์ œ๊ณต. ์•ฑ ๋ฃจํŠธ ๊ทผ์ฒ˜์— 1ํšŒ ๋งˆ์šดํŠธ
<GuideOverlay /> ๋ฐฑ๋“œ๋กญ ยท ํ•˜์ด๋ผ์ดํŠธ ยท ํˆดํŒ ๋ Œ๋” (Portal)
<GuideNotification /> ์™„๋ฃŒ/์ทจ์†Œ ํ† ์ŠคํŠธ (Portal). ui.showToast๊ฐ€ ์ฃผ์–ด์ง€๋ฉด ์ž์ฒด ๋ Œ๋” ์ƒ๋žต
<TestHighlight /> ๊ฐ€์ด๋“œ์™€ ๋…๋ฆฝ์ ์œผ๋กœ, ์ปจํ…Œ์ด๋„ˆ ์•ˆ ํŠน์ • ๋ฌธ์žฅ์„ ํ˜•๊ด‘ํŽœ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๊ฐ•์กฐ

Hook: useGuide()

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();

Utilities

์ด๋ฆ„ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์„ค๋ช…
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 ์‘๋‹ต ํŒŒ์‹ฑ

์ฑ—๋ด‡/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 }์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.


ํ˜•๊ด‘ํŽœ ํ•˜์ด๋ผ์ดํŠธ (testHighlight)

ํŠน์ • ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ์ฃผ์–ด์ง„ ๋ฌธ์žฅ ๋ชฉ๋ก๊ณผ ์ผ์น˜ํ•˜๋Š” ํ…์ŠคํŠธ๋ฅผ ํ˜•๊ด‘ํŽœ์ด ์ง€๋‚˜๊ฐ€๋“ฏ์ด ์ขŒโ†’์šฐ๋กœ ์ฑ„์›Œ์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๊ฐ•์กฐํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๊ฐ€์ด๋“œ ์˜ค๋ฒ„๋ ˆ์ด์™€ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋ฏ€๋กœ, ๋ฌธ์„œ/๋ฆฌ๋ทฐ ํ™”๋ฉด์— "์ด ๋ถ€๋ถ„ ๋ด์ฃผ์„ธ์š”" ์‹์˜ ์ปจํ…์ŠคํŠธ ๊ฐ•์กฐ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ปจ์…‰

  • containerId๋กœ ์ง€์ •ํ•œ DOM ์—˜๋ฆฌ๋จผํŠธ(document.getElementById)๋ฅผ ๋ฃจํŠธ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ๊ทธ ์•ˆ์˜ ํ…์ŠคํŠธ ๋…ธ๋“œ๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ, sentences ๋ฐฐ์—ด์˜ ๋ฌธ์žฅ๊ณผ ์ผ์น˜ํ•˜๋Š” ๋ถ€๋ถ„์„ <span class="crg-test-highlight">๋กœ ๊ฐ์Œ‰๋‹ˆ๋‹ค.
  • ๊ฐ์‹ธ์ง„ span์—๋Š” CSS keyframe crg-test-highlight-sweep ์ด ์ ์šฉ๋˜์–ด ๋ฐฐ๊ฒฝ(ํ˜•๊ด‘์ƒ‰)์ด 0% โ†’ 100%๋กœ ์ฑ„์›Œ์ง€๋Š” sweep ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์žฌ์ƒ๋ฉ๋‹ˆ๋‹ค.
  • ์—ฌ๋Ÿฌ ๋งค์น˜๋Š” stagger(ms) ๊ฐ„๊ฒฉ์œผ๋กœ ์ˆœ์ฐจ ์žฌ์ƒ๋˜์–ด ์ž์—ฐ์Šค๋Ÿฌ์šด ํ๋ฆ„์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

API

testHighlight(options): () => void

๋ช…๋ นํ˜• 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์œผ๋กœ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

<TestHighlight /> โ€” ์„ ์–ธํ˜• ๋ž˜ํผ

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]} โ€” ๋ณธ๋ฌธ์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์žฌ์‹คํ–‰)

๋™์ž‘ ํ๋ฆ„

  1. containerId๋กœ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ฐพ๊ณ , ๋ชป ์ฐพ์œผ๋ฉด no-op cleanup์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  2. sentences๋ฅผ trim + ์ค‘๋ณต ์ œ๊ฑฐ + ๊ธธ์ด ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ํ›„ ๋‹จ์ผ ์ •๊ทœ์‹์œผ๋กœ ํ•ฉ์นฉ๋‹ˆ๋‹ค. (๊ธด ๋ฌธ์žฅ์ด ์šฐ์„  ๋งค์นญ๋˜์–ด ๋ถ€๋ถ„ ๋งค์น˜๋ฅผ ๊ฐ€๋ฆฝ๋‹ˆ๋‹ค.)
  3. TreeWalker๋กœ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์˜ ํ…์ŠคํŠธ ๋…ธ๋“œ๋งŒ ์ˆœํšŒํ•ฉ๋‹ˆ๋‹ค. <script>, <style>, ์ด๋ฏธ ํ•˜์ด๋ผ์ดํŠธ๋œ span์˜ ์ž์‹์€ ์ž๋™์œผ๋กœ ์Šคํ‚ตํ•ฉ๋‹ˆ๋‹ค.
  4. ๋งค์น˜๊ฐ€ ์žˆ๋Š” ํ…์ŠคํŠธ ๋…ธ๋“œ๋Š” ๋งค์น˜ ๋ถ€๋ถ„๋งŒ <span class="crg-test-highlight">๋กœ ๊ฐ์‹ผ document fragment๋กœ ๊ต์ฒด๋ฉ๋‹ˆ๋‹ค.
  5. ์ƒ์„ฑ๋œ span์—๋Š” ์ธ๋ฑ์Šค์— ๋น„๋ก€ํ•œ animation-delay๊ฐ€ ๋ถ€์—ฌ๋˜์–ด sweep ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ˆœ์ฐจ ์žฌ์ƒ๋ฉ๋‹ˆ๋‹ค.
  6. 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%๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.


Chatbot (<Chatbot />)

๋งค๋‰ด์–ผ/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="์˜ˆ: ์ˆ˜๊ธ‰์ž ๋“ฑ๋ก์€ ์–ด๋–ป๊ฒŒ ํ•˜๋‚˜์š”?"
/>;

Props

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) ๋ฏธ๋””์–ด ์ฟผ๋ฆฌ๋กœ ํŒ์ •

๋ฐ์ดํ„ฐ ์ŠคํŽ™

ChatbotAnswer

ํ‚ค ํƒ€์ž… ํ•„์ˆ˜ ์„ค๋ช…
id string โœ… ๊ณ ์œ  ์‹๋ณ„์ž
question string โœ… ๋Œ€ํ‘œ ์งˆ๋ฌธ. suggestion chip ๋ผ๋ฒจ๊ณผ bot ์‘๋‹ต ํ…์ŠคํŠธ์— ์‚ฌ์šฉ
matchKeywords string[] โŒ defaultMatchAnswer๊ฐ€ AND ๋งค์นญ์— ์‚ฌ์šฉ. ๋ฏธ์ง€์ • ์‹œ question ์‚ฌ์šฉ
highlightSentences string[] โŒ ๋ทฐ์–ด ๋ Œ๋” ํ›„ <TestHighlight />๋กœ ์ž๋™ ๊ฐ•์กฐํ•  ๋ฌธ์žฅ(diff ๋ชจ๋“œ์—์„œ๋Š” ๋น„ํ™œ์„ฑ)
versions ChatbotVersion[] โœ… ๋ฒ„์ „ 1๊ฐœ ์ด์ƒ. ๋งˆ์ง€๋ง‰ ์›์†Œ๊ฐ€ "์ตœ์‹ "์œผ๋กœ ๊ฐ„์ฃผ๋จ

ChatbotVersion

ํ‚ค ํƒ€์ž… ํ•„์ˆ˜ ์„ค๋ช…
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 ๋˜๋Š” legacy keyCode 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์„ ๊ทธ๋Œ€๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
    • ์ด๋ ฅ๋ณด๊ธฐ โ€” diffHtml prop์ด ์ œ๊ณต๋˜๊ณ  versions โ‰ฅ 2์ผ ๋•Œ๋งŒ ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๋ฉ”ํƒ€ ํ–‰์— ์ขŒยท์šฐ ๋‘ ๊ฐœ์˜ ๋ฒ„์ „ ์…€๋ ‰ํ„ฐ(shadcn Select)๊ฐ€ ๋“ฑ์žฅํ•˜๋ฉฐ ๊ธฐ๋ณธ๊ฐ’์€ ๋งˆ์ง€๋ง‰ ๋‘ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ์„ ํƒ์„ ๋ฐ”๊พธ๋ฉด ์ฆ‰์‹œ diffHtml(from.html, to.html)์ด ์žฌ๊ณ„์‚ฐ๋˜์–ด ๋ทฐ์–ด๊ฐ€ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค. ์„ ํƒ๋œ ์ด๋ ฅ(selectedHistoryId)์ด ๋ฐ”๋€Œ๋ฉด ๋‘ ์…€๋ ‰ํ„ฐ๋Š” ์ƒˆ ๋‹ต๋ณ€์˜ ๋งˆ์ง€๋ง‰ ๋‘ ๋ฒ„์ „์œผ๋กœ ๋ฆฌ์…‹๋ฉ๋‹ˆ๋‹ค.

๋ ˆ์ด์•„์›ƒ

layout prop ('auto' | 'desktop' | 'mobile', ๊ธฐ๋ณธ 'auto')์œผ๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค. 'auto'๋Š” viewport ๋„ˆ๋น„ (max-width: 768px) ๋ฏธ๋””์–ด ์ฟผ๋ฆฌ๋กœ ๋ฐ์Šคํฌํ†ฑ/๋ชจ๋ฐ”์ผ์„ ํŒ์ •ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ์Šคํฌํ†ฑ ('desktop' ๋˜๋Š” auto + > 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 ์˜์—ญ์œผ๋กœ ํ˜ธ์ŠคํŠธ ํŽ˜์ด์ง€ ์ธํ„ฐ๋ž™์…˜์ด ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋ฐ”์ผ ('mobile' ๋˜๋Š” auto + โ‰ค 768px)

์˜์—ญ ์œ„์น˜/ํ˜•ํƒœ ๋น„๊ณ 
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๋กœ ๋‹ซํž™๋‹ˆ๋‹ค.


HTML ๋ทฐ์–ด (.crg-chatbot-html-view)

๋ทฐ์–ด ๋ณธ๋ฌธ์€ 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

Diff ๋งˆ์ปค ํด๋ž˜์Šค (html-diff-ts ํ˜ธํ™˜)

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)

๋””์ž์ธ ํ† ํฐ ์ „์ฒด ๋ชฉ๋ก

Typography

ํ† ํฐ (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

Color โ€” Palette

--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 ํŒ”๋ ˆํŠธ ๊ทธ๋Œ€๋กœ)

Color โ€” Semantic

ํ† ํฐ ๊ธฐ๋ณธ๊ฐ’ 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

Elevation

ํ† ํฐ 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;
}

UI ์ปค์Šคํ„ฐ๋งˆ์ด์ง• & ํ…Œ๋งˆ

๋ฒ„ํŠผ/ํ† ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ ๊ต์ฒด

<GuideProvider
  components={{
    renderButton: ({ variant, onClick, children }) => (
      <MyButton variant={variant} onClick={onClick}>
        {children}
      </MyButton>
    ),
    showToast: (message, type) => toast[type](message),
  }}
>
  ...
</GuideProvider>

CSS ๋ณ€์ˆ˜๋กœ ํ…Œ๋งˆ ์กฐ์ •

๋ฒˆ๋“ค๋œ 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

About

No description, website, or topics provided.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors