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:
275
scripts/linear-sync.cjs
Normal file
275
scripts/linear-sync.cjs
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user