Communitygithub.com

UI 데모

Playwright를 사용하여 세련된 UI 데모 비디오를 녹화합니다. 사용자가 웹 애플리케이션의 데모, 둘러보기, 화면 녹화 또는 튜토리얼 비디오를 요청할 때 사용하세요. 보이는 커서, 자연스러운 페이싱, 전문적인 느낌의 WebM 비디오를 생성합니다.

UI 데모란 무엇인가요?

이 스킬은 Playwright의 비디오 녹화 기능을 활용하여 웹 애플리케이션의 고품질 데모 비디오를 제작하는 AI 에이전트용 도구입니다. 발견(Discover), 리허설(Rehearse), 녹화(Record)의 3단계 프로세스를 통해 안정적이고 전문적인 결과물을 보장합니다. 발견 단계에서는 대상 페이지의 실제 상호작용 요소(입력 필드, 선택 상자, 버튼 등)를 분석하여 정확한 셀렉터를 파악하고, 리허설 단계에서 모든 셀렉터의 가시성을 검증하며, 녹화 단계에서만 실제 영상을 생성합니다. SVG 화살표 커서 오버레이를 주입하여 마우스 움직임을 시각화하고, 자연스러운 페이싱(타이핑 지연 25-40ms, 클릭 후 대기 시간 등), 부드러운 스크롤, 그리고 자막 바를 지원합니다. Claude Code, Cursor, Codex 등 다양한 AI 코딩 에이전트 플랫폼에서 사용할 수 있으며, 기본적으로 1280x720 해상도의 WebM 비디오를 출력합니다.

지원 대상~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/ui-demo

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

UI 데모 비디오 레코더

Playwright의 비디오 녹화 기능과 주입된 커서 오버레이, 자연스러운 페이싱 및 스토리텔링 구성을 사용하여 웹 애플리케이션의 세련된 데모 비디오를 녹화합니다.

사용 시기

  • 사용자가 "데모 비디오", "화면 녹화", "둘러보기", 또는 "튜토리얼"을 요청할 때
  • 사용자가 기능이나 워크플로우를 시각적으로 선보이고 싶을 때
  • 사용자가 문서화, 온보딩 또는 이해관계자 프레젠테이션을 위한 비디오가 필요할 때

3단계 프로세스

모든 데모는 발견(Discover) -> 리허설(Rehearse) -> 녹화(Record) 세 단계를 거칩니다. 절대 바로 녹화 단계로 건너뛰지 마세요.


1단계: 발견

스크립트를 작성하기 전에 먼저 대상 페이지를 탐색하여 실제로 무엇이 있는지 이해합니다.

이유

직접 보지 않은 것에 대해 스크립트를 작성할 수 없습니다. 필드는 <textarea>가 아닌 <input>일 수 있고, 드롭다운은 <select>가 아닌 커스텀 컴포넌트일 수 있으며, 댓글 상자는 @멘션이나 #태그를 지원할 수 있습니다. 가정은 조용히 녹화를 망칩니다.

방법

흐름에 있는 각 페이지로 이동하여 대화형 요소를 덤프합니다:

// 데모 스크립트를 작성하기 전에 흐름의 각 페이지에 대해 실행
const fields = await page.evaluate(() => {
  const els = [];
  document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
    if (el.offsetParent !== null) {
      els.push({
        tag: el.tagName,
        type: el.type || '',
        name: el.name || '',
        placeholder: el.placeholder || '',
        text: el.textContent?.trim().substring(0, 40) || '',
        contentEditable: el.contentEditable === 'true',
        role: el.getAttribute('role') || '',
      });
    }
  });
  return els;
});
console.log(JSON.stringify(fields, null, 2));

확인할 사항

  • 양식 필드: <select>, <input>, 커스텀 드롭다운 또는 콤보박스인가요?
  • 선택 옵션: 옵션 값과 텍스트를 모두 덤프합니다. 플레이스홀더는 종종 value="0" 또는 value="" 값을 가지며 비어 있지 않아 보입니다. Array.from(el.options).map(o => ({ value: o.value, text: o.text }))를 사용하세요. 텍스트에 "선택"이 포함되거나 값이 "0"인 옵션은 건너뜁니다.
  • 리치 텍스트: 댓글 상자가 @멘션, #태그, 마크다운 또는 이모지를 지원하나요? 플레이스홀더 텍스트를 확인하세요.
  • 필수 필드: 어떤 필드가 양식 제출을 차단하나요? required, 레이블의 *를 확인하고 빈 상태로 제출을 시도하여 유효성 검사 오류를 확인하세요.
  • 동적 콘텐츠: 다른 필드를 채운 후에 필드가 나타나나요?
  • 버튼 레이블: "제출", "요청 제출" 또는 "보내기"와 같은 정확한 텍스트.
  • 테이블 열 헤더: 테이블 기반 모달의 경우, 각 input[type="number"]를 해당 열 헤더에 매핑하고 모든 숫자 입력이 동일한 것을 의미한다고 가정하지 마세요.

결과물

각 페이지에 대한 필드 맵으로, 스크립트에 올바른 셀렉터를 작성하는 데 사용됩니다. 예시:

