✨ 주요 개선사항: - 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>
16 KiB
16 KiB
Linear 프로젝트 관리 도구 연동 가이드
개요
이 가이드는 Zellyy Finance 프로젝트에 Linear.app 프로젝트 관리 도구를 완전히 연동하는 방법을 설명합니다. Linear와 GitHub의 양방향 동기화, 자동화된 워크플로우, 실시간 리포팅 시스템 구축을 다룹니다.
목차
1. Linear 계정 및 프로젝트 설정
1.1 Linear 워크스페이스 생성
- Linear.app 접속
- "Create workspace" 클릭
- 워크스페이스 정보 입력:
- Workspace name:
Zellyy Finance - URL:
zellyy-finance - Plan: Professional (권장)
- Workspace name:
1.2 프로젝트 구조 설정
# 프로젝트 구조
Zellyy Finance/
├── Teams/
│ ├── Frontend
│ ├── Backend
│ └── DevOps
├── Projects/
│ ├── Web App
│ ├── Mobile App
│ └── Infrastructure
└── Roadmap/
├── Q1 2025
├── Q2 2025
└── Q3 2025
1.3 이슈 타입 및 라벨 체계
이슈 타입
- Epic: 대규모 기능 그룹
- Feature: 새로운 기능
- Bug: 버그 수정
- Task: 일반 작업
- Improvement: 개선 사항
라벨 체계
Priority:
- P0: Critical
- P1: High
- P2: Medium
- P3: Low
Type:
- frontend
- backend
- mobile
- devops
- security
- performance
Status:
- backlog
- todo
- in-progress
- review
- done
- cancelled
1.4 워크플로우 상태 정의
graph LR
A[Backlog] --> B[Todo]
B --> C[In Progress]
C --> D[In Review]
D --> E[Done]
C --> F[Blocked]
F --> C
B --> G[Cancelled]
2. GitHub 연동 설정
2.1 Linear GitHub 앱 설치
- Linear 설정 → Integrations → GitHub
- "Install GitHub App" 클릭
- 권한 승인:
- Repository access:
zellyy-finance - Permissions: Read & Write
- Repository access:
2.2 브랜치 명명 규칙
# Linear 이슈 ID 기반 브랜치명
feature/ZEL-123-user-authentication
bugfix/ZEL-456-login-error
task/ZEL-789-update-dependencies
2.3 커밋 메시지 규칙
# Linear 이슈 자동 연결
git commit -m "feat: implement user authentication [ZEL-123]"
git commit -m "fix: resolve login error (Fixes ZEL-456)"
git commit -m "chore: update dependencies - ZEL-789"
2.4 PR 템플릿 설정
.github/pull_request_template.md:
## 개요
<!-- PR 설명 -->
## Linear 이슈
Closes ZEL-XXX
## 변경 사항
- [ ] 기능 A 구현
- [ ] 버그 B 수정
- [ ] 테스트 추가
## 테스트
- [ ] 유닛 테스트 통과
- [ ] E2E 테스트 통과
- [ ] 수동 테스트 완료
## 스크린샷
<!-- 필요시 스크린샷 첨부 -->
3. 워크플로우 자동화
3.1 Linear API 설정
- Linear Settings → API → Personal API keys
- "Create key" 클릭
- Key name:
zellyy-finance-automation - 생성된 키를 GitHub Secrets에 저장:
LINEAR_API_KEY=lin_api_xxxxxxxxxxxxxxxx
3.2 GitHub Actions 워크플로우
.github/workflows/linear-integration.yml:
name: Linear Integration
on:
pull_request:
types: [opened, closed, ready_for_review]
issues:
types: [opened, closed]
issue_comment:
types: [created]
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
jobs:
sync-linear:
name: Sync with Linear
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Extract Linear Issue ID
id: linear-issue
run: |
# PR 제목/본문에서 Linear 이슈 ID 추출
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
ISSUE_ID=$(echo "${{ github.event.pull_request.title }} ${{ github.event.pull_request.body }}" | grep -oE 'ZEL-[0-9]+' | head -1)
fi
echo "issue_id=$ISSUE_ID" >> $GITHUB_OUTPUT
- name: Update Linear Issue Status
if: steps.linear-issue.outputs.issue_id
run: |
node scripts/linear-sync.js \
--issue-id="${{ steps.linear-issue.outputs.issue_id }}" \
--event="${{ github.event_name }}" \
--action="${{ github.event.action }}"
- name: Create Linear Comment
if: github.event_name == 'pull_request' && github.event.action == 'opened'
run: |
node scripts/linear-comment.js \
--issue-id="${{ steps.linear-issue.outputs.issue_id }}" \
--pr-url="${{ github.event.pull_request.html_url }}" \
--pr-author="${{ github.event.pull_request.user.login }}"
3.3 Linear 동기화 스크립트
scripts/linear-sync.js:
#!/usr/bin/env node
const { LinearClient } = require('@linear/sdk');
const { program } = require('commander');
// Linear 클라이언트 초기화
const linear = new LinearClient({
apiKey: process.env.LINEAR_API_KEY
});
program
.option('--issue-id <id>', 'Linear issue ID')
.option('--event <event>', 'GitHub event type')
.option('--action <action>', 'GitHub action type')
.parse();
const options = program.opts();
async function updateIssueStatus() {
try {
// 이슈 조회
const issue = await linear.issue(options.issueId);
if (!issue) {
console.error(`Issue ${options.issueId} not found`);
return;
}
let stateId;
// 이벤트에 따른 상태 결정
if (options.event === 'pull_request') {
switch (options.action) {
case 'opened':
stateId = await getStateId('In Progress');
break;
case 'ready_for_review':
stateId = await getStateId('In Review');
break;
case 'closed':
if (process.env.GITHUB_PR_MERGED === 'true') {
stateId = await getStateId('Done');
}
break;
}
}
// 상태 업데이트
if (stateId) {
await issue.update({ stateId });
console.log(`Updated ${options.issueId} status`);
}
} catch (error) {
console.error('Failed to update Linear issue:', error);
process.exit(1);
}
}
async function getStateId(stateName) {
const states = await linear.workflowStates();
const state = states.nodes.find(s => s.name === stateName);
return state?.id;
}
updateIssueStatus();
4. 릴리즈 관리 시스템
4.1 Semantic Release 연동
.releaserc.json 업데이트:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec",
{
"prepareCmd": "node scripts/linear-release-prep.js ${nextRelease.version}",
"successCmd": "node scripts/linear-release-complete.js ${nextRelease.version}"
}
],
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
"@semantic-release/git"
]
}
4.2 릴리즈 준비 스크립트
scripts/linear-release-prep.js:
#!/usr/bin/env node
const { LinearClient } = require('@linear/sdk');
const fs = require('fs');
const linear = new LinearClient({
apiKey: process.env.LINEAR_API_KEY
});
const version = process.argv[2];
async function prepareRelease() {
try {
// 완료된 이슈 조회
const issues = await linear.issues({
filter: {
state: { name: { eq: "Done" } },
project: { name: { eq: "Zellyy Finance" } }
}
});
// 릴리즈 노트 생성
const releaseNotes = {
version,
date: new Date().toISOString(),
issues: issues.nodes.map(issue => ({
id: issue.identifier,
title: issue.title,
type: issue.labels.nodes[0]?.name || 'task',
url: issue.url
}))
};
// 릴리즈 노트 파일 저장
fs.writeFileSync(
`releases/v${version}.json`,
JSON.stringify(releaseNotes, null, 2)
);
console.log(`Prepared release notes for v${version}`);
} catch (error) {
console.error('Failed to prepare release:', error);
process.exit(1);
}
}
prepareRelease();
5. 팀 협업 도구
5.1 Slack 연동 설정
- Linear Settings → Integrations → Slack
- "Connect Slack" 클릭
- 채널 매핑:
#dev-frontend→ Frontend team#dev-backend→ Backend team#dev-mobile→ Mobile team
5.2 Slack 알림 규칙
알림 트리거:
- 이슈 생성: 담당 팀 채널
- 이슈 할당: 담당자 DM
- 상태 변경: 관련 채널
- 코멘트 추가: 멘션된 사용자
- 우선순위 변경: P0/P1만 전체 알림
5.3 일일 스탠드업 자동화
scripts/daily-standup.js:
#!/usr/bin/env node
const { LinearClient } = require('@linear/sdk');
const { WebClient } = require('@slack/web-api');
const linear = new LinearClient({
apiKey: process.env.LINEAR_API_KEY
});
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
async function generateStandup() {
const teams = ['Frontend', 'Backend', 'Mobile'];
for (const team of teams) {
// 어제 완료된 이슈
const completed = await getIssues(team, 'Done', 1);
// 오늘 진행중인 이슈
const inProgress = await getIssues(team, 'In Progress');
// 블로커
const blocked = await getIssues(team, 'Blocked');
const message = formatStandupMessage(team, {
completed,
inProgress,
blocked
});
await postToSlack(team, message);
}
}
async function getIssues(team, state, daysAgo = 0) {
const filter = {
team: { name: { eq: team } },
state: { name: { eq: state } }
};
if (daysAgo > 0) {
const date = new Date();
date.setDate(date.getDate() - daysAgo);
filter.updatedAt = { gte: date.toISOString() };
}
const issues = await linear.issues({ filter });
return issues.nodes;
}
function formatStandupMessage(team, data) {
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: `${team} Team 일일 스탠드업 📋`
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*어제 완료* ✅\n${formatIssues(data.completed)}`
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*오늘 진행* 🚀\n${formatIssues(data.inProgress)}`
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*블로커* 🚨\n${formatIssues(data.blocked)}`
}
}
]
};
}
// 매일 오전 9시 실행
generateStandup();
6. 리포팅 대시보드
6.1 프로젝트 메트릭 수집
scripts/linear-metrics.js:
#!/usr/bin/env node
const { LinearClient } = require('@linear/sdk');
const fs = require('fs');
const linear = new LinearClient({
apiKey: process.env.LINEAR_API_KEY
});
async function collectMetrics() {
const metrics = {
timestamp: new Date().toISOString(),
teams: {},
overall: {
totalIssues: 0,
completedIssues: 0,
averageLeadTime: 0,
velocity: 0
}
};
// 팀별 메트릭 수집
const teams = await linear.teams();
for (const team of teams.nodes) {
const teamMetrics = await getTeamMetrics(team.id);
metrics.teams[team.name] = teamMetrics;
// 전체 메트릭 집계
metrics.overall.totalIssues += teamMetrics.totalIssues;
metrics.overall.completedIssues += teamMetrics.completedIssues;
}
// 메트릭 저장
const filename = `metrics/linear-${new Date().toISOString().split('T')[0]}.json`;
fs.writeFileSync(filename, JSON.stringify(metrics, null, 2));
return metrics;
}
async function getTeamMetrics(teamId) {
// 이번 주 이슈들
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() - 7);
const issues = await linear.issues({
filter: {
team: { id: { eq: teamId } },
createdAt: { gte: weekStart.toISOString() }
}
});
const completed = issues.nodes.filter(i => i.state.name === 'Done');
const leadTimes = completed.map(i => calculateLeadTime(i));
return {
totalIssues: issues.nodes.length,
completedIssues: completed.length,
averageLeadTime: average(leadTimes),
byPriority: groupByPriority(issues.nodes),
byType: groupByType(issues.nodes)
};
}
function calculateLeadTime(issue) {
const created = new Date(issue.createdAt);
const completed = new Date(issue.completedAt);
return (completed - created) / (1000 * 60 * 60); // hours
}
collectMetrics();
6.2 React 대시보드 컴포넌트
src/components/linear/Dashboard.tsx:
import React, { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart, LineChart, PieChart } from 'recharts';
import { useLinearMetrics } from '@/hooks/useLinearMetrics';
export function LinearDashboard() {
const { metrics, loading, error } = useLinearMetrics();
if (loading) return <div>Loading metrics...</div>;
if (error) return <div>Error loading metrics</div>;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 완료율 카드 */}
<Card>
<CardHeader>
<CardTitle>완료율</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">
{((metrics.overall.completedIssues / metrics.overall.totalIssues) * 100).toFixed(1)}%
</div>
<p className="text-sm text-muted-foreground">
{metrics.overall.completedIssues} / {metrics.overall.totalIssues} 이슈
</p>
</CardContent>
</Card>
{/* 평균 리드타임 */}
<Card>
<CardHeader>
<CardTitle>평균 리드타임</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">
{metrics.overall.averageLeadTime.toFixed(1)}h
</div>
<p className="text-sm text-muted-foreground">
생성에서 완료까지
</p>
</CardContent>
</Card>
{/* 팀별 진행률 차트 */}
<Card className="col-span-full">
<CardHeader>
<CardTitle>팀별 진행률</CardTitle>
</CardHeader>
<CardContent>
<TeamProgressChart data={metrics.teams} />
</CardContent>
</Card>
{/* 번다운 차트 */}
<Card className="col-span-2">
<CardHeader>
<CardTitle>스프린트 번다운</CardTitle>
</CardHeader>
<CardContent>
<BurndownChart data={metrics.burndown} />
</CardContent>
</Card>
{/* 이슈 타입별 분포 */}
<Card>
<CardHeader>
<CardTitle>이슈 타입 분포</CardTitle>
</CardHeader>
<CardContent>
<IssueTypeChart data={metrics.issueTypes} />
</CardContent>
</Card>
</div>
);
}
설정 체크리스트
초기 설정
- Linear 워크스페이스 생성
- 팀 및 프로젝트 구조 설정
- 라벨 및 워크플로우 정의
- GitHub 앱 연동
자동화 구축
- Linear API 키 생성
- GitHub Actions 워크플로우 구현
- 동기화 스크립트 배포
- 릴리즈 자동화 설정
팀 협업
- Slack 연동 설정
- 알림 규칙 구성
- 일일 스탠드업 자동화
- 팀 템플릿 생성
리포팅
- 메트릭 수집 스케줄러 설정
- 대시보드 컴포넌트 구현
- 리포트 자동 생성 설정
- 데이터 시각화 구현
문제 해결
Linear API 연결 실패
# API 키 확인
echo $LINEAR_API_KEY
# 권한 확인
curl -H "Authorization: $LINEAR_API_KEY" \
https://api.linear.app/graphql \
-d '{"query":"{ viewer { id email }}"}'
GitHub 동기화 문제
- GitHub 앱 권한 재확인
- Webhook 전송 로그 확인
- Linear 이슈 ID 형식 검증
Slack 알림 미작동
- Slack 봇 토큰 유효성 확인
- 채널 권한 설정 확인
- 알림 필터 규칙 검토
이 가이드는 Zellyy Finance 프로젝트의 Linear 통합을 위한 완전한 참조 문서입니다.