@decencia/ch-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +15 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +117 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/archives.d.ts +3 -0
- package/dist/commands/archives.d.ts.map +1 -0
- package/dist/commands/archives.js +371 -0
- package/dist/commands/archives.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +149 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +115 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/db-schema.d.ts +3 -0
- package/dist/commands/db-schema.d.ts.map +1 -0
- package/dist/commands/db-schema.js +76 -0
- package/dist/commands/db-schema.js.map +1 -0
- package/dist/commands/dev-status.d.ts +3 -0
- package/dist/commands/dev-status.d.ts.map +1 -0
- package/dist/commands/dev-status.js +175 -0
- package/dist/commands/dev-status.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +124 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/invitations.d.ts +3 -0
- package/dist/commands/invitations.d.ts.map +1 -0
- package/dist/commands/invitations.js +48 -0
- package/dist/commands/invitations.js.map +1 -0
- package/dist/commands/members.d.ts +3 -0
- package/dist/commands/members.d.ts.map +1 -0
- package/dist/commands/members.js +93 -0
- package/dist/commands/members.js.map +1 -0
- package/dist/commands/notifications.d.ts +3 -0
- package/dist/commands/notifications.d.ts.map +1 -0
- package/dist/commands/notifications.js +163 -0
- package/dist/commands/notifications.js.map +1 -0
- package/dist/commands/prd.d.ts +3 -0
- package/dist/commands/prd.d.ts.map +1 -0
- package/dist/commands/prd.js +74 -0
- package/dist/commands/prd.js.map +1 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +215 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/qna.d.ts +3 -0
- package/dist/commands/qna.d.ts.map +1 -0
- package/dist/commands/qna.js +211 -0
- package/dist/commands/qna.js.map +1 -0
- package/dist/commands/specs.d.ts +3 -0
- package/dist/commands/specs.d.ts.map +1 -0
- package/dist/commands/specs.js +277 -0
- package/dist/commands/specs.js.map +1 -0
- package/dist/commands/sprints.d.ts +3 -0
- package/dist/commands/sprints.d.ts.map +1 -0
- package/dist/commands/sprints.js +262 -0
- package/dist/commands/sprints.js.map +1 -0
- package/dist/commands/sqa.d.ts +3 -0
- package/dist/commands/sqa.d.ts.map +1 -0
- package/dist/commands/sqa.js +452 -0
- package/dist/commands/sqa.js.map +1 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +13 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +174 -0
- package/dist/output.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +7 -0
- package/dist/utils.js.map +1 -0
- package/package.json +33 -0
- package/src/client.ts +137 -0
- package/src/commands/archives.ts +334 -0
- package/src/commands/auth.ts +154 -0
- package/src/commands/dashboard.ts +112 -0
- package/src/commands/db-schema.ts +74 -0
- package/src/commands/dev-status.ts +166 -0
- package/src/commands/init.ts +92 -0
- package/src/commands/members.ts +92 -0
- package/src/commands/notifications.ts +169 -0
- package/src/commands/prd.ts +72 -0
- package/src/commands/projects.ts +207 -0
- package/src/commands/qna.ts +205 -0
- package/src/commands/specs.ts +257 -0
- package/src/commands/sprints.ts +255 -0
- package/src/commands/sqa.ts +422 -0
- package/src/config.ts +98 -0
- package/src/index.ts +69 -0
- package/src/output.ts +186 -0
- package/src/utils.ts +3 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createClient } from '../client';
|
|
3
|
+
import { formatOutput, printSuccess, printError, printInfo } from '../output';
|
|
4
|
+
|
|
5
|
+
export function registerSprintsCommand(program: Command): void {
|
|
6
|
+
const sprints = program
|
|
7
|
+
.command('sprints')
|
|
8
|
+
.description('스프린트 관리');
|
|
9
|
+
|
|
10
|
+
// ch sprints list
|
|
11
|
+
sprints
|
|
12
|
+
.command('list')
|
|
13
|
+
.description('스프린트 목록 조회')
|
|
14
|
+
.option('--status <status>', '상태 필터 (scheduled|inProgress|completed)')
|
|
15
|
+
.option('--sort <field>', '정렬 필드')
|
|
16
|
+
.option('--desc', '내림차순 정렬')
|
|
17
|
+
.option('--page <number>', '페이지 번호', '1')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const globalOpts = program.opts();
|
|
20
|
+
try {
|
|
21
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
22
|
+
const params: any = {};
|
|
23
|
+
if (options.status) params.status = options.status;
|
|
24
|
+
if (options.sort) params.sortBy = options.sort;
|
|
25
|
+
if (options.desc) params.sortOrder = 'desc';
|
|
26
|
+
params.page = parseInt(options.page, 10);
|
|
27
|
+
|
|
28
|
+
const { data } = await client.get('/sprints', { params });
|
|
29
|
+
formatOutput(data, globalOpts);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Error already handled
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ch sprints get
|
|
36
|
+
sprints
|
|
37
|
+
.command('get <sprintId>')
|
|
38
|
+
.description('스프린트 상세 조회')
|
|
39
|
+
.action(async (sprintId: string) => {
|
|
40
|
+
const globalOpts = program.opts();
|
|
41
|
+
try {
|
|
42
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
43
|
+
const { data } = await client.get(`/sprints/${sprintId}`);
|
|
44
|
+
formatOutput(data, globalOpts);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Error already handled
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ch sprints create
|
|
51
|
+
sprints
|
|
52
|
+
.command('create')
|
|
53
|
+
.description('스프린트 생성')
|
|
54
|
+
.requiredOption('--name <name>', 'Sprint명')
|
|
55
|
+
.requiredOption('--start <date>', '시작일 (YYYY-MM-DD)')
|
|
56
|
+
.requiredOption('--end <date>', '종료일 (YYYY-MM-DD)')
|
|
57
|
+
.option('--content <content>', '내용')
|
|
58
|
+
.option('--status <status>', '상태')
|
|
59
|
+
.option('--specs <specIds>', '연결할 기능명세 ID (쉼표 구분)')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
const globalOpts = program.opts();
|
|
62
|
+
try {
|
|
63
|
+
if (globalOpts.dryRun) {
|
|
64
|
+
printInfo(`[DRY-RUN] 스프린트 "${options.name}" 생성 요청`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
68
|
+
const body: any = {
|
|
69
|
+
name: options.name,
|
|
70
|
+
startDate: options.start,
|
|
71
|
+
endDate: options.end,
|
|
72
|
+
};
|
|
73
|
+
if (options.content) body.content = options.content;
|
|
74
|
+
if (options.status) body.status = options.status;
|
|
75
|
+
if (options.specs) body.relatedSpecs = options.specs.split(',').map((s: string) => s.trim());
|
|
76
|
+
|
|
77
|
+
const { data } = await client.post('/sprints', body);
|
|
78
|
+
formatOutput(data, globalOpts);
|
|
79
|
+
printSuccess('스프린트가 생성되었습니다.');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Error already handled
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ch sprints update
|
|
86
|
+
sprints
|
|
87
|
+
.command('update <sprintId>')
|
|
88
|
+
.description('스프린트 수정')
|
|
89
|
+
.option('--name <name>', 'Sprint명')
|
|
90
|
+
.option('--content <content>', '내용')
|
|
91
|
+
.option('--status <status>', '상태')
|
|
92
|
+
.action(async (sprintId: string, options) => {
|
|
93
|
+
const globalOpts = program.opts();
|
|
94
|
+
try {
|
|
95
|
+
if (globalOpts.dryRun) {
|
|
96
|
+
printInfo(`[DRY-RUN] 스프린트 "${sprintId}" 수정 요청`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const body: any = {};
|
|
100
|
+
if (options.name) body.name = options.name;
|
|
101
|
+
if (options.content) body.content = options.content;
|
|
102
|
+
if (options.status) body.status = options.status;
|
|
103
|
+
|
|
104
|
+
if (Object.keys(body).length === 0) {
|
|
105
|
+
printError('수정할 필드를 지정해주세요.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
110
|
+
const { data } = await client.patch(`/sprints/${sprintId}`, body);
|
|
111
|
+
formatOutput(data, globalOpts);
|
|
112
|
+
printSuccess('스프린트가 수정되었습니다.');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// Error already handled
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ch sprints set-status
|
|
119
|
+
sprints
|
|
120
|
+
.command('set-status <sprintId> <status>')
|
|
121
|
+
.description('스프린트 상태 변경')
|
|
122
|
+
.action(async (sprintId: string, status: string) => {
|
|
123
|
+
const globalOpts = program.opts();
|
|
124
|
+
try {
|
|
125
|
+
if (globalOpts.dryRun) {
|
|
126
|
+
printInfo(`[DRY-RUN] 스프린트 "${sprintId}" 상태를 "${status}"로 변경 요청`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
130
|
+
const { data } = await client.patch(`/sprints/${sprintId}/status`, { status });
|
|
131
|
+
formatOutput(data, globalOpts);
|
|
132
|
+
printSuccess(`스프린트 상태가 "${status}"로 변경되었습니다.`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// Error already handled
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ch sprints set-dates
|
|
139
|
+
sprints
|
|
140
|
+
.command('set-dates <sprintId>')
|
|
141
|
+
.description('스프린트 기간 변경')
|
|
142
|
+
.requiredOption('--start <date>', '시작일 (YYYY-MM-DD)')
|
|
143
|
+
.requiredOption('--end <date>', '종료일 (YYYY-MM-DD)')
|
|
144
|
+
.action(async (sprintId: string, options) => {
|
|
145
|
+
const globalOpts = program.opts();
|
|
146
|
+
try {
|
|
147
|
+
if (globalOpts.dryRun) {
|
|
148
|
+
printInfo(`[DRY-RUN] 스프린트 "${sprintId}" 기간 변경 요청`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
152
|
+
const { data } = await client.patch(`/sprints/${sprintId}/dates`, {
|
|
153
|
+
startDate: options.start,
|
|
154
|
+
endDate: options.end,
|
|
155
|
+
});
|
|
156
|
+
formatOutput(data, globalOpts);
|
|
157
|
+
printSuccess('스프린트 기간이 변경되었습니다.');
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Error already handled
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ch sprints add-specs
|
|
164
|
+
sprints
|
|
165
|
+
.command('add-specs <sprintId> <specIds>')
|
|
166
|
+
.description('스프린트에 기능명세 연결 (쉼표 구분)')
|
|
167
|
+
.action(async (sprintId: string, specIds: string) => {
|
|
168
|
+
const globalOpts = program.opts();
|
|
169
|
+
try {
|
|
170
|
+
if (globalOpts.dryRun) {
|
|
171
|
+
printInfo(`[DRY-RUN] 스프린트 "${sprintId}"에 기능명세 연결 요청`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
175
|
+
const ids = specIds.split(',').map((s) => s.trim());
|
|
176
|
+
const { data } = await client.post(`/sprints/${sprintId}/specs`, { specIds: ids });
|
|
177
|
+
formatOutput(data, globalOpts);
|
|
178
|
+
printSuccess('기능명세가 스프린트에 연결되었습니다.');
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// Error already handled
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ch sprints remove-specs
|
|
185
|
+
sprints
|
|
186
|
+
.command('remove-specs <sprintId> <specIds>')
|
|
187
|
+
.description('스프린트에서 기능명세 연결 해제 (쉼표 구분)')
|
|
188
|
+
.action(async (sprintId: string, specIds: string) => {
|
|
189
|
+
const globalOpts = program.opts();
|
|
190
|
+
try {
|
|
191
|
+
if (globalOpts.dryRun) {
|
|
192
|
+
printInfo(`[DRY-RUN] 스프린트 "${sprintId}" 기능명세 해제 요청`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
196
|
+
const ids = specIds.split(',').map((s) => s.trim());
|
|
197
|
+
await client.delete(`/sprints/${sprintId}/specs`, { data: { specIds: ids } });
|
|
198
|
+
printSuccess('기능명세 연결이 해제되었습니다.');
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// Error already handled
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ch sprints delete
|
|
205
|
+
sprints
|
|
206
|
+
.command('delete <sprintId>')
|
|
207
|
+
.description('스프린트 삭제')
|
|
208
|
+
.action(async (sprintId: string) => {
|
|
209
|
+
const globalOpts = program.opts();
|
|
210
|
+
try {
|
|
211
|
+
if (globalOpts.dryRun) {
|
|
212
|
+
printInfo(`[DRY-RUN] 스프린트 "${sprintId}" 삭제 요청`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
216
|
+
await client.delete(`/sprints/${sprintId}`);
|
|
217
|
+
printSuccess('스프린트가 삭제되었습니다.');
|
|
218
|
+
} catch (error) {
|
|
219
|
+
// Error already handled
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ch sprints progress
|
|
224
|
+
sprints
|
|
225
|
+
.command('progress <sprintId>')
|
|
226
|
+
.description('스프린트 진행률 조회')
|
|
227
|
+
.action(async (sprintId: string) => {
|
|
228
|
+
const globalOpts = program.opts();
|
|
229
|
+
try {
|
|
230
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
231
|
+
const { data } = await client.get(`/sprints/${sprintId}/progress`);
|
|
232
|
+
formatOutput(data, globalOpts);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// Error already handled
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ch sprints timeline
|
|
239
|
+
sprints
|
|
240
|
+
.command('timeline')
|
|
241
|
+
.description('스프린트 타임라인 조회')
|
|
242
|
+
.option('--weeks <number>', '주 수', '8')
|
|
243
|
+
.action(async (options) => {
|
|
244
|
+
const globalOpts = program.opts();
|
|
245
|
+
try {
|
|
246
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
247
|
+
const { data } = await client.get('/sprints/timeline', {
|
|
248
|
+
params: { weeks: parseInt(options.weeks, 10) },
|
|
249
|
+
});
|
|
250
|
+
formatOutput(data, globalOpts);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
// Error already handled
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import FormData from 'form-data';
|
|
5
|
+
import { createClient } from '../client';
|
|
6
|
+
import { formatOutput, printSuccess, printError, printInfo } from '../output';
|
|
7
|
+
|
|
8
|
+
export function registerSqaCommand(program: Command): void {
|
|
9
|
+
const sqa = program
|
|
10
|
+
.command('sqa')
|
|
11
|
+
.description('SQA 관리 (시트 + 수행)');
|
|
12
|
+
|
|
13
|
+
// ══════════════════════════════════════════════════
|
|
14
|
+
// Sheet (시트 템플릿) commands
|
|
15
|
+
// ══════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
// ch sqa list — 시트 목록
|
|
18
|
+
sqa
|
|
19
|
+
.command('list')
|
|
20
|
+
.description('SQA 시트 목록 조회')
|
|
21
|
+
.option('--sort <field>', '정렬 필드')
|
|
22
|
+
.option('--desc', '내림차순 정렬')
|
|
23
|
+
.option('--page <number>', '페이지 번호', '1')
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
const globalOpts = program.opts();
|
|
26
|
+
try {
|
|
27
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
28
|
+
const params: any = {};
|
|
29
|
+
if (options.sort) params.sortBy = options.sort;
|
|
30
|
+
if (options.desc) params.sortOrder = 'desc';
|
|
31
|
+
params.page = parseInt(options.page, 10);
|
|
32
|
+
|
|
33
|
+
const { data } = await client.get('/sqa', { params });
|
|
34
|
+
formatOutput(data, globalOpts);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Error already handled
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ch sqa get <sheetId> — 시트 상세
|
|
41
|
+
sqa
|
|
42
|
+
.command('get <sheetId>')
|
|
43
|
+
.description('SQA 시트 상세 조회')
|
|
44
|
+
.action(async (sheetId: string) => {
|
|
45
|
+
const globalOpts = program.opts();
|
|
46
|
+
try {
|
|
47
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
48
|
+
const { data } = await client.get(`/sqa/${sheetId}`);
|
|
49
|
+
formatOutput(data, globalOpts);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// Error already handled
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ch sqa create — 시트 생성
|
|
56
|
+
sqa
|
|
57
|
+
.command('create')
|
|
58
|
+
.description('SQA 시트 생성')
|
|
59
|
+
.requiredOption('--name <name>', '시트명')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
const globalOpts = program.opts();
|
|
62
|
+
try {
|
|
63
|
+
if (globalOpts.dryRun) {
|
|
64
|
+
printInfo(`[DRY-RUN] SQA 시트 "${options.name}" 생성 요청`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
68
|
+
const { data } = await client.post('/sqa', {
|
|
69
|
+
name: options.name,
|
|
70
|
+
});
|
|
71
|
+
formatOutput(data, globalOpts);
|
|
72
|
+
printSuccess('SQA 시트가 생성되었습니다.');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Error already handled
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ch sqa update <sheetId> — 시트 수정
|
|
79
|
+
sqa
|
|
80
|
+
.command('update <sheetId>')
|
|
81
|
+
.description('SQA 시트 수정')
|
|
82
|
+
.option('--name <name>', '시트명')
|
|
83
|
+
.action(async (sheetId: string, options) => {
|
|
84
|
+
const globalOpts = program.opts();
|
|
85
|
+
try {
|
|
86
|
+
if (globalOpts.dryRun) {
|
|
87
|
+
printInfo(`[DRY-RUN] SQA 시트 "${sheetId}" 수정 요청`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const body: any = {};
|
|
91
|
+
if (options.name) body.name = options.name;
|
|
92
|
+
|
|
93
|
+
if (Object.keys(body).length === 0) {
|
|
94
|
+
printError('수정할 필드를 지정하세요 (--name).');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
99
|
+
const { data } = await client.patch(`/sqa/${sheetId}`, body);
|
|
100
|
+
formatOutput(data, globalOpts);
|
|
101
|
+
printSuccess('SQA 시트가 수정되었습니다.');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Error already handled
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ch sqa delete <sheetId> — 시트 삭제
|
|
108
|
+
sqa
|
|
109
|
+
.command('delete <sheetId>')
|
|
110
|
+
.description('SQA 시트 삭제')
|
|
111
|
+
.action(async (sheetId: string) => {
|
|
112
|
+
const globalOpts = program.opts();
|
|
113
|
+
try {
|
|
114
|
+
if (globalOpts.dryRun) {
|
|
115
|
+
printInfo(`[DRY-RUN] SQA 시트 "${sheetId}" 삭제 요청`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
119
|
+
await client.delete(`/sqa/${sheetId}`);
|
|
120
|
+
printSuccess('SQA 시트가 삭제되었습니다.');
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// Error already handled
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ch sqa add-item <sheetId> — 시트에 테스트 항목 추가
|
|
127
|
+
sqa
|
|
128
|
+
.command('add-item <sheetId>')
|
|
129
|
+
.description('SQA 시트에 테스트 항목 추가')
|
|
130
|
+
.requiredOption('--test <testItem>', '테스트 항목명')
|
|
131
|
+
.option('--spec <specId>', '관련 기능명세 ID')
|
|
132
|
+
.action(async (sheetId: string, options) => {
|
|
133
|
+
const globalOpts = program.opts();
|
|
134
|
+
try {
|
|
135
|
+
if (globalOpts.dryRun) {
|
|
136
|
+
printInfo(`[DRY-RUN] SQA 시트 "${sheetId}"에 항목 추가 요청`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
140
|
+
const { data } = await client.post(`/sqa/${sheetId}/items`, {
|
|
141
|
+
testItem: options.test,
|
|
142
|
+
relatedSpec: options.spec || '',
|
|
143
|
+
});
|
|
144
|
+
formatOutput(data, globalOpts);
|
|
145
|
+
printSuccess('테스트 항목이 추가되었습니다.');
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Error already handled
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ══════════════════════════════════════════════════
|
|
152
|
+
// Run (수행) commands
|
|
153
|
+
// ══════════════════════════════════════════════════
|
|
154
|
+
|
|
155
|
+
// ch sqa start-run <sheetId> — 시트 기반 수행 시작
|
|
156
|
+
sqa
|
|
157
|
+
.command('start-run <sheetId>')
|
|
158
|
+
.description('시트 기반 SQA 수행 시작 (항목을 스냅샷하여 Run 생성)')
|
|
159
|
+
.requiredOption('--date <date>', '수행일 (YYYY-MM-DD)')
|
|
160
|
+
.action(async (sheetId: string, options) => {
|
|
161
|
+
const globalOpts = program.opts();
|
|
162
|
+
try {
|
|
163
|
+
if (globalOpts.dryRun) {
|
|
164
|
+
printInfo(`[DRY-RUN] SQA 시트 "${sheetId}" 기반 수행 시작 요청`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
168
|
+
const { data } = await client.post(`/sqa/${sheetId}/start-run`, {
|
|
169
|
+
performDate: options.date,
|
|
170
|
+
});
|
|
171
|
+
formatOutput(data, globalOpts);
|
|
172
|
+
printSuccess('SQA 수행이 시작되었습니다.');
|
|
173
|
+
} catch (error) {
|
|
174
|
+
// Error already handled
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ch sqa runs — 수행 목록
|
|
179
|
+
sqa
|
|
180
|
+
.command('runs')
|
|
181
|
+
.description('SQA 수행 목록 조회')
|
|
182
|
+
.option('--sort <field>', '정렬 필드')
|
|
183
|
+
.option('--desc', '내림차순 정렬')
|
|
184
|
+
.option('--page <number>', '페이지 번호', '1')
|
|
185
|
+
.action(async (options) => {
|
|
186
|
+
const globalOpts = program.opts();
|
|
187
|
+
try {
|
|
188
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
189
|
+
const params: any = {};
|
|
190
|
+
if (options.sort) params.sortBy = options.sort;
|
|
191
|
+
if (options.desc) params.sortOrder = 'desc';
|
|
192
|
+
params.page = parseInt(options.page, 10);
|
|
193
|
+
|
|
194
|
+
const { data } = await client.get('/sqa/runs/list', { params });
|
|
195
|
+
formatOutput(data, globalOpts);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Error already handled
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ch sqa run <runId> — 수행 상세
|
|
202
|
+
sqa
|
|
203
|
+
.command('run <runId>')
|
|
204
|
+
.description('SQA 수행 상세 조회')
|
|
205
|
+
.action(async (runId: string) => {
|
|
206
|
+
const globalOpts = program.opts();
|
|
207
|
+
try {
|
|
208
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
209
|
+
const { data } = await client.get(`/sqa/runs/${runId}`);
|
|
210
|
+
formatOutput(data, globalOpts);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
// Error already handled
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ch sqa check <runId> <itemId> — 수행 항목 체크
|
|
217
|
+
sqa
|
|
218
|
+
.command('check <runId> <itemId>')
|
|
219
|
+
.description('SQA 수행 항목 검토결과 체크')
|
|
220
|
+
.requiredOption('--result <result>', '결과 (yes|no)')
|
|
221
|
+
.option('--note <note>', '비고')
|
|
222
|
+
.action(async (runId: string, itemId: string, options) => {
|
|
223
|
+
const globalOpts = program.opts();
|
|
224
|
+
try {
|
|
225
|
+
if (globalOpts.dryRun) {
|
|
226
|
+
printInfo(`[DRY-RUN] SQA Run "${runId}" 항목 "${itemId}" 체크 요청`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
230
|
+
const body: any = { result: options.result };
|
|
231
|
+
if (options.note) body.note = options.note;
|
|
232
|
+
|
|
233
|
+
const { data } = await client.patch(`/sqa/runs/${runId}/items/${itemId}/check`, body);
|
|
234
|
+
formatOutput(data, globalOpts);
|
|
235
|
+
printSuccess('검토결과가 체크되었습니다.');
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// Error already handled
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ch sqa check-bulk <runId> — 수행 항목 일괄 체크
|
|
242
|
+
sqa
|
|
243
|
+
.command('check-bulk <runId>')
|
|
244
|
+
.description('SQA 수행 여러 항목 일괄 체크')
|
|
245
|
+
.requiredOption('--file <filePath>', '결과 JSON 파일 경로')
|
|
246
|
+
.action(async (runId: string, options) => {
|
|
247
|
+
const globalOpts = program.opts();
|
|
248
|
+
try {
|
|
249
|
+
if (globalOpts.dryRun) {
|
|
250
|
+
printInfo(`[DRY-RUN] SQA Run "${runId}" 일괄 체크 요청`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const filePath = path.resolve(options.file);
|
|
255
|
+
if (!fs.existsSync(filePath)) {
|
|
256
|
+
printError(`파일을 찾을 수 없습니다: ${filePath}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
261
|
+
let results: any;
|
|
262
|
+
try {
|
|
263
|
+
results = JSON.parse(fileContent);
|
|
264
|
+
} catch {
|
|
265
|
+
printError('파일이 올바른 JSON 형식이 아닙니다.');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
270
|
+
const { data } = await client.post(`/sqa/runs/${runId}/items/check-bulk`, results);
|
|
271
|
+
formatOutput(data, globalOpts);
|
|
272
|
+
printSuccess('일괄 체크가 완료되었습니다.');
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// Error already handled
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ch sqa complete <runId> — 수행 완료
|
|
279
|
+
sqa
|
|
280
|
+
.command('complete <runId>')
|
|
281
|
+
.description('SQA 수행 완료')
|
|
282
|
+
.action(async (runId: string) => {
|
|
283
|
+
const globalOpts = program.opts();
|
|
284
|
+
try {
|
|
285
|
+
if (globalOpts.dryRun) {
|
|
286
|
+
printInfo(`[DRY-RUN] SQA Run "${runId}" 수행 완료 요청`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
290
|
+
const { data } = await client.post(`/sqa/runs/${runId}/complete`);
|
|
291
|
+
formatOutput(data, globalOpts);
|
|
292
|
+
printSuccess('SQA 수행이 완료되었습니다.');
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// Error already handled
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ch sqa summary <runId> — 수행 집계
|
|
299
|
+
sqa
|
|
300
|
+
.command('summary <runId>')
|
|
301
|
+
.description('SQA 수행 Pass/Fail 집계 조회')
|
|
302
|
+
.action(async (runId: string) => {
|
|
303
|
+
const globalOpts = program.opts();
|
|
304
|
+
try {
|
|
305
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
306
|
+
const { data } = await client.get(`/sqa/runs/${runId}/summary`);
|
|
307
|
+
formatOutput(data, globalOpts);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// Error already handled
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ch sqa export <runId> — 수행 엑셀 Export
|
|
314
|
+
sqa
|
|
315
|
+
.command('export <runId>')
|
|
316
|
+
.description('SQA 수행 엑셀 Export')
|
|
317
|
+
.requiredOption('--output <filePath>', '출력 파일 경로')
|
|
318
|
+
.action(async (runId: string, options) => {
|
|
319
|
+
const globalOpts = program.opts();
|
|
320
|
+
try {
|
|
321
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
322
|
+
const response = await client.get(`/sqa/runs/${runId}/export`, {
|
|
323
|
+
responseType: 'arraybuffer',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const outputPath = path.resolve(options.output);
|
|
327
|
+
fs.writeFileSync(outputPath, Buffer.from(response.data));
|
|
328
|
+
printSuccess(`엑셀 파일이 저장되었습니다: ${outputPath}`);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// Error already handled
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ch sqa delete-run <runId> — 수행 삭제
|
|
335
|
+
sqa
|
|
336
|
+
.command('delete-run <runId>')
|
|
337
|
+
.description('SQA 수행 삭제')
|
|
338
|
+
.action(async (runId: string) => {
|
|
339
|
+
const globalOpts = program.opts();
|
|
340
|
+
try {
|
|
341
|
+
if (globalOpts.dryRun) {
|
|
342
|
+
printInfo(`[DRY-RUN] SQA Run "${runId}" 삭제 요청`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
346
|
+
await client.delete(`/sqa/runs/${runId}`);
|
|
347
|
+
printSuccess('SQA 수행이 삭제되었습니다.');
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// Error already handled
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ══════════════════════════════════════════════════
|
|
354
|
+
// Import (엑셀 → 시트)
|
|
355
|
+
// ══════════════════════════════════════════════════
|
|
356
|
+
|
|
357
|
+
sqa
|
|
358
|
+
.command('import')
|
|
359
|
+
.description('SQA 엑셀 Import (시트 생성)')
|
|
360
|
+
.requiredOption('--file <filePath>', '엑셀 파일 경로')
|
|
361
|
+
.option('--name <name>', '시트명')
|
|
362
|
+
.option('--date <date>', '수행일')
|
|
363
|
+
.action(async (options) => {
|
|
364
|
+
const globalOpts = program.opts();
|
|
365
|
+
try {
|
|
366
|
+
if (globalOpts.dryRun) {
|
|
367
|
+
printInfo('[DRY-RUN] SQA 엑셀 Import 요청');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const filePath = path.resolve(options.file);
|
|
372
|
+
if (!fs.existsSync(filePath)) {
|
|
373
|
+
printError(`파일을 찾을 수 없습니다: ${filePath}`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const form = new FormData();
|
|
378
|
+
form.append('file', fs.createReadStream(filePath));
|
|
379
|
+
if (options.name) form.append('name', options.name);
|
|
380
|
+
if (options.date) form.append('performDate', options.date);
|
|
381
|
+
|
|
382
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
383
|
+
const { data } = await client.post('/sqa/import', form, {
|
|
384
|
+
headers: {
|
|
385
|
+
...form.getHeaders(),
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
formatOutput(data, globalOpts);
|
|
389
|
+
printSuccess('SQA 엑셀 Import가 완료되었습니다.');
|
|
390
|
+
} catch (error) {
|
|
391
|
+
// Error already handled
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
sqa
|
|
396
|
+
.command('import-preview')
|
|
397
|
+
.description('SQA 엑셀 Import 미리보기')
|
|
398
|
+
.requiredOption('--file <filePath>', '엑셀 파일 경로')
|
|
399
|
+
.action(async (options) => {
|
|
400
|
+
const globalOpts = program.opts();
|
|
401
|
+
try {
|
|
402
|
+
const filePath = path.resolve(options.file);
|
|
403
|
+
if (!fs.existsSync(filePath)) {
|
|
404
|
+
printError(`파일을 찾을 수 없습니다: ${filePath}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const form = new FormData();
|
|
409
|
+
form.append('file', fs.createReadStream(filePath));
|
|
410
|
+
|
|
411
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
412
|
+
const { data } = await client.post('/sqa/import-preview', form, {
|
|
413
|
+
headers: {
|
|
414
|
+
...form.getHeaders(),
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
formatOutput(data, globalOpts);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
// Error already handled
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|