redmine-devrelay-client 0.7.5 → 0.9.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.
@@ -1,5 +1,5 @@
1
1
  import type { RedmineHttp } from "./http.js";
2
- import type { IssueCategoryOption, IssuePriorityOption, IssueStatusOption, ProjectVersionOption, RedmineNamed } from "./types.js";
2
+ import type { IssueCategoryOption, ListIssueCustomFieldsResult, IssuePriorityOption, IssueStatusOption, ProjectVersionOption, RedmineNamed } from "./types.js";
3
3
  /**
4
4
  * Match `name` against a list of id/name options: exact (case- and
5
5
  * space-insensitive) first, then substring. Same rules as matchMemberByName.
@@ -15,3 +15,13 @@ export declare function listIssuePriorities(http: RedmineHttp): Promise<IssuePri
15
15
  export declare function listProjectVersions(http: RedmineHttp, projectId: number): Promise<ProjectVersionOption[]>;
16
16
  /** 범주 (GET /projects/:id/issue_categories.json) */
17
17
  export declare function listIssueCategories(http: RedmineHttp, projectId: number): Promise<IssueCategoryOption[]>;
18
+ /**
19
+ * 프로젝트에서 쓸 수 있는 일감 사용자 정의 필드 (id + 이름).
20
+ *
21
+ * 1. GET /projects/:id.json?include=issue_custom_fields — Redmine 4.2+.
22
+ * 키가 오면 그것이 답이다 (빈 배열이면 정말 없는 것).
23
+ * 2. GET /custom_fields.json — 관리자 키에서만. 401/403이면 건너뛴다.
24
+ * 3. 프로젝트 최근 이슈 100건의 custom_fields를 합친다. 어떤 버전·권한에서도
25
+ * 되지만, 값이 한 번도 안 붙은 필드는 빠질 수 있다 (source: "issues").
26
+ */
27
+ export declare function listProjectIssueCustomFields(http: RedmineHttp, projectId: number): Promise<ListIssueCustomFieldsResult>;
package/dist/metadata.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { RedmineError } from "./errors.js";
1
2
  function compact(s) {
2
3
  return s.toLowerCase().replace(/\s+/g, "");
3
4
  }
@@ -54,4 +55,57 @@ export async function listIssueCategories(http, projectId) {
54
55
  assignedTo: c.assigned_to ?? null,
55
56
  }));
56
57
  }
