feat(notify): 작업 배정·댓글·PM 지정 시 대상 유저에게 쪽지함 알림(+모바일 푸시)
All checks were successful
build-and-push / build (push) Successful in 32s
All checks were successful
build-and-push / build (push) Successful in 32s
- 작업 생성/수정으로 담당자 배정 시 그 구성원에게 알림(type=task), 본인배정 제외 - 내 작업에 새 댓글 달리면 담당자에게 알림(본인 댓글 제외) - 프로젝트 PM 지정/변경 시 PM에게 알림 - 모두 기존 s.notify() 사용 → Notification 생성 + 등록기기로 FCM 푸시(자격증명 시 자동) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c865baccd2
commit
dce6bb215e
@ -212,6 +212,9 @@ func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
s.db.Create(&p)
|
s.db.Create(&p)
|
||||||
s.audit(r, "create", "project", p.ID, p.Name)
|
s.audit(r, "create", "project", p.ID, p.Name)
|
||||||
|
if p.PMEmail != "" && !s.owns(r, p.PMEmail) {
|
||||||
|
s.notify(p.PMEmail, "project", "프로젝트 PM으로 지정되었습니다", p.Name, "/projects/"+p.ID)
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusCreated, p)
|
writeJSON(w, http.StatusCreated, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,6 +227,7 @@ func (s *Server) handlePatchProject(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusNotFound, "프로젝트를 찾을 수 없습니다")
|
writeError(w, http.StatusNotFound, "프로젝트를 찾을 수 없습니다")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
oldPM := p.PMEmail
|
||||||
var patch map[string]interface{}
|
var patch map[string]interface{}
|
||||||
if err := decodeJSON(r, &patch); err != nil {
|
if err := decodeJSON(r, &patch); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
@ -238,6 +242,9 @@ func (s *Server) handlePatchProject(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.db.First(&p, "id = ?", p.ID)
|
s.db.First(&p, "id = ?", p.ID)
|
||||||
|
if p.PMEmail != "" && !strings.EqualFold(p.PMEmail, oldPM) && !s.owns(r, p.PMEmail) {
|
||||||
|
s.notify(p.PMEmail, "project", "프로젝트 PM으로 지정되었습니다", p.Name, "/projects/"+p.ID)
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, p)
|
writeJSON(w, http.StatusOK, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,9 +390,21 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
t.Lane = "todo"
|
t.Lane = "todo"
|
||||||
}
|
}
|
||||||
s.db.Create(&t)
|
s.db.Create(&t)
|
||||||
|
// 담당자로 배정되면 본인에게 알림(+모바일 푸시). 본인이 본인에게 단 경우는 제외.
|
||||||
|
if t.Assignee != "" && !s.owns(r, t.Assignee) {
|
||||||
|
s.notify(t.Assignee, "task", "작업에 배정되었습니다",
|
||||||
|
fmt.Sprintf("[%s] %s", s.projectName(id), t.Title), "/projects/"+id)
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusCreated, t)
|
writeJSON(w, http.StatusCreated, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// projectName returns a project's name (best-effort) for notification text.
|
||||||
|
func (s *Server) projectName(id string) string {
|
||||||
|
var p models.Project
|
||||||
|
s.db.Select("name").First(&p, "id = ?", id)
|
||||||
|
return p.Name
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handlePatchTask(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handlePatchTask(w http.ResponseWriter, r *http.Request) {
|
||||||
var t models.ProjectTask
|
var t models.ProjectTask
|
||||||
if err := s.db.First(&t, "id = ?", chi.URLParam(r, "tId")).Error; err != nil {
|
if err := s.db.First(&t, "id = ?", chi.URLParam(r, "tId")).Error; err != nil {
|
||||||
@ -396,6 +415,7 @@ func (s *Server) handlePatchTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusForbidden, "권한이 없습니다")
|
writeError(w, http.StatusForbidden, "권한이 없습니다")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
oldAssignee := t.Assignee
|
||||||
var patch map[string]interface{}
|
var patch map[string]interface{}
|
||||||
if err := decodeJSON(r, &patch); err != nil {
|
if err := decodeJSON(r, &patch); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
@ -414,6 +434,11 @@ func (s *Server) handlePatchTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
patch = snakeKeys(patch)
|
patch = snakeKeys(patch)
|
||||||
s.db.Model(&t).Updates(patch)
|
s.db.Model(&t).Updates(patch)
|
||||||
s.db.First(&t, "id = ?", t.ID)
|
s.db.First(&t, "id = ?", t.ID)
|
||||||
|
// 담당자가 새 사람으로 바뀌면 그 사람에게 알림(+푸시).
|
||||||
|
if t.Assignee != "" && !strings.EqualFold(t.Assignee, oldAssignee) && !s.owns(r, t.Assignee) {
|
||||||
|
s.notify(t.Assignee, "task", "작업에 배정되었습니다",
|
||||||
|
fmt.Sprintf("[%s] %s", s.projectName(t.ProjectID), t.Title), "/projects/"+t.ProjectID)
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, t)
|
writeJSON(w, http.StatusOK, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,6 +484,18 @@ func (s *Server) handleCreateTaskComment(w http.ResponseWriter, r *http.Request)
|
|||||||
c.TaskID = tid
|
c.TaskID = tid
|
||||||
c.AuthorEmail = s.email(r)
|
c.AuthorEmail = s.email(r)
|
||||||
s.db.Create(&c)
|
s.db.Create(&c)
|
||||||
|
// 댓글이 달리면 작업 담당자에게 알림(본인 댓글 제외).
|
||||||
|
var t models.ProjectTask
|
||||||
|
if s.db.Select("assignee", "title", "project_id").First(&t, "id = ?", tid).Error == nil {
|
||||||
|
if t.Assignee != "" && !s.owns(r, t.Assignee) {
|
||||||
|
body := c.Body
|
||||||
|
if len(body) > 50 {
|
||||||
|
body = body[:50] + "…"
|
||||||
|
}
|
||||||
|
s.notify(t.Assignee, "task", "내 작업에 새 댓글",
|
||||||
|
fmt.Sprintf("%s — %s", t.Title, body), "/projects/"+t.ProjectID)
|
||||||
|
}
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusCreated, c)
|
writeJSON(w, http.StatusCreated, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user