All checks were successful
build-and-push / build (push) Successful in 32s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
192 lines
7.6 KiB
Go
192 lines
7.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"spin/internal/config"
|
|
"spin/internal/push"
|
|
"spin/internal/storage"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Server bundles dependencies shared by all handlers.
|
|
type Server struct {
|
|
db *gorm.DB
|
|
store *storage.Storage
|
|
cfg config.Config
|
|
push *push.Sender
|
|
}
|
|
|
|
// NewRouter wires up the chi router and mounts the /api routes for every module.
|
|
func NewRouter(db *gorm.DB, store *storage.Storage, cfg config.Config, pusher *push.Sender) http.Handler {
|
|
s := &Server{db: db, store: store, cfg: cfg, push: pusher}
|
|
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.RealIP)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
r.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowedMethods: []string{"GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"*"},
|
|
ExposedHeaders: []string{"*"},
|
|
AllowCredentials: false,
|
|
MaxAge: 300,
|
|
}))
|
|
|
|
r.Route("/api", func(r chi.Router) {
|
|
// Public health for k8s probes.
|
|
r.Get("/health", s.handleHealth)
|
|
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(authMiddleware(cfg.DevAuth, cfg.AdminGroups))
|
|
// auto-provision a Member on first login (Keycloak), then continue.
|
|
r.Use(s.ensureMember)
|
|
|
|
// identity / navigation
|
|
r.Get("/me", s.handleMe)
|
|
r.Get("/me/nav", s.handleNav)
|
|
r.Post("/me/avatar", s.handleUploadAvatar)
|
|
|
|
// push device registration (spin-mobile)
|
|
r.Post("/devices", s.handleRegisterDevice)
|
|
r.Post("/devices/unregister", s.handleUnregisterDevice)
|
|
|
|
// inbox / notifications (메일함)
|
|
r.Get("/notifications", s.handleListNotifications)
|
|
r.Get("/notifications/unread-count", s.handleUnreadCount)
|
|
r.Post("/notifications/{id}/read", s.handleMarkRead)
|
|
r.Post("/notifications/read-all", s.handleMarkAllRead)
|
|
|
|
// ---- slice 1: members / org ----
|
|
r.Get("/members", s.handleListMembers)
|
|
r.Post("/members", s.handleCreateMember)
|
|
r.Get("/members/{id}", s.handleGetMember)
|
|
r.Get("/members/{id}/avatar", s.handleGetAvatar)
|
|
r.Patch("/members/{id}", s.handlePatchMember)
|
|
r.Delete("/members/{id}", s.handleDeleteMember)
|
|
r.Get("/departments", s.handleListDepartments)
|
|
r.Post("/departments", s.handleCreateDepartment)
|
|
r.Patch("/departments/{id}", s.handlePatchDepartment)
|
|
r.Delete("/departments/{id}", s.handleDeleteDepartment)
|
|
r.Get("/audit", s.handleListAudit)
|
|
|
|
// ---- slice 2: attendance / leave ----
|
|
r.Get("/attendance", s.handleListAttendance) // own (admin: ?email= or all)
|
|
r.Post("/attendance/punch", s.handlePunch) // clock in/out
|
|
r.Post("/attendance/status", s.handleSetWorkStatus) // 출근/퇴근/휴식/미팅/이동
|
|
r.Get("/attendance/status", s.handleListWorkStatus) // presence log (admin: all/?email=)
|
|
r.Get("/attendance/timesheet", s.handleTimesheet) // monthly roll-up (admin mgmt)
|
|
r.Get("/leave", s.handleListLeave)
|
|
r.Get("/leave/balance", s.handleLeaveBalance) // 남은 연차(소수점)
|
|
r.Post("/leave", s.handleCreateLeave)
|
|
r.Post("/leave/{id}/decide", s.handleDecideLeave) // admin approve/reject
|
|
r.Post("/leave/{id}/cancel", s.handleCancelLeave)
|
|
r.Get("/overtime", s.handleListOvertime)
|
|
r.Post("/overtime", s.handleCreateOvertime)
|
|
r.Post("/overtime/{id}/decide", s.handleDecideOvertime)
|
|
r.Get("/work-policy", s.handleGetWorkPolicy)
|
|
r.Put("/work-policy", s.handlePutWorkPolicy)
|
|
r.Get("/approvals", s.handleApprovalQueue) // admin queue
|
|
|
|
// ---- slice 3: projects ----
|
|
r.Get("/companies", s.handleListCompanies)
|
|
r.Post("/companies", s.handleCreateCompany)
|
|
r.Patch("/companies/{id}", s.handlePatchCompany)
|
|
r.Delete("/companies/{id}", s.handleDeleteCompany)
|
|
r.Get("/products", s.handleListProducts)
|
|
r.Post("/products", s.handleCreateProduct)
|
|
r.Patch("/products/{id}", s.handlePatchProduct)
|
|
r.Delete("/products/{id}", s.handleDeleteProduct)
|
|
r.Get("/versions", s.handleListVersions)
|
|
r.Post("/versions", s.handleCreateVersion)
|
|
r.Patch("/versions/{id}", s.handlePatchVersion)
|
|
r.Delete("/versions/{id}", s.handleDeleteVersion)
|
|
r.Get("/projects", s.handleListProjects)
|
|
r.Post("/projects", s.handleCreateProject)
|
|
r.Get("/projects/{id}", s.handleGetProject)
|
|
r.Patch("/projects/{id}", s.handlePatchProject)
|
|
r.Delete("/projects/{id}", s.handleDeleteProject)
|
|
r.Get("/projects/{id}/members", s.handleListProjectMembers)
|
|
r.Post("/projects/{id}/members", s.handleUpsertProjectMember)
|
|
r.Delete("/project-members/{pmId}", s.handleDeleteProjectMember)
|
|
r.Get("/projects/{id}/contacts", s.handleListContacts)
|
|
r.Post("/projects/{id}/contacts", s.handleUpsertContact)
|
|
r.Delete("/contacts/{cId}", s.handleDeleteContact)
|
|
r.Get("/projects/{id}/tasks", s.handleListTasks)
|
|
r.Post("/projects/{id}/tasks", s.handleCreateTask)
|
|
r.Patch("/tasks/{tId}", s.handlePatchTask)
|
|
r.Delete("/tasks/{tId}", s.handleDeleteTask)
|
|
// admin-only commercial block
|
|
r.Get("/projects/{id}/contract", s.handleGetContract)
|
|
r.Put("/projects/{id}/contract", s.handlePutContract)
|
|
r.Get("/projects/{id}/files", s.handleListContractFiles)
|
|
r.Post("/projects/{id}/files", s.handleUploadContractFile)
|
|
r.Get("/files/{fId}/download", s.handleDownloadContractFile)
|
|
r.Delete("/files/{fId}", s.handleDeleteContractFile)
|
|
r.Get("/projects/{id}/payments", s.handleListPayments)
|
|
r.Post("/projects/{id}/payments", s.handleCreatePayment)
|
|
r.Patch("/payments/{payId}", s.handlePatchPayment)
|
|
r.Delete("/payments/{payId}", s.handleDeletePayment)
|
|
|
|
// ---- slice 4: incentive ----
|
|
r.Get("/incentive/config", s.handleGetIncentiveConfig)
|
|
r.Put("/incentive/config", s.handlePutIncentiveConfig)
|
|
r.Get("/incentive/me", s.handleMyIncentive) // member dashboard
|
|
r.Get("/incentive/stages", s.handleListStages) // ?projectId=
|
|
r.Post("/incentive/projects/{id}/recompute", s.handleRecomputeProject)
|
|
r.Post("/incentive/stages/{stId}/status", s.handleSetStageStatus)
|
|
r.Get("/incentive/user-incentives", s.handleListUserIncentives)
|
|
r.Patch("/incentive/user-incentives/{uiId}", s.handlePatchUserIncentive)
|
|
r.Get("/incentive/settlements", s.handleListSettlements)
|
|
r.Post("/incentive/settlements/run", s.handleRunSettlement)
|
|
r.Post("/incentive/settlements/{sId}/fix", s.handleFixSettlement)
|
|
r.Post("/incentive/simulate", s.handleSimulate)
|
|
|
|
// ---- slice 5: accounting ----
|
|
r.Get("/accounts", s.handleListAccounts)
|
|
r.Post("/accounts", s.handleCreateAccount)
|
|
r.Get("/transactions", s.handleListTransactions)
|
|
r.Post("/transactions", s.handleCreateTransaction)
|
|
r.Patch("/transactions/{txId}", s.handlePatchTransaction)
|
|
r.Delete("/transactions/{txId}", s.handleDeleteTransaction)
|
|
r.Get("/taxes", s.handleListTaxes)
|
|
r.Post("/taxes", s.handleCreateTax)
|
|
r.Patch("/taxes/{taxId}", s.handlePatchTax)
|
|
r.Delete("/taxes/{taxId}", s.handleDeleteTax)
|
|
r.Get("/accounting/summary", s.handleAccountingSummary) // cashflow vs incentive gap
|
|
|
|
// ---- slice 6: dashboard ----
|
|
r.Get("/dashboard", s.handleDashboard)
|
|
})
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
// --- shared helpers ---------------------------------------------------------
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|
|
|
|
func decodeJSON(r *http.Request, v interface{}) error {
|
|
return json.NewDecoder(r.Body).Decode(v)
|
|
}
|