58
+ function named(f) {
59
+ return { id: f.id, name: f.name };
60
+ }
61
+ function isAuthOrPermissionError(err) {
62
+ return (err instanceof RedmineError &&
63
+ (err.code === "REDMINE_PERMISSION_DENIED" ||
64
+ err.code === "REDMINE_AUTHENTICATION_ERROR"));
65
+ }
66
+ /**
67
+ * 프로젝트에서 쓸 수 있는 일감 사용자 정의 필드 (id + 이름).
68
+ *
69
+ * 1. GET /projects/:id.json?include=issue_custom_fields — Redmine 4.2+.
70
+ * 키가 오면 그것이 답이다 (빈 배열이면 정말 없는 것).
71
+ * 2. GET /custom_fields.json — 관리자 키에서만. 401/403이면 건너뛴다.
72
+ * 3. 프로젝트 최근 이슈 100건의 custom_fields를 합친다. 어떤 버전·권한에서도
73
+ * 되지만, 값이 한 번도 안 붙은 필드는 빠질 수 있다 (source: "issues").
74
+ */
75
+ export async function listProjectIssueCustomFields(http, projectId) {
76
+ const project = await http.getJson(`/projects/${projectId}.json`, { include: "issue_custom_fields" });
77
+ const fromProject = project?.project?.issue_custom_fields;
78
+ if (Array.isArray(fromProject)) {
79
+ return { fields: fromProject.map(named), source: "project" };
80
+ }
81
+ try {
82
+ const admin = await http.getJson("/custom_fields.json");
83
+ const fields = (admin?.custom_fields ?? [])
84
+ .filter((f) => f.customized_type === "issue")
85
+ // projects가 없으면 판단할 수 없으니 넣어 둔다 (빠지는 것보다 낫다)
86
+ .filter((f) => f.is_for_all ||
87
+ !f.projects ||
88
+ f.projects.some((p) => p.id === projectId))
89
+ .map(named);
90
+ return { fields, source: "custom_fields" };
91
+ }
92
+ catch (err) {
93
+ if (!isAuthOrPermissionError(err))
94
+ throw err;
95
+ }
96
+ const sample = await http.getJson("/issues.json", {
97
+ project_id: projectId,
98
+ status_id: "*",
99
+ limit: 100,
100
+ sort: "updated_on:desc",
101
+ });
102
+ const seen = new Map();
103
+ for (const issue of sample?.issues ?? []) {
104
+ for (const f of issue.custom_fields ?? []) {
105
+ if (!seen.has(f.id))
106
+ seen.set(f.id, named(f));
107
+ }
108
+ }
109
+ return { fields: [...seen.values()], source: "issues" };
110
+ }
57
111
  //# sourceMappingURL=metadata.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAWA,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAU,EACV,IAAY;IAEZ,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,8BAA8B;AAC9B,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAC7B,gBAAgB,CACjB,CAAC;IACF,OAAO,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,oCAAoC;AACpC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAE5B,sBAAsB,CAAC,CAAC;IAC3B,OAAO,CAAC,IAAI,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9C,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;KAC/B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAE5B,qCAAqC,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,EAAE,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;KACjC,CAAC,CAAC,CAAC;AACN,CAAC;AAED,8CAA8C;AAC9C,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAiB,EACjB,SAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAI5B,aAAa,SAAS,gBAAgB,CAAC,CAAC;IAC3C,OAAO,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxC,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM;QAC1B,OAAO,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI;KAC5B,CAAC,CAAC,CAAC;AACN,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAiB,EACjB,SAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAE5B,aAAa,SAAS,wBAAwB,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,EAAE,gBAAgB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,UAAU,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI;KAClC,CAAC,CAAC,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { RedmineConfig } from "./config.js";
