feat: Stage 2 TypeScript 타입 안전성 개선 - any 타입 83개 → 62개 대폭 감소
✨ 주요 개선사항: - any 타입 83개에서 62개로 21개 수정 (25% 감소) - 모든 ESLint 에러 11개 → 0개 완전 해결 - 타입 안전성 대폭 향상으로 런타임 오류 가능성 감소 🔧 수정된 파일들: • PWADebug.tsx - 사용하지 않는 import들에 _ prefix 추가 • categoryUtils.ts - 불필요한 any 캐스트 제거 • TransactionsHeader.tsx - BudgetData 인터페이스 정의 • storageUtils.ts - generic 타입과 unknown 타입 적용 • 각종 error handler들 - Error | {message?: string} 타입 적용 • test 파일들 - 적절한 mock 인터페이스 정의 • 유틸리티 파일들 - any → unknown 또는 적절한 타입으로 교체 🏆 성과: - 코드 품질 크게 향상 (280 → 80 문제로 71% 감소) - TypeScript 컴파일러의 타입 체크 효과성 증대 - 개발자 경험 개선 (IDE 자동완성, 타입 추론 등) 🧹 추가 정리: - ESLint no-console/no-alert 경고 해결 - Prettier 포맷팅 적용으로 코드 스타일 통일 🎯 다음 단계: 남은 62개 any 타입 계속 개선 예정 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
420
scripts/linear-release-prep.cjs
Normal file
420
scripts/linear-release-prep.cjs
Normal file
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Linear 릴리즈 준비 스크립트
|
||||
* semantic-release와 연동하여 릴리즈 노트에 Linear 이슈 포함
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Linear 클라이언트 (동일한 구조)
|
||||
class LinearClient {
|
||||
constructor({ apiKey }) {
|
||||
this.apiKey = apiKey;
|
||||
this.baseUrl = "https://api.linear.app/graphql";
|
||||
}
|
||||
|
||||
async query(query, variables = {}) {
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: this.apiKey,
|
||||
},
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.errors) {
|
||||
throw new Error(`Linear API Error: ${JSON.stringify(data.errors)}`);
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async getRecentIssues(daysBack = 30) {
|
||||
const sinceDate = new Date();
|
||||
sinceDate.setDate(sinceDate.getDate() - daysBack);
|
||||
|
||||
const query = `
|
||||
query GetRecentIssues($since: DateTimeOrDuration!) {
|
||||
issues(
|
||||
filter: {
|
||||
updatedAt: { gte: $since },
|
||||
state: { name: { eq: "Done" } }
|
||||
},
|
||||
orderBy: updatedAt
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
identifier
|
||||
title
|
||||
description
|
||||
url
|
||||
state {
|
||||
name
|
||||
}
|
||||
priority
|
||||
labels {
|
||||
nodes {
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
assignee {
|
||||
name
|
||||
email
|
||||
}
|
||||
team {
|
||||
name
|
||||
}
|
||||
project {
|
||||
name
|
||||
}
|
||||
completedAt
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await this.query(query, { since: sinceDate.toISOString() });
|
||||
return result.issues.nodes;
|
||||
}
|
||||
|
||||
async getProjectMilestones() {
|
||||
const query = `
|
||||
query GetProjectMilestones {
|
||||
projects {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
milestones {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
targetDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await this.query(query);
|
||||
return result.projects.nodes;
|
||||
}
|
||||
|
||||
async archiveIssues(issueIds) {
|
||||
const query = `
|
||||
mutation ArchiveIssues($issueIds: [String!]!) {
|
||||
issueArchive(input: { ids: $issueIds }) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await this.query(query, { issueIds });
|
||||
return result.issueArchive;
|
||||
}
|
||||
}
|
||||
|
||||
const version = process.argv[2];
|
||||
const apiKey = process.env.LINEAR_API_KEY;
|
||||
|
||||
if (!version) {
|
||||
console.error("Usage: node linear-release-prep.js <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
console.error("Error: LINEAR_API_KEY environment variable not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const linear = new LinearClient({ apiKey });
|
||||
|
||||
/**
|
||||
* 이슈를 카테고리별로 분류
|
||||
*/
|
||||
function categorizeIssues(issues) {
|
||||
const categories = {
|
||||
features: [],
|
||||
bugfixes: [],
|
||||
improvements: [],
|
||||
tasks: [],
|
||||
other: [],
|
||||
};
|
||||
|
||||
issues.forEach((issue) => {
|
||||
const labels = issue.labels.nodes.map((l) => l.name.toLowerCase());
|
||||
const title = issue.title.toLowerCase();
|
||||
|
||||
if (labels.includes("feature") || title.includes("feat")) {
|
||||
categories.features.push(issue);
|
||||
} else if (
|
||||
labels.includes("bug") ||
|
||||
labels.includes("bugfix") ||
|
||||
title.includes("fix")
|
||||
) {
|
||||
categories.bugfixes.push(issue);
|
||||
} else if (
|
||||
labels.includes("improvement") ||
|
||||
labels.includes("enhancement")
|
||||
) {
|
||||
categories.improvements.push(issue);
|
||||
} else if (labels.includes("task") || labels.includes("chore")) {
|
||||
categories.tasks.push(issue);
|
||||
} else {
|
||||
categories.other.push(issue);
|
||||
}
|
||||
});
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* 릴리즈 노트 생성
|
||||
*/
|
||||
function generateReleaseNotes(issues, version) {
|
||||
const categorized = categorizeIssues(issues);
|
||||
const releaseDate = new Date().toLocaleDateString("ko-KR");
|
||||
|
||||
let notes = `# Zellyy Finance v${version}\n\n`;
|
||||
notes += `**릴리즈 날짜**: ${releaseDate}\n`;
|
||||
notes += `**완료된 이슈**: ${issues.length}개\n\n`;
|
||||
|
||||
// 새로운 기능
|
||||
if (categorized.features.length > 0) {
|
||||
notes += `## ✨ 새로운 기능\n\n`;
|
||||
categorized.features.forEach((issue) => {
|
||||
notes += `- ${issue.title} ([${issue.identifier}](${issue.url}))\n`;
|
||||
});
|
||||
notes += "\n";
|
||||
}
|
||||
|
||||
// 버그 수정
|
||||
if (categorized.bugfixes.length > 0) {
|
||||
notes += `## 🐛 버그 수정\n\n`;
|
||||
categorized.bugfixes.forEach((issue) => {
|
||||
notes += `- ${issue.title} ([${issue.identifier}](${issue.url}))\n`;
|
||||
});
|
||||
notes += "\n";
|
||||
}
|
||||
|
||||
// 개선사항
|
||||
if (categorized.improvements.length > 0) {
|
||||
notes += `## ⚡ 개선사항\n\n`;
|
||||
categorized.improvements.forEach((issue) => {
|
||||
notes += `- ${issue.title} ([${issue.identifier}](${issue.url}))\n`;
|
||||
});
|
||||
notes += "\n";
|
||||
}
|
||||
|
||||
// 기타 작업
|
||||
if (categorized.tasks.length > 0) {
|
||||
notes += `## 🔧 기타 작업\n\n`;
|
||||
categorized.tasks.forEach((issue) => {
|
||||
notes += `- ${issue.title} ([${issue.identifier}](${issue.url}))\n`;
|
||||
});
|
||||
notes += "\n";
|
||||
}
|
||||
|
||||
// 기타
|
||||
if (categorized.other.length > 0) {
|
||||
notes += `## 📋 기타\n\n`;
|
||||
categorized.other.forEach((issue) => {
|
||||
notes += `- ${issue.title} ([${issue.identifier}](${issue.url}))\n`;
|
||||
});
|
||||
notes += "\n";
|
||||
}
|
||||
|
||||
// 기여자 정보
|
||||
const contributors = [
|
||||
...new Set(issues.map((i) => i.assignee?.name).filter(Boolean)),
|
||||
];
|
||||
if (contributors.length > 0) {
|
||||
notes += `## 👥 기여자\n\n`;
|
||||
contributors.forEach((contributor) => {
|
||||
notes += `- ${contributor}\n`;
|
||||
});
|
||||
notes += "\n";
|
||||
}
|
||||
|
||||
notes += `---\n\n`;
|
||||
notes += `전체 변경사항은 [GitHub 릴리즈](https://github.com/zellyy-finance/zellyy-finance/releases/tag/v${version})에서 확인할 수 있습니다.\n`;
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메타데이터 생성
|
||||
*/
|
||||
function generateMetadata(issues, version) {
|
||||
const categorized = categorizeIssues(issues);
|
||||
|
||||
return {
|
||||
version,
|
||||
releaseDate: new Date().toISOString(),
|
||||
totalIssues: issues.length,
|
||||
categories: {
|
||||
features: categorized.features.length,
|
||||
bugfixes: categorized.bugfixes.length,
|
||||
improvements: categorized.improvements.length,
|
||||
tasks: categorized.tasks.length,
|
||||
other: categorized.other.length,
|
||||
},
|
||||
issues: issues.map((issue) => ({
|
||||
id: issue.identifier,
|
||||
title: issue.title,
|
||||
url: issue.url,
|
||||
team: issue.team?.name,
|
||||
project: issue.project?.name,
|
||||
assignee: issue.assignee?.name,
|
||||
labels: issue.labels.nodes.map((l) => l.name),
|
||||
priority: issue.priority,
|
||||
completedAt: issue.completedAt,
|
||||
createdAt: issue.createdAt,
|
||||
})),
|
||||
stats: {
|
||||
averageCompletionTime: calculateAverageCompletionTime(issues),
|
||||
teamDistribution: getTeamDistribution(issues),
|
||||
priorityDistribution: getPriorityDistribution(issues),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function calculateAverageCompletionTime(issues) {
|
||||
const completionTimes = issues
|
||||
.filter((i) => i.completedAt && i.createdAt)
|
||||
.map((i) => {
|
||||
const created = new Date(i.createdAt);
|
||||
const completed = new Date(i.completedAt);
|
||||
return (completed - created) / (1000 * 60 * 60 * 24); // days
|
||||
});
|
||||
|
||||
if (completionTimes.length === 0) return 0;
|
||||
return completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length;
|
||||
}
|
||||
|
||||
function getTeamDistribution(issues) {
|
||||
const distribution = {};
|
||||
issues.forEach((issue) => {
|
||||
const team = issue.team?.name || "Unassigned";
|
||||
distribution[team] = (distribution[team] || 0) + 1;
|
||||
});
|
||||
return distribution;
|
||||
}
|
||||
|
||||
function getPriorityDistribution(issues) {
|
||||
const distribution = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 };
|
||||
issues.forEach((issue) => {
|
||||
const priority = issue.priority || 0;
|
||||
distribution[priority] = (distribution[priority] || 0) + 1;
|
||||
});
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* 릴리즈 준비 메인 함수
|
||||
*/
|
||||
async function prepareRelease() {
|
||||
try {
|
||||
console.log(`🚀 Preparing release v${version}...`);
|
||||
|
||||
// 완료된 이슈 조회
|
||||
console.log("📋 Fetching completed Linear issues...");
|
||||
const issues = await linear.getRecentIssues(30);
|
||||
|
||||
console.log(`Found ${issues.length} completed issues in the last 30 days`);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log("⚠️ No completed issues found for this release");
|
||||
return;
|
||||
}
|
||||
|
||||
// 디렉토리 생성
|
||||
const releasesDir = path.join(process.cwd(), "releases");
|
||||
if (!fs.existsSync(releasesDir)) {
|
||||
fs.mkdirSync(releasesDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 릴리즈 노트 생성
|
||||
console.log("📝 Generating release notes...");
|
||||
const releaseNotes = generateReleaseNotes(issues, version);
|
||||
|
||||
// 릴리즈 노트 파일 저장
|
||||
const notesPath = path.join(releasesDir, `v${version}-notes.md`);
|
||||
fs.writeFileSync(notesPath, releaseNotes);
|
||||
console.log(`✅ Release notes saved: ${notesPath}`);
|
||||
|
||||
// 메타데이터 생성
|
||||
console.log("🔢 Generating metadata...");
|
||||
const metadata = generateMetadata(issues, version);
|
||||
|
||||
// 메타데이터 파일 저장
|
||||
const metadataPath = path.join(releasesDir, `v${version}-metadata.json`);
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
||||
console.log(`✅ Metadata saved: ${metadataPath}`);
|
||||
|
||||
// CHANGELOG.md 업데이트
|
||||
console.log("📄 Updating CHANGELOG.md...");
|
||||
updateChangelog(releaseNotes);
|
||||
|
||||
// 릴리즈 요약 출력
|
||||
console.log("\n📊 Release Summary:");
|
||||
console.log(` Version: v${version}`);
|
||||
console.log(` Total Issues: ${issues.length}`);
|
||||
console.log(` Features: ${metadata.categories.features}`);
|
||||
console.log(` Bug Fixes: ${metadata.categories.bugfixes}`);
|
||||
console.log(` Improvements: ${metadata.categories.improvements}`);
|
||||
console.log(` Tasks: ${metadata.categories.tasks}`);
|
||||
console.log(
|
||||
` Average Completion Time: ${metadata.stats.averageCompletionTime.toFixed(1)} days`
|
||||
);
|
||||
|
||||
console.log("\n✅ Release preparation completed successfully!");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to prepare release:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CHANGELOG.md 업데이트
|
||||
*/
|
||||
function updateChangelog(releaseNotes) {
|
||||
const changelogPath = "CHANGELOG.md";
|
||||
|
||||
try {
|
||||
let changelog = "";
|
||||
if (fs.existsSync(changelogPath)) {
|
||||
changelog = fs.readFileSync(changelogPath, "utf8");
|
||||
} else {
|
||||
changelog =
|
||||
"# Changelog\n\n이 파일은 Zellyy Finance의 모든 주요 변경사항을 기록합니다.\n\n";
|
||||
}
|
||||
|
||||
// 새 릴리즈 노트를 맨 위에 추가
|
||||
const lines = changelog.split("\n");
|
||||
const titleIndex = lines.findIndex((line) => line.startsWith("# "));
|
||||
|
||||
if (titleIndex !== -1) {
|
||||
// 제목 다음에 새 릴리즈 노트 삽입
|
||||
lines.splice(titleIndex + 2, 0, releaseNotes);
|
||||
changelog = lines.join("\n");
|
||||
} else {
|
||||
// 제목이 없으면 처음에 추가
|
||||
changelog = releaseNotes + "\n" + changelog;
|
||||
}
|
||||
|
||||
fs.writeFileSync(changelogPath, changelog);
|
||||
console.log(`✅ Updated ${changelogPath}`);
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Failed to update CHANGELOG.md:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 실행
|
||||
prepareRelease();
|
||||
Reference in New Issue
Block a user