feat: 회사/제품/버전 PATCH·DELETE, 세금 DELETE, 기준정보 nav
All checks were successful
build-and-push / build (push) Successful in 32s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
theorose49 2026-06-28 11:45:43 +09:00
parent db0f857f0a
commit dcf8b415db
4 changed files with 70 additions and 0 deletions

View File

@ -45,6 +45,7 @@ var navItems = []NavItem{
{Key: "approvals", Label: "승인 관리", Path: "/admin/approvals", Icon: "CheckSquare", AdminOnly: true, Section: "관리자"}, {Key: "approvals", Label: "승인 관리", Path: "/admin/approvals", Icon: "CheckSquare", AdminOnly: true, Section: "관리자"},
{Key: "attendance-admin", Label: "근무 관리", Path: "/admin/attendance", Icon: "ClipboardList", AdminOnly: true, Section: "관리자"}, {Key: "attendance-admin", Label: "근무 관리", Path: "/admin/attendance", Icon: "ClipboardList", AdminOnly: true, Section: "관리자"},
{Key: "projects-admin", Label: "프로젝트 관리", Path: "/admin/projects", Icon: "FolderCog", AdminOnly: true, Section: "관리자"}, {Key: "projects-admin", Label: "프로젝트 관리", Path: "/admin/projects", Icon: "FolderCog", AdminOnly: true, Section: "관리자"},
{Key: "master", Label: "기준정보", Path: "/admin/master", Icon: "Database", AdminOnly: true, Section: "관리자"},
{Key: "incentive-admin", Label: "인센티브 관리", Path: "/admin/incentive", Icon: "Calculator", AdminOnly: true, Section: "관리자"}, {Key: "incentive-admin", Label: "인센티브 관리", Path: "/admin/incentive", Icon: "Calculator", AdminOnly: true, Section: "관리자"},
{Key: "accounting", Label: "회계", Path: "/admin/accounting", Icon: "Wallet", AdminOnly: true, Section: "관리자"}, {Key: "accounting", Label: "회계", Path: "/admin/accounting", Icon: "Wallet", AdminOnly: true, Section: "관리자"},
{Key: "members", Label: "구성원", Path: "/admin/members", Icon: "Users", AdminOnly: true, Section: "관리자"}, {Key: "members", Label: "구성원", Path: "/admin/members", Icon: "Users", AdminOnly: true, Section: "관리자"},

View File

@ -141,6 +141,14 @@ func (s *Server) handlePatchTax(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, t) writeJSON(w, http.StatusOK, t)
} }
func (s *Server) handleDeleteTax(w http.ResponseWriter, r *http.Request) {
if !s.requireAdmin(w, r) {
return
}
s.db.Delete(&models.TaxRecord{}, "id = ?", chi.URLParam(r, "taxId"))
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
// ---- summary: cashflow vs incentive gap ----------------------------------- // ---- summary: cashflow vs incentive gap -----------------------------------
type acctSummary struct { type acctSummary struct {

View File

@ -78,6 +78,60 @@ func (s *Server) handleCreateVersion(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, v) writeJSON(w, http.StatusCreated, v)
} }
// patchByID applies a whitelisted-free JSON patch to a model row (admin only).
func (s *Server) patchModel(w http.ResponseWriter, r *http.Request, dest interface{}, id string) {
if !s.requireAdmin(w, r) {
return
}
if err := s.db.First(dest, "id = ?", id).Error; err != nil {
writeError(w, http.StatusNotFound, "찾을 수 없습니다")
return
}
var patch map[string]interface{}
if err := decodeJSON(r, &patch); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
delete(patch, "id")
if err := s.db.Model(dest).Updates(patch).Error; err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.db.First(dest, "id = ?", id)
writeJSON(w, http.StatusOK, dest)
}
func (s *Server) handlePatchCompany(w http.ResponseWriter, r *http.Request) {
s.patchModel(w, r, &models.Company{}, chi.URLParam(r, "id"))
}
func (s *Server) handleDeleteCompany(w http.ResponseWriter, r *http.Request) {
if !s.requireAdmin(w, r) {
return
}
s.db.Delete(&models.Company{}, "id = ?", chi.URLParam(r, "id"))
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
func (s *Server) handlePatchProduct(w http.ResponseWriter, r *http.Request) {
s.patchModel(w, r, &models.Product{}, chi.URLParam(r, "id"))
}
func (s *Server) handleDeleteProduct(w http.ResponseWriter, r *http.Request) {
if !s.requireAdmin(w, r) {
return
}
s.db.Delete(&models.Product{}, "id = ?", chi.URLParam(r, "id"))
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
func (s *Server) handlePatchVersion(w http.ResponseWriter, r *http.Request) {
s.patchModel(w, r, &models.Version{}, chi.URLParam(r, "id"))
}
func (s *Server) handleDeleteVersion(w http.ResponseWriter, r *http.Request) {
if !s.requireAdmin(w, r) {
return
}
s.db.Delete(&models.Version{}, "id = ?", chi.URLParam(r, "id"))
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
// ---- projects ------------------------------------------------------------- // ---- projects -------------------------------------------------------------
// myProjectIDs returns the project IDs the caller is a member of (or PM of). // myProjectIDs returns the project IDs the caller is a member of (or PM of).

View File

@ -98,10 +98,16 @@ func NewRouter(db *gorm.DB, store *storage.Storage, cfg config.Config, pusher *p
// ---- slice 3: projects ---- // ---- slice 3: projects ----
r.Get("/companies", s.handleListCompanies) r.Get("/companies", s.handleListCompanies)
r.Post("/companies", s.handleCreateCompany) r.Post("/companies", s.handleCreateCompany)
r.Patch("/companies/{id}", s.handlePatchCompany)
r.Delete("/companies/{id}", s.handleDeleteCompany)
r.Get("/products", s.handleListProducts) r.Get("/products", s.handleListProducts)
r.Post("/products", s.handleCreateProduct) r.Post("/products", s.handleCreateProduct)
r.Patch("/products/{id}", s.handlePatchProduct)
r.Delete("/products/{id}", s.handleDeleteProduct)
r.Get("/versions", s.handleListVersions) r.Get("/versions", s.handleListVersions)
r.Post("/versions", s.handleCreateVersion) r.Post("/versions", s.handleCreateVersion)
r.Patch("/versions/{id}", s.handlePatchVersion)
r.Delete("/versions/{id}", s.handleDeleteVersion)
r.Get("/projects", s.handleListProjects) r.Get("/projects", s.handleListProjects)
r.Post("/projects", s.handleCreateProject) r.Post("/projects", s.handleCreateProject)
r.Get("/projects/{id}", s.handleGetProject) r.Get("/projects/{id}", s.handleGetProject)
@ -153,6 +159,7 @@ func NewRouter(db *gorm.DB, store *storage.Storage, cfg config.Config, pusher *p
r.Get("/taxes", s.handleListTaxes) r.Get("/taxes", s.handleListTaxes)
r.Post("/taxes", s.handleCreateTax) r.Post("/taxes", s.handleCreateTax)
r.Patch("/taxes/{taxId}", s.handlePatchTax) r.Patch("/taxes/{taxId}", s.handlePatchTax)
r.Delete("/taxes/{taxId}", s.handleDeleteTax)
r.Get("/accounting/summary", s.handleAccountingSummary) // cashflow vs incentive gap r.Get("/accounting/summary", s.handleAccountingSummary) // cashflow vs incentive gap
// ---- slice 6: dashboard ---- // ---- slice 6: dashboard ----