@bifos/dooray-cli 0.10.0 → 0.11.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 +67 -0
- package/dist/index.js +142 -51
- package/package.json +1 -1
- package/skills/dooray-cli/SKILL.md +60 -1
package/README.md
CHANGED
|
@@ -91,6 +91,22 @@ dooray post get <project> 42 --json # JSON 출력
|
|
|
91
91
|
대상: `post get`/`edit`/`done`/`workflow`, `post comment list`/`add`/`edit`/`delete`, `post file list`/`upload`/`download`/`download-all`/`delete`.
|
|
92
92
|
AI 에이전트는 사용자 메시지의 Dooray URL을 그대로 첫 인자로 전달하면 가장 빠르다 (ADR-020).
|
|
93
93
|
|
|
94
|
+
**projectId 직접 입력** (Issue #78, ADR-030):
|
|
95
|
+
|
|
96
|
+
`member=me` 응답에 없는 프로젝트 (다른 팀 / 권한만 있는 프로젝트) 도 projectId (15+자리 numeric) 를 직접 입력하면 자동으로 cache 우회.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# 코드 매칭 (기존)
|
|
100
|
+
dooray post search <project> "keyword"
|
|
101
|
+
|
|
102
|
+
# projectId 직접 입력 — member 아닌 프로젝트도 자동화 가능
|
|
103
|
+
dooray post search 1234567890123456789 "keyword"
|
|
104
|
+
dooray post list 1234567890123456789
|
|
105
|
+
dooray member list 1234567890123456789
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
권한 검증은 후속 API 호출 시점 — 권한 없으면 4xx.
|
|
109
|
+
|
|
94
110
|
### 업무 생성
|
|
95
111
|
|
|
96
112
|
```bash
|
|
@@ -209,6 +225,22 @@ interactive ($EDITOR) 모드에서는 위 6개 옵션이 무시되고 stderr 경
|
|
|
209
225
|
`post edit --dry-run --json` 사용 시 출력에 `users: { to, cc }` 가 포함되어 API 호출 없이 변경 결과 미리보기 가능.
|
|
210
226
|
`post create --dry-run` 은 본문만 출력하며 `users` 는 포함하지 않음.
|
|
211
227
|
|
|
228
|
+
**그룹 cc / mention 사용 예 (Issue #76 fix)**:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# code 부분일치
|
|
232
|
+
dooray post create <project> ... --cc-group "개발"
|
|
233
|
+
|
|
234
|
+
# code 정확 일치
|
|
235
|
+
dooray post create <project> ... --mention-group "all"
|
|
236
|
+
|
|
237
|
+
# 19자리 id 직접 입력 (response shape robustness 또는 code 누락 그룹 회피)
|
|
238
|
+
dooray post create <project> ... --cc-group "<19자리 group id>"
|
|
239
|
+
|
|
240
|
+
# 후보 탐색
|
|
241
|
+
dooray project groups <project>
|
|
242
|
+
```
|
|
243
|
+
|
|
212
244
|
```bash
|
|
213
245
|
dooray post edit <project> <post-number> --cc-group dev-team --dry-run --json | jq '.users.cc'
|
|
214
246
|
# 출력 예: [{ "type": "group", "group": { "projectMemberGroupId": "...", "members": [] } }, ...]
|
|
@@ -367,6 +399,22 @@ dooray wiki page file list "https://<tenant>.dooray.com/wiki/<wikiId>/<pageId>"
|
|
|
367
399
|
dooray wiki page file upload --id <pageId> --project <project> --file ./README.md
|
|
368
400
|
```
|
|
369
401
|
|
|
402
|
+
`--json` 옵션으로 자동화 파이프라인에서 동일 parse 코드를 사용할 수 있습니다 (ADR-031):
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
# download — { outputPath, fileName, size }
|
|
406
|
+
dooray wiki page file download <project> <page-id> --file-id <id> -o ./ --json
|
|
407
|
+
|
|
408
|
+
# download-all — { count, succeeded, failed } (부분 실패 시 exit 1)
|
|
409
|
+
dooray wiki page file download-all <project> <page-id> -o ./ --json | jq '.failed'
|
|
410
|
+
|
|
411
|
+
# delete — { fileId, status: "deleted" }
|
|
412
|
+
dooray wiki page file delete <project> <page-id> --file-id <id> --json
|
|
413
|
+
|
|
414
|
+
# upload — res.result raw (--quiet 는 id 만)
|
|
415
|
+
dooray wiki page file upload <project> <page-id> --file ./report.pdf --json
|
|
416
|
+
```
|
|
417
|
+
|
|
370
418
|
**주의**:
|
|
371
419
|
- `upload` 시 multipart 필드 순서 (`type` → `file`) 가 중요.
|
|
372
420
|
클라이언트가 자동으로 강제 (ADR-029 참조)
|
|
@@ -458,6 +506,25 @@ dooray post file upload <project> <number> ./report.pdf
|
|
|
458
506
|
dooray post file delete <project> <number> <file-id>
|
|
459
507
|
```
|
|
460
508
|
|
|
509
|
+
자동화 스크립트에서 `--json` 옵션으로 구조화된 출력을 파이프로 가공할 수 있습니다 (ADR-031):
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
# download — { outputPath, fileName, size }
|
|
513
|
+
dooray post file download <project> <number> --file-id <id> -o ./ --json
|
|
514
|
+
# 출력: {"outputPath": "./<fileName>", "fileName": "...", "size": 12345}
|
|
515
|
+
|
|
516
|
+
# download-all — { count, succeeded, failed } (부분 실패 시 exit 1)
|
|
517
|
+
dooray post file download-all <project> <number> -o ./ --json | jq '.failed'
|
|
518
|
+
# 출력: [] 또는 [{"fileId": "...", "error": "..."}]
|
|
519
|
+
|
|
520
|
+
# delete — { fileId, status: "deleted" }
|
|
521
|
+
dooray post file delete <project> <number> --file-id <id> --json
|
|
522
|
+
# 출력: {"fileId": "...", "status": "deleted"}
|
|
523
|
+
|
|
524
|
+
# upload — res.result raw (--quiet 는 id 만)
|
|
525
|
+
dooray post file upload <project> <number> ./report.pdf --json
|
|
526
|
+
```
|
|
527
|
+
|
|
461
528
|
URL/`--id`/`--url` 모드에서는 sub-id를 옵션으로 전달:
|
|
462
529
|
```bash
|
|
463
530
|
dooray post file download --url <url> --file-id <fileId> -o ./downloads
|
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
|
}
|
|
@@ -1549,13 +1554,15 @@ var projectWorkflowsCommand = new import_commander7.Command("workflows").descrip
|
|
|
1549
1554
|
var import_commander8 = require("commander");
|
|
1550
1555
|
|
|
1551
1556
|
// src/resolvers/member-group.ts
|
|
1557
|
+
var GROUP_ID_RE = /^\d{15,}$/;
|
|
1552
1558
|
async function fetchAllMemberGroups(client, projectId) {
|
|
1553
1559
|
const all = [];
|
|
1554
1560
|
let page = 0;
|
|
1555
1561
|
const size = RESOLVER_FETCH_PAGE_SIZE;
|
|
1556
1562
|
while (true) {
|
|
1557
1563
|
const res = await client.getProjectMemberGroups(projectId, { page, size });
|
|
1558
|
-
|
|
1564
|
+
const groups = res.result.flat();
|
|
1565
|
+
for (const g of groups) {
|
|
1559
1566
|
all.push({ id: g.id, code: g.code });
|
|
1560
1567
|
}
|
|
1561
1568
|
const total = res.totalCount ?? all.length;
|
|
@@ -1576,17 +1583,30 @@ function hasValidCode(g) {
|
|
|
1576
1583
|
}
|
|
1577
1584
|
async function resolveMemberGroup(client, projectId, input2) {
|
|
1578
1585
|
const groups = await ensureMemberGroups(client, projectId);
|
|
1586
|
+
if (GROUP_ID_RE.test(input2)) {
|
|
1587
|
+
const found = groups.find((g) => g.id === input2);
|
|
1588
|
+
if (found) {
|
|
1589
|
+
return { id: found.id, code: found.code ?? "" };
|
|
1590
|
+
}
|
|
1591
|
+
throw new DoorayCliError(
|
|
1592
|
+
`\uADF8\uB8F9 id \uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: "${input2}"
|
|
1593
|
+
\uC804\uCCB4 \uBAA9\uB85D\uC740 \`dooray project groups <project>\` \uB85C \uD655\uC778\uD558\uC138\uC694.`,
|
|
1594
|
+
EXIT_PARAM_ERROR
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1579
1597
|
const valid = groups.filter(hasValidCode);
|
|
1580
1598
|
const skipped = groups.length - valid.length;
|
|
1581
1599
|
if (skipped > 0) {
|
|
1582
1600
|
process.stderr.write(
|
|
1583
|
-
|
|
1601
|
+
// ADR 번호 정정: ADR-026 → ADR-028
|
|
1602
|
+
`\u26A0 ${skipped}\uAC1C \uADF8\uB8F9\uC5D0 code \uAC00 \uC5C6\uC5B4 \uB9E4\uCE6D\uC5D0\uC11C \uC81C\uC678\uD588\uC2B5\uB2C8\uB2E4 (ADR-028).
|
|
1603
|
+
id \uC9C1\uC811 \uC785\uB825 (15+\uC790\uB9AC numeric) \uB610\uB294 UI \uC218\uB3D9 cc / \`--cc <member>\` \uC6B0\uD68C \uAC00\uB2A5.
|
|
1584
1604
|
`
|
|
1585
1605
|
);
|
|
1586
1606
|
}
|
|
1587
1607
|
const adapter = valid.map((g) => ({ name: g.code, id: g.id, code: g.code }));
|
|
1588
1608
|
const match = matchByName(adapter, input2, "\uADF8\uB8F9", (g) => `${g.code} (${g.id})`, {
|
|
1589
|
-
helpHint: "dooray project groups <project
|
|
1609
|
+
helpHint: "\uC804\uCCB4 \uBAA9\uB85D: `dooray project groups <project>` / id \uC9C1\uC811 \uC785\uB825\uB3C4 \uAC00\uB2A5 (15+\uC790\uB9AC numeric \u2014 code \uB204\uB77D \uADF8\uB8F9\uB3C4 \uB9E4\uCE6D, ADR-028)"
|
|
1590
1610
|
});
|
|
1591
1611
|
return { id: match.id, code: match.code };
|
|
1592
1612
|
}
|
|
@@ -3752,7 +3772,49 @@ var fileListCommand = new import_commander29.Command("list").description("\uC5C5
|
|
|
3752
3772
|
var import_commander30 = require("commander");
|
|
3753
3773
|
var import_promises9 = require("fs/promises");
|
|
3754
3774
|
var import_node_path6 = require("path");
|
|
3775
|
+
|
|
3776
|
+
// src/formatters/file-output.ts
|
|
3777
|
+
function emitDownloadResult(globalOpts, result) {
|
|
3778
|
+
if (globalOpts.json) {
|
|
3779
|
+
printJson(result);
|
|
3780
|
+
} else {
|
|
3781
|
+
process.stdout.write(`${result.outputPath}
|
|
3782
|
+
`);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
function emitDownloadAllResult(globalOpts, data) {
|
|
3786
|
+
if (globalOpts.json) {
|
|
3787
|
+
printJson(data);
|
|
3788
|
+
} else if (globalOpts.quiet) {
|
|
3789
|
+
for (const s of data.succeeded) {
|
|
3790
|
+
process.stdout.write(`${s.path}
|
|
3791
|
+
`);
|
|
3792
|
+
}
|
|
3793
|
+
if (data.failed.length > 0) {
|
|
3794
|
+
process.stderr.write(`${data.failed.length} failed
|
|
3795
|
+
`);
|
|
3796
|
+
}
|
|
3797
|
+
} else {
|
|
3798
|
+
process.stdout.write(`
|
|
3799
|
+
\uC644\uB8CC: ${data.succeeded.length}/${data.count}
|
|
3800
|
+
`);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
function emitDeleteResult(globalOpts, result) {
|
|
3804
|
+
if (globalOpts.json) {
|
|
3805
|
+
printJson({ fileId: result.fileId, status: "deleted" });
|
|
3806
|
+
} else if (globalOpts.quiet) {
|
|
3807
|
+
process.stdout.write(`${result.fileId}
|
|
3808
|
+
`);
|
|
3809
|
+
} else {
|
|
3810
|
+
process.stdout.write(`\uD30C\uC77C(${result.fileId})\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
|
|
3811
|
+
`);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
// src/commands/post/file/download.ts
|
|
3755
3816
|
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) => {
|
|
3817
|
+
const globalOpts = fileDownloadCommand.optsWithGlobals();
|
|
3756
3818
|
const config = await getConfigOrThrow();
|
|
3757
3819
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
3758
3820
|
let projectArg;
|
|
@@ -3799,11 +3861,11 @@ var fileDownloadCommand = new import_commander30.Command("download").description
|
|
|
3799
3861
|
urlOpt: opts.url
|
|
3800
3862
|
});
|
|
3801
3863
|
const { buffer, fileName } = await client.downloadPostFile(projectId, postId, fileId);
|
|
3802
|
-
const
|
|
3864
|
+
const safeName = (0, import_node_path6.basename)(decodeURIComponent(fileName));
|
|
3865
|
+
const outputPath = (0, import_node_path6.join)(opts.output, safeName);
|
|
3803
3866
|
await (0, import_promises9.writeFile)(outputPath, Buffer.from(buffer));
|
|
3804
3867
|
stopSpinner(true, "\uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC");
|
|
3805
|
-
|
|
3806
|
-
`);
|
|
3868
|
+
emitDownloadResult(globalOpts, { outputPath, fileName: safeName, size: buffer.byteLength });
|
|
3807
3869
|
});
|
|
3808
3870
|
|
|
3809
3871
|
// src/commands/post/file/download-all.ts
|
|
@@ -3811,9 +3873,10 @@ var import_commander31 = require("commander");
|
|
|
3811
3873
|
var import_promises10 = require("fs/promises");
|
|
3812
3874
|
var import_node_path7 = require("path");
|
|
3813
3875
|
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) => {
|
|
3876
|
+
const globalOpts = fileDownloadAllCommand.optsWithGlobals();
|
|
3814
3877
|
const config = await getConfigOrThrow();
|
|
3815
3878
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
3816
|
-
|
|
3879
|
+
startSpinner("\uCCA8\uBD80\uD30C\uC77C \uBAA9\uB85D \uC870\uD68C \uC911...");
|
|
3817
3880
|
const { projectId, postId } = await resolvePostInput(client, {
|
|
3818
3881
|
projectArg: project,
|
|
3819
3882
|
postNumberArg: postNumberStr,
|
|
@@ -3823,23 +3886,38 @@ var fileDownloadAllCommand = new import_commander31.Command("download-all").desc
|
|
|
3823
3886
|
const res = await client.getPostFiles(projectId, postId);
|
|
3824
3887
|
if (res.result.length === 0) {
|
|
3825
3888
|
stopSpinner(true, "\uCCA8\uBD80\uD30C\uC77C \uC5C6\uC74C");
|
|
3826
|
-
|
|
3889
|
+
if (globalOpts.json) {
|
|
3890
|
+
printJson({ count: 0, succeeded: [], failed: [] });
|
|
3891
|
+
} else if (!globalOpts.quiet) {
|
|
3892
|
+
process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
3893
|
+
}
|
|
3827
3894
|
return;
|
|
3828
3895
|
}
|
|
3896
|
+
stopSpinner(true, `${res.result.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2DC\uC791`);
|
|
3829
3897
|
await (0, import_promises10.mkdir)(opts.output, { recursive: true });
|
|
3830
|
-
const
|
|
3898
|
+
const succeeded = [];
|
|
3899
|
+
const failed = [];
|
|
3831
3900
|
for (const file of res.result) {
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3901
|
+
try {
|
|
3902
|
+
const { buffer, fileName } = await client.downloadPostFile(projectId, postId, file.id);
|
|
3903
|
+
const safeName = (0, import_node_path7.basename)(decodeURIComponent(fileName));
|
|
3904
|
+
const outputPath = (0, import_node_path7.join)(opts.output, safeName);
|
|
3905
|
+
await (0, import_promises10.writeFile)(outputPath, Buffer.from(buffer));
|
|
3906
|
+
succeeded.push({ path: outputPath, fileName: safeName });
|
|
3907
|
+
if (!globalOpts.json && !globalOpts.quiet) {
|
|
3908
|
+
process.stdout.write(`\u2713 ${safeName}
|
|
3909
|
+
`);
|
|
3910
|
+
}
|
|
3911
|
+
} catch (e) {
|
|
3912
|
+
failed.push({ fileId: file.id, error: e instanceof Error ? e.message : String(e) });
|
|
3913
|
+
if (!globalOpts.json) {
|
|
3914
|
+
process.stderr.write(`\u2717 ${file.name} (${file.id}): ${e instanceof Error ? e.message : String(e)}
|
|
3841
3915
|
`);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3842
3918
|
}
|
|
3919
|
+
emitDownloadAllResult(globalOpts, { count: res.result.length, succeeded, failed });
|
|
3920
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
3843
3921
|
});
|
|
3844
3922
|
|
|
3845
3923
|
// src/commands/post/file/upload.ts
|
|
@@ -3908,6 +3986,7 @@ var fileUploadCommand = new import_commander32.Command("upload").description("\u
|
|
|
3908
3986
|
// src/commands/post/file/delete.ts
|
|
3909
3987
|
var import_commander33 = require("commander");
|
|
3910
3988
|
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) => {
|
|
3989
|
+
const globalOpts = fileDeleteCommand.optsWithGlobals();
|
|
3911
3990
|
const config = await getConfigOrThrow();
|
|
3912
3991
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
3913
3992
|
let projectArg;
|
|
@@ -3955,8 +4034,7 @@ var fileDeleteCommand = new import_commander33.Command("delete").description("\u
|
|
|
3955
4034
|
});
|
|
3956
4035
|
await client.deletePostFile(projectId, postId, fileId);
|
|
3957
4036
|
stopSpinner(true, "\uC0AD\uC81C \uC644\uB8CC");
|
|
3958
|
-
|
|
3959
|
-
`);
|
|
4037
|
+
emitDeleteResult(globalOpts, { fileId });
|
|
3960
4038
|
});
|
|
3961
4039
|
|
|
3962
4040
|
// src/commands/wiki/list.ts
|
|
@@ -4264,6 +4342,7 @@ var wikiPageFileListCommand = new import_commander39.Command("list").description
|
|
|
4264
4342
|
// src/commands/wiki/page-file/upload.ts
|
|
4265
4343
|
var import_commander40 = require("commander");
|
|
4266
4344
|
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) => {
|
|
4345
|
+
const globalOpts = wikiPageFileUploadCommand.optsWithGlobals();
|
|
4267
4346
|
const config = await getConfigOrThrow();
|
|
4268
4347
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4269
4348
|
const fileType = opts.type;
|
|
@@ -4320,18 +4399,25 @@ var wikiPageFileUploadCommand = new import_commander40.Command("upload").descrip
|
|
|
4320
4399
|
try {
|
|
4321
4400
|
const res = await client.uploadWikiPageFile(wikiId, pageId, filePath, fileType);
|
|
4322
4401
|
stopSpinner(true, "\uC5C5\uB85C\uB4DC \uC644\uB8CC");
|
|
4323
|
-
|
|
4402
|
+
if (globalOpts.json) {
|
|
4403
|
+
printJson(res.result);
|
|
4404
|
+
} else if (globalOpts.quiet) {
|
|
4405
|
+
process.stdout.write(`${res.result.id}
|
|
4324
4406
|
`);
|
|
4325
|
-
|
|
4407
|
+
} else {
|
|
4408
|
+
process.stdout.write(`attachFileId: ${res.result.attachFileId}
|
|
4326
4409
|
`);
|
|
4327
|
-
|
|
4410
|
+
process.stdout.write(`name: ${res.result.name}
|
|
4328
4411
|
`);
|
|
4329
|
-
|
|
4412
|
+
process.stdout.write(`size: ${res.result.size}
|
|
4330
4413
|
`);
|
|
4331
|
-
|
|
4332
|
-
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");
|
|
4333
|
-
process.stdout.write(` 
|
|
4414
|
+
process.stdout.write(`type: ${res.result.type}
|
|
4334
4415
|
`);
|
|
4416
|
+
if (fileType === "inline_image") {
|
|
4417
|
+
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");
|
|
4418
|
+
process.stdout.write(` 
|
|
4419
|
+
`);
|
|
4420
|
+
}
|
|
4335
4421
|
}
|
|
4336
4422
|
} catch (e) {
|
|
4337
4423
|
stopSpinner(false);
|
|
@@ -4344,6 +4430,7 @@ var import_commander41 = require("commander");
|
|
|
4344
4430
|
var import_promises11 = require("fs/promises");
|
|
4345
4431
|
var import_node_path9 = require("path");
|
|
4346
4432
|
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) => {
|
|
4433
|
+
const globalOpts = wikiPageFileDownloadCommand.optsWithGlobals();
|
|
4347
4434
|
const config = await getConfigOrThrow();
|
|
4348
4435
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4349
4436
|
let projectArg;
|
|
@@ -4392,11 +4479,11 @@ var wikiPageFileDownloadCommand = new import_commander41.Command("download").des
|
|
|
4392
4479
|
startSpinner("\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC911...");
|
|
4393
4480
|
try {
|
|
4394
4481
|
const { buffer, fileName } = await client.downloadWikiPageFile(wikiId, pageId, fileId);
|
|
4395
|
-
const
|
|
4482
|
+
const safeName = (0, import_node_path9.basename)(decodeURIComponent(fileName));
|
|
4483
|
+
const outputPath = (0, import_node_path9.join)(opts.output, safeName);
|
|
4396
4484
|
await (0, import_promises11.writeFile)(outputPath, Buffer.from(buffer));
|
|
4397
4485
|
stopSpinner(true, `\uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC: ${outputPath}`);
|
|
4398
|
-
|
|
4399
|
-
`);
|
|
4486
|
+
emitDownloadResult(globalOpts, { outputPath, fileName: safeName, size: buffer.byteLength });
|
|
4400
4487
|
} catch (e) {
|
|
4401
4488
|
stopSpinner(false);
|
|
4402
4489
|
throw e;
|
|
@@ -4408,6 +4495,7 @@ var import_commander42 = require("commander");
|
|
|
4408
4495
|
var import_promises12 = require("fs/promises");
|
|
4409
4496
|
var import_node_path10 = require("path");
|
|
4410
4497
|
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) => {
|
|
4498
|
+
const globalOpts = wikiPageFileDownloadAllCommand.optsWithGlobals();
|
|
4411
4499
|
const config = await getConfigOrThrow();
|
|
4412
4500
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4413
4501
|
const { wikiId, pageId } = await resolveWikiPageInput(client, {
|
|
@@ -4428,40 +4516,44 @@ var wikiPageFileDownloadAllCommand = new import_commander42.Command("download-al
|
|
|
4428
4516
|
}
|
|
4429
4517
|
if (allFiles.length === 0) {
|
|
4430
4518
|
stopSpinner(true, "\uCCA8\uBD80\uD30C\uC77C \uC5C6\uC74C");
|
|
4431
|
-
|
|
4519
|
+
if (globalOpts.json) {
|
|
4520
|
+
printJson({ count: 0, succeeded: [], failed: [] });
|
|
4521
|
+
} else if (!globalOpts.quiet) {
|
|
4522
|
+
process.stdout.write("\uCCA8\uBD80\uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n");
|
|
4523
|
+
}
|
|
4432
4524
|
return;
|
|
4433
4525
|
}
|
|
4434
4526
|
stopSpinner(true, `${allFiles.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2DC\uC791`);
|
|
4435
4527
|
await (0, import_promises12.mkdir)(opts.output, { recursive: true });
|
|
4436
|
-
|
|
4437
|
-
const
|
|
4528
|
+
const succeeded = [];
|
|
4529
|
+
const failed = [];
|
|
4438
4530
|
for (const f of allFiles) {
|
|
4439
4531
|
try {
|
|
4440
4532
|
const { buffer, fileName } = await client.downloadWikiPageFile(wikiId, pageId, f.id);
|
|
4441
|
-
|
|
4442
|
-
|
|
4533
|
+
const safeName = (0, import_node_path10.basename)(decodeURIComponent(fileName));
|
|
4534
|
+
const outputPath = (0, import_node_path10.join)(opts.output, safeName);
|
|
4535
|
+
await (0, import_promises12.writeFile)(outputPath, Buffer.from(buffer));
|
|
4536
|
+
succeeded.push({ path: outputPath, fileName: safeName });
|
|
4537
|
+
if (!globalOpts.json && !globalOpts.quiet) {
|
|
4538
|
+
process.stdout.write(`\u2713 ${safeName}
|
|
4443
4539
|
`);
|
|
4444
|
-
|
|
4540
|
+
}
|
|
4445
4541
|
} catch (e) {
|
|
4446
|
-
|
|
4447
|
-
|
|
4542
|
+
failed.push({ fileId: f.id, error: e instanceof Error ? e.message : String(e) });
|
|
4543
|
+
if (!globalOpts.json) {
|
|
4544
|
+
process.stderr.write(`\u2717 ${f.name} (${f.id}): ${e instanceof Error ? e.message : String(e)}
|
|
4448
4545
|
`);
|
|
4546
|
+
}
|
|
4449
4547
|
}
|
|
4450
4548
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
`);
|
|
4454
|
-
if (failures.length > 0) {
|
|
4455
|
-
throw new DoorayCliError(
|
|
4456
|
-
`${failures.length}\uAC1C \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (\uC131\uACF5: ${successCount}/${allFiles.length})`,
|
|
4457
|
-
EXIT_API_ERROR
|
|
4458
|
-
);
|
|
4459
|
-
}
|
|
4549
|
+
emitDownloadAllResult(globalOpts, { count: allFiles.length, succeeded, failed });
|
|
4550
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
4460
4551
|
});
|
|
4461
4552
|
|
|
4462
4553
|
// src/commands/wiki/page-file/delete.ts
|
|
4463
4554
|
var import_commander43 = require("commander");
|
|
4464
4555
|
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) => {
|
|
4556
|
+
const globalOpts = wikiPageFileDeleteCommand.optsWithGlobals();
|
|
4465
4557
|
const config = await getConfigOrThrow();
|
|
4466
4558
|
const client = new DoorayApiClient(config.apiKey, config.baseUrl);
|
|
4467
4559
|
let projectArg;
|
|
@@ -4511,8 +4603,7 @@ var wikiPageFileDeleteCommand = new import_commander43.Command("delete").descrip
|
|
|
4511
4603
|
try {
|
|
4512
4604
|
await client.deleteWikiPageFile(wikiId, pageId, fileId);
|
|
4513
4605
|
stopSpinner(true, "\uC0AD\uC81C \uC644\uB8CC");
|
|
4514
|
-
|
|
4515
|
-
`);
|
|
4606
|
+
emitDeleteResult(globalOpts, { fileId });
|
|
4516
4607
|
} catch (e) {
|
|
4517
4608
|
stopSpinner(false);
|
|
4518
4609
|
throw e;
|
|
@@ -5560,7 +5651,7 @@ function sanitizeArgv(argv) {
|
|
|
5560
5651
|
|
|
5561
5652
|
// src/index.ts
|
|
5562
5653
|
var program = new import_commander61.Command();
|
|
5563
|
-
program.name("dooray").description("Dooray REST API CLI").version("0.
|
|
5654
|
+
program.name("dooray").description("Dooray REST API CLI").version("0.11.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");
|
|
5564
5655
|
program.hook("preAction", () => {
|
|
5565
5656
|
const opts = program.opts();
|
|
5566
5657
|
if (opts.color === false || process.env.NO_COLOR) {
|
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@ dooray doctor # 설정 검증
|
|
|
62
62
|
| 멤버 상세 (organizationMemberId) | `dooray member get <organizationMemberId>` (cache 우회, ADR-021) |
|
|
63
63
|
| organization 전체 멤버 검색 | `dooray member search <keyword>` (이름 기본), `--email`(이메일 exact), `--user-code`(사번 like), `--user-code-exact`(사번 exact), `--page`/`--size` |
|
|
64
64
|
| 업무 목록 조회 | `dooray post list <project>` |
|
|
65
|
-
| 업무 검색 | `dooray post search <project> "<keyword>"` |
|
|
65
|
+
| 업무 검색 | `dooray post search <project|projectId> "<keyword>"` — projectId (15+자리 numeric) 직접 입력 시 cache 우회 (ADR-030) |
|
|
66
66
|
| 업무 상세 보기 | `dooray post get <project> <number>` |
|
|
67
67
|
| 업무 생성 | `dooray post create <project> --title "..." [--body "..." \| --body-file <path>]` (`--tag`/`--parent`/`--workflow`/`--milestone` 지원) |
|
|
68
68
|
| 템플릿 기반 업무 생성 | `dooray post create <project> --template <name\|id>` — body/users/tags 자동 채움 (사용자 옵션 우선 override, ADR-027) |
|
|
@@ -104,6 +104,7 @@ dooray doctor # 설정 검증
|
|
|
104
104
|
| 전체 첨부파일 다운로드 | `dooray post file download-all <project> <number>` |
|
|
105
105
|
| 첨부파일 업로드 | `dooray post file upload <project> <number> <file-path>` |
|
|
106
106
|
| 첨부파일 삭제 | `dooray post file delete <project> <number> <file-id>` |
|
|
107
|
+
| file 명령군 자동화 파싱 | `dooray post file <verb> ... --json` — `download` = `{outputPath,fileName,size}`, `download-all` = `{count,succeeded,failed}` (부분 실패 시 exit 1), `delete` = `{fileId,status}`, `upload` = `res.result` raw (ADR-031) |
|
|
107
108
|
| 댓글 첨부 목록 | `dooray post comment file list <project> <number> <comment-id>` |
|
|
108
109
|
| 댓글 파일 업로드 | `dooray post comment file upload <project> <number> <comment-id> <path>` |
|
|
109
110
|
| 댓글 파일 다운로드 | `dooray post comment file download <project> <number> <comment-id> <file-id>` |
|
|
@@ -111,6 +112,7 @@ dooray doctor # 설정 검증
|
|
|
111
112
|
| 참조자(cc) 멤버/그룹 추가 | `dooray post edit <project> <number> --cc-group <code>` — 기존 참조자 유지 + 그룹 추가 (dedupe, ADR-025) |
|
|
112
113
|
| 참조자 전체 교체 | `dooray post edit <project> <number> --cc-clear --cc <name>` — 기존 참조자 비우고 신규 멤버만 |
|
|
113
114
|
| 신규 업무 + 그룹 cc | `dooray post create <project> --title "..." --cc-group <code>` — 생성 시 그룹 참조자 포함 |
|
|
115
|
+
| `--cc-group <code\|id>` / `--mention-group <code\|id>` | 그룹 매칭 — 15+자리 numeric → id 직접 / 그 외 → code matchByName (부분일치, ADR-028) |
|
|
114
116
|
| 상위 업무 설정/변경 | `dooray post edit <project> <number> --title "<원제목>" --parent <ref>` (`<ref>`: `<project>/<number>` 또는 raw postId. `--title` 필수, unset 미지원) |
|
|
115
117
|
| `dooray post edit --id <postId> --tag <name>` | 태그 추가 (반복, dedupe) |
|
|
116
118
|
| `dooray post edit --id <postId> --tag-clear --tag <name>` | 태그 전체 교체 |
|
|
@@ -206,6 +208,16 @@ dooray wiki pages <project> --json
|
|
|
206
208
|
dooray wiki page get <project> <pageId> --json
|
|
207
209
|
```
|
|
208
210
|
|
|
211
|
+
### 첨부파일 일괄 다운로드 후 실패 분리 (ADR-031)
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# --json 으로 구조화 출력 → jq 로 성공/실패 분리
|
|
215
|
+
RESULT=$(dooray post file download-all <project> <number> -o ./ --json)
|
|
216
|
+
echo "$RESULT" | jq -r '.failed[] | "\(.fileId): \(.error)"' >&2
|
|
217
|
+
echo "$RESULT" | jq -r '.succeeded[].path'
|
|
218
|
+
# exit code 1 이 설정되어 있으면 실패 있는 상태
|
|
219
|
+
```
|
|
220
|
+
|
|
209
221
|
### 위키 페이지 첨부파일 — 스킬 파일 팀 공유 (Issue #70)
|
|
210
222
|
|
|
211
223
|
**스킬 파일 팀 공유**: 팀 위키에 스킬 파일 (예: `SKILL.md`) 을 `wiki page file upload` 로 첨부 → 팀원이 `wiki page file download-all` 로 일괄 받아 `~/.claude/skills/` 에 그대로 설치.
|
|
@@ -451,6 +463,35 @@ dooray post comment latest <project> <number>
|
|
|
451
463
|
|
|
452
464
|
---
|
|
453
465
|
|
|
466
|
+
## 그룹 멘션 / cc 시 AI agent 동선 (Issue #76, ADR-028)
|
|
467
|
+
|
|
468
|
+
자연어 그룹명을 사용자가 지칭했을 때 AI agent 의 의사결정 순서:
|
|
469
|
+
|
|
470
|
+
1. **사용자가 명확한 code 를 줬으면 바로 시도**
|
|
471
|
+
```bash
|
|
472
|
+
dooray post create <project> --mention-group "<code>"
|
|
473
|
+
```
|
|
474
|
+
부분일치 가능 (예: "AI-Data" → "AI-Data파트" 매칭).
|
|
475
|
+
|
|
476
|
+
2. **부분일치 모호 / 매칭 실패 시 후보 탐색**
|
|
477
|
+
```bash
|
|
478
|
+
dooray project groups <project>
|
|
479
|
+
```
|
|
480
|
+
ID + Code 표 출력.
|
|
481
|
+
AI agent 가 자연어 의도와 가장 가까운 code 선택 후 재시도.
|
|
482
|
+
|
|
483
|
+
3. **모든 컬럼이 빈값일 때 (response shape 이상) 회피**
|
|
484
|
+
- ADR-028 fix 이후 거의 발생 안 함 (`fetchAllMemberGroups` 가 nested array 정규화)
|
|
485
|
+
- 만약 발생 시: 사용자에게 그룹 id (UI 의 그룹 URL 에서 19자리 numeric) 확인 요청
|
|
486
|
+
- `--cc-group <id>` / `--mention-group <id>` 직접 입력
|
|
487
|
+
- 또는 그룹 멤버를 개별 `--cc <member>` / `--mention <member>` 로 지정
|
|
488
|
+
|
|
489
|
+
4. **모호한 자연어 매핑은 사용자에게 확인**
|
|
490
|
+
- 후보가 여러 개일 때 임의 선택 금지 — 사용자에게 선택지 제시
|
|
491
|
+
- 예: "AI-Data파트 / AI-Data실험팀 — 어느 그룹인가요?"
|
|
492
|
+
|
|
493
|
+
순서 고정 — 멤버 먼저, 그룹 다음 (기존 정책 유지).
|
|
494
|
+
|
|
454
495
|
## 멘션·링크 자동 삽입 (first-class)
|
|
455
496
|
|
|
456
497
|
`post create`, `post edit`, `post comment add`, `post comment edit` 모두 지원:
|
|
@@ -557,6 +598,24 @@ POST_ID=$(dooray post create <project> \
|
|
|
557
598
|
템플릿 본문의 `${year}` / `${month}` 등 매크로는 Dooray 가 자동 치환 (`interpolation=true` 기본).
|
|
558
599
|
사용자 정의 변수는 미지원 — 필요 시 client 측 string replace 로 처리.
|
|
559
600
|
|
|
601
|
+
## projectId 직접 입력 시나리오 (Issue #78, ADR-030)
|
|
602
|
+
|
|
603
|
+
AI agent 가 `member=me` 응답에 없는 프로젝트의 업무를 다뤄야 할 때:
|
|
604
|
+
|
|
605
|
+
1. **사용자가 projectId (15+자리 numeric) 를 줬으면 그대로 명령에 사용**:
|
|
606
|
+
```bash
|
|
607
|
+
dooray post search 1234567890123456789 "keyword"
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
2. **사용자가 코드만 줬고 cache 매칭 실패 (member 아닌 프로젝트)**:
|
|
611
|
+
- 에러 메시지의 ADR-030 안내 확인
|
|
612
|
+
- 사용자에게 "프로젝트 ID 가 필요합니다 — Dooray UI 의 프로젝트 URL 에서 확인 가능" 요청
|
|
613
|
+
- 또는 `dooray project list --type private` 로 private 캐시 갱신 시도
|
|
614
|
+
|
|
615
|
+
3. **권한 없는 projectId 입력 시**: resolver 통과 후 후속 API 4xx 발생 — 에러 메시지에서 권한 부재 확인 후 사용자에게 보고
|
|
616
|
+
|
|
617
|
+
권한 검증이 resolver 단보다 한 단계 지연되는 trade-off — AI 친화적 자동화 우선 (ADR-030).
|
|
618
|
+
|
|
560
619
|
## 캐시
|
|
561
620
|
|
|
562
621
|
프로젝트, 멤버, 워크플로우, 위키 정보는 `~/.dooray/cache/`에 캐시된다.
|