2
+ import type { RedmineHttp } from "./http.js";
3
+ import type { SearchTextInput, SearchTextResult } from "./types.js";
4
+ export declare const SEARCH_TEXT_TYPES: readonly ["issues", "news", "documents", "changesets", "wiki_pages", "messages", "projects"];
5
+ export declare function buildSearchTextQuery(input: SearchTextInput): Record<string, string | number>;
6
+ /**
7
+ * 전문 검색 (GET /search.json, Redmine 3.3+). 프로젝트를 주면
8
+ * /projects/:id/search.json 으로 범위를 좁힌다. 구버전 Redmine은 이 경로가 없어
9
+ * 로그인 페이지로 튕기며 401/404가 오는데, 그건 키 문제가 아니므로 안내를 바꿔 준다.
10
+ */
11
+ export declare function searchText(http: RedmineHttp, config: RedmineConfig, input: SearchTextInput): Promise<SearchTextResult>;
package/dist/search.js ADDED
@@ -0,0 +1,89 @@
1
+ import { RedmineError } from "./errors.js";
2
+ export const SEARCH_TEXT_TYPES = [
3
+ "issues",
4
+ "news",
5
+ "documents",
6
+ "changesets",
7
+ "wiki_pages",
8
+ "messages",
9
+ "projects",
10
+ ];
11
+ export function buildSearchTextQuery(input) {
12
+ const query = { q: input.query.trim() };
13
+ const types = input.types?.length ? input.types : ["issues"];
14
+ for (const t of types)
15
+ query[t] = 1;
16
+ if (input.titlesOnly)
17
+ query.titles_only = 1;
18
+ if (input.openIssuesOnly)
19
+ query.open_issues = 1;
20
+ if (input.allWords !== false)
21
+ query.all_words = 1;
22
+ if (input.includeAttachments)
23
+ query.attachments = 1;
24
+ return query;
25
+ }
26
+ function normalizeRow(raw) {
27
+ return {
28
+ id: raw.id,
29
+ title: raw.title ?? "",
30
+ type: raw.type ?? "",
31
+ url: raw.url ?? "",
32
+ description: raw.description ?? "",
33
+ datetime: raw.datetime ?? null,
34
+ };
35
+ }
36
+ /**
37
+ * 전문 검색 (GET /search.json, Redmine 3.3+). 프로젝트를 주면
38
+ * /projects/:id/search.json 으로 범위를 좁힌다. 구버전 Redmine은 이 경로가 없어
39
+ * 로그인 페이지로 튕기며 401/404가 오는데, 그건 키 문제가 아니므로 안내를 바꿔 준다.
40
+ */
41
+ export async function searchText(http, config, input) {
42
+ const q = input.query.trim();
43
+ if (!q) {
44
+ throw new RedmineError({
45
+ code: "REDMINE_VALIDATION_ERROR",
46
+ message: "query must be non-empty",
47
+ check: ["Pass the words to search for"],
48
+ });
49
+ }
50
+ const limit = Math.min(input.limit ?? 25, config.maxResultCount, 100);
51
+ const offset = input.offset ?? 0;
52
+ const path = input.projectId !== undefined
53
+ ? `/projects/${input.projectId}/search.json`
54
+ : "/search.json";
55
+ let data;
56
+ try {
57
+ data = await http.getJson(path, {
58
+ ...buildSearchTextQuery(input),
59
+ limit,
60
+ offset,
61
+ });
62
+ }
63
+ catch (err) {
64
+ if (err instanceof RedmineError &&
65
+ (err.httpStatus === 401 || err.httpStatus === 404)) {
66
+ throw new RedmineError({
67
+ code: "REDMINE_UNKNOWN_ERROR",
68
+ message: "Full-text search is not available on this Redmine (the /search.json API needs Redmine 3.3+ and the search module)",
69
+ httpStatus: err.httpStatus,
70
+ retrySafe: false,
71
+ check: [
72
+ "Use redmine_search_issues with subjectContains instead",
73
+ "If other calls also fail, check REDMINE_API_KEY",
74
+ ],
75
+ });
76
+ }
77
+ throw err;
78
+ }
79
+ const rows = (data?.results ?? []).map(normalizeRow);
80
+ const totalCount = data?.total_count ?? rows.length;
81
+ return {
82
+ query: q,
83
+ results: rows,
84
+ totalCount,
85
+ returnedCount: rows.length,
86
+ hasMore: offset + rows.length < totalCount,
87
+ };
88
+ }
89
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { RedmineConfig } from "./config.js";
2
+ import type { RedmineHttp } from "./http.js";
3
+ import type { CreateTimeEntryInput, CreateTimeEntryResult, ListTimeEntriesInput, ListTimeEntriesResult, TimeEntry, TimeEntryActivityOption } from "./types.js";
4
+ type RawNamed = {
5
+ id: number;
6
+ name: string;
7
+ };
8
+ type RawTimeEntry = {
9
+ id: number;
10
+ project?: RawNamed | null;
11
+ issue?: {
12
+ id: number;
13
+ } | null;
14
+ user?: RawNamed | null;
15
+ activity?: RawNamed | null;
16
+ hours?: number | string | null;
17
+ comments?: string | null;
18
+ spent_on?: string | null;
19
+ created_on?: string | null;
20
+ updated_on?: string | null;
21
+ };
22
+ export declare function normalizeTimeEntry(raw: RawTimeEntry): TimeEntry;
23
+ /**
24
+ * Redmine의 spent_on 필터 문법: 둘 다 있으면 `><from|to`, 하나면 `>=from` / `<=to`.
25
+ */
26
+ export declare function buildTimeEntryQuery(input: ListTimeEntriesInput): Record<string, string | number>;
27
+ /** 작업시간 목록 (GET /time_entries.json) — 최근 날짜부터, 페이지를 이어 받는다 */
28
+ export declare function listTimeEntries(http: RedmineHttp, config: RedmineConfig, input?: ListTimeEntriesInput): Promise<ListTimeEntriesResult>;
29
+ /** 작업시간 기록 (POST /time_entries.json) */
30
+ export declare function createTimeEntry(http: RedmineHttp, input: CreateTimeEntryInput): Promise<CreateTimeEntryResult>;
31
+ /** 작업 분류(활동) 목록 (GET /enumerations/time_entry_activities.json) */
32
+ export declare function listTimeEntryActivities(http: RedmineHttp): Promise<TimeEntryActivityOption[]>;
33
+ export {};
@@ -0,0 +1,124 @@
1
+ import { RedmineError } from "./errors.js";
2
+ function toHours(value) {
3
+ const n = typeof value === "string" ? Number(value) : value ?? 0;
4
+ return Number.isFinite(n) ? n : 0;
5
+ }
6
+ export function normalizeTimeEntry(raw) {
7
+ return {
8
+ id: raw.id,
9
+ project: raw.project ?? null,
10
+ issue: raw.issue ?? null,
11
+ user: raw.user ?? null,
12
+ activity: raw.activity ?? null,
13
+ hours: toHours(raw.hours),
14
+ comments: raw.comments ?? "",
15
+ spentOn: raw.spent_on ?? "",
16
+ createdOn: raw.created_on ?? null,
17
+ updatedOn: raw.updated_on ?? null,
18
+ };
19
+ }
20
+ /**
21
+ * Redmine의 spent_on 필터 문법: 둘 다 있으면 `><from|to`, 하나면 `>=from` / `<=to`.
22
+ */
23
+ export function buildTimeEntryQuery(input) {
24
+ const query = {};
25
+ if (input.issueId !== undefined)
26
+ query.issue_id = input.issueId;
27
+ if (input.projectId !== undefined)
28
+ query.project_id = input.projectId;
29
+ if (input.userId !== undefined)
30
+ query.user_id = String(input.userId);
31
+ if (input.activityId !== undefined)
32
+ query.activity_id = input.activityId;
33
+ if (input.spentFrom && input.spentTo) {
34
+ query.spent_on = `><${input.spentFrom}|${input.spentTo}`;
35
+ }
36
+ else if (input.spentFrom) {
37
+ query.spent_on = `>=${input.spentFrom}`;
38
+ }
39
+ else if (input.spentTo) {
40
+ query.spent_on = `<=${input.spentTo}`;
41
+ }
42
+ query.sort = "spent_on:desc";
43
+ return query;
44
+ }
45
+ /** 작업시간 목록 (GET /time_entries.json) — 최근 날짜부터, 페이지를 이어 받는다 */
46
+ export async function listTimeEntries(http, config, input = {}) {
47
+ const wanted = Math.min(input.limit ?? config.maxResultCount, config.maxResultCount);
48
+ const baseQuery = buildTimeEntryQuery(input);
49
+ const startOffset = input.offset ?? 0;
50
+ const collected = [];
51
+ let offset = startOffset;
52
+ let totalCount = 0;
53
+ while (collected.length < wanted) {
54
+ const pageLimit = Math.min(100, wanted - collected.length);
55
+ const page = await http.getJson("/time_entries.json", { ...baseQuery, limit: pageLimit, offset });
56
+ totalCount = page.total_count;
57
+ const rows = page.time_entries ?? [];
58
+ collected.push(...rows.map(normalizeTimeEntry));
59
+ offset += rows.length;
60
+ if (rows.length === 0 || offset >= totalCount)
61
+ break;
62
+ }
63
+ const totalHours = collected.reduce((sum, e) => sum + e.hours, 0);
64
+ return {
65
+ entries: collected,
66
+ totalCount,
67
+ returnedCount: collected.length,
68
+ hasMore: startOffset + collected.length < totalCount,
69
+ // 부동소수 합계를 사람이 읽을 수 있게 소수 둘째 자리까지
70
+ totalHours: Math.round(totalHours * 100) / 100,
71
+ };
72
+ }
73
+ /** 작업시간 기록 (POST /time_entries.json) */
74
+ export async function createTimeEntry(http, input) {
75
+ if (input.issueId === undefined && input.projectId === undefined) {
76
+ throw new RedmineError({
77
+ code: "REDMINE_VALIDATION_ERROR",
78
+ message: "A time entry needs issueId or projectId",
79
+ check: ["Pass the issue id (preferred) or a project id"],
80
+ });
81
+ }
82
+ if (!(input.hours > 0)) {
83
+ throw new RedmineError({
84
+ code: "REDMINE_VALIDATION_ERROR",
85
+ message: `hours must be positive (got ${input.hours})`,
86
+ check: ["Pass hours as a positive number, e.g. 1.5"],
87
+ });
88
+ }
89
+ const timeEntry = { hours: input.hours };
90
+ if (input.issueId !== undefined)
91
+ timeEntry.issue_id = input.issueId;
92
+ else
93
+ timeEntry.project_id = input.projectId;
94
+ if (input.spentOn !== undefined)
95
+ timeEntry.spent_on = input.spentOn;
96
+ if (input.activityId !== undefined)
97
+ timeEntry.activity_id = input.activityId;
98
+ if (input.comments !== undefined)
99
+ timeEntry.comments = input.comments;
100
+ const data = await http.postJson("/time_entries.json", { time_entry: timeEntry });
101
+ if (!data?.time_entry) {
102
+ throw new Error("Redmine createTimeEntry returned empty body");
103
+ }
104
+ const entry = normalizeTimeEntry(data.time_entry);
105
+ return {
106
+ id: entry.id,
107
+ hours: entry.hours,
108
+ spentOn: entry.spentOn,
109
+ issueId: entry.issue?.id ?? input.issueId ?? null,
110
+ projectId: entry.project?.id ?? input.projectId ?? null,
111
+ activity: entry.activity,
112
+ comments: entry.comments,
113
+ };
114
+ }
115
+ /** 작업 분류(활동) 목록 (GET /enumerations/time_entry_activities.json) */
116
+ export async function listTimeEntryActivities(http) {
117
+ const data = await http.getJson("/enumerations/time_entry_activities.json");
118
+ return (data?.time_entry_activities ?? []).map((a) => ({
119
+ id: a.id,
120
+ name: a.name,
121
+ isDefault: Boolean(a.is_default),
122
+ }));
123
+ }
124
+ //# sourceMappingURL=timeEntries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeEntries.js","sourceRoot":"","sources":["../src/timeEntries.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -40,6 +40,87 @@ export type ProjectVersionOption = RedmineNamed & {
40
40
  export type IssueCategoryOption = RedmineNamed & {
41
41
  assignedTo: RedmineNamed | null;
42
42
  };
