✨ 주요 개선사항: - 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>
276 lines
7.0 KiB
JavaScript
276 lines
7.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Linear 이슈 상태 동기화 스크립트
|
|
* GitHub 이벤트에 따라 Linear 이슈 상태를 자동으로 업데이트
|
|
*/
|
|
|
|
// Simple command line argument parsing without commander dependency
|
|
|
|
// 임시 Linear 클라이언트 (실제 구현 시 SDK 사용)
|
|
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 getIssue(issueId) {
|
|
const query = `
|
|
query GetIssue($id: String!) {
|
|
issue(id: $id) {
|
|
id
|
|
identifier
|
|
title
|
|
state {
|
|
id
|
|
name
|
|
}
|
|
team {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
const result = await this.query(query, { id: issueId });
|
|
return result.issue;
|
|
}
|
|
|
|
async updateIssue(issueId, input) {
|
|
const query = `
|
|
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
issueUpdate(id: $id, input: $input) {
|
|
success
|
|
issue {
|
|
id
|
|
state {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
const result = await this.query(query, { id: issueId, input });
|
|
return result.issueUpdate;
|
|
}
|
|
|
|
async getWorkflowStates(teamId) {
|
|
const query = `
|
|
query GetWorkflowStates($teamId: ID!) {
|
|
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
|
|
nodes {
|
|
id
|
|
name
|
|
type
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
const result = await this.query(query, { teamId });
|
|
return result.workflowStates.nodes;
|
|
}
|
|
|
|
async createComment(issueId, body) {
|
|
const query = `
|
|
mutation CreateComment($issueId: String!, $body: String!) {
|
|
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
success
|
|
comment {
|
|
id
|
|
body
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
const result = await this.query(query, { issueId, body });
|
|
return result.commentCreate;
|
|
}
|
|
}
|
|
|
|
// Simple command line argument parsing
|
|
const args = process.argv.slice(2);
|
|
const options = {
|
|
issueId: null,
|
|
event: null,
|
|
action: null,
|
|
prUrl: null,
|
|
prAuthor: null,
|
|
prMerged: "false",
|
|
apiKey: null,
|
|
};
|
|
|
|
// Extract arguments
|
|
args.forEach((arg) => {
|
|
if (arg.startsWith("--issue-id=")) {
|
|
options.issueId = arg.split("=")[1];
|
|
} else if (arg.startsWith("--event=")) {
|
|
options.event = arg.split("=")[1];
|
|
} else if (arg.startsWith("--action=")) {
|
|
options.action = arg.split("=")[1];
|
|
} else if (arg.startsWith("--pr-url=")) {
|
|
options.prUrl = arg.split("=")[1];
|
|
} else if (arg.startsWith("--pr-author=")) {
|
|
options.prAuthor = arg.split("=")[1];
|
|
} else if (arg.startsWith("--pr-merged=")) {
|
|
options.prMerged = arg.split("=")[1];
|
|
} else if (arg.startsWith("--api-key=")) {
|
|
options.apiKey = arg.split("=")[1];
|
|
}
|
|
});
|
|
|
|
// Linear 클라이언트 초기화
|
|
const apiKey = options.apiKey || process.env.LINEAR_API_KEY;
|
|
if (!apiKey) {
|
|
console.error("Error: Linear API key not provided");
|
|
process.exit(1);
|
|
}
|
|
|
|
const linear = new LinearClient({ apiKey });
|
|
|
|
/**
|
|
* 이슈 상태 업데이트
|
|
*/
|
|
async function updateIssueStatus() {
|
|
try {
|
|
if (!options.issueId) {
|
|
console.log("No Linear issue ID provided, skipping");
|
|
return;
|
|
}
|
|
|
|
console.log(`Processing Linear issue: ${options.issueId}`);
|
|
|
|
// 이슈 조회
|
|
const issue = await linear.getIssue(options.issueId);
|
|
|
|
if (!issue) {
|
|
console.error(`Issue ${options.issueId} not found`);
|
|
return;
|
|
}
|
|
|
|
console.log(`Found issue: ${issue.identifier} - ${issue.title}`);
|
|
console.log(`Current state: ${issue.state.name}`);
|
|
|
|
// 상태 변경 로직
|
|
let newStateName = null;
|
|
let comment = null;
|
|
|
|
if (options.event === "pull_request") {
|
|
switch (options.action) {
|
|
case "opened":
|
|
newStateName = "In Progress";
|
|
comment = `🔗 Pull Request opened: ${options.prUrl}\nAuthor: @${options.prAuthor}`;
|
|
break;
|
|
|
|
case "ready_for_review":
|
|
newStateName = "In Review";
|
|
comment = `👀 Pull Request is ready for review: ${options.prUrl}`;
|
|
break;
|
|
|
|
case "closed":
|
|
if (options.prMerged === "true") {
|
|
newStateName = "Done";
|
|
comment = `✅ Pull Request merged: ${options.prUrl}`;
|
|
} else {
|
|
comment = `❌ Pull Request closed without merging: ${options.prUrl}`;
|
|
}
|
|
break;
|
|
}
|
|
} else if (options.event === "push" && options.action === "commit") {
|
|
// 커밋 푸시 시 In Progress로 변경
|
|
if (issue.state.name === "Todo" || issue.state.name === "Backlog") {
|
|
newStateName = "In Progress";
|
|
comment = `💻 Work started with new commits`;
|
|
}
|
|
}
|
|
|
|
// 상태 업데이트 필요시
|
|
if (newStateName && newStateName !== issue.state.name) {
|
|
console.log(`Updating state: ${issue.state.name} → ${newStateName}`);
|
|
|
|
// 팀의 워크플로우 상태 조회
|
|
const states = await linear.getWorkflowStates(issue.team.id);
|
|
const targetState = states.find((s) => s.name === newStateName);
|
|
|
|
if (targetState) {
|
|
const result = await linear.updateIssue(issue.id, {
|
|
stateId: targetState.id,
|
|
});
|
|
|
|
if (result.success) {
|
|
console.log(
|
|
`✅ Successfully updated issue state to: ${newStateName}`
|
|
);
|
|
} else {
|
|
console.error("Failed to update issue state");
|
|
}
|
|
} else {
|
|
console.warn(`State "${newStateName}" not found in team workflow`);
|
|
}
|
|
}
|
|
|
|
// 코멘트 추가
|
|
if (comment) {
|
|
const commentResult = await linear.createComment(issue.id, comment);
|
|
if (commentResult.success) {
|
|
console.log("✅ Added comment to issue");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error syncing with Linear:", error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 디버그 정보 출력
|
|
*/
|
|
function debugInfo() {
|
|
console.log("\n=== Linear Sync Debug Info ===");
|
|
console.log("Options:", {
|
|
issueId: options.issueId,
|
|
event: options.event,
|
|
action: options.action,
|
|
prUrl: options.prUrl,
|
|
prAuthor: options.prAuthor,
|
|
prMerged: options.prMerged,
|
|
});
|
|
console.log("API Key:", apiKey ? "Provided" : "Missing");
|
|
console.log("==============================\n");
|
|
}
|
|
|
|
// 메인 실행
|
|
async function main() {
|
|
console.log("🔄 Starting Linear sync...");
|
|
|
|
if (process.env.DEBUG) {
|
|
debugInfo();
|
|
}
|
|
|
|
await updateIssueStatus();
|
|
|
|
console.log("✅ Linear sync completed");
|
|
}
|
|
|
|
// 실행
|
|
main().catch((error) => {
|
|
console.error("Fatal error:", error);
|
|
process.exit(1);
|
|
});
|