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 = `![${imageAlt}](${markdownImagePath})`; 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, }; }