43
+ /** 프로젝트에 켜진 일감 사용자 정의 필드 (id + 이름) */
44
+ export type IssueCustomFieldOption = RedmineNamed;
45
+ export type ListIssueCustomFieldsResult = {
46
+ fields: IssueCustomFieldOption[];
47
+ /**
48
+ * project — GET /projects/:id.json?include=issue_custom_fields (Redmine 4.2+)
49
+ * custom_fields — GET /custom_fields.json (관리자 키)
50
+ * issues — 프로젝트 최근 이슈에서 합친 것. 값이 한 번도 안 붙은 필드는 빠질 수 있음
51
+ */
52
+ source: "project" | "custom_fields" | "issues";
53
+ };
54
+ /**
55
+ * 쓰기용 사용자 정의 필드 값. 문자열, 다중 선택이면 문자열 배열,
56
+ * 빈 문자열이면 비움 (Redmine은 ""를 비우기로 해석).
57
+ */
58
+ export type IssueCustomFieldValue = string | string[];
59
+ export type IssueCustomFieldWrite = {
60
+ id: number;
61
+ value: IssueCustomFieldValue;
62
+ };
63
+ /** 작업 분류(활동) — time_entry_activities */
64
+ export type TimeEntryActivityOption = RedmineNamed & {
65
+ isDefault: boolean;
66
+ };
67
+ /** 작업시간 한 건 (GET /time_entries.json 행) */
68
+ export type TimeEntry = {
69
+ id: number;
70
+ project: RedmineNamed | null;
71
+ issue: {
72
+ id: number;
73
+ } | null;
74
+ user: RedmineNamed | null;
75
+ activity: RedmineNamed | null;
76
+ hours: number;
77
+ comments: string;
78
+ /** YYYY-MM-DD */
79
+ spentOn: string;
80
+ createdOn: string | null;
81
+ updatedOn: string | null;
82
+ };
83
+ export type ListTimeEntriesInput = {
84
+ issueId?: number;
85
+ projectId?: number;
86
+ /** "me" 또는 user id */
87
+ userId?: "me" | number;
88
+ /** YYYY-MM-DD (포함) */
89
+ spentFrom?: string;
90
+ /** YYYY-MM-DD (포함) */
91
+ spentTo?: string;
92
+ activityId?: number;
93
+ limit?: number;
94
+ offset?: number;
95
+ };
96
+ export type ListTimeEntriesResult = {
97
+ entries: TimeEntry[];
98
+ totalCount: number;
99
+ returnedCount: number;
100
+ hasMore: boolean;
101
+ /** 돌려준 행들의 hours 합 (전체 합이 아님 — hasMore면 더 있다) */
102
+ totalHours: number;
103
+ };
104
+ export type CreateTimeEntryInput = {
105
+ /** 둘 중 하나는 필수. 일감이 있으면 일감을 우선 */
106
+ issueId?: number;
107
+ projectId?: number;
108
+ hours: number;
109
+ /** YYYY-MM-DD, 생략하면 Redmine이 오늘로 */
110
+ spentOn?: string;
111
+ /** 활동 id. Redmine에 기본 활동이 없으면 필수 */
112
+ activityId?: number;
113
+ comments?: string;
114
+ };
115
+ export type CreateTimeEntryResult = {
116
+ id: number;
117
+ hours: number;
118
+ spentOn: string;
119
+ issueId: number | null;
120
+ projectId: number | null;
121
+ activity: RedmineNamed | null;
122
+ comments: string;
123
+ };
43
124
  /** 이름 또는 id로 지정할 수 있는 필드 값 */
44
125
  export type NamedRef = number | string;
45
126
  export type IssueInclude = "journals" | "attachments" | "relations" | "children" | "allowed_statuses";
@@ -73,6 +154,13 @@ export type NormalizedIssueSummary = {
73
154
  updatedOn: string | null;
74
155
  createdOn: string | null;
75
156
  };
157
+ /** journal 한 건의 필드 변경: property attr → name은 필드 (status_id, done_ratio, assigned_to_id …) */
158
+ export type JournalDetail = {
159
+ property: string;
160
+ name: string;
161
+ oldValue: string | null;
162
+ newValue: string | null;
163
+ };
76
164
  export type NormalizedIssueDetail = NormalizedIssueSummary & {
77
165
  description: string;
78
166
  author: {
@@ -102,6 +190,8 @@ export type NormalizedIssueDetail = NormalizedIssueSummary & {
102
190
  notes: string;
103
191
  createdOn: string;
104
192
  privateNotes: boolean;
193
+ /** 필드 변경 내역 (상태·진척도·담당자 …). Redmine의 journal details */
194
+ details: JournalDetail[];
105
195
  }>;
106
196
  attachments?: Array<{
107
197
  id: number;
@@ -120,18 +210,32 @@ export type NormalizedIssueDetail = NormalizedIssueSummary & {
120
210
  export type SearchIssuesInput = {
121
211
  projectId?: number;
122
212
  issueId?: number;
213
+ /** "me", user id — names are resolved by the MCP layer before reaching here */
123
214
  assignedTo?: string | number;
124
215
  status?: "open" | "closed" | "all" | number;
125
216
  trackerId?: number;
126
217
  priorityId?: number;
218
+ /** 대상 버전 id */
219
+ fixedVersionId?: number;
220
+ /** 범주 id */
221
+ categoryId?: number;
222
+ /** 작성자: "me" 또는 user id */
223
+ authorId?: "me" | number;
224
+ /** 일감관리자(watcher): "me" 또는 user id */
225
+ watcherId?: "me" | number;
127
226
  subjectContains?: string;
128
227
  createdAfter?: string;
228
+ createdBefore?: string;
129
229
  updatedAfter?: string;
130
- parentIssueId?: number;
230
+ updatedBefore?: string;
231
+ /** 완료기한 범위 (포함) */
232
+ dueAfter?: string;
233
+ dueBefore?: string;
131
234
  customFields?: Array<{
132
235
  id: number;
133
236
  value: string;
134
237
  }>;
238
+ parentIssueId?: number;
135
239
  sort?: Array<{
136
240
  field: string;
137
241
  direction: "asc" | "desc";
@@ -139,6 +243,38 @@ export type SearchIssuesInput = {
139
243
  limit?: number;
140
244
  offset?: number;
141
245
  };
246
+ export type SearchTextType = "issues" | "news" | "documents" | "changesets" | "wiki_pages" | "messages" | "projects";
247
+ /** 전문 검색 (GET /search.json) */
248
+ export type SearchTextInput = {
249
+ query: string;
250
+ /** 있으면 /projects/:id/search.json */
251
+ projectId?: number;
252
+ /** 기본 ["issues"] */
253
+ types?: SearchTextType[];
254
+ titlesOnly?: boolean;
255
+ openIssuesOnly?: boolean;
256
+ /** 기본 true — 모든 단어가 들어간 결과만 */
257
+ allWords?: boolean;
258
+ includeAttachments?: boolean;
259
+ limit?: number;
260
+ offset?: number;
261
+ };
262
+ export type SearchTextRow = {
263
+ id: number;
264
+ title: string;
265
+ /** "issue", "issue closed", "wiki-page", "news" … Redmine이 주는 그대로 */
266
+ type: string;
267
+ url: string;
268
+ description: string;
269
+ datetime: string | null;
270
+ };
271
+ export type SearchTextResult = {
272
+ query: string;
273
+ results: SearchTextRow[];
274
+ totalCount: number;
275
+ returnedCount: number;
276
+ hasMore: boolean;
277
+ };
142
278
  export type AttachmentInput = {
143
279
  path: string;
144
280
  filename?: string;
@@ -163,6 +299,42 @@ export type IssueUploadToken = {
163
299
  };
164
300
  export declare const ATTACHMENT_MAX_FILES = 5;
165
301
  export declare const ATTACHMENT_MAX_BYTES: number;
302
+ /** 내려받기 기본 상한 (요청으로 올릴 수 있는 최대는 HARD) */
303
+ export declare const ATTACHMENT_DOWNLOAD_MAX_BYTES: number;
304
+ export declare const ATTACHMENT_DOWNLOAD_HARD_MAX_BYTES: number;
305
+ /** 텍스트 파일을 결과에 바로 실어 주는 상한 */
306
+ export declare const ATTACHMENT_TEXT_INLINE_MAX_BYTES: number;
307
+ /** GET /attachments/:id.json */
308
+ export type AttachmentInfo = {
309
+ id: number;
310
+ filename: string;
311
+ filesize: number;
312
+ /** 구버전 Redmine은 안 준다 → 내려받을 때 응답 헤더로 채움 */
313
+ contentType: string | null;
314
+ description: string;
315
+ contentUrl: string;
316
+ author: RedmineNamed | null;
317
+ createdOn: string | null;
318
+ };
319
+ export type DownloadAttachmentInput = {
320
+ attachmentId: number;
321
+ /** 저장 폴더. 생략하면 OS 임시 폴더 아래 redmine-devrelay/attachments/<id>/ */
322
+ destDir?: string;
323
+ /** 기본 ATTACHMENT_DOWNLOAD_MAX_BYTES, 최대 ATTACHMENT_DOWNLOAD_HARD_MAX_BYTES */
324
+ maxBytes?: number;
325
+ /** 텍스트 파일이면 내용을 함께 돌려줄지 (기본 true) */
326
+ inlineText?: boolean;
327
+ };
328
+ export type DownloadAttachmentResult = {
329
+ attachment: AttachmentInfo;
330
+ /** 저장된 로컬 경로 */
331
+ path: string;
332
+ sizeBytes: number;
333
+ contentType: string | null;
334
+ /** 텍스트로 판단되면 UTF-8 본문 (ATTACHMENT_TEXT_INLINE_MAX_BYTES까지) */
335
+ text?: string;
336
+ textTruncated?: boolean;
337
+ };
166
338
  /** Redmine relation_type values (연결된 일감 관계 종류) */
167
339
  export declare const ISSUE_RELATION_TYPES: readonly ["relates", "duplicates", "duplicated", "blocks", "blocked", "precedes", "follows", "copied_to", "copied_from"];
168
340
  export type IssueRelationType = (typeof ISSUE_RELATION_TYPES)[number];
@@ -222,6 +394,8 @@ export type CreateIssueInput = {
222
394
  fixedVersionId?: number;
223
395
  /** 범주 (category_id) */
224
396
  categoryId?: number;
397
+ /** 사용자 정의 필드 (custom_fields) — 프로젝트에 켜진 필드만 */
398
+ customFields?: IssueCustomFieldWrite[];
225
399
  /** "me", numeric user id (담당자) */
226
400
  assignedTo?: "me" | number;
227
401
  /** 일감관리자 — Redmine watcher_user_ids */
@@ -254,6 +428,8 @@ export type UpdateIssueInput = {
254
428
  fixedVersionId?: number | null;
255
429
  /** 범주 — number sets it, null clears it */
256
430
  categoryId?: number | null;
431
+ /** 사용자 정의 필드 — 넘긴 필드만 바뀜, value ""면 비움 */
432
+ customFields?: IssueCustomFieldWrite[];
257
433
  assignedTo?: "me" | number;
258
434
  /** replace-all when provided (including empty) */
259
435
  watcherUserIds?: number[];
package/dist/types.js CHANGED
@@ -1,5 +1,10 @@
1
1
  export const ATTACHMENT_MAX_FILES = 5;
2
2
  export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
3
+ /** 내려받기 기본 상한 (요청으로 올릴 수 있는 최대는 HARD) */
4
+ export const ATTACHMENT_DOWNLOAD_MAX_BYTES = 10 * 1024 * 1024;
5
+ export const ATTACHMENT_DOWNLOAD_HARD_MAX_BYTES = 50 * 1024 * 1024;
6
+ /** 텍스트 파일을 결과에 바로 실어 주는 상한 */
7
+ export const ATTACHMENT_TEXT_INLINE_MAX_BYTES = 200 * 1024;
3
8
  /** Redmine relation_type values (연결된 일감 관계 종류) */
4
9
  export const ISSUE_RELATION_TYPES = [
5
10
  "relates",
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA8IA,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AACtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAErD,kDAAkD;AAClD,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,SAAS;IACT,YAAY;IACZ,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,UAAU;IACV,SAAS;IACT,WAAW;IACX,aAAa;CACL,CAAC;AAIX,mDAAmD;AACnD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,SAAS,CAAU,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAwSA,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AACtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAErD,yCAAyC;AACzC,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9D,MAAM,CAAC,MAAM,kCAAkC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACnE,8BAA8B;AAC9B,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,GAAG,IAAI,CAAC;AAoC3D,kDAAkD;AAClD,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,SAAS;IACT,YAAY;IACZ,YAAY;IACZ,QAAQ;IACR,SAAS;IACT,UAAU;IACV,SAAS;IACT,WAAW;IACX,aAAa;CACL,CAAC;AAIX,mDAAmD;AACnD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,SAAS,CAAU,CAAC"}
package/dist/writes.js CHANGED
@@ -29,6 +29,12 @@ function applyOptionalIssueFields(issue, input, opts = {}) {
29
29
  if (input.categoryId !== undefined) {
30
30
  issue.category_id = input.categoryId === null ? "" : input.categoryId;
31
31
  }
32
+ if (input.customFields !== undefined && input.customFields.length > 0) {
33
+ issue.custom_fields = input.customFields.map((f) => ({
34
+ id: f.id,
35
+ value: f.value,
36
+ }));
37
+ }
32
38
  if (input.assignedTo !== undefined)
33
39
  issue.assigned_to_id = input.assignedTo;
34
40
  if (input.watcherUserIds !== undefined) {