@bifos/dooray-cli 0.10.1 → 0.12.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/README.md +102 -20
- package/dist/index.js +188 -51
- package/package.json +3 -3
- package/skills/dooray-cli/SKILL.md +98 -26
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ dooray project members <project> # 멤버 목록
|
|
|
50
50
|
dooray project workflows <project> # 워크플로우 목록
|
|
51
51
|
dooray project groups <project> # 멤버 그룹 목록 (ID / Code)
|
|
52
52
|
dooray project tags <project> # 태그 목록 (ID / Color / Name / Group / Mandatory)
|
|
53
|
-
dooray project templates <project> # 템플릿 목록 (ID / Template Name
|
|
53
|
+
dooray project templates <project> # 템플릿 목록 (ID / Template Name)
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
> **태그 캐시 갱신**: 이전 버전에서 캐시한 태그가 색상 없이 표시되면 `dooray cache clear` 실행 후 다시 조회하세요.
|
|
@@ -59,7 +59,7 @@ dooray project templates <project> # 템플릿 목록 (ID / Template Na
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
dooray member list <project> # 프로젝트 멤버 목록 (이름·organizationMemberId)
|
|
62
|
-
dooray member get <organizationMemberId> # 멤버 상세 (cache
|
|
62
|
+
dooray member get <organizationMemberId> # 멤버 상세 (cache 우회)
|
|
63
63
|
|
|
64
64
|
# organization 전체 멤버 검색
|
|
65
65
|
dooray member search 홍길동 # 이름 검색
|
|
@@ -83,13 +83,34 @@ dooray post get <project> 42 --json # JSON 출력
|
|
|
83
83
|
| 방식 | 예시 |
|
|
84
84
|
|---|---|
|
|
85
85
|
| `<project> <number>` | `dooray post get <project> 42` |
|
|
86
|
-
| Dooray URL positional
|
|
87
|
-
| Dooray URL positional (브라우저 주소창 복사본) | `dooray post get https://x.dooray.com/task/<projectId>/<postId>` |
|
|
86
|
+
| Dooray URL positional | `dooray post get https://x.dooray.com/task/to/<postId>` |
|
|
88
87
|
| `--id <postId>` | `dooray post get --id <postId>` |
|
|
89
|
-
| `--url <url>` | `dooray post get --url https://x.dooray.com/task/to
|
|
88
|
+
| `--url <url>` | `dooray post get --url https://x.dooray.com/task/to/<postId>` |
|
|
89
|
+
|
|
90
|
+
지원 URL 형식 3종 (positional 첫 인자 / `--url` 공통):
|
|
91
|
+
|
|
92
|
+
- `https://*.dooray.com/task/to/<postId>`
|
|
93
|
+
- `https://*.dooray.com/task/<projectId>/<postId>` — 브라우저 주소창 복사본
|
|
94
|
+
- `https://*.dooray.com/project/tasks/<postId>` — 프로젝트 업무 목록에서 열기
|
|
90
95
|
|
|
91
96
|
대상: `post get`/`edit`/`done`/`workflow`, `post comment list`/`add`/`edit`/`delete`, `post file list`/`upload`/`download`/`download-all`/`delete`.
|
|
92
|
-
AI 에이전트는 사용자 메시지의 Dooray URL을 그대로 첫 인자로 전달하면 가장
|
|
97
|
+
AI 에이전트는 사용자 메시지의 Dooray URL을 그대로 첫 인자로 전달하면 가장 빠르다.
|
|
98
|
+
|
|
99
|
+
**projectId 직접 입력**:
|
|
100
|
+
|
|
101
|
+
`member=me` 응답에 없는 프로젝트 (다른 팀 / 권한만 있는 프로젝트) 도 projectId (15+자리 numeric) 를 직접 입력하면 자동으로 cache 우회.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# 코드 매칭 (기존)
|
|
105
|
+
dooray post search <project> "keyword"
|
|
106
|
+
|
|
107
|
+
# projectId 직접 입력 — member 아닌 프로젝트도 자동화 가능
|
|
108
|
+
dooray post search 1234567890123456789 "keyword"
|
|
109
|
+
dooray post list 1234567890123456789
|
|
110
|
+
dooray member list 1234567890123456789
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
권한 검증은 후속 API 호출 시점 — 권한 없으면 4xx.
|
|
93
114
|
|
|
94
115
|
### 업무 생성
|
|
95
116
|
|
|
@@ -115,7 +136,18 @@ dooray post create <project> \
|
|
|
115
136
|
> mandatory-tag 정책 프로젝트(예: `<project>`)에서는 mandatory 그룹마다 1개 이상 `--tag`로 지정해야 한다.
|
|
116
137
|
> 누락 시 클라이언트가 사전 검증으로 후보 목록과 함께 에러 출력.
|
|
117
138
|
|
|
118
|
-
|
|
139
|
+
> **`post create` 출력의 `.id` 는 internal postId (19자리 숫자)입니다.**
|
|
140
|
+
> 이 ID 로 후속 조회·수정·댓글 작업을 할 때는 **`--id <postId>`** 를 사용하세요.
|
|
141
|
+
> `<project> <업무번호>` 의 번호 자리에 postId 를 넣으면 안내 에러가 발생합니다.
|
|
142
|
+
>
|
|
143
|
+
> ```bash
|
|
144
|
+
> # 생성 후 바로 조회하는 패턴
|
|
145
|
+
> POST_ID=$(dooray post create <project> --title "..." --json | jq -r '.id')
|
|
146
|
+
> dooray post get --id "$POST_ID"
|
|
147
|
+
> dooray post comment add --id "$POST_ID" --body "첫 댓글"
|
|
148
|
+
> ```
|
|
149
|
+
|
|
150
|
+
#### 템플릿 기반 정형 task
|
|
119
151
|
|
|
120
152
|
```bash
|
|
121
153
|
# 프로젝트의 템플릿 목록
|
|
@@ -166,7 +198,7 @@ dooray post comment add <project> 42 --body "댓글 내용"
|
|
|
166
198
|
dooray post comment add <project> 42 --body-file ./comment.md
|
|
167
199
|
```
|
|
168
200
|
|
|
169
|
-
> table 출력의 Creator 컬럼은 프로젝트 멤버 캐시로 자동 enrich되며, `--json`은 raw 응답을
|
|
201
|
+
> table 출력의 Creator 컬럼은 프로젝트 멤버 캐시로 자동 enrich되며, `--json`은 raw 응답을 유지한다.
|
|
170
202
|
|
|
171
203
|
#### 멘션·내부 링크 자동 삽입
|
|
172
204
|
|
|
@@ -190,7 +222,7 @@ dooray post comment add <project> <post-number> --body "..." --link-task <projec
|
|
|
190
222
|
> `--mention` / `--mention-group` / `--link-task` / `--dry-run` 은 `post create`, `post edit`, `post comment add`, `post comment edit` 4 명령 모두 지원.
|
|
191
223
|
> 이전 버전 캐시는 orgId가 없으므로 첫 호출 시 자동 갱신됩니다 (또는 `dooray cache clear`).
|
|
192
224
|
|
|
193
|
-
#### 참조자(cc) / 담당자(to) 변경
|
|
225
|
+
#### 참조자(cc) / 담당자(to) 변경
|
|
194
226
|
|
|
195
227
|
```bash
|
|
196
228
|
# 멤버/그룹 추가 (기존 참조자 유지 + dedupe)
|
|
@@ -209,7 +241,7 @@ interactive ($EDITOR) 모드에서는 위 6개 옵션이 무시되고 stderr 경
|
|
|
209
241
|
`post edit --dry-run --json` 사용 시 출력에 `users: { to, cc }` 가 포함되어 API 호출 없이 변경 결과 미리보기 가능.
|
|
210
242
|
`post create --dry-run` 은 본문만 출력하며 `users` 는 포함하지 않음.
|
|
211
243
|
|
|
212
|
-
**그룹 cc / mention 사용
|
|
244
|
+
**그룹 cc / mention 사용 예**:
|
|
213
245
|
|
|
214
246
|
```bash
|
|
215
247
|
# code 부분일치
|
|
@@ -230,7 +262,7 @@ dooray post edit <project> <post-number> --cc-group dev-team --dry-run --json |
|
|
|
230
262
|
# 출력 예: [{ "type": "group", "group": { "projectMemberGroupId": "...", "members": [] } }, ...]
|
|
231
263
|
```
|
|
232
264
|
|
|
233
|
-
#### 생성 후 태그 변경
|
|
265
|
+
#### 생성 후 태그 변경
|
|
234
266
|
|
|
235
267
|
```bash
|
|
236
268
|
# 기존 태그 유지 + 신규 추가 (dedupe)
|
|
@@ -244,7 +276,7 @@ dooray post edit --id <postId> --tag-remove "<group>: <name>"
|
|
|
244
276
|
```
|
|
245
277
|
|
|
246
278
|
`--title` / `--body` 없이 단독 호출 가능 — 기존 본문은 자동 재전송.
|
|
247
|
-
mandatory tag 그룹은 `post create` 와 동일하게 사전
|
|
279
|
+
mandatory tag 그룹은 `post create` 와 동일하게 사전 검증.
|
|
248
280
|
|
|
249
281
|
#### 상위 업무 변경 (`--parent`)
|
|
250
282
|
|
|
@@ -261,7 +293,7 @@ dooray post edit --id <postId> --title "<원제목>" --parent <other-parent-post
|
|
|
261
293
|
|
|
262
294
|
interactive ($EDITOR) 모드에서 `--parent` 사용 시 무시 + stderr 경고.
|
|
263
295
|
**parent 만 단독 변경하려면 `--title "<원제목>"` 동반 필요** — `post edit` 는 본문 변경(`--title`/`--body`) 동반 시에만 non-interactive 분기로 들어감.
|
|
264
|
-
parent 변경은 그 분기 안에서만 수행됨.
|
|
296
|
+
parent 변경은 그 분기 안에서만 수행됨.
|
|
265
297
|
|
|
266
298
|
`--dry-run --json` 출력의 `parentChange` 필드는 **사용자 입력 원문 그대로** (`<project>/<number>` 또는 raw postId).
|
|
267
299
|
resolver 처리 전 미리보기 값이며 실제 호출 대상 `postId` 가 아님.
|
|
@@ -269,7 +301,7 @@ dry-run 은 API 미호출 원칙을 유지해 `resolvePostRef` 도 건너뜀.
|
|
|
269
301
|
|
|
270
302
|
#### `--to` / `--cc` / `--mention` 입력 형식 (자동 분기)
|
|
271
303
|
|
|
272
|
-
이름 외에도 이메일 / organizationMemberId 직접 입력 가능 — **동명이인 우회 + ID 직접
|
|
304
|
+
이름 외에도 이메일 / organizationMemberId 직접 입력 가능 — **동명이인 우회 + ID 직접 입력**:
|
|
273
305
|
|
|
274
306
|
```bash
|
|
275
307
|
# 이름 (이전부터 지원, 부분일치)
|
|
@@ -353,7 +385,7 @@ dooray wiki page edit <project> <page-id> --body "..." | --body-file <path> #
|
|
|
353
385
|
dooray wiki page edit <project> <page-id> # $EDITOR (플래그 없을 때)
|
|
354
386
|
```
|
|
355
387
|
|
|
356
|
-
#### 위키 페이지 첨부파일
|
|
388
|
+
#### 위키 페이지 첨부파일
|
|
357
389
|
|
|
358
390
|
post 의 `post file` 명령군과 동일 패턴 — `<project> <page-id>` 외에도 `--id`/`--url`/positional URL 지원.
|
|
359
391
|
|
|
@@ -383,11 +415,42 @@ dooray wiki page file list "https://<tenant>.dooray.com/wiki/<wikiId>/<pageId>"
|
|
|
383
415
|
dooray wiki page file upload --id <pageId> --project <project> --file ./README.md
|
|
384
416
|
```
|
|
385
417
|
|
|
418
|
+
`--json` 옵션으로 자동화 파이프라인에서 동일 parse 코드를 사용할 수 있습니다:
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
# download — { outputPath, fileName, size }
|
|
422
|
+
dooray wiki page file download <project> <page-id> --file-id <id> -o ./ --json
|
|
423
|
+
|
|
424
|
+
# download-all — { count, succeeded, failed } (부분 실패 시 exit 1)
|
|
425
|
+
dooray wiki page file download-all <project> <page-id> -o ./ --json | jq '.failed'
|
|
426
|
+
|
|
427
|
+
# delete — { fileId, status: "deleted" }
|
|
428
|
+
dooray wiki page file delete <project> <page-id> --file-id <id> --json
|
|
429
|
+
|
|
430
|
+
# upload (general) — res.result raw (--quiet 는 id 만)
|
|
431
|
+
dooray wiki page file upload <project> <page-id> --file ./report.pdf --json
|
|
432
|
+
|
|
433
|
+
# upload (inline_image) — res.result + markdownSnippet 필드 추가
|
|
434
|
+
dooray wiki page file upload <project> <page-id> --file ./diagram.png --type inline_image --json
|
|
435
|
+
# 출력 예:
|
|
436
|
+
# {
|
|
437
|
+
# "id": "<id>",
|
|
438
|
+
# "attachFileId": "<attachFileId>",
|
|
439
|
+
# "name": "diagram.png",
|
|
440
|
+
# "size": 12345,
|
|
441
|
+
# "type": "inline_image",
|
|
442
|
+
# "markdownSnippet": ""
|
|
443
|
+
# }
|
|
444
|
+
|
|
445
|
+
# 자동화: markdownSnippet 을 jq 로 추출해 본문에 삽입
|
|
446
|
+
SNIPPET=$(dooray wiki page file upload <project> <page-id> --file ./diagram.png --type inline_image --json | jq -r '.markdownSnippet')
|
|
447
|
+
```
|
|
448
|
+
|
|
386
449
|
**주의**:
|
|
387
450
|
- `upload` 시 multipart 필드 순서 (`type` → `file`) 가 중요.
|
|
388
|
-
클라이언트가 자동으로 강제
|
|
451
|
+
클라이언트가 자동으로 강제
|
|
389
452
|
- `inline_image` 로 올린 파일은 본문에 markdown 으로 박혀야 위키에서 보임.
|
|
390
|
-
|
|
453
|
+
`--json` 의 `markdownSnippet` 을 복사하거나 jq 로 추출해 `dooray wiki page edit` 본문에 직접 추가
|
|
391
454
|
- `delete` 는 confirm 없이 즉시 삭제 (실수 방지 책임은 호출자)
|
|
392
455
|
|
|
393
456
|
#### 위키 페이지 댓글
|
|
@@ -474,6 +537,25 @@ dooray post file upload <project> <number> ./report.pdf
|
|
|
474
537
|
dooray post file delete <project> <number> <file-id>
|
|
475
538
|
```
|
|
476
539
|
|
|
540
|
+
자동화 스크립트에서 `--json` 옵션으로 구조화된 출력을 파이프로 가공할 수 있습니다:
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
# download — { outputPath, fileName, size }
|
|
544
|
+
dooray post file download <project> <number> --file-id <id> -o ./ --json
|
|
545
|
+
# 출력: {"outputPath": "./<fileName>", "fileName": "...", "size": 12345}
|
|
546
|
+
|
|
547
|
+
# download-all — { count, succeeded, failed } (부분 실패 시 exit 1)
|
|
548
|
+
dooray post file download-all <project> <number> -o ./ --json | jq '.failed'
|
|
549
|
+
# 출력: [] 또는 [{"fileId": "...", "error": "..."}]
|
|
550
|
+
|
|
551
|
+
# delete — { fileId, status: "deleted" }
|
|
552
|
+
dooray post file delete <project> <number> --file-id <id> --json
|
|
553
|
+
# 출력: {"fileId": "...", "status": "deleted"}
|
|
554
|
+
|
|
555
|
+
# upload — res.result raw (--quiet 는 id 만)
|
|
556
|
+
dooray post file upload <project> <number> ./report.pdf --json
|
|
557
|
+
```
|
|
558
|
+
|
|
477
559
|
URL/`--id`/`--url` 모드에서는 sub-id를 옵션으로 전달:
|
|
478
560
|
```bash
|
|
479
561
|
dooray post file download --url <url> --file-id <fileId> -o ./downloads
|
|
@@ -486,7 +568,7 @@ dooray post comment delete --url <url> --comment-id <commentId>
|
|
|
486
568
|
### 댓글 첨부 파일 (`post comment file *`)
|
|
487
569
|
|
|
488
570
|
자동화로 댓글에 인라인 이미지 / 파일을 삽입할 때 사용.
|
|
489
|
-
4 명령 (list/upload/download/delete) 모두 `<project> <post-number> <comment-id>` 또는 `--id <postId> --comment-id <logId>` / `--url <url> --comment-id <logId>` 패턴
|
|
571
|
+
4 명령 (list/upload/download/delete) 모두 `<project> <post-number> <comment-id>` 또는 `--id <postId> --comment-id <logId>` / `--url <url> --comment-id <logId>` 패턴 지원.
|
|
490
572
|
|
|
491
573
|
```bash
|
|
492
574
|
# 첨부 목록
|
|
@@ -503,7 +585,7 @@ dooray post comment file delete <project> <post-num> <comment-id> <file-id> --ye
|
|
|
503
585
|
```
|
|
504
586
|
|
|
505
587
|
> Dooray REST API 가 댓글 전용 attachment endpoint 를 제공하지 않아 내부적으로
|
|
506
|
-
> post-level files API 와 댓글 본문 PUT 의 합성으로
|
|
588
|
+
> post-level files API 와 댓글 본문 PUT 의 합성으로 동작한다. 단일 명령
|
|
507
589
|
> = 단일 파일 — 다중 파일은 호출자가 반복 호출.
|
|
508
590
|
|
|
509
591
|
## 출력 모드
|
|
@@ -550,7 +632,7 @@ dooray feedback --last --title "재현" --body "추가 설명" --dry-run # 미
|
|
|
550
632
|
dooray feedback --dry-run
|
|
551
633
|
```
|
|
552
634
|
|
|
553
|
-
> **개인정보
|
|
635
|
+
> **개인정보 보호**: `--last` 모드에서 argv는 `--api-key`/`--token`/`Authorization` 등 시크릿 패턴을 자동 마스킹 후 저장합니다. cwd/env는 미저장.
|
|
554
636
|
|
|
555
637
|
## 캐시
|
|
556
638
|
|
package/dist/index.js
CHANGED
|
@@ -1206,6 +1206,7 @@ var setupCommand = new import_commander4.Command("setup").description("\uB300\uD
|
|
|
1206
1206
|
var import_commander5 = require("commander");
|
|
1207
1207
|
|
|
1208
1208
|
// src/resolvers/project.ts
|
|
1209
|
+
var PROJECT_ID_RE = /^\d{15,}$/;
|
|
1209
1210
|
async function fetchAllProjects(client, options) {
|
|
1210
1211
|
const all = [];
|
|
1211
1212
|
let page = 0;
|
|
@@ -1243,6 +1244,9 @@ async function ensurePrivateProjects(client) {
|
|
|
1243
1244
|
return items;
|
|
1244
1245
|
}
|
|
1245
1246
|
async function resolveProject(client, input2) {
|
|
1247
|
+
if (PROJECT_ID_RE.test(input2)) {
|
|
1248
|
+
return input2;
|
|
1249
|
+
}
|
|
1246
1250
|
const projects = await ensureProjects(client);
|
|
1247
1251
|
const match = projects.find((p) => p.code === input2 || p.id === input2);
|
|
1248
1252
|
if (match) return match.id;
|
|
@@ -1253,7 +1257,8 @@ async function resolveProject(client, input2) {
|
|
|
1253
1257
|
}
|
|
1254
1258
|
throw new DoorayCliError(
|
|
1255
1259
|
`\uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input2}
|
|
1256
|
-
\uAC1C\uC778 \uD504\uB85C\uC81D\uD2B8\uB77C\uBA74: dooray project list --type private \uB85C \uCE90\uC2DC\uB97C \uAC31\uC2E0\uD558\uC138\uC694
|
|
1260
|
+
\uAC1C\uC778 \uD504\uB85C\uC81D\uD2B8\uB77C\uBA74: dooray project list --type private \uB85C \uCE90\uC2DC\uB97C \uAC31\uC2E0\uD558\uC138\uC694
|
|
1261
|
+
member=me \uC751\uB2F5\uC5D0 \uC5C6\uB294 \uD504\uB85C\uC81D\uD2B8\uB294 projectId (15+\uC790\uB9AC numeric) \uC9C1\uC811 \uC785\uB825\uC73C\uB85C \uC6B0\uD68C \uAC00\uB2A5 (ADR-030)`,
|
|
1257
1262
|
EXIT_PARAM_ERROR
|
|
1258
1263
|
);
|
|
1259
1264
|
}
|
|
@@ -1994,12 +1999,15 @@ async function resolvePost(client, projectId, postNumber) {
|
|
|
1994
1999
|
// src/utils/dooray-url.ts
|
|
1995
2000
|
var TASK_URL_RE = /^https?:\/\/[\w.-]+\.dooray\.com\/task\/to\/(\d+)(?:[/?#].*)?$/;
|
|
1996
2001
|
var TASK_URL_ALT_RE = /^https?:\/\/[\w.-]+\.dooray\.com\/task\/(?:\d+)\/(\d+)(?:[/?#].*)?$/;
|
|
2002
|
+
var TASK_PROJECT_TASKS_RE = /^https?:\/\/[\w.-]+\.dooray\.com\/project\/tasks\/(\d+)(?:[/?#].*)?$/;
|
|
1997
2003
|
var WIKI_URL_RE = /^https?:\/\/[\w.-]+\.dooray\.com\/wiki\/(\d+)\/(\d+)(?:[/?#].*)?$/;
|
|
1998
2004
|
function parseDoorayTaskUrl(input2) {
|
|
1999
2005
|
const m1 = TASK_URL_RE.exec(input2);
|
|
2000
2006
|
if (m1) return m1[1];
|
|
2001
2007
|
const m2 = TASK_URL_ALT_RE.exec(input2);
|
|
2002
2008
|
if (m2) return m2[1];
|
|
2009
|
+
const m3 = TASK_PROJECT_TASKS_RE.exec(input2);
|
|
2010
|
+
if (m3) return m3[1];
|
|
2003
2011
|
return null;
|
|
2004
2012
|
}
|
|
2005
2013
|
function parseDoorayWikiUrl(input2) {
|
|
@@ -2012,7 +2020,18 @@ function isLikelyDoorayUrl(input2) {
|
|
|
2012
2020
|
}
|
|
2013
2021
|
|
|
2014
2022
|
// src/resolvers/post-input.ts
|
|
2015
|
-
|
|
2023
|
+
function classifyPostInputToken(token) {
|
|
2024
|
+
if (/^https?:\/\//.test(token)) return "url";
|
|
2025
|
+
if (/^\d{15,}$/.test(token)) return "postId";
|
|
2026
|
+
if (/^\d+$/.test(token)) return "postNumber";
|
|
2027
|
+
return "project";
|
|
2028
|
+
}
|
|
2029
|
+
var URL_FORMAT_HINT = "\uC9C0\uC6D0 \uD615\uC2DD:\n https://x.dooray.com/task/to/{postId}\n https://x.dooray.com/task/{projectId}/{postId}\n https://x.dooray.com/project/tasks/{postId}";
|
|
2030
|
+
var INPUT_HELP = `\uC5C5\uBB34\uB97C \uC2DD\uBCC4\uD560 \uC815\uBCF4\uAC00 \uBD80\uC871\uD569\uB2C8\uB2E4. \uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC785\uB825\uD558\uC138\uC694:
|
|
2031
|
+
- <project> <post-number> \uC608: my-project 337
|
|
2032
|
+
- --id <postId> \uC608: --id 1234567890123456789
|
|
2033
|
+
- <Dooray URL>
|
|
2034
|
+
${URL_FORMAT_HINT}`;
|
|
2016
2035
|
async function resolveByPostId(client, postId) {
|
|
2017
2036
|
const res = await client.getPostStandalone(postId);
|
|
2018
2037
|
const d = res.result;
|
|
@@ -2043,13 +2062,28 @@ async function resolvePostInput(client, args) {
|
|
|
2043
2062
|
if (!postId) {
|
|
2044
2063
|
throw new DoorayCliError(
|
|
2045
2064
|
`--url \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: "${urlOpt}"
|
|
2046
|
-
|
|
2065
|
+
${URL_FORMAT_HINT}`,
|
|
2047
2066
|
EXIT_PARAM_ERROR
|
|
2048
2067
|
);
|
|
2049
2068
|
}
|
|
2050
2069
|
return resolveByPostId(client, postId);
|
|
2051
2070
|
}
|
|
2052
2071
|
if (idOpt) {
|
|
2072
|
+
const idType = classifyPostInputToken(idOpt);
|
|
2073
|
+
if (idType === "postNumber") {
|
|
2074
|
+
throw new DoorayCliError(
|
|
2075
|
+
`"${idOpt}" \uB294 \uC5C5\uBB34 \uBC88\uD638\uB85C \uBCF4\uC785\uB2C8\uB2E4. <project> <post-number> \uD615\uC2DD\uC744 \uC4F0\uC138\uC694:
|
|
2076
|
+
dooray post get <project> ${idOpt}`,
|
|
2077
|
+
EXIT_PARAM_ERROR
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
if (idType === "url") {
|
|
2081
|
+
throw new DoorayCliError(
|
|
2082
|
+
`"${idOpt}" \uB294 URL \uD615\uC2DD\uC785\uB2C8\uB2E4. --url \uC635\uC158\uC744 \uC4F0\uC138\uC694:
|
|
2083
|
+
dooray post get --url "${idOpt}"`,
|
|
2084
|
+
EXIT_PARAM_ERROR
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2053
2087
|
return resolveByPostId(client, idOpt);
|
|
2054
2088
|
}
|
|
2055
2089
|
if (projectArg && !postNumberArg && isLikelyDoorayUrl(projectArg)) {
|
|
@@ -2057,13 +2091,28 @@ async function resolvePostInput(client, args) {
|
|
|
2057
2091
|
if (!postId) {
|
|
2058
2092
|
throw new DoorayCliError(
|
|
2059
2093
|
`Dooray URL \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: "${projectArg}"
|
|
2060
|
-
|
|
2094
|
+
${URL_FORMAT_HINT}`,
|
|
2061
2095
|
EXIT_PARAM_ERROR
|
|
2062
2096
|
);
|
|
2063
2097
|
}
|
|
2064
2098
|
return resolveByPostId(client, postId);
|
|
2065
2099
|
}
|
|
2066
2100
|
if (projectArg && postNumberArg) {
|
|
2101
|
+
const numType = classifyPostInputToken(postNumberArg);
|
|
2102
|
+
if (numType === "postId") {
|
|
2103
|
+
throw new DoorayCliError(
|
|
2104
|
+
`"${postNumberArg}" \uB294 \uB0B4\uBD80 ID(postId)\uB85C \uBCF4\uC785\uB2C8\uB2E4.
|
|
2105
|
+
\uC5C5\uBB34 \uBC88\uD638(#N)\uAC00 \uC544\uB2CC \uB0B4\uBD80 ID \uB85C \uC870\uD68C\uD558\uB824\uBA74 --id \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uC138\uC694:
|
|
2106
|
+
dooray post get --id ${postNumberArg}`,
|
|
2107
|
+
EXIT_PARAM_ERROR
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
if (numType !== "postNumber") {
|
|
2111
|
+
throw new DoorayCliError(
|
|
2112
|
+
`<post-number>\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: "${postNumberArg}"`,
|
|
2113
|
+
EXIT_PARAM_ERROR
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2067
2116
|
const projectId = await resolveProject(client, projectArg);
|
|
2068
2117
|
const num = Number(postNumberArg);
|
|
2069
2118
|
if (!Number.isFinite(num) || num <= 0) {
|
|
@@ -3767,7 +3816,49 @@ var fileListCommand = new import_commander29.Command("list").description("\uC5C5
|
|
|
3767
3816
|
var import_commander30 = require("commander");
|
|
3768
3817
|
var import_promises9 = require("fs/promises");
|
|
3769
3818
|
var import_node_path6 = require("path");
|
|
3819
|
+
|
|
3820
|
+
// src/formatters/file-output.ts
|
|
3821
|
+
function emitDownloadResult(globalOpts, result) {
|
|
3822
|
+
if (globalOpts.json) {
|
|
3823
|
+
printJson(result);
|
|
3824
|
+
} else {
|
|
3825
|
+
process.stdout.write(`${result.outputPath}
|
|
3826
|
+
`);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
function emitDownloadAllResult(globalOpts, data) {
|
|
3830
|
+
if (globalOpts.json) {
|
|
3831
|
+
printJson(data);
|
|
3832
|
+
} else if (globalOpts.quiet) {
|
|
3833
|
+
for (const s of data.succeeded) {
|
|
3834
|
+
process.stdout.write(`${s.path}
|
|
3835
|
+
`);
|
|
3836
|
+
}
|
|
3837
|
+
if (data.failed.length > 0) {
|
|
3838
|
+
process.stderr.write(`${data.failed.length} failed
|
|
3839
|
+
`);
|
|
3840
|
+
}
|
|
3841
|
+
} else {
|
|
3842
|
+
process.stdout.write(`
|
|
3843
|
+
\uC644\uB8CC: ${data.succeeded.length}/${data.count}
|
|
3844
|
+
`);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
function emitDeleteResult(globalOpts, result) {
|
|
3848
|
+
if (globalOpts.json) {
|
|
3849
|
+
printJson({ fileId: result.fileId, status: "deleted" });
|
|
3850
|
+
} else if (globalOpts.quiet) {
|
|
3851
|
+
process.stdout.write(`${result.fileId}
|
|
3852
|
+
`);
|
|
3853
|
+
} else {
|
|
3854
|
+
process.stdout.write(`\uD30C\uC77C(${result.fileId})\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
|
|
3855
|
+
`);
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
// src/commands/post/file/download.ts
|
|
3770
3860
|
var fileDownloadCommand = new import_commander30.Command("download").description("\uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "\uC5C5\uBB34 \uBC88\uD638 \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C ID (positional \uBAA8\uB4DC)").option("--id <postId>", "Dooray post ID (project/post-number \uB300\uC2E0)").option("--url <url>", "Dooray \uC5C5\uBB34 URL (project/post-number \uB300\uC2E0)").option("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (arg1, arg2, arg3, opts) => {
|
|
3861
|
+
const globalOpts = fileDownloadCommand.optsWithGlobals();
|
|
3771
3862
|
const config = await getConfigOrThrow();
|
|
3772
3863
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
3773
3864
|
let projectArg;
|
|
@@ -3814,11 +3905,11 @@ var fileDownloadCommand = new import_commander30.Command("download").description
|
|
|
3814
3905
|
urlOpt: opts.url
|
|
3815
3906
|
});
|
|
3816
3907
|
const { buffer, fileName } = await client.downloadPostFile(projectId, postId, fileId);
|
|
3817
|
-
const
|
|
3908
|
+
const safeName = (0, import_node_path6.basename)(decodeURIComponent(fileName));
|
|
3909
|
+
const outputPath = (0, import_node_path6.join)(opts.output, safeName);
|
|
3818
3910
|
await (0, import_promises9.writeFile)(outputPath, Buffer.from(buffer));
|
|
3819
3911
|
stopSpinner(true, "\uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC");
|
|
3820
|
-
|
|
3821
|
-
`);
|
|
3912
|
+
emitDownloadResult(globalOpts, { outputPath, fileName: safeName, size: buffer.byteLength });
|
|
3822
3913
|
});
|
|
3823
3914
|
|
|
3824
3915
|
// src/commands/post/file/download-all.ts
|
|
@@ -3826,9 +3917,10 @@ var import_commander31 = require("commander");
|
|
|
3826
3917
|
var import_promises10 = require("fs/promises");
|
|
3827
3918
|
var import_node_path7 = require("path");
|
|
3828
3919
|
var fileDownloadAllCommand = new import_commander31.Command("download-all").description("\uC5C5\uBB34\uC758 \uBAA8\uB4E0 \uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC").argument("[project]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (\uB610\uB294 \uCCAB \uC778\uC790\uC5D0 Dooray URL)").argument("[post-number]", "\uC5C5\uBB34 \uBC88\uD638 (project\uC640 \uD568\uAED8 \uC0AC\uC6A9)").option("--id <postId>", "Dooray post ID (project/post-number \uB300\uC2E0)").option("--url <url>", "Dooray \uC5C5\uBB34 URL (project/post-number \uB300\uC2E0)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (project, postNumberStr, opts) => {
|
|
3920
|
+
const globalOpts = fileDownloadAllCommand.optsWithGlobals();
|
|
3829
3921
|
const config = await getConfigOrThrow();
|
|
3830
3922
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
3831
|
-
|
|
3923
|
+
startSpinner("\uCCA8\uBD80\uD30C\uC77C \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
3832
3924
|
const { projectId, postId } = await resolvePostInput(client, {
|
|
3833
3925
|
projectArg: project,
|
|
3834
3926
|
postNumberArg: postNumberStr,
|
|
@@ -3838,23 +3930,38 @@ var fileDownloadAllCommand = new import_commander31.Command("download-all").desc
|
|
|
3838
3930
|
const res = await client.getPostFiles(projectId, postId);
|
|
3839
3931
|
if (res.result.length === 0) {
|
|
3840
3932
|
stopSpinner(true, "\uCCA8\uBD80\uD30C\uC77C \uC5C6\uC74C");
|
|
3841
|
-
|
|
3933
|
+
if (globalOpts.json) {
|
|
3934
|
+
printJson({ count: 0, succeeded: [], failed: [] });
|
|
3935
|
+
} else if (!globalOpts.quiet) {
|
|
3936
|
+
process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
3937
|
+
}
|
|
3842
3938
|
return;
|
|
3843
3939
|
}
|
|
3940
|
+
stopSpinner(true, `${res.result.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2DC\uC791`);
|
|
3844
3941
|
await (0, import_promises10.mkdir)(opts.output, { recursive: true });
|
|
3845
|
-
const
|
|
3942
|
+
const succeeded = [];
|
|
3943
|
+
const failed = [];
|
|
3846
3944
|
for (const file of res.result) {
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
process.stdout.write(`${path3}
|
|
3945
|
+
try {
|
|
3946
|
+
const { buffer, fileName } = await client.downloadPostFile(projectId, postId, file.id);
|
|
3947
|
+
const safeName = (0, import_node_path7.basename)(decodeURIComponent(fileName));
|
|
3948
|
+
const outputPath = (0, import_node_path7.join)(opts.output, safeName);
|
|
3949
|
+
await (0, import_promises10.writeFile)(outputPath, Buffer.from(buffer));
|
|
3950
|
+
succeeded.push({ path: outputPath, fileName: safeName });
|
|
3951
|
+
if (!globalOpts.json && !globalOpts.quiet) {
|
|
3952
|
+
process.stdout.write(`\u2713 ${safeName}
|
|
3856
3953
|
`);
|
|
3954
|
+
}
|
|
3955
|
+
} catch (e) {
|
|
3956
|
+
failed.push({ fileId: file.id, error: e instanceof Error ? e.message : String(e) });
|
|
3957
|
+
if (!globalOpts.json) {
|
|
3958
|
+
process.stderr.write(`\u2717 ${file.name} (${file.id}): ${e instanceof Error ? e.message : String(e)}
|
|
3959
|
+
`);
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3857
3962
|
}
|
|
3963
|
+
emitDownloadAllResult(globalOpts, { count: res.result.length, succeeded, failed });
|
|
3964
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
3858
3965
|
});
|
|
3859
3966
|
|
|
3860
3967
|
// src/commands/post/file/upload.ts
|
|
@@ -3923,6 +4030,7 @@ var fileUploadCommand = new import_commander32.Command("upload").description("\u
|
|
|
3923
4030
|
// src/commands/post/file/delete.ts
|
|
3924
4031
|
var import_commander33 = require("commander");
|
|
3925
4032
|
var fileDeleteCommand = new import_commander33.Command("delete").description("\uCCA8\uBD80\uD30C\uC77C \uC0AD\uC81C").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "\uC5C5\uBB34 \uBC88\uD638 \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C ID (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <postId>", "Dooray post ID (project/post-number \uB300\uC2E0)").option("--url <url>", "Dooray \uC5C5\uBB34 URL (project/post-number \uB300\uC2E0)").option("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").action(async (arg1, arg2, arg3, opts) => {
|
|
4033
|
+
const globalOpts = fileDeleteCommand.optsWithGlobals();
|
|
3926
4034
|
const config = await getConfigOrThrow();
|
|
3927
4035
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
3928
4036
|
let projectArg;
|
|
@@ -3970,8 +4078,7 @@ var fileDeleteCommand = new import_commander33.Command("delete").description("\u
|
|
|
3970
4078
|
});
|
|
3971
4079
|
await client.deletePostFile(projectId, postId, fileId);
|
|
3972
4080
|
stopSpinner(true, "\uC0AD\uC81C \uC644\uB8CC");
|
|
3973
|
-
|
|
3974
|
-
`);
|
|
4081
|
+
emitDeleteResult(globalOpts, { fileId });
|
|
3975
4082
|
});
|
|
3976
4083
|
|
|
3977
4084
|
// src/commands/wiki/list.ts
|
|
@@ -4278,7 +4385,15 @@ var wikiPageFileListCommand = new import_commander39.Command("list").description
|
|
|
4278
4385
|
|
|
4279
4386
|
// src/commands/wiki/page-file/upload.ts
|
|
4280
4387
|
var import_commander40 = require("commander");
|
|
4388
|
+
|
|
4389
|
+
// src/utils/wiki-snippet.ts
|
|
4390
|
+
function wikiInlineImageSnippet(wikiId, attachFileId, name) {
|
|
4391
|
+
return ``;
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
// src/commands/wiki/page-file/upload.ts
|
|
4281
4395
|
var wikiPageFileUploadCommand = new import_commander40.Command("upload").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uCCA8\uBD80\uD30C\uC77C \uC5C5\uB85C\uB4DC (multipart type \uC21C\uC11C \uAC15\uC81C, ADR-029)").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray Wiki URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C \uACBD\uB85C").argument("[arg2]", "page-id \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C \uACBD\uB85C").argument("[arg3]", "\uD30C\uC77C \uACBD\uB85C (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").option("--file <path>", "\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uACBD\uB85C (positional \uB300\uCCB4)").option("--type <type>", "\uD30C\uC77C \uD0C0\uC785: general | inline_image (\uAE30\uBCF8 general)", "general").action(async (arg1, arg2, arg3, opts) => {
|
|
4396
|
+
const globalOpts = wikiPageFileUploadCommand.optsWithGlobals();
|
|
4282
4397
|
const config = await getConfigOrThrow();
|
|
4283
4398
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4284
4399
|
const fileType = opts.type;
|
|
@@ -4335,18 +4450,35 @@ var wikiPageFileUploadCommand = new import_commander40.Command("upload").descrip
|
|
|
4335
4450
|
try {
|
|
4336
4451
|
const res = await client.uploadWikiPageFile(wikiId, pageId, filePath, fileType);
|
|
4337
4452
|
stopSpinner(true, "\uC5C5\uB85C\uB4DC \uC644\uB8CC");
|
|
4338
|
-
|
|
4453
|
+
if (globalOpts.json) {
|
|
4454
|
+
const payload = fileType === "inline_image" ? {
|
|
4455
|
+
...res.result,
|
|
4456
|
+
markdownSnippet: wikiInlineImageSnippet(
|
|
4457
|
+
wikiId,
|
|
4458
|
+
res.result.attachFileId,
|
|
4459
|
+
res.result.name
|
|
4460
|
+
)
|
|
4461
|
+
} : res.result;
|
|
4462
|
+
printJson(payload);
|
|
4463
|
+
} else if (globalOpts.quiet) {
|
|
4464
|
+
process.stdout.write(`${res.result.id}
|
|
4339
4465
|
`);
|
|
4340
|
-
|
|
4466
|
+
} else {
|
|
4467
|
+
process.stdout.write(`attachFileId: ${res.result.attachFileId}
|
|
4341
4468
|
`);
|
|
4342
|
-
|
|
4469
|
+
process.stdout.write(`name: ${res.result.name}
|
|
4343
4470
|
`);
|
|
4344
|
-
|
|
4471
|
+
process.stdout.write(`size: ${res.result.size}
|
|
4345
4472
|
`);
|
|
4346
|
-
|
|
4347
|
-
process.stdout.write("\n\uBCF8\uBB38 \uC0BD\uC785\uC6A9 markdown snippet (\uC9C1\uC811 wiki page edit \uC73C\uB85C \uBCF8\uBB38\uC5D0 \uBC15\uC73C\uC138\uC694):\n");
|
|
4348
|
-
process.stdout.write(` 
|
|
4473
|
+
process.stdout.write(`type: ${res.result.type}
|
|
4349
4474
|
`);
|
|
4475
|
+
if (fileType === "inline_image") {
|
|
4476
|
+
process.stdout.write("\n\uBCF8\uBB38 \uC0BD\uC785\uC6A9 markdown snippet (\uC9C1\uC811 wiki page edit \uC73C\uB85C \uBCF8\uBB38\uC5D0 \uBC15\uC73C\uC138\uC694):\n");
|
|
4477
|
+
process.stdout.write(
|
|
4478
|
+
` ${wikiInlineImageSnippet(wikiId, res.result.attachFileId, res.result.name)}
|
|
4479
|
+
`
|
|
4480
|
+
);
|
|
4481
|
+
}
|
|
4350
4482
|
}
|
|
4351
4483
|
} catch (e) {
|
|
4352
4484
|
stopSpinner(false);
|
|
@@ -4359,6 +4491,7 @@ var import_commander41 = require("commander");
|
|
|
4359
4491
|
var import_promises11 = require("fs/promises");
|
|
4360
4492
|
var import_node_path9 = require("path");
|
|
4361
4493
|
var wikiPageFileDownloadCommand = new import_commander41.Command("download").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray Wiki URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "page-id \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C ID (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").option("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (arg1, arg2, arg3, opts) => {
|
|
4494
|
+
const globalOpts = wikiPageFileDownloadCommand.optsWithGlobals();
|
|
4362
4495
|
const config = await getConfigOrThrow();
|
|
4363
4496
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4364
4497
|
let projectArg;
|
|
@@ -4407,11 +4540,11 @@ var wikiPageFileDownloadCommand = new import_commander41.Command("download").des
|
|
|
4407
4540
|
startSpinner("\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
|
|
4408
4541
|
try {
|
|
4409
4542
|
const { buffer, fileName } = await client.downloadWikiPageFile(wikiId, pageId, fileId);
|
|
4410
|
-
const
|
|
4543
|
+
const safeName = (0, import_node_path9.basename)(decodeURIComponent(fileName));
|
|
4544
|
+
const outputPath = (0, import_node_path9.join)(opts.output, safeName);
|
|
4411
4545
|
await (0, import_promises11.writeFile)(outputPath, Buffer.from(buffer));
|
|
4412
4546
|
stopSpinner(true, `\uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC: ${outputPath}`);
|
|
4413
|
-
|
|
4414
|
-
`);
|
|
4547
|
+
emitDownloadResult(globalOpts, { outputPath, fileName: safeName, size: buffer.byteLength });
|
|
4415
4548
|
} catch (e) {
|
|
4416
4549
|
stopSpinner(false);
|
|
4417
4550
|
throw e;
|
|
@@ -4423,6 +4556,7 @@ var import_commander42 = require("commander");
|
|
|
4423
4556
|
var import_promises12 = require("fs/promises");
|
|
4424
4557
|
var import_node_path10 = require("path");
|
|
4425
4558
|
var wikiPageFileDownloadAllCommand = new import_commander42.Command("download-all").description("\uC704\uD0A4 \uD398\uC774\uC9C0\uC758 \uBAA8\uB4E0 \uCCA8\uBD80\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC (general + inline image)").argument("[project]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (\uB610\uB294 \uCCAB \uC778\uC790\uC5D0 Dooray Wiki URL)").argument("[page-id]", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID (project\uC640 \uD568\uAED8 \uC0AC\uC6A9)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID (--project \uB3D9\uBC18 \uD544\uC694)").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").option("-o, --output <dir>", "\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC", ".").action(async (project, pageIdArg, opts) => {
|
|
4559
|
+
const globalOpts = wikiPageFileDownloadAllCommand.optsWithGlobals();
|
|
4426
4560
|
const config = await getConfigOrThrow();
|
|
4427
4561
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4428
4562
|
const { wikiId, pageId } = await resolveWikiPageInput(client, {
|
|
@@ -4443,40 +4577,44 @@ var wikiPageFileDownloadAllCommand = new import_commander42.Command("download-al
|
|
|
4443
4577
|
}
|
|
4444
4578
|
if (allFiles.length === 0) {
|
|
4445
4579
|
stopSpinner(true, "\uCCA8\uBD80\uD30C\uC77C \uC5C6\uC74C");
|
|
4446
|
-
|
|
4580
|
+
if (globalOpts.json) {
|
|
4581
|
+
printJson({ count: 0, succeeded: [], failed: [] });
|
|
4582
|
+
} else if (!globalOpts.quiet) {
|
|
4583
|
+
process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
4584
|
+
}
|
|
4447
4585
|
return;
|
|
4448
4586
|
}
|
|
4449
4587
|
stopSpinner(true, `${allFiles.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2DC\uC791`);
|
|
4450
4588
|
await (0, import_promises12.mkdir)(opts.output, { recursive: true });
|
|
4451
|
-
|
|
4452
|
-
const
|
|
4589
|
+
const succeeded = [];
|
|
4590
|
+
const failed = [];
|
|
4453
4591
|
for (const f of allFiles) {
|
|
4454
4592
|
try {
|
|
4455
4593
|
const { buffer, fileName } = await client.downloadWikiPageFile(wikiId, pageId, f.id);
|
|
4456
|
-
|
|
4457
|
-
|
|
4594
|
+
const safeName = (0, import_node_path10.basename)(decodeURIComponent(fileName));
|
|
4595
|
+
const outputPath = (0, import_node_path10.join)(opts.output, safeName);
|
|
4596
|
+
await (0, import_promises12.writeFile)(outputPath, Buffer.from(buffer));
|
|
4597
|
+
succeeded.push({ path: outputPath, fileName: safeName });
|
|
4598
|
+
if (!globalOpts.json && !globalOpts.quiet) {
|
|
4599
|
+
process.stdout.write(`\u2713 ${safeName}
|
|
4458
4600
|
`);
|
|
4459
|
-
|
|
4601
|
+
}
|
|
4460
4602
|
} catch (e) {
|
|
4461
|
-
|
|
4462
|
-
|
|
4603
|
+
failed.push({ fileId: f.id, error: e instanceof Error ? e.message : String(e) });
|
|
4604
|
+
if (!globalOpts.json) {
|
|
4605
|
+
process.stderr.write(`\u2717 ${f.name} (${f.id}): ${e instanceof Error ? e.message : String(e)}
|
|
4463
4606
|
`);
|
|
4607
|
+
}
|
|
4464
4608
|
}
|
|
4465
4609
|
}
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
`);
|
|
4469
|
-
if (failures.length > 0) {
|
|
4470
|
-
throw new DoorayCliError(
|
|
4471
|
-
`${failures.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (\uC131\uACF5: ${successCount}/${allFiles.length})`,
|
|
4472
|
-
EXIT_API_ERROR
|
|
4473
|
-
);
|
|
4474
|
-
}
|
|
4610
|
+
emitDownloadAllResult(globalOpts, { count: allFiles.length, succeeded, failed });
|
|
4611
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
4475
4612
|
});
|
|
4476
4613
|
|
|
4477
4614
|
// src/commands/wiki/page-file/delete.ts
|
|
4478
4615
|
var import_commander43 = require("commander");
|
|
4479
4616
|
var wikiPageFileDeleteCommand = new import_commander43.Command("delete").description("\uC704\uD0A4 \uD398\uC774\uC9C0 \uCCA8\uBD80\uD30C\uC77C \uC0AD\uC81C").argument("[arg1]", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC, Dooray Wiki URL, \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg2]", "page-id \uB610\uB294 (`--id`/`--url` \uBAA8\uB4DC\uC77C \uB54C) \uD30C\uC77C ID").argument("[arg3]", "\uD30C\uC77C ID (positional 3\uAC1C \uBAA8\uB4DC)").option("--id <pageId>", "\uC704\uD0A4 \uD398\uC774\uC9C0 ID").option("--url <url>", "Dooray Wiki URL").option("--project <code>", "\uD504\uB85C\uC81D\uD2B8 \uCF54\uB4DC (--id \uBAA8\uB4DC\uC5D0\uC11C wikiId \uD574\uC11D\uC6A9)").option("--file-id <fileId>", "\uD30C\uC77C ID (positional \uB300\uCCB4)").action(async (arg1, arg2, arg3, opts) => {
|
|
4617
|
+
const globalOpts = wikiPageFileDeleteCommand.optsWithGlobals();
|
|
4480
4618
|
const config = await getConfigOrThrow();
|
|
4481
4619
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4482
4620
|
let projectArg;
|
|
@@ -4526,8 +4664,7 @@ var wikiPageFileDeleteCommand = new import_commander43.Command("delete").descrip
|
|
|
4526
4664
|
try {
|
|
4527
4665
|
await client.deleteWikiPageFile(wikiId, pageId, fileId);
|
|
4528
4666
|
stopSpinner(true, "\uC0AD\uC81C \uC644\uB8CC");
|
|
4529
|
-
|
|
4530
|
-
`);
|
|
4667
|
+
emitDeleteResult(globalOpts, { fileId });
|
|
4531
4668
|
} catch (e) {
|
|
4532
4669
|
stopSpinner(false);
|
|
4533
4670
|
throw e;
|
|
@@ -5575,7 +5712,7 @@ function sanitizeArgv(argv) {
|
|
|
5575
5712
|
|
|
5576
5713
|
// src/index.ts
|
|
5577
5714
|
var program = new import_commander61.Command();
|
|
5578
|
-
program.name("dooray").description("Dooray REST API CLI").version("0.
|
|
5715
|
+
program.name("dooray").description("Dooray REST API CLI").version("0.12.0").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--quiet", "ID\uB9CC \uCD9C\uB825").option("--no-color", "\uC0C9\uC0C1 \uBE44\uD65C\uC131\uD654");
|
|
5579
5716
|
program.hook("preAction", () => {
|
|
5580
5717
|
const opts = program.opts();
|
|
5581
5718
|
if (opts.color === false || process.env.NO_COLOR) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bifos/dooray-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "CLI tool for Dooray project management — AI agent & terminal friendly",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dooray",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
],
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "https://github.com/jon890/dooray-cli.git"
|
|
14
|
+
"url": "git+https://github.com/jon890/dooray-cli.git"
|
|
15
15
|
},
|
|
16
16
|
"homepage": "https://github.com/jon890/dooray-cli#readme",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/jon890/dooray-cli/issues"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
|
-
"dooray": "
|
|
21
|
+
"dooray": "dist/index.js"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
@@ -47,8 +47,13 @@ dooray doctor # 설정 검증
|
|
|
47
47
|
|
|
48
48
|
> **공통 (post 하위 16개 명령)**: 아래 명령은 `<project> <number>` 외에도 `--id <postId>`, `--url <url>`, 또는 첫 인자에 Dooray URL 을 직접 받는다.
|
|
49
49
|
> `post get`/`edit`/`done`/`workflow`, `post comment list`/`add`/`edit`/`delete`, `post file list`/`upload`/`download`/`download-all`/`delete`, `post comment file list`/`upload`/`download`/`delete`.
|
|
50
|
-
>
|
|
51
|
-
>
|
|
50
|
+
>
|
|
51
|
+
> 지원 URL 형식 3종 (positional 첫 인자 / `--url` 공통):
|
|
52
|
+
> - `https://*.dooray.com/task/to/<postId>`
|
|
53
|
+
> - `https://*.dooray.com/task/<projectId>/<postId>` — 브라우저 주소창 복사본
|
|
54
|
+
> - `https://*.dooray.com/project/tasks/<postId>` — 프로젝트 업무 목록 → 업무 열기
|
|
55
|
+
>
|
|
56
|
+
> **사용자가 URL을 줬으면 그대로 첫 인자로 전달**하는 것이 가장 빠른 경로 (resolve 단계 단축).
|
|
52
57
|
|
|
53
58
|
| 의도 | 커맨드 |
|
|
54
59
|
|------|--------|
|
|
@@ -59,17 +64,17 @@ dooray doctor # 설정 검증
|
|
|
59
64
|
| 프로젝트 멤버 그룹 목록 | `dooray project groups <project>` (ID / Code) |
|
|
60
65
|
| 프로젝트 태그 목록 | `dooray project tags <project>` (ID / Color / Name / Group / Mandatory) |
|
|
61
66
|
| 프로젝트 템플릿 목록 | `dooray project templates <project>` (id / templateName) |
|
|
62
|
-
| 멤버 상세 (organizationMemberId) | `dooray member get <organizationMemberId>` (cache
|
|
67
|
+
| 멤버 상세 (organizationMemberId) | `dooray member get <organizationMemberId>` (cache 우회) |
|
|
63
68
|
| organization 전체 멤버 검색 | `dooray member search <keyword>` (이름 기본), `--email`(이메일 exact), `--user-code`(사번 like), `--user-code-exact`(사번 exact), `--page`/`--size` |
|
|
64
69
|
| 업무 목록 조회 | `dooray post list <project>` |
|
|
65
|
-
| 업무 검색 | `dooray post search <project> "<keyword>"` |
|
|
66
|
-
| 업무 상세 보기 | `dooray post get <project> <number>` |
|
|
70
|
+
| 업무 검색 | `dooray post search <project|projectId> "<keyword>"` — projectId (15+자리 numeric) 직접 입력 시 cache 우회 |
|
|
71
|
+
| 업무 상세 보기 | `dooray post get <project> <number>` (번호) / `dooray post get --id <postId>` (internal ID) |
|
|
67
72
|
| 업무 생성 | `dooray post create <project> --title "..." [--body "..." \| --body-file <path>]` (`--tag`/`--parent`/`--workflow`/`--milestone` 지원) |
|
|
68
|
-
| 템플릿 기반 업무 생성 | `dooray post create <project> --template <name\|id>` — body/users/tags 자동 채움 (사용자 옵션 우선 override
|
|
73
|
+
| 템플릿 기반 업무 생성 | `dooray post create <project> --template <name\|id>` — body/users/tags 자동 채움 (사용자 옵션 우선 override) |
|
|
69
74
|
| 업무 제목/본문 수정 | `dooray post edit <project> <number> --title "..." --body "..."` 또는 `--body-file <path>` |
|
|
70
75
|
| 업무 완료 처리 | `dooray post done <project> <number>` |
|
|
71
76
|
| 업무 워크플로우 변경 | `dooray post workflow <project> <number> <workflow>` |
|
|
72
|
-
| 댓글 조회 | `dooray post comment list <project> <number>` (`--sort`, `--reverse`, `--latest`, `--since`, `--from-author` 필터. table: Creator 자동 채움, `--json`: raw
|
|
77
|
+
| 댓글 조회 | `dooray post comment list <project> <number>` (`--sort`, `--reverse`, `--latest`, `--since`, `--from-author` 필터. table: Creator 자동 채움, `--json`: raw) |
|
|
73
78
|
| 최신 댓글 조회 | `dooray post comment latest <project> <number>` — 최신 댓글 1개 빠른 조회. `-n <N>`으로 N개 지정 |
|
|
74
79
|
| 단일 댓글 조회 | `dooray post comment get <project> <number> <comment-id>` — 본문·메타·attachments 직접 fetch. `--id`/`--url` + `--comment-id` 모드 지원 |
|
|
75
80
|
| 댓글 추가 | `dooray post comment add <project> <number> --body "..."` 또는 `--body-file <path>` |
|
|
@@ -83,7 +88,7 @@ dooray doctor # 설정 검증
|
|
|
83
88
|
| 위키 페이지 수정 (본문) | `dooray wiki page edit <project> <page-id> --body "..."` 또는 `--body-file ./new.md` |
|
|
84
89
|
| 위키 페이지 수정 (에디터) | `dooray wiki page edit <project> <page-id>` (플래그 없으면 $EDITOR 열림) |
|
|
85
90
|
| 위키 페이지 첨부 목록 | `dooray wiki page file list <project> <page-id>` (general + inline 합산, type 컬럼) |
|
|
86
|
-
| 위키 페이지 첨부 업로드 | `dooray wiki page file upload <project> <page-id> --file <path> [--type inline_image]` (multipart type
|
|
91
|
+
| 위키 페이지 첨부 업로드 | `dooray wiki page file upload <project> <page-id> --file <path> [--type inline_image]` (multipart `type` 필드를 `file` 앞에 전송) |
|
|
87
92
|
| 위키 페이지 첨부 다운로드 | `dooray wiki page file download <project> <page-id> --file-id <id> -o <dir>` |
|
|
88
93
|
| 위키 페이지 첨부 일괄 다운로드 | `dooray wiki page file download-all <project> <page-id> -o <dir>` (files + images 전부) |
|
|
89
94
|
| 위키 페이지 첨부 삭제 | `dooray wiki page file delete <project> <page-id> --file-id <id>` (confirm 없음) |
|
|
@@ -104,14 +109,15 @@ dooray doctor # 설정 검증
|
|
|
104
109
|
| 전체 첨부파일 다운로드 | `dooray post file download-all <project> <number>` |
|
|
105
110
|
| 첨부파일 업로드 | `dooray post file upload <project> <number> <file-path>` |
|
|
106
111
|
| 첨부파일 삭제 | `dooray post file delete <project> <number> <file-id>` |
|
|
112
|
+
| file 명령군 자동화 파싱 | `dooray post file <verb> ... --json` — `download` = `{outputPath,fileName,size}`, `download-all` = `{count,succeeded,failed}` (부분 실패 시 exit 1), `delete` = `{fileId,status}`, `upload` = `res.result` raw |
|
|
107
113
|
| 댓글 첨부 목록 | `dooray post comment file list <project> <number> <comment-id>` |
|
|
108
114
|
| 댓글 파일 업로드 | `dooray post comment file upload <project> <number> <comment-id> <path>` |
|
|
109
115
|
| 댓글 파일 다운로드 | `dooray post comment file download <project> <number> <comment-id> <file-id>` |
|
|
110
116
|
| 댓글 파일 삭제 | `dooray post comment file delete <project> <number> <comment-id> <file-id> --yes` |
|
|
111
|
-
| 참조자(cc) 멤버/그룹 추가 | `dooray post edit <project> <number> --cc-group <code>` — 기존 참조자 유지 + 그룹 추가 (dedupe
|
|
117
|
+
| 참조자(cc) 멤버/그룹 추가 | `dooray post edit <project> <number> --cc-group <code>` — 기존 참조자 유지 + 그룹 추가 (dedupe) |
|
|
112
118
|
| 참조자 전체 교체 | `dooray post edit <project> <number> --cc-clear --cc <name>` — 기존 참조자 비우고 신규 멤버만 |
|
|
113
119
|
| 신규 업무 + 그룹 cc | `dooray post create <project> --title "..." --cc-group <code>` — 생성 시 그룹 참조자 포함 |
|
|
114
|
-
| `--cc-group <code\|id>` / `--mention-group <code\|id>` | 그룹 매칭 — 15+자리 numeric → id 직접 / 그 외 → code matchByName (
|
|
120
|
+
| `--cc-group <code\|id>` / `--mention-group <code\|id>` | 그룹 매칭 — 15+자리 numeric → id 직접 / 그 외 → code matchByName (부분일치) |
|
|
115
121
|
| 상위 업무 설정/변경 | `dooray post edit <project> <number> --title "<원제목>" --parent <ref>` (`<ref>`: `<project>/<number>` 또는 raw postId. `--title` 필수, unset 미지원) |
|
|
116
122
|
| `dooray post edit --id <postId> --tag <name>` | 태그 추가 (반복, dedupe) |
|
|
117
123
|
| `dooray post edit --id <postId> --tag-clear --tag <name>` | 태그 전체 교체 |
|
|
@@ -186,7 +192,7 @@ dooray post comment add <project> 42 --body "진행 상황 업데이트: 80% 완
|
|
|
186
192
|
### 시나리오 — 댓글에 스크린샷 자동 첨부
|
|
187
193
|
|
|
188
194
|
스크립트가 스크린샷을 댓글에 삽입하거나, 에이전트가 결과 파일을 첨부 댓글로 보고할 때 사용.
|
|
189
|
-
Dooray REST API 가 댓글 전용 attachment endpoint 를 미지원하므로 내부적으로 post-level files API + 댓글 본문 PUT 합성으로
|
|
195
|
+
Dooray REST API 가 댓글 전용 attachment endpoint 를 미지원하므로 내부적으로 post-level files API + 댓글 본문 PUT 합성으로 동작한다.
|
|
190
196
|
|
|
191
197
|
```bash
|
|
192
198
|
# 1. 댓글을 먼저 만든다 (텍스트만, --json 으로 commentId 획득)
|
|
@@ -207,7 +213,17 @@ dooray wiki pages <project> --json
|
|
|
207
213
|
dooray wiki page get <project> <pageId> --json
|
|
208
214
|
```
|
|
209
215
|
|
|
210
|
-
###
|
|
216
|
+
### 첨부파일 일괄 다운로드 후 실패 분리
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# --json 으로 구조화 출력 → jq 로 성공/실패 분리
|
|
220
|
+
RESULT=$(dooray post file download-all <project> <number> -o ./ --json)
|
|
221
|
+
echo "$RESULT" | jq -r '.failed[] | "\(.fileId): \(.error)"' >&2
|
|
222
|
+
echo "$RESULT" | jq -r '.succeeded[].path'
|
|
223
|
+
# exit code 1 이 설정되어 있으면 실패 있는 상태
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 위키 페이지 첨부파일 — 스킬 파일 팀 공유
|
|
211
227
|
|
|
212
228
|
**스킬 파일 팀 공유**: 팀 위키에 스킬 파일 (예: `SKILL.md`) 을 `wiki page file upload` 로 첨부 → 팀원이 `wiki page file download-all` 로 일괄 받아 `~/.claude/skills/` 에 그대로 설치.
|
|
213
229
|
|
|
@@ -222,6 +238,30 @@ dooray wiki page file download-all <project> <page-id> -o ~/.claude/skills/my-sk
|
|
|
222
238
|
dooray wiki page file list <project> <page-id>
|
|
223
239
|
```
|
|
224
240
|
|
|
241
|
+
### 위키 페이지 인라인 이미지 업로드 후 본문 자동 삽입
|
|
242
|
+
|
|
243
|
+
`--type inline_image` 로 업로드 시 `--json` 응답에 `markdownSnippet` 필드가 포함됩니다.
|
|
244
|
+
jq 로 추출해 본문에 바로 삽입하는 자동화가 가능합니다.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# 1. 인라인 이미지 업로드 — --json 으로 markdownSnippet 추출
|
|
248
|
+
SNIPPET=$(dooray wiki page file upload <project> <page-id> \
|
|
249
|
+
--file ./diagram.png --type inline_image --json \
|
|
250
|
+
| jq -r '.markdownSnippet')
|
|
251
|
+
# SNIPPET = ""
|
|
252
|
+
|
|
253
|
+
# 2. 기존 본문 조회
|
|
254
|
+
CURRENT_BODY=$(dooray wiki page get <project> <page-id> --json | jq -r '.body.content')
|
|
255
|
+
|
|
256
|
+
# 3. snippet 을 본문 끝에 추가해 업데이트
|
|
257
|
+
NEW_BODY="${CURRENT_BODY}
|
|
258
|
+
|
|
259
|
+
${SNIPPET}"
|
|
260
|
+
dooray wiki page edit <project> <page-id> --body "$NEW_BODY"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**참고**: `general` 타입은 `markdownSnippet` 없음. `--quiet` 은 id 만 출력 (snippet 미포함).
|
|
264
|
+
|
|
225
265
|
### 위키 페이지 댓글 — 회의록 결정사항 자동 누적
|
|
226
266
|
|
|
227
267
|
**회의록 결정사항 자동 누적**: 회의록 위키 페이지에 자동화 봇이 `wiki page comment add` 로 결정사항을 댓글로 누적, `wiki page comment list --latest 20` 으로 최근 토론 흐름 추적.
|
|
@@ -262,25 +302,39 @@ dooray wiki page comment latest <project> <page-id>
|
|
|
262
302
|
|
|
263
303
|
## 커맨드 상세
|
|
264
304
|
|
|
265
|
-
### 업무 식별 방식 (post 하위 16개 명령
|
|
305
|
+
### 업무 식별 방식 (post 하위 16개 명령 공통)
|
|
266
306
|
|
|
267
307
|
아래 16개 명령은 4가지 입력을 모두 받는다:
|
|
268
308
|
`post get`/`edit`/`done`/`workflow`, `post comment list`/`add`/`edit`/`delete`, `post file list`/`upload`/`download`/`download-all`/`delete`, `post comment file list`/`upload`/`download`/`delete`.
|
|
269
309
|
|
|
270
310
|
```bash
|
|
271
|
-
# (1) 기존 positional — 가장 익숙한 형태
|
|
311
|
+
# (1) 기존 positional — 가장 익숙한 형태 (<number> 는 업무 번호 #N)
|
|
272
312
|
dooray post get <project> 42
|
|
273
313
|
|
|
274
314
|
# (2) Dooray URL을 첫 인자로 — 사용자 메시지에서 URL을 그대로 복사할 때 최적
|
|
315
|
+
# 지원 형식 3종 모두 동일하게 작동
|
|
275
316
|
dooray post get https://x.dooray.com/task/to/<postId>
|
|
317
|
+
dooray post get https://x.dooray.com/task/<projectId>/<postId>
|
|
318
|
+
dooray post get https://x.dooray.com/project/tasks/<postId>
|
|
276
319
|
|
|
277
|
-
# (3) --id <postId>
|
|
320
|
+
# (3) --id <postId> — post create 결과의 .id 를 그대로 전달
|
|
278
321
|
dooray post get --id <postId>
|
|
279
322
|
|
|
280
|
-
# (4) --url <url>
|
|
323
|
+
# (4) --url <url> — URL 형식 3종 모두 지원
|
|
281
324
|
dooray post get --url https://x.dooray.com/task/to/<postId>
|
|
282
325
|
```
|
|
283
326
|
|
|
327
|
+
> ⚠️ **`post create` 결과 `.id` 는 internal postId (19자리 숫자)입니다.**
|
|
328
|
+
> 이 숫자를 `<project> <업무번호>` 의 번호 자리에 넣으면 안내 에러가 발생합니다.
|
|
329
|
+
> 후속 조회·수정·댓글은 반드시 **`--id <postId>`** 를 사용하세요.
|
|
330
|
+
>
|
|
331
|
+
> ```bash
|
|
332
|
+
> POST_ID=$(dooray post create <project> --title "..." --json | jq -r '.id')
|
|
333
|
+
> dooray post get --id "$POST_ID" # ✅ --id 사용
|
|
334
|
+
> dooray post comment add --id "$POST_ID" --body "댓글" # ✅ --id 사용
|
|
335
|
+
> # dooray post get <project> "$POST_ID" # ❌ 안내 에러 발생
|
|
336
|
+
> ```
|
|
337
|
+
|
|
284
338
|
**우선순위 / 충돌 규칙**: `--id`+`--url` 동시 지정 → 에러.
|
|
285
339
|
`--id`/`--url`+positional 동시 지정 → 에러.
|
|
286
340
|
URL/`--id`/`--url` 모드는 standalone API(`getPost(postId)`)로 resolve 단계를 단축.
|
|
@@ -339,7 +393,7 @@ dooray post edit <project> <number> --body "새 본문"
|
|
|
339
393
|
dooray post edit <project> <number> --title "새 제목" --body-file ./updated.md
|
|
340
394
|
```
|
|
341
395
|
|
|
342
|
-
### 참조자(cc) / 담당자(to) 변경 — 멤버 · 그룹
|
|
396
|
+
### 참조자(cc) / 담당자(to) 변경 — 멤버 · 그룹
|
|
343
397
|
|
|
344
398
|
```bash
|
|
345
399
|
# 기존 참조자 유지 + 그룹 추가 (dedupe: organizationMemberId / projectMemberGroupId)
|
|
@@ -361,7 +415,7 @@ dooray post edit --id "$POST_ID" --cc-group qa-team --dry-run --json \
|
|
|
361
415
|
|
|
362
416
|
> interactive (`$EDITOR`) 모드에서는 위 옵션이 무시되고 stderr 경고가 출력됩니다.
|
|
363
417
|
|
|
364
|
-
## 동명이인 우회 — 이메일 / memberId 직접
|
|
418
|
+
## 동명이인 우회 — 이메일 / memberId 직접
|
|
365
419
|
|
|
366
420
|
이름이 동일한 멤버가 여러 명이라 `--cc 홍길동` 이 모호로 실패할 때:
|
|
367
421
|
|
|
@@ -379,7 +433,7 @@ dooray post edit --id "$POST_ID" --cc "$MEMBER_ID"
|
|
|
379
433
|
- 이메일 정규형 — searchMembers exact
|
|
380
434
|
- 그 외 — 이름 부분일치
|
|
381
435
|
|
|
382
|
-
## 신규 업무 생성 후 그룹 cc 첨부
|
|
436
|
+
## 신규 업무 생성 후 그룹 cc 첨부
|
|
383
437
|
|
|
384
438
|
자동화 패턴:
|
|
385
439
|
1. audit 리포트 분석
|
|
@@ -400,7 +454,7 @@ dooray post edit --id "$POST_ID" --cc-group qa-team
|
|
|
400
454
|
|
|
401
455
|
---
|
|
402
456
|
|
|
403
|
-
## 자식 업무 먼저 → 후속 부모 지정
|
|
457
|
+
## 자식 업무 먼저 → 후속 부모 지정
|
|
404
458
|
|
|
405
459
|
```bash
|
|
406
460
|
# 1. 자식 업무 생성 (parent 모르고)
|
|
@@ -414,7 +468,7 @@ dooray post edit --id "$CHILD_ID" --title "subtask A" --parent <project>/<parent
|
|
|
414
468
|
|
|
415
469
|
---
|
|
416
470
|
|
|
417
|
-
## 태그 사후 분류 자동화
|
|
471
|
+
## 태그 사후 분류 자동화
|
|
418
472
|
|
|
419
473
|
분류 분석 결과를 받아 태그를 재분류하는 자동화는 단독 호출 패턴이 효율적:
|
|
420
474
|
|
|
@@ -426,7 +480,7 @@ dooray post edit --id "$POST_ID" --tag "분류: $CATEGORY"
|
|
|
426
480
|
```
|
|
427
481
|
|
|
428
482
|
태그만 변경하는 시나리오에서 `--title` / `--body` 강제 없음.
|
|
429
|
-
mandatory 그룹은 친절한 에러 메시지로
|
|
483
|
+
mandatory 그룹은 친절한 에러 메시지로 안내.
|
|
430
484
|
|
|
431
485
|
---
|
|
432
486
|
|
|
@@ -452,7 +506,7 @@ dooray post comment latest <project> <number>
|
|
|
452
506
|
|
|
453
507
|
---
|
|
454
508
|
|
|
455
|
-
## 그룹 멘션 / cc 시 AI agent 동선
|
|
509
|
+
## 그룹 멘션 / cc 시 AI agent 동선
|
|
456
510
|
|
|
457
511
|
자연어 그룹명을 사용자가 지칭했을 때 AI agent 의 의사결정 순서:
|
|
458
512
|
|
|
@@ -470,7 +524,7 @@ dooray post comment latest <project> <number>
|
|
|
470
524
|
AI agent 가 자연어 의도와 가장 가까운 code 선택 후 재시도.
|
|
471
525
|
|
|
472
526
|
3. **모든 컬럼이 빈값일 때 (response shape 이상) 회피**
|
|
473
|
-
-
|
|
527
|
+
- 최근 수정 이후 거의 발생 안 함 (`fetchAllMemberGroups` 가 nested array 정규화)
|
|
474
528
|
- 만약 발생 시: 사용자에게 그룹 id (UI 의 그룹 URL 에서 19자리 numeric) 확인 요청
|
|
475
529
|
- `--cc-group <id>` / `--mention-group <id>` 직접 입력
|
|
476
530
|
- 또는 그룹 멤버를 개별 `--cc <member>` / `--mention <member>` 로 지정
|
|
@@ -554,7 +608,7 @@ dooray feedback --last --title "에러 제목" --body "추가 설명" --dry-run
|
|
|
554
608
|
dooray feedback --last --title "에러 제목" --body "추가 설명" # 실제 등록
|
|
555
609
|
```
|
|
556
610
|
|
|
557
|
-
> **참고**: `--last` 모드는 `trackLastRun: true` (
|
|
611
|
+
> **참고**: `--last` 모드는 `trackLastRun: true` (opt-in)가 설정된 경우에만 직전 실패 명령이 자동 기록됨.
|
|
558
612
|
> argv는 시크릿 패턴(`--api-key`/`--token`/`Authorization`) 마스킹 후 저장.
|
|
559
613
|
|
|
560
614
|
## 에러 핸들링
|
|
@@ -571,7 +625,7 @@ CLI 에러 발생 시 복구 방법:
|
|
|
571
625
|
|
|
572
626
|
---
|
|
573
627
|
|
|
574
|
-
## 정형 task 자동화
|
|
628
|
+
## 정형 task 자동화
|
|
575
629
|
|
|
576
630
|
매주 같은 형식의 task 를 만드는 자동화는 템플릿 + override 패턴이 효율적:
|
|
577
631
|
|
|
@@ -587,6 +641,24 @@ POST_ID=$(dooray post create <project> \
|
|
|
587
641
|
템플릿 본문의 `${year}` / `${month}` 등 매크로는 Dooray 가 자동 치환 (`interpolation=true` 기본).
|
|
588
642
|
사용자 정의 변수는 미지원 — 필요 시 client 측 string replace 로 처리.
|
|
589
643
|
|
|
644
|
+
## projectId 직접 입력 시나리오
|
|
645
|
+
|
|
646
|
+
AI agent 가 `member=me` 응답에 없는 프로젝트의 업무를 다뤄야 할 때:
|
|
647
|
+
|
|
648
|
+
1. **사용자가 projectId (15+자리 numeric) 를 줬으면 그대로 명령에 사용**:
|
|
649
|
+
```bash
|
|
650
|
+
dooray post search 1234567890123456789 "keyword"
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
2. **사용자가 코드만 줬고 cache 매칭 실패 (member 아닌 프로젝트)**:
|
|
654
|
+
- 에러 메시지의 안내 확인
|
|
655
|
+
- 사용자에게 "프로젝트 ID 가 필요합니다 — Dooray UI 의 프로젝트 URL 에서 확인 가능" 요청
|
|
656
|
+
- 또는 `dooray project list --type private` 로 private 캐시 갱신 시도
|
|
657
|
+
|
|
658
|
+
3. **권한 없는 projectId 입력 시**: resolver 통과 후 후속 API 4xx 발생 — 에러 메시지에서 권한 부재 확인 후 사용자에게 보고
|
|
659
|
+
|
|
660
|
+
권한 검증이 resolver 단보다 한 단계 지연되는 trade-off — AI 친화적 자동화 우선.
|
|
661
|
+
|
|
590
662
|
## 캐시
|
|
591
663
|
|
|
592
664
|
프로젝트, 멤버, 워크플로우, 위키 정보는 `~/.dooray/cache/`에 캐시된다.
|