v0.1.31 로컬 실시간 확인 스크립트 추가

This commit is contained in:
2026-04-14 16:27:23 +09:00
parent 8c591b3b93
commit fd5c654118
7 changed files with 236 additions and 4 deletions

View File

@@ -1,7 +1,7 @@
# 배포 가이드
## 현재 버전
- `v0.1.30`
- `v0.1.31`
## Git 기본 설정
- 저장소 작성자 정보는 아래 값으로 통일한다.
@@ -45,6 +45,16 @@ npm install
npm run dev
```
## 로컬 실시간 확인
```bash
npm run dev:ghost:start
npm run dev:watch
```
- `npm run dev:ghost:start`는 로컬 Ghost 컨테이너를 시작한다.
- `npm run dev:watch`는 초기 빌드와 sync를 한 번 수행한 뒤, Tailwind watch와 파일 변경 감지 기반 theme sync를 계속 유지한다.
- 템플릿이나 자산을 저장한 뒤 브라우저 새로고침만으로 바로 반영 상태를 확인할 수 있다.
## 로컬 스타일 빌드
```bash
npm run build:alpine

View File

@@ -1,5 +1,8 @@
# 의사결정 이력
## 2026-04-14 v0.1.31
zip 업로드 없이 로컬 Ghost에서 바로 확인하려면 자동 sync가 필요했지만, `fs.watch` 기반 재귀 watcher는 현재 환경에서 `EMFILE` 오류로 안정적으로 유지되지 않았다. 그래서 별도 의존성을 추가하지 않고, 제외 디렉터리를 뺀 파일 목록의 수정 시간을 주기적으로 비교하는 polling 방식으로 `dev:watch`를 구현했다. 이 방식은 다소 단순하지만 현재 저장소 크기에서는 충분히 가볍고, 사용자가 템플릿을 저장한 뒤 바로 브라우저 새로고침으로 확인할 수 있다는 점이 더 중요하다고 판단했다.
## 2026-04-14 v0.1.30
`prose`는 Tailwind 기본 유틸리티가 아니라 `@tailwindcss/typography` 플러그인 영역인데, 현재 저장소에는 해당 플러그인이 들어있지 않았다. 이번 단계에서는 사용자가 이미 수정한 Tailwind 마크업을 최대한 유지하는 것이 우선이었기 때문에, 의존성을 새로 늘리기보다 `assets/styles/tailwind.css``@layer components`에 원본 소스 기준 타이포그래피 규칙을 직접 옮겨 적용했다. 동시에 `bg-accent/10`처럼 현재 빌드에서 누락되던 유틸리티도 같은 방식으로 보강해 카드 태그 배경과 리스트 구분선이 다시 보이도록 정리했다.

View File

@@ -1,7 +1,7 @@
# 파일-화면 매핑 가이드
## 현재 버전
- `v0.1.30`
- `v0.1.31`
## 공통 레이아웃
- [default.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/default.hbs): 전체 3열 셸과 공통 자산 로드
@@ -34,5 +34,6 @@
- [assets/built/theme.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/built/theme.js): 인터랙션 스크립트
- [assets/styles/tailwind.css](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/styles/tailwind.css): Tailwind 입력 파일, `prose` 타이포그래피 규칙, accent/구분선 보조 유틸리티
- [tailwind.config.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/tailwind.config.js): Tailwind 스캔 경로, 테마 설정, preflight 초기화 설정
- [scripts/dev-watch.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/scripts/dev-watch.js): 로컬 파일 변경 감지 후 `dev:sync`와 Tailwind watch를 함께 실행하는 개발용 watcher
- [routes.yaml.example](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/routes.yaml.example): Ghost 커스텀 라우트 예시
- [package.json](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/package.json): Ghost 테마 메타데이터

View File

@@ -1,7 +1,7 @@
# 기술 명세
## 현재 버전
- `v0.1.30`
- `v0.1.31`
## 테마 개요
- Ghost `v5` 대응 커스텀 테마
@@ -16,6 +16,7 @@
- Tailwind CSS 빌드 결과물(`assets/built/tailwind.css`)을 기존 `screen.css`와 함께 로드
- Tailwind 기본 초기화(`preflight`)를 활성화해 브라우저 기본 마진과 폼 스타일을 리셋
- Alpine.js 로컬 자산(`assets/built/alpine.js`)을 전역 로드
- `npm run dev:watch`는 초기 `dev:prepare` 실행 후 Tailwind `--watch`와 파일 변경 감지 기반 `dev:sync`를 함께 실행함
- 좌측 카테고리 영역은 Alpine.js로 제어되며 `1024px` 이상에서 기본 열림, 미만에서 기본 닫힘
- 좌측 네비게이션 마커와 카테고리 마커는 동일한 세로 바 → 원형 hover 패턴 사용
- 전역 `ol`, `ul`, `menu` 기본 패딩과 리스트 스타일 리셋 적용

View File

@@ -1,5 +1,10 @@
# 업데이트 로그
## v0.1.31 - 2026-04-14
- `npm run dev:watch` 스크립트 추가.
- 로컬 파일 변경 감지 후 자동 `dev:sync` 반영 흐름 추가.
- Tailwind `--watch`와 테마 sync를 함께 사용하는 로컬 확인 루프 정리.
## v0.1.30 - 2026-04-14
- `prose` 본문 타이포그래피 스타일 추가.
- `ul`, `ol`, `blockquote`, `code`, `table` 본문 표현 복구.

View File

