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)