/purchase-requests/new:
  - 예산 코드: <select> (페이지의 첫 번째 select, 4개 옵션)
  - 희망 배송일: <input type="date">
  - 컨텍스트: <textarea> (input 아님)
  - BOM 테이블: 인라인 편집 가능한 셀, span.cursor-pointer -> input 패턴
  - 제출: <button> text="제출"

/purchase-requests/N (세부 정보):
  - 댓글: <input placeholder="메시지를 입력하세요..."> @user 및 #PR 태그 지원
  - 보내기: <button> text="보내기" (입력 내용이 있을 때까지 비활성화)

2단계: 리허설

녹화 없이 모든 단계를 실행합니다. 모든 셀렉터가 확인되는지 검증합니다.

이유

조용한 셀렉터 실패는 데모 녹화가 깨지는 주된 이유입니다. 리허설은 녹화 시간을 낭비하기 전에 이를 포착합니다.

방법

ensureVisible을 사용하세요. 로그를 기록하고 실패 시 명확히 알리는 래퍼입니다:

async function ensureVisible(page, locator, label) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    const msg = `리허설 실패: "${label}"을(를) 찾을 수 없음 - 셀렉터: ${typeof locator === 'string' ? locator : '(locator object)'}`;
    console.error(msg);
    const found = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
        .filter(el => el.offsetParent !== null)
        .map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
        .join('\n  ');
    });
    console.error('  보이는 요소들:\n  ' + found);
    return false;
  }
  console.log(`리허설 성공: "${label}"`);
  return true;
}

리허설 스크립트 구조

const steps = [
  { label: '로그인 이메일 필드', selector: '#email' },
  { label: '로그인 제출', selector: 'button[type="submit"]' },
  { label: '새 요청 버튼', selector: 'button:has-text("새 요청")' },
  { label: '예산 코드 선택', selector: 'select' },
  { label: '배송일', selector: 'input[type="date"]:visible' },
  { label: '설명 필드', selector: 'textarea:visible' },
  { label: '항목 추가 버튼', selector: 'button:has-text("항목 추가")' },
  { label: '제출 버튼', selector: 'button:has-text("제출")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('리허설 실패 - 녹화 전에 셀렉터 수정');
  process.exit(1);
}
console.log('리허설 통과 - 모든 셀렉터 확인됨');

리허설 실패 시

  1. 보이는 요소 덤프를 읽습니다.
  2. 올바른 셀렉터를 찾습니다.
  3. 스크립트를 업데이트합니다.
  4. 리허설을 다시 실행합니다.
  5. 모든 셀렉터가 통과할 때만 진행합니다.

3단계: 녹화

발견 및 리허설이 통과한 후에만 녹화를 만듭니다.

녹화 원칙

1. 스토리텔링 흐름

비디오를 이야기로 계획하세요. 사용자가 지정한 순서를 따르거나 다음 기본 순서를 사용하세요:

  • 진입: 로그인하거나 시작점으로 이동합니다.
  • 컨텍스트: 주변을 둘러보며 시청자가 방향을 잡을 수 있도록 합니다.
  • 액션: 주요 워크플로우 단계를 수행합니다.
  • 변형: 설정, 테마 또는 현지화와 같은 보조 기능을 보여줍니다.
  • 결과: 결과, 확인 또는 새 상태를 보여줍니다.

2. 페이싱

  • 로그인 후: 4초
  • 탐색 후: 3초
  • 버튼 클릭 후: 2초
  • 주요 단계 사이: 1.5-2초
  • 최종 작업 후: 3초
  • 입력 지연: 문자당 25-40ms

3. 커서 오버레이

마우스 움직임을 따라가는 SVG 화살표 커서를 주입합니다:

async function injectCursor(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-cursor')) return;
    const cursor = document.createElement('div');
    cursor.id = 'demo-cursor';
    cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
    </svg>`;
    cursor.style.cssText = `
      position: fixed; z-index: 999999; pointer-events: none;
      width: 24px; height: 24px;
      transition: left 0.1s, top 0.1s;
      filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
    `;
    cursor.style.left = '0px';
    cursor.style.top = '0px';
    document.body.appendChild(cursor);
    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });
  });
}

오버레이는 탐색 시 파괴되므로 모든 페이지 탐색 후 injectCursor(page)를 호출하세요.

4. 마우스 움직임

절대 커서를 순간이동시키지 마세요. 클릭하기 전에 대상으로 이동하세요:

async function moveAndClick(page, locator, label, opts = {}) {
  const { postClickDelay = 800, ...clickOpts } = opts;
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`경고: moveAndClick 건너뜀 - "${label}"을(를) 찾을 수 없음`);
    return false;
  }
  try {
    await el.scrollIntoViewIfNeeded();
    await page.waitForTimeout(300);
    const box = await el.boundingBox();
    if (box) {
      await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
      await page.waitForTimeout(400);
    }
    await el.click(clickOpts);
  } catch (e) {
    console.error(`경고: moveAndClick 실패 - "${label}": ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

모든 호출에는 디버깅을 위한 설명적인 label을 포함하세요.

5. 입력

즉시 채우지 않고 눈에 보이게 입력하세요:

async function typeSlowly(page, locator, text, label, charDelay = 35) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`경고: typeSlowly 건너뜀 - "${label}"을(를) 찾을 수 없음`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. 스크롤

점프 대신 부드러운 스크롤을 사용하세요:

await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
await page.waitForTimeout(1500);

7. 대시보드 패닝

대시보드 또는 개요 페이지를 표시할 때 주요 요소 위로 커서를 이동하세요:

async function panElements(page, selector, maxCount = 6) {
  const elements = await page.locator(selector).all();
  for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
    try {
      const box = await elements[i].boundingBox();
      if (box && box.y < 700) {
        await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
        await page.waitForTimeout(600);
      }
    } catch (e) {
      console.warn(`경고: panElements 요소 ${i} 건너뜀 (셀렉터: "${selector}"): ${e.message}`);
    }
  }
}

8. 자막

뷰포트 하단에 자막 막대를 주입합니다:

async function injectSubtitleBar(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-subtitle')) return;
    const bar = document.createElement('div');
    bar.id = 'demo-subtitle';
    bar.style.cssText = `
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
      text-align: center; padding: 12px 24px;
      background: rgba(0, 0, 0, 0.75);
      color: white; font-family: -apple-system, "Segoe UI", sans-serif;
      font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
      transition: opacity 0.3s;
      pointer-events: none;
    `;
    bar.textContent = '';
    bar.style.opacity = '0';
    document.body.appendChild(bar);
  });
}

