Initial import
This commit is contained in:
63
scripts/capture-component-screenshot.mjs
Executable file
63
scripts/capture-component-screenshot.mjs
Executable file
@@ -0,0 +1,63 @@
|
||||
import process from 'node:process';
|
||||
import { chromium } from 'playwright';
|
||||
import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const componentId = process.argv[2];
|
||||
const captureDate = process.argv[3] ?? getKstDate();
|
||||
const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174';
|
||||
|
||||
if (!componentId) {
|
||||
console.error('Usage: node scripts/capture-component-screenshot.mjs <component-id> [YYYY-MM-DD]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const screenshotFileName = `${componentId}.png`;
|
||||
const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({
|
||||
cwd,
|
||||
captureDate,
|
||||
screenshotFileName,
|
||||
});
|
||||
const targetSelector = `#component-sample-${componentId}`;
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
viewport: {
|
||||
width: 1600,
|
||||
height: 1200,
|
||||
},
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureDirectory(screenshotDir);
|
||||
|
||||
const targetUrl = new URL(baseUrl);
|
||||
targetUrl.searchParams.set('topMenu', 'apis');
|
||||
|
||||
await page.goto(targetUrl.toString(), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.waitForLoadState('networkidle').catch(() => {});
|
||||
|
||||
const target = page.locator(targetSelector).first();
|
||||
await target.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await target.scrollIntoViewIfNeeded();
|
||||
await target.screenshot({
|
||||
path: screenshotPath,
|
||||
animations: 'disabled',
|
||||
});
|
||||
|
||||
await updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt: componentId,
|
||||
markdownImagePath,
|
||||
});
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
console.log(`Linked in: ${worklogPath}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
117
scripts/capture-feature-screenshot.mjs
Executable file
117
scripts/capture-feature-screenshot.mjs
Executable file
@@ -0,0 +1,117 @@
|
||||
import process from 'node:process';
|
||||
import { chromium } from 'playwright';
|
||||
import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs';
|
||||
|
||||
const FEATURE_CAPTURE_PRESETS = {
|
||||
'docs-worklogs': {
|
||||
topMenu: 'docs',
|
||||
screenshotFileName: 'feature-docs-worklogs.png',
|
||||
targetSelector: '.app-main-card',
|
||||
},
|
||||
'play-layout': {
|
||||
topMenu: 'play',
|
||||
screenshotFileName: 'feature-play-layout.png',
|
||||
targetSelector: '.app-main-card',
|
||||
query: { playSection: 'layout' },
|
||||
},
|
||||
'apis-components': {
|
||||
topMenu: 'apis',
|
||||
screenshotFileName: 'feature-apis-components.png',
|
||||
targetSelector: '.app-main-card',
|
||||
},
|
||||
'apis-widgets': {
|
||||
topMenu: 'apis',
|
||||
screenshotFileName: 'feature-apis-widgets.png',
|
||||
targetSelector: '.app-main-card',
|
||||
afterNavigation: async (page) => {
|
||||
await page.getByRole('menuitem', { name: 'Widgets' }).click();
|
||||
},
|
||||
},
|
||||
'plans-board': {
|
||||
topMenu: 'plans',
|
||||
screenshotFileName: 'feature-plans-board.png',
|
||||
targetSelector: '.app-main-card',
|
||||
},
|
||||
'plans-charts': {
|
||||
topMenu: 'plans',
|
||||
screenshotFileName: 'feature-plans-charts.png',
|
||||
targetSelector: '.app-main-card',
|
||||
query: { planSection: 'charts' },
|
||||
},
|
||||
'chat-live': {
|
||||
topMenu: 'chat',
|
||||
screenshotFileName: 'feature-chat-live.png',
|
||||
targetSelector: '.app-main-card, .app-chat-panel',
|
||||
},
|
||||
'chat-errors': {
|
||||
topMenu: 'chat',
|
||||
screenshotFileName: 'feature-chat-errors.png',
|
||||
targetSelector: '.app-main-card, .app-chat-panel',
|
||||
afterNavigation: async (page) => {
|
||||
await page.getByRole('menuitem', { name: '에러 로그' }).click();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const cwd = process.cwd();
|
||||
const presetKey = process.argv[2];
|
||||
const captureDate = process.argv[3] ?? getKstDate();
|
||||
const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174';
|
||||
|
||||
if (!presetKey || !(presetKey in FEATURE_CAPTURE_PRESETS)) {
|
||||
console.error(`Usage: node scripts/capture-feature-screenshot.mjs <${Object.keys(FEATURE_CAPTURE_PRESETS).join('|')}> [YYYY-MM-DD]`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const preset = FEATURE_CAPTURE_PRESETS[presetKey];
|
||||
const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({
|
||||
cwd,
|
||||
captureDate,
|
||||
screenshotFileName: preset.screenshotFileName,
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1600, height: 1200 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureDirectory(screenshotDir);
|
||||
|
||||
const targetUrl = new URL(baseUrl);
|
||||
targetUrl.searchParams.set('topMenu', preset.topMenu);
|
||||
|
||||
if (preset.query) {
|
||||
for (const [key, value] of Object.entries(preset.query)) {
|
||||
targetUrl.searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
await page.goto(targetUrl.toString(), { waitUntil: 'networkidle' });
|
||||
|
||||
if (preset.afterNavigation) {
|
||||
await preset.afterNavigation(page);
|
||||
await page.waitForLoadState('networkidle').catch(() => {});
|
||||
}
|
||||
|
||||
const target = page.locator(preset.targetSelector).first();
|
||||
await target.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await target.scrollIntoViewIfNeeded();
|
||||
await target.screenshot({
|
||||
path: screenshotPath,
|
||||
animations: 'disabled',
|
||||
});
|
||||
|
||||
await updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt: preset.screenshotFileName.replace('.png', ''),
|
||||
markdownImagePath,
|
||||
});
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
console.log(`Linked in: ${worklogPath}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
45
scripts/capture-fullscreen-toggle-screenshot.mjs
Executable file
45
scripts/capture-fullscreen-toggle-screenshot.mjs
Executable file
@@ -0,0 +1,45 @@
|
||||
import process from 'node:process';
|
||||
import { chromium } from 'playwright';
|
||||
import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const captureDate = process.argv[2] ?? getKstDate();
|
||||
const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174';
|
||||
const screenshotFileName = 'main-content-fullscreen-toggle.png';
|
||||
const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({
|
||||
cwd,
|
||||
captureDate,
|
||||
screenshotFileName,
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1600, height: 1200 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureDirectory(screenshotDir);
|
||||
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle' });
|
||||
await page.getByLabel('콘텐츠 최대화').click();
|
||||
|
||||
const target = page.locator('.app-main-content--expanded').first();
|
||||
await target.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await target.screenshot({
|
||||
path: screenshotPath,
|
||||
animations: 'disabled',
|
||||
});
|
||||
|
||||
await updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt: 'main-content-fullscreen-toggle',
|
||||
markdownImagePath,
|
||||
});
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
console.log(`Linked in: ${worklogPath}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
61
scripts/capture-menu-screenshot.mjs
Executable file
61
scripts/capture-menu-screenshot.mjs
Executable file
@@ -0,0 +1,61 @@
|
||||
import process from 'node:process';
|
||||
import { chromium } from 'playwright';
|
||||
import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const menuGroup = process.argv[2] ?? 'docs';
|
||||
const captureDate = process.argv[3] ?? getKstDate();
|
||||
const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174';
|
||||
|
||||
const supportedMenuGroups = new Set(['docs', 'plans']);
|
||||
|
||||
if (!supportedMenuGroups.has(menuGroup)) {
|
||||
console.error('Usage: node scripts/capture-menu-screenshot.mjs [docs|plans] [YYYY-MM-DD]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const screenshotFileName = `${menuGroup}-menu.png`;
|
||||
const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({
|
||||
cwd,
|
||||
captureDate,
|
||||
screenshotFileName,
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1600, height: 1200 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureDirectory(screenshotDir);
|
||||
|
||||
const targetUrl = new URL(baseUrl);
|
||||
targetUrl.searchParams.set('topMenu', menuGroup === 'docs' ? 'docs' : 'plans');
|
||||
|
||||
await page.goto(targetUrl.toString(), { waitUntil: 'networkidle' });
|
||||
|
||||
if (menuGroup === 'plans') {
|
||||
await page.getByLabel('설정').click();
|
||||
await page.locator('.app-header__settings-menu').first().waitFor({ state: 'visible', timeout: 30000 });
|
||||
}
|
||||
|
||||
const target = page.locator('.app-shell').first();
|
||||
await target.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await target.screenshot({
|
||||
path: screenshotPath,
|
||||
animations: 'disabled',
|
||||
});
|
||||
|
||||
await updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt: screenshotFileName.replace('.png', ''),
|
||||
markdownImagePath,
|
||||
});
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
console.log(`Linked in: ${worklogPath}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
50
scripts/capture-plan-board-mobile-screenshot.mjs
Executable file
50
scripts/capture-plan-board-mobile-screenshot.mjs
Executable file
@@ -0,0 +1,50 @@
|
||||
import process from 'node:process';
|
||||
import { chromium } from 'playwright';
|
||||
import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const captureDate = process.argv[2] ?? getKstDate();
|
||||
const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174';
|
||||
const screenshotFileName = 'plan-board-mobile-memo-detail.png';
|
||||
const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({
|
||||
cwd,
|
||||
captureDate,
|
||||
screenshotFileName,
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 430, height: 932 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
deviceScaleFactor: 3,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await ensureDirectory(screenshotDir);
|
||||
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle' });
|
||||
await page.getByText('Plans').click();
|
||||
await page.getByRole('button', { name: '새 메모' }).click();
|
||||
|
||||
const overlayCard = page.locator('.plan-board-page__overlay-card').first();
|
||||
await overlayCard.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await overlayCard.screenshot({
|
||||
path: screenshotPath,
|
||||
animations: 'disabled',
|
||||
});
|
||||
|
||||
await updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt: 'plan-board-mobile-memo-detail',
|
||||
markdownImagePath,
|
||||
});
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
console.log(`Linked in: ${worklogPath}`);
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
98
scripts/capture-search-command-screenshot.mjs
Executable file
98
scripts/capture-search-command-screenshot.mjs
Executable file
@@ -0,0 +1,98 @@
|
||||
import process from 'node:process';
|
||||
import { chromium } from 'playwright';
|
||||
import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs';
|
||||
|
||||
const cwd = process.cwd();
|
||||
const captureDate = process.argv[2] ?? getKstDate();
|
||||
const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174';
|
||||
const screenshotFileName = 'search-command.png';
|
||||
const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({
|
||||
cwd,
|
||||
captureDate,
|
||||
screenshotFileName,
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 430, height: 932 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
deviceScaleFactor: 3,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await ensureDirectory(screenshotDir);
|
||||
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
await page.evaluate(() => {
|
||||
const sensor = Array.from(document.querySelectorAll('div')).find((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
return (
|
||||
style.position === 'fixed' &&
|
||||
style.top === '0px' &&
|
||||
style.right === '0px' &&
|
||||
style.touchAction === 'none' &&
|
||||
element.childElementCount === 0
|
||||
);
|
||||
});
|
||||
|
||||
if (!sensor) {
|
||||
throw new Error('Search gesture sensor not found.');
|
||||
}
|
||||
|
||||
const rect = sensor.getBoundingClientRect();
|
||||
const startX = rect.right - 24;
|
||||
const startY = rect.top + 18;
|
||||
const endY = startY + 120;
|
||||
|
||||
const createTouch = (clientY) =>
|
||||
new Touch({
|
||||
identifier: 1,
|
||||
target: sensor,
|
||||
clientX: startX,
|
||||
clientY,
|
||||
radiusX: 12,
|
||||
radiusY: 12,
|
||||
rotationAngle: 0,
|
||||
force: 1,
|
||||
});
|
||||
|
||||
const dispatch = (type, touches) => {
|
||||
sensor.dispatchEvent(
|
||||
new TouchEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
touches,
|
||||
targetTouches: touches,
|
||||
changedTouches: touches,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
dispatch('touchstart', [createTouch(startY)]);
|
||||
dispatch('touchmove', [createTouch(endY)]);
|
||||
dispatch('touchend', []);
|
||||
});
|
||||
|
||||
const modal = page.locator('.search-command-modal .ant-modal-content').first();
|
||||
await modal.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await modal.screenshot({
|
||||
path: screenshotPath,
|
||||
animations: 'disabled',
|
||||
});
|
||||
|
||||
await updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt: 'search-command',
|
||||
markdownImagePath,
|
||||
});
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
console.log(`Linked in: ${worklogPath}`);
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
83
scripts/capture-settings-screenshot.mjs
Executable file
83
scripts/capture-settings-screenshot.mjs
Executable file
@@ -0,0 +1,83 @@
|
||||
import process from 'node:process';
|
||||
import { chromium } from 'playwright';
|
||||
import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs';
|
||||
|
||||
const ACCESS_TOKEN = 'usr_7f3a9c2d8e1b4a6f';
|
||||
const TOKEN_ACCESS_STORAGE_KEY = 'work-app.token-access.registered-token';
|
||||
|
||||
const SETTINGS_CAPTURE_PRESETS = {
|
||||
automation: {
|
||||
screenshotFileName: 'settings-app.png',
|
||||
triggerLabel: '앱 설정',
|
||||
},
|
||||
notification: {
|
||||
screenshotFileName: 'settings-notification.png',
|
||||
triggerLabel: '알림',
|
||||
},
|
||||
};
|
||||
|
||||
const cwd = process.cwd();
|
||||
const presetKey = process.argv[2];
|
||||
const captureDate = process.argv[3] ?? getKstDate();
|
||||
const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:4173';
|
||||
|
||||
if (!presetKey || !(presetKey in SETTINGS_CAPTURE_PRESETS)) {
|
||||
console.error(`Usage: node scripts/capture-settings-screenshot.mjs <${Object.keys(SETTINGS_CAPTURE_PRESETS).join('|')}> [YYYY-MM-DD]`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const preset = SETTINGS_CAPTURE_PRESETS[presetKey];
|
||||
const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({
|
||||
cwd,
|
||||
captureDate,
|
||||
screenshotFileName: preset.screenshotFileName,
|
||||
});
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1600, height: 1200 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
|
||||
await context.addInitScript(
|
||||
({ tokenAccessStorageKey, accessToken }) => {
|
||||
window.localStorage.setItem(tokenAccessStorageKey, accessToken);
|
||||
},
|
||||
{
|
||||
tokenAccessStorageKey: TOKEN_ACCESS_STORAGE_KEY,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
},
|
||||
);
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await ensureDirectory(screenshotDir);
|
||||
|
||||
const targetUrl = new URL(baseUrl);
|
||||
targetUrl.searchParams.set('topMenu', 'plans');
|
||||
|
||||
await page.goto(targetUrl.toString(), { waitUntil: 'networkidle' });
|
||||
await page.getByLabel('설정').click();
|
||||
await page.getByRole('button', { name: preset.triggerLabel }).click();
|
||||
|
||||
const modal = page.locator('.ant-modal-root .ant-modal-content').last();
|
||||
await modal.waitFor({ state: 'visible', timeout: 30000 });
|
||||
await modal.screenshot({
|
||||
path: screenshotPath,
|
||||
animations: 'disabled',
|
||||
});
|
||||
|
||||
await updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt: preset.screenshotFileName.replace('.png', ''),
|
||||
markdownImagePath,
|
||||
});
|
||||
|
||||
console.log(`Saved: ${screenshotPath}`);
|
||||
console.log(`Linked in: ${worklogPath}`);
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
1716
scripts/run-plan-codex-once.mjs
Executable file
1716
scripts/run-plan-codex-once.mjs
Executable file
File diff suppressed because it is too large
Load Diff
1045
scripts/run-server-command-runner.mjs
Normal file
1045
scripts/run-server-command-runner.mjs
Normal file
File diff suppressed because it is too large
Load Diff
146
scripts/serve-app-dist.mjs
Executable file
146
scripts/serve-app-dist.mjs
Executable file
@@ -0,0 +1,146 @@
|
||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
||||
import { extname, isAbsolute, join, normalize } from 'node:path';
|
||||
import { createServer } from 'node:http';
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
const port = Number(process.env.PORT ?? 5173);
|
||||
const distDirName = process.env.APP_DIST_DIR ?? 'app-dist';
|
||||
const rootDir = normalize(isAbsolute(distDirName) ? distDirName : join(process.cwd(), distDirName));
|
||||
const workServerUrl = new URL(process.env.WORK_SERVER_URL ?? 'http://127.0.0.1:3100');
|
||||
const proxyPrefixes = ['/api', '/.codex_chat'];
|
||||
|
||||
const mimeTypes = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.ico': 'image/x-icon',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.png': 'image/png',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
||||
'.woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
function looksLikeStaticAsset(requestedPath) {
|
||||
const normalizedPath = requestedPath.split('?')[0] ?? requestedPath;
|
||||
const extension = extname(normalizedPath);
|
||||
|
||||
return extension.length > 0;
|
||||
}
|
||||
|
||||
function resolvePath(urlPath) {
|
||||
const decodedPath = decodeURIComponent(urlPath.split('?')[0] || '/');
|
||||
const requestedPath = decodedPath === '/' ? '/index.html' : decodedPath;
|
||||
const absolutePath = normalize(join(rootDir, requestedPath));
|
||||
|
||||
if (!absolutePath.startsWith(rootDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (existsSync(absolutePath) && statSync(absolutePath).isFile()) {
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
if (looksLikeStaticAsset(requestedPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return join(rootDir, 'index.html');
|
||||
}
|
||||
|
||||
function shouldProxyRequest(urlPath = '/') {
|
||||
return proxyPrefixes.some((prefix) => urlPath === prefix || urlPath.startsWith(`${prefix}/`));
|
||||
}
|
||||
|
||||
function readRequestBody(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
|
||||
request.on('data', (chunk) => {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
});
|
||||
request.on('end', () => {
|
||||
resolve(chunks.length > 0 ? Buffer.concat(chunks) : null);
|
||||
});
|
||||
request.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function proxyRequest(request, response) {
|
||||
const targetUrl = new URL(request.url ?? '/', workServerUrl);
|
||||
const headers = new Headers();
|
||||
|
||||
Object.entries(request.headers).forEach(([key, value]) => {
|
||||
if (value == null || key.toLowerCase() === 'host' || key.toLowerCase() === 'connection') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => headers.append(key, item));
|
||||
return;
|
||||
}
|
||||
|
||||
headers.set(key, value);
|
||||
});
|
||||
|
||||
const method = request.method ?? 'GET';
|
||||
const body = method === 'GET' || method === 'HEAD' ? undefined : await readRequestBody(request);
|
||||
|
||||
try {
|
||||
const upstreamResponse = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
response.writeHead(
|
||||
upstreamResponse.status,
|
||||
Object.fromEntries(upstreamResponse.headers.entries()),
|
||||
);
|
||||
|
||||
if (!upstreamResponse.body) {
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
Readable.fromWeb(upstreamResponse.body).pipe(response);
|
||||
} catch (error) {
|
||||
response.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
message: error instanceof Error ? error.message : 'Failed to proxy request to work server.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const server = createServer(async (request, response) => {
|
||||
if (shouldProxyRequest(request.url ?? '/')) {
|
||||
await proxyRequest(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = resolvePath(request.url ?? '/');
|
||||
|
||||
if (!resolvedPath || !existsSync(resolvedPath)) {
|
||||
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
response.end('Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = extname(resolvedPath);
|
||||
const contentType = mimeTypes[extension] ?? 'application/octet-stream';
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': extension === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable',
|
||||
});
|
||||
|
||||
createReadStream(resolvedPath).pipe(response);
|
||||
});
|
||||
|
||||
server.listen(port, '0.0.0.0', () => {
|
||||
console.log(`${distDirName} server listening on http://0.0.0.0:${port}`);
|
||||
});
|
||||
176
scripts/worklog-capture-utils.mjs
Executable file
176
scripts/worklog-capture-utils.mjs
Executable file
@@ -0,0 +1,176 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const KST_DATE_FORMATTER = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Asia/Seoul',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
|
||||
const WORKLOG_TEMPLATE = `# {date} 작업일지
|
||||
|
||||
## 오늘 작업
|
||||
|
||||
- 화면 캡처 추가 예정
|
||||
|
||||
## 스크린샷
|
||||
|
||||
- 저장소 기준 연결된 스크린샷 없음
|
||||
|
||||
## 소스
|
||||
|
||||
### 파일 1: \`path/to/file.tsx\`
|
||||
|
||||
- 변경 목적과 핵심 수정 내용을 한 줄로 정리
|
||||
|
||||
\`\`\`diff
|
||||
# 이 파일의 핵심 diff
|
||||
- before
|
||||
+ after
|
||||
\`\`\`
|
||||
|
||||
### 파일 2: \`path/to/another-file.ts\`
|
||||
|
||||
- 필요 없으면 이 섹션은 삭제
|
||||
|
||||
## 실행 커맨드
|
||||
|
||||
\`\`\`bash
|
||||
\`\`\`
|
||||
|
||||
## 변경 파일
|
||||
|
||||
-
|
||||
`;
|
||||
|
||||
const OBSOLETE_WORKLOG_SECTION_TITLES = new Set([
|
||||
'커밋 목록',
|
||||
'변경 요약',
|
||||
'변경 통계',
|
||||
'라인 통계',
|
||||
]);
|
||||
|
||||
function normalizeLevelTwoHeading(section) {
|
||||
const match = section.match(/^##\s+([^\n]+)$/m);
|
||||
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return match[1]?.replace(/\s+\(.*\)\s*$/u, '').trim() ?? '';
|
||||
}
|
||||
|
||||
function stripObsoleteWorklogSections(content) {
|
||||
const sections = content.split(/\n(?=##\s+)/);
|
||||
const preservedSections = sections.filter((section) => {
|
||||
const normalizedHeading = normalizeLevelTwoHeading(section);
|
||||
return !OBSOLETE_WORKLOG_SECTION_TITLES.has(normalizedHeading);
|
||||
});
|
||||
|
||||
return `${preservedSections.join('\n\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
||||
}
|
||||
|
||||
export async function ensureDirectory(dirPath) {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
export function getKstDate(date = new Date()) {
|
||||
return KST_DATE_FORMATTER.format(date);
|
||||
}
|
||||
|
||||
export async function ensureWorklogFile(worklogPath, captureDate) {
|
||||
try {
|
||||
await fs.access(worklogPath);
|
||||
const content = await fs.readFile(worklogPath, 'utf8');
|
||||
const normalizedContent = stripObsoleteWorklogSections(content);
|
||||
|
||||
if (normalizedContent !== content) {
|
||||
await fs.writeFile(worklogPath, normalizedContent, 'utf8');
|
||||
}
|
||||
} catch {
|
||||
await ensureDirectory(path.dirname(worklogPath));
|
||||
const content = WORKLOG_TEMPLATE.replaceAll('{date}', captureDate);
|
||||
await fs.writeFile(worklogPath, content, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateWorklogCaptureSection({
|
||||
worklogPath,
|
||||
captureDate,
|
||||
imageAlt,
|
||||
markdownImagePath,
|
||||
}) {
|
||||
await ensureWorklogFile(worklogPath, captureDate);
|
||||
|
||||
const imageLine = ``;
|
||||
let content = await fs.readFile(worklogPath, 'utf8');
|
||||
|
||||
if (content.includes(imageLine)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.includes('## 스크린샷') && !content.includes('## 화면 캡처')) {
|
||||
content += `\n\n## 스크린샷\n\n${imageLine}\n`;
|
||||
await fs.writeFile(worklogPath, content, 'utf8');
|
||||
return;
|
||||
}
|
||||
|
||||
const sections = content.split(/\n(?=##\s+)/);
|
||||
const screenshotSections = [];
|
||||
const remainingSections = [];
|
||||
|
||||
for (const section of sections) {
|
||||
if (/^##\s+(?:스크린샷|화면 캡처)\s*$/m.test(section)) {
|
||||
screenshotSections.push(section);
|
||||
} else {
|
||||
remainingSections.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
const mergedScreenshotBody = screenshotSections
|
||||
.map((section) => section.replace(/^##\s+(?:스크린샷|화면 캡처)\s*$/m, '').trim())
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
const screenshotLines = Array.from(
|
||||
new Set(
|
||||
[mergedScreenshotBody, imageLine]
|
||||
.join('\n')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && line !== '- 저장소 기준 연결된 스크린샷 없음'),
|
||||
),
|
||||
);
|
||||
|
||||
const mergedScreenshotSection = `## 스크린샷\n\n${screenshotLines.join('\n')}`.trim();
|
||||
const sourceSectionIndex = remainingSections.findIndex((section) => /^##\s+소스\s*$/m.test(section));
|
||||
|
||||
if (sourceSectionIndex === -1) {
|
||||
remainingSections.push(mergedScreenshotSection);
|
||||
} else {
|
||||
remainingSections.splice(Math.max(1, sourceSectionIndex), 0, mergedScreenshotSection);
|
||||
}
|
||||
|
||||
content = remainingSections.join('\n\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
||||
await fs.writeFile(worklogPath, content, 'utf8');
|
||||
}
|
||||
|
||||
export function resolveCapturePaths({
|
||||
cwd = process.cwd(),
|
||||
captureDate,
|
||||
screenshotFileName,
|
||||
}) {
|
||||
const screenshotDir = path.join(cwd, 'docs', 'assets', 'worklogs', captureDate);
|
||||
const screenshotPath = path.join(screenshotDir, screenshotFileName);
|
||||
const worklogPath = path.join(cwd, 'docs', 'worklogs', `${captureDate}.md`);
|
||||
const markdownImagePath = `../assets/worklogs/${captureDate}/${screenshotFileName}`;
|
||||
|
||||
return {
|
||||
screenshotDir,
|
||||
screenshotPath,
|
||||
worklogPath,
|
||||
markdownImagePath,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user