@@ -1,6 +1,6 @@
{
"name": "ghost-theme-thred-clone",
"version": "0.1.30",
"version": "0.1.31",
"private": true,
"description": "A Ghost theme inspired by the Thred reference layout.",
"keywords": [
@@ -79,6 +79,7 @@
"build:tailwind": "tailwindcss -c ./tailwind.config.js -i ./assets/styles/tailwind.css -o ./assets/built/tailwind.css --minify",
"dev": "npm run dev:ghost:start",
"dev:prepare": "npm run build:alpine && npm run build:tailwind && npm run dev:sync",
"dev:watch": "node ./scripts/dev-watch.js",
"dev:sync": "sh ./scripts/sync-theme.sh",
"dev:seed": "node ./scripts/build-sample-content.js",
"dev:seed:zip": "npm run dev:seed && cd seed && zip -q -r thred-inspired-sample-content.ghost.zip thred-inspired-sample-content.ghost.json",

211
scripts/dev-watch.js Normal file
View File

@@ -0,0 +1,211 @@
const fs = require('fs')
const path = require('path')
const { spawn } = require('child_process')
const rootDir = path.resolve(__dirname, '..')
const ignoredDirectories = new Set([
'.git',
'.cursor',
'.vscode',
'.docker',
'docs',
'local-ghost',
'node_modules',
'seed',
'theme-export'
])
const ignoredFiles = new Set([
'.DS_Store'
])
const syncExtensions = new Set([
'.css',
'.hbs',
'.js',
'.json',
'.svg',
'.yaml',
'.yml'
])
const watchedRoots = [rootDir]
let syncTimer = null
let syncProcess = null
let scanTimer = null
let previousSnapshot = new Map()
/**
* @param {string} relativePath
*/
function shouldIgnorePath(relativePath) {
if (!relativePath || relativePath === '.') {
return false
}
const segments = relativePath.split(path.sep)
return segments.some((segment) => ignoredDirectories.has(segment) || ignoredFiles.has(segment))
}
/**
* @param {string} relativePath
*/
function shouldSyncFile(relativePath) {
if (shouldIgnorePath(relativePath)) {
return false
}
if (relativePath === path.join('assets', 'styles', 'tailwind.css')) {
return false
}
const extension = path.extname(relativePath)
return syncExtensions.has(extension)
}
/**
* @param {string} command
* @param {string[]} args
* @param {(code: number | null) => void} [onExit]
*/
function runCommand(command, args, onExit) {
const child = spawn(command, args, {
cwd: rootDir,
stdio: 'inherit',
shell: false
})
child.on('exit', (code) => {
if (onExit) {
onExit(code)
}
})
return child
}
function queueSync() {
if (syncTimer) {
clearTimeout(syncTimer)
}
syncTimer = setTimeout(() => {
if (syncProcess) {
queueSync()
return
}
console.log('[dev:watch] theme sync')
syncProcess = runCommand('npm', ['run', 'dev:sync'], (code) => {
syncProcess = null
if (code !== 0) {
console.error(`[dev:watch] sync failed with code ${code}`)
}
})
}, 180)
}
/**
* @param {string} directoryPath
* @param {Map<string, number>} snapshot
*/
function collectSnapshot(directoryPath, snapshot) {
if (!fs.existsSync(directoryPath)) {
return
}
for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
const entryPath = path.join(directoryPath, entry.name)
const relativePath = path.relative(rootDir, entryPath)
if (shouldIgnorePath(relativePath)) {
continue
}
if (entry.isDirectory()) {
collectSnapshot(entryPath, snapshot)
continue
}
if (shouldSyncFile(relativePath)) {
snapshot.set(relativePath, fs.statSync(entryPath).mtimeMs)
}
}
}
function buildSnapshot() {
const snapshot = new Map()
for (const watchedRoot of watchedRoots) {
collectSnapshot(watchedRoot, snapshot)
}
return snapshot
}
function scanForChanges() {
const nextSnapshot = buildSnapshot()
for (const [relativePath, modifiedTime] of nextSnapshot.entries()) {
if (previousSnapshot.get(relativePath) !== modifiedTime) {
console.log(`[dev:watch] change detected: ${relativePath}`)
queueSync()
previousSnapshot = nextSnapshot
return
}
}
for (const relativePath of previousSnapshot.keys()) {
if (!nextSnapshot.has(relativePath)) {
console.log(`[dev:watch] change detected: ${relativePath}`)
queueSync()
previousSnapshot = nextSnapshot
return
}
}
previousSnapshot = nextSnapshot
}
function startPolling() {
previousSnapshot = buildSnapshot()
scanTimer = setInterval(scanForChanges, 800)
}
function shutdown() {
if (syncTimer) {
clearTimeout(syncTimer)
}
if (tailwindProcess && !tailwindProcess.killed) {
tailwindProcess.kill('SIGINT')
}
if (syncProcess && !syncProcess.killed) {
syncProcess.kill('SIGINT')
}
if (scanTimer) {
clearInterval(scanTimer)
}
process.exit(0)
}
console.log('[dev:watch] initial prepare')
runCommand('npm', ['run', 'dev:prepare'], (prepareCode) => {
if (prepareCode !== 0) {
process.exit(prepareCode || 1)
}
console.log('[dev:watch] tailwind watch start')
tailwindProcess = runCommand('npm', ['run', 'build:tailwind', '--', '--watch'])
console.log('[dev:watch] filesystem watch start')
startPolling()
})
let tailwindProcess = null
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)