async function showSubtitle(page, text) {
  await page.evaluate((t) => {
    const bar = document.getElementById('demo-subtitle');
    if (!bar) return;
    if (t) {
      bar.textContent = t;
      bar.style.opacity = '1';
    } else {
      bar.style.opacity = '0';
    }
  }, text);
  if (text) await page.waitForTimeout(800);
}

탐색할 때마다 injectCursor(page)와 함께 injectSubtitleBar(page)를 호출하세요.

사용 패턴:

await showSubtitle(page, '1단계 - 로그인 중');
await showSubtitle(page, '2단계 - 대시보드 개요');
await showSubtitle(page, '');

지침:

  • 자막 텍스트는 짧게, 이상적으로는 60자 미만으로 유지하세요.
  • 일관성을 위해 N단계 - 액션 형식을 사용하세요.
  • UI가 스스로 말할 수 있는 긴 일시 중지 동안에는 자막을 지우세요.

스크립트 템플릿

'use strict';
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');

const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000';
const VIDEO_DIR = path.join(__dirname, 'screenshots');
const OUTPUT_NAME = 'demo-FEATURE.webm';
const REHEARSAL = process.argv.includes('--rehearse');

// injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible, panElements를 여기에 붙여넣으세요.

(async () => {
  const browser = await chromium.launch({ headless: true });

  if (REHEARSAL) {
    const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
    const page = await context.newPage();
    // 흐름을 탐색하고 각 셀렉터에 대해 ensureVisible을 실행합니다.
    await browser.close();
    return;
  }

  const context = await browser.newContext({
    recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
    viewport: { width: 1280, height: 720 }
  });
  const page = await context.newPage();

  try {
    await injectCursor(page);
    await injectSubtitleBar(page);

    await showSubtitle(page, '1단계 - 로그인 중');
    // 로그인 액션

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, '2단계 - 대시보드 개요');
    // 대시보드 패닝

    await showSubtitle(page, '3단계 - 주요 워크플로우');
    // 액션 시퀀스

    await showSubtitle(page, '4단계 - 결과');
    // 최종 결과 표시
    await showSubtitle(page, '');
  } catch (err) {
    console.error('데모 오류:', err.message);
  } finally {
    await context.close();
    const video = page.video();
    if (video) {
      const src = await video.path();
      const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
      try {
        fs.copyFileSync(src, dest);
        console.log('비디오 저장됨:', dest);
      } catch (e) {
        console.error('오류: 비디오 복사 실패:', e.message);
        console.error('  원본:', src);
        console.error('  대상:', dest);
      }
    }
    await browser.close();
  }
})();

사용법:

# 2단계: 리허설
node demo-script.cjs --rehearse

# 3단계: 녹화
node demo-script.cjs

녹화 전 체크리스트

  • 발견 단계 완료
  • 리허설 통과, 모든 셀렉터 정상
  • 헤드리스 모드 활성화
  • 해상도 1280x720으로 설정
  • 모든 탐색 후 커서 및 자막 오버레이 재주입
  • 주요 전환 시 showSubtitle(page, 'N단계 - ...') 사용
  • 모든 클릭에 설명적인 레이블과 함께 moveAndClick 사용
  • 눈에 보이는 입력에 typeSlowly 사용
  • 조용한 캐치 없음; 오류는 명시적으로 기록
  • 인젝션, 리허설 및 이동/클릭 헬퍼가 최종 스크립트에 포함됨
  • 출력 파일 이름이 기능과 일치 (예: demo-purchase-request.webm)

참고

이 문서를 영어 원본과 함께 참조하세요. 기술 용어 및 코드는 번역되지 않습니다.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