# 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 ## 개요 ## 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 ', 'Linear issue ID') .option('--event ', 'GitHub event type') .option('--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
Loading metrics...
; if (error) return
Error loading metrics
; return (
{/* 완료율 카드 */} 완료율
{((metrics.overall.completedIssues / metrics.overall.totalIssues) * 100).toFixed(1)}%

{metrics.overall.completedIssues} / {metrics.overall.totalIssues} 이슈

{/* 평균 리드타임 */} 평균 리드타임
{metrics.overall.averageLeadTime.toFixed(1)}h

생성에서 완료까지

{/* 팀별 진행률 차트 */} 팀별 진행률 {/* 번다운 차트 */} 스프린트 번다운 {/* 이슈 타입별 분포 */} 이슈 타입 분포
); } ``` ## 설정 체크리스트 ### 초기 설정 - [ ] 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 통합을 위한 완전한 참조 문서입니다.