@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.
Files changed (101) hide show
  1. package/dist/client.d.ts +15 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +117 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/commands/archives.d.ts +3 -0
  6. package/dist/commands/archives.d.ts.map +1 -0
  7. package/dist/commands/archives.js +371 -0
  8. package/dist/commands/archives.js.map +1 -0
  9. package/dist/commands/auth.d.ts +3 -0
  10. package/dist/commands/auth.d.ts.map +1 -0
  11. package/dist/commands/auth.js +149 -0
  12. package/dist/commands/auth.js.map +1 -0
  13. package/dist/commands/dashboard.d.ts +3 -0
  14. package/dist/commands/dashboard.d.ts.map +1 -0
  15. package/dist/commands/dashboard.js +115 -0
  16. package/dist/commands/dashboard.js.map +1 -0
  17. package/dist/commands/db-schema.d.ts +3 -0
  18. package/dist/commands/db-schema.d.ts.map +1 -0
  19. package/dist/commands/db-schema.js +76 -0
  20. package/dist/commands/db-schema.js.map +1 -0
  21. package/dist/commands/dev-status.d.ts +3 -0
  22. package/dist/commands/dev-status.d.ts.map +1 -0
  23. package/dist/commands/dev-status.js +175 -0
  24. package/dist/commands/dev-status.js.map +1 -0
  25. package/dist/commands/init.d.ts +3 -0
  26. package/dist/commands/init.d.ts.map +1 -0
  27. package/dist/commands/init.js +124 -0
  28. package/dist/commands/init.js.map +1 -0
  29. package/dist/commands/invitations.d.ts +3 -0
  30. package/dist/commands/invitations.d.ts.map +1 -0
  31. package/dist/commands/invitations.js +48 -0
  32. package/dist/commands/invitations.js.map +1 -0
  33. package/dist/commands/members.d.ts +3 -0
  34. package/dist/commands/members.d.ts.map +1 -0
  35. package/dist/commands/members.js +93 -0
  36. package/dist/commands/members.js.map +1 -0
  37. package/dist/commands/notifications.d.ts +3 -0
  38. package/dist/commands/notifications.d.ts.map +1 -0
  39. package/dist/commands/notifications.js +163 -0
  40. package/dist/commands/notifications.js.map +1 -0
  41. package/dist/commands/prd.d.ts +3 -0
  42. package/dist/commands/prd.d.ts.map +1 -0
  43. package/dist/commands/prd.js +74 -0
  44. package/dist/commands/prd.js.map +1 -0
  45. package/dist/commands/projects.d.ts +3 -0
  46. package/dist/commands/projects.d.ts.map +1 -0
  47. package/dist/commands/projects.js +215 -0
  48. package/dist/commands/projects.js.map +1 -0
  49. package/dist/commands/qna.d.ts +3 -0
  50. package/dist/commands/qna.d.ts.map +1 -0
  51. package/dist/commands/qna.js +211 -0
  52. package/dist/commands/qna.js.map +1 -0
  53. package/dist/commands/specs.d.ts +3 -0
  54. package/dist/commands/specs.d.ts.map +1 -0
  55. package/dist/commands/specs.js +277 -0
  56. package/dist/commands/specs.js.map +1 -0
  57. package/dist/commands/sprints.d.ts +3 -0
  58. package/dist/commands/sprints.d.ts.map +1 -0
  59. package/dist/commands/sprints.js +262 -0
  60. package/dist/commands/sprints.js.map +1 -0
  61. package/dist/commands/sqa.d.ts +3 -0
  62. package/dist/commands/sqa.d.ts.map +1 -0
  63. package/dist/commands/sqa.js +452 -0
  64. package/dist/commands/sqa.js.map +1 -0
  65. package/dist/config.d.ts +28 -0
  66. package/dist/config.d.ts.map +1 -0
  67. package/dist/config.js +115 -0
  68. package/dist/config.js.map +1 -0
  69. package/dist/index.d.ts +3 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +97 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/output.d.ts +13 -0
  74. package/dist/output.d.ts.map +1 -0
  75. package/dist/output.js +174 -0
  76. package/dist/output.js.map +1 -0
  77. package/dist/utils.d.ts +2 -0
  78. package/dist/utils.d.ts.map +1 -0
  79. package/dist/utils.js +7 -0
  80. package/dist/utils.js.map +1 -0
  81. package/package.json +33 -0
  82. package/src/client.ts +137 -0
  83. package/src/commands/archives.ts +334 -0
  84. package/src/commands/auth.ts +154 -0
  85. package/src/commands/dashboard.ts +112 -0
  86. package/src/commands/db-schema.ts +74 -0
  87. package/src/commands/dev-status.ts +166 -0
  88. package/src/commands/init.ts +92 -0
  89. package/src/commands/members.ts +92 -0
  90. package/src/commands/notifications.ts +169 -0
  91. package/src/commands/prd.ts +72 -0
  92. package/src/commands/projects.ts +207 -0
  93. package/src/commands/qna.ts +205 -0
  94. package/src/commands/specs.ts +257 -0
  95. package/src/commands/sprints.ts +255 -0
  96. package/src/commands/sqa.ts +422 -0
  97. package/src/config.ts +98 -0
  98. package/src/index.ts +69 -0
  99. package/src/output.ts +186 -0
  100. package/src/utils.ts +3 -0
  101. 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
+ }