@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,207 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createClient } from '../client';
|
|
3
|
+
import { formatOutput, printSuccess, printError, printInfo } from '../output';
|
|
4
|
+
import { parseCommaSeparated } from '../utils';
|
|
5
|
+
|
|
6
|
+
export function registerProjectsCommand(program: Command): void {
|
|
7
|
+
const projects = program
|
|
8
|
+
.command('projects')
|
|
9
|
+
.description('프로젝트 관리');
|
|
10
|
+
|
|
11
|
+
// ch projects list
|
|
12
|
+
projects
|
|
13
|
+
.command('list')
|
|
14
|
+
.description('프로젝트 목록 조회')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
const globalOpts = program.opts();
|
|
17
|
+
try {
|
|
18
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
19
|
+
const { data } = await client.get('/projects');
|
|
20
|
+
formatOutput(data, globalOpts);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Error already handled
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ch projects create
|
|
27
|
+
projects
|
|
28
|
+
.command('create')
|
|
29
|
+
.description('프로젝트 생성')
|
|
30
|
+
.requiredOption('--name <name>', '프로젝트명')
|
|
31
|
+
.option('--desc <description>', '프로젝트 설명')
|
|
32
|
+
.action(async (options) => {
|
|
33
|
+
const globalOpts = program.opts();
|
|
34
|
+
try {
|
|
35
|
+
if (globalOpts.dryRun) {
|
|
36
|
+
printInfo(`[DRY-RUN] 프로젝트 "${options.name}" 생성 요청`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
40
|
+
const body: any = { name: options.name };
|
|
41
|
+
if (options.desc) body.description = options.desc;
|
|
42
|
+
const { data } = await client.post('/projects', body);
|
|
43
|
+
formatOutput(data, globalOpts);
|
|
44
|
+
printSuccess('프로젝트가 생성되었습니다.');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Error already handled
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ch projects info
|
|
51
|
+
projects
|
|
52
|
+
.command('info')
|
|
53
|
+
.description('현재 프로젝트 상세 정보')
|
|
54
|
+
.action(async () => {
|
|
55
|
+
const globalOpts = program.opts();
|
|
56
|
+
try {
|
|
57
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
58
|
+
const { data } = await client.get('/projects/current');
|
|
59
|
+
formatOutput(data, globalOpts);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Error already handled
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ch projects update
|
|
66
|
+
projects
|
|
67
|
+
.command('update')
|
|
68
|
+
.description('프로젝트 정보 수정')
|
|
69
|
+
.option('--name <name>', '프로젝트명')
|
|
70
|
+
.option('--desc <description>', '프로젝트 설명')
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
const globalOpts = program.opts();
|
|
73
|
+
try {
|
|
74
|
+
if (globalOpts.dryRun) {
|
|
75
|
+
printInfo('[DRY-RUN] 프로젝트 수정 요청');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const body: any = {};
|
|
79
|
+
if (options.name) body.name = options.name;
|
|
80
|
+
if (options.desc) body.description = options.desc;
|
|
81
|
+
if (Object.keys(body).length === 0) {
|
|
82
|
+
printError('수정할 필드를 지정하세요 (--name 또는 --desc).');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
86
|
+
const { data } = await client.patch('/projects/current', body);
|
|
87
|
+
formatOutput(data, globalOpts);
|
|
88
|
+
printSuccess('프로젝트가 수정되었습니다.');
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Error already handled
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ch projects archive
|
|
95
|
+
projects
|
|
96
|
+
.command('archive')
|
|
97
|
+
.description('프로젝트 아카이브')
|
|
98
|
+
.action(async () => {
|
|
99
|
+
const globalOpts = program.opts();
|
|
100
|
+
try {
|
|
101
|
+
if (globalOpts.dryRun) {
|
|
102
|
+
printInfo('[DRY-RUN] 프로젝트 아카이브 요청');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
106
|
+
await client.post('/projects/current/archive');
|
|
107
|
+
printSuccess('프로젝트가 아카이브되었습니다.');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Error already handled
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ch projects unarchive
|
|
114
|
+
projects
|
|
115
|
+
.command('unarchive')
|
|
116
|
+
.description('프로젝트 아카이브 해제')
|
|
117
|
+
.action(async () => {
|
|
118
|
+
const globalOpts = program.opts();
|
|
119
|
+
try {
|
|
120
|
+
if (globalOpts.dryRun) {
|
|
121
|
+
printInfo('[DRY-RUN] 프로젝트 아카이브 해제 요청');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
125
|
+
await client.post('/projects/current/unarchive');
|
|
126
|
+
printSuccess('프로젝트 아카이브가 해제되었습니다.');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Error already handled
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ch projects set-schema
|
|
133
|
+
projects
|
|
134
|
+
.command('set-schema')
|
|
135
|
+
.description('프로젝트 기능명세 스키마 설정 (허용값 관리)')
|
|
136
|
+
.option('--devices <values>', '허용 디바이스 목록 (쉼표 구분, 예: web,app,kiosk)')
|
|
137
|
+
.option('--domains <values>', '허용 도메인 목록 (쉼표 구분, 예: user,admin)')
|
|
138
|
+
.option('--types <values>', '허용 기능유형 목록 (쉼표 구분, 예: 인증,커뮤니티,결제)')
|
|
139
|
+
.option('--permissions <values>', '허용 권한 목록 (쉼표 구분, 예: 비회원,회원,관리자)')
|
|
140
|
+
.action(async (options) => {
|
|
141
|
+
const globalOpts = program.opts();
|
|
142
|
+
try {
|
|
143
|
+
if (globalOpts.dryRun) {
|
|
144
|
+
printInfo('[DRY-RUN] 프로젝트 스키마 설정 요청');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const specSchema: any = {};
|
|
148
|
+
if (options.devices) specSchema.devices = parseCommaSeparated(options.devices);
|
|
149
|
+
if (options.domains) specSchema.domains = parseCommaSeparated(options.domains);
|
|
150
|
+
if (options.types) specSchema.featureTypes = parseCommaSeparated(options.types);
|
|
151
|
+
if (options.permissions) specSchema.permissions = parseCommaSeparated(options.permissions);
|
|
152
|
+
|
|
153
|
+
if (Object.keys(specSchema).length === 0) {
|
|
154
|
+
printError('설정할 스키마 필드를 지정하세요 (--devices, --domains, --types, --permissions).');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
159
|
+
const { data } = await client.patch('/projects/current', { specSchema });
|
|
160
|
+
formatOutput(data, globalOpts);
|
|
161
|
+
printSuccess('프로젝트 스키마가 설정되었습니다.');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// Error already handled
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ch projects get-schema
|
|
168
|
+
projects
|
|
169
|
+
.command('get-schema')
|
|
170
|
+
.description('프로젝트 기능명세 스키마 조회')
|
|
171
|
+
.action(async () => {
|
|
172
|
+
const globalOpts = program.opts();
|
|
173
|
+
try {
|
|
174
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
175
|
+
const { data } = await client.get('/projects/current');
|
|
176
|
+
if (data.specSchema) {
|
|
177
|
+
formatOutput(data.specSchema, globalOpts);
|
|
178
|
+
} else {
|
|
179
|
+
printInfo('스키마가 설정되지 않았습니다. "ch projects set-schema"로 설정하세요.');
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Error already handled
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ch projects delete
|
|
187
|
+
projects
|
|
188
|
+
.command('delete')
|
|
189
|
+
.description('프로젝트 영구 삭제')
|
|
190
|
+
.requiredOption('--confirm <projectName>', '삭제 확인을 위한 프로젝트명 입력')
|
|
191
|
+
.action(async (options) => {
|
|
192
|
+
const globalOpts = program.opts();
|
|
193
|
+
try {
|
|
194
|
+
if (globalOpts.dryRun) {
|
|
195
|
+
printInfo(`[DRY-RUN] 프로젝트 삭제 요청 (확인: "${options.confirm}")`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
199
|
+
await client.delete('/projects/current', {
|
|
200
|
+
data: { confirmName: options.confirm },
|
|
201
|
+
});
|
|
202
|
+
printSuccess('프로젝트가 영구 삭제되었습니다.');
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// Error already handled
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createClient } from '../client';
|
|
3
|
+
import { formatOutput, printSuccess, printError, printInfo } from '../output';
|
|
4
|
+
|
|
5
|
+
export function registerQnaCommand(program: Command): void {
|
|
6
|
+
const qna = program
|
|
7
|
+
.command('qna')
|
|
8
|
+
.description('QnA 관리');
|
|
9
|
+
|
|
10
|
+
// ch qna list
|
|
11
|
+
qna
|
|
12
|
+
.command('list')
|
|
13
|
+
.description('QnA 목록 조회')
|
|
14
|
+
.option('--status <status>', '상태 필터 (awaitingReply|replied|reflected)')
|
|
15
|
+
.option('--category <category>', '카테고리 필터 (modification|question|etc)')
|
|
16
|
+
.option('--search <keyword>', '제목/내용 검색')
|
|
17
|
+
.option('--sort <field>', '정렬 필드')
|
|
18
|
+
.option('--desc', '내림차순 정렬')
|
|
19
|
+
.option('--page <number>', '페이지 번호', '1')
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
const globalOpts = program.opts();
|
|
22
|
+
try {
|
|
23
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
24
|
+
const params: any = {};
|
|
25
|
+
if (options.status) params.status = options.status;
|
|
26
|
+
if (options.category) params.category = options.category;
|
|
27
|
+
if (options.search) params.search = options.search;
|
|
28
|
+
if (options.sort) params.sortBy = options.sort;
|
|
29
|
+
if (options.desc) params.sortOrder = 'desc';
|
|
30
|
+
params.page = parseInt(options.page, 10);
|
|
31
|
+
|
|
32
|
+
const { data } = await client.get('/qna', { params });
|
|
33
|
+
formatOutput(data, globalOpts);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Error already handled
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ch qna get
|
|
40
|
+
qna
|
|
41
|
+
.command('get <qnaId>')
|
|
42
|
+
.description('QnA 상세 조회')
|
|
43
|
+
.action(async (qnaId: string) => {
|
|
44
|
+
const globalOpts = program.opts();
|
|
45
|
+
try {
|
|
46
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
47
|
+
const { data } = await client.get(`/qna/${qnaId}`);
|
|
48
|
+
formatOutput(data, globalOpts);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Error already handled
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ch qna create
|
|
55
|
+
qna
|
|
56
|
+
.command('create')
|
|
57
|
+
.description('QnA 질문 등록')
|
|
58
|
+
.requiredOption('--title <title>', '제목')
|
|
59
|
+
.requiredOption('--category <category>', '카테고리 (modification|question|etc)')
|
|
60
|
+
.requiredOption('--content <content>', '내용')
|
|
61
|
+
.option('--spec <specId>', '관련 기능명세 ID')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
const globalOpts = program.opts();
|
|
64
|
+
try {
|
|
65
|
+
if (globalOpts.dryRun) {
|
|
66
|
+
printInfo(`[DRY-RUN] QnA "${options.title}" 등록 요청`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
70
|
+
const body: any = {
|
|
71
|
+
title: options.title,
|
|
72
|
+
category: options.category,
|
|
73
|
+
content: options.content,
|
|
74
|
+
};
|
|
75
|
+
if (options.spec) body.relatedSpec = options.spec;
|
|
76
|
+
|
|
77
|
+
const { data } = await client.post('/qna', body);
|
|
78
|
+
formatOutput(data, globalOpts);
|
|
79
|
+
printSuccess('QnA가 등록되었습니다.');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Error already handled
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ch qna answer
|
|
86
|
+
qna
|
|
87
|
+
.command('answer <qnaId>')
|
|
88
|
+
.description('QnA 답변 등록')
|
|
89
|
+
.requiredOption('--content <content>', '답변 내용')
|
|
90
|
+
.action(async (qnaId: string, options) => {
|
|
91
|
+
const globalOpts = program.opts();
|
|
92
|
+
try {
|
|
93
|
+
if (globalOpts.dryRun) {
|
|
94
|
+
printInfo(`[DRY-RUN] QnA "${qnaId}" 답변 등록 요청`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
98
|
+
const { data } = await client.patch(`/qna/${qnaId}/answer`, {
|
|
99
|
+
content: options.content,
|
|
100
|
+
});
|
|
101
|
+
formatOutput(data, globalOpts);
|
|
102
|
+
printSuccess('답변이 등록되었습니다.');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Error already handled
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ch qna update-answer
|
|
109
|
+
qna
|
|
110
|
+
.command('update-answer <qnaId>')
|
|
111
|
+
.description('QnA 답변 수정')
|
|
112
|
+
.requiredOption('--content <content>', '수정할 답변 내용')
|
|
113
|
+
.action(async (qnaId: string, options) => {
|
|
114
|
+
const globalOpts = program.opts();
|
|
115
|
+
try {
|
|
116
|
+
if (globalOpts.dryRun) {
|
|
117
|
+
printInfo(`[DRY-RUN] QnA "${qnaId}" 답변 수정 요청`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
121
|
+
const { data } = await client.patch(`/qna/${qnaId}/update-answer`, {
|
|
122
|
+
content: options.content,
|
|
123
|
+
});
|
|
124
|
+
formatOutput(data, globalOpts);
|
|
125
|
+
printSuccess('답변이 수정되었습니다.');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Error already handled
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ch qna reflect
|
|
132
|
+
qna
|
|
133
|
+
.command('reflect <qnaId>')
|
|
134
|
+
.description('QnA 반영 완료')
|
|
135
|
+
.requiredOption('--note <note>', '반영 내역')
|
|
136
|
+
.action(async (qnaId: string, options) => {
|
|
137
|
+
const globalOpts = program.opts();
|
|
138
|
+
try {
|
|
139
|
+
if (globalOpts.dryRun) {
|
|
140
|
+
printInfo(`[DRY-RUN] QnA "${qnaId}" 반영 완료 요청`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
144
|
+
const { data } = await client.patch(`/qna/${qnaId}/reflect`, {
|
|
145
|
+
note: options.note,
|
|
146
|
+
});
|
|
147
|
+
formatOutput(data, globalOpts);
|
|
148
|
+
printSuccess('반영이 완료되었습니다.');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Error already handled
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ch qna delete
|
|
155
|
+
qna
|
|
156
|
+
.command('delete <qnaId>')
|
|
157
|
+
.description('QnA 삭제')
|
|
158
|
+
.action(async (qnaId: string) => {
|
|
159
|
+
const globalOpts = program.opts();
|
|
160
|
+
try {
|
|
161
|
+
if (globalOpts.dryRun) {
|
|
162
|
+
printInfo(`[DRY-RUN] QnA "${qnaId}" 삭제 요청`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
166
|
+
await client.delete(`/qna/${qnaId}`);
|
|
167
|
+
printSuccess('QnA가 삭제되었습니다.');
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// Error already handled
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ch qna navigate
|
|
174
|
+
qna
|
|
175
|
+
.command('navigate <qnaId>')
|
|
176
|
+
.description('이전/다음 QnA 조회')
|
|
177
|
+
.requiredOption('--direction <direction>', '방향 (prev|next)')
|
|
178
|
+
.action(async (qnaId: string, options) => {
|
|
179
|
+
const globalOpts = program.opts();
|
|
180
|
+
try {
|
|
181
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
182
|
+
const { data } = await client.get(`/qna/${qnaId}/navigate`, {
|
|
183
|
+
params: { direction: options.direction },
|
|
184
|
+
});
|
|
185
|
+
formatOutput(data, globalOpts);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Error already handled
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ch qna pending-count
|
|
192
|
+
qna
|
|
193
|
+
.command('pending-count')
|
|
194
|
+
.description('답변대기 QnA 건수 조회')
|
|
195
|
+
.action(async () => {
|
|
196
|
+
const globalOpts = program.opts();
|
|
197
|
+
try {
|
|
198
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
199
|
+
const { data } = await client.get('/qna/pending-count');
|
|
200
|
+
formatOutput(data, globalOpts);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
// Error already handled
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createClient } from '../client';
|
|
3
|
+
import { formatOutput, printSuccess, printError, printInfo } from '../output';
|
|
4
|
+
import { parseCommaSeparated } from '../utils';
|
|
5
|
+
|
|
6
|
+
export function registerSpecsCommand(program: Command): void {
|
|
7
|
+
const specs = program
|
|
8
|
+
.command('specs')
|
|
9
|
+
.description('기능명세 관리');
|
|
10
|
+
|
|
11
|
+
// ch specs meta
|
|
12
|
+
specs
|
|
13
|
+
.command('meta')
|
|
14
|
+
.description('기능명세 메타데이터 조회 (프로젝트 스키마 + 사용 현황)')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
const globalOpts = program.opts();
|
|
17
|
+
try {
|
|
18
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
19
|
+
const { data } = await client.get('/specs/meta');
|
|
20
|
+
formatOutput(data, globalOpts);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Error already handled
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ch specs list
|
|
27
|
+
specs
|
|
28
|
+
.command('list')
|
|
29
|
+
.description('기능명세 목록 조회')
|
|
30
|
+
.option('--device <device>', '디바이스 필터 (해당 값을 포함하는 기능명세)')
|
|
31
|
+
.option('--domain <domain>', '도메인 필터')
|
|
32
|
+
.option('--type <featureType>', '기능유형 필터 (해당 값을 포함하는 기능명세)')
|
|
33
|
+
.option('--status <status>', '진행상황 필터')
|
|
34
|
+
.option('--group-by <field>', '그룹핑 (devices|domain|featureTypes|status)')
|
|
35
|
+
.option('--search <keyword>', '이름 검색')
|
|
36
|
+
.option('--sort <field>', '정렬 필드')
|
|
37
|
+
.option('--desc', '내림차순 정렬')
|
|
38
|
+
.option('--page <number>', '페이지 번호', '1')
|
|
39
|
+
.option('--per-page <number>', '페이지당 건수', '20')
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
const globalOpts = program.opts();
|
|
42
|
+
try {
|
|
43
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
44
|
+
const params: any = {};
|
|
45
|
+
if (options.device) params.device = options.device;
|
|
46
|
+
if (options.domain) params.domain = options.domain;
|
|
47
|
+
if (options.type) params.featureType = options.type;
|
|
48
|
+
if (options.status) params.status = options.status;
|
|
49
|
+
if (options.groupBy) params.groupBy = options.groupBy;
|
|
50
|
+
if (options.search) params.search = options.search;
|
|
51
|
+
if (options.sort) params.sortBy = options.sort;
|
|
52
|
+
if (options.desc) params.sortOrder = 'desc';
|
|
53
|
+
params.page = parseInt(options.page, 10);
|
|
54
|
+
params.perPage = parseInt(options.perPage, 10);
|
|
55
|
+
|
|
56
|
+
const { data } = await client.get('/specs', { params });
|
|
57
|
+
formatOutput(data, globalOpts);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Error already handled
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ch specs get
|
|
64
|
+
specs
|
|
65
|
+
.command('get <specId>')
|
|
66
|
+
.description('기능명세 상세 조회')
|
|
67
|
+
.action(async (specId: string) => {
|
|
68
|
+
const globalOpts = program.opts();
|
|
69
|
+
try {
|
|
70
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
71
|
+
const { data } = await client.get(`/specs/${specId}`);
|
|
72
|
+
formatOutput(data, globalOpts);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Error already handled
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ch specs create
|
|
79
|
+
specs
|
|
80
|
+
.command('create')
|
|
81
|
+
.description('기능명세 생성')
|
|
82
|
+
.requiredOption('--name <name>', '기능명')
|
|
83
|
+
.requiredOption('--device <devices>', '디바이스 (쉼표 구분, 예: web,app)')
|
|
84
|
+
.requiredOption('--domain <domain>', '도메인')
|
|
85
|
+
.requiredOption('--type <featureTypes>', '기능유형 (쉼표 구분, 예: 인증,마이페이지)')
|
|
86
|
+
.option('--permission <permissions>', '권한 (쉼표 구분, 예: 비회원,회원)')
|
|
87
|
+
.option('--content <content>', '내용')
|
|
88
|
+
.option('--dto <dto>', 'DTO')
|
|
89
|
+
.option('--status <status>', '진행상황')
|
|
90
|
+
.option('--note <note>', '비고')
|
|
91
|
+
.action(async (options) => {
|
|
92
|
+
const globalOpts = program.opts();
|
|
93
|
+
try {
|
|
94
|
+
if (globalOpts.dryRun) {
|
|
95
|
+
printInfo(`[DRY-RUN] 기능명세 "${options.name}" 생성 요청`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
99
|
+
const body: any = {
|
|
100
|
+
name: options.name,
|
|
101
|
+
devices: parseCommaSeparated(options.device),
|
|
102
|
+
domain: options.domain,
|
|
103
|
+
featureTypes: parseCommaSeparated(options.type),
|
|
104
|
+
};
|
|
105
|
+
if (options.permission) body.permissions = parseCommaSeparated(options.permission);
|
|
106
|
+
if (options.content) body.content = options.content;
|
|
107
|
+
if (options.dto) body.dto = options.dto;
|
|
108
|
+
if (options.status) body.status = options.status;
|
|
109
|
+
if (options.note) body.note = options.note;
|
|
110
|
+
|
|
111
|
+
const { data } = await client.post('/specs', body);
|
|
112
|
+
formatOutput(data, globalOpts);
|
|
113
|
+
printSuccess('기능명세가 생성되었습니다.');
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Error already handled
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ch specs update
|
|
120
|
+
specs
|
|
121
|
+
.command('update <specId>')
|
|
122
|
+
.description('기능명세 수정')
|
|
123
|
+
.option('--name <name>', '기능명')
|
|
124
|
+
.option('--device <devices>', '디바이스 (쉼표 구분)')
|
|
125
|
+
.option('--domain <domain>', '도메인')
|
|
126
|
+
.option('--type <featureTypes>', '기능유형 (쉼표 구분)')
|
|
127
|
+
.option('--permission <permissions>', '권한 (쉼표 구분)')
|
|
128
|
+
.option('--content <content>', '내용')
|
|
129
|
+
.option('--dto <dto>', 'DTO')
|
|
130
|
+
.option('--status <status>', '진행상황')
|
|
131
|
+
.option('--note <note>', '비고')
|
|
132
|
+
.option('--new-version', '새 버전으로 수정')
|
|
133
|
+
.action(async (specId: string, options) => {
|
|
134
|
+
const globalOpts = program.opts();
|
|
135
|
+
try {
|
|
136
|
+
if (globalOpts.dryRun) {
|
|
137
|
+
printInfo(`[DRY-RUN] 기능명세 "${specId}" 수정 요청`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
141
|
+
const body: any = {};
|
|
142
|
+
if (options.name) body.name = options.name;
|
|
143
|
+
if (options.device) body.devices = parseCommaSeparated(options.device);
|
|
144
|
+
if (options.domain) body.domain = options.domain;
|
|
145
|
+
if (options.type) body.featureTypes = parseCommaSeparated(options.type);
|
|
146
|
+
if (options.permission) body.permissions = parseCommaSeparated(options.permission);
|
|
147
|
+
if (options.content) body.content = options.content;
|
|
148
|
+
if (options.dto) body.dto = options.dto;
|
|
149
|
+
if (options.status) body.status = options.status;
|
|
150
|
+
if (options.note) body.note = options.note;
|
|
151
|
+
|
|
152
|
+
if (Object.keys(body).length === 0) {
|
|
153
|
+
printError('수정할 필드를 지정하세요.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const params: any = {};
|
|
158
|
+
if (options.newVersion) params.newVersion = 'true';
|
|
159
|
+
|
|
160
|
+
const { data } = await client.patch(`/specs/${specId}`, body, { params });
|
|
161
|
+
formatOutput(data, globalOpts);
|
|
162
|
+
printSuccess(options.newVersion ? '기능명세가 새 버전으로 수정되었습니다.' : '기능명세가 수정되었습니다.');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Error already handled
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ch specs set-status
|
|
169
|
+
specs
|
|
170
|
+
.command('set-status <specId> <status>')
|
|
171
|
+
.description('기능명세 진행상황 변경')
|
|
172
|
+
.action(async (specId: string, status: string) => {
|
|
173
|
+
const globalOpts = program.opts();
|
|
174
|
+
try {
|
|
175
|
+
if (globalOpts.dryRun) {
|
|
176
|
+
printInfo(`[DRY-RUN] 기능명세 "${specId}" 상태를 "${status}"로 변경 요청`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
180
|
+
const { data } = await client.patch(`/specs/${specId}/status`, { status });
|
|
181
|
+
formatOutput(data, globalOpts);
|
|
182
|
+
printSuccess(`기능명세 상태가 "${status}"로 변경되었습니다.`);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// Error already handled
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ch specs delete
|
|
189
|
+
specs
|
|
190
|
+
.command('delete <specId>')
|
|
191
|
+
.description('기능명세 삭제')
|
|
192
|
+
.action(async (specId: string) => {
|
|
193
|
+
const globalOpts = program.opts();
|
|
194
|
+
try {
|
|
195
|
+
if (globalOpts.dryRun) {
|
|
196
|
+
printInfo(`[DRY-RUN] 기능명세 "${specId}" 삭제 요청`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
200
|
+
await client.delete(`/specs/${specId}`);
|
|
201
|
+
printSuccess('기능명세가 삭제되었습니다.');
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// Error already handled
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ch specs versions
|
|
208
|
+
specs
|
|
209
|
+
.command('versions <specId>')
|
|
210
|
+
.description('기능명세 버전 이력 조회')
|
|
211
|
+
.action(async (specId: string) => {
|
|
212
|
+
const globalOpts = program.opts();
|
|
213
|
+
try {
|
|
214
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
215
|
+
const { data } = await client.get(`/specs/${specId}/versions`);
|
|
216
|
+
formatOutput(data, globalOpts);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Error already handled
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ch specs related
|
|
223
|
+
specs
|
|
224
|
+
.command('related <specId>')
|
|
225
|
+
.description('기능명세 관련 데이터 조회')
|
|
226
|
+
.requiredOption('--type <type>', '관련 데이터 유형 (sprints|qna|sqa)')
|
|
227
|
+
.action(async (specId: string, options) => {
|
|
228
|
+
const globalOpts = program.opts();
|
|
229
|
+
try {
|
|
230
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
231
|
+
const { data } = await client.get(`/specs/${specId}/related`, {
|
|
232
|
+
params: { type: options.type },
|
|
233
|
+
});
|
|
234
|
+
formatOutput(data, globalOpts);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Error already handled
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ch specs export
|
|
241
|
+
specs
|
|
242
|
+
.command('export')
|
|
243
|
+
.description('기능명세 전체 내보내기')
|
|
244
|
+
.option('--format <format>', '출력 형식 (json|csv)', 'json')
|
|
245
|
+
.action(async (options) => {
|
|
246
|
+
const globalOpts = program.opts();
|
|
247
|
+
try {
|
|
248
|
+
const client = createClient({ projectId: globalOpts.project });
|
|
249
|
+
const { data } = await client.get('/specs/export', {
|
|
250
|
+
params: { format: options.format },
|
|
251
|
+
});
|
|
252
|
+
formatOutput(data, globalOpts);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// Error already handled
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|