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:
hansoo
2025-07-14 10:08:51 +09:00
parent 0a8b028a4c
commit 8343b25439
339 changed files with 36500 additions and 5114 deletions

View File

@@ -0,0 +1,692 @@
# Linear 프로젝트 관리 도구 연동 가이드
## 개요
이 가이드는 Zellyy Finance 프로젝트에 Linear.app 프로젝트 관리 도구를 완전히 연동하는 방법을 설명합니다. Linear와 GitHub의 양방향 동기화, 자동화된 워크플로우, 실시간 리포팅 시스템 구축을 다룹니다.
## 목차
1. [Linear 계정 및 프로젝트 설정](#1-linear-계정-및-프로젝트-설정)
2. [GitHub 연동 설정](#2-github-연동-설정)
3. [워크플로우 자동화](#3-워크플로우-자동화)
4. [릴리즈 관리 시스템](#4-릴리즈-관리-시스템)
5. [팀 협업 도구](#5-팀-협업-도구)
6. [리포팅 대시보드](#6-리포팅-대시보드)
## 1. Linear 계정 및 프로젝트 설정
### 1.1 Linear 워크스페이스 생성
1. [Linear.app](https://linear.app) 접속
2. "Create workspace" 클릭
3. 워크스페이스 정보 입력:
- Workspace name: `Zellyy Finance`
- URL: `zellyy-finance`
- Plan: Professional (권장)
### 1.2 프로젝트 구조 설정
```yaml
# 프로젝트 구조
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**: 개선 사항
#### 라벨 체계
```yaml
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 워크플로우 상태 정의
```mermaid
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 앱 설치
1. Linear 설정 → Integrations → GitHub
2. "Install GitHub App" 클릭
3. 권한 승인:
- Repository access: `zellyy-finance`
- Permissions: Read & Write
### 2.2 브랜치 명명 규칙
```bash
# Linear 이슈 ID 기반 브랜치명
feature/ZEL-123-user-authentication
bugfix/ZEL-456-login-error
task/ZEL-789-update-dependencies
```
### 2.3 커밋 메시지 규칙
```bash
# 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`:
```markdown
## 개요
<!-- PR 설명 -->
## Linear 이슈
Closes ZEL-XXX
## 변경 사항
- [ ] 기능 A 구현
- [ ] 버그 B 수정
- [ ] 테스트 추가
## 테스트
- [ ] 유닛 테스트 통과
- [ ] E2E 테스트 통과
- [ ] 수동 테스트 완료
## 스크린샷
<!-- 필요시 스크린샷 첨부 -->
```
## 3. 워크플로우 자동화
### 3.1 Linear API 설정
1. Linear Settings → API → Personal API keys
2. "Create key" 클릭
3. Key name: `zellyy-finance-automation`
4. 생성된 키를 GitHub Secrets에 저장:
```bash
LINEAR_API_KEY=lin_api_xxxxxxxxxxxxxxxx
```
### 3.2 GitHub Actions 워크플로우
`.github/workflows/linear-integration.yml`:
```yaml
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`:
```javascript
#!/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` 업데이트:
```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`:
```javascript
#!/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 연동 설정
1. Linear Settings → Integrations → Slack
2. "Connect Slack" 클릭
3. 채널 매핑:
- `#dev-frontend` → Frontend team
- `#dev-backend` → Backend team
- `#dev-mobile` → Mobile team
### 5.2 Slack 알림 규칙
```yaml
알림 트리거:
- 이슈 생성: 담당 팀 채널
- 이슈 할당: 담당자 DM
- 상태 변경: 관련 채널
- 코멘트 추가: 멘션된 사용자
- 우선순위 변경: P0/P1만 전체 알림
```
### 5.3 일일 스탠드업 자동화
`scripts/daily-standup.js`:
```javascript
#!/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`:
```javascript
#!/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`:
```typescript
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 연결 실패
```bash
# 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 통합을 위한 완전한 참조 문서입니다.