Files
zellyy-finance/docs/linear-integration-guide.md
hansoo 8343b25439 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>
2025-07-14 10:08:51 +09:00

16 KiB

Linear 프로젝트 관리 도구 연동 가이드

개요

이 가이드는 Zellyy Finance 프로젝트에 Linear.app 프로젝트 관리 도구를 완전히 연동하는 방법을 설명합니다. Linear와 GitHub의 양방향 동기화, 자동화된 워크플로우, 실시간 리포팅 시스템 구축을 다룹니다.

목차

  1. Linear 계정 및 프로젝트 설정
  2. GitHub 연동 설정
  3. 워크플로우 자동화
  4. 릴리즈 관리 시스템
  5. 팀 협업 도구
  6. 리포팅 대시보드

1. Linear 계정 및 프로젝트 설정

1.1 Linear 워크스페이스 생성

  1. Linear.app 접속
  2. "Create workspace" 클릭
  3. 워크스페이스 정보 입력:
    • Workspace name: Zellyy Finance
    • URL: zellyy-finance
    • Plan: Professional (권장)

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 앱 설치

  1. Linear 설정 → Integrations → GitHub
  2. "Install GitHub App" 클릭
  3. 권한 승인:
    • Repository access: zellyy-finance
    • Permissions: Read & Write

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 설정

  1. Linear Settings → API → Personal API keys
  2. "Create key" 클릭
  3. Key name: zellyy-finance-automation
  4. 생성된 키를 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 연동 설정

  1. Linear Settings → Integrations → Slack
  2. "Connect Slack" 클릭
  3. 채널 매핑:
    • #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 통합을 위한 완전한 참조 문서입니다.