177 lines
4.5 KiB
JavaScript
Executable File
177 lines
4.5 KiB
JavaScript
Executable File
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,
|
|
};
|
|
}
|