feat(perm): 일반 구성원도 업체담당자·주의사항·작업 CRUD 허용 (비민감 정보)
All checks were successful
build-and-push / build (push) Successful in 33s
All checks were successful
build-and-push / build (push) Successful in 33s
- 업체 담당자(ClientContact) upsert/delete를 requireAdmin → canSeeProject
- PATCH /projects/{id}/notes: 구성원이 주의사항·계약범위(글/그림)만 편집(화이트리스트)
- 작업(타임라인)은 기존대로 구성원 CRUD(삭제 포함, 백엔드 이미 허용)
- 유지(관리자 전용): 프로젝트 핵심정보(상태/PM/일정/업체) 수정, 작업자 기여도, 계약/정산
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dce6bb215e
commit
7e1bb6606e
@ -335,8 +335,11 @@ func (s *Server) handleListContacts(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, out)
|
writeJSON(w, http.StatusOK, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 업체 담당자는 협업 정보라 프로젝트 구성원이면 누구나 CRUD 가능(관리자 전용 아님).
|
||||||
func (s *Server) handleUpsertContact(w http.ResponseWriter, r *http.Request) {
|
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
|
return
|
||||||
}
|
}
|
||||||
var c models.ClientContact
|
var c models.ClientContact
|
||||||
@ -344,7 +347,7 @@ func (s *Server) handleUpsertContact(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.ProjectID = chi.URLParam(r, "id")
|
c.ProjectID = id
|
||||||
if c.ID != "" {
|
if c.ID != "" {
|
||||||
s.db.Save(&c)
|
s.db.Save(&c)
|
||||||
} else {
|
} 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) {
|
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
|
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})
|
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) ----------------------------------------------
|
// ---- tasks (gantt / kanban) ----------------------------------------------
|
||||||
|
|
||||||
func (s *Server) handleListTasks(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@ -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.Get("/projects/{id}/contacts", s.handleListContacts)
|
||||||
r.Post("/projects/{id}/contacts", s.handleUpsertContact)
|
r.Post("/projects/{id}/contacts", s.handleUpsertContact)
|
||||||
r.Delete("/contacts/{cId}", s.handleDeleteContact)
|
r.Delete("/contacts/{cId}", s.handleDeleteContact)
|
||||||
|
r.Patch("/projects/{id}/notes", s.handlePatchProjectNotes)
|
||||||
r.Get("/projects/{id}/tasks", s.handleListTasks)
|
r.Get("/projects/{id}/tasks", s.handleListTasks)
|
||||||
r.Post("/projects/{id}/tasks", s.handleCreateTask)
|
r.Post("/projects/{id}/tasks", s.handleCreateTask)
|
||||||
r.Patch("/tasks/{tId}", s.handlePatchTask)
|
r.Patch("/tasks/{tId}", s.handlePatchTask)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user