fix(project): 프로젝트 삭제 캐스케이드 + 생성 검증 공백 보강 (심화 QA 발견)
All checks were successful
build-and-push / build (push) Successful in 32s
All checks were successful
build-and-push / build (push) Successful in 32s
- handleDeleteProject: 작업·댓글·작업자·담당자·계약·첨부·분할입금·메일메모/메시지/상태까지 함께 삭제(고아 레코드 방지) - 서버측 필수값 검증 추가(프론트뿐 아니라 API도): · 작업 제목, 담당자 이름, 구성원 이메일 필수 → 400 · 작업자 기여도 0~100 범위 → 400 · 일정 제목·시작일 필수, 종료<시작 거부 → 400 · 휴가 종류·시작일 필수, 종료<시작 거부 → 400 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e32e0f22b
commit
b196f85bb3
@ -145,6 +145,14 @@ func (s *Server) handleCreateLeave(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
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.MemberEmail = s.email(r) // members can only file for themselves
|
||||||
lv.Status = models.StatusPending
|
lv.Status = models.StatusPending
|
||||||
lv.Approver = ""
|
lv.Approver = ""
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package httpapi
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"spin/internal/models"
|
"spin/internal/models"
|
||||||
|
|
||||||
@ -26,6 +27,14 @@ func (s *Server) handleCreateEvent(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
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.ID = ""
|
||||||
e.OwnerEmail = s.email(r)
|
e.OwnerEmail = s.email(r)
|
||||||
s.db.Create(&e)
|
s.db.Create(&e)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package httpapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"spin/internal/models"
|
"spin/internal/models"
|
||||||
|
|
||||||
@ -63,6 +64,10 @@ func (s *Server) handleCreateMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(m.Email) == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "이메일이 필요합니다")
|
||||||
|
return
|
||||||
|
}
|
||||||
if m.Role == "" {
|
if m.Role == "" {
|
||||||
m.Role = models.RoleMember
|
m.Role = models.RoleMember
|
||||||
}
|
}
|
||||||
|
|||||||
@ -276,7 +276,21 @@ func (s *Server) handleDeleteProject(w http.ResponseWriter, r *http.Request) {
|
|||||||
if !s.requireAdmin(w, r) {
|
if !s.requireAdmin(w, r) {
|
||||||
return
|
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})
|
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())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
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")
|
pm.ProjectID = chi.URLParam(r, "id")
|
||||||
if pm.ID != "" {
|
if pm.ID != "" {
|
||||||
s.db.Save(&pm)
|
s.db.Save(&pm)
|
||||||
@ -349,6 +371,10 @@ func (s *Server) handleUpsertContact(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(c.Name) == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "담당자 이름을 입력하세요")
|
||||||
|
return
|
||||||
|
}
|
||||||
c.ProjectID = id
|
c.ProjectID = id
|
||||||
if c.ID != "" {
|
if c.ID != "" {
|
||||||
s.db.Save(&c)
|
s.db.Save(&c)
|
||||||
@ -436,6 +462,10 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(t.Title) == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "작업명을 입력하세요")
|
||||||
|
return
|
||||||
|
}
|
||||||
t.ProjectID = id
|
t.ProjectID = id
|
||||||
if t.Lane == "" {
|
if t.Lane == "" {
|
||||||
t.Lane = "todo"
|
t.Lane = "todo"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user