✨ 주요 개선사항: - 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>
647 lines
17 KiB
JavaScript
647 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Linear GitHub 앱 연동 및 기본 연결 설정 스크립트
|
||
* Linear와 GitHub 간의 기본 연동을 설정하고 검증
|
||
*/
|
||
|
||
const { execSync } = require("child_process");
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
|
||
// Simple command line argument parsing
|
||
const args = process.argv.slice(2);
|
||
const options = {
|
||
linearApiKey: null,
|
||
githubToken: null,
|
||
setup: args.includes("--setup"),
|
||
verify: args.includes("--verify"),
|
||
configure: args.includes("--configure"),
|
||
help: args.includes("--help") || args.includes("-h"),
|
||
};
|
||
|
||
// Extract API keys from arguments
|
||
const linearApiIndex = args.findIndex((arg) => arg.startsWith("--linear-api="));
|
||
if (linearApiIndex !== -1) {
|
||
options.linearApiKey = args[linearApiIndex].split("=")[1];
|
||
}
|
||
|
||
const githubTokenIndex = args.findIndex((arg) =>
|
||
arg.startsWith("--github-token=")
|
||
);
|
||
if (githubTokenIndex !== -1) {
|
||
options.githubToken = args[githubTokenIndex].split("=")[1];
|
||
}
|
||
|
||
/**
|
||
* Linear API 클라이언트
|
||
*/
|
||
class LinearClient {
|
||
constructor(apiKey) {
|
||
this.apiKey = apiKey;
|
||
this.baseUrl = "https://api.linear.app/graphql";
|
||
}
|
||
|
||
async query(query, variables = {}) {
|
||
try {
|
||
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;
|
||
} catch (error) {
|
||
throw new Error(`Linear API call failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async getWorkspaceInfo() {
|
||
const query = `
|
||
query GetWorkspaceInfo {
|
||
organization {
|
||
id
|
||
name
|
||
urlKey
|
||
teams {
|
||
nodes {
|
||
id
|
||
name
|
||
key
|
||
issueCount
|
||
}
|
||
}
|
||
}
|
||
viewer {
|
||
id
|
||
name
|
||
email
|
||
}
|
||
}
|
||
`;
|
||
|
||
return await this.query(query);
|
||
}
|
||
|
||
async createWebhook(url, teamId) {
|
||
const query = `
|
||
mutation CreateWebhook($url: String!, $teamId: String!) {
|
||
webhookCreate(input: {
|
||
url: $url
|
||
teamId: $teamId
|
||
allPublicTeams: false
|
||
resourceTypes: ["Issue", "IssueLabel", "Comment"]
|
||
}) {
|
||
success
|
||
webhook {
|
||
id
|
||
url
|
||
enabled
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
return await this.query(query, { url, teamId });
|
||
}
|
||
|
||
async listWebhooks() {
|
||
const query = `
|
||
query ListWebhooks {
|
||
webhooks {
|
||
nodes {
|
||
id
|
||
url
|
||
enabled
|
||
resourceTypes
|
||
team {
|
||
name
|
||
key
|
||
}
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
return await this.query(query);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GitHub API 클라이언트
|
||
*/
|
||
class GitHubClient {
|
||
constructor(token, repo) {
|
||
this.token = token;
|
||
this.repo = repo;
|
||
this.baseUrl = "https://api.github.com";
|
||
}
|
||
|
||
async request(endpoint, method = "GET", data = null) {
|
||
const url = `${this.baseUrl}${endpoint}`;
|
||
const options = {
|
||
method,
|
||
headers: {
|
||
Authorization: `token ${this.token}`,
|
||
Accept: "application/vnd.github.v3+json",
|
||
"User-Agent": "Zellyy-Finance-Linear-Integration",
|
||
},
|
||
};
|
||
|
||
if (data) {
|
||
options.headers["Content-Type"] = "application/json";
|
||
options.body = JSON.stringify(data);
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(url, options);
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.text();
|
||
throw new Error(`GitHub API Error: ${response.status} ${errorData}`);
|
||
}
|
||
|
||
return await response.json();
|
||
} catch (error) {
|
||
throw new Error(`GitHub API call failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async getRepository() {
|
||
return await this.request(`/repos/${this.repo}`);
|
||
}
|
||
|
||
async createWebhook(config) {
|
||
return await this.request(`/repos/${this.repo}/hooks`, "POST", config);
|
||
}
|
||
|
||
async listWebhooks() {
|
||
return await this.request(`/repos/${this.repo}/hooks`);
|
||
}
|
||
|
||
async createSecret(name, encryptedValue) {
|
||
return await this.request(
|
||
`/repos/${this.repo}/actions/secrets/${name}`,
|
||
"PUT",
|
||
{
|
||
encrypted_value: encryptedValue,
|
||
}
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 환경 설정 검증
|
||
*/
|
||
function validateEnvironment() {
|
||
console.log("🔍 환경 설정 검증 중...");
|
||
|
||
const requiredFiles = [
|
||
".github/workflows/linear-integration.yml",
|
||
"scripts/linear-sync.cjs",
|
||
"scripts/linear-comment.cjs",
|
||
".env.linear.example",
|
||
];
|
||
|
||
const missingFiles = [];
|
||
|
||
requiredFiles.forEach((file) => {
|
||
if (!fs.existsSync(file)) {
|
||
missingFiles.push(file);
|
||
}
|
||
});
|
||
|
||
if (missingFiles.length > 0) {
|
||
console.error("❌ 필수 파일들이 누락되었습니다:");
|
||
missingFiles.forEach((file) => console.error(` - ${file}`));
|
||
return false;
|
||
}
|
||
|
||
console.log("✅ 모든 필수 파일이 존재합니다.");
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Git 리포지토리 정보 확인
|
||
*/
|
||
function getGitInfo() {
|
||
try {
|
||
// GitHub remote이 있는지 먼저 확인
|
||
let remoteUrl;
|
||
try {
|
||
remoteUrl = execSync("git config --get remote.github.url", {
|
||
encoding: "utf8",
|
||
}).trim();
|
||
} catch {
|
||
// github remote가 없으면 origin 확인
|
||
remoteUrl = execSync("git config --get remote.origin.url", {
|
||
encoding: "utf8",
|
||
}).trim();
|
||
}
|
||
|
||
// GitHub URL에서 owner/repo 추출
|
||
const match = remoteUrl.match(/github\.com[/:](.*?)\/(.*)(?:\.git)?$/);
|
||
if (match) {
|
||
const owner = match[1];
|
||
const repo = match[2].replace(".git", "");
|
||
return { owner, repo, fullName: `${owner}/${repo}` };
|
||
}
|
||
|
||
// GitHub remote가 아닌 경우 수동으로 GitHub 정보 사용
|
||
console.log(
|
||
"ℹ️ GitHub remote가 감지되지 않았습니다. zellycloud/zellyy-finance를 사용합니다."
|
||
);
|
||
return {
|
||
owner: "zellycloud",
|
||
repo: "zellyy-finance",
|
||
fullName: "zellycloud/zellyy-finance",
|
||
};
|
||
} catch (error) {
|
||
throw new Error(`Git 정보를 가져올 수 없습니다: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Linear 워크스페이스 정보 출력
|
||
*/
|
||
async function displayLinearInfo(linear) {
|
||
console.log("\n📋 Linear 워크스페이스 정보:");
|
||
|
||
try {
|
||
const info = await linear.getWorkspaceInfo();
|
||
|
||
console.log(
|
||
` 조직: ${info.organization.name} (${info.organization.urlKey})`
|
||
);
|
||
console.log(` 사용자: ${info.viewer.name} (${info.viewer.email})`);
|
||
console.log(` 팀 수: ${info.organization.teams.nodes.length}`);
|
||
|
||
console.log("\n👥 팀 목록:");
|
||
info.organization.teams.nodes.forEach((team) => {
|
||
console.log(` - ${team.name} (${team.key}): ${team.issueCount}개 이슈`);
|
||
});
|
||
|
||
return info;
|
||
} catch (error) {
|
||
console.error("❌ Linear 정보를 가져올 수 없습니다:", error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GitHub 리포지토리 정보 출력
|
||
*/
|
||
async function displayGitHubInfo(github) {
|
||
console.log("\n📁 GitHub 리포지토리 정보:");
|
||
|
||
try {
|
||
const repo = await github.getRepository();
|
||
|
||
console.log(` 이름: ${repo.full_name}`);
|
||
console.log(` 설명: ${repo.description || "설명 없음"}`);
|
||
console.log(` 언어: ${repo.language || "N/A"}`);
|
||
console.log(` 프라이빗: ${repo.private ? "Yes" : "No"}`);
|
||
console.log(` 기본 브랜치: ${repo.default_branch}`);
|
||
|
||
return repo;
|
||
} catch (error) {
|
||
console.error("❌ GitHub 정보를 가져올 수 없습니다:", error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 환경 변수 파일 생성
|
||
*/
|
||
function createEnvFile(linearInfo, gitInfo) {
|
||
console.log("\n📝 환경 변수 파일 생성 중...");
|
||
|
||
const envContent = `# Linear GitHub 연동 설정
|
||
# 이 파일을 .env로 복사하고 실제 값으로 업데이트하세요
|
||
|
||
# Linear API 설정
|
||
LINEAR_API_KEY=${options.linearApiKey || "your-linear-api-key"}
|
||
LINEAR_WORKSPACE_ID=${linearInfo.organization.id}
|
||
LINEAR_TEAM_ID=${linearInfo.organization.teams.nodes[0]?.id || "your-team-id"}
|
||
|
||
# GitHub 설정
|
||
GITHUB_TOKEN=${options.githubToken || "your-github-token"}
|
||
GITHUB_REPOSITORY=${gitInfo.fullName}
|
||
|
||
# 웹훅 URL (GitHub Actions에서 자동 설정)
|
||
LINEAR_WEBHOOK_URL=https://api.github.com/repos/${gitInfo.fullName}/dispatches
|
||
|
||
# 디버그 모드
|
||
DEBUG=false
|
||
`;
|
||
|
||
const envFile = ".env.linear";
|
||
fs.writeFileSync(envFile, envContent);
|
||
|
||
console.log(`✅ 환경 변수 파일이 생성되었습니다: ${envFile}`);
|
||
console.log(" 이 파일을 .env로 복사하거나 기존 .env에 내용을 추가하세요.");
|
||
}
|
||
|
||
/**
|
||
* GitHub Secrets 설정 안내
|
||
*/
|
||
function displaySecretsGuide() {
|
||
console.log("\n🔐 GitHub Secrets 설정 안내:");
|
||
console.log("다음 secrets을 GitHub 리포지토리에 추가해야 합니다:");
|
||
console.log("");
|
||
console.log("1. Repository Settings → Secrets and variables → Actions");
|
||
console.log('2. "New repository secret" 클릭');
|
||
console.log("3. 다음 secrets 추가:");
|
||
console.log(" - Name: LINEAR_API_KEY");
|
||
console.log(` - Value: ${options.linearApiKey || "your-linear-api-key"}`);
|
||
console.log("");
|
||
console.log("선택적 secrets:");
|
||
console.log(" - SLACK_BOT_TOKEN (Slack 연동용)");
|
||
console.log(" - SLACK_WEBHOOK_URL (Slack 알림용)");
|
||
}
|
||
|
||
/**
|
||
* Linear 웹훅 설정
|
||
*/
|
||
async function setupLinearWebhooks(linear, gitInfo) {
|
||
console.log("\n🔗 Linear 웹훅 설정 중...");
|
||
|
||
try {
|
||
// 기존 웹훅 확인
|
||
const existingWebhooks = await linear.listWebhooks();
|
||
const githubWebhook = existingWebhooks.webhooks.nodes.find(
|
||
(webhook) =>
|
||
webhook.url.includes("github.com") ||
|
||
webhook.url.includes(gitInfo.fullName)
|
||
);
|
||
|
||
if (githubWebhook) {
|
||
console.log("✅ GitHub 웹훅이 이미 설정되어 있습니다:");
|
||
console.log(` URL: ${githubWebhook.url}`);
|
||
console.log(` 활성화: ${githubWebhook.enabled ? "Yes" : "No"}`);
|
||
console.log(` 팀: ${githubWebhook.team?.name || "All teams"}`);
|
||
return;
|
||
}
|
||
|
||
console.log("ℹ️ Linear 웹훅 설정은 수동으로 진행해야 합니다:");
|
||
console.log("1. Linear → Settings → API → Webhooks");
|
||
console.log('2. "Create webhook" 클릭');
|
||
console.log(
|
||
`3. URL: https://api.github.com/repos/${gitInfo.fullName}/dispatches`
|
||
);
|
||
console.log("4. Resource types: Issue, Comment, IssueLabel 선택");
|
||
console.log("5. Team: 원하는 팀 선택 (또는 모든 팀)");
|
||
} catch (error) {
|
||
console.error("❌ 웹훅 설정 중 오류 발생:", error.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GitHub Actions 워크플로우 검증
|
||
*/
|
||
function verifyWorkflow() {
|
||
console.log("\n🔧 GitHub Actions 워크플로우 검증 중...");
|
||
|
||
const workflowPath = ".github/workflows/linear-integration.yml";
|
||
|
||
if (!fs.existsSync(workflowPath)) {
|
||
console.error("❌ Linear integration 워크플로우를 찾을 수 없습니다.");
|
||
return false;
|
||
}
|
||
|
||
const workflow = fs.readFileSync(workflowPath, "utf8");
|
||
|
||
const checks = [
|
||
{ name: "Pull request triggers", pattern: /on:\s*\n.*pull_request:/s },
|
||
{ name: "Push triggers", pattern: /push:/ },
|
||
{ name: "Linear ID extraction", pattern: /extract.*linear.*id/i },
|
||
{ name: "Environment variables", pattern: /LINEAR_API_KEY/ },
|
||
{ name: "Sync scripts", pattern: /linear-sync/ },
|
||
{ name: "Comment scripts", pattern: /linear-comment/ },
|
||
];
|
||
|
||
let allPassed = true;
|
||
|
||
checks.forEach((check) => {
|
||
if (check.pattern.test(workflow)) {
|
||
console.log(` ✅ ${check.name}`);
|
||
} else {
|
||
console.log(` ❌ ${check.name} 누락`);
|
||
allPassed = false;
|
||
}
|
||
});
|
||
|
||
return allPassed;
|
||
}
|
||
|
||
/**
|
||
* 통합 테스트 실행
|
||
*/
|
||
async function runIntegrationTest() {
|
||
console.log("\n🧪 통합 테스트 실행 중...");
|
||
|
||
try {
|
||
const testCommand = `node scripts/test-linear-integration.cjs --api-key=${options.linearApiKey}`;
|
||
const result = execSync(testCommand, { encoding: "utf8", stdio: "pipe" });
|
||
|
||
console.log("✅ 통합 테스트 성공");
|
||
console.log(result);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("❌ 통합 테스트 실패:", error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 설정 완료 안내
|
||
*/
|
||
function displaySetupComplete() {
|
||
console.log("\n🎉 Linear GitHub 연동 설정이 완료되었습니다!");
|
||
console.log("");
|
||
console.log("다음 단계:");
|
||
console.log("1. GitHub Secrets에 LINEAR_API_KEY 추가");
|
||
console.log("2. Linear에서 웹훅 설정 (위의 안내 참조)");
|
||
console.log("3. Pull Request 생성하여 연동 테스트");
|
||
console.log("");
|
||
console.log("테스트 방법:");
|
||
console.log(
|
||
"1. 새 브랜치 생성: git checkout -b feature/test-linear-integration"
|
||
);
|
||
console.log(
|
||
'2. 커밋 메시지에 Linear 이슈 ID 포함: git commit -m "feat: test integration [ZEL-1]"'
|
||
);
|
||
console.log(
|
||
'3. Pull Request 제목에 Linear 이슈 ID 포함: "Test Linear integration (ZEL-1)"'
|
||
);
|
||
console.log("4. GitHub Actions 로그에서 연동 동작 확인");
|
||
}
|
||
|
||
/**
|
||
* 사용법 출력
|
||
*/
|
||
function printUsage() {
|
||
console.log(`
|
||
🔧 Linear GitHub 연동 설정 도구
|
||
|
||
사용법:
|
||
node scripts/linear-github-setup.cjs [옵션]
|
||
|
||
옵션:
|
||
--setup 전체 설정 실행
|
||
--verify 기존 설정 검증
|
||
--configure 환경 설정만 생성
|
||
--linear-api=KEY Linear API 키 지정
|
||
--github-token=TOKEN GitHub 토큰 지정
|
||
--help, -h 이 도움말 출력
|
||
|
||
예시:
|
||
# 전체 설정 실행
|
||
node scripts/linear-github-setup.cjs --setup --linear-api=lin_api_xxx
|
||
|
||
# 기존 설정 검증
|
||
node scripts/linear-github-setup.cjs --verify
|
||
|
||
# 환경 설정만 생성
|
||
node scripts/linear-github-setup.cjs --configure --linear-api=lin_api_xxx
|
||
|
||
환경 변수:
|
||
LINEAR_API_KEY Linear API 키
|
||
GITHUB_TOKEN GitHub 개인 액세스 토큰
|
||
|
||
필수 조건:
|
||
- Git 리포지토리 (GitHub remote 설정됨)
|
||
- Linear API 키
|
||
- GitHub 개인 액세스 토큰 (repo 권한)
|
||
`);
|
||
}
|
||
|
||
/**
|
||
* 메인 함수
|
||
*/
|
||
async function main() {
|
||
if (options.help) {
|
||
printUsage();
|
||
process.exit(0);
|
||
}
|
||
|
||
console.log("🚀 Linear GitHub 연동 설정 도구");
|
||
console.log("=====================================\n");
|
||
|
||
// 환경 검증
|
||
if (!validateEnvironment()) {
|
||
console.error(
|
||
"❌ 환경 설정이 올바르지 않습니다. 먼저 필수 파일들을 생성해주세요."
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
// Git 정보 확인
|
||
let gitInfo;
|
||
try {
|
||
gitInfo = getGitInfo();
|
||
console.log(`✅ Git 리포지토리: ${gitInfo.fullName}`);
|
||
} catch (error) {
|
||
console.error("❌", error.message);
|
||
process.exit(1);
|
||
}
|
||
|
||
// API 키 설정
|
||
const linearApiKey = options.linearApiKey || process.env.LINEAR_API_KEY;
|
||
const githubToken = options.githubToken || process.env.GITHUB_TOKEN;
|
||
|
||
if (!linearApiKey) {
|
||
console.error(
|
||
"❌ Linear API 키가 필요합니다. --linear-api 옵션을 사용하거나 LINEAR_API_KEY 환경 변수를 설정하세요."
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
// 클라이언트 초기화
|
||
const linear = new LinearClient(linearApiKey);
|
||
const github = githubToken
|
||
? new GitHubClient(githubToken, gitInfo.fullName)
|
||
: null;
|
||
|
||
// 검증 모드
|
||
if (options.verify) {
|
||
console.log("🔍 기존 설정 검증 중...\n");
|
||
|
||
const linearInfo = await displayLinearInfo(linear);
|
||
if (github) await displayGitHubInfo(github);
|
||
|
||
const workflowValid = verifyWorkflow();
|
||
const testPassed = await runIntegrationTest();
|
||
|
||
if (workflowValid && testPassed) {
|
||
console.log("\n✅ 모든 검증이 성공했습니다!");
|
||
} else {
|
||
console.log("\n❌ 일부 검증이 실패했습니다.");
|
||
process.exit(1);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
// 설정 모드
|
||
if (options.setup || options.configure) {
|
||
console.log("⚙️ Linear GitHub 연동 설정 시작...\n");
|
||
|
||
// Linear 정보 확인
|
||
const linearInfo = await displayLinearInfo(linear);
|
||
|
||
// GitHub 정보 확인 (토큰이 있는 경우)
|
||
if (github) {
|
||
await displayGitHubInfo(github);
|
||
}
|
||
|
||
// 환경 설정 파일 생성
|
||
createEnvFile(linearInfo, gitInfo);
|
||
|
||
if (options.setup) {
|
||
// 웹훅 설정
|
||
await setupLinearWebhooks(linear, gitInfo);
|
||
|
||
// Secrets 안내
|
||
displaySecretsGuide();
|
||
|
||
// 워크플로우 검증
|
||
verifyWorkflow();
|
||
|
||
// 완료 안내
|
||
displaySetupComplete();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
// 기본 동작: 정보 표시
|
||
const linearInfo = await displayLinearInfo(linear);
|
||
if (github) await displayGitHubInfo(github);
|
||
|
||
console.log("\n💡 다음 명령어로 설정을 진행하세요:");
|
||
console.log(
|
||
` node scripts/linear-github-setup.cjs --setup --linear-api=${linearApiKey}`
|
||
);
|
||
}
|
||
|
||
// 실행
|
||
if (require.main === module) {
|
||
main().catch((error) => {
|
||
console.error("\n❌ 설정 중 오류 발생:", error.message);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
module.exports = { LinearClient, GitHubClient, validateEnvironment };
|