diff --git a/app/modules/project/db.py b/app/modules/project/db.py index 97f86ab..06e7df2 100644 --- a/app/modules/project/db.py +++ b/app/modules/project/db.py @@ -548,6 +548,15 @@ class ProjectStore: newly_completed = True elif not done and prev.get("completed_at"): sets.append("completed_at = NULL") + # 직접 완료 토글 — 하위 업무는 세션(칸반 컬럼)이 없어 위 자동 세팅이 + # 적용되지 않는다. "stage_id" 를 같이 보냈으면 그쪽 판정이 우선한다. + elif "completed" in fields: + if fields["completed"] and not prev.get("completed_at"): + sets.append("completed_at = %s") + params.append(now_kst()) + newly_completed = True + elif not fields["completed"] and prev.get("completed_at"): + sets.append("completed_at = NULL") if not sets: return {**prev, "_newly_assigned": None, "_newly_completed": False} diff --git a/app/modules/project/router.py b/app/modules/project/router.py index d593e38..2d37f8d 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -506,6 +506,12 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse: tasks if p["id"] == project_id else st.list_tasks(project_id=p["id"], limit=2000) ) + # 이 업무가 '어느 프로젝트를 보다가' 담겼는지 표시 — 멀티호밍된 업무는 + # 원 프로젝트(t.project_id)와 연결된 프로젝트(p.id) 양쪽에서 각각 한 번씩 + # 나온다(각 프로젝트 화면에 보여야 하므로). "전체 프로젝트" 집계 화면은 + # context_project_id == project_id(원 소속)인 것만 세어 중복을 없앤다. + for t in p_tasks: + t["context_project_id"] = p["id"] all_tasks.extend(p_tasks) stages_by_project[p["id"]] = st.list_stages(project_id=p["id"]) projects_meta.append({ @@ -989,14 +995,18 @@ async def api_add_task_link( ) -> JSONResponse: """업무를 다른 프로젝트(의 한 세션)에도 연결한다(아사나의 멀티호밍). - 원래 소속(프로젝트)과 연결할 대상 프로젝트 양쪽에 대한 관리 권한이 필요하다. + 최초 연결은 원래 소속(프로젝트)과 연결할 대상 프로젝트 양쪽에 대한 관리 + 권한이 필요하다(안 그러면 접근 권한 없는 프로젝트의 업무를 아무나 자기 + 프로젝트로 끌어와 제목 등을 노출시킬 수 있다). 이미 연결된 업무를 그 + 프로젝트 안에서 다른 세션으로 옮기는 것(보드 드래그)은 대상 프로젝트 + 권한만 있으면 된다 — 매번 원 프로젝트 권한까지 다시 확인하면 원 + 프로젝트 멤버가 아닌 사람은 이미 허용된 카드조차 못 옮기게 된다. """ user = _require_user(request) st = _db_or_503(request) task = st.get_task(task_id=task_id) if task is None: raise HTTPException(status_code=404, detail="업무를 찾을 수 없습니다.") - _require_manage(request, st, user, task["project_id"]) try: target_project_id = int(payload.get("project_id")) except (TypeError, ValueError): @@ -1004,6 +1014,11 @@ async def api_add_task_link( if target_project_id == task["project_id"]: raise HTTPException(status_code=400, detail="이미 이 업무의 기본 프로젝트입니다.") _require_manage(request, st, user, target_project_id) + already_linked = any( + link["project_id"] == target_project_id for link in st.list_task_links(task_id=task_id) + ) + if not already_linked: + _require_manage(request, st, user, task["project_id"]) stage_id = payload.get("stage_id") link = st.add_task_link( task_id=task_id, project_id=target_project_id, @@ -1184,6 +1199,27 @@ async def api_download_attachment(request: Request, attachment_id: int): ) +@router.get("/api/attachments/{attachment_id}/inline") +async def api_inline_attachment(request: Request, attachment_id: int): + """설명란에 붙여넣은/업로드한 이미지를 `` 로 바로 보여주기 위한 + 서빙. `/download` 는 `filename=` 을 줘서 강제 다운로드(Content-Disposition: + attachment)가 걸리므로 이미지 삽입에는 못 쓴다 — filename 을 안 주면 브라우저가 + 그대로 렌더링한다.""" + user = _require_user(request) + st = _db_or_503(request) + att = st.get_attachment(attachment_id=attachment_id) + if att is None: + raise HTTPException(status_code=404, detail="첨부를 찾을 수 없습니다.") + task = st.get_task(task_id=att["task_id"]) + if task is None: + raise HTTPException(status_code=404, detail="업무를 찾을 수 없습니다.") + _require_manage(request, st, user, task["project_id"]) + path = _attach_dir(request, att["task_id"]) / att["stored_name"] + if not path.exists(): + raise HTTPException(status_code=404, detail="파일이 디스크에 없습니다.") + return FileResponse(str(path), media_type=att.get("content_type") or "application/octet-stream") + + @router.delete("/api/attachments/{attachment_id}") async def api_delete_attachment(request: Request, attachment_id: int) -> JSONResponse: user = _require_user(request) diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index 5b17706..984e000 100644 --- a/app/modules/project/templates/project/inbox.html +++ b/app/modules/project/templates/project/inbox.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -45,5 +45,5 @@ {% endif %} - + {% endblock %} diff --git a/app/modules/project/templates/project/index.html b/app/modules/project/templates/project/index.html index 501d602..2d0b8a1 100644 --- a/app/modules/project/templates/project/index.html +++ b/app/modules/project/templates/project/index.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -244,12 +244,17 @@ - + +
+ + +
+
-
@@ -271,6 +276,29 @@ +
+
+ checklist 하위 업무 +
+ +
+ + +
+
+
attach_file 첨부파일 @@ -321,5 +349,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index b30fee5..4115b74 100644 --- a/app/modules/project/templates/project/project.html +++ b/app/modules/project/templates/project/project.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + @@ -118,6 +118,12 @@
+ +
+ + +
@@ -166,7 +172,7 @@