@@ -27,12 +27,21 @@ def _csv(value: str | Iterable[str] | None) -> str | None:
2727 return "," .join (str (v ) for v in value )
2828
2929
30- async def _proxy (method : str , dooray_path : str , * , params : dict | None = None , data : dict | None = None ) -> dict :
30+ async def _proxy (
31+ method : str ,
32+ dooray_path : str ,
33+ * ,
34+ params : dict | None = None ,
35+ data : dict | None = None ,
36+ keep_data_nulls : Iterable [str ] = (),
37+ ) -> dict :
38+ keep = set (keep_data_nulls )
39+ body = {k : v for k , v in (data or {}).items () if v is not None or k in keep }
3140 resp = await _client .request (
3241 method ,
3342 f"/v1/admin-api/proxy/dooray/{ dooray_path .lstrip ('/' )} " ,
3443 params = {k : v for k , v in (params or {}).items () if v is not None },
35- json = { k : v for k , v in ( data or {}). items () if v is not None } or None ,
44+ json = body or None ,
3645 headers = {"Authorization" : f"Bearer { (await get_context ().get_state (AUTH_KEY )).jwt } " },
3746 )
3847 if message := _PROXY_ERRORS .get (resp .status_code ):
@@ -49,6 +58,15 @@ def _members(ids: Iterable[str] | None) -> list[dict] | None:
4958 return [{"type" : "member" , "member" : {"organizationMemberId" : m }} for m in ids ] if ids else None
5059
5160
61+ def _users (to_member_ids : Iterable [str ] | None , cc_member_ids : Iterable [str ] | None ) -> dict | None :
62+ users = {}
63+ if to := _members (to_member_ids ):
64+ users ["to" ] = to
65+ if cc := _members (cc_member_ids ):
66+ users ["cc" ] = cc
67+ return users or None
68+
69+
5270async def _with_member_names (rows : list [dict ]) -> list [dict ]:
5371 sem = Semaphore (8 )
5472
@@ -175,7 +193,8 @@ async def dooray_post_comments(project_id: str, post_id: str) -> dict:
175193 description = (
176194 "⚠️ Dooray 에 즉시 반영. 새 업무(Post)를 생성한다. 본문은 마크다운. "
177195 "담당자/참조자 id 는 `dooray_project_choices`(프로젝트 멤버)·`auth_status`의 dooray.member_id(본인)에서 확보. "
178- "priority=highest|high|normal|low|lowest|none."
196+ "priority=highest|high|normal|low|lowest|none. "
197+ "workflow_id 지정 시 생성 직후 해당 상태로 이동(미지정이면 프로젝트 기본 상태=보통 보류/등록)."
179198 ),
180199 tags = tag ,
181200 )
@@ -190,27 +209,29 @@ async def dooray_create_post(
190209 tag_ids : list [str ] | None = None ,
191210 priority : str | None = None ,
192211 parent_post_id : str | None = None ,
212+ workflow_id : str | None = None ,
193213 ) -> dict :
194- users = {}
195- if to_member_ids :
196- users ["to" ] = _members (to_member_ids )
197- if cc_member_ids :
198- users ["cc" ] = _members (cc_member_ids )
199-
200- return await _proxy (
214+ created = await _proxy (
201215 method = "POST" ,
202216 dooray_path = f"/project/v1/projects/{ project_id } /posts" ,
203217 data = {
204218 "subject" : subject ,
205219 "body" : {"mimeType" : "text/x-markdown" , "content" : body_markdown },
206- "users" : users or None ,
220+ "users" : _users ( to_member_ids , cc_member_ids ) ,
207221 "dueDate" : due_date ,
208222 "milestoneId" : milestone_id ,
209223 "tagIds" : tag_ids ,
210224 "priority" : priority ,
211225 "parentPostId" : parent_post_id ,
212226 },
213227 )
228+ if workflow_id and (post_id := (created .get ("result" ) or {}).get ("id" )):
229+ await _proxy (
230+ method = "POST" ,
231+ dooray_path = f"/project/v1/projects/{ project_id } /posts/{ post_id } /set-workflow" ,
232+ data = {"workflowId" : workflow_id },
233+ )
234+ return created
214235
215236 @mcp .tool (
216237 title = "Dooray 업무 수정" ,
@@ -229,12 +250,6 @@ async def dooray_update_post(
229250 tag_ids : list [str ] | None = None ,
230251 priority : str | None = None ,
231252 ) -> dict :
232- users = {}
233- if to_member_ids :
234- users ["to" ] = _members (to_member_ids )
235- if cc_member_ids :
236- users ["cc" ] = _members (cc_member_ids )
237-
238253 return await _proxy (
239254 method = "PUT" ,
240255 dooray_path = f"/project/v1/projects/{ project_id } /posts/{ post_id } " ,
@@ -243,7 +258,7 @@ async def dooray_update_post(
243258 "body" : (
244259 {"mimeType" : "text/x-markdown" , "content" : body_markdown } if body_markdown is not None else None
245260 ),
246- "users" : users or None ,
261+ "users" : _users ( to_member_ids , cc_member_ids ) ,
247262 "dueDate" : due_date ,
248263 "milestoneId" : milestone_id ,
249264 "tagIds" : tag_ids ,
@@ -265,7 +280,11 @@ async def dooray_set_post_workflow(project_id: str, post_id: str, workflow_id: s
265280
266281 @mcp .tool (
267282 title = "Dooray 업무 완료 처리" ,
268- description = "⚠️ Dooray 에 즉시 반영. 업무를 완료(closed) 상태로 변경." ,
283+ description = (
284+ "⚠️ Dooray 에 즉시 반영. 업무를 완료(closed) 상태로 변경. "
285+ "‼️ 업무 삭제(휴지통)는 이 도구(및 Dooray API)로 불가 — 완료 처리가 최대치다. "
286+ "정말 지우려면 Dooray 웹 UI에서 수동으로만 가능."
287+ ),
269288 tags = tag ,
270289 )
271290 async def dooray_set_post_done (project_id : str , post_id : str ) -> dict :
@@ -274,6 +293,60 @@ async def dooray_set_post_done(project_id: str, post_id: str) -> dict:
274293 dooray_path = f"/project/v1/projects/{ project_id } /posts/{ post_id } /set-done" ,
275294 )
276295
296+ @mcp .tool (
297+ title = "Dooray 업무 상위(에픽) 설정/해제" ,
298+ description = (
299+ "⚠️ Dooray 에 즉시 반영. 기존 업무의 상위 업무(에픽 등)를 설정하거나 해제한다. "
300+ "parent_post_id 지정=그 업무의 하위로 묶기, 미지정(None)=상위 해제(최상위로). "
301+ "제약: 이미 하위 업무를 가진 업무는 다른 업무의 하위로 설정할 수 없다(2단계 계층 불가). "
302+ "상위-하위 관계는 같은 프로젝트 안에서만 가능."
303+ ),
304+ tags = tag ,
305+ )
306+ async def dooray_set_parent_post (project_id : str , post_id : str , parent_post_id : str | None = None ) -> dict :
307+ return await _proxy (
308+ method = "POST" ,
309+ dooray_path = f"/project/v1/projects/{ project_id } /posts/{ post_id } /set-parent-post" ,
310+ data = {"parentPostId" : parent_post_id },
311+ keep_data_nulls = ("parentPostId" ,),
312+ )
313+
314+ @mcp .tool (
315+ title = "Dooray 업무 이동" ,
316+ description = (
317+ "⚠️ Dooray 에 즉시 반영·비가역. 업무를 다른 프로젝트로 이동한다. "
318+ "주의: 이동하면 그 업무의 워크플로(단계)·태그 정보는 사라진다. "
319+ "include_sub_posts=True 면 하위 업무도 함께 이동. target_project_id 는 `dooray_projects` 로 확보."
320+ ),
321+ tags = tag ,
322+ )
323+ async def dooray_move_post (
324+ project_id : str , post_id : str , target_project_id : str , include_sub_posts : bool = True
325+ ) -> dict :
326+ return await _proxy (
327+ method = "POST" ,
328+ dooray_path = f"/project/v1/projects/{ project_id } /posts/{ post_id } /move" ,
329+ data = {"targetProjectId" : target_project_id , "includeSubPosts" : include_sub_posts },
330+ )
331+
332+ @mcp .tool (
333+ title = "Dooray 태그 정의 생성" ,
334+ description = (
335+ "⚠️ Dooray 에 즉시 반영. 프로젝트에 새 태그(정의)를 생성한다. "
336+ "name 이 `그룹명:태그명` 형식이면 그룹 태그, 그냥 `태그명`이면 개별 태그. color 는 6자리 hex(예: ffffff). "
337+ "생성된 태그 id 는 이후 업무의 tag_ids 로 사용. "
338+ "‼️ 주의: 한 번 만든 태그 정의는 이 도구(및 Dooray API)로는 수정·삭제할 수 없다 — "
339+ "오타/불필요 태그 정리는 오직 Dooray 웹 UI에서 수동으로만 가능하니, 생성 전 name·color 를 신중히 확정할 것."
340+ ),
341+ tags = tag ,
342+ )
343+ async def dooray_add_tag (project_id : str , name : str , color : str = "ffffff" ) -> dict :
344+ return await _proxy (
345+ method = "POST" ,
346+ dooray_path = f"/project/v1/projects/{ project_id } /tags" ,
347+ data = {"name" : name , "color" : color },
348+ )
349+
277350 @mcp .tool (
278351 title = "Dooray 업무 댓글 작성" ,
279352 description = "⚠️ Dooray 에 즉시 반영(담당자에게 알림). 업무에 댓글(마크다운)을 작성." ,
0 commit comments