From dce6bb215e28f14e99b6dff70537bcfda8c756e9 Mon Sep 17 00:00:00 2001 From: theorose49 Date: Tue, 30 Jun 2026 13:19:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(notify):=20=EC=9E=91=EC=97=85=20=EB=B0=B0?= =?UTF-8?q?=EC=A0=95=C2=B7=EB=8C=93=EA=B8=80=C2=B7PM=20=EC=A7=80=EC=A0=95?= =?UTF-8?q?=20=EC=8B=9C=20=EB=8C=80=EC=83=81=20=EC=9C=A0=EC=A0=80=EC=97=90?= =?UTF-8?q?=EA=B2=8C=20=EC=AA=BD=EC=A7=80=ED=95=A8=20=EC=95=8C=EB=A6=BC(+?= =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20=ED=91=B8=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 작업 생성/수정으로 담당자 배정 시 그 구성원에게 알림(type=task), 본인배정 제외 - 내 작업에 새 댓글 달리면 담당자에게 알림(본인 댓글 제외) - 프로젝트 PM 지정/변경 시 PM에게 알림 - 모두 기존 s.notify() 사용 → Notification 생성 + 등록기기로 FCM 푸시(자격증명 시 자동) Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/httpapi/handlers_projects.go | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/httpapi/handlers_projects.go b/internal/httpapi/handlers_projects.go index fbcef63..1d46a0e 100644 --- a/internal/httpapi/handlers_projects.go +++ b/internal/httpapi/handlers_projects.go @@ -212,6 +212,9 @@ func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) { } s.db.Create(&p) 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) } @@ -224,6 +227,7 @@ func (s *Server) handlePatchProject(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "프로젝트를 찾을 수 없습니다") return } + oldPM := p.PMEmail var patch map[string]interface{} if err := decodeJSON(r, &patch); err != nil { writeError(w, http.StatusBadRequest, err.Error()) @@ -238,6 +242,9 @@ func (s *Server) handlePatchProject(w http.ResponseWriter, r *http.Request) { return } 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) } @@ -383,9 +390,21 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) { t.Lane = "todo" } 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) } +// 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) { var t models.ProjectTask 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, "권한이 없습니다") return } + oldAssignee := t.Assignee var patch map[string]interface{} if err := decodeJSON(r, &patch); err != nil { writeError(w, http.StatusBadRequest, err.Error()) @@ -414,6 +434,11 @@ func (s *Server) handlePatchTask(w http.ResponseWriter, r *http.Request) { patch = snakeKeys(patch) s.db.Model(&t).Updates(patch) 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) } @@ -459,6 +484,18 @@ func (s *Server) handleCreateTaskComment(w http.ResponseWriter, r *http.Request) c.TaskID = tid c.AuthorEmail = s.email(r) 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) }