From b196f85bb3471f81220e8e1f5dac347cfe01d439 Mon Sep 17 00:00:00 2001 From: theorose49 Date: Thu, 2 Jul 2026 09:54:23 +0900 Subject: [PATCH] =?UTF-8?q?fix(project):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=82=AD=EC=A0=9C=20=EC=BA=90=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20+=20=EC=83=9D=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B3=B5=EB=B0=B1=20=EB=B3=B4=EA=B0=95=20(?= =?UTF-8?q?=EC=8B=AC=ED=99=94=20QA=20=EB=B0=9C=EA=B2=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handleDeleteProject: 작업·댓글·작업자·담당자·계약·첨부·분할입금·메일메모/메시지/상태까지 함께 삭제(고아 레코드 방지) - 서버측 필수값 검증 추가(프론트뿐 아니라 API도): · 작업 제목, 담당자 이름, 구성원 이메일 필수 → 400 · 작업자 기여도 0~100 범위 → 400 · 일정 제목·시작일 필수, 종료<시작 거부 → 400 · 휴가 종류·시작일 필수, 종료<시작 거부 → 400 Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/httpapi/handlers_attendance.go | 8 +++++++ internal/httpapi/handlers_calendar.go | 9 +++++++ internal/httpapi/handlers_members.go | 5 ++++ internal/httpapi/handlers_projects.go | 32 ++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/internal/httpapi/handlers_attendance.go b/internal/httpapi/handlers_attendance.go index b3fa5d9..255029a 100644 --- a/internal/httpapi/handlers_attendance.go +++ b/internal/httpapi/handlers_attendance.go @@ -145,6 +145,14 @@ func (s *Server) handleCreateLeave(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, err.Error()) return } + if lv.Type == "" || lv.StartDate == "" { + writeError(w, http.StatusBadRequest, "휴가 종류와 시작일이 필요합니다") + return + } + if lv.EndDate != "" && lv.EndDate < lv.StartDate { + writeError(w, http.StatusBadRequest, "종료일이 시작일보다 빠릅니다") + return + } lv.MemberEmail = s.email(r) // members can only file for themselves lv.Status = models.StatusPending lv.Approver = "" diff --git a/internal/httpapi/handlers_calendar.go b/internal/httpapi/handlers_calendar.go index ec8e72e..e978bd7 100644 --- a/internal/httpapi/handlers_calendar.go +++ b/internal/httpapi/handlers_calendar.go @@ -3,6 +3,7 @@ package httpapi import ( "encoding/json" "net/http" + "strings" "spin/internal/models" @@ -26,6 +27,14 @@ func (s *Server) handleCreateEvent(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, err.Error()) return } + if strings.TrimSpace(e.Title) == "" || strings.TrimSpace(e.Start) == "" { + writeError(w, http.StatusBadRequest, "제목과 시작일이 필요합니다") + return + } + if e.End != "" && e.End < e.Start { + writeError(w, http.StatusBadRequest, "종료일이 시작일보다 빠릅니다") + return + } e.ID = "" e.OwnerEmail = s.email(r) s.db.Create(&e) diff --git a/internal/httpapi/handlers_members.go b/internal/httpapi/handlers_members.go index 5a38376..3940c43 100644 --- a/internal/httpapi/handlers_members.go +++ b/internal/httpapi/handlers_members.go @@ -2,6 +2,7 @@ package httpapi import ( "net/http" + "strings" "spin/internal/models" @@ -63,6 +64,10 @@ func (s *Server) handleCreateMember(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, err.Error()) return } + if strings.TrimSpace(m.Email) == "" { + writeError(w, http.StatusBadRequest, "이메일이 필요합니다") + return + } if m.Role == "" { m.Role = models.RoleMember } diff --git a/internal/httpapi/handlers_projects.go b/internal/httpapi/handlers_projects.go index 97ca7e2..90d26f9 100644 --- a/internal/httpapi/handlers_projects.go +++ b/internal/httpapi/handlers_projects.go @@ -276,7 +276,21 @@ func (s *Server) handleDeleteProject(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return } - s.db.Delete(&models.Project{}, "id = ?", chi.URLParam(r, "id")) + id := chi.URLParam(r, "id") + // 자식 데이터까지 함께 삭제(고아 레코드 방지). task_comments는 task_id 기준이라 별도. + var taskIDs []string + s.db.Model(&models.ProjectTask{}).Where("project_id = ?", id).Pluck("id", &taskIDs) + if len(taskIDs) > 0 { + s.db.Where("task_id IN ?", taskIDs).Delete(&models.TaskComment{}) + } + for _, m := range []interface{}{ + &models.ProjectTask{}, &models.ProjectMember{}, &models.ClientContact{}, + &models.Contract{}, &models.ContractFile{}, &models.PaymentSplit{}, + &models.MailNote{}, &models.ProjectMailMsg{}, &models.ProjectMailState{}, + } { + s.db.Where("project_id = ?", id).Delete(m) + } + s.db.Delete(&models.Project{}, "id = ?", id) writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) } @@ -302,6 +316,14 @@ func (s *Server) handleUpsertProjectMember(w http.ResponseWriter, r *http.Reques writeError(w, http.StatusBadRequest, err.Error()) return } + if strings.TrimSpace(pm.MemberEmail) == "" { + writeError(w, http.StatusBadRequest, "작업자를 선택하세요") + return + } + if pm.Portion < 0 || pm.Portion > 100 { + writeError(w, http.StatusBadRequest, "기여도는 0~100 사이여야 합니다") + return + } pm.ProjectID = chi.URLParam(r, "id") if pm.ID != "" { s.db.Save(&pm) @@ -349,6 +371,10 @@ func (s *Server) handleUpsertContact(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, err.Error()) return } + if strings.TrimSpace(c.Name) == "" { + writeError(w, http.StatusBadRequest, "담당자 이름을 입력하세요") + return + } c.ProjectID = id if c.ID != "" { s.db.Save(&c) @@ -436,6 +462,10 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, err.Error()) return } + if strings.TrimSpace(t.Title) == "" { + writeError(w, http.StatusBadRequest, "작업명을 입력하세요") + return + } t.ProjectID = id if t.Lane == "" { t.Lane = "todo"