From 7e1bb6606ec9e011c3c7c0a48568a454b5db905e Mon Sep 17 00:00:00 2001 From: theorose49 Date: Tue, 30 Jun 2026 13:50:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(perm):=20=EC=9D=BC=EB=B0=98=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=EC=9B=90=EB=8F=84=20=EC=97=85=EC=B2=B4=EB=8B=B4?= =?UTF-8?q?=EB=8B=B9=EC=9E=90=C2=B7=EC=A3=BC=EC=9D=98=EC=82=AC=ED=95=AD?= =?UTF-8?q?=C2=B7=EC=9E=91=EC=97=85=20CRUD=20=ED=97=88=EC=9A=A9=20(?= =?UTF-8?q?=EB=B9=84=EB=AF=BC=EA=B0=90=20=EC=A0=95=EB=B3=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 업체 담당자(ClientContact) upsert/delete를 requireAdmin → canSeeProject - PATCH /projects/{id}/notes: 구성원이 주의사항·계약범위(글/그림)만 편집(화이트리스트) - 작업(타임라인)은 기존대로 구성원 CRUD(삭제 포함, 백엔드 이미 허용) - 유지(관리자 전용): 프로젝트 핵심정보(상태/PM/일정/업체) 수정, 작업자 기여도, 계약/정산 Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/httpapi/handlers_projects.go | 57 +++++++++++++++++++++++++-- internal/httpapi/router.go | 1 + 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/internal/httpapi/handlers_projects.go b/internal/httpapi/handlers_projects.go index 1d46a0e..5e860fe 100644 --- a/internal/httpapi/handlers_projects.go +++ b/internal/httpapi/handlers_projects.go @@ -335,8 +335,11 @@ func (s *Server) handleListContacts(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, out) } +// 업체 담당자는 협업 정보라 프로젝트 구성원이면 누구나 CRUD 가능(관리자 전용 아님). func (s *Server) handleUpsertContact(w http.ResponseWriter, r *http.Request) { - if !s.requireAdmin(w, r) { + id := chi.URLParam(r, "id") + if !s.canSeeProject(r, id) { + writeError(w, http.StatusForbidden, "권한이 없습니다") return } var c models.ClientContact @@ -344,7 +347,7 @@ func (s *Server) handleUpsertContact(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, err.Error()) return } - c.ProjectID = chi.URLParam(r, "id") + c.ProjectID = id if c.ID != "" { s.db.Save(&c) } else { @@ -354,13 +357,59 @@ func (s *Server) handleUpsertContact(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleDeleteContact(w http.ResponseWriter, r *http.Request) { - if !s.requireAdmin(w, r) { + var c models.ClientContact + if err := s.db.First(&c, "id = ?", chi.URLParam(r, "cId")).Error; err != nil { + writeError(w, http.StatusNotFound, "담당자를 찾을 수 없습니다") return } - s.db.Delete(&models.ClientContact{}, "id = ?", chi.URLParam(r, "cId")) + if !s.canSeeProject(r, c.ProjectID) { + writeError(w, http.StatusForbidden, "권한이 없습니다") + return + } + s.db.Delete(&models.ClientContact{}, "id = ?", c.ID) writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) } +// handlePatchProjectNotes lets any project member edit NON-sensitive descriptive +// fields (주의사항·계약범위 글/그림). 핵심정보(상태·PM·일정·업체)와 계약/정산은 +// 여전히 관리자 전용(handlePatchProject). +func (s *Server) handlePatchProjectNotes(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + if !s.canSeeProject(r, id) { + writeError(w, http.StatusForbidden, "권한이 없습니다") + return + } + var in struct { + Cautions *string `json:"cautions"` + ScopeText *string `json:"scopeText"` + ScopeGraphic *string `json:"scopeGraphic"` + } + if err := decodeJSON(r, &in); err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + upd := map[string]interface{}{} + if in.Cautions != nil { + upd["cautions"] = *in.Cautions + } + if in.ScopeText != nil { + upd["scope_text"] = *in.ScopeText + } + if in.ScopeGraphic != nil { + upd["scope_graphic"] = *in.ScopeGraphic + } + var p models.Project + if err := s.db.First(&p, "id = ?", id).Error; err != nil { + writeError(w, http.StatusNotFound, "프로젝트를 찾을 수 없습니다") + return + } + if len(upd) > 0 { + s.db.Model(&p).Updates(upd) + s.db.First(&p, "id = ?", id) + } + writeJSON(w, http.StatusOK, p) +} + // ---- tasks (gantt / kanban) ---------------------------------------------- func (s *Server) handleListTasks(w http.ResponseWriter, r *http.Request) { diff --git a/internal/httpapi/router.go b/internal/httpapi/router.go index d23e7b6..6ac4ce2 100644 --- a/internal/httpapi/router.go +++ b/internal/httpapi/router.go @@ -122,6 +122,7 @@ func NewRouter(db *gorm.DB, store *storage.Storage, cfg config.Config, pusher *p r.Get("/projects/{id}/contacts", s.handleListContacts) r.Post("/projects/{id}/contacts", s.handleUpsertContact) r.Delete("/contacts/{cId}", s.handleDeleteContact) + r.Patch("/projects/{id}/notes", s.handlePatchProjectNotes) r.Get("/projects/{id}/tasks", s.handleListTasks) r.Post("/projects/{id}/tasks", s.handleCreateTask) r.Patch("/tasks/{tId}", s.handlePatchTask)