✨ 주요 개선사항: - 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>
692 lines
16 KiB
Markdown
692 lines
16 KiB
Markdown
# 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 통합을 위한 완전한 참조 문서입니다. |