diff --git a/README.md b/README.md index a8c0885..d9b662a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,14 @@ Storybook은 기존 HM Component·Pattern·Theme·Variant·State·Interaction을 현재 검증된 테스트 배포 기준은 `HMDesignTest-Portable-Test-r3.zip`입니다. 사전 빌드된 Storybook과 Windows x64 포터블 Node를 포함하며 정확한 포함 범위는 r3의 BUILD-INFO·Inventory를 기준으로 확인합니다. `HMDesignTest-Portable-Test-r2.zip`과 `HMDesignTest-Portable-Test.zip`은 이전 스냅샷 기록으로 보존하며 현재 기준이 아닙니다. 포터블 ZIP의 기본 포트는 6007이고 충돌 시 자동으로 다른 포트를 사용합니다. 개발 저장소 Storybook의 6024와는 별개입니다. 테스트 사용자는 ZIP 해제 → `HMDesignTest/start-preview.bat` 더블클릭 → 표시된 로컬 URL 열기만 수행합니다. 별도 Node/npm 설치, npm 명령, Git 작업 또는 운영 업로드는 필요 없습니다. [실행 및 포함 범위](docs/guidelines/portable-test-distribution.md). +## Gitea 소스 다운로드 / Windows Storybook 실행 + +정식 저장소 `https://gitea.hmac.kr/b25027/HMDesign.git`를 Git Clone하거나 Gitea 소스 ZIP을 다운로드해 전체 압축을 풉니다. 이 방식은 포터블 ZIP과 달리 PC에 Node.js/npm이 필요합니다. 현재 lockfile의 요구 조건은 **Node.js 20.x의 20.19 이상 또는 22.12 이상**이며 Node.js 21.x는 지원하지 않습니다. + +프로젝트 루트의 `start-storybook.bat`을 더블클릭하세요. 의존성이 없으면 `npm ci`를 자동 실행하고, workspace 패키지 산출물이 없으면 기존 `npm run build:packages`를 실행합니다. 이후 기존 `npm run storybook`으로 서버를 시작하며, HMDesign Storybook의 Story index와 preview HTTP 응답이 준비된 뒤 기본 브라우저에서 **http://localhost:6024/** 를 엽니다. 이미 준비된 HMDesign Storybook이 있으면 서버를 추가 실행하지 않고 기존 주소를 엽니다. 서버 실행 창의 `Ctrl+C`로 종료합니다. + +실행 파일 위치를 기준으로 동작하므로 한글·공백 폴더, 다른 드라이브 및 Git이 없는 ZIP 환경에서도 사용할 수 있습니다. 최초 설치에는 npm 레지스트리 접속이 필요합니다. 포트 6024를 다른 프로그램이 사용하거나 아직 준비되지 않은 서버가 점유하면 포트를 바꾸지 않고 오류를 표시합니다. 해당 프로그램을 종료하거나 기존 서버 준비를 기다린 뒤 다시 실행하세요. 오류가 발생한 경우에만 창을 유지합니다. 출력된 오류, Node.js/npm의 PATH와 버전, 전체 소스 압축 해제 여부 및 네트워크를 확인하세요. 의존성이 불완전하면 프로젝트 루트에서 `npm ci`를 직접 실행하고 다시 시도합니다. + ## 오전 소스 ZIP / 개발자 전용 절차 (AI 참조용) 오전 소스 배포의 역사적 기준은 `HMDesignTest.zip`입니다. 당시 전달 구성에서는 `.git` 디렉터리와 Git 이력·원격 연결 정보를 제외하고 `.gitignore`와 소스·Storybook·문서·패키지 설정을 유지했습니다. 이 절의 수량·실행 조건은 오전 ZIP에 대한 기록이며 현재 포터블 r3의 포함 범위는 위 절과 BUILD-INFO를 기준으로 확인합니다. `HMDesignSystem-test-2026-10-08-r4.zip`, `HMDesignSystem-Distribution-Test-v2.zip`, `HMDesignSystem-AI-Reference-Test.zip`은 오전 기준을 대체하지 않습니다. diff --git a/scripts/start-storybook.mjs b/scripts/start-storybook.mjs new file mode 100644 index 0000000..d7ed106 --- /dev/null +++ b/scripts/start-storybook.mjs @@ -0,0 +1,84 @@ +// Git source launcher. Uses the existing npm scripts; no portable runtime. +import { spawn } from 'node:child_process'; +import { readFileSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import net from 'node:net'; + +process.chdir(fileURLToPath(new URL('../', import.meta.url))); +const pkg = JSON.parse(readFileSync('package.json', 'utf8')); +const portMatch = pkg.scripts?.storybook?.match(/(?:-p|--port)\s+(\d+)/); +if (!portMatch) throw new Error('Cannot determine the port from package.json scripts.storybook.'); +const port = Number(portMatch[1]); +const url = `http://localhost:${port}/`; +const [major, minor] = process.versions.node.split('.').map(Number); +if (!((major === 20 && minor >= 19) || (major === 22 && minor >= 12) || major > 22)) { + throw new Error('Node.js 20.19+ (20.x) or 22.12+ is required by the lockfile.'); +} + +function run(args, options = {}) { + return spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', args], { + stdio: 'inherit', windowsHide: true, ...options, + }); +} +function completed(child) { + return new Promise((resolve, reject) => { + child.once('error', reject); + child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`Command failed with exit code ${code}. See output above.`))); + }); +} +async function ready() { + try { + const response = await fetch(`${url}index.json`, { signal: AbortSignal.timeout(1500) }); + if (!response.ok) return false; + const index = await response.json(); + if (!index.entries?.['components-form-button--all'] || !index.entries?.['theme-designer-playground--editor']) return false; + return (await fetch(`${url}iframe.html`, { signal: AbortSignal.timeout(1500) })).ok; + } catch { return false; } +} +function occupied() { + return new Promise(resolve => { + const socket = net.connect({ host: '127.0.0.1', port }); + socket.setTimeout(1500); + socket.once('connect', () => { socket.destroy(); resolve(true); }); + socket.once('error', () => { socket.destroy(); resolve(false); }); + socket.once('timeout', () => { socket.destroy(); resolve(true); }); + }); +} +async function openBrowser() { + await completed(spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `Start-Process '${url}'`], { + stdio: 'inherit', windowsHide: true, + })); +} +if (await ready()) { + await openBrowser(); +} else { + if (await occupied()) throw new Error(`Port ${port} is occupied by a server that is not a ready HMDesign Storybook. Stop it and retry.`); + await completed(run('npm.cmd --version', { stdio: 'ignore' })); + if (!existsSync('node_modules/.bin/storybook.cmd')) await completed(run('npm.cmd ci')); + const missingDist = ['packages/ui', 'packages/center-graphic'].some(dir => { + const workspace = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8')); + return Object.values(workspace.exports).some(value => { + const entry = typeof value === 'string' ? value : value.import; + return entry && !existsSync(`${dir}/${entry}`); + }); + }); + if (missingDist) await completed(run('npm.cmd run build:packages')); + // Exact port prevents interactive port selection, including a startup race. + const server = run('npm.cmd run storybook -- --exact-port --ci'); + let stopped = false; + const exit = completed(server).finally(() => { stopped = true; }); + // Attach a handler immediately while readiness polling is in progress. + exit.catch(() => {}); + const stop = () => completed(run(`taskkill /pid ${server.pid} /t /f`, { stdio: 'ignore' })).catch(() => {}); + process.once('SIGINT', async () => { await stop(); process.exit(0); }); + process.once('SIGTERM', async () => { await stop(); process.exit(0); }); + try { + const deadline = Date.now() + 180000; + while (!stopped && Date.now() < deadline) { + if (await ready()) { await openBrowser(); await exit; break; } + await new Promise(resolve => setTimeout(resolve, 500)); + } + if (stopped) await exit; + else throw new Error('Storybook readiness timed out after 3 minutes.'); + } catch (error) { await stop(); throw error; } +} diff --git a/start-storybook.bat b/start-storybook.bat new file mode 100644 index 0000000..253bb30 --- /dev/null +++ b/start-storybook.bat @@ -0,0 +1,24 @@ +@echo off +setlocal EnableExtensions DisableDelayedExpansion +chcp 65001 >nul +title HMDesign - Storybook +pushd "%~dp0" +if errorlevel 1 goto root_error +node scripts\start-storybook.mjs +if errorlevel 1 goto fail +popd +endlocal +exit /b 0 +:fail +echo. +echo ERROR: Storybook could not start. Review the error above. +echo Check Node.js/npm, dependency installation, and port availability. +popd +pause +endlocal +exit /b 1 +:root_error +echo ERROR: Cannot access the launcher folder. +pause +endlocal +exit /b 1