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:
429
scripts/linear-release-complete.cjs
Normal file
429
scripts/linear-release-complete.cjs
Normal file
@@ -0,0 +1,429 @@
|
||||
#!/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 createProject(name, description) {
|
||||
const query = `
|
||||
mutation CreateProject($name: String!, $description: String!) {
|
||||
projectCreate(input: { name: $name, description: $description }) {
|
||||
success
|
||||
project {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await this.query(query, { name, description });
|
||||
return result.projectCreate;
|
||||
}
|
||||
|
||||
async createMilestone(projectId, name, targetDate) {
|
||||
const query = `
|
||||
mutation CreateMilestone($projectId: String!, $name: String!, $targetDate: DateTime) {
|
||||
projectMilestoneCreate(input: {
|
||||
projectId: $projectId,
|
||||
name: $name,
|
||||
targetDate: $targetDate
|
||||
}) {
|
||||
success
|
||||
projectMilestone {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await this.query(query, { projectId, name, targetDate });
|
||||
return result.projectMilestoneCreate;
|
||||
}
|
||||
|
||||
async getIssuesFromMetadata(metadataPath) {
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
console.warn(`Metadata file not found: ${metadataPath}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
||||
return metadata.issues || [];
|
||||
}
|
||||
|
||||
async addCommentToIssue(issueId, body) {
|
||||
const query = `
|
||||
mutation CreateComment($issueId: String!, $body: String!) {
|
||||
commentCreate(input: { issueId: $issueId, body: $body }) {
|
||||
success
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await this.query(query, { issueId, body });
|
||||
return result.commentCreate;
|
||||
}
|
||||
|
||||
async moveIssuesToProject(issueIds, projectId) {
|
||||
const query = `
|
||||
mutation MoveIssuesToProject($issueIds: [String!]!, $projectId: String!) {
|
||||
issueBatchUpdate(input: { ids: $issueIds, projectId: $projectId }) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await this.query(query, { issueIds, projectId });
|
||||
return result.issueBatchUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
const version = process.argv[2];
|
||||
const apiKey = process.env.LINEAR_API_KEY;
|
||||
|
||||
if (!version) {
|
||||
console.error("Usage: node linear-release-complete.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 generateReleaseMessage(version, githubUrl) {
|
||||
return `🎉 **릴리즈 v${version} 완료!**
|
||||
|
||||
이 이슈는 v${version} 릴리즈에 포함되었습니다.
|
||||
|
||||
**릴리즈 정보:**
|
||||
- 버전: v${version}
|
||||
- 날짜: ${new Date().toLocaleDateString("ko-KR")}
|
||||
- GitHub: ${githubUrl}
|
||||
|
||||
감사합니다! 🚀`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 릴리즈 통계 생성
|
||||
*/
|
||||
function generateReleaseStats(issues) {
|
||||
const stats = {
|
||||
total: issues.length,
|
||||
byTeam: {},
|
||||
byPriority: {},
|
||||
byType: {},
|
||||
};
|
||||
|
||||
issues.forEach((issue) => {
|
||||
// 팀별 통계
|
||||
const team = issue.team || "Unassigned";
|
||||
stats.byTeam[team] = (stats.byTeam[team] || 0) + 1;
|
||||
|
||||
// 우선순위별 통계
|
||||
const priority = `P${issue.priority || 0}`;
|
||||
stats.byPriority[priority] = (stats.byPriority[priority] || 0) + 1;
|
||||
|
||||
// 타입별 통계 (라벨 기반)
|
||||
const labels = issue.labels || [];
|
||||
if (labels.includes("feature")) {
|
||||
stats.byType.features = (stats.byType.features || 0) + 1;
|
||||
} else if (labels.includes("bug")) {
|
||||
stats.byType.bugs = (stats.byType.bugs || 0) + 1;
|
||||
} else if (labels.includes("improvement")) {
|
||||
stats.byType.improvements = (stats.byType.improvements || 0) + 1;
|
||||
} else {
|
||||
stats.byType.other = (stats.byType.other || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack 알림 전송 (시뮬레이션)
|
||||
*/
|
||||
async function sendSlackNotification(version, stats, githubUrl) {
|
||||
const slackWebhook = process.env.SLACK_WEBHOOK_URL;
|
||||
|
||||
if (!slackWebhook) {
|
||||
console.log("📢 Slack webhook not configured, skipping notification");
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
text: `🎉 Zellyy Finance v${version} 릴리즈 완료!`,
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: `🎉 Zellyy Finance v${version} 릴리즈!`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*총 ${stats.total}개 이슈*가 완료되었습니다.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*새 기능:* ${stats.byType.features || 0}개`,
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*버그 수정:* ${stats.byType.bugs || 0}개`,
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*개선사항:* ${stats.byType.improvements || 0}개`,
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*기타:* ${stats.byType.other || 0}개`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "GitHub 릴리즈 보기",
|
||||
},
|
||||
url: githubUrl,
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(slackWebhook, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log("✅ Slack notification sent");
|
||||
} else {
|
||||
console.warn("⚠️ Failed to send Slack notification");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Error sending Slack notification:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 릴리즈 완료 메인 함수
|
||||
*/
|
||||
async function completeRelease() {
|
||||
try {
|
||||
console.log(`🏁 Completing release v${version}...`);
|
||||
|
||||
// 메타데이터 파일에서 이슈 정보 로드
|
||||
const metadataPath = path.join(
|
||||
process.cwd(),
|
||||
"releases",
|
||||
`v${version}-metadata.json`
|
||||
);
|
||||
const issues = await linear.getIssuesFromMetadata(metadataPath);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log(
|
||||
"⚠️ No issues found in metadata, skipping post-release tasks"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📋 Processing ${issues.length} issues from v${version}`);
|
||||
|
||||
// GitHub 릴리즈 URL 생성
|
||||
const githubUrl = `https://github.com/zellyy-finance/zellyy-finance/releases/tag/v${version}`;
|
||||
|
||||
// 각 이슈에 릴리즈 완료 코멘트 추가
|
||||
console.log("💬 Adding release completion comments to issues...");
|
||||
const releaseMessage = generateReleaseMessage(version, githubUrl);
|
||||
|
||||
let commentCount = 0;
|
||||
for (const issue of issues) {
|
||||
try {
|
||||
const result = await linear.addCommentToIssue(issue.id, releaseMessage);
|
||||
if (result.success) {
|
||||
commentCount++;
|
||||
console.log(` ✅ Comment added to ${issue.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
` ⚠️ Failed to add comment to ${issue.id}:`,
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Added comments to ${commentCount}/${issues.length} issues`);
|
||||
|
||||
// 릴리즈 프로젝트 생성 (옵션)
|
||||
if (process.env.CREATE_RELEASE_PROJECT === "true") {
|
||||
console.log("📁 Creating release project...");
|
||||
|
||||
try {
|
||||
const projectResult = await linear.createProject(
|
||||
`Release v${version}`,
|
||||
`Issues completed in release v${version}`
|
||||
);
|
||||
|
||||
if (projectResult.success) {
|
||||
console.log(
|
||||
`✅ Created release project: ${projectResult.project.id}`
|
||||
);
|
||||
|
||||
// 이슈들을 릴리즈 프로젝트로 이동
|
||||
const issueIds = issues.map((i) => i.id);
|
||||
const moveResult = await linear.moveIssuesToProject(
|
||||
issueIds,
|
||||
projectResult.project.id
|
||||
);
|
||||
|
||||
if (moveResult.success) {
|
||||
console.log(
|
||||
`✅ Moved ${issueIds.length} issues to release project`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Failed to create release project:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 릴리즈 통계 생성
|
||||
console.log("📊 Generating release statistics...");
|
||||
const stats = generateReleaseStats(issues);
|
||||
|
||||
// 통계 출력
|
||||
console.log("\n📈 Release Statistics:");
|
||||
console.log(` Total Issues: ${stats.total}`);
|
||||
console.log(` By Type:`);
|
||||
Object.entries(stats.byType).forEach(([type, count]) => {
|
||||
console.log(` ${type}: ${count}`);
|
||||
});
|
||||
console.log(` By Team:`);
|
||||
Object.entries(stats.byTeam).forEach(([team, count]) => {
|
||||
console.log(` ${team}: ${count}`);
|
||||
});
|
||||
|
||||
// 통계 파일 저장
|
||||
const statsPath = path.join(
|
||||
process.cwd(),
|
||||
"releases",
|
||||
`v${version}-stats.json`
|
||||
);
|
||||
fs.writeFileSync(statsPath, JSON.stringify(stats, null, 2));
|
||||
console.log(`✅ Statistics saved: ${statsPath}`);
|
||||
|
||||
// Slack 알림 전송
|
||||
console.log("📢 Sending notifications...");
|
||||
await sendSlackNotification(version, stats, githubUrl);
|
||||
|
||||
// 릴리즈 요약 리포트 생성
|
||||
const summaryPath = path.join(
|
||||
process.cwd(),
|
||||
"releases",
|
||||
`v${version}-summary.md`
|
||||
);
|
||||
const summaryContent = generateSummaryReport(version, stats, githubUrl);
|
||||
fs.writeFileSync(summaryPath, summaryContent);
|
||||
console.log(`✅ Summary report saved: ${summaryPath}`);
|
||||
|
||||
console.log("\n🎉 Release completion tasks finished successfully!");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to complete release:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 리포트 생성
|
||||
*/
|
||||
function generateSummaryReport(version, stats, githubUrl) {
|
||||
return `# Release v${version} Summary
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
- **Total Issues Completed**: ${stats.total}
|
||||
- **Release Date**: ${new Date().toLocaleDateString("ko-KR")}
|
||||
- **GitHub Release**: [v${version}](${githubUrl})
|
||||
|
||||
### By Type
|
||||
${Object.entries(stats.byType)
|
||||
.map(([type, count]) => `- **${type}**: ${count}`)
|
||||
.join("\n")}
|
||||
|
||||
### By Team
|
||||
${Object.entries(stats.byTeam)
|
||||
.map(([team, count]) => `- **${team}**: ${count}`)
|
||||
.join("\n")}
|
||||
|
||||
### By Priority
|
||||
${Object.entries(stats.byPriority)
|
||||
.map(([priority, count]) => `- **${priority}**: ${count}`)
|
||||
.join("\n")}
|
||||
|
||||
## 🎯 Key Achievements
|
||||
|
||||
This release represents significant progress in the Zellyy Finance project with ${stats.total} completed issues across multiple teams.
|
||||
|
||||
---
|
||||
|
||||
*Generated automatically by Linear release automation*
|
||||
`;
|
||||
}
|
||||
|
||||
// 실행
|
||||
completeRelease();
|
||||
Reference in New Issue